@jaggerxtrm/specialists 3.6.10 → 3.6.12
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/benchmarks/executor-benchmark-matrix.json +25 -0
- package/config/skills/using-specialists/SKILL.md +78 -1
- package/config/specialists/debugger.specialist.json +1 -1
- package/config/specialists/executor.specialist.json +3 -3
- package/config/specialists/overthinker.specialist.json +1 -1
- package/config/specialists/planner.specialist.json +1 -1
- package/config/specialists/reviewer.specialist.json +2 -2
- package/config/specialists/sync-docs.specialist.json +2 -2
- package/dist/index.js +1863 -272
- package/package.json +3 -2
- package/config/specialists/.serena/project.yml +0 -151
package/dist/index.js
CHANGED
|
@@ -17907,6 +17907,49 @@ function findTokenUsage(payload) {
|
|
|
17907
17907
|
}
|
|
17908
17908
|
return normalizeTokenUsage(record3);
|
|
17909
17909
|
}
|
|
17910
|
+
function findApiErrorMessage(payload) {
|
|
17911
|
+
if (!payload || typeof payload !== "object")
|
|
17912
|
+
return;
|
|
17913
|
+
const record3 = payload;
|
|
17914
|
+
const direct = [record3.errorMessage, record3.error_message, record3.error, record3.message].find((value) => typeof value === "string" && value.trim().length > 0);
|
|
17915
|
+
if (typeof direct === "string")
|
|
17916
|
+
return direct.trim();
|
|
17917
|
+
const nestedError = record3.error;
|
|
17918
|
+
if (nestedError && typeof nestedError === "object") {
|
|
17919
|
+
const nested = nestedError;
|
|
17920
|
+
const nestedMessage = [nested.message, nested.errorMessage, nested.error_message].find((value) => typeof value === "string" && value.trim().length > 0);
|
|
17921
|
+
if (typeof nestedMessage === "string")
|
|
17922
|
+
return nestedMessage.trim();
|
|
17923
|
+
}
|
|
17924
|
+
const message = record3.assistantMessageEvent;
|
|
17925
|
+
if (message && typeof message === "object") {
|
|
17926
|
+
const nested = message;
|
|
17927
|
+
const nestedMessage = [nested.errorMessage, nested.error_message, nested.error, nested.message].find((value) => typeof value === "string" && value.trim().length > 0);
|
|
17928
|
+
if (typeof nestedMessage === "string")
|
|
17929
|
+
return nestedMessage.trim();
|
|
17930
|
+
}
|
|
17931
|
+
return;
|
|
17932
|
+
}
|
|
17933
|
+
function extractApiErrorFromStderr(stderr) {
|
|
17934
|
+
const compact = stderr.trim();
|
|
17935
|
+
if (!compact)
|
|
17936
|
+
return;
|
|
17937
|
+
const patterns = [
|
|
17938
|
+
/You have hit your ChatGPT usage limit[^\n]*/i,
|
|
17939
|
+
/rate limit[^\n]*/i,
|
|
17940
|
+
/quota[^\n]*/i,
|
|
17941
|
+
/auth(?:entication)?[^\n]*/i,
|
|
17942
|
+
/unauthori[sz]ed[^\n]*/i,
|
|
17943
|
+
/forbidden[^\n]*/i,
|
|
17944
|
+
/overloaded[^\n]*/i
|
|
17945
|
+
];
|
|
17946
|
+
for (const pattern of patterns) {
|
|
17947
|
+
const match = compact.match(pattern);
|
|
17948
|
+
if (match)
|
|
17949
|
+
return match[0].trim();
|
|
17950
|
+
}
|
|
17951
|
+
return;
|
|
17952
|
+
}
|
|
17910
17953
|
function normalizeToolResultPart(contentPart) {
|
|
17911
17954
|
if (!contentPart || typeof contentPart !== "object")
|
|
17912
17955
|
return;
|
|
@@ -18049,6 +18092,7 @@ class PiAgentSession {
|
|
|
18049
18092
|
_pendingRequests = new Map;
|
|
18050
18093
|
_nextRequestId = 1;
|
|
18051
18094
|
_stderrBuffer = "";
|
|
18095
|
+
_apiError;
|
|
18052
18096
|
_stallTimer;
|
|
18053
18097
|
_stallError;
|
|
18054
18098
|
_testWindowToolCallIds = new Set;
|
|
@@ -18147,7 +18191,9 @@ class PiAgentSession {
|
|
|
18147
18191
|
donePromise.catch(() => {});
|
|
18148
18192
|
this._donePromise = donePromise;
|
|
18149
18193
|
this.proc.stderr?.on("data", (chunk) => {
|
|
18150
|
-
|
|
18194
|
+
const text = chunk.toString();
|
|
18195
|
+
this._stderrBuffer += text;
|
|
18196
|
+
this._apiError ??= extractApiErrorFromStderr(this._stderrBuffer) ?? extractApiErrorFromStderr(text);
|
|
18151
18197
|
});
|
|
18152
18198
|
this.proc.stdout?.on("data", (chunk) => {
|
|
18153
18199
|
this._lineBuffer += chunk.toString();
|
|
@@ -18308,6 +18354,12 @@ class PiAgentSession {
|
|
|
18308
18354
|
}
|
|
18309
18355
|
this._updateTokenUsage(findTokenUsage(event), "agent_end");
|
|
18310
18356
|
this._updateFinishReason(findFinishReason(event), "agent_end");
|
|
18357
|
+
const apiError = findApiErrorMessage(event) ?? this._apiError ?? extractApiErrorFromStderr(this._stderrBuffer);
|
|
18358
|
+
if (apiError) {
|
|
18359
|
+
this._apiError = apiError;
|
|
18360
|
+
this._metrics.api_error = apiError;
|
|
18361
|
+
this.options.onMetric?.({ type: "api_error", source: "stderr", errorMessage: apiError });
|
|
18362
|
+
}
|
|
18311
18363
|
this._agentEndReceived = true;
|
|
18312
18364
|
this._clearStallTimer();
|
|
18313
18365
|
this.options.onEvent?.("agent_end");
|
|
@@ -18434,6 +18486,16 @@ class PiAgentSession {
|
|
|
18434
18486
|
this.options.onEvent?.("message_done");
|
|
18435
18487
|
break;
|
|
18436
18488
|
}
|
|
18489
|
+
case "error": {
|
|
18490
|
+
const apiError = findApiErrorMessage(ae) ?? findApiErrorMessage(event);
|
|
18491
|
+
if (apiError) {
|
|
18492
|
+
this._apiError = apiError;
|
|
18493
|
+
this._metrics.api_error = apiError;
|
|
18494
|
+
this.options.onMetric?.({ type: "api_error", source: "rpc", errorMessage: apiError });
|
|
18495
|
+
}
|
|
18496
|
+
this.options.onEvent?.("message_error");
|
|
18497
|
+
break;
|
|
18498
|
+
}
|
|
18437
18499
|
}
|
|
18438
18500
|
}
|
|
18439
18501
|
}
|
|
@@ -18966,7 +19028,7 @@ function resolveCurrentBranch(cwd = process.cwd()) {
|
|
|
18966
19028
|
var init_job_root = () => {};
|
|
18967
19029
|
|
|
18968
19030
|
// src/specialist/observability-sqlite.ts
|
|
18969
|
-
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
19031
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, statSync } from "fs";
|
|
18970
19032
|
import { join as join5 } from "path";
|
|
18971
19033
|
function loadBunDatabase() {
|
|
18972
19034
|
if (_probed)
|
|
@@ -19508,7 +19570,9 @@ function migrateToV10(db) {
|
|
|
19508
19570
|
|
|
19509
19571
|
class SqliteClient {
|
|
19510
19572
|
db;
|
|
19573
|
+
dbPath;
|
|
19511
19574
|
constructor(dbPath) {
|
|
19575
|
+
this.dbPath = dbPath;
|
|
19512
19576
|
const Ctor = loadBunDatabase();
|
|
19513
19577
|
this.db = new Ctor(dbPath);
|
|
19514
19578
|
this.db.run(`PRAGMA busy_timeout=${BUSY_TIMEOUT_MS}`);
|
|
@@ -20081,6 +20145,19 @@ class SqliteClient {
|
|
|
20081
20145
|
return this.db.query("SELECT chain_id, epic_id, chain_root_bead_id, chain_root_job_id, updated_at_ms FROM epic_chain_membership WHERE epic_id = ? ORDER BY updated_at_ms DESC").all(epicId);
|
|
20082
20146
|
}, "listEpicChains");
|
|
20083
20147
|
}
|
|
20148
|
+
deleteEpicChainMembership(epicId, chainIds) {
|
|
20149
|
+
if (chainIds.length === 0)
|
|
20150
|
+
return [];
|
|
20151
|
+
return withRetry(() => {
|
|
20152
|
+
const existing = new Set(this.db.query("SELECT chain_id FROM epic_chain_membership WHERE epic_id = ?").all(epicId).map((row) => row.chain_id));
|
|
20153
|
+
const removable = chainIds.filter((chainId) => existing.has(chainId));
|
|
20154
|
+
if (removable.length === 0)
|
|
20155
|
+
return [];
|
|
20156
|
+
const placeholders = removable.map(() => "?").join(", ");
|
|
20157
|
+
this.db.query(`DELETE FROM epic_chain_membership WHERE epic_id = ? AND chain_id IN (${placeholders})`).run(epicId, ...removable);
|
|
20158
|
+
return removable;
|
|
20159
|
+
}, "deleteEpicChainMembership");
|
|
20160
|
+
}
|
|
20084
20161
|
listEpicChainsWithLatestJob(epicId) {
|
|
20085
20162
|
return withRetry(() => {
|
|
20086
20163
|
const rows = this.db.query(`
|
|
@@ -20331,6 +20408,242 @@ class SqliteClient {
|
|
|
20331
20408
|
transaction();
|
|
20332
20409
|
}, "invalidateMemoriesCache");
|
|
20333
20410
|
}
|
|
20411
|
+
hasActiveJobs(statuses = ["running", "starting"]) {
|
|
20412
|
+
return this.listActiveJobs(statuses).length > 0;
|
|
20413
|
+
}
|
|
20414
|
+
listActiveJobs(statuses = ["running", "starting"]) {
|
|
20415
|
+
return withRetry(() => {
|
|
20416
|
+
if (statuses.length === 0)
|
|
20417
|
+
return [];
|
|
20418
|
+
const placeholders = statuses.map(() => "?").join(", ");
|
|
20419
|
+
return this.db.query(`
|
|
20420
|
+
SELECT job_id, specialist, status
|
|
20421
|
+
FROM specialist_jobs
|
|
20422
|
+
WHERE status IN (${placeholders})
|
|
20423
|
+
ORDER BY updated_at_ms DESC
|
|
20424
|
+
`).all(...statuses);
|
|
20425
|
+
}, "listActiveJobs");
|
|
20426
|
+
}
|
|
20427
|
+
getDatabaseSizeBytes() {
|
|
20428
|
+
try {
|
|
20429
|
+
return statSync(this.dbPath).size;
|
|
20430
|
+
} catch {
|
|
20431
|
+
return 0;
|
|
20432
|
+
}
|
|
20433
|
+
}
|
|
20434
|
+
vacuumDatabase() {
|
|
20435
|
+
return withRetry(() => {
|
|
20436
|
+
const beforeBytes = this.getDatabaseSizeBytes();
|
|
20437
|
+
this.db.run("VACUUM");
|
|
20438
|
+
const afterBytes = this.getDatabaseSizeBytes();
|
|
20439
|
+
return { beforeBytes, afterBytes };
|
|
20440
|
+
}, "vacuumDatabase");
|
|
20441
|
+
}
|
|
20442
|
+
pruneObservabilityData(options) {
|
|
20443
|
+
return withRetry(() => {
|
|
20444
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
20445
|
+
const eventsRetentionMs = options.eventsRetentionMs ?? 30 * 24 * 60 * 60 * 1000;
|
|
20446
|
+
const eventsCutoffMs = nowMs - eventsRetentionMs;
|
|
20447
|
+
const terminalStatuses = ["done", "error", "stopped"];
|
|
20448
|
+
const activeStatuses = ["running", "starting", "waiting"];
|
|
20449
|
+
const skippedActiveChainJobs = this.db.query(`
|
|
20450
|
+
SELECT COUNT(*) AS count
|
|
20451
|
+
FROM specialist_jobs stale
|
|
20452
|
+
WHERE stale.updated_at_ms < ?
|
|
20453
|
+
AND stale.status IN (${terminalStatuses.map(() => "?").join(", ")})
|
|
20454
|
+
AND stale.chain_id IS NOT NULL
|
|
20455
|
+
AND EXISTS (
|
|
20456
|
+
SELECT 1
|
|
20457
|
+
FROM specialist_jobs active
|
|
20458
|
+
WHERE active.chain_id = stale.chain_id
|
|
20459
|
+
AND active.status IN (${activeStatuses.map(() => "?").join(", ")})
|
|
20460
|
+
)
|
|
20461
|
+
`).get(options.beforeMs, ...terminalStatuses, ...activeStatuses)?.count ?? 0;
|
|
20462
|
+
const resultCandidates = this.db.query(`
|
|
20463
|
+
SELECT COUNT(*) AS count
|
|
20464
|
+
FROM specialist_results results
|
|
20465
|
+
LEFT JOIN specialist_jobs jobs ON jobs.job_id = results.job_id
|
|
20466
|
+
WHERE results.updated_at_ms < ?
|
|
20467
|
+
AND (
|
|
20468
|
+
jobs.job_id IS NULL
|
|
20469
|
+
OR jobs.chain_id IS NULL
|
|
20470
|
+
OR NOT EXISTS (
|
|
20471
|
+
SELECT 1
|
|
20472
|
+
FROM specialist_jobs active
|
|
20473
|
+
WHERE active.chain_id = jobs.chain_id
|
|
20474
|
+
AND active.status IN (${activeStatuses.map(() => "?").join(", ")})
|
|
20475
|
+
)
|
|
20476
|
+
)
|
|
20477
|
+
`).get(options.beforeMs, ...activeStatuses)?.count ?? 0;
|
|
20478
|
+
const jobCandidates = this.db.query(`
|
|
20479
|
+
SELECT COUNT(*) AS count
|
|
20480
|
+
FROM specialist_jobs stale
|
|
20481
|
+
WHERE stale.updated_at_ms < ?
|
|
20482
|
+
AND stale.status IN (${terminalStatuses.map(() => "?").join(", ")})
|
|
20483
|
+
AND (
|
|
20484
|
+
stale.chain_id IS NULL
|
|
20485
|
+
OR NOT EXISTS (
|
|
20486
|
+
SELECT 1
|
|
20487
|
+
FROM specialist_jobs active
|
|
20488
|
+
WHERE active.chain_id = stale.chain_id
|
|
20489
|
+
AND active.status IN (${activeStatuses.map(() => "?").join(", ")})
|
|
20490
|
+
)
|
|
20491
|
+
)
|
|
20492
|
+
`).get(options.beforeMs, ...terminalStatuses, ...activeStatuses)?.count ?? 0;
|
|
20493
|
+
const eventsCandidates = this.db.query("SELECT COUNT(*) AS count FROM specialist_events WHERE t < ?").get(eventsCutoffMs)?.count ?? 0;
|
|
20494
|
+
const epicCandidates = options.includeEpics ? this.db.query(`
|
|
20495
|
+
SELECT COUNT(*) AS count
|
|
20496
|
+
FROM epic_runs epic
|
|
20497
|
+
WHERE epic.updated_at_ms < ?
|
|
20498
|
+
AND epic.status IN ('merged', 'failed', 'abandoned')
|
|
20499
|
+
AND NOT EXISTS (
|
|
20500
|
+
SELECT 1
|
|
20501
|
+
FROM epic_chain_membership membership
|
|
20502
|
+
WHERE membership.epic_id = epic.epic_id
|
|
20503
|
+
)
|
|
20504
|
+
`).get(options.beforeMs)?.count ?? 0 : 0;
|
|
20505
|
+
if (!options.apply) {
|
|
20506
|
+
return {
|
|
20507
|
+
dryRun: true,
|
|
20508
|
+
beforeMs: options.beforeMs,
|
|
20509
|
+
eventsCutoffMs,
|
|
20510
|
+
includeEpics: options.includeEpics,
|
|
20511
|
+
deletedEvents: eventsCandidates,
|
|
20512
|
+
deletedResults: resultCandidates,
|
|
20513
|
+
deletedJobs: jobCandidates,
|
|
20514
|
+
deletedEpicRuns: epicCandidates,
|
|
20515
|
+
skippedActiveChainJobs
|
|
20516
|
+
};
|
|
20517
|
+
}
|
|
20518
|
+
const deleteResults = this.db.query(`
|
|
20519
|
+
DELETE FROM specialist_results
|
|
20520
|
+
WHERE updated_at_ms < ?
|
|
20521
|
+
AND (
|
|
20522
|
+
job_id NOT IN (SELECT job_id FROM specialist_jobs WHERE chain_id IS NOT NULL)
|
|
20523
|
+
OR job_id IN (
|
|
20524
|
+
SELECT jobs.job_id
|
|
20525
|
+
FROM specialist_jobs jobs
|
|
20526
|
+
WHERE jobs.chain_id IS NULL
|
|
20527
|
+
OR NOT EXISTS (
|
|
20528
|
+
SELECT 1
|
|
20529
|
+
FROM specialist_jobs active
|
|
20530
|
+
WHERE active.chain_id = jobs.chain_id
|
|
20531
|
+
AND active.status IN (${activeStatuses.map(() => "?").join(", ")})
|
|
20532
|
+
)
|
|
20533
|
+
)
|
|
20534
|
+
)
|
|
20535
|
+
`);
|
|
20536
|
+
const deletedResults = deleteResults.run(options.beforeMs, ...activeStatuses).changes ?? 0;
|
|
20537
|
+
const deleteEvents = this.db.query("DELETE FROM specialist_events WHERE t < ?");
|
|
20538
|
+
const deletedEvents = deleteEvents.run(eventsCutoffMs).changes ?? 0;
|
|
20539
|
+
const deleteJobs = this.db.query(`
|
|
20540
|
+
DELETE FROM specialist_jobs
|
|
20541
|
+
WHERE updated_at_ms < ?
|
|
20542
|
+
AND status IN (${terminalStatuses.map(() => "?").join(", ")})
|
|
20543
|
+
AND (
|
|
20544
|
+
chain_id IS NULL
|
|
20545
|
+
OR NOT EXISTS (
|
|
20546
|
+
SELECT 1
|
|
20547
|
+
FROM specialist_jobs active
|
|
20548
|
+
WHERE active.chain_id = specialist_jobs.chain_id
|
|
20549
|
+
AND active.status IN (${activeStatuses.map(() => "?").join(", ")})
|
|
20550
|
+
)
|
|
20551
|
+
)
|
|
20552
|
+
`);
|
|
20553
|
+
const deletedJobs = deleteJobs.run(options.beforeMs, ...terminalStatuses, ...activeStatuses).changes ?? 0;
|
|
20554
|
+
let deletedEpicRuns = 0;
|
|
20555
|
+
if (options.includeEpics) {
|
|
20556
|
+
const deleteEpics = this.db.query(`
|
|
20557
|
+
DELETE FROM epic_runs
|
|
20558
|
+
WHERE updated_at_ms < ?
|
|
20559
|
+
AND status IN ('merged', 'failed', 'abandoned')
|
|
20560
|
+
AND NOT EXISTS (
|
|
20561
|
+
SELECT 1
|
|
20562
|
+
FROM epic_chain_membership membership
|
|
20563
|
+
WHERE membership.epic_id = epic_runs.epic_id
|
|
20564
|
+
)
|
|
20565
|
+
`);
|
|
20566
|
+
deletedEpicRuns = deleteEpics.run(options.beforeMs).changes ?? 0;
|
|
20567
|
+
}
|
|
20568
|
+
return {
|
|
20569
|
+
dryRun: false,
|
|
20570
|
+
beforeMs: options.beforeMs,
|
|
20571
|
+
eventsCutoffMs,
|
|
20572
|
+
includeEpics: options.includeEpics,
|
|
20573
|
+
deletedEvents,
|
|
20574
|
+
deletedResults,
|
|
20575
|
+
deletedJobs,
|
|
20576
|
+
deletedEpicRuns,
|
|
20577
|
+
skippedActiveChainJobs
|
|
20578
|
+
};
|
|
20579
|
+
}, "pruneObservabilityData");
|
|
20580
|
+
}
|
|
20581
|
+
scanOrphans() {
|
|
20582
|
+
return withRetry(() => {
|
|
20583
|
+
const findings = [];
|
|
20584
|
+
const chainMembershipWithoutJobs = this.db.query(`
|
|
20585
|
+
SELECT membership.chain_id, membership.epic_id
|
|
20586
|
+
FROM epic_chain_membership membership
|
|
20587
|
+
LEFT JOIN specialist_jobs jobs ON jobs.chain_id = membership.chain_id
|
|
20588
|
+
WHERE jobs.job_id IS NULL
|
|
20589
|
+
`).all();
|
|
20590
|
+
for (const row of chainMembershipWithoutJobs) {
|
|
20591
|
+
findings.push({
|
|
20592
|
+
kind: "orphan",
|
|
20593
|
+
code: "chain_membership_without_jobs",
|
|
20594
|
+
message: `chain ${row.chain_id} has epic membership but no jobs`,
|
|
20595
|
+
details: { chain_id: row.chain_id, epic_id: row.epic_id }
|
|
20596
|
+
});
|
|
20597
|
+
}
|
|
20598
|
+
const epicsWithoutChains = this.db.query(`
|
|
20599
|
+
SELECT epic.epic_id, epic.status
|
|
20600
|
+
FROM epic_runs epic
|
|
20601
|
+
LEFT JOIN epic_chain_membership membership ON membership.epic_id = epic.epic_id
|
|
20602
|
+
WHERE membership.chain_id IS NULL
|
|
20603
|
+
`).all();
|
|
20604
|
+
for (const row of epicsWithoutChains) {
|
|
20605
|
+
findings.push({
|
|
20606
|
+
kind: "orphan",
|
|
20607
|
+
code: "epic_without_chains",
|
|
20608
|
+
message: `epic ${row.epic_id} has no chain membership`,
|
|
20609
|
+
details: { epic_id: row.epic_id, status: row.status }
|
|
20610
|
+
});
|
|
20611
|
+
}
|
|
20612
|
+
const jobEpicWithoutMembership = this.db.query(`
|
|
20613
|
+
SELECT jobs.job_id, jobs.epic_id, jobs.chain_id
|
|
20614
|
+
FROM specialist_jobs jobs
|
|
20615
|
+
LEFT JOIN epic_chain_membership membership
|
|
20616
|
+
ON membership.chain_id = jobs.chain_id
|
|
20617
|
+
AND membership.epic_id = jobs.epic_id
|
|
20618
|
+
WHERE jobs.epic_id IS NOT NULL
|
|
20619
|
+
AND (jobs.chain_id IS NULL OR membership.chain_id IS NULL)
|
|
20620
|
+
`).all();
|
|
20621
|
+
for (const row of jobEpicWithoutMembership) {
|
|
20622
|
+
findings.push({
|
|
20623
|
+
kind: "integrity-violation",
|
|
20624
|
+
code: "job_epic_without_membership",
|
|
20625
|
+
message: `job ${row.job_id} references epic without chain membership link`,
|
|
20626
|
+
details: { job_id: row.job_id, epic_id: row.epic_id, chain_id: row.chain_id ?? null }
|
|
20627
|
+
});
|
|
20628
|
+
}
|
|
20629
|
+
const worktreeRows = this.db.query(`
|
|
20630
|
+
SELECT DISTINCT job_id, worktree_column
|
|
20631
|
+
FROM specialist_jobs
|
|
20632
|
+
WHERE worktree_column IS NOT NULL AND worktree_column != ''
|
|
20633
|
+
`).all();
|
|
20634
|
+
for (const row of worktreeRows) {
|
|
20635
|
+
if (existsSync4(row.worktree_column))
|
|
20636
|
+
continue;
|
|
20637
|
+
findings.push({
|
|
20638
|
+
kind: "stale-pointer",
|
|
20639
|
+
code: "worktree_missing_on_disk",
|
|
20640
|
+
message: `job ${row.job_id} points to missing worktree path`,
|
|
20641
|
+
details: { job_id: row.job_id, worktree_path: row.worktree_column }
|
|
20642
|
+
});
|
|
20643
|
+
}
|
|
20644
|
+
return findings;
|
|
20645
|
+
}, "scanOrphans");
|
|
20646
|
+
}
|
|
20334
20647
|
close() {
|
|
20335
20648
|
this.db.close();
|
|
20336
20649
|
}
|
|
@@ -20757,6 +21070,9 @@ function resolveOutputContractSchema(responseFormat, outputType, outputSchema) {
|
|
|
20757
21070
|
}
|
|
20758
21071
|
return mergedSchema;
|
|
20759
21072
|
}
|
|
21073
|
+
function shellQuote(value) {
|
|
21074
|
+
return `'${value.replace(/'/g, `'''`)}'`;
|
|
21075
|
+
}
|
|
20760
21076
|
function buildOutputContractInstruction(responseFormat, outputType, outputSchema) {
|
|
20761
21077
|
if (responseFormat === "text")
|
|
20762
21078
|
return "";
|
|
@@ -20781,6 +21097,58 @@ function buildOutputContractInstruction(responseFormat, outputType, outputSchema
|
|
|
20781
21097
|
${lines.join(`
|
|
20782
21098
|
`)}`;
|
|
20783
21099
|
}
|
|
21100
|
+
function buildReviewerDiffContext(cwd, maxFiles = 20) {
|
|
21101
|
+
const stat2 = execSync2("git diff --stat", {
|
|
21102
|
+
cwd,
|
|
21103
|
+
encoding: "utf8",
|
|
21104
|
+
timeout: 1e4,
|
|
21105
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
21106
|
+
}).trim();
|
|
21107
|
+
const files = execSync2("git diff --name-only", {
|
|
21108
|
+
cwd,
|
|
21109
|
+
encoding: "utf8",
|
|
21110
|
+
timeout: 1e4,
|
|
21111
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
21112
|
+
}).split(`
|
|
21113
|
+
`).map((line) => line.trim()).filter(Boolean).slice(0, maxFiles);
|
|
21114
|
+
if (files.length === 0) {
|
|
21115
|
+
throw new Error("Reviewer startup blocked: git diff is empty. No patch context to review.");
|
|
21116
|
+
}
|
|
21117
|
+
const hunks = files.map((file) => {
|
|
21118
|
+
const diff = execSync2(`git diff -- ${shellQuote(file)}`, {
|
|
21119
|
+
cwd,
|
|
21120
|
+
encoding: "utf8",
|
|
21121
|
+
timeout: 1e4,
|
|
21122
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
21123
|
+
}).trim();
|
|
21124
|
+
return diff ? `### ${file}
|
|
21125
|
+
${diff}` : `### ${file}
|
|
21126
|
+
(no hunks)`;
|
|
21127
|
+
}).join(`
|
|
21128
|
+
|
|
21129
|
+
`);
|
|
21130
|
+
return { stat: stat2, files, hunks };
|
|
21131
|
+
}
|
|
21132
|
+
function buildReviewerDiffInstruction(context) {
|
|
21133
|
+
return `
|
|
21134
|
+
|
|
21135
|
+
---
|
|
21136
|
+
## Reviewer Diff Context
|
|
21137
|
+
Review only patch below. Ignore unrelated files, repo-wide exploration, and filesystem hunting.
|
|
21138
|
+
If patch context is empty, stop and fail fast.
|
|
21139
|
+
|
|
21140
|
+
Diff stat:
|
|
21141
|
+
${context.stat || "(no stat)"}
|
|
21142
|
+
|
|
21143
|
+
Changed files:
|
|
21144
|
+
${context.files.map((file) => `- ${file}`).join(`
|
|
21145
|
+
`)}
|
|
21146
|
+
|
|
21147
|
+
Diff hunks:
|
|
21148
|
+
${context.hunks}
|
|
21149
|
+
---
|
|
21150
|
+
`;
|
|
21151
|
+
}
|
|
20784
21152
|
function tryParseJson(input) {
|
|
20785
21153
|
try {
|
|
20786
21154
|
return { value: JSON.parse(input) };
|
|
@@ -20944,11 +21312,21 @@ class SpecialistRunner {
|
|
|
20944
21312
|
const preScriptOutput = formatScriptOutput(preResults);
|
|
20945
21313
|
const resolvedPrompt = this.resolvePromptWithBeadContext(options, beadsClient);
|
|
20946
21314
|
const beadVariables = options.inputBeadId ? { bead_context: resolvedPrompt, bead_id: options.inputBeadId } : {};
|
|
20947
|
-
const
|
|
21315
|
+
const lineageVariables = {
|
|
21316
|
+
...options.reusedFromJobId ? { reused_from_job_id: options.reusedFromJobId } : {},
|
|
21317
|
+
...options.worktreeOwnerJobId ? { worktree_owner_job_id: options.worktreeOwnerJobId } : {}
|
|
21318
|
+
};
|
|
21319
|
+
const beadTemplateVariables = {
|
|
21320
|
+
prompt: resolvedPrompt,
|
|
21321
|
+
bead_id: options.inputBeadId ?? "",
|
|
21322
|
+
...lineageVariables
|
|
21323
|
+
};
|
|
20948
21324
|
const variables = {
|
|
20949
21325
|
prompt: resolvedPrompt,
|
|
20950
21326
|
cwd: runCwd,
|
|
20951
21327
|
pre_script_output: preScriptOutput,
|
|
21328
|
+
bead_id: options.inputBeadId ?? "",
|
|
21329
|
+
...lineageVariables,
|
|
20952
21330
|
...options.variables ?? {},
|
|
20953
21331
|
...beadVariables
|
|
20954
21332
|
};
|
|
@@ -20961,7 +21339,7 @@ class SpecialistRunner {
|
|
|
20961
21339
|
estimated_tokens: Math.ceil(renderedTask.length / 4),
|
|
20962
21340
|
system_prompt_present: !!prompt.system
|
|
20963
21341
|
});
|
|
20964
|
-
let agentsMd =
|
|
21342
|
+
let agentsMd = renderTemplate(prompt.system ?? "", beadTemplateVariables);
|
|
20965
21343
|
{
|
|
20966
21344
|
const sanitizedBeadId = options.inputBeadId ? sanitizeBeadIdForPrompt(options.inputBeadId) : "";
|
|
20967
21345
|
const beadInstructions = sanitizedBeadId ? `
|
|
@@ -21092,6 +21470,10 @@ ${summaries.join(`
|
|
|
21092
21470
|
}
|
|
21093
21471
|
})
|
|
21094
21472
|
});
|
|
21473
|
+
if (metadata.name === "reviewer" && options.reusedFromJobId) {
|
|
21474
|
+
const reviewerDiffContext = buildReviewerDiffContext(runCwd);
|
|
21475
|
+
agentsMd += buildReviewerDiffInstruction(reviewerDiffContext);
|
|
21476
|
+
}
|
|
21095
21477
|
const responseFormat = execution.response_format ?? "text";
|
|
21096
21478
|
const outputType = execution.output_type ?? "custom";
|
|
21097
21479
|
const specialistOutputSchema = prompt.output_schema;
|
|
@@ -21691,6 +22073,13 @@ function mapCallbackEventToTimelineEvent(callbackEvent, context) {
|
|
|
21691
22073
|
...context.extensionError?.extension ? { extension: context.extensionError.extension } : {},
|
|
21692
22074
|
...context.extensionError?.errorMessage ? { error_message: context.extensionError.errorMessage } : {}
|
|
21693
22075
|
};
|
|
22076
|
+
case "api_error":
|
|
22077
|
+
return {
|
|
22078
|
+
t,
|
|
22079
|
+
type: TIMELINE_EVENT_TYPES.ERROR,
|
|
22080
|
+
source: context.apiError?.source ?? "rpc",
|
|
22081
|
+
error_message: context.apiError?.errorMessage ?? "Unknown API error"
|
|
22082
|
+
};
|
|
21694
22083
|
case "memory_injection":
|
|
21695
22084
|
return {
|
|
21696
22085
|
t,
|
|
@@ -21713,12 +22102,13 @@ function mapCallbackEventToTimelineEvent(callbackEvent, context) {
|
|
|
21713
22102
|
return null;
|
|
21714
22103
|
}
|
|
21715
22104
|
}
|
|
21716
|
-
function createRunStartEvent(specialist, beadId) {
|
|
22105
|
+
function createRunStartEvent(specialist, beadId, startupSnapshot) {
|
|
21717
22106
|
return {
|
|
21718
22107
|
t: Date.now(),
|
|
21719
22108
|
type: TIMELINE_EVENT_TYPES.RUN_START,
|
|
21720
22109
|
specialist,
|
|
21721
|
-
bead_id: beadId
|
|
22110
|
+
bead_id: beadId,
|
|
22111
|
+
...startupSnapshot ? { startup_snapshot: startupSnapshot } : {}
|
|
21722
22112
|
};
|
|
21723
22113
|
}
|
|
21724
22114
|
function createMetaEvent(model, backend) {
|
|
@@ -21875,6 +22265,7 @@ var init_timeline_events = __esm(() => {
|
|
|
21875
22265
|
RETRY: "retry",
|
|
21876
22266
|
MODEL_CHANGE: "model_change",
|
|
21877
22267
|
EXTENSION_ERROR: "extension_error",
|
|
22268
|
+
ERROR: "error",
|
|
21878
22269
|
AUTO_COMMIT_SUCCESS: "auto_commit_success",
|
|
21879
22270
|
AUTO_COMMIT_SKIPPED: "auto_commit_skipped",
|
|
21880
22271
|
AUTO_COMMIT_FAILED: "auto_commit_failed",
|
|
@@ -21938,6 +22329,27 @@ function evaluateEpicMergeReadiness(input) {
|
|
|
21938
22329
|
summary: `Epic ${input.epicId} is merge-ready and all chains are terminal.`
|
|
21939
22330
|
};
|
|
21940
22331
|
}
|
|
22332
|
+
function appendEpicTransitionAudit(statusJson, entry) {
|
|
22333
|
+
const fallback = {
|
|
22334
|
+
transitions: []
|
|
22335
|
+
};
|
|
22336
|
+
let parsed = fallback;
|
|
22337
|
+
if (statusJson) {
|
|
22338
|
+
try {
|
|
22339
|
+
const candidate = JSON.parse(statusJson);
|
|
22340
|
+
if (candidate && typeof candidate === "object") {
|
|
22341
|
+
parsed = candidate;
|
|
22342
|
+
}
|
|
22343
|
+
} catch {
|
|
22344
|
+
parsed = fallback;
|
|
22345
|
+
}
|
|
22346
|
+
}
|
|
22347
|
+
const previous = Array.isArray(parsed.transitions) ? parsed.transitions.filter((item) => Boolean(item) && typeof item === "object") : [];
|
|
22348
|
+
return JSON.stringify({
|
|
22349
|
+
...parsed,
|
|
22350
|
+
transitions: [...previous, entry]
|
|
22351
|
+
});
|
|
22352
|
+
}
|
|
21941
22353
|
function summarizeEpicTransition(epicId, from, to) {
|
|
21942
22354
|
return `Epic ${epicId}: ${from} -> ${to}`;
|
|
21943
22355
|
}
|
|
@@ -21954,6 +22366,72 @@ var init_epic_lifecycle = __esm(() => {
|
|
|
21954
22366
|
};
|
|
21955
22367
|
});
|
|
21956
22368
|
|
|
22369
|
+
// src/specialist/process-liveness.ts
|
|
22370
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
22371
|
+
function isValidPid(pid) {
|
|
22372
|
+
return Number.isInteger(pid) && pid > 0;
|
|
22373
|
+
}
|
|
22374
|
+
function isAliveBySignal(pid) {
|
|
22375
|
+
try {
|
|
22376
|
+
process.kill(pid, 0);
|
|
22377
|
+
return true;
|
|
22378
|
+
} catch {
|
|
22379
|
+
return false;
|
|
22380
|
+
}
|
|
22381
|
+
}
|
|
22382
|
+
function parseBootTimeMs() {
|
|
22383
|
+
try {
|
|
22384
|
+
const procStat = readFileSync4("/proc/stat", "utf-8");
|
|
22385
|
+
const bootLine = procStat.split(`
|
|
22386
|
+
`).find((line) => line.startsWith("btime "));
|
|
22387
|
+
if (!bootLine)
|
|
22388
|
+
return;
|
|
22389
|
+
const bootSeconds = Number.parseInt(bootLine.split(/\s+/)[1] ?? "", 10);
|
|
22390
|
+
if (!Number.isFinite(bootSeconds) || bootSeconds <= 0)
|
|
22391
|
+
return;
|
|
22392
|
+
return bootSeconds * 1000;
|
|
22393
|
+
} catch {
|
|
22394
|
+
return;
|
|
22395
|
+
}
|
|
22396
|
+
}
|
|
22397
|
+
function parseProcessStartTimeMs(pid) {
|
|
22398
|
+
try {
|
|
22399
|
+
const statRaw = readFileSync4(`/proc/${pid}/stat`, "utf-8");
|
|
22400
|
+
const closeParenIndex = statRaw.lastIndexOf(")");
|
|
22401
|
+
if (closeParenIndex < 0)
|
|
22402
|
+
return;
|
|
22403
|
+
const suffix = statRaw.slice(closeParenIndex + 1).trim();
|
|
22404
|
+
const fields = suffix.split(/\s+/);
|
|
22405
|
+
const startTimeTicksText = fields[PROC_STAT_START_TIME_INDEX - 2];
|
|
22406
|
+
if (!startTimeTicksText)
|
|
22407
|
+
return;
|
|
22408
|
+
const startTimeTicks = Number.parseInt(startTimeTicksText, 10);
|
|
22409
|
+
if (!Number.isFinite(startTimeTicks) || startTimeTicks < 0)
|
|
22410
|
+
return;
|
|
22411
|
+
const bootTimeMs = parseBootTimeMs();
|
|
22412
|
+
if (bootTimeMs === undefined)
|
|
22413
|
+
return;
|
|
22414
|
+
const ticksPerSecond = 100;
|
|
22415
|
+
return bootTimeMs + Math.floor(startTimeTicks * 1000 / ticksPerSecond);
|
|
22416
|
+
} catch {
|
|
22417
|
+
return;
|
|
22418
|
+
}
|
|
22419
|
+
}
|
|
22420
|
+
function isProcessAlive(pid, startTimeMs) {
|
|
22421
|
+
if (!isValidPid(pid))
|
|
22422
|
+
return false;
|
|
22423
|
+
if (!isAliveBySignal(pid))
|
|
22424
|
+
return false;
|
|
22425
|
+
if (startTimeMs === undefined)
|
|
22426
|
+
return true;
|
|
22427
|
+
const actualStartTimeMs = parseProcessStartTimeMs(pid);
|
|
22428
|
+
if (actualStartTimeMs === undefined)
|
|
22429
|
+
return true;
|
|
22430
|
+
return Math.abs(actualStartTimeMs - startTimeMs) <= 2000;
|
|
22431
|
+
}
|
|
22432
|
+
var PROC_STAT_START_TIME_INDEX = 21;
|
|
22433
|
+
var init_process_liveness = () => {};
|
|
22434
|
+
|
|
21957
22435
|
// src/specialist/epic-readiness.ts
|
|
21958
22436
|
function parseReviewerVerdict(output) {
|
|
21959
22437
|
if (!output)
|
|
@@ -21984,7 +22462,19 @@ function evaluateChainReadiness(chainId, jobs, chainRootBeadId) {
|
|
|
21984
22462
|
}
|
|
21985
22463
|
const orderedJobs = [...jobs].sort((a, b) => a.started_at_ms - b.started_at_ms);
|
|
21986
22464
|
const activeJobs = orderedJobs.filter((job) => ACTIVE_JOB_STATUSES.has(job.status));
|
|
22465
|
+
const deadActiveJobs = activeJobs.filter((job) => job.pid !== undefined && !isProcessAlive(job.pid, job.started_at_ms));
|
|
21987
22466
|
const hasActiveJobs = activeJobs.length > 0;
|
|
22467
|
+
if (deadActiveJobs.length > 0) {
|
|
22468
|
+
return {
|
|
22469
|
+
chain_id: chainId,
|
|
22470
|
+
chain_root_bead_id: chainRootBeadId,
|
|
22471
|
+
state: "failed",
|
|
22472
|
+
reviewer_verdict: "missing",
|
|
22473
|
+
blocking_reason: `Active chain jobs appear dead: ${deadActiveJobs.map((job) => job.id).join(", ")}`,
|
|
22474
|
+
has_active_jobs: false,
|
|
22475
|
+
job_ids: orderedJobs.map((job) => job.id)
|
|
22476
|
+
};
|
|
22477
|
+
}
|
|
21988
22478
|
if (hasActiveJobs) {
|
|
21989
22479
|
return {
|
|
21990
22480
|
chain_id: chainId,
|
|
@@ -22151,6 +22641,7 @@ function loadEpicReadinessSummary(sqlite, epicId) {
|
|
|
22151
22641
|
id: status.id,
|
|
22152
22642
|
specialist: status.specialist,
|
|
22153
22643
|
status: status.status,
|
|
22644
|
+
pid: status.pid,
|
|
22154
22645
|
started_at_ms: status.started_at_ms,
|
|
22155
22646
|
result_text: sqlite.readResult(status.id) ?? undefined
|
|
22156
22647
|
}));
|
|
@@ -22193,6 +22684,7 @@ function syncEpicStateFromReadiness(sqlite, summary) {
|
|
|
22193
22684
|
var ACTIVE_JOB_STATUSES, TERMINAL_JOB_STATUSES, REVIEWER_VERDICT_REGEX;
|
|
22194
22685
|
var init_epic_readiness = __esm(() => {
|
|
22195
22686
|
init_epic_lifecycle();
|
|
22687
|
+
init_process_liveness();
|
|
22196
22688
|
ACTIVE_JOB_STATUSES = new Set(["starting", "running", "waiting"]);
|
|
22197
22689
|
TERMINAL_JOB_STATUSES = new Set(["done", "error"]);
|
|
22198
22690
|
REVIEWER_VERDICT_REGEX = /Verdict:\s*(PASS|PARTIAL|FAIL)/i;
|
|
@@ -22271,10 +22763,10 @@ import {
|
|
|
22271
22763
|
mkdirSync as mkdirSync3,
|
|
22272
22764
|
openSync,
|
|
22273
22765
|
readdirSync,
|
|
22274
|
-
readFileSync as
|
|
22766
|
+
readFileSync as readFileSync5,
|
|
22275
22767
|
renameSync,
|
|
22276
22768
|
rmSync,
|
|
22277
|
-
statSync,
|
|
22769
|
+
statSync as statSync2,
|
|
22278
22770
|
writeFileSync as writeFileSync3,
|
|
22279
22771
|
writeSync
|
|
22280
22772
|
} from "fs";
|
|
@@ -22685,7 +23177,7 @@ class Supervisor {
|
|
|
22685
23177
|
if (!existsSync7(path))
|
|
22686
23178
|
return null;
|
|
22687
23179
|
try {
|
|
22688
|
-
const status = JSON.parse(
|
|
23180
|
+
const status = JSON.parse(readFileSync5(path, "utf-8"));
|
|
22689
23181
|
return this.withComputedLiveness(status);
|
|
22690
23182
|
} catch {
|
|
22691
23183
|
return null;
|
|
@@ -22727,7 +23219,7 @@ class Supervisor {
|
|
|
22727
23219
|
if (!existsSync7(path))
|
|
22728
23220
|
continue;
|
|
22729
23221
|
try {
|
|
22730
|
-
const status = JSON.parse(
|
|
23222
|
+
const status = JSON.parse(readFileSync5(path, "utf-8"));
|
|
22731
23223
|
jobs.push(this.withComputedLiveness(status));
|
|
22732
23224
|
} catch {}
|
|
22733
23225
|
}
|
|
@@ -22807,7 +23299,7 @@ class Supervisor {
|
|
|
22807
23299
|
for (const entry of readdirSync(this.resolvedJobsDir)) {
|
|
22808
23300
|
const dir = join8(this.resolvedJobsDir, entry);
|
|
22809
23301
|
try {
|
|
22810
|
-
const stat2 =
|
|
23302
|
+
const stat2 = statSync2(dir);
|
|
22811
23303
|
if (!stat2.isDirectory())
|
|
22812
23304
|
continue;
|
|
22813
23305
|
if (stat2.mtimeMs < cutoff)
|
|
@@ -22828,7 +23320,7 @@ class Supervisor {
|
|
|
22828
23320
|
if (!existsSync7(statusPath))
|
|
22829
23321
|
continue;
|
|
22830
23322
|
try {
|
|
22831
|
-
const s = JSON.parse(
|
|
23323
|
+
const s = JSON.parse(readFileSync5(statusPath, "utf-8"));
|
|
22832
23324
|
if (s.status === "running" || s.status === "starting") {
|
|
22833
23325
|
if (!s.pid)
|
|
22834
23326
|
continue;
|
|
@@ -22890,6 +23382,32 @@ class Supervisor {
|
|
|
22890
23382
|
mkdirSync3(dir, { recursive: true });
|
|
22891
23383
|
mkdirSync3(this.readyDir(), { recursive: true });
|
|
22892
23384
|
const nodeId = runOptions.variables?.node_id ?? runOptions.variables?.SPECIALISTS_NODE_ID;
|
|
23385
|
+
const variablesKeys = Object.keys(runOptions.variables ?? {});
|
|
23386
|
+
const activatedSkills = (runOptions.variables?.activated_skills ?? runOptions.variables?.skills_activated ?? "").split(",").map((skill) => skill.trim()).filter((skill) => skill.length > 0);
|
|
23387
|
+
const startupContext = {
|
|
23388
|
+
job_id: id,
|
|
23389
|
+
specialist_name: runOptions.name,
|
|
23390
|
+
...runOptions.inputBeadId ? { bead_id: runOptions.inputBeadId } : {},
|
|
23391
|
+
...runOptions.reusedFromJobId ? { reused_from_job_id: runOptions.reusedFromJobId } : {},
|
|
23392
|
+
...runOptions.worktreeOwnerJobId ? { worktree_owner_job_id: runOptions.worktreeOwnerJobId } : {},
|
|
23393
|
+
...runOptions.worktreeOwnerJobId || runOptions.workingDirectory ? {
|
|
23394
|
+
chain_id: runOptions.worktreeOwnerJobId ?? id,
|
|
23395
|
+
chain_root_job_id: runOptions.worktreeOwnerJobId ?? id
|
|
23396
|
+
} : {},
|
|
23397
|
+
...runOptions.variables?.chain_root_bead_id ? { chain_root_bead_id: runOptions.variables.chain_root_bead_id } : {},
|
|
23398
|
+
...runOptions.workingDirectory ? { worktree_path: runOptions.workingDirectory } : {},
|
|
23399
|
+
...runOptions.workingDirectory ? { branch: resolveCurrentBranch(runOptions.workingDirectory) } : { branch: resolveCurrentBranch() },
|
|
23400
|
+
variables_keys: variablesKeys,
|
|
23401
|
+
reviewed_job_id_present: variablesKeys.includes("reviewed_job_id"),
|
|
23402
|
+
reused_worktree_awareness_present: variablesKeys.includes("reused_worktree_awareness"),
|
|
23403
|
+
bead_context_present: variablesKeys.includes("bead_context"),
|
|
23404
|
+
...activatedSkills.length > 0 ? {
|
|
23405
|
+
skills: {
|
|
23406
|
+
count: activatedSkills.length,
|
|
23407
|
+
activated: activatedSkills
|
|
23408
|
+
}
|
|
23409
|
+
} : {}
|
|
23410
|
+
};
|
|
22893
23411
|
const initialStatus = {
|
|
22894
23412
|
id,
|
|
22895
23413
|
specialist: runOptions.name,
|
|
@@ -22908,7 +23426,8 @@ class Supervisor {
|
|
|
22908
23426
|
chain_root_job_id: runOptions.worktreeOwnerJobId ?? id
|
|
22909
23427
|
} : { chain_kind: "prep" },
|
|
22910
23428
|
...runOptions.epicId ? { epic_id: runOptions.epicId } : {},
|
|
22911
|
-
...runOptions.workingDirectory ? { branch: resolveCurrentBranch(runOptions.workingDirectory) } : { branch: resolveCurrentBranch() }
|
|
23429
|
+
...runOptions.workingDirectory ? { branch: resolveCurrentBranch(runOptions.workingDirectory) } : { branch: resolveCurrentBranch() },
|
|
23430
|
+
startup_context: startupContext
|
|
22912
23431
|
};
|
|
22913
23432
|
this.writeStatusFileOnly(id, initialStatus);
|
|
22914
23433
|
const statusWatchdogPid = startDetachedStatusWatchdog(this.statusPath(id), process.pid);
|
|
@@ -22978,7 +23497,7 @@ class Supervisor {
|
|
|
22978
23497
|
appendTimelineEvent(createStatusChangeEvent("waiting", previousStatus));
|
|
22979
23498
|
}
|
|
22980
23499
|
};
|
|
22981
|
-
const runStartEvent = appendTimelineEventFileOnly(createRunStartEvent(runOptions.name, runOptions.inputBeadId));
|
|
23500
|
+
const runStartEvent = appendTimelineEventFileOnly(createRunStartEvent(runOptions.name, runOptions.inputBeadId, statusSnapshot.startup_context));
|
|
22982
23501
|
try {
|
|
22983
23502
|
this.withSqliteOperation("upsertStatusWithEvent:run_start", (client) => client.upsertStatusWithEvent(statusSnapshot, runStartEvent));
|
|
22984
23503
|
} catch (error2) {
|
|
@@ -23283,6 +23802,14 @@ ${appendError}
|
|
|
23283
23802
|
return;
|
|
23284
23803
|
}
|
|
23285
23804
|
})();
|
|
23805
|
+
if (eventType === "memory_injection" && memoryInjection) {
|
|
23806
|
+
setStatus({
|
|
23807
|
+
startup_context: {
|
|
23808
|
+
...statusSnapshot.startup_context ?? {},
|
|
23809
|
+
memory_injection: memoryInjection
|
|
23810
|
+
}
|
|
23811
|
+
});
|
|
23812
|
+
}
|
|
23286
23813
|
const timelineEvent = mapCallbackEventToTimelineEvent(eventType, {
|
|
23287
23814
|
tool: toolState?.tool,
|
|
23288
23815
|
toolCallId,
|
|
@@ -23340,6 +23867,16 @@ ${appendError}
|
|
|
23340
23867
|
appendTimelineEvent(createFinishReasonEvent(metricEvent.finish_reason, metricEvent.source));
|
|
23341
23868
|
return;
|
|
23342
23869
|
}
|
|
23870
|
+
if (metricEvent.type === "api_error") {
|
|
23871
|
+
mergeRunMetrics({ api_error: metricEvent.errorMessage });
|
|
23872
|
+
appendTimelineEvent({
|
|
23873
|
+
t: Date.now(),
|
|
23874
|
+
type: TIMELINE_EVENT_TYPES.ERROR,
|
|
23875
|
+
source: metricEvent.source,
|
|
23876
|
+
error_message: metricEvent.errorMessage
|
|
23877
|
+
});
|
|
23878
|
+
return;
|
|
23879
|
+
}
|
|
23343
23880
|
if (metricEvent.type === "turn_summary") {
|
|
23344
23881
|
mergeRunMetrics({
|
|
23345
23882
|
turns: metricEvent.turn_index,
|
|
@@ -23731,7 +24268,7 @@ __export(exports_list, {
|
|
|
23731
24268
|
ArgParseError: () => ArgParseError
|
|
23732
24269
|
});
|
|
23733
24270
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
23734
|
-
import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as
|
|
24271
|
+
import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
23735
24272
|
import { join as join9 } from "path";
|
|
23736
24273
|
import readline from "readline";
|
|
23737
24274
|
function permissionBadge(permission) {
|
|
@@ -23764,7 +24301,7 @@ function toLiveJob(status) {
|
|
|
23764
24301
|
}
|
|
23765
24302
|
function readJobStatus(statusPath) {
|
|
23766
24303
|
try {
|
|
23767
|
-
return JSON.parse(
|
|
24304
|
+
return JSON.parse(readFileSync6(statusPath, "utf-8"));
|
|
23768
24305
|
} catch {
|
|
23769
24306
|
return null;
|
|
23770
24307
|
}
|
|
@@ -24333,7 +24870,7 @@ var exports_init = {};
|
|
|
24333
24870
|
__export(exports_init, {
|
|
24334
24871
|
run: () => run6
|
|
24335
24872
|
});
|
|
24336
|
-
import { copyFileSync, cpSync, existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as
|
|
24873
|
+
import { copyFileSync, cpSync, existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync7, readlinkSync, renameSync as renameSync2, symlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
24337
24874
|
import { spawnSync as spawnSync9 } from "child_process";
|
|
24338
24875
|
import { basename as basename3, dirname as dirname5, join as join10, relative, resolve as resolve4 } from "path";
|
|
24339
24876
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -24375,7 +24912,7 @@ function loadJson(path, fallback) {
|
|
|
24375
24912
|
if (!existsSync9(path))
|
|
24376
24913
|
return structuredClone(fallback);
|
|
24377
24914
|
try {
|
|
24378
|
-
return JSON.parse(
|
|
24915
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
24379
24916
|
} catch {
|
|
24380
24917
|
return structuredClone(fallback);
|
|
24381
24918
|
}
|
|
@@ -24718,7 +25255,7 @@ function ensureProjectMcp(cwd) {
|
|
|
24718
25255
|
}
|
|
24719
25256
|
function ensureGitignore(cwd) {
|
|
24720
25257
|
const gitignorePath = join10(cwd, ".gitignore");
|
|
24721
|
-
const existing = existsSync9(gitignorePath) ?
|
|
25258
|
+
const existing = existsSync9(gitignorePath) ? readFileSync7(gitignorePath, "utf-8") : "";
|
|
24722
25259
|
let added = 0;
|
|
24723
25260
|
const lines = existing.split(`
|
|
24724
25261
|
`);
|
|
@@ -24765,7 +25302,7 @@ function ensureObservabilityDb(cwd) {
|
|
|
24765
25302
|
function ensureAgentsMd(cwd) {
|
|
24766
25303
|
const agentsPath = join10(cwd, "AGENTS.md");
|
|
24767
25304
|
if (existsSync9(agentsPath)) {
|
|
24768
|
-
const existing =
|
|
25305
|
+
const existing = readFileSync7(agentsPath, "utf-8");
|
|
24769
25306
|
if (existing.includes(AGENTS_MARKER)) {
|
|
24770
25307
|
skip("AGENTS.md already has Specialists section");
|
|
24771
25308
|
} else {
|
|
@@ -24783,7 +25320,7 @@ function readJsonObject(path) {
|
|
|
24783
25320
|
if (!existsSync9(path))
|
|
24784
25321
|
return {};
|
|
24785
25322
|
try {
|
|
24786
|
-
return JSON.parse(
|
|
25323
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
24787
25324
|
} catch {
|
|
24788
25325
|
return {};
|
|
24789
25326
|
}
|
|
@@ -25056,35 +25593,77 @@ var exports_db = {};
|
|
|
25056
25593
|
__export(exports_db, {
|
|
25057
25594
|
run: () => run8
|
|
25058
25595
|
});
|
|
25059
|
-
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as
|
|
25060
|
-
import { join as join11 } from "path";
|
|
25596
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
25597
|
+
import { dirname as dirname6, join as join11, resolve as resolve5 } from "path";
|
|
25598
|
+
function formatBytes(bytes) {
|
|
25599
|
+
if (bytes < 1024)
|
|
25600
|
+
return `${bytes} B`;
|
|
25601
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
25602
|
+
let value = bytes;
|
|
25603
|
+
let unitIndex = -1;
|
|
25604
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
25605
|
+
value /= 1024;
|
|
25606
|
+
unitIndex += 1;
|
|
25607
|
+
}
|
|
25608
|
+
return `${value.toFixed(2)} ${units[unitIndex]}`;
|
|
25609
|
+
}
|
|
25610
|
+
function parseIsoDate(input2) {
|
|
25611
|
+
const parsed = Date.parse(input2);
|
|
25612
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
25613
|
+
}
|
|
25614
|
+
function parseDuration(input2) {
|
|
25615
|
+
const match = input2.trim().toLowerCase().match(/^(\d+)([smhdw])$/);
|
|
25616
|
+
if (!match)
|
|
25617
|
+
return null;
|
|
25618
|
+
const amount = Number(match[1]);
|
|
25619
|
+
const unit = match[2];
|
|
25620
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
25621
|
+
return null;
|
|
25622
|
+
const multipliers = {
|
|
25623
|
+
s: 1000,
|
|
25624
|
+
m: 60 * 1000,
|
|
25625
|
+
h: 60 * 60 * 1000,
|
|
25626
|
+
d: DAY_MS,
|
|
25627
|
+
w: 7 * DAY_MS
|
|
25628
|
+
};
|
|
25629
|
+
return amount * multipliers[unit];
|
|
25630
|
+
}
|
|
25631
|
+
function parseBeforeArgument(raw) {
|
|
25632
|
+
const durationMs = parseDuration(raw);
|
|
25633
|
+
if (durationMs !== null)
|
|
25634
|
+
return Date.now() - durationMs;
|
|
25635
|
+
const isoMs = parseIsoDate(raw);
|
|
25636
|
+
if (isoMs !== null)
|
|
25637
|
+
return isoMs;
|
|
25638
|
+
throw new Error(`Invalid --before value '${raw}'. Use ISO date or duration like 7d.`);
|
|
25639
|
+
}
|
|
25061
25640
|
function printDbHelp() {
|
|
25062
25641
|
console.log([
|
|
25063
25642
|
"",
|
|
25064
|
-
"Usage: specialists db <setup|backfill>",
|
|
25643
|
+
"Usage: specialists db <setup|backfill|vacuum|prune>",
|
|
25065
25644
|
"",
|
|
25066
|
-
"Human-only commands for
|
|
25645
|
+
"Human-only commands for shared observability SQLite database.",
|
|
25067
25646
|
"",
|
|
25068
25647
|
"Commands:",
|
|
25069
|
-
" setup
|
|
25070
|
-
" init
|
|
25071
|
-
" backfill
|
|
25072
|
-
"
|
|
25648
|
+
" setup Provision database file + schema + .gitignore entries",
|
|
25649
|
+
" init Alias for setup",
|
|
25650
|
+
" backfill [--events] Import historical .specialists/jobs/*/status.json rows",
|
|
25651
|
+
" vacuum Run SQLite VACUUM (refuses when running/starting jobs exist)",
|
|
25652
|
+
" prune --before <iso|duration> Prune old rows (default dry-run)",
|
|
25653
|
+
" [--dry-run] [--apply] [--include-epics]",
|
|
25073
25654
|
"",
|
|
25074
25655
|
"Behavior:",
|
|
25075
|
-
" -
|
|
25076
|
-
"
|
|
25077
|
-
" -
|
|
25078
|
-
" -
|
|
25079
|
-
" - ensures .gitignore excludes .db, .db-wal, and .db-shm files under .specialists/db/",
|
|
25080
|
-
" - backfill skips jobs already present in SQLite by job_id (idempotent)",
|
|
25656
|
+
" - prune keeps specialist_events last 30 days always",
|
|
25657
|
+
" - prune removes specialist_results and terminal specialist_jobs older than --before",
|
|
25658
|
+
" - prune never touches active-chain jobs",
|
|
25659
|
+
" - prune never touches epic_runs unless --include-epics",
|
|
25081
25660
|
"",
|
|
25082
25661
|
"Examples:",
|
|
25083
25662
|
" specialists db setup",
|
|
25084
|
-
" specialists db backfill",
|
|
25085
25663
|
" specialists db backfill --events",
|
|
25086
|
-
"
|
|
25087
|
-
"
|
|
25664
|
+
" specialists db vacuum",
|
|
25665
|
+
" specialists db prune --before 30d --dry-run",
|
|
25666
|
+
" specialists db prune --before 2026-01-01T00:00:00Z --apply --include-epics",
|
|
25088
25667
|
""
|
|
25089
25668
|
].join(`
|
|
25090
25669
|
`));
|
|
@@ -25094,7 +25673,7 @@ function assertHumanInteractiveTerminal(commandName) {
|
|
|
25094
25673
|
const inAgentSession = !forceSetup && (!process.stdin.isTTY || !!process.env.SPECIALISTS_TMUX_SESSION || !!process.env.SPECIALISTS_JOB_ID || !!process.env.PI_SESSION_ID || !!process.env.PI_RPC_SOCKET);
|
|
25095
25674
|
if (!inAgentSession)
|
|
25096
25675
|
return;
|
|
25097
|
-
console.error(`specialists db ${commandName} requires
|
|
25676
|
+
console.error(`specialists db ${commandName} requires interactive terminal. user-only setup command.`);
|
|
25098
25677
|
process.exit(1);
|
|
25099
25678
|
}
|
|
25100
25679
|
function printSetupResult(created, gitignoreUpdated, location) {
|
|
@@ -25123,9 +25702,48 @@ function parseBackfillOptions(argv) {
|
|
|
25123
25702
|
}
|
|
25124
25703
|
return { importEvents };
|
|
25125
25704
|
}
|
|
25705
|
+
function parsePruneOptions(argv) {
|
|
25706
|
+
let beforeValue = null;
|
|
25707
|
+
let apply = false;
|
|
25708
|
+
let dryRun = true;
|
|
25709
|
+
let includeEpics = false;
|
|
25710
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
25711
|
+
const argument = argv[index];
|
|
25712
|
+
if (argument === "--before") {
|
|
25713
|
+
const value = argv[index + 1];
|
|
25714
|
+
if (!value)
|
|
25715
|
+
throw new Error("Missing value for --before");
|
|
25716
|
+
beforeValue = value;
|
|
25717
|
+
index += 1;
|
|
25718
|
+
continue;
|
|
25719
|
+
}
|
|
25720
|
+
if (argument === "--apply") {
|
|
25721
|
+
apply = true;
|
|
25722
|
+
dryRun = false;
|
|
25723
|
+
continue;
|
|
25724
|
+
}
|
|
25725
|
+
if (argument === "--dry-run") {
|
|
25726
|
+
dryRun = true;
|
|
25727
|
+
apply = false;
|
|
25728
|
+
continue;
|
|
25729
|
+
}
|
|
25730
|
+
if (argument === "--include-epics") {
|
|
25731
|
+
includeEpics = true;
|
|
25732
|
+
continue;
|
|
25733
|
+
}
|
|
25734
|
+
throw new Error(`Unknown option for db prune: '${argument}'`);
|
|
25735
|
+
}
|
|
25736
|
+
if (!beforeValue)
|
|
25737
|
+
throw new Error("Missing required --before for db prune");
|
|
25738
|
+
return {
|
|
25739
|
+
beforeMs: parseBeforeArgument(beforeValue),
|
|
25740
|
+
apply: apply && !dryRun,
|
|
25741
|
+
includeEpics
|
|
25742
|
+
};
|
|
25743
|
+
}
|
|
25126
25744
|
function parseStatusFile(jobDirectoryPath, fallbackJobId) {
|
|
25127
25745
|
const statusPath = join11(jobDirectoryPath, "status.json");
|
|
25128
|
-
const statusRaw =
|
|
25746
|
+
const statusRaw = readFileSync8(statusPath, "utf-8");
|
|
25129
25747
|
const parsed = JSON.parse(statusRaw);
|
|
25130
25748
|
const jobId = typeof parsed.id === "string" && parsed.id.length > 0 ? parsed.id : fallbackJobId;
|
|
25131
25749
|
const specialist = typeof parsed.specialist === "string" && parsed.specialist.length > 0 ? parsed.specialist : "unknown";
|
|
@@ -25142,7 +25760,7 @@ function parseStatusFile(jobDirectoryPath, fallbackJobId) {
|
|
|
25142
25760
|
function replayEvents(eventsPath, sqliteClient, status) {
|
|
25143
25761
|
if (!existsSync10(eventsPath))
|
|
25144
25762
|
return 0;
|
|
25145
|
-
const rawContent =
|
|
25763
|
+
const rawContent = readFileSync8(eventsPath, "utf-8");
|
|
25146
25764
|
const lines = rawContent.split(`
|
|
25147
25765
|
`).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
25148
25766
|
let importedEvents = 0;
|
|
@@ -25239,6 +25857,231 @@ ${bold7("specialists db backfill")}
|
|
|
25239
25857
|
}
|
|
25240
25858
|
console.log("");
|
|
25241
25859
|
}
|
|
25860
|
+
function runVacuum() {
|
|
25861
|
+
const sqliteClient = createObservabilitySqliteClient();
|
|
25862
|
+
if (!sqliteClient) {
|
|
25863
|
+
throw new Error("Failed to initialize observability SQLite schema. Run `specialists db setup` first and ensure sqlite3 is installed.");
|
|
25864
|
+
}
|
|
25865
|
+
try {
|
|
25866
|
+
const activeJobs = sqliteClient.listActiveJobs(["running", "starting"]);
|
|
25867
|
+
if (activeJobs.length > 0) {
|
|
25868
|
+
const listing = activeJobs.slice(0, 5).map((job) => `${job.job_id}:${job.status}`).join(", ");
|
|
25869
|
+
throw new Error(`Refusing vacuum while active jobs exist (${activeJobs.length}): ${listing}`);
|
|
25870
|
+
}
|
|
25871
|
+
const { beforeBytes, afterBytes } = sqliteClient.vacuumDatabase();
|
|
25872
|
+
const savedBytes = Math.max(0, beforeBytes - afterBytes);
|
|
25873
|
+
console.log(`
|
|
25874
|
+
${bold7("specialists db vacuum")}
|
|
25875
|
+
`);
|
|
25876
|
+
console.log(` ${green5("\u2713")} before: ${formatBytes(beforeBytes)} (${beforeBytes} bytes)`);
|
|
25877
|
+
console.log(` ${green5("\u2713")} after: ${formatBytes(afterBytes)} (${afterBytes} bytes)`);
|
|
25878
|
+
console.log(` ${green5("\u2713")} saved: ${formatBytes(savedBytes)} (${savedBytes} bytes)`);
|
|
25879
|
+
console.log("");
|
|
25880
|
+
} finally {
|
|
25881
|
+
sqliteClient.close();
|
|
25882
|
+
}
|
|
25883
|
+
}
|
|
25884
|
+
function runPrune(options) {
|
|
25885
|
+
const sqliteClient = createObservabilitySqliteClient();
|
|
25886
|
+
if (!sqliteClient) {
|
|
25887
|
+
throw new Error("Failed to initialize observability SQLite schema. Run `specialists db setup` first and ensure sqlite3 is installed.");
|
|
25888
|
+
}
|
|
25889
|
+
try {
|
|
25890
|
+
const report = sqliteClient.pruneObservabilityData({
|
|
25891
|
+
beforeMs: options.beforeMs,
|
|
25892
|
+
includeEpics: options.includeEpics,
|
|
25893
|
+
apply: options.apply
|
|
25894
|
+
});
|
|
25895
|
+
console.log(`
|
|
25896
|
+
${bold7("specialists db prune")}
|
|
25897
|
+
`);
|
|
25898
|
+
console.log(` ${report.dryRun ? yellow6("\u25CB dry-run") : green5("\u2713 applied")}`);
|
|
25899
|
+
console.log(` ${green5("\u2713")} before: ${new Date(report.beforeMs).toISOString()}`);
|
|
25900
|
+
console.log(` ${green5("\u2713")} events cutoff (fixed 30d): ${new Date(report.eventsCutoffMs).toISOString()}`);
|
|
25901
|
+
console.log(` ${green5("\u2713")} specialist_events: ${report.deletedEvents}`);
|
|
25902
|
+
console.log(` ${green5("\u2713")} specialist_results: ${report.deletedResults}`);
|
|
25903
|
+
console.log(` ${green5("\u2713")} specialist_jobs: ${report.deletedJobs}`);
|
|
25904
|
+
console.log(` ${report.includeEpics ? green5("\u2713") : yellow6("\u25CB")} epic_runs: ${report.deletedEpicRuns} ${report.includeEpics ? "" : "(skipped, use --include-epics)"}`);
|
|
25905
|
+
console.log(` ${yellow6("\u25CB")} skipped active-chain jobs: ${report.skippedActiveChainJobs}`);
|
|
25906
|
+
console.log("");
|
|
25907
|
+
} finally {
|
|
25908
|
+
sqliteClient.close();
|
|
25909
|
+
}
|
|
25910
|
+
}
|
|
25911
|
+
function parseBenchmarkExportOptions(argv) {
|
|
25912
|
+
const defaultOutput = resolve5(process.cwd(), ".specialists/benchmarks/executor-benchmark-rows.jsonl");
|
|
25913
|
+
let outputPath = defaultOutput;
|
|
25914
|
+
let epicId;
|
|
25915
|
+
let includePrepJobs = false;
|
|
25916
|
+
for (let i = 0;i < argv.length; i += 1) {
|
|
25917
|
+
const argument = argv[i];
|
|
25918
|
+
if (argument === "--output" && argv[i + 1]) {
|
|
25919
|
+
outputPath = resolve5(process.cwd(), argv[i + 1]);
|
|
25920
|
+
i += 1;
|
|
25921
|
+
continue;
|
|
25922
|
+
}
|
|
25923
|
+
if (argument === "--epic" && argv[i + 1]) {
|
|
25924
|
+
epicId = argv[i + 1];
|
|
25925
|
+
i += 1;
|
|
25926
|
+
continue;
|
|
25927
|
+
}
|
|
25928
|
+
if (argument === "--include-prep") {
|
|
25929
|
+
includePrepJobs = true;
|
|
25930
|
+
continue;
|
|
25931
|
+
}
|
|
25932
|
+
throw new Error(`Unknown option for db benchmark-export: '${argument}'`);
|
|
25933
|
+
}
|
|
25934
|
+
return { outputPath, epicId, includePrepJobs };
|
|
25935
|
+
}
|
|
25936
|
+
function parseReviewerVerdict2(output2) {
|
|
25937
|
+
if (!output2)
|
|
25938
|
+
return "MISSING";
|
|
25939
|
+
const match = output2.match(/Verdict:\s*(PASS|PARTIAL|FAIL)/i);
|
|
25940
|
+
if (!match?.[1])
|
|
25941
|
+
return "MISSING";
|
|
25942
|
+
return match[1].toUpperCase();
|
|
25943
|
+
}
|
|
25944
|
+
function parseReviewerScore(output2) {
|
|
25945
|
+
if (!output2)
|
|
25946
|
+
return null;
|
|
25947
|
+
const match = output2.match(/(?:Reviewer\s+)?Score(?:\s*\(0-100\))?\s*[:=]\s*(\d+(?:\.\d+)?)/i);
|
|
25948
|
+
return match?.[1] ? Number(match[1]) : null;
|
|
25949
|
+
}
|
|
25950
|
+
function parseGateResult(output2, key) {
|
|
25951
|
+
if (!output2)
|
|
25952
|
+
return null;
|
|
25953
|
+
const regex = key === "lint" ? /(?:lint_pass|lint)\s*[:=]\s*(true|false|pass|fail)/i : /(?:tsc_pass|tsc(?:\s*--noEmit)?)\s*[:=]\s*(true|false|pass|fail)/i;
|
|
25954
|
+
const match = output2.match(regex);
|
|
25955
|
+
if (!match?.[1])
|
|
25956
|
+
return null;
|
|
25957
|
+
const normalized = match[1].toLowerCase();
|
|
25958
|
+
return normalized === "true" || normalized === "pass";
|
|
25959
|
+
}
|
|
25960
|
+
function readLatestRunCompleteEvent(events) {
|
|
25961
|
+
for (let index = events.length - 1;index >= 0; index -= 1) {
|
|
25962
|
+
const event = events[index];
|
|
25963
|
+
if (event?.type === "run_complete") {
|
|
25964
|
+
return event;
|
|
25965
|
+
}
|
|
25966
|
+
}
|
|
25967
|
+
return null;
|
|
25968
|
+
}
|
|
25969
|
+
function inferFailureNotes(input2) {
|
|
25970
|
+
const notes = [];
|
|
25971
|
+
if (input2.runComplete?.status === "ERROR") {
|
|
25972
|
+
notes.push(`run_complete_status=ERROR${input2.runComplete.error ? `: ${input2.runComplete.error}` : ""}`);
|
|
25973
|
+
}
|
|
25974
|
+
if (input2.runComplete?.status === "CANCELLED") {
|
|
25975
|
+
notes.push("run_complete_status=CANCELLED");
|
|
25976
|
+
}
|
|
25977
|
+
if (input2.runComplete?.exit_reason) {
|
|
25978
|
+
notes.push(`exit_reason=${input2.runComplete.exit_reason}`);
|
|
25979
|
+
}
|
|
25980
|
+
if (input2.runComplete?.finish_reason) {
|
|
25981
|
+
notes.push(`finish_reason=${input2.runComplete.finish_reason}`);
|
|
25982
|
+
}
|
|
25983
|
+
if (input2.status.error) {
|
|
25984
|
+
notes.push(`status_error=${input2.status.error}`);
|
|
25985
|
+
}
|
|
25986
|
+
if (input2.reviewerVerdict !== "PASS" && input2.hasLaterExecutorInChain) {
|
|
25987
|
+
notes.push("fix_loop_rerun_detected_after_non_pass_review");
|
|
25988
|
+
}
|
|
25989
|
+
if (!input2.runComplete) {
|
|
25990
|
+
notes.push("missing_run_complete_event_fallback_to_status_metrics");
|
|
25991
|
+
}
|
|
25992
|
+
return notes;
|
|
25993
|
+
}
|
|
25994
|
+
function runBenchmarkExport(options) {
|
|
25995
|
+
const sqliteClient = createObservabilitySqliteClient();
|
|
25996
|
+
if (!sqliteClient) {
|
|
25997
|
+
throw new Error("Failed to initialize observability SQLite schema. Run `specialists db setup` first and ensure sqlite3 is installed.");
|
|
25998
|
+
}
|
|
25999
|
+
try {
|
|
26000
|
+
const statuses = sqliteClient.listStatuses().filter((status) => options.includePrepJobs || status.chain_kind === "chain").filter((status) => !options.epicId || status.epic_id === options.epicId);
|
|
26001
|
+
const byChain = new Map;
|
|
26002
|
+
for (const status of statuses) {
|
|
26003
|
+
const chainId = status.chain_id ?? `job:${status.id}`;
|
|
26004
|
+
const group = byChain.get(chainId) ?? [];
|
|
26005
|
+
group.push(status);
|
|
26006
|
+
byChain.set(chainId, group);
|
|
26007
|
+
}
|
|
26008
|
+
const rows = [];
|
|
26009
|
+
for (const chainStatuses of byChain.values()) {
|
|
26010
|
+
const ordered = [...chainStatuses].sort((a, b) => a.started_at_ms - b.started_at_ms);
|
|
26011
|
+
const executorStatuses = ordered.filter((status) => status.specialist === "executor");
|
|
26012
|
+
const reviewerStatuses = ordered.filter((status) => status.specialist === "reviewer");
|
|
26013
|
+
executorStatuses.forEach((executorStatus, executorIndex) => {
|
|
26014
|
+
const nextExecutor = executorStatuses[executorIndex + 1];
|
|
26015
|
+
const reviewer = reviewerStatuses.find((candidate) => {
|
|
26016
|
+
if (candidate.started_at_ms < executorStatus.started_at_ms)
|
|
26017
|
+
return false;
|
|
26018
|
+
if (!nextExecutor)
|
|
26019
|
+
return true;
|
|
26020
|
+
return candidate.started_at_ms < nextExecutor.started_at_ms;
|
|
26021
|
+
}) ?? null;
|
|
26022
|
+
const runComplete = readLatestRunCompleteEvent(sqliteClient.readEvents(executorStatus.id));
|
|
26023
|
+
const reviewerOutput = reviewer ? sqliteClient.readResult(reviewer.id) : null;
|
|
26024
|
+
const reviewerVerdict = parseReviewerVerdict2(reviewerOutput);
|
|
26025
|
+
const totalTokens = runComplete?.token_usage?.total_tokens ?? runComplete?.metrics?.token_usage?.total_tokens ?? executorStatus.metrics?.token_usage?.total_tokens ?? null;
|
|
26026
|
+
const costUsd = runComplete?.token_usage?.cost_usd ?? runComplete?.metrics?.token_usage?.cost_usd ?? executorStatus.metrics?.token_usage?.cost_usd ?? null;
|
|
26027
|
+
const elapsedMs = runComplete ? Math.round(runComplete.elapsed_s * 1000) : typeof executorStatus.elapsed_s === "number" ? Math.round(executorStatus.elapsed_s * 1000) : null;
|
|
26028
|
+
const hasLaterExecutorInChain = Boolean(nextExecutor);
|
|
26029
|
+
const failureNotes = inferFailureNotes({
|
|
26030
|
+
status: executorStatus,
|
|
26031
|
+
runComplete,
|
|
26032
|
+
reviewerVerdict,
|
|
26033
|
+
hasLaterExecutorInChain
|
|
26034
|
+
});
|
|
26035
|
+
rows.push({
|
|
26036
|
+
task_id: executorStatus.chain_root_bead_id ?? executorStatus.bead_id ?? "unknown_task",
|
|
26037
|
+
model_id: executorStatus.model ?? null,
|
|
26038
|
+
executor_job_id: executorStatus.id,
|
|
26039
|
+
reviewer_job_id: reviewer?.id ?? null,
|
|
26040
|
+
lint_pass: parseGateResult(reviewerOutput, "lint"),
|
|
26041
|
+
tsc_pass: parseGateResult(reviewerOutput, "tsc"),
|
|
26042
|
+
reviewer_verdict: reviewerVerdict,
|
|
26043
|
+
reviewer_score_if_present: parseReviewerScore(reviewerOutput),
|
|
26044
|
+
total_tokens: totalTokens,
|
|
26045
|
+
cost_usd: costUsd,
|
|
26046
|
+
elapsed_ms: elapsedMs,
|
|
26047
|
+
failure_notes: failureNotes,
|
|
26048
|
+
source_of_truth: {
|
|
26049
|
+
task_id: "specialist_jobs.chain_root_bead_id fallback bead_id",
|
|
26050
|
+
model_id: "specialist_jobs.status_json.model",
|
|
26051
|
+
executor_job_id: "specialist_jobs.job_id",
|
|
26052
|
+
reviewer_job_id: "specialist_jobs.job_id where specialist=reviewer in same chain window",
|
|
26053
|
+
lint_pass: "reviewer specialist_results.output regex parse; null when absent",
|
|
26054
|
+
tsc_pass: "reviewer specialist_results.output regex parse; null when absent",
|
|
26055
|
+
reviewer_verdict: "reviewer specialist_results.output Verdict: PASS|PARTIAL|FAIL",
|
|
26056
|
+
reviewer_score_if_present: "reviewer specialist_results.output score regex; null when absent",
|
|
26057
|
+
total_tokens: runComplete ? "specialist_events.type=run_complete.token_usage.total_tokens" : "status_json.metrics.token_usage.total_tokens fallback",
|
|
26058
|
+
cost_usd: runComplete ? "specialist_events.type=run_complete.token_usage.cost_usd" : "status_json.metrics.token_usage.cost_usd fallback",
|
|
26059
|
+
elapsed_ms: runComplete ? "specialist_events.type=run_complete.elapsed_s * 1000" : "status_json.elapsed_s * 1000 fallback",
|
|
26060
|
+
failure_notes: "run_complete.error/status + status_json.error + chain sequencing heuristics"
|
|
26061
|
+
}
|
|
26062
|
+
});
|
|
26063
|
+
});
|
|
26064
|
+
}
|
|
26065
|
+
rows.sort((a, b) => a.task_id.localeCompare(b.task_id) || a.executor_job_id.localeCompare(b.executor_job_id));
|
|
26066
|
+
const outputDirectory = dirname6(options.outputPath);
|
|
26067
|
+
mkdirSync5(outputDirectory, { recursive: true });
|
|
26068
|
+
const jsonl = rows.map((row) => JSON.stringify(row)).join(`
|
|
26069
|
+
`);
|
|
26070
|
+
writeFileSync5(options.outputPath, rows.length > 0 ? `${jsonl}
|
|
26071
|
+
` : "", "utf-8");
|
|
26072
|
+
console.log(`
|
|
26073
|
+
${bold7("specialists db benchmark-export")}
|
|
26074
|
+
`);
|
|
26075
|
+
console.log(` ${green5("\u2713")} rows exported: ${rows.length}`);
|
|
26076
|
+
console.log(` ${green5("\u2713")} output: ${options.outputPath}`);
|
|
26077
|
+
if (options.epicId) {
|
|
26078
|
+
console.log(` ${green5("\u2713")} epic filter: ${options.epicId}`);
|
|
26079
|
+
}
|
|
26080
|
+
console.log("");
|
|
26081
|
+
} finally {
|
|
26082
|
+
sqliteClient.close();
|
|
26083
|
+
}
|
|
26084
|
+
}
|
|
25242
26085
|
function runSetup() {
|
|
25243
26086
|
const location = resolveObservabilityDbLocation(process.cwd());
|
|
25244
26087
|
if (isPathInsideJobsDirectory(location.dbPath, location.gitRoot)) {
|
|
@@ -25270,17 +26113,32 @@ async function run8(argv = process.argv.slice(3)) {
|
|
|
25270
26113
|
runBackfill(options);
|
|
25271
26114
|
return;
|
|
25272
26115
|
}
|
|
26116
|
+
if (subcommand === "vacuum") {
|
|
26117
|
+
runVacuum();
|
|
26118
|
+
return;
|
|
26119
|
+
}
|
|
26120
|
+
if (subcommand === "prune") {
|
|
26121
|
+
const options = parsePruneOptions(argv.slice(1));
|
|
26122
|
+
runPrune(options);
|
|
26123
|
+
return;
|
|
26124
|
+
}
|
|
26125
|
+
if (subcommand === "benchmark-export") {
|
|
26126
|
+
const options = parseBenchmarkExportOptions(argv.slice(1));
|
|
26127
|
+
runBenchmarkExport(options);
|
|
26128
|
+
return;
|
|
26129
|
+
}
|
|
25273
26130
|
console.error(`Unknown db subcommand: '${subcommand}'`);
|
|
25274
26131
|
printDbHelp();
|
|
25275
26132
|
process.exit(1);
|
|
25276
26133
|
}
|
|
25277
|
-
var bold7 = (s) => `\x1B[1m${s}\x1B[0m`, green5 = (s) => `\x1B[32m${s}\x1B[0m`, yellow6 = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
26134
|
+
var DAY_MS, bold7 = (s) => `\x1B[1m${s}\x1B[0m`, green5 = (s) => `\x1B[32m${s}\x1B[0m`, yellow6 = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
25278
26135
|
var init_db = __esm(() => {
|
|
25279
26136
|
init_observability_db();
|
|
25280
26137
|
init_job_root();
|
|
25281
26138
|
init_observability_sqlite();
|
|
25282
26139
|
init_chain_identity();
|
|
25283
26140
|
init_timeline_events();
|
|
26141
|
+
DAY_MS = 24 * 60 * 60 * 1000;
|
|
25284
26142
|
});
|
|
25285
26143
|
|
|
25286
26144
|
// src/cli/validate.ts
|
|
@@ -25410,7 +26268,7 @@ __export(exports_edit, {
|
|
|
25410
26268
|
run: () => run10
|
|
25411
26269
|
});
|
|
25412
26270
|
import { spawnSync as spawnSync10 } from "child_process";
|
|
25413
|
-
import { existsSync as existsSync12, readdirSync as readdirSync5, readFileSync as
|
|
26271
|
+
import { existsSync as existsSync12, readdirSync as readdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
|
|
25414
26272
|
import { join as join13 } from "path";
|
|
25415
26273
|
function loadPresets() {
|
|
25416
26274
|
const paths = [
|
|
@@ -25420,7 +26278,7 @@ function loadPresets() {
|
|
|
25420
26278
|
for (const p of paths) {
|
|
25421
26279
|
if (existsSync12(p)) {
|
|
25422
26280
|
try {
|
|
25423
|
-
const data = JSON.parse(
|
|
26281
|
+
const data = JSON.parse(readFileSync9(p, "utf-8"));
|
|
25424
26282
|
return data;
|
|
25425
26283
|
} catch {
|
|
25426
26284
|
return {};
|
|
@@ -25773,7 +26631,7 @@ function getRawValue(args, resolvedPath) {
|
|
|
25773
26631
|
if (!MULTILINE_FILE_PATHS.has(resolvedPath.normalizedPath)) {
|
|
25774
26632
|
fail(`Error: --file is only supported for: ${Array.from(MULTILINE_FILE_PATHS).join(", ")}`);
|
|
25775
26633
|
}
|
|
25776
|
-
return
|
|
26634
|
+
return readFileSync9(args.filePath, "utf-8");
|
|
25777
26635
|
}
|
|
25778
26636
|
function getAtPath(root, segments) {
|
|
25779
26637
|
let current = root;
|
|
@@ -25883,7 +26741,7 @@ async function run10() {
|
|
|
25883
26741
|
}
|
|
25884
26742
|
const targets2 = await resolveTargets(args);
|
|
25885
26743
|
for (const target of targets2) {
|
|
25886
|
-
const raw =
|
|
26744
|
+
const raw = readFileSync9(target.filePath, "utf-8");
|
|
25887
26745
|
const doc2 = JSON.parse(raw);
|
|
25888
26746
|
for (const [fieldPath, fieldValue] of Object.entries(preset.fields)) {
|
|
25889
26747
|
const resolved = resolvePath2(fieldPath);
|
|
@@ -25897,7 +26755,7 @@ async function run10() {
|
|
|
25897
26755
|
printDryRun(target.filePath, raw, updated);
|
|
25898
26756
|
continue;
|
|
25899
26757
|
}
|
|
25900
|
-
|
|
26758
|
+
writeFileSync6(target.filePath, updated, "utf-8");
|
|
25901
26759
|
const fieldList = Object.keys(preset.fields).map((f) => yellow8(f)).join(", ");
|
|
25902
26760
|
console.log(`${green7("\u2713")} ${bold9(target.name)}: applied preset ${bold9(args.preset)} (${fieldList})`);
|
|
25903
26761
|
}
|
|
@@ -25909,7 +26767,7 @@ async function run10() {
|
|
|
25909
26767
|
fail("Error: no specialists found");
|
|
25910
26768
|
}
|
|
25911
26769
|
for (const target of targets) {
|
|
25912
|
-
const raw =
|
|
26770
|
+
const raw = readFileSync9(target.filePath, "utf-8");
|
|
25913
26771
|
let doc2;
|
|
25914
26772
|
try {
|
|
25915
26773
|
const parsed = JSON.parse(raw);
|
|
@@ -25933,7 +26791,7 @@ async function run10() {
|
|
|
25933
26791
|
printDryRun(target.filePath, raw, updated);
|
|
25934
26792
|
continue;
|
|
25935
26793
|
}
|
|
25936
|
-
|
|
26794
|
+
writeFileSync6(target.filePath, updated, "utf-8");
|
|
25937
26795
|
console.log(`${green7("\u2713")} ${bold9(target.name)}: ${yellow8(resolvedPath.normalizedPath)} = ${formatOutputValue(nextValue)}` + dim7(` (${target.filePath})`));
|
|
25938
26796
|
}
|
|
25939
26797
|
}
|
|
@@ -26052,8 +26910,8 @@ var init_config = __esm(() => {
|
|
|
26052
26910
|
});
|
|
26053
26911
|
|
|
26054
26912
|
// src/specialist/worktree.ts
|
|
26055
|
-
import { existsSync as existsSync13, symlinkSync as symlinkSync2, mkdirSync as
|
|
26056
|
-
import { join as join14, resolve as
|
|
26913
|
+
import { existsSync as existsSync13, symlinkSync as symlinkSync2, mkdirSync as mkdirSync6 } from "fs";
|
|
26914
|
+
import { join as join14, resolve as resolve6 } from "path";
|
|
26057
26915
|
import { spawnSync as spawnSync11, execFileSync as execFileSync2 } from "child_process";
|
|
26058
26916
|
function deriveBranchName(beadId, specialistName) {
|
|
26059
26917
|
return `feature/${beadId}-${slugify(specialistName)}`;
|
|
@@ -26086,11 +26944,11 @@ function provisionWorktree(options) {
|
|
|
26086
26944
|
const branch = deriveBranchName(options.beadId, options.specialistName);
|
|
26087
26945
|
const existingPath = findExistingWorktree(branch, cwd);
|
|
26088
26946
|
if (existingPath) {
|
|
26089
|
-
return { branch, worktreePath:
|
|
26947
|
+
return { branch, worktreePath: resolve6(existingPath), reused: true };
|
|
26090
26948
|
}
|
|
26091
26949
|
const worktreeBase = options.worktreeBase ?? join14(commonRoot, ".worktrees", options.beadId);
|
|
26092
26950
|
const worktreeName = deriveWorktreeName(options.beadId, options.specialistName);
|
|
26093
|
-
const worktreePath =
|
|
26951
|
+
const worktreePath = resolve6(join14(worktreeBase, worktreeName));
|
|
26094
26952
|
createWorktreeViaBd(worktreePath, branch, commonRoot);
|
|
26095
26953
|
symlinkPiNpmCache(commonRoot, worktreePath);
|
|
26096
26954
|
return { branch, worktreePath, reused: false };
|
|
@@ -26101,7 +26959,7 @@ function symlinkPiNpmCache(commonRoot, worktreePath) {
|
|
|
26101
26959
|
if (!existsSync13(source) || existsSync13(target))
|
|
26102
26960
|
return;
|
|
26103
26961
|
try {
|
|
26104
|
-
|
|
26962
|
+
mkdirSync6(join14(worktreePath, ".pi"), { recursive: true });
|
|
26105
26963
|
symlinkSync2(source, target);
|
|
26106
26964
|
} catch {}
|
|
26107
26965
|
}
|
|
@@ -26161,9 +27019,10 @@ __export(exports_merge, {
|
|
|
26161
27019
|
executePublicationPlan: () => executePublicationPlan,
|
|
26162
27020
|
evaluateMergeWorthiness: () => evaluateMergeWorthiness,
|
|
26163
27021
|
ensureTerminalJobs: () => ensureTerminalJobs,
|
|
26164
|
-
checkEpicUnresolvedGuard: () => checkEpicUnresolvedGuard
|
|
27022
|
+
checkEpicUnresolvedGuard: () => checkEpicUnresolvedGuard,
|
|
27023
|
+
assertMainRepoCleanForMerge: () => assertMainRepoCleanForMerge
|
|
26165
27024
|
});
|
|
26166
|
-
import { existsSync as existsSync14, readdirSync as readdirSync6, readFileSync as
|
|
27025
|
+
import { existsSync as existsSync14, readdirSync as readdirSync6, readFileSync as readFileSync10 } from "fs";
|
|
26167
27026
|
import { spawnSync as spawnSync12 } from "child_process";
|
|
26168
27027
|
import { join as join15 } from "path";
|
|
26169
27028
|
function parseOptions(argv) {
|
|
@@ -26353,7 +27212,7 @@ function readAllJobStatuses() {
|
|
|
26353
27212
|
const statusPath = join15(jobsDir, entry.name, "status.json");
|
|
26354
27213
|
if (!existsSync14(statusPath))
|
|
26355
27214
|
continue;
|
|
26356
|
-
const parsed = readJson(
|
|
27215
|
+
const parsed = readJson(readFileSync10(statusPath, "utf-8"));
|
|
26357
27216
|
if (!parsed || typeof parsed !== "object")
|
|
26358
27217
|
continue;
|
|
26359
27218
|
statuses.push(parsed);
|
|
@@ -26480,6 +27339,29 @@ function readChangedFilesForLastMerge(cwd = process.cwd()) {
|
|
|
26480
27339
|
return diff.stdout.split(`
|
|
26481
27340
|
`).map((line) => line.trim()).filter(Boolean);
|
|
26482
27341
|
}
|
|
27342
|
+
function isMergeDirtyIgnored(path) {
|
|
27343
|
+
return MERGE_DIRTY_IGNORE_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
27344
|
+
}
|
|
27345
|
+
function assertMainRepoCleanForMerge(cwd) {
|
|
27346
|
+
const status = runCommand("git", ["status", "--porcelain", "--untracked-files=no"], cwd);
|
|
27347
|
+
if (status.status !== 0) {
|
|
27348
|
+
throw new Error(`Unable to read git status in '${cwd}'.`);
|
|
27349
|
+
}
|
|
27350
|
+
const dirty = status.stdout.split(`
|
|
27351
|
+
`).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
27352
|
+
const match = /^..\s(.+?)(?:\s->\s(.+))?$/.exec(line);
|
|
27353
|
+
const path = match ? match[2] ?? match[1] ?? "" : line.slice(3).trim();
|
|
27354
|
+
return path.trim();
|
|
27355
|
+
}).filter((path) => path && !isMergeDirtyIgnored(path));
|
|
27356
|
+
if (dirty.length === 0)
|
|
27357
|
+
return;
|
|
27358
|
+
const list = dirty.map((path) => `- ${path}`).join(`
|
|
27359
|
+
`);
|
|
27360
|
+
throw new Error(`Refusing merge: main repo '${cwd}' has uncommitted changes that could cause spurious conflicts.
|
|
27361
|
+
` + `Dirty files (tracked, non-dist):
|
|
27362
|
+
${list}
|
|
27363
|
+
` + `Resolve by committing, stashing, or reverting these changes, then retry merge.`);
|
|
27364
|
+
}
|
|
26483
27365
|
function parseNameStatusLine(line) {
|
|
26484
27366
|
const trimmed = line.trim();
|
|
26485
27367
|
if (!trimmed)
|
|
@@ -26648,6 +27530,7 @@ function printUsageAndExit(message) {
|
|
|
26648
27530
|
}
|
|
26649
27531
|
function runMergePlan(targets, options) {
|
|
26650
27532
|
const mainRepoRoot = resolveMainWorktreeRoot();
|
|
27533
|
+
assertMainRepoCleanForMerge(mainRepoRoot);
|
|
26651
27534
|
const mergedSteps = [];
|
|
26652
27535
|
for (const target of targets) {
|
|
26653
27536
|
rebaseBranchOntoMaster(target.branch, target.worktreePath);
|
|
@@ -26740,13 +27623,17 @@ async function run12() {
|
|
|
26740
27623
|
const mergedSteps = runMergePlan(targets, { rebuild: options.rebuild });
|
|
26741
27624
|
printSummary(mergedSteps, options.rebuild);
|
|
26742
27625
|
}
|
|
26743
|
-
var TERMINAL_STATUSES, NOISE_PATH_PREFIXES;
|
|
27626
|
+
var TERMINAL_STATUSES, NOISE_PATH_PREFIXES, MERGE_DIRTY_IGNORE_PREFIXES;
|
|
26744
27627
|
var init_merge = __esm(() => {
|
|
26745
27628
|
init_job_root();
|
|
26746
27629
|
init_observability_sqlite();
|
|
26747
27630
|
init_epic_lifecycle();
|
|
26748
27631
|
TERMINAL_STATUSES = new Set(["done", "error", "cancelled"]);
|
|
26749
27632
|
NOISE_PATH_PREFIXES = [".xtrm/reports/", ".wolf/", ".specialists/jobs/"];
|
|
27633
|
+
MERGE_DIRTY_IGNORE_PREFIXES = [
|
|
27634
|
+
...NOISE_PATH_PREFIXES,
|
|
27635
|
+
"dist/"
|
|
27636
|
+
];
|
|
26750
27637
|
});
|
|
26751
27638
|
|
|
26752
27639
|
// src/cli/format-helpers.ts
|
|
@@ -26844,6 +27731,9 @@ function formatEventLine(event, options) {
|
|
|
26844
27731
|
detailParts.push(`backend=${event.backend}`);
|
|
26845
27732
|
} else if (event.type === "tool") {
|
|
26846
27733
|
detail = formatToolDetail(event);
|
|
27734
|
+
} else if (event.type === "error") {
|
|
27735
|
+
detailParts.push(`source=${event.source}`);
|
|
27736
|
+
detailParts.push(`error=${event.error_message}`);
|
|
26847
27737
|
} else if (event.type === "run_complete") {
|
|
26848
27738
|
detailParts.push(`status=${event.status}`);
|
|
26849
27739
|
detailParts.push(`elapsed=${formatElapsed(event.elapsed_s)}`);
|
|
@@ -26933,6 +27823,8 @@ function formatEventInline(event) {
|
|
|
26933
27823
|
}
|
|
26934
27824
|
case "stale_warning":
|
|
26935
27825
|
return yellow10(`[warning] ${event.reason}: ${Math.round(event.silence_ms / 1000)}s silent`);
|
|
27826
|
+
case "error":
|
|
27827
|
+
return red2(`[error] ${event.source}: ${event.error_message}`);
|
|
26936
27828
|
default:
|
|
26937
27829
|
return null;
|
|
26938
27830
|
}
|
|
@@ -26966,7 +27858,7 @@ var init_format_helpers = __esm(() => {
|
|
|
26966
27858
|
turn_summary: "TURN+",
|
|
26967
27859
|
compaction: "CMPCT",
|
|
26968
27860
|
retry: "RETRY",
|
|
26969
|
-
error: "
|
|
27861
|
+
error: "ERROR"
|
|
26970
27862
|
};
|
|
26971
27863
|
});
|
|
26972
27864
|
|
|
@@ -26976,7 +27868,7 @@ __export(exports_run, {
|
|
|
26976
27868
|
run: () => run13
|
|
26977
27869
|
});
|
|
26978
27870
|
import { join as join16 } from "path";
|
|
26979
|
-
import { readFileSync as
|
|
27871
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
26980
27872
|
import { randomBytes } from "crypto";
|
|
26981
27873
|
import { spawn as cpSpawn, execSync as execSync3 } from "child_process";
|
|
26982
27874
|
async function parseArgs6(argv) {
|
|
@@ -27091,13 +27983,13 @@ async function parseArgs6(argv) {
|
|
|
27091
27983
|
process.exit(1);
|
|
27092
27984
|
}
|
|
27093
27985
|
if (!prompt && !beadId && !process.stdin.isTTY) {
|
|
27094
|
-
prompt = await new Promise((
|
|
27986
|
+
prompt = await new Promise((resolve7) => {
|
|
27095
27987
|
let buf = "";
|
|
27096
27988
|
process.stdin.setEncoding("utf-8");
|
|
27097
27989
|
process.stdin.on("data", (chunk) => {
|
|
27098
27990
|
buf += chunk;
|
|
27099
27991
|
});
|
|
27100
|
-
process.stdin.on("end", () =>
|
|
27992
|
+
process.stdin.on("end", () => resolve7(buf.trim()));
|
|
27101
27993
|
});
|
|
27102
27994
|
}
|
|
27103
27995
|
if (!prompt && !beadId && !reuseJobId) {
|
|
@@ -27255,7 +28147,7 @@ function startEventTailer(jobId, jobsDir, mode, specialist, beadId) {
|
|
|
27255
28147
|
const drain = () => {
|
|
27256
28148
|
let content;
|
|
27257
28149
|
try {
|
|
27258
|
-
content =
|
|
28150
|
+
content = readFileSync11(eventsPath, "utf-8");
|
|
27259
28151
|
} catch {
|
|
27260
28152
|
return;
|
|
27261
28153
|
}
|
|
@@ -27311,9 +28203,26 @@ function formatFooterModel(backend, model) {
|
|
|
27311
28203
|
return model;
|
|
27312
28204
|
return model.startsWith(`${backend}/`) ? model : `${backend}/${model}`;
|
|
27313
28205
|
}
|
|
27314
|
-
function
|
|
28206
|
+
function shellQuote2(value) {
|
|
27315
28207
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
27316
28208
|
}
|
|
28209
|
+
function extractReviewedJobIdOverride(prompt) {
|
|
28210
|
+
const match = prompt.match(/(?:^|\n)\s*reviewed_job_id\s*:\s*([^\n]+)/i);
|
|
28211
|
+
const candidate = match?.[1]?.trim();
|
|
28212
|
+
return candidate ? candidate : undefined;
|
|
28213
|
+
}
|
|
28214
|
+
function buildReusedWorktreeAwarenessBlock(options) {
|
|
28215
|
+
const owner = options.worktreeOwnerJobId ?? options.reusedFromJobId;
|
|
28216
|
+
return [
|
|
28217
|
+
"## Reused workspace awareness (from --job)",
|
|
28218
|
+
`You are entering an existing worktree reused from job: ${options.reusedFromJobId}.`,
|
|
28219
|
+
`Worktree chain owner job: ${owner}.`,
|
|
28220
|
+
"Workspace may contain uncommitted edits, staged changes, generated files, or partial fixes from prior handoff steps.",
|
|
28221
|
+
"Before edits, run and inspect: git status --short --branch, git diff --stat, git diff --cached --stat.",
|
|
28222
|
+
"Treat existing tree state as real input context \u2014 do not assume clean baseline."
|
|
28223
|
+
].join(`
|
|
28224
|
+
`);
|
|
28225
|
+
}
|
|
27317
28226
|
async function run13() {
|
|
27318
28227
|
const args = await parseArgs6(process.argv.slice(3));
|
|
27319
28228
|
const loader = new SpecialistLoader;
|
|
@@ -27339,14 +28248,14 @@ async function run13() {
|
|
|
27339
28248
|
const latestPath = join16(jobsDir2, "latest");
|
|
27340
28249
|
const oldLatest = (() => {
|
|
27341
28250
|
try {
|
|
27342
|
-
return
|
|
28251
|
+
return readFileSync11(latestPath, "utf-8").trim();
|
|
27343
28252
|
} catch {
|
|
27344
28253
|
return "";
|
|
27345
28254
|
}
|
|
27346
28255
|
})();
|
|
27347
28256
|
const cwd = process.cwd();
|
|
27348
28257
|
const innerArgs = process.argv.slice(2).filter((a) => a !== "--background");
|
|
27349
|
-
const cmd = `${process.execPath} ${process.argv[1]} ${innerArgs.map(
|
|
28258
|
+
const cmd = `${process.execPath} ${process.argv[1]} ${innerArgs.map(shellQuote2).join(" ")}`;
|
|
27350
28259
|
let childPid;
|
|
27351
28260
|
if (isTmuxAvailable()) {
|
|
27352
28261
|
const suffix = randomBytes(3).toString("hex");
|
|
@@ -27367,7 +28276,7 @@ async function run13() {
|
|
|
27367
28276
|
while (Date.now() < deadline) {
|
|
27368
28277
|
await new Promise((r) => setTimeout(r, 100));
|
|
27369
28278
|
try {
|
|
27370
|
-
const current =
|
|
28279
|
+
const current = readFileSync11(latestPath, "utf-8").trim();
|
|
27371
28280
|
if (current && current !== oldLatest) {
|
|
27372
28281
|
jobId2 = current;
|
|
27373
28282
|
break;
|
|
@@ -27449,10 +28358,19 @@ async function run13() {
|
|
|
27449
28358
|
} else if (args.epicId) {
|
|
27450
28359
|
epicId = args.epicId;
|
|
27451
28360
|
}
|
|
28361
|
+
variables = {
|
|
28362
|
+
...variables ?? {},
|
|
28363
|
+
reused_worktree_awareness: ""
|
|
28364
|
+
};
|
|
27452
28365
|
if (args.reuseJobId) {
|
|
28366
|
+
const reviewedJobId = extractReviewedJobIdOverride(prompt) ?? args.reuseJobId;
|
|
27453
28367
|
variables = {
|
|
27454
28368
|
...variables ?? {},
|
|
27455
|
-
reviewed_job_id:
|
|
28369
|
+
reviewed_job_id: reviewedJobId,
|
|
28370
|
+
reused_worktree_awareness: buildReusedWorktreeAwarenessBlock({
|
|
28371
|
+
reusedFromJobId: args.reuseJobId,
|
|
28372
|
+
worktreeOwnerJobId
|
|
28373
|
+
})
|
|
27456
28374
|
};
|
|
27457
28375
|
}
|
|
27458
28376
|
if (!prompt && !effectiveBeadId) {
|
|
@@ -27613,7 +28531,7 @@ var init_node_resolve = __esm(() => {
|
|
|
27613
28531
|
});
|
|
27614
28532
|
|
|
27615
28533
|
// src/specialist/job-control.ts
|
|
27616
|
-
import { existsSync as existsSync15, readFileSync as
|
|
28534
|
+
import { existsSync as existsSync15, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
|
|
27617
28535
|
import { join as join17 } from "path";
|
|
27618
28536
|
|
|
27619
28537
|
class JobControl {
|
|
@@ -27645,8 +28563,8 @@ class JobControl {
|
|
|
27645
28563
|
}
|
|
27646
28564
|
};
|
|
27647
28565
|
let resolveJobId;
|
|
27648
|
-
const jobIdPromise = new Promise((
|
|
27649
|
-
resolveJobId =
|
|
28566
|
+
const jobIdPromise = new Promise((resolve7) => {
|
|
28567
|
+
resolveJobId = resolve7;
|
|
27650
28568
|
});
|
|
27651
28569
|
this.supervisor = new Supervisor({
|
|
27652
28570
|
runner: this.runner,
|
|
@@ -27692,7 +28610,7 @@ class JobControl {
|
|
|
27692
28610
|
if (!existsSync15(resultPath))
|
|
27693
28611
|
return null;
|
|
27694
28612
|
try {
|
|
27695
|
-
return
|
|
28613
|
+
return readFileSync12(resultPath, "utf-8");
|
|
27696
28614
|
} catch {
|
|
27697
28615
|
return null;
|
|
27698
28616
|
}
|
|
@@ -27711,7 +28629,7 @@ class JobControl {
|
|
|
27711
28629
|
if (deadline !== undefined && Date.now() >= deadline) {
|
|
27712
28630
|
throw new Error(`Timed out waiting for terminal status for job ${jobId}`);
|
|
27713
28631
|
}
|
|
27714
|
-
await new Promise((
|
|
28632
|
+
await new Promise((resolve7) => setTimeout(resolve7, backoffMs));
|
|
27715
28633
|
backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
|
|
27716
28634
|
}
|
|
27717
28635
|
}
|
|
@@ -27725,7 +28643,7 @@ class JobControl {
|
|
|
27725
28643
|
}
|
|
27726
28644
|
const jsonLine = `${JSON.stringify(payload)}
|
|
27727
28645
|
`;
|
|
27728
|
-
|
|
28646
|
+
writeFileSync7(status.fifo_path, jsonLine, { flag: "a" });
|
|
27729
28647
|
}
|
|
27730
28648
|
resultPath(jobId) {
|
|
27731
28649
|
return join17(this.jobsDir, jobId, "result.txt");
|
|
@@ -27890,7 +28808,7 @@ function hashOutput(output2, salt) {
|
|
|
27890
28808
|
return createHash3("sha256").update(value).digest("hex");
|
|
27891
28809
|
}
|
|
27892
28810
|
function sleep2(ms) {
|
|
27893
|
-
return new Promise((
|
|
28811
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
27894
28812
|
}
|
|
27895
28813
|
function toContextHealth(contextPct) {
|
|
27896
28814
|
if (contextPct === null)
|
|
@@ -29803,10 +30721,10 @@ var exports_node = {};
|
|
|
29803
30721
|
__export(exports_node, {
|
|
29804
30722
|
handleNodeCommand: () => handleNodeCommand
|
|
29805
30723
|
});
|
|
29806
|
-
import { existsSync as existsSync16, readFileSync as
|
|
30724
|
+
import { existsSync as existsSync16, readFileSync as readFileSync13, readdirSync as readdirSync7 } from "fs";
|
|
29807
30725
|
import { randomUUID } from "crypto";
|
|
29808
30726
|
import { spawnSync as spawnSync14 } from "child_process";
|
|
29809
|
-
import { basename as basename4, join as join18, resolve as
|
|
30727
|
+
import { basename as basename4, join as join18, resolve as resolve7 } from "path";
|
|
29810
30728
|
function parseNodeArgs(argv) {
|
|
29811
30729
|
const command = argv[0];
|
|
29812
30730
|
const supportedCommands = new Set(["run", "list", "promote", "members", "memory", "stop", "spawn-member", "create-bead", "complete", "wait-phase"]);
|
|
@@ -29990,7 +30908,7 @@ function toNodeName(filePath) {
|
|
|
29990
30908
|
function discoverNodeConfigs(cwd) {
|
|
29991
30909
|
const discoveredByName = new Map;
|
|
29992
30910
|
for (const directory of NODE_DISCOVERY_DIRS) {
|
|
29993
|
-
const absoluteDir =
|
|
30911
|
+
const absoluteDir = resolve7(cwd, directory.path);
|
|
29994
30912
|
if (!existsSync16(absoluteDir))
|
|
29995
30913
|
continue;
|
|
29996
30914
|
const files = readdirSync7(absoluteDir).filter((fileName) => fileName.endsWith(NODE_CONFIG_SUFFIX));
|
|
@@ -30005,7 +30923,7 @@ function discoverNodeConfigs(cwd) {
|
|
|
30005
30923
|
return [...discoveredByName.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
30006
30924
|
}
|
|
30007
30925
|
function resolveNodeConfigPath(cwd, input2) {
|
|
30008
|
-
const explicitPath =
|
|
30926
|
+
const explicitPath = resolve7(cwd, input2);
|
|
30009
30927
|
if (existsSync16(explicitPath)) {
|
|
30010
30928
|
return explicitPath;
|
|
30011
30929
|
}
|
|
@@ -30101,7 +31019,7 @@ async function handleNodeRun(args) {
|
|
|
30101
31019
|
throw new Error("Observability SQLite DB is unavailable. Run: specialists db setup");
|
|
30102
31020
|
}
|
|
30103
31021
|
try {
|
|
30104
|
-
const rawConfig = args.inlineJson ? args.inlineJson :
|
|
31022
|
+
const rawConfig = args.inlineJson ? args.inlineJson : readFileSync13(resolveNodeConfigPath(process.cwd(), args.nodeConfigInput), "utf-8");
|
|
30105
31023
|
const config2 = parseNodeConfig(rawConfig);
|
|
30106
31024
|
const loader = new SpecialistLoader;
|
|
30107
31025
|
const runner = new SpecialistRunner({
|
|
@@ -30550,7 +31468,7 @@ async function handleNodeAction(args) {
|
|
|
30550
31468
|
if (deadline !== null && Date.now() >= deadline) {
|
|
30551
31469
|
throw new Error(`Timed out waiting for phase '${args.phaseId}' members: ${memberKeys.join(", ")}`);
|
|
30552
31470
|
}
|
|
30553
|
-
await new Promise((
|
|
31471
|
+
await new Promise((resolve8) => setTimeout(resolve8, 500));
|
|
30554
31472
|
}
|
|
30555
31473
|
} finally {
|
|
30556
31474
|
sqliteClient.close();
|
|
@@ -30608,42 +31526,255 @@ async function handleNodeCommand(argv) {
|
|
|
30608
31526
|
await handleNodeMemory(parsed);
|
|
30609
31527
|
return;
|
|
30610
31528
|
}
|
|
30611
|
-
if (parsed.command === "stop") {
|
|
30612
|
-
await handleNodeStop(parsed);
|
|
30613
|
-
return;
|
|
31529
|
+
if (parsed.command === "stop") {
|
|
31530
|
+
await handleNodeStop(parsed);
|
|
31531
|
+
return;
|
|
31532
|
+
}
|
|
31533
|
+
if (parsed.command === "spawn-member" || parsed.command === "create-bead" || parsed.command === "complete" || parsed.command === "wait-phase") {
|
|
31534
|
+
await handleNodeAction(parsed);
|
|
31535
|
+
return;
|
|
31536
|
+
}
|
|
31537
|
+
emitNodeCommandError(`Unsupported node command: ${parsed.command}`, parsed.jsonMode);
|
|
31538
|
+
}
|
|
31539
|
+
var NODE_CONFIG_SUFFIX = ".node.json", NODE_DISCOVERY_DIRS;
|
|
31540
|
+
var init_node = __esm(() => {
|
|
31541
|
+
init_loader();
|
|
31542
|
+
init_runner();
|
|
31543
|
+
init_circuitBreaker();
|
|
31544
|
+
init_hooks();
|
|
31545
|
+
init_observability_sqlite();
|
|
31546
|
+
init_beads();
|
|
31547
|
+
init_supervisor();
|
|
31548
|
+
init_job_root();
|
|
31549
|
+
init_node_resolve();
|
|
31550
|
+
init_node_supervisor();
|
|
31551
|
+
NODE_DISCOVERY_DIRS = [
|
|
31552
|
+
{ path: ".specialists/default/nodes", source: "default" },
|
|
31553
|
+
{ path: "config/nodes", source: "project" }
|
|
31554
|
+
];
|
|
31555
|
+
});
|
|
31556
|
+
|
|
31557
|
+
// src/specialist/epic-reconciler.ts
|
|
31558
|
+
import { mkdirSync as mkdirSync7, openSync as openSync2, readFileSync as readFileSync14, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "fs";
|
|
31559
|
+
import { join as join19 } from "path";
|
|
31560
|
+
function buildEpicLockPath(epicId) {
|
|
31561
|
+
const location = resolveObservabilityDbLocation();
|
|
31562
|
+
const lockDir = join19(location.dbDirectory, "locks");
|
|
31563
|
+
mkdirSync7(lockDir, { recursive: true });
|
|
31564
|
+
return join19(lockDir, `epic-${epicId}.lock`);
|
|
31565
|
+
}
|
|
31566
|
+
function withEpicAdvisoryLock(epicId, action) {
|
|
31567
|
+
const lockPath = buildEpicLockPath(epicId);
|
|
31568
|
+
let lockFd = null;
|
|
31569
|
+
try {
|
|
31570
|
+
lockFd = openSync2(lockPath, "wx");
|
|
31571
|
+
writeFileSync8(lockPath, JSON.stringify({ epic_id: epicId, pid: process.pid, created_at_ms: Date.now() }));
|
|
31572
|
+
} catch {
|
|
31573
|
+
let holder = "unknown";
|
|
31574
|
+
try {
|
|
31575
|
+
holder = readFileSync14(lockPath, "utf-8");
|
|
31576
|
+
} catch {
|
|
31577
|
+
holder = "unknown";
|
|
31578
|
+
}
|
|
31579
|
+
throw new Error(`Epic advisory lock busy for ${epicId}. Holder: ${holder}`);
|
|
31580
|
+
}
|
|
31581
|
+
try {
|
|
31582
|
+
return action();
|
|
31583
|
+
} finally {
|
|
31584
|
+
if (lockFd !== null) {
|
|
31585
|
+
try {
|
|
31586
|
+
rmSync2(lockPath, { force: true });
|
|
31587
|
+
} catch {}
|
|
31588
|
+
}
|
|
31589
|
+
}
|
|
31590
|
+
}
|
|
31591
|
+
function hasRedirectMarkers(statusJson) {
|
|
31592
|
+
if (!statusJson)
|
|
31593
|
+
return false;
|
|
31594
|
+
try {
|
|
31595
|
+
const parsed = JSON.parse(statusJson);
|
|
31596
|
+
return Object.keys(parsed).some((key) => key.toLowerCase().includes("redirect"));
|
|
31597
|
+
} catch {
|
|
31598
|
+
return false;
|
|
31599
|
+
}
|
|
31600
|
+
}
|
|
31601
|
+
function clearRedirectMarkers(statusJson) {
|
|
31602
|
+
if (!statusJson)
|
|
31603
|
+
return statusJson;
|
|
31604
|
+
try {
|
|
31605
|
+
const parsed = JSON.parse(statusJson);
|
|
31606
|
+
const cleanedEntries = Object.entries(parsed).filter(([key]) => !key.toLowerCase().includes("redirect"));
|
|
31607
|
+
return JSON.stringify(Object.fromEntries(cleanedEntries));
|
|
31608
|
+
} catch {
|
|
31609
|
+
return statusJson;
|
|
31610
|
+
}
|
|
31611
|
+
}
|
|
31612
|
+
function collectEpicJobs(sqlite, epicId) {
|
|
31613
|
+
const chainIds = new Set(sqlite.listEpicChains(epicId).map((chain) => chain.chain_id));
|
|
31614
|
+
return sqlite.listStatuses().filter((status) => status.epic_id === epicId || (status.chain_id ? chainIds.has(status.chain_id) : false));
|
|
31615
|
+
}
|
|
31616
|
+
function detectStaleChainRefs(sqlite, epicId) {
|
|
31617
|
+
return sqlite.listEpicChains(epicId).map((chain) => chain.chain_id).filter((chainId) => sqlite.listChainJobIds(chainId).length === 0);
|
|
31618
|
+
}
|
|
31619
|
+
function detectDeadBlockingJobs(jobs) {
|
|
31620
|
+
return jobs.filter((job) => ACTIVE_JOB_STATUSES2.has(job.status) && isJobDead(job)).map((job) => job.id);
|
|
31621
|
+
}
|
|
31622
|
+
function detectIntegrityFlags(sqlite, epicId, jobs) {
|
|
31623
|
+
const chainIds = new Set(sqlite.listEpicChains(epicId).map((chain) => chain.chain_id));
|
|
31624
|
+
const flags = [];
|
|
31625
|
+
for (const job of jobs) {
|
|
31626
|
+
if (job.chain_kind === "chain" && !job.chain_id) {
|
|
31627
|
+
flags.push(`job:${job.id}:chain_kind=chain missing chain_id`);
|
|
31628
|
+
}
|
|
31629
|
+
if (job.chain_id && !chainIds.has(job.chain_id) && job.epic_id === epicId) {
|
|
31630
|
+
flags.push(`job:${job.id}:references chain ${job.chain_id} missing from epic membership`);
|
|
31631
|
+
}
|
|
31632
|
+
if (job.chain_id && chainIds.has(job.chain_id) && job.epic_id && job.epic_id !== epicId) {
|
|
31633
|
+
flags.push(`job:${job.id}:chain ${job.chain_id} linked to epic ${job.epic_id}, expected ${epicId}`);
|
|
31634
|
+
}
|
|
31635
|
+
}
|
|
31636
|
+
return flags;
|
|
31637
|
+
}
|
|
31638
|
+
function markDeadJobsAsError(sqlite, jobs) {
|
|
31639
|
+
const deadBlockingIds = detectDeadBlockingJobs(jobs);
|
|
31640
|
+
const now = Date.now();
|
|
31641
|
+
for (const jobId of deadBlockingIds) {
|
|
31642
|
+
const current = jobs.find((job) => job.id === jobId);
|
|
31643
|
+
if (!current)
|
|
31644
|
+
continue;
|
|
31645
|
+
sqlite.upsertStatus({
|
|
31646
|
+
...current,
|
|
31647
|
+
status: "error",
|
|
31648
|
+
error: current.error ?? "epic reconciler: detected dead pid/tmux for active job",
|
|
31649
|
+
last_event_at_ms: now
|
|
31650
|
+
});
|
|
31651
|
+
}
|
|
31652
|
+
return deadBlockingIds;
|
|
31653
|
+
}
|
|
31654
|
+
function syncEpicState(sqlite, epicId, apply) {
|
|
31655
|
+
const epicRun = sqlite.readEpicRun(epicId);
|
|
31656
|
+
const jobs = collectEpicJobs(sqlite, epicId);
|
|
31657
|
+
const readinessBefore = loadEpicReadinessSummary(sqlite, epicId);
|
|
31658
|
+
const drift = {
|
|
31659
|
+
stale_chain_refs: detectStaleChainRefs(sqlite, epicId),
|
|
31660
|
+
dead_jobs_blocking_readiness: detectDeadBlockingJobs(jobs),
|
|
31661
|
+
integrity_flags: detectIntegrityFlags(sqlite, epicId, jobs),
|
|
31662
|
+
stale_redirect_markers: epicRun && hasRedirectMarkers(epicRun.status_json) ? [epicId] : []
|
|
31663
|
+
};
|
|
31664
|
+
let deadJobsMarkedError = [];
|
|
31665
|
+
let staleChainRefsPruned = [];
|
|
31666
|
+
let readinessResynced = false;
|
|
31667
|
+
let redirectMarkersCleared = false;
|
|
31668
|
+
if (apply) {
|
|
31669
|
+
if (drift.dead_jobs_blocking_readiness.length > 0) {
|
|
31670
|
+
deadJobsMarkedError = markDeadJobsAsError(sqlite, jobs);
|
|
31671
|
+
}
|
|
31672
|
+
if (drift.stale_chain_refs.length > 0) {
|
|
31673
|
+
staleChainRefsPruned = sqlite.deleteEpicChainMembership(epicId, drift.stale_chain_refs);
|
|
31674
|
+
}
|
|
31675
|
+
const readinessNext = loadEpicReadinessSummary(sqlite, epicId);
|
|
31676
|
+
const synced = syncEpicStateFromReadiness(sqlite, readinessNext);
|
|
31677
|
+
readinessResynced = synced.status !== readinessNext.persisted_state;
|
|
31678
|
+
if (epicRun && drift.stale_redirect_markers.length > 0) {
|
|
31679
|
+
const cleaned = clearRedirectMarkers(epicRun.status_json);
|
|
31680
|
+
if (cleaned && cleaned !== epicRun.status_json) {
|
|
31681
|
+
sqlite.upsertEpicRun({
|
|
31682
|
+
...epicRun,
|
|
31683
|
+
status_json: cleaned,
|
|
31684
|
+
updated_at_ms: Date.now()
|
|
31685
|
+
});
|
|
31686
|
+
redirectMarkersCleared = true;
|
|
31687
|
+
}
|
|
31688
|
+
}
|
|
31689
|
+
}
|
|
31690
|
+
const readinessAfter = loadEpicReadinessSummary(sqlite, epicId);
|
|
31691
|
+
return {
|
|
31692
|
+
epic_id: epicId,
|
|
31693
|
+
apply,
|
|
31694
|
+
drift,
|
|
31695
|
+
repairs: {
|
|
31696
|
+
dead_jobs_marked_error: deadJobsMarkedError,
|
|
31697
|
+
stale_chain_refs_pruned: staleChainRefsPruned,
|
|
31698
|
+
readiness_resynced: readinessResynced,
|
|
31699
|
+
redirect_markers_cleared: redirectMarkersCleared
|
|
31700
|
+
},
|
|
31701
|
+
readiness_before: readinessBefore,
|
|
31702
|
+
readiness_after: readinessAfter
|
|
31703
|
+
};
|
|
31704
|
+
}
|
|
31705
|
+
function listLiveMemberJobIds(sqlite, epicId) {
|
|
31706
|
+
return collectEpicJobs(sqlite, epicId).filter((job) => ACTIVE_JOB_STATUSES2.has(job.status) && !isJobDead(job)).map((job) => job.id);
|
|
31707
|
+
}
|
|
31708
|
+
function buildAbandonedStatusJson(epic, epicId, fromState, reason, forced) {
|
|
31709
|
+
const base = appendEpicTransitionAudit(epic?.status_json, {
|
|
31710
|
+
from: fromState,
|
|
31711
|
+
to: "abandoned",
|
|
31712
|
+
at_ms: Date.now(),
|
|
31713
|
+
reason,
|
|
31714
|
+
trigger: "sp epic abandon",
|
|
31715
|
+
forced
|
|
31716
|
+
});
|
|
31717
|
+
try {
|
|
31718
|
+
const parsed = JSON.parse(base);
|
|
31719
|
+
return JSON.stringify({
|
|
31720
|
+
...parsed,
|
|
31721
|
+
epic_id: epicId,
|
|
31722
|
+
status: "abandoned",
|
|
31723
|
+
reason,
|
|
31724
|
+
forced
|
|
31725
|
+
});
|
|
31726
|
+
} catch {
|
|
31727
|
+
return base;
|
|
31728
|
+
}
|
|
31729
|
+
}
|
|
31730
|
+
function abandonEpic(sqlite, epicId, reason, force) {
|
|
31731
|
+
const epic = sqlite.readEpicRun(epicId);
|
|
31732
|
+
const fromState = epic?.status ?? "open";
|
|
31733
|
+
if (fromState === "merged") {
|
|
31734
|
+
throw new Error(`Epic ${epicId} already merged. Abandon blocked.`);
|
|
31735
|
+
}
|
|
31736
|
+
if (fromState === "failed" || fromState === "abandoned") {
|
|
31737
|
+
throw new Error(`Epic ${epicId} already terminal in state '${fromState}'.`);
|
|
30614
31738
|
}
|
|
30615
|
-
|
|
30616
|
-
|
|
30617
|
-
|
|
31739
|
+
const liveMemberJobIds = listLiveMemberJobIds(sqlite, epicId);
|
|
31740
|
+
if (!force && liveMemberJobIds.length > 0) {
|
|
31741
|
+
throw new Error(`Epic ${epicId} has live members: ${liveMemberJobIds.join(", ")}. Retry with --force to abandon anyway.`);
|
|
30618
31742
|
}
|
|
30619
|
-
|
|
31743
|
+
const statusJson = buildAbandonedStatusJson(epic, epicId, fromState, reason, force);
|
|
31744
|
+
sqlite.upsertEpicRun({
|
|
31745
|
+
epic_id: epicId,
|
|
31746
|
+
status: "abandoned",
|
|
31747
|
+
status_json: statusJson,
|
|
31748
|
+
updated_at_ms: Date.now()
|
|
31749
|
+
});
|
|
31750
|
+
return {
|
|
31751
|
+
epic_id: epicId,
|
|
31752
|
+
from_state: fromState,
|
|
31753
|
+
to_state: "abandoned",
|
|
31754
|
+
forced: force,
|
|
31755
|
+
reason,
|
|
31756
|
+
live_member_job_ids: liveMemberJobIds
|
|
31757
|
+
};
|
|
30620
31758
|
}
|
|
30621
|
-
var
|
|
30622
|
-
var
|
|
30623
|
-
|
|
30624
|
-
|
|
30625
|
-
|
|
30626
|
-
init_hooks();
|
|
30627
|
-
init_observability_sqlite();
|
|
30628
|
-
init_beads();
|
|
31759
|
+
var ACTIVE_JOB_STATUSES2;
|
|
31760
|
+
var init_epic_reconciler = __esm(() => {
|
|
31761
|
+
init_epic_lifecycle();
|
|
31762
|
+
init_epic_readiness();
|
|
31763
|
+
init_observability_db();
|
|
30629
31764
|
init_supervisor();
|
|
30630
|
-
|
|
30631
|
-
init_node_resolve();
|
|
30632
|
-
init_node_supervisor();
|
|
30633
|
-
NODE_DISCOVERY_DIRS = [
|
|
30634
|
-
{ path: ".specialists/default/nodes", source: "default" },
|
|
30635
|
-
{ path: "config/nodes", source: "project" }
|
|
30636
|
-
];
|
|
31765
|
+
ACTIVE_JOB_STATUSES2 = new Set(["starting", "running", "waiting"]);
|
|
30637
31766
|
});
|
|
30638
31767
|
|
|
30639
31768
|
// src/cli/epic.ts
|
|
30640
31769
|
var exports_epic = {};
|
|
30641
31770
|
__export(exports_epic, {
|
|
31771
|
+
handleEpicSyncCommand: () => handleEpicSyncCommand,
|
|
30642
31772
|
handleEpicStatusCommand: () => handleEpicStatusCommand,
|
|
30643
31773
|
handleEpicResolveCommand: () => handleEpicResolveCommand,
|
|
30644
31774
|
handleEpicMergeCommand: () => handleEpicMergeCommand,
|
|
30645
31775
|
handleEpicListCommand: () => handleEpicListCommand,
|
|
30646
|
-
handleEpicCommand: () => handleEpicCommand
|
|
31776
|
+
handleEpicCommand: () => handleEpicCommand,
|
|
31777
|
+
handleEpicAbandonCommand: () => handleEpicAbandonCommand
|
|
30647
31778
|
});
|
|
30648
31779
|
import { spawnSync as spawnSync15 } from "child_process";
|
|
30649
31780
|
function runCommand2(command, args, cwd = process.cwd()) {
|
|
@@ -30743,6 +31874,65 @@ function parseResolveOptions(argv) {
|
|
|
30743
31874
|
}
|
|
30744
31875
|
return { epicId, dryRun, json };
|
|
30745
31876
|
}
|
|
31877
|
+
function parseSyncOptions(argv) {
|
|
31878
|
+
const epicId = parseEpicId(argv);
|
|
31879
|
+
let apply = false;
|
|
31880
|
+
let json = false;
|
|
31881
|
+
for (const argument of argv) {
|
|
31882
|
+
if (argument === "--apply") {
|
|
31883
|
+
apply = true;
|
|
31884
|
+
continue;
|
|
31885
|
+
}
|
|
31886
|
+
if (argument === "--json") {
|
|
31887
|
+
json = true;
|
|
31888
|
+
continue;
|
|
31889
|
+
}
|
|
31890
|
+
if (argument.startsWith("-") && argument !== "--apply" && argument !== "--json") {
|
|
31891
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
31892
|
+
}
|
|
31893
|
+
}
|
|
31894
|
+
return { epicId, apply, json };
|
|
31895
|
+
}
|
|
31896
|
+
function parseAbandonOptions(argv) {
|
|
31897
|
+
let epicId = "";
|
|
31898
|
+
let reason = "";
|
|
31899
|
+
let force = false;
|
|
31900
|
+
let json = false;
|
|
31901
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
31902
|
+
const argument = argv[index];
|
|
31903
|
+
if (argument === "--force") {
|
|
31904
|
+
force = true;
|
|
31905
|
+
continue;
|
|
31906
|
+
}
|
|
31907
|
+
if (argument === "--json") {
|
|
31908
|
+
json = true;
|
|
31909
|
+
continue;
|
|
31910
|
+
}
|
|
31911
|
+
if (argument === "--reason") {
|
|
31912
|
+
const value = argv[index + 1];
|
|
31913
|
+
if (!value || value.startsWith("-")) {
|
|
31914
|
+
throw new Error("Missing value for --reason");
|
|
31915
|
+
}
|
|
31916
|
+
reason = value.trim();
|
|
31917
|
+
index += 1;
|
|
31918
|
+
continue;
|
|
31919
|
+
}
|
|
31920
|
+
if (argument.startsWith("-")) {
|
|
31921
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
31922
|
+
}
|
|
31923
|
+
if (epicId.length > 0) {
|
|
31924
|
+
throw new Error("Only one epic ID is supported");
|
|
31925
|
+
}
|
|
31926
|
+
epicId = argument;
|
|
31927
|
+
}
|
|
31928
|
+
if (!epicId) {
|
|
31929
|
+
throw new Error("Missing epic ID");
|
|
31930
|
+
}
|
|
31931
|
+
if (reason.length === 0) {
|
|
31932
|
+
throw new Error("Missing required --reason <text>");
|
|
31933
|
+
}
|
|
31934
|
+
return { epicId, reason, force, json };
|
|
31935
|
+
}
|
|
30746
31936
|
function readEpicChildrenFromBeads(epicId) {
|
|
30747
31937
|
const result = runCommand2("bd", ["children", epicId]);
|
|
30748
31938
|
if (result.status !== 0) {
|
|
@@ -31106,6 +32296,94 @@ async function handleEpicMergeCommand(argv) {
|
|
|
31106
32296
|
process.exit(1);
|
|
31107
32297
|
}
|
|
31108
32298
|
}
|
|
32299
|
+
async function handleEpicSyncCommand(argv) {
|
|
32300
|
+
let options;
|
|
32301
|
+
try {
|
|
32302
|
+
options = parseSyncOptions(argv);
|
|
32303
|
+
} catch (error2) {
|
|
32304
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
32305
|
+
console.error(message);
|
|
32306
|
+
console.error("Usage: specialists epic sync <epic-id> [--apply] [--json]");
|
|
32307
|
+
process.exit(1);
|
|
32308
|
+
}
|
|
32309
|
+
const sqlite = createObservabilitySqliteClient();
|
|
32310
|
+
if (!sqlite) {
|
|
32311
|
+
const message = "Observability SQLite database not available. Run `sp db setup` first.";
|
|
32312
|
+
if (options.json) {
|
|
32313
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
32314
|
+
} else {
|
|
32315
|
+
console.error(message);
|
|
32316
|
+
}
|
|
32317
|
+
process.exit(1);
|
|
32318
|
+
}
|
|
32319
|
+
try {
|
|
32320
|
+
const result = withEpicAdvisoryLock(options.epicId, () => syncEpicState(sqlite, options.epicId, options.apply));
|
|
32321
|
+
if (options.json) {
|
|
32322
|
+
console.log(JSON.stringify(result, null, 2));
|
|
32323
|
+
return;
|
|
32324
|
+
}
|
|
32325
|
+
console.log("");
|
|
32326
|
+
console.log(`Epic ${result.epic_id} sync (${result.apply ? "apply" : "dry-run"})`);
|
|
32327
|
+
console.log(` stale_chain_refs: ${result.drift.stale_chain_refs.length}`);
|
|
32328
|
+
console.log(` dead_jobs_blocking_readiness: ${result.drift.dead_jobs_blocking_readiness.length}`);
|
|
32329
|
+
console.log(` integrity_flags: ${result.drift.integrity_flags.length}`);
|
|
32330
|
+
console.log(` stale_redirect_markers: ${result.drift.stale_redirect_markers.length}`);
|
|
32331
|
+
if (result.apply) {
|
|
32332
|
+
console.log(` repaired_dead_jobs: ${result.repairs.dead_jobs_marked_error.length}`);
|
|
32333
|
+
console.log(` stale_chain_refs_pruned: ${result.repairs.stale_chain_refs_pruned.length}`);
|
|
32334
|
+
console.log(` readiness_resynced: ${result.repairs.readiness_resynced}`);
|
|
32335
|
+
console.log(` redirect_markers_cleared: ${result.repairs.redirect_markers_cleared}`);
|
|
32336
|
+
}
|
|
32337
|
+
console.log(` readiness_before: ${result.readiness_before.readiness_state}`);
|
|
32338
|
+
console.log(` readiness_after: ${result.readiness_after.readiness_state}`);
|
|
32339
|
+
console.log("");
|
|
32340
|
+
} finally {
|
|
32341
|
+
sqlite.close();
|
|
32342
|
+
}
|
|
32343
|
+
}
|
|
32344
|
+
async function handleEpicAbandonCommand(argv) {
|
|
32345
|
+
let options;
|
|
32346
|
+
try {
|
|
32347
|
+
options = parseAbandonOptions(argv);
|
|
32348
|
+
} catch (error2) {
|
|
32349
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
32350
|
+
console.error(message);
|
|
32351
|
+
console.error("Usage: specialists epic abandon <epic-id> --reason <text> [--force] [--json]");
|
|
32352
|
+
process.exit(1);
|
|
32353
|
+
}
|
|
32354
|
+
const sqlite = createObservabilitySqliteClient();
|
|
32355
|
+
if (!sqlite) {
|
|
32356
|
+
const message = "Observability SQLite database not available. Run `sp db setup` first.";
|
|
32357
|
+
if (options.json) {
|
|
32358
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
32359
|
+
} else {
|
|
32360
|
+
console.error(message);
|
|
32361
|
+
}
|
|
32362
|
+
process.exit(1);
|
|
32363
|
+
}
|
|
32364
|
+
try {
|
|
32365
|
+
const result = withEpicAdvisoryLock(options.epicId, () => abandonEpic(sqlite, options.epicId, options.reason, options.force));
|
|
32366
|
+
if (options.json) {
|
|
32367
|
+
console.log(JSON.stringify(result, null, 2));
|
|
32368
|
+
return;
|
|
32369
|
+
}
|
|
32370
|
+
console.log(`Epic ${result.epic_id}: ${result.from_state} -> ${result.to_state}`);
|
|
32371
|
+
console.log(`Reason: ${result.reason}`);
|
|
32372
|
+
if (result.forced) {
|
|
32373
|
+
console.log("Mode: forced");
|
|
32374
|
+
}
|
|
32375
|
+
} catch (error2) {
|
|
32376
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
32377
|
+
if (options.json) {
|
|
32378
|
+
console.log(JSON.stringify({ epic_id: options.epicId, error: message }, null, 2));
|
|
32379
|
+
} else {
|
|
32380
|
+
console.error(message);
|
|
32381
|
+
}
|
|
32382
|
+
process.exit(1);
|
|
32383
|
+
} finally {
|
|
32384
|
+
sqlite.close();
|
|
32385
|
+
}
|
|
32386
|
+
}
|
|
31109
32387
|
async function handleEpicStatusCommand(argv) {
|
|
31110
32388
|
let options;
|
|
31111
32389
|
try {
|
|
@@ -31190,12 +32468,14 @@ async function handleEpicCommand(argv) {
|
|
|
31190
32468
|
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
31191
32469
|
console.log([
|
|
31192
32470
|
"",
|
|
31193
|
-
"Usage: specialists epic <list|status|resolve|merge> [options]",
|
|
32471
|
+
"Usage: specialists epic <list|status|resolve|sync|abandon|merge> [options]",
|
|
31194
32472
|
"",
|
|
31195
32473
|
"Commands:",
|
|
31196
32474
|
" list [--unresolved] [--json] List epics with lifecycle and readiness summary",
|
|
31197
32475
|
" status <epic-id> [--json] Show epic state, chain statuses, and merge readiness",
|
|
31198
32476
|
" resolve <epic-id> [--dry-run] [--json] Transition epic from open to resolving",
|
|
32477
|
+
" sync <epic-id> [--apply] [--json] Reconcile epic drift (dry-run by default)",
|
|
32478
|
+
" abandon <epic-id> --reason <text> [--force] [--json] Transition epic to abandoned",
|
|
31199
32479
|
" merge <epic-id> [--rebuild] [--pr] [--json] Publish epic-owned chains in dependency order",
|
|
31200
32480
|
"",
|
|
31201
32481
|
"Epic lifecycle states:",
|
|
@@ -31215,6 +32495,9 @@ async function handleEpicCommand(argv) {
|
|
|
31215
32495
|
" specialists epic list --unresolved --json",
|
|
31216
32496
|
" specialists epic resolve unitAI-3f7b",
|
|
31217
32497
|
" specialists epic status unitAI-3f7b --json",
|
|
32498
|
+
" specialists epic sync unitAI-3f7b",
|
|
32499
|
+
" specialists epic sync unitAI-3f7b --apply",
|
|
32500
|
+
' specialists epic abandon unitAI-3f7b --reason "scope changed"',
|
|
31218
32501
|
" specialists epic merge unitAI-3f7b --rebuild",
|
|
31219
32502
|
" specialists epic merge unitAI-3f7b --pr",
|
|
31220
32503
|
""
|
|
@@ -31230,6 +32513,14 @@ async function handleEpicCommand(argv) {
|
|
|
31230
32513
|
await handleEpicResolveCommand(argv.slice(1));
|
|
31231
32514
|
return;
|
|
31232
32515
|
}
|
|
32516
|
+
if (subcommand === "sync") {
|
|
32517
|
+
await handleEpicSyncCommand(argv.slice(1));
|
|
32518
|
+
return;
|
|
32519
|
+
}
|
|
32520
|
+
if (subcommand === "abandon") {
|
|
32521
|
+
await handleEpicAbandonCommand(argv.slice(1));
|
|
32522
|
+
return;
|
|
32523
|
+
}
|
|
31233
32524
|
if (subcommand === "merge") {
|
|
31234
32525
|
await handleEpicMergeCommand(argv.slice(1));
|
|
31235
32526
|
return;
|
|
@@ -31239,12 +32530,13 @@ async function handleEpicCommand(argv) {
|
|
|
31239
32530
|
return;
|
|
31240
32531
|
}
|
|
31241
32532
|
console.error(`Unknown epic subcommand: ${subcommand}`);
|
|
31242
|
-
console.error("Usage: specialists epic <list|status|resolve|merge>");
|
|
32533
|
+
console.error("Usage: specialists epic <list|status|resolve|sync|abandon|merge>");
|
|
31243
32534
|
process.exit(1);
|
|
31244
32535
|
}
|
|
31245
32536
|
var RUNNING_STATUSES;
|
|
31246
32537
|
var init_epic = __esm(() => {
|
|
31247
32538
|
init_epic_lifecycle();
|
|
32539
|
+
init_epic_reconciler();
|
|
31248
32540
|
init_observability_sqlite();
|
|
31249
32541
|
init_merge();
|
|
31250
32542
|
RUNNING_STATUSES = new Set(["starting", "running", "waiting", "degraded"]);
|
|
@@ -31256,8 +32548,8 @@ __export(exports_status, {
|
|
|
31256
32548
|
run: () => run14
|
|
31257
32549
|
});
|
|
31258
32550
|
import { spawnSync as spawnSync16 } from "child_process";
|
|
31259
|
-
import { existsSync as existsSync17, readFileSync as
|
|
31260
|
-
import { join as
|
|
32551
|
+
import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
|
|
32552
|
+
import { join as join20 } from "path";
|
|
31261
32553
|
function ok2(msg) {
|
|
31262
32554
|
console.log(` ${green8("\u2713")} ${msg}`);
|
|
31263
32555
|
}
|
|
@@ -31348,10 +32640,10 @@ function countJobEvents(sqliteClient, jobsDir, jobId) {
|
|
|
31348
32640
|
} catch (error2) {
|
|
31349
32641
|
console.warn(`SQLite events read failed for job ${jobId}; falling back to events.jsonl`, error2);
|
|
31350
32642
|
}
|
|
31351
|
-
const eventsFile =
|
|
32643
|
+
const eventsFile = join20(jobsDir, jobId, "events.jsonl");
|
|
31352
32644
|
if (!existsSync17(eventsFile))
|
|
31353
32645
|
return 0;
|
|
31354
|
-
const raw =
|
|
32646
|
+
const raw = readFileSync15(eventsFile, "utf-8").trim();
|
|
31355
32647
|
if (!raw)
|
|
31356
32648
|
return 0;
|
|
31357
32649
|
return raw.split(`
|
|
@@ -31382,10 +32674,10 @@ function getLatestContextSnapshot(sqliteClient, jobsDir, jobId) {
|
|
|
31382
32674
|
} catch (error2) {
|
|
31383
32675
|
console.warn(`SQLite events read failed for job ${jobId}; falling back to events.jsonl`, error2);
|
|
31384
32676
|
}
|
|
31385
|
-
const eventsFile =
|
|
32677
|
+
const eventsFile = join20(jobsDir, jobId, "events.jsonl");
|
|
31386
32678
|
if (!existsSync17(eventsFile))
|
|
31387
32679
|
return null;
|
|
31388
|
-
const lines =
|
|
32680
|
+
const lines = readFileSync15(eventsFile, "utf-8").split(`
|
|
31389
32681
|
`);
|
|
31390
32682
|
for (let index = lines.length - 1;index >= 0; index -= 1) {
|
|
31391
32683
|
const line = lines[index].trim();
|
|
@@ -31490,7 +32782,7 @@ async function run14() {
|
|
|
31490
32782
|
`).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean)) : new Set;
|
|
31491
32783
|
const bdInstalled = isInstalled2("bd");
|
|
31492
32784
|
const bdVersion = bdInstalled ? cmd("bd", ["--version"]) : null;
|
|
31493
|
-
const beadsPresent = existsSync17(
|
|
32785
|
+
const beadsPresent = existsSync17(join20(process.cwd(), ".beads"));
|
|
31494
32786
|
const specialistsBin = cmd("which", ["specialists"]);
|
|
31495
32787
|
const jobsDir = resolveJobsDir();
|
|
31496
32788
|
let jobs = [];
|
|
@@ -31651,8 +32943,8 @@ __export(exports_ps, {
|
|
|
31651
32943
|
run: () => run15
|
|
31652
32944
|
});
|
|
31653
32945
|
import { spawnSync as spawnSync17 } from "child_process";
|
|
31654
|
-
import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as
|
|
31655
|
-
import { join as
|
|
32946
|
+
import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync16 } from "fs";
|
|
32947
|
+
import { join as join21 } from "path";
|
|
31656
32948
|
function parseArgs7(argv) {
|
|
31657
32949
|
let nodeId;
|
|
31658
32950
|
const positional = [];
|
|
@@ -31686,21 +32978,21 @@ function readStatusesFromFiles(jobsDir) {
|
|
|
31686
32978
|
return [];
|
|
31687
32979
|
const statuses = [];
|
|
31688
32980
|
for (const entry of readdirSync8(jobsDir)) {
|
|
31689
|
-
const statusPath =
|
|
32981
|
+
const statusPath = join21(jobsDir, entry, "status.json");
|
|
31690
32982
|
if (!existsSync18(statusPath))
|
|
31691
32983
|
continue;
|
|
31692
32984
|
try {
|
|
31693
|
-
statuses.push(JSON.parse(
|
|
32985
|
+
statuses.push(JSON.parse(readFileSync16(statusPath, "utf-8")));
|
|
31694
32986
|
} catch {}
|
|
31695
32987
|
}
|
|
31696
32988
|
return statuses.sort((a, b) => b.started_at_ms - a.started_at_ms);
|
|
31697
32989
|
}
|
|
31698
32990
|
function readLastToolEventFromFile(jobsDir, jobId) {
|
|
31699
|
-
const eventsPath =
|
|
32991
|
+
const eventsPath = join21(jobsDir, jobId, "events.jsonl");
|
|
31700
32992
|
if (!existsSync18(eventsPath))
|
|
31701
32993
|
return;
|
|
31702
32994
|
try {
|
|
31703
|
-
const lines =
|
|
32995
|
+
const lines = readFileSync16(eventsPath, "utf-8").split(`
|
|
31704
32996
|
`);
|
|
31705
32997
|
for (let index = lines.length - 1;index >= 0; index -= 1) {
|
|
31706
32998
|
const line = lines[index]?.trim();
|
|
@@ -32458,8 +33750,8 @@ var exports_result = {};
|
|
|
32458
33750
|
__export(exports_result, {
|
|
32459
33751
|
run: () => run16
|
|
32460
33752
|
});
|
|
32461
|
-
import { existsSync as existsSync19, readFileSync as
|
|
32462
|
-
import { join as
|
|
33753
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
|
|
33754
|
+
import { join as join22 } from "path";
|
|
32463
33755
|
function parseArgs8(argv) {
|
|
32464
33756
|
let jobId;
|
|
32465
33757
|
let nodeId;
|
|
@@ -32543,9 +33835,96 @@ function resolveJobIdFromNodeMember(sqliteClient, nodeId, memberKey) {
|
|
|
32543
33835
|
}
|
|
32544
33836
|
return member.job_id;
|
|
32545
33837
|
}
|
|
33838
|
+
function readTimelineEventsForResult(sqliteClient, jobsDir, jobId) {
|
|
33839
|
+
if (sqliteClient) {
|
|
33840
|
+
try {
|
|
33841
|
+
return sqliteClient.readEvents(jobId);
|
|
33842
|
+
} catch {}
|
|
33843
|
+
}
|
|
33844
|
+
const eventsPath = join22(jobsDir, jobId, "events.jsonl");
|
|
33845
|
+
if (!existsSync19(eventsPath))
|
|
33846
|
+
return [];
|
|
33847
|
+
return readFileSync17(eventsPath, "utf-8").split(`
|
|
33848
|
+
`).map((line) => line.trim()).filter(Boolean).map((line) => parseTimelineEvent(line)).filter((event) => event !== null);
|
|
33849
|
+
}
|
|
33850
|
+
function deriveStartupSnapshot(status, events) {
|
|
33851
|
+
const runStartEvent = events.find((event) => event.type === "run_start");
|
|
33852
|
+
const startupFromEvent = runStartEvent?.type === "run_start" ? runStartEvent.startup_snapshot ?? null : null;
|
|
33853
|
+
const memoryMeta = events.find((event) => event.type === "meta" && !!event.memory_injection);
|
|
33854
|
+
const memoryInjection = memoryMeta?.type === "meta" ? memoryMeta.memory_injection : undefined;
|
|
33855
|
+
const merged = {
|
|
33856
|
+
...startupFromEvent ?? {},
|
|
33857
|
+
...status.startup_context ?? {},
|
|
33858
|
+
...memoryInjection ? { memory_injection: memoryInjection } : {}
|
|
33859
|
+
};
|
|
33860
|
+
if (!merged.job_id)
|
|
33861
|
+
merged.job_id = status.id;
|
|
33862
|
+
if (!merged.specialist_name)
|
|
33863
|
+
merged.specialist_name = status.specialist;
|
|
33864
|
+
if (!merged.bead_id && status.bead_id)
|
|
33865
|
+
merged.bead_id = status.bead_id;
|
|
33866
|
+
if (!merged.reused_from_job_id && status.reused_from_job_id)
|
|
33867
|
+
merged.reused_from_job_id = status.reused_from_job_id;
|
|
33868
|
+
if (!merged.worktree_owner_job_id && status.worktree_owner_job_id)
|
|
33869
|
+
merged.worktree_owner_job_id = status.worktree_owner_job_id;
|
|
33870
|
+
if (!merged.chain_id && status.chain_id)
|
|
33871
|
+
merged.chain_id = status.chain_id;
|
|
33872
|
+
if (!merged.chain_root_job_id && status.chain_root_job_id)
|
|
33873
|
+
merged.chain_root_job_id = status.chain_root_job_id;
|
|
33874
|
+
if (!merged.chain_root_bead_id && status.chain_root_bead_id)
|
|
33875
|
+
merged.chain_root_bead_id = status.chain_root_bead_id;
|
|
33876
|
+
if (!merged.worktree_path && status.worktree_path)
|
|
33877
|
+
merged.worktree_path = status.worktree_path;
|
|
33878
|
+
if (!merged.branch && status.branch)
|
|
33879
|
+
merged.branch = status.branch;
|
|
33880
|
+
return Object.keys(merged).length > 0 ? merged : null;
|
|
33881
|
+
}
|
|
33882
|
+
function deriveApiError(events) {
|
|
33883
|
+
const errorEvent = [...events].reverse().find((event) => event.type === "error");
|
|
33884
|
+
return errorEvent?.error_message ?? null;
|
|
33885
|
+
}
|
|
33886
|
+
function formatStartupSnapshot(snapshot) {
|
|
33887
|
+
if (!snapshot)
|
|
33888
|
+
return null;
|
|
33889
|
+
const lines = [`
|
|
33890
|
+
--- startup context ---`];
|
|
33891
|
+
const push = (key, value) => {
|
|
33892
|
+
if (value === undefined || value === null)
|
|
33893
|
+
return;
|
|
33894
|
+
lines.push(`${key}: ${Array.isArray(value) ? value.join(", ") : String(value)}`);
|
|
33895
|
+
};
|
|
33896
|
+
push("job_id", snapshot.job_id);
|
|
33897
|
+
push("specialist_name", snapshot.specialist_name);
|
|
33898
|
+
push("bead_id", snapshot.bead_id);
|
|
33899
|
+
push("reused_from_job_id", snapshot.reused_from_job_id);
|
|
33900
|
+
push("worktree_owner_job_id", snapshot.worktree_owner_job_id);
|
|
33901
|
+
push("chain_id", snapshot.chain_id);
|
|
33902
|
+
push("chain_root_job_id", snapshot.chain_root_job_id);
|
|
33903
|
+
push("chain_root_bead_id", snapshot.chain_root_bead_id);
|
|
33904
|
+
push("worktree_path", snapshot.worktree_path);
|
|
33905
|
+
push("branch", snapshot.branch);
|
|
33906
|
+
push("variables_keys", snapshot.variables_keys);
|
|
33907
|
+
push("reviewed_job_id_present", snapshot.reviewed_job_id_present);
|
|
33908
|
+
push("reused_worktree_awareness_present", snapshot.reused_worktree_awareness_present);
|
|
33909
|
+
push("bead_context_present", snapshot.bead_context_present);
|
|
33910
|
+
if (snapshot.memory_injection) {
|
|
33911
|
+
push("memory.static_tokens", snapshot.memory_injection.static_tokens);
|
|
33912
|
+
push("memory.memory_tokens", snapshot.memory_injection.memory_tokens);
|
|
33913
|
+
push("memory.gitnexus_tokens", snapshot.memory_injection.gitnexus_tokens);
|
|
33914
|
+
push("memory.total_tokens", snapshot.memory_injection.total_tokens);
|
|
33915
|
+
}
|
|
33916
|
+
if (snapshot.skills) {
|
|
33917
|
+
push("skills.count", snapshot.skills.count);
|
|
33918
|
+
push("skills.activated", snapshot.skills.activated);
|
|
33919
|
+
}
|
|
33920
|
+
lines.push("---");
|
|
33921
|
+
return `${lines.join(`
|
|
33922
|
+
`)}
|
|
33923
|
+
`;
|
|
33924
|
+
}
|
|
32546
33925
|
async function run16() {
|
|
32547
33926
|
const args = parseArgs8(process.argv.slice(3));
|
|
32548
|
-
const emitJson = (status, output2, error2) => {
|
|
33927
|
+
const emitJson = (status, output2, error2, startupContext = null) => {
|
|
32549
33928
|
console.log(JSON.stringify({
|
|
32550
33929
|
job: status ? {
|
|
32551
33930
|
id: status.id,
|
|
@@ -32555,17 +33934,20 @@ async function run16() {
|
|
|
32555
33934
|
backend: status.backend ?? null,
|
|
32556
33935
|
bead_id: status.bead_id ?? null,
|
|
32557
33936
|
metrics: status.metrics ?? null,
|
|
33937
|
+
startup_context: startupContext,
|
|
32558
33938
|
error: status.error ?? null
|
|
32559
33939
|
} : null,
|
|
32560
33940
|
output: output2,
|
|
33941
|
+
startup_context: startupContext,
|
|
32561
33942
|
error: error2
|
|
32562
33943
|
}, null, 2));
|
|
32563
33944
|
};
|
|
32564
|
-
const jobsDir =
|
|
33945
|
+
const jobsDir = join22(process.cwd(), ".specialists", "jobs");
|
|
32565
33946
|
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
32566
33947
|
const sqliteClient = createObservabilitySqliteClient();
|
|
32567
|
-
const emitHumanResult = (output2, status, trailingFooter) => {
|
|
32568
|
-
|
|
33948
|
+
const emitHumanResult = (output2, status, startupContext, trailingFooter) => {
|
|
33949
|
+
const startupBlock = formatStartupSnapshot(startupContext);
|
|
33950
|
+
process.stdout.write(startupBlock ? `${startupBlock}${output2}` : output2);
|
|
32569
33951
|
const tokenSummaryParts = formatTokenUsageSummary(status.metrics?.token_usage).filter((part) => !part.startsWith("cost="));
|
|
32570
33952
|
const formattedCost = formatCostUsd(status.metrics?.token_usage?.cost_usd);
|
|
32571
33953
|
if (tokenSummaryParts.length === 0 && !formattedCost) {
|
|
@@ -32594,7 +33976,7 @@ async function run16() {
|
|
|
32594
33976
|
const resolvedNodeId = args.nodeId ? resolveNodeRefWithClient(args.nodeId, sqliteClient) : resolveSingleActiveNodeRef(sqliteClient);
|
|
32595
33977
|
return resolveJobIdFromNodeMember(sqliteClient, resolvedNodeId, args.memberKey);
|
|
32596
33978
|
})();
|
|
32597
|
-
const resultPath =
|
|
33979
|
+
const resultPath = join22(jobsDir, jobId, "result.txt");
|
|
32598
33980
|
const readResultOutput = () => {
|
|
32599
33981
|
try {
|
|
32600
33982
|
const sqliteResult = sqliteClient?.readResult(jobId) ?? null;
|
|
@@ -32606,7 +33988,7 @@ async function run16() {
|
|
|
32606
33988
|
if (!existsSync19(resultPath)) {
|
|
32607
33989
|
return null;
|
|
32608
33990
|
}
|
|
32609
|
-
return
|
|
33991
|
+
return readFileSync17(resultPath, "utf-8");
|
|
32610
33992
|
};
|
|
32611
33993
|
if (args.wait) {
|
|
32612
33994
|
const startMs = Date.now();
|
|
@@ -32621,26 +34003,33 @@ async function run16() {
|
|
|
32621
34003
|
process.exit(1);
|
|
32622
34004
|
}
|
|
32623
34005
|
if (status2.status === "done") {
|
|
34006
|
+
const events2 = readTimelineEventsForResult(sqliteClient, jobsDir, jobId);
|
|
34007
|
+
const startupContext2 = deriveStartupSnapshot(status2, events2);
|
|
34008
|
+
const apiError2 = status2.error ?? deriveApiError(events2);
|
|
32624
34009
|
const output3 = readResultOutput();
|
|
32625
34010
|
if (!output3) {
|
|
34011
|
+
const message = apiError2 ? `Job ${jobId} failed: ${apiError2}` : `Result not found for job ${jobId}`;
|
|
32626
34012
|
if (args.json) {
|
|
32627
|
-
emitJson(status2, null,
|
|
34013
|
+
emitJson(status2, null, message, startupContext2);
|
|
32628
34014
|
} else {
|
|
32629
|
-
|
|
34015
|
+
process.stderr.write(`${red3(message)}
|
|
34016
|
+
`);
|
|
32630
34017
|
}
|
|
32631
34018
|
process.exit(1);
|
|
32632
34019
|
}
|
|
34020
|
+
const enrichedStatus2 = apiError2 && !status2.error ? { ...status2, error: apiError2 } : status2;
|
|
32633
34021
|
if (args.json) {
|
|
32634
|
-
emitJson(
|
|
34022
|
+
emitJson(enrichedStatus2, output3, null, startupContext2);
|
|
32635
34023
|
} else {
|
|
32636
|
-
emitHumanResult(output3,
|
|
34024
|
+
emitHumanResult(output3, enrichedStatus2, startupContext2);
|
|
32637
34025
|
}
|
|
32638
34026
|
return;
|
|
32639
34027
|
}
|
|
32640
34028
|
if (status2.status === "error") {
|
|
34029
|
+
const startupContext2 = deriveStartupSnapshot(status2, readTimelineEventsForResult(sqliteClient, jobsDir, jobId));
|
|
32641
34030
|
const message = `Job ${jobId} failed: ${status2.error ?? "unknown error"}`;
|
|
32642
34031
|
if (args.json) {
|
|
32643
|
-
emitJson(status2, null, message);
|
|
34032
|
+
emitJson(status2, null, message, startupContext2);
|
|
32644
34033
|
} else {
|
|
32645
34034
|
process.stderr.write(`${red3(`Job ${jobId} failed:`)} ${status2.error ?? "unknown error"}
|
|
32646
34035
|
`);
|
|
@@ -32652,7 +34041,8 @@ async function run16() {
|
|
|
32652
34041
|
if (elapsedSecs >= args.timeout) {
|
|
32653
34042
|
const timeoutMessage = `Timeout: job ${jobId} did not complete within ${args.timeout}s`;
|
|
32654
34043
|
if (args.json) {
|
|
32655
|
-
|
|
34044
|
+
const startupContext2 = deriveStartupSnapshot(status2, readTimelineEventsForResult(sqliteClient, jobsDir, jobId));
|
|
34045
|
+
emitJson(status2, null, timeoutMessage, startupContext2);
|
|
32656
34046
|
} else {
|
|
32657
34047
|
process.stderr.write(`${timeoutMessage}
|
|
32658
34048
|
`);
|
|
@@ -32673,11 +34063,12 @@ async function run16() {
|
|
|
32673
34063
|
process.exit(1);
|
|
32674
34064
|
}
|
|
32675
34065
|
if (status.status === "running" || status.status === "starting") {
|
|
34066
|
+
const startupContext2 = deriveStartupSnapshot(status, readTimelineEventsForResult(sqliteClient, jobsDir, jobId));
|
|
32676
34067
|
const output3 = readResultOutput();
|
|
32677
34068
|
if (!output3) {
|
|
32678
34069
|
const message = `Job ${jobId} is still ${status.status}. Use 'specialists feed --job ${jobId}' to follow.`;
|
|
32679
34070
|
if (args.json) {
|
|
32680
|
-
emitJson(status, null, message);
|
|
34071
|
+
emitJson(status, null, message, startupContext2);
|
|
32681
34072
|
} else {
|
|
32682
34073
|
process.stderr.write(`${dim10(message)}
|
|
32683
34074
|
`);
|
|
@@ -32685,20 +34076,21 @@ async function run16() {
|
|
|
32685
34076
|
process.exit(1);
|
|
32686
34077
|
}
|
|
32687
34078
|
if (args.json) {
|
|
32688
|
-
emitJson(status, output3, null);
|
|
34079
|
+
emitJson(status, output3, null, startupContext2);
|
|
32689
34080
|
} else {
|
|
32690
34081
|
process.stderr.write(`${dim10(`Job ${jobId} is currently ${status.status}. Showing last completed output while it continues.`)}
|
|
32691
34082
|
`);
|
|
32692
|
-
emitHumanResult(output3, status);
|
|
34083
|
+
emitHumanResult(output3, status, startupContext2);
|
|
32693
34084
|
}
|
|
32694
34085
|
return;
|
|
32695
34086
|
}
|
|
32696
34087
|
if (status.status === "waiting") {
|
|
34088
|
+
const startupContext2 = deriveStartupSnapshot(status, readTimelineEventsForResult(sqliteClient, jobsDir, jobId));
|
|
32697
34089
|
const output3 = readResultOutput();
|
|
32698
34090
|
if (!output3) {
|
|
32699
34091
|
const message = `Job ${jobId} is waiting for input. Use: specialists resume ${jobId} "..."`;
|
|
32700
34092
|
if (args.json) {
|
|
32701
|
-
emitJson(status, null, message);
|
|
34093
|
+
emitJson(status, null, message, startupContext2);
|
|
32702
34094
|
} else {
|
|
32703
34095
|
process.stderr.write(`${dim10(message)}
|
|
32704
34096
|
`);
|
|
@@ -32709,36 +34101,44 @@ async function run16() {
|
|
|
32709
34101
|
--- Session is waiting for your input. Use: specialists resume ${jobId} "..." ---
|
|
32710
34102
|
`;
|
|
32711
34103
|
if (args.json) {
|
|
32712
|
-
emitJson(status, `${output3}${waitingFooter}`, null);
|
|
34104
|
+
emitJson(status, `${output3}${waitingFooter}`, null, startupContext2);
|
|
32713
34105
|
} else {
|
|
32714
|
-
emitHumanResult(output3, status, waitingFooter);
|
|
34106
|
+
emitHumanResult(output3, status, startupContext2, waitingFooter);
|
|
32715
34107
|
}
|
|
32716
34108
|
return;
|
|
32717
34109
|
}
|
|
32718
34110
|
if (status.status === "error") {
|
|
32719
|
-
const
|
|
34111
|
+
const events2 = readTimelineEventsForResult(sqliteClient, jobsDir, jobId);
|
|
34112
|
+
const startupContext2 = deriveStartupSnapshot(status, events2);
|
|
34113
|
+
const message = `Job ${jobId} failed: ${status.error ?? deriveApiError(events2) ?? "unknown error"}`;
|
|
32720
34114
|
if (args.json) {
|
|
32721
|
-
emitJson(status, null, message);
|
|
34115
|
+
emitJson(status, null, message, startupContext2);
|
|
32722
34116
|
} else {
|
|
32723
|
-
process.stderr.write(`${red3(`Job ${jobId} failed:`)} ${status.error ?? "unknown error"}
|
|
34117
|
+
process.stderr.write(`${red3(`Job ${jobId} failed:`)} ${status.error ?? deriveApiError(events2) ?? "unknown error"}
|
|
32724
34118
|
`);
|
|
32725
34119
|
}
|
|
32726
34120
|
process.exit(1);
|
|
32727
34121
|
}
|
|
34122
|
+
const events = readTimelineEventsForResult(sqliteClient, jobsDir, jobId);
|
|
34123
|
+
const apiError = status.error ?? deriveApiError(events);
|
|
32728
34124
|
const output2 = readResultOutput();
|
|
32729
34125
|
if (!output2) {
|
|
34126
|
+
const message = apiError ? `Job ${jobId} failed: ${apiError}` : `Result not found for job ${jobId}`;
|
|
32730
34127
|
if (args.json) {
|
|
32731
|
-
emitJson(status, null,
|
|
34128
|
+
emitJson(status, null, message);
|
|
32732
34129
|
} else {
|
|
32733
|
-
|
|
34130
|
+
process.stderr.write(`${red3(message)}
|
|
34131
|
+
`);
|
|
32734
34132
|
}
|
|
32735
34133
|
process.exit(1);
|
|
32736
34134
|
}
|
|
34135
|
+
const startupContext = deriveStartupSnapshot(status, events);
|
|
34136
|
+
const enrichedStatus = apiError && !status.error ? { ...status, error: apiError } : status;
|
|
32737
34137
|
if (args.json) {
|
|
32738
|
-
emitJson(
|
|
34138
|
+
emitJson(enrichedStatus, output2, null, startupContext);
|
|
32739
34139
|
return;
|
|
32740
34140
|
}
|
|
32741
|
-
emitHumanResult(output2,
|
|
34141
|
+
emitHumanResult(output2, enrichedStatus, startupContext);
|
|
32742
34142
|
} catch (error2) {
|
|
32743
34143
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
32744
34144
|
if (args.json) {
|
|
@@ -32756,13 +34156,14 @@ var dim10 = (s) => `\x1B[2m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
|
32756
34156
|
var init_result = __esm(() => {
|
|
32757
34157
|
init_supervisor();
|
|
32758
34158
|
init_observability_sqlite();
|
|
34159
|
+
init_timeline_events();
|
|
32759
34160
|
init_node_resolve();
|
|
32760
34161
|
init_format_helpers();
|
|
32761
34162
|
});
|
|
32762
34163
|
|
|
32763
34164
|
// src/specialist/timeline-query.ts
|
|
32764
|
-
import { existsSync as existsSync20, readdirSync as readdirSync9, readFileSync as
|
|
32765
|
-
import { basename as basename5, join as
|
|
34165
|
+
import { existsSync as existsSync20, readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
|
|
34166
|
+
import { basename as basename5, join as join23 } from "path";
|
|
32766
34167
|
function readJobEvents(jobDir) {
|
|
32767
34168
|
const jobId = basename5(jobDir);
|
|
32768
34169
|
try {
|
|
@@ -32772,10 +34173,10 @@ function readJobEvents(jobDir) {
|
|
|
32772
34173
|
return sqliteEvents;
|
|
32773
34174
|
}
|
|
32774
34175
|
} catch {}
|
|
32775
|
-
const eventsPath =
|
|
34176
|
+
const eventsPath = join23(jobDir, "events.jsonl");
|
|
32776
34177
|
if (!existsSync20(eventsPath))
|
|
32777
34178
|
return [];
|
|
32778
|
-
const content =
|
|
34179
|
+
const content = readFileSync18(eventsPath, "utf-8");
|
|
32779
34180
|
const lines = content.split(`
|
|
32780
34181
|
`).filter(Boolean);
|
|
32781
34182
|
const events = [];
|
|
@@ -32788,7 +34189,7 @@ function readJobEvents(jobDir) {
|
|
|
32788
34189
|
return events;
|
|
32789
34190
|
}
|
|
32790
34191
|
function readJobEventsById(jobsDir, jobId) {
|
|
32791
|
-
return readJobEvents(
|
|
34192
|
+
return readJobEvents(join23(jobsDir, jobId));
|
|
32792
34193
|
}
|
|
32793
34194
|
function readAllJobEvents(jobsDir) {
|
|
32794
34195
|
if (!existsSync20(jobsDir))
|
|
@@ -32796,7 +34197,7 @@ function readAllJobEvents(jobsDir) {
|
|
|
32796
34197
|
const batches = [];
|
|
32797
34198
|
const entries = readdirSync9(jobsDir);
|
|
32798
34199
|
for (const entry of entries) {
|
|
32799
|
-
const jobDir =
|
|
34200
|
+
const jobDir = join23(jobsDir, entry);
|
|
32800
34201
|
try {
|
|
32801
34202
|
const stat2 = __require("fs").statSync(jobDir);
|
|
32802
34203
|
if (!stat2.isDirectory())
|
|
@@ -32805,12 +34206,12 @@ function readAllJobEvents(jobsDir) {
|
|
|
32805
34206
|
continue;
|
|
32806
34207
|
}
|
|
32807
34208
|
const jobId = entry;
|
|
32808
|
-
const statusPath =
|
|
34209
|
+
const statusPath = join23(jobDir, "status.json");
|
|
32809
34210
|
let specialist = "unknown";
|
|
32810
34211
|
let beadId;
|
|
32811
34212
|
if (existsSync20(statusPath)) {
|
|
32812
34213
|
try {
|
|
32813
|
-
const status = JSON.parse(
|
|
34214
|
+
const status = JSON.parse(readFileSync18(statusPath, "utf-8"));
|
|
32814
34215
|
specialist = status.specialist ?? "unknown";
|
|
32815
34216
|
beadId = status.bead_id;
|
|
32816
34217
|
} catch {}
|
|
@@ -32908,12 +34309,12 @@ __export(exports_feed, {
|
|
|
32908
34309
|
import {
|
|
32909
34310
|
closeSync as closeSync2,
|
|
32910
34311
|
existsSync as existsSync21,
|
|
32911
|
-
openSync as
|
|
32912
|
-
readFileSync as
|
|
34312
|
+
openSync as openSync3,
|
|
34313
|
+
readFileSync as readFileSync19,
|
|
32913
34314
|
readdirSync as readdirSync10,
|
|
32914
|
-
statSync as
|
|
34315
|
+
statSync as statSync3
|
|
32915
34316
|
} from "fs";
|
|
32916
|
-
import { join as
|
|
34317
|
+
import { join as join24 } from "path";
|
|
32917
34318
|
function getHumanEventKey(event) {
|
|
32918
34319
|
switch (event.type) {
|
|
32919
34320
|
case "meta":
|
|
@@ -32934,6 +34335,8 @@ function getHumanEventKey(event) {
|
|
|
32934
34335
|
return `run_start:${event.specialist}:${event.bead_id ?? ""}`;
|
|
32935
34336
|
case "run_complete":
|
|
32936
34337
|
return `run_complete:${event.status}:${event.error ?? ""}`;
|
|
34338
|
+
case "error":
|
|
34339
|
+
return `error:${event.source}:${event.error_message}`;
|
|
32937
34340
|
case "token_usage":
|
|
32938
34341
|
return `token_usage:${event.token_usage.total_tokens ?? ""}:${event.source}`;
|
|
32939
34342
|
case "finish_reason":
|
|
@@ -32981,6 +34384,50 @@ function formatWaitingBanner(jobId, specialist) {
|
|
|
32981
34384
|
const prefix = magenta3(bold10("WAIT"));
|
|
32982
34385
|
return `${prefix} ${specialist} (${jobId}) is waiting for input. Use: specialists resume ${jobId} "..."`;
|
|
32983
34386
|
}
|
|
34387
|
+
function formatStartupContextLine(event) {
|
|
34388
|
+
if (event.type === "run_start") {
|
|
34389
|
+
const snapshot = event.startup_snapshot;
|
|
34390
|
+
if (!snapshot)
|
|
34391
|
+
return null;
|
|
34392
|
+
const parts = [];
|
|
34393
|
+
if (snapshot.job_id)
|
|
34394
|
+
parts.push(`job=${snapshot.job_id}`);
|
|
34395
|
+
if (snapshot.specialist_name)
|
|
34396
|
+
parts.push(`specialist=${snapshot.specialist_name}`);
|
|
34397
|
+
if (snapshot.bead_id)
|
|
34398
|
+
parts.push(`bead=${snapshot.bead_id}`);
|
|
34399
|
+
if (snapshot.reused_from_job_id)
|
|
34400
|
+
parts.push(`reused=${snapshot.reused_from_job_id}`);
|
|
34401
|
+
if (snapshot.worktree_owner_job_id)
|
|
34402
|
+
parts.push(`owner=${snapshot.worktree_owner_job_id}`);
|
|
34403
|
+
if (snapshot.chain_id)
|
|
34404
|
+
parts.push(`chain=${snapshot.chain_id}`);
|
|
34405
|
+
if (snapshot.chain_root_job_id)
|
|
34406
|
+
parts.push(`chain_root_job=${snapshot.chain_root_job_id}`);
|
|
34407
|
+
if (snapshot.chain_root_bead_id)
|
|
34408
|
+
parts.push(`chain_root_bead=${snapshot.chain_root_bead_id}`);
|
|
34409
|
+
if (snapshot.worktree_path)
|
|
34410
|
+
parts.push(`worktree=${snapshot.worktree_path}`);
|
|
34411
|
+
if (snapshot.branch)
|
|
34412
|
+
parts.push(`branch=${snapshot.branch}`);
|
|
34413
|
+
if (snapshot.variables_keys)
|
|
34414
|
+
parts.push(`vars=[${snapshot.variables_keys.join(",")}]`);
|
|
34415
|
+
if (snapshot.reviewed_job_id_present !== undefined)
|
|
34416
|
+
parts.push(`reviewed_present=${snapshot.reviewed_job_id_present}`);
|
|
34417
|
+
if (snapshot.reused_worktree_awareness_present !== undefined)
|
|
34418
|
+
parts.push(`reuse_awareness_present=${snapshot.reused_worktree_awareness_present}`);
|
|
34419
|
+
if (snapshot.bead_context_present !== undefined)
|
|
34420
|
+
parts.push(`bead_context_present=${snapshot.bead_context_present}`);
|
|
34421
|
+
if (snapshot.skills)
|
|
34422
|
+
parts.push(`skills=${snapshot.skills.count}`);
|
|
34423
|
+
return parts.length > 0 ? dim8(` \u21B3 startup ${parts.join(" ")}`) : null;
|
|
34424
|
+
}
|
|
34425
|
+
if (event.type === "meta" && event.memory_injection) {
|
|
34426
|
+
const mem = event.memory_injection;
|
|
34427
|
+
return dim8(` \u21B3 memory static=${mem.static_tokens} dynamic=${mem.memory_tokens} gitnexus=${mem.gitnexus_tokens} total=${mem.total_tokens}`);
|
|
34428
|
+
}
|
|
34429
|
+
return null;
|
|
34430
|
+
}
|
|
32984
34431
|
function parseSince(value) {
|
|
32985
34432
|
if (value.includes("T") || value.includes("-")) {
|
|
32986
34433
|
return new Date(value).getTime();
|
|
@@ -33007,8 +34454,8 @@ function parseCursor(value, defaultJobId) {
|
|
|
33007
34454
|
function readFileFresh(filePath) {
|
|
33008
34455
|
let fd = null;
|
|
33009
34456
|
try {
|
|
33010
|
-
fd =
|
|
33011
|
-
return
|
|
34457
|
+
fd = openSync3(filePath, "r");
|
|
34458
|
+
return readFileSync19(fd, "utf-8");
|
|
33012
34459
|
} catch {
|
|
33013
34460
|
return null;
|
|
33014
34461
|
} finally {
|
|
@@ -33025,7 +34472,7 @@ function readStatusJson(sqliteClient, jobsDir, jobId) {
|
|
|
33025
34472
|
} catch (error2) {
|
|
33026
34473
|
console.warn(`SQLite status read failed for job ${jobId}; falling back to status.json`, error2);
|
|
33027
34474
|
}
|
|
33028
|
-
const statusPath =
|
|
34475
|
+
const statusPath = join24(jobsDir, jobId, "status.json");
|
|
33029
34476
|
const raw = readFileFresh(statusPath);
|
|
33030
34477
|
if (!raw)
|
|
33031
34478
|
return null;
|
|
@@ -33195,6 +34642,9 @@ function printSnapshot(sqliteClient, merged, options, jobsDir) {
|
|
|
33195
34642
|
contextPct: meta.contextPct,
|
|
33196
34643
|
colorize
|
|
33197
34644
|
}));
|
|
34645
|
+
const startupContextLine = formatStartupContextLine(event);
|
|
34646
|
+
if (startupContextLine)
|
|
34647
|
+
console.log(startupContextLine);
|
|
33198
34648
|
}
|
|
33199
34649
|
}
|
|
33200
34650
|
function compareMergedEvents(a, b) {
|
|
@@ -33235,9 +34685,9 @@ function listMatchingJobIds(sqliteClient, jobsDir, options) {
|
|
|
33235
34685
|
return [];
|
|
33236
34686
|
const jobIds = [];
|
|
33237
34687
|
for (const entry of readdirSync10(jobsDir)) {
|
|
33238
|
-
const jobDir =
|
|
34688
|
+
const jobDir = join24(jobsDir, entry);
|
|
33239
34689
|
try {
|
|
33240
|
-
if (!
|
|
34690
|
+
if (!statSync3(jobDir).isDirectory())
|
|
33241
34691
|
continue;
|
|
33242
34692
|
} catch {
|
|
33243
34693
|
continue;
|
|
@@ -33269,7 +34719,7 @@ function readJobEventsFresh(sqliteClient, jobsDir, jobId) {
|
|
|
33269
34719
|
} catch (error2) {
|
|
33270
34720
|
console.warn(`SQLite events read failed for job ${jobId}; falling back to events.jsonl`, error2);
|
|
33271
34721
|
}
|
|
33272
|
-
const eventsPath =
|
|
34722
|
+
const eventsPath = join24(jobsDir, jobId, "events.jsonl");
|
|
33273
34723
|
const content = readFileFresh(eventsPath);
|
|
33274
34724
|
if (!content)
|
|
33275
34725
|
return [];
|
|
@@ -33345,7 +34795,7 @@ async function followMerged(sqliteClient, jobsDir, options) {
|
|
|
33345
34795
|
}
|
|
33346
34796
|
const lastPrintedEventKey = new Map;
|
|
33347
34797
|
const seenMetaKey = new Map;
|
|
33348
|
-
await new Promise((
|
|
34798
|
+
await new Promise((resolve8) => {
|
|
33349
34799
|
const interval = setInterval(() => {
|
|
33350
34800
|
const batches = filteredBatches();
|
|
33351
34801
|
for (const jobId of listMatchingJobIds(sqliteClient, jobsDir, options)) {
|
|
@@ -33415,11 +34865,14 @@ async function followMerged(sqliteClient, jobsDir, options) {
|
|
|
33415
34865
|
contextPct: meta.contextPct,
|
|
33416
34866
|
colorize
|
|
33417
34867
|
}));
|
|
34868
|
+
const startupContextLine = formatStartupContextLine(event);
|
|
34869
|
+
if (startupContextLine)
|
|
34870
|
+
console.log(startupContextLine);
|
|
33418
34871
|
}
|
|
33419
34872
|
}
|
|
33420
34873
|
if (!options.forever && trackedJobs.size > 0 && completedJobs.size === trackedJobs.size) {
|
|
33421
34874
|
clearInterval(interval);
|
|
33422
|
-
|
|
34875
|
+
resolve8();
|
|
33423
34876
|
}
|
|
33424
34877
|
}, 500);
|
|
33425
34878
|
});
|
|
@@ -33428,7 +34881,7 @@ async function run17() {
|
|
|
33428
34881
|
const options = parseArgs9(process.argv.slice(3));
|
|
33429
34882
|
const sqliteClient = createObservabilitySqliteClient();
|
|
33430
34883
|
try {
|
|
33431
|
-
const jobsDir =
|
|
34884
|
+
const jobsDir = join24(process.cwd(), ".specialists", "jobs");
|
|
33432
34885
|
if (!existsSync21(jobsDir)) {
|
|
33433
34886
|
console.log(dim8("No jobs directory found."));
|
|
33434
34887
|
return;
|
|
@@ -33468,8 +34921,8 @@ var exports_poll = {};
|
|
|
33468
34921
|
__export(exports_poll, {
|
|
33469
34922
|
run: () => run18
|
|
33470
34923
|
});
|
|
33471
|
-
import { existsSync as existsSync22, readFileSync as
|
|
33472
|
-
import { join as
|
|
34924
|
+
import { existsSync as existsSync22, readFileSync as readFileSync20 } from "fs";
|
|
34925
|
+
import { join as join25 } from "path";
|
|
33473
34926
|
function parseArgs10(argv) {
|
|
33474
34927
|
let jobId;
|
|
33475
34928
|
let cursor = 0;
|
|
@@ -33506,19 +34959,19 @@ function parseArgs10(argv) {
|
|
|
33506
34959
|
return { jobId, cursor, outputCursor };
|
|
33507
34960
|
}
|
|
33508
34961
|
function readJobState(jobsDir, jobId, cursor, outputCursor) {
|
|
33509
|
-
const jobDir =
|
|
33510
|
-
const statusPath =
|
|
34962
|
+
const jobDir = join25(jobsDir, jobId);
|
|
34963
|
+
const statusPath = join25(jobDir, "status.json");
|
|
33511
34964
|
let status = null;
|
|
33512
34965
|
if (existsSync22(statusPath)) {
|
|
33513
34966
|
try {
|
|
33514
|
-
status = JSON.parse(
|
|
34967
|
+
status = JSON.parse(readFileSync20(statusPath, "utf-8"));
|
|
33515
34968
|
} catch {}
|
|
33516
34969
|
}
|
|
33517
|
-
const resultPath =
|
|
34970
|
+
const resultPath = join25(jobDir, "result.txt");
|
|
33518
34971
|
let fullOutput = "";
|
|
33519
34972
|
if (existsSync22(resultPath)) {
|
|
33520
34973
|
try {
|
|
33521
|
-
fullOutput =
|
|
34974
|
+
fullOutput = readFileSync20(resultPath, "utf-8");
|
|
33522
34975
|
} catch {}
|
|
33523
34976
|
}
|
|
33524
34977
|
const events = readJobEventsById(jobsDir, jobId);
|
|
@@ -33550,8 +35003,8 @@ function readJobState(jobsDir, jobId, cursor, outputCursor) {
|
|
|
33550
35003
|
}
|
|
33551
35004
|
async function run18() {
|
|
33552
35005
|
const { jobId, cursor, outputCursor } = parseArgs10(process.argv.slice(3));
|
|
33553
|
-
const jobsDir =
|
|
33554
|
-
const jobDir =
|
|
35006
|
+
const jobsDir = join25(process.cwd(), ".specialists", "jobs");
|
|
35007
|
+
const jobDir = join25(jobsDir, jobId);
|
|
33555
35008
|
if (!existsSync22(jobDir)) {
|
|
33556
35009
|
const result2 = {
|
|
33557
35010
|
job_id: jobId,
|
|
@@ -33583,7 +35036,7 @@ var exports_steer = {};
|
|
|
33583
35036
|
__export(exports_steer, {
|
|
33584
35037
|
run: () => run19
|
|
33585
35038
|
});
|
|
33586
|
-
import { writeFileSync as
|
|
35039
|
+
import { writeFileSync as writeFileSync9 } from "fs";
|
|
33587
35040
|
async function run19() {
|
|
33588
35041
|
const jobId = process.argv[3];
|
|
33589
35042
|
const message = process.argv[4];
|
|
@@ -33614,7 +35067,7 @@ async function run19() {
|
|
|
33614
35067
|
try {
|
|
33615
35068
|
const payload = JSON.stringify({ type: "steer", message }) + `
|
|
33616
35069
|
`;
|
|
33617
|
-
|
|
35070
|
+
writeFileSync9(status.fifo_path, payload, { flag: "a" });
|
|
33618
35071
|
process.stdout.write(`${green10("\u2713")} Steer message sent to job ${jobId}
|
|
33619
35072
|
`);
|
|
33620
35073
|
} catch (err) {
|
|
@@ -33637,7 +35090,7 @@ var exports_resume = {};
|
|
|
33637
35090
|
__export(exports_resume, {
|
|
33638
35091
|
run: () => run20
|
|
33639
35092
|
});
|
|
33640
|
-
import { writeFileSync as
|
|
35093
|
+
import { writeFileSync as writeFileSync10 } from "fs";
|
|
33641
35094
|
async function run20() {
|
|
33642
35095
|
const jobId = process.argv[3];
|
|
33643
35096
|
const task = process.argv[4];
|
|
@@ -33668,7 +35121,7 @@ async function run20() {
|
|
|
33668
35121
|
try {
|
|
33669
35122
|
const payload = JSON.stringify({ type: "resume", task }) + `
|
|
33670
35123
|
`;
|
|
33671
|
-
|
|
35124
|
+
writeFileSync10(status.fifo_path, payload, { flag: "a" });
|
|
33672
35125
|
process.stdout.write(`${green11("\u2713")} Resume sent to job ${jobId}
|
|
33673
35126
|
`);
|
|
33674
35127
|
process.stdout.write(` Use 'specialists feed ${jobId} --follow' to watch the response.
|
|
@@ -33700,15 +35153,15 @@ async function run21() {
|
|
|
33700
35153
|
}
|
|
33701
35154
|
|
|
33702
35155
|
// src/specialist/worktree-gc.ts
|
|
33703
|
-
import { existsSync as existsSync23, readdirSync as readdirSync11, readFileSync as
|
|
33704
|
-
import { join as
|
|
35156
|
+
import { existsSync as existsSync23, readdirSync as readdirSync11, readFileSync as readFileSync21 } from "fs";
|
|
35157
|
+
import { join as join26 } from "path";
|
|
33705
35158
|
import { spawnSync as spawnSync18 } from "child_process";
|
|
33706
35159
|
function readJobStatus2(jobDir) {
|
|
33707
|
-
const statusPath =
|
|
35160
|
+
const statusPath = join26(jobDir, "status.json");
|
|
33708
35161
|
if (!existsSync23(statusPath))
|
|
33709
35162
|
return null;
|
|
33710
35163
|
try {
|
|
33711
|
-
return JSON.parse(
|
|
35164
|
+
return JSON.parse(readFileSync21(statusPath, "utf-8"));
|
|
33712
35165
|
} catch {
|
|
33713
35166
|
return null;
|
|
33714
35167
|
}
|
|
@@ -33726,7 +35179,7 @@ function collectWorktreeGcCandidates(jobsDir) {
|
|
|
33726
35179
|
for (const entry of readdirSync11(jobsDir, { withFileTypes: true })) {
|
|
33727
35180
|
if (!entry.isDirectory())
|
|
33728
35181
|
continue;
|
|
33729
|
-
const status = readJobStatus2(
|
|
35182
|
+
const status = readJobStatus2(join26(jobsDir, entry.name));
|
|
33730
35183
|
if (!status)
|
|
33731
35184
|
continue;
|
|
33732
35185
|
if (isActive(status.status))
|
|
@@ -33784,11 +35237,11 @@ __export(exports_clean, {
|
|
|
33784
35237
|
import {
|
|
33785
35238
|
existsSync as existsSync24,
|
|
33786
35239
|
readdirSync as readdirSync12,
|
|
33787
|
-
readFileSync as
|
|
33788
|
-
rmSync as
|
|
33789
|
-
statSync as
|
|
35240
|
+
readFileSync as readFileSync22,
|
|
35241
|
+
rmSync as rmSync3,
|
|
35242
|
+
statSync as statSync4
|
|
33790
35243
|
} from "fs";
|
|
33791
|
-
import { join as
|
|
35244
|
+
import { join as join27 } from "path";
|
|
33792
35245
|
function parseTtlDaysFromEnvironment() {
|
|
33793
35246
|
const rawValue = process.env.SPECIALISTS_JOB_TTL_DAYS ?? process.env.JOB_TTL_DAYS;
|
|
33794
35247
|
if (!rawValue)
|
|
@@ -33844,8 +35297,8 @@ function readDirectorySizeBytes(directoryPath) {
|
|
|
33844
35297
|
let totalBytes = 0;
|
|
33845
35298
|
const entries = readdirSync12(directoryPath, { withFileTypes: true });
|
|
33846
35299
|
for (const entry of entries) {
|
|
33847
|
-
const entryPath =
|
|
33848
|
-
const stats =
|
|
35300
|
+
const entryPath = join27(directoryPath, entry.name);
|
|
35301
|
+
const stats = statSync4(entryPath);
|
|
33849
35302
|
if (stats.isDirectory()) {
|
|
33850
35303
|
totalBytes += readDirectorySizeBytes(entryPath);
|
|
33851
35304
|
continue;
|
|
@@ -33857,7 +35310,7 @@ function readDirectorySizeBytes(directoryPath) {
|
|
|
33857
35310
|
function containsProtectedSqliteArtifact(directoryPath) {
|
|
33858
35311
|
const entries = readdirSync12(directoryPath, { withFileTypes: true });
|
|
33859
35312
|
for (const entry of entries) {
|
|
33860
|
-
const entryPath =
|
|
35313
|
+
const entryPath = join27(directoryPath, entry.name);
|
|
33861
35314
|
if (entry.isDirectory()) {
|
|
33862
35315
|
if (containsProtectedSqliteArtifact(entryPath))
|
|
33863
35316
|
return true;
|
|
@@ -33872,21 +35325,21 @@ function containsProtectedSqliteArtifact(directoryPath) {
|
|
|
33872
35325
|
function readCompletedJobDirectory(baseDirectory, entry) {
|
|
33873
35326
|
if (!entry.isDirectory())
|
|
33874
35327
|
return null;
|
|
33875
|
-
const directoryPath =
|
|
35328
|
+
const directoryPath = join27(baseDirectory, entry.name);
|
|
33876
35329
|
if (containsProtectedSqliteArtifact(directoryPath))
|
|
33877
35330
|
return null;
|
|
33878
|
-
const statusFilePath =
|
|
35331
|
+
const statusFilePath = join27(directoryPath, "status.json");
|
|
33879
35332
|
if (!existsSync24(statusFilePath))
|
|
33880
35333
|
return null;
|
|
33881
35334
|
let statusData;
|
|
33882
35335
|
try {
|
|
33883
|
-
statusData = JSON.parse(
|
|
35336
|
+
statusData = JSON.parse(readFileSync22(statusFilePath, "utf-8"));
|
|
33884
35337
|
} catch {
|
|
33885
35338
|
return null;
|
|
33886
35339
|
}
|
|
33887
35340
|
if (!COMPLETED_STATUSES.has(statusData.status))
|
|
33888
35341
|
return null;
|
|
33889
|
-
const directoryStats =
|
|
35342
|
+
const directoryStats = statSync4(directoryPath);
|
|
33890
35343
|
return {
|
|
33891
35344
|
id: entry.name,
|
|
33892
35345
|
directoryPath,
|
|
@@ -33923,7 +35376,7 @@ function selectJobsToRemove(completedJobs, options) {
|
|
|
33923
35376
|
const cutoffMs = Date.now() - ttlDays * MS_PER_DAY;
|
|
33924
35377
|
return jobsByNewest.filter((job) => job.modifiedAtMs < cutoffMs);
|
|
33925
35378
|
}
|
|
33926
|
-
function
|
|
35379
|
+
function formatBytes2(bytes) {
|
|
33927
35380
|
if (bytes < 1024)
|
|
33928
35381
|
return `${bytes} B`;
|
|
33929
35382
|
const units = ["KB", "MB", "GB", "TB"];
|
|
@@ -33938,7 +35391,7 @@ function formatBytes(bytes) {
|
|
|
33938
35391
|
function renderSummary(removedCount, freedBytes, dryRun) {
|
|
33939
35392
|
const action = dryRun ? "Would remove" : "Removed";
|
|
33940
35393
|
const noun = removedCount === 1 ? "directory" : "directories";
|
|
33941
|
-
return `${action} ${removedCount} job ${noun} (${
|
|
35394
|
+
return `${action} ${removedCount} job ${noun} (${formatBytes2(freedBytes)} freed)`;
|
|
33942
35395
|
}
|
|
33943
35396
|
function printDryRunPlan(jobs) {
|
|
33944
35397
|
if (jobs.length === 0)
|
|
@@ -33992,7 +35445,7 @@ async function run22() {
|
|
|
33992
35445
|
return;
|
|
33993
35446
|
}
|
|
33994
35447
|
for (const job of jobsToRemove) {
|
|
33995
|
-
|
|
35448
|
+
rmSync3(job.directoryPath, { recursive: true, force: true });
|
|
33996
35449
|
}
|
|
33997
35450
|
console.log(renderSummary(jobsToRemove.length, freedBytes, false));
|
|
33998
35451
|
if (worktreeCandidates.length > 0) {
|
|
@@ -34115,6 +35568,10 @@ async function run23() {
|
|
|
34115
35568
|
const guard = checkEpicUnresolvedGuard(beadId);
|
|
34116
35569
|
if (guard.blocked && guard.epicId && guard.epicStatus && isEpicUnresolvedState(guard.epicStatus)) {
|
|
34117
35570
|
console.log(`Chain ${beadId} belongs to unresolved epic ${guard.epicId} (${guard.epicStatus}).`);
|
|
35571
|
+
if (guard.epicStatus === "open") {
|
|
35572
|
+
console.log(`Epic ${guard.epicId} still open. Run: sp epic resolve ${guard.epicId}`);
|
|
35573
|
+
process.exit(1);
|
|
35574
|
+
}
|
|
34118
35575
|
console.log(`Redirecting session close publication to epic merge (${options.pr ? "PR mode" : "direct mode"}).`);
|
|
34119
35576
|
const args = ["merge", guard.epicId, ...options.rebuild ? ["--rebuild"] : [], ...options.pr ? ["--pr"] : []];
|
|
34120
35577
|
await handleEpicMergeCommand(args);
|
|
@@ -34137,10 +35594,51 @@ __export(exports_stop, {
|
|
|
34137
35594
|
function resolveTerminalStatus(jobId) {
|
|
34138
35595
|
return hasRunCompleteEvent(jobId) ? "done" : "cancelled";
|
|
34139
35596
|
}
|
|
35597
|
+
function parseStopArgs(argv) {
|
|
35598
|
+
let jobId;
|
|
35599
|
+
let force = false;
|
|
35600
|
+
for (const token of argv) {
|
|
35601
|
+
if (token === "--force") {
|
|
35602
|
+
force = true;
|
|
35603
|
+
continue;
|
|
35604
|
+
}
|
|
35605
|
+
if (!token.startsWith("-") && !jobId) {
|
|
35606
|
+
jobId = token;
|
|
35607
|
+
continue;
|
|
35608
|
+
}
|
|
35609
|
+
throw new Error(`Unknown option: ${token}`);
|
|
35610
|
+
}
|
|
35611
|
+
return { jobId, force };
|
|
35612
|
+
}
|
|
35613
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
35614
|
+
const deadline = Date.now() + timeoutMs;
|
|
35615
|
+
while (Date.now() < deadline) {
|
|
35616
|
+
if (!isProcessAlive(pid))
|
|
35617
|
+
return true;
|
|
35618
|
+
await new Promise((resolve8) => setTimeout(resolve8, 100));
|
|
35619
|
+
}
|
|
35620
|
+
return !isProcessAlive(pid);
|
|
35621
|
+
}
|
|
35622
|
+
function tryKillProcessGroup(pid) {
|
|
35623
|
+
try {
|
|
35624
|
+
process.kill(-pid, "SIGKILL");
|
|
35625
|
+
} catch (err) {
|
|
35626
|
+
if (err.code !== "ESRCH")
|
|
35627
|
+
throw err;
|
|
35628
|
+
}
|
|
35629
|
+
}
|
|
34140
35630
|
async function run24() {
|
|
34141
|
-
|
|
35631
|
+
let parsed;
|
|
35632
|
+
try {
|
|
35633
|
+
parsed = parseStopArgs(process.argv.slice(3));
|
|
35634
|
+
} catch (err) {
|
|
35635
|
+
console.error(err.message);
|
|
35636
|
+
console.error("Usage: specialists|sp stop <job-id> [--force]");
|
|
35637
|
+
process.exit(1);
|
|
35638
|
+
}
|
|
35639
|
+
const { jobId, force } = parsed;
|
|
34142
35640
|
if (!jobId) {
|
|
34143
|
-
console.error("Usage: specialists|sp stop <job-id>");
|
|
35641
|
+
console.error("Usage: specialists|sp stop <job-id> [--force]");
|
|
34144
35642
|
process.exit(1);
|
|
34145
35643
|
}
|
|
34146
35644
|
const jobsDir = resolveJobsDir(process.cwd());
|
|
@@ -34161,33 +35659,53 @@ async function run24() {
|
|
|
34161
35659
|
`);
|
|
34162
35660
|
process.exit(1);
|
|
34163
35661
|
}
|
|
35662
|
+
const pid = status.pid;
|
|
34164
35663
|
const tmuxSession = status.tmux_session;
|
|
34165
|
-
const
|
|
34166
|
-
|
|
34167
|
-
|
|
34168
|
-
|
|
34169
|
-
process.stdout.write(`${green12("\u2713")} Marked ${jobId} as
|
|
35664
|
+
const isAlreadyDead = !isProcessAlive(pid, status.started_at_ms);
|
|
35665
|
+
if (force && isAlreadyDead) {
|
|
35666
|
+
supervisor.updateJobStatus(jobId, "error");
|
|
35667
|
+
tryKillProcessGroup(pid);
|
|
35668
|
+
process.stdout.write(`${green12("\u2713")} Marked ${jobId} as error (PID ${pid} already dead)
|
|
34170
35669
|
`);
|
|
34171
|
-
|
|
34172
|
-
|
|
34173
|
-
|
|
34174
|
-
|
|
34175
|
-
|
|
34176
|
-
|
|
34177
|
-
if (err.code === "ESRCH") {
|
|
34178
|
-
process.stderr.write(`${red6(`Process ${status.pid} not found.`)} Job may have already completed.
|
|
35670
|
+
} else {
|
|
35671
|
+
const terminalStatus = resolveTerminalStatus(jobId);
|
|
35672
|
+
supervisor.updateJobStatus(jobId, terminalStatus);
|
|
35673
|
+
try {
|
|
35674
|
+
process.kill(pid, "SIGTERM");
|
|
35675
|
+
process.stdout.write(`${green12("\u2713")} Marked ${jobId} as ${terminalStatus} and sent SIGTERM to PID ${pid}
|
|
34179
35676
|
`);
|
|
34180
|
-
if (
|
|
34181
|
-
|
|
34182
|
-
|
|
35677
|
+
if (force) {
|
|
35678
|
+
const exited = await waitForProcessExit(pid, 5000);
|
|
35679
|
+
if (!exited) {
|
|
35680
|
+
supervisor.updateJobStatus(jobId, "error");
|
|
35681
|
+
tryKillProcessGroup(pid);
|
|
35682
|
+
process.stderr.write(`${red6("Force stop:")} PID ${pid} ignored SIGTERM, marked ${jobId} as error and killed process group.
|
|
34183
35683
|
`);
|
|
35684
|
+
}
|
|
34184
35685
|
}
|
|
34185
|
-
}
|
|
34186
|
-
|
|
35686
|
+
} catch (err) {
|
|
35687
|
+
if (err.code === "ESRCH") {
|
|
35688
|
+
if (force) {
|
|
35689
|
+
supervisor.updateJobStatus(jobId, "error");
|
|
35690
|
+
tryKillProcessGroup(pid);
|
|
35691
|
+
process.stdout.write(`${green12("\u2713")} Marked ${jobId} as error (PID ${pid} already gone)
|
|
34187
35692
|
`);
|
|
34188
|
-
|
|
35693
|
+
} else {
|
|
35694
|
+
process.stderr.write(`${red6(`Process ${pid} not found.`)} Job may have already completed.
|
|
35695
|
+
`);
|
|
35696
|
+
}
|
|
35697
|
+
} else {
|
|
35698
|
+
process.stderr.write(`${red6("Error:")} ${err.message}
|
|
35699
|
+
`);
|
|
35700
|
+
process.exit(1);
|
|
35701
|
+
}
|
|
34189
35702
|
}
|
|
34190
35703
|
}
|
|
35704
|
+
if (tmuxSession) {
|
|
35705
|
+
killTmuxSession(tmuxSession);
|
|
35706
|
+
process.stdout.write(`${dim11(` tmux session ${tmuxSession} killed`)}
|
|
35707
|
+
`);
|
|
35708
|
+
}
|
|
34191
35709
|
} finally {
|
|
34192
35710
|
await supervisor.dispose();
|
|
34193
35711
|
}
|
|
@@ -34197,6 +35715,7 @@ var init_stop = __esm(() => {
|
|
|
34197
35715
|
init_supervisor();
|
|
34198
35716
|
init_job_root();
|
|
34199
35717
|
init_observability_sqlite();
|
|
35718
|
+
init_process_liveness();
|
|
34200
35719
|
init_tmux_utils();
|
|
34201
35720
|
});
|
|
34202
35721
|
|
|
@@ -34206,15 +35725,15 @@ __export(exports_attach, {
|
|
|
34206
35725
|
run: () => run25
|
|
34207
35726
|
});
|
|
34208
35727
|
import { execFileSync as execFileSync3, spawnSync as spawnSync20 } from "child_process";
|
|
34209
|
-
import { readFileSync as
|
|
34210
|
-
import { join as
|
|
35728
|
+
import { readFileSync as readFileSync23 } from "fs";
|
|
35729
|
+
import { join as join28 } from "path";
|
|
34211
35730
|
function exitWithError(message) {
|
|
34212
35731
|
console.error(message);
|
|
34213
35732
|
process.exit(1);
|
|
34214
35733
|
}
|
|
34215
35734
|
function readStatus(statusPath, jobId) {
|
|
34216
35735
|
try {
|
|
34217
|
-
return JSON.parse(
|
|
35736
|
+
return JSON.parse(readFileSync23(statusPath, "utf-8"));
|
|
34218
35737
|
} catch (error2) {
|
|
34219
35738
|
if (error2 && typeof error2 === "object" && "code" in error2 && error2.code === "ENOENT") {
|
|
34220
35739
|
exitWithError(`Job \`${jobId}\` not found. Run \`specialists status\` to see active jobs.`);
|
|
@@ -34228,8 +35747,8 @@ async function run25() {
|
|
|
34228
35747
|
if (!jobId) {
|
|
34229
35748
|
exitWithError("Usage: specialists attach <job-id>");
|
|
34230
35749
|
}
|
|
34231
|
-
const jobsDir =
|
|
34232
|
-
const statusPath =
|
|
35750
|
+
const jobsDir = join28(process.cwd(), ".specialists", "jobs");
|
|
35751
|
+
const statusPath = join28(jobsDir, jobId, "status.json");
|
|
34233
35752
|
const status = readStatus(statusPath, jobId);
|
|
34234
35753
|
if (status.status === "done" || status.status === "error") {
|
|
34235
35754
|
exitWithError(`Job \`${jobId}\` has already completed (status: ${status.status}). Use \`specialists result ${jobId}\` to read output.`);
|
|
@@ -34490,8 +36009,8 @@ __export(exports_doctor, {
|
|
|
34490
36009
|
});
|
|
34491
36010
|
import { createHash as createHash4 } from "crypto";
|
|
34492
36011
|
import { spawnSync as spawnSync21 } from "child_process";
|
|
34493
|
-
import { existsSync as existsSync25, lstatSync as lstatSync2, mkdirSync as
|
|
34494
|
-
import { dirname as
|
|
36012
|
+
import { existsSync as existsSync25, lstatSync as lstatSync2, mkdirSync as mkdirSync8, readdirSync as readdirSync13, readFileSync as readFileSync24, readlinkSync as readlinkSync2, writeFileSync as writeFileSync11 } from "fs";
|
|
36013
|
+
import { dirname as dirname7, join as join29, relative as relative2, resolve as resolve8 } from "path";
|
|
34495
36014
|
function ok3(msg) {
|
|
34496
36015
|
console.log(` ${green14("\u2713")} ${msg}`);
|
|
34497
36016
|
}
|
|
@@ -34523,7 +36042,7 @@ function loadJson2(path) {
|
|
|
34523
36042
|
if (!existsSync25(path))
|
|
34524
36043
|
return null;
|
|
34525
36044
|
try {
|
|
34526
|
-
return JSON.parse(
|
|
36045
|
+
return JSON.parse(readFileSync24(path, "utf8"));
|
|
34527
36046
|
} catch {
|
|
34528
36047
|
return null;
|
|
34529
36048
|
}
|
|
@@ -34566,7 +36085,7 @@ function checkBd() {
|
|
|
34566
36085
|
return false;
|
|
34567
36086
|
}
|
|
34568
36087
|
ok3(`bd installed ${dim13(sp("bd", ["--version"]).stdout || "")}`);
|
|
34569
|
-
if (existsSync25(
|
|
36088
|
+
if (existsSync25(join29(CWD, ".beads")))
|
|
34570
36089
|
ok3(".beads/ present in project");
|
|
34571
36090
|
else
|
|
34572
36091
|
warn3(".beads/ not found in project");
|
|
@@ -34586,7 +36105,7 @@ function checkHooks() {
|
|
|
34586
36105
|
section3("Claude Code hooks (2 expected)");
|
|
34587
36106
|
let allPresent = true;
|
|
34588
36107
|
for (const name of HOOK_NAMES) {
|
|
34589
|
-
const canonicalPath =
|
|
36108
|
+
const canonicalPath = join29(HOOKS_DIR, name);
|
|
34590
36109
|
if (!existsSync25(canonicalPath)) {
|
|
34591
36110
|
fail4(`${relative2(CWD, canonicalPath)} ${red7("missing")}`);
|
|
34592
36111
|
fix("specialists init");
|
|
@@ -34594,10 +36113,10 @@ function checkHooks() {
|
|
|
34594
36113
|
} else {
|
|
34595
36114
|
ok3(relative2(CWD, canonicalPath));
|
|
34596
36115
|
}
|
|
34597
|
-
const claudeHookPath =
|
|
36116
|
+
const claudeHookPath = join29(CLAUDE_HOOKS_DIR, name);
|
|
34598
36117
|
const symlinkState = isSymlinkTo(claudeHookPath, canonicalPath);
|
|
34599
36118
|
if (symlinkState.ok) {
|
|
34600
|
-
ok3(`${relative2(CWD, claudeHookPath)} -> ${relative2(
|
|
36119
|
+
ok3(`${relative2(CWD, claudeHookPath)} -> ${relative2(dirname7(claudeHookPath), canonicalPath)}`);
|
|
34601
36120
|
continue;
|
|
34602
36121
|
}
|
|
34603
36122
|
allPresent = false;
|
|
@@ -34649,14 +36168,14 @@ function checkMCP() {
|
|
|
34649
36168
|
}
|
|
34650
36169
|
function hashFile(path) {
|
|
34651
36170
|
const hash = createHash4("sha256");
|
|
34652
|
-
hash.update(
|
|
36171
|
+
hash.update(readFileSync24(path));
|
|
34653
36172
|
return hash.digest("hex");
|
|
34654
36173
|
}
|
|
34655
36174
|
function collectFileHashes(rootDir) {
|
|
34656
36175
|
const hashes = new Map;
|
|
34657
36176
|
const visit2 = (dir) => {
|
|
34658
36177
|
for (const entry of readdirSync13(dir, { withFileTypes: true })) {
|
|
34659
|
-
const fullPath =
|
|
36178
|
+
const fullPath = join29(dir, entry.name);
|
|
34660
36179
|
if (entry.isDirectory()) {
|
|
34661
36180
|
visit2(fullPath);
|
|
34662
36181
|
continue;
|
|
@@ -34684,8 +36203,8 @@ function isSymlinkTo(linkPath, expectedTargetPath) {
|
|
|
34684
36203
|
return { ok: false, reason: "not-symlink" };
|
|
34685
36204
|
try {
|
|
34686
36205
|
const rawTarget = readlinkSync2(linkPath);
|
|
34687
|
-
const resolvedTarget =
|
|
34688
|
-
const resolvedExpected =
|
|
36206
|
+
const resolvedTarget = resolve8(dirname7(linkPath), rawTarget);
|
|
36207
|
+
const resolvedExpected = resolve8(expectedTargetPath);
|
|
34689
36208
|
if (resolvedTarget !== resolvedExpected) {
|
|
34690
36209
|
return { ok: false, reason: "wrong-target", target: rawTarget };
|
|
34691
36210
|
}
|
|
@@ -34743,7 +36262,7 @@ function checkSkillDrift() {
|
|
|
34743
36262
|
}
|
|
34744
36263
|
let linksOk = true;
|
|
34745
36264
|
for (const scope of ["claude", "pi"]) {
|
|
34746
|
-
const activeRoot =
|
|
36265
|
+
const activeRoot = join29(XTRM_ACTIVE_SKILLS_DIR, scope);
|
|
34747
36266
|
if (!existsSync25(activeRoot)) {
|
|
34748
36267
|
fail4(`${relative2(CWD, activeRoot)}/ missing`);
|
|
34749
36268
|
fix("specialists init --sync-skills");
|
|
@@ -34752,8 +36271,8 @@ function checkSkillDrift() {
|
|
|
34752
36271
|
}
|
|
34753
36272
|
const defaultSkills = readdirSync13(XTRM_DEFAULT_SKILLS_DIR, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
34754
36273
|
for (const skillName of defaultSkills) {
|
|
34755
|
-
const activeLinkPath =
|
|
34756
|
-
const expectedTarget =
|
|
36274
|
+
const activeLinkPath = join29(activeRoot, skillName);
|
|
36275
|
+
const expectedTarget = join29(XTRM_DEFAULT_SKILLS_DIR, skillName);
|
|
34757
36276
|
const state = isSymlinkTo(activeLinkPath, expectedTarget);
|
|
34758
36277
|
if (state.ok)
|
|
34759
36278
|
continue;
|
|
@@ -34772,14 +36291,14 @@ function checkSkillDrift() {
|
|
|
34772
36291
|
}
|
|
34773
36292
|
}
|
|
34774
36293
|
const skillRootChecks = [
|
|
34775
|
-
{ root:
|
|
34776
|
-
{ root:
|
|
36294
|
+
{ root: join29(CLAUDE_DIR, "skills"), expected: ACTIVE_CLAUDE_SKILLS_DIR },
|
|
36295
|
+
{ root: join29(PI_DIR, "skills"), expected: ACTIVE_PI_SKILLS_DIR }
|
|
34777
36296
|
];
|
|
34778
36297
|
let rootLinksOk = true;
|
|
34779
36298
|
for (const check2 of skillRootChecks) {
|
|
34780
36299
|
const state = isSymlinkTo(check2.root, check2.expected);
|
|
34781
36300
|
if (state.ok) {
|
|
34782
|
-
ok3(`${relative2(CWD, check2.root)} -> ${relative2(
|
|
36301
|
+
ok3(`${relative2(CWD, check2.root)} -> ${relative2(dirname7(check2.root), check2.expected)}`);
|
|
34783
36302
|
continue;
|
|
34784
36303
|
}
|
|
34785
36304
|
rootLinksOk = false;
|
|
@@ -34799,9 +36318,9 @@ function checkSkillDrift() {
|
|
|
34799
36318
|
}
|
|
34800
36319
|
function checkRuntimeDirs() {
|
|
34801
36320
|
section3(".specialists/ runtime directories");
|
|
34802
|
-
const rootDir =
|
|
34803
|
-
const jobsDir =
|
|
34804
|
-
const readyDir =
|
|
36321
|
+
const rootDir = join29(CWD, ".specialists");
|
|
36322
|
+
const jobsDir = join29(rootDir, "jobs");
|
|
36323
|
+
const readyDir = join29(rootDir, "ready");
|
|
34805
36324
|
let allOk = true;
|
|
34806
36325
|
if (!existsSync25(rootDir)) {
|
|
34807
36326
|
warn3(".specialists/ not found in current project");
|
|
@@ -34812,7 +36331,7 @@ function checkRuntimeDirs() {
|
|
|
34812
36331
|
for (const [subDir, label] of [[jobsDir, "jobs"], [readyDir, "ready"]]) {
|
|
34813
36332
|
if (!existsSync25(subDir)) {
|
|
34814
36333
|
warn3(`.specialists/${label}/ missing \u2014 auto-creating`);
|
|
34815
|
-
|
|
36334
|
+
mkdirSync8(subDir, { recursive: true });
|
|
34816
36335
|
ok3(`.specialists/${label}/ created`);
|
|
34817
36336
|
} else {
|
|
34818
36337
|
ok3(`.specialists/${label}/ present`);
|
|
@@ -34843,10 +36362,10 @@ function compareVersions(left, right) {
|
|
|
34843
36362
|
}
|
|
34844
36363
|
function setStatusError(statusPath) {
|
|
34845
36364
|
try {
|
|
34846
|
-
const raw =
|
|
36365
|
+
const raw = readFileSync24(statusPath, "utf8");
|
|
34847
36366
|
const status = JSON.parse(raw);
|
|
34848
36367
|
status.status = "error";
|
|
34849
|
-
|
|
36368
|
+
writeFileSync11(statusPath, `${JSON.stringify(status, null, 2)}
|
|
34850
36369
|
`, "utf8");
|
|
34851
36370
|
} catch {}
|
|
34852
36371
|
}
|
|
@@ -34865,11 +36384,11 @@ function cleanupProcesses(jobsDir, dryRun) {
|
|
|
34865
36384
|
zombieJobIds: []
|
|
34866
36385
|
};
|
|
34867
36386
|
for (const jobId of entries) {
|
|
34868
|
-
const statusPath =
|
|
36387
|
+
const statusPath = join29(jobsDir, jobId, "status.json");
|
|
34869
36388
|
if (!existsSync25(statusPath))
|
|
34870
36389
|
continue;
|
|
34871
36390
|
try {
|
|
34872
|
-
const status = JSON.parse(
|
|
36391
|
+
const status = JSON.parse(readFileSync24(statusPath, "utf8"));
|
|
34873
36392
|
result.total += 1;
|
|
34874
36393
|
if (status.status !== "running" && status.status !== "starting")
|
|
34875
36394
|
continue;
|
|
@@ -34900,9 +36419,52 @@ function renderProcessSummary(result, dryRun) {
|
|
|
34900
36419
|
const action = dryRun ? "would be marked error" : "marked error";
|
|
34901
36420
|
return `${result.zombies} zombie job${result.zombies === 1 ? "" : "s"} found (${result.updated} ${action})`;
|
|
34902
36421
|
}
|
|
36422
|
+
function runDoctorOrphans() {
|
|
36423
|
+
const sqliteClient = createObservabilitySqliteClient();
|
|
36424
|
+
if (!sqliteClient) {
|
|
36425
|
+
console.log(`
|
|
36426
|
+
${bold13("specialists doctor orphans")}
|
|
36427
|
+
`);
|
|
36428
|
+
fail4("observability SQLite not available");
|
|
36429
|
+
fix("specialists db setup");
|
|
36430
|
+
console.log("");
|
|
36431
|
+
process.exit(1);
|
|
36432
|
+
}
|
|
36433
|
+
try {
|
|
36434
|
+
const findings = sqliteClient.scanOrphans();
|
|
36435
|
+
const byKind = {
|
|
36436
|
+
orphan: findings.filter((item) => item.kind === "orphan"),
|
|
36437
|
+
stalePointer: findings.filter((item) => item.kind === "stale-pointer"),
|
|
36438
|
+
integrity: findings.filter((item) => item.kind === "integrity-violation")
|
|
36439
|
+
};
|
|
36440
|
+
console.log(`
|
|
36441
|
+
${bold13("specialists doctor orphans")}
|
|
36442
|
+
`);
|
|
36443
|
+
if (findings.length === 0) {
|
|
36444
|
+
ok3("No orphan/stale/integrity findings");
|
|
36445
|
+
console.log("");
|
|
36446
|
+
return;
|
|
36447
|
+
}
|
|
36448
|
+
const renderGroup = (label, rows) => {
|
|
36449
|
+
if (rows.length === 0)
|
|
36450
|
+
return;
|
|
36451
|
+
console.log(` ${yellow12("\u25CB")} ${label}: ${rows.length}`);
|
|
36452
|
+
for (const row of rows) {
|
|
36453
|
+
console.log(` - [${row.code}] ${row.message}`);
|
|
36454
|
+
}
|
|
36455
|
+
};
|
|
36456
|
+
renderGroup("orphan", byKind.orphan);
|
|
36457
|
+
renderGroup("stale-pointer", byKind.stalePointer);
|
|
36458
|
+
renderGroup("integrity-violation", byKind.integrity);
|
|
36459
|
+
console.log("");
|
|
36460
|
+
process.exit(1);
|
|
36461
|
+
} finally {
|
|
36462
|
+
sqliteClient.close();
|
|
36463
|
+
}
|
|
36464
|
+
}
|
|
34903
36465
|
function checkZombieJobs() {
|
|
34904
36466
|
section3("Background jobs");
|
|
34905
|
-
const jobsDir =
|
|
36467
|
+
const jobsDir = join29(CWD, ".specialists", "jobs");
|
|
34906
36468
|
if (!existsSync25(jobsDir)) {
|
|
34907
36469
|
hint("No .specialists/jobs/ \u2014 skipping");
|
|
34908
36470
|
return true;
|
|
@@ -34921,7 +36483,16 @@ function checkZombieJobs() {
|
|
|
34921
36483
|
}
|
|
34922
36484
|
return result.zombies === 0;
|
|
34923
36485
|
}
|
|
34924
|
-
async function run27() {
|
|
36486
|
+
async function run27(argv = process.argv.slice(3)) {
|
|
36487
|
+
const subcommand = argv[0];
|
|
36488
|
+
if (subcommand === "orphans") {
|
|
36489
|
+
runDoctorOrphans();
|
|
36490
|
+
return;
|
|
36491
|
+
}
|
|
36492
|
+
if (subcommand && subcommand !== "--help" && subcommand !== "-h") {
|
|
36493
|
+
console.error(`Unknown doctor subcommand: '${subcommand}'`);
|
|
36494
|
+
process.exit(1);
|
|
36495
|
+
}
|
|
34925
36496
|
console.log(`
|
|
34926
36497
|
${bold13("specialists doctor")}
|
|
34927
36498
|
`);
|
|
@@ -34946,20 +36517,21 @@ ${bold13("specialists doctor")}
|
|
|
34946
36517
|
}
|
|
34947
36518
|
var bold13 = (s) => `\x1B[1m${s}\x1B[0m`, dim13 = (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, ACTIVE_CLAUDE_SKILLS_DIR, ACTIVE_PI_SKILLS_DIR, CONFIG_SKILLS_DIR, SPECIALISTS_DIR, HOOKS_DIR, CLAUDE_HOOKS_DIR, SETTINGS_FILE, MCP_FILE2, HOOK_NAMES;
|
|
34948
36519
|
var init_doctor = __esm(() => {
|
|
36520
|
+
init_observability_sqlite();
|
|
34949
36521
|
CWD = process.cwd();
|
|
34950
|
-
CLAUDE_DIR =
|
|
34951
|
-
PI_DIR =
|
|
34952
|
-
XTRM_SKILLS_DIR =
|
|
34953
|
-
XTRM_DEFAULT_SKILLS_DIR =
|
|
34954
|
-
XTRM_ACTIVE_SKILLS_DIR =
|
|
34955
|
-
ACTIVE_CLAUDE_SKILLS_DIR =
|
|
34956
|
-
ACTIVE_PI_SKILLS_DIR =
|
|
34957
|
-
CONFIG_SKILLS_DIR =
|
|
34958
|
-
SPECIALISTS_DIR =
|
|
34959
|
-
HOOKS_DIR =
|
|
34960
|
-
CLAUDE_HOOKS_DIR =
|
|
34961
|
-
SETTINGS_FILE =
|
|
34962
|
-
MCP_FILE2 =
|
|
36522
|
+
CLAUDE_DIR = join29(CWD, ".claude");
|
|
36523
|
+
PI_DIR = join29(CWD, ".pi");
|
|
36524
|
+
XTRM_SKILLS_DIR = join29(CWD, ".xtrm", "skills");
|
|
36525
|
+
XTRM_DEFAULT_SKILLS_DIR = join29(XTRM_SKILLS_DIR, "default");
|
|
36526
|
+
XTRM_ACTIVE_SKILLS_DIR = join29(XTRM_SKILLS_DIR, "active");
|
|
36527
|
+
ACTIVE_CLAUDE_SKILLS_DIR = join29(XTRM_ACTIVE_SKILLS_DIR, "claude");
|
|
36528
|
+
ACTIVE_PI_SKILLS_DIR = join29(XTRM_ACTIVE_SKILLS_DIR, "pi");
|
|
36529
|
+
CONFIG_SKILLS_DIR = join29(CWD, "config", "skills");
|
|
36530
|
+
SPECIALISTS_DIR = join29(CWD, ".specialists");
|
|
36531
|
+
HOOKS_DIR = join29(CWD, ".xtrm", "hooks", "specialists");
|
|
36532
|
+
CLAUDE_HOOKS_DIR = join29(CLAUDE_DIR, "hooks");
|
|
36533
|
+
SETTINGS_FILE = join29(CLAUDE_DIR, "settings.json");
|
|
36534
|
+
MCP_FILE2 = join29(CWD, ".mcp.json");
|
|
34963
36535
|
HOOK_NAMES = [
|
|
34964
36536
|
"specialists-complete.mjs",
|
|
34965
36537
|
"specialists-session-start.mjs"
|
|
@@ -38920,7 +40492,7 @@ var AssertObjectSchema = custom2((v) => v !== null && (typeof v === "object" ||
|
|
|
38920
40492
|
var ProgressTokenSchema = union([string2(), number2().int()]);
|
|
38921
40493
|
var CursorSchema = string2();
|
|
38922
40494
|
var TaskCreationParamsSchema = looseObject({
|
|
38923
|
-
ttl:
|
|
40495
|
+
ttl: number2().optional(),
|
|
38924
40496
|
pollInterval: number2().optional()
|
|
38925
40497
|
});
|
|
38926
40498
|
var TaskMetadataSchema = object2({
|
|
@@ -39074,7 +40646,8 @@ var ClientCapabilitiesSchema = object2({
|
|
|
39074
40646
|
roots: object2({
|
|
39075
40647
|
listChanged: boolean2().optional()
|
|
39076
40648
|
}).optional(),
|
|
39077
|
-
tasks: ClientTasksCapabilitySchema.optional()
|
|
40649
|
+
tasks: ClientTasksCapabilitySchema.optional(),
|
|
40650
|
+
extensions: record(string2(), AssertObjectSchema).optional()
|
|
39078
40651
|
});
|
|
39079
40652
|
var InitializeRequestParamsSchema = BaseRequestParamsSchema.extend({
|
|
39080
40653
|
protocolVersion: string2(),
|
|
@@ -39099,7 +40672,8 @@ var ServerCapabilitiesSchema = object2({
|
|
|
39099
40672
|
tools: object2({
|
|
39100
40673
|
listChanged: boolean2().optional()
|
|
39101
40674
|
}).optional(),
|
|
39102
|
-
tasks: ServerTasksCapabilitySchema.optional()
|
|
40675
|
+
tasks: ServerTasksCapabilitySchema.optional(),
|
|
40676
|
+
extensions: record(string2(), AssertObjectSchema).optional()
|
|
39103
40677
|
});
|
|
39104
40678
|
var InitializeResultSchema = ResultSchema.extend({
|
|
39105
40679
|
protocolVersion: string2(),
|
|
@@ -39214,6 +40788,7 @@ var ResourceSchema = object2({
|
|
|
39214
40788
|
uri: string2(),
|
|
39215
40789
|
description: optional(string2()),
|
|
39216
40790
|
mimeType: optional(string2()),
|
|
40791
|
+
size: optional(number2()),
|
|
39217
40792
|
annotations: AnnotationsSchema.optional(),
|
|
39218
40793
|
_meta: optional(looseObject({}))
|
|
39219
40794
|
});
|
|
@@ -41209,6 +42784,10 @@ class Protocol {
|
|
|
41209
42784
|
this._progressHandlers.clear();
|
|
41210
42785
|
this._taskProgressTokens.clear();
|
|
41211
42786
|
this._pendingDebouncedNotifications.clear();
|
|
42787
|
+
for (const info of this._timeoutInfo.values()) {
|
|
42788
|
+
clearTimeout(info.timeoutId);
|
|
42789
|
+
}
|
|
42790
|
+
this._timeoutInfo.clear();
|
|
41212
42791
|
for (const controller of this._requestHandlerAbortControllers.values()) {
|
|
41213
42792
|
controller.abort();
|
|
41214
42793
|
}
|
|
@@ -41339,7 +42918,9 @@ class Protocol {
|
|
|
41339
42918
|
await capturedTransport?.send(errorResponse);
|
|
41340
42919
|
}
|
|
41341
42920
|
}).catch((error2) => this._onerror(new Error(`Failed to send response: ${error2}`))).finally(() => {
|
|
41342
|
-
this._requestHandlerAbortControllers.
|
|
42921
|
+
if (this._requestHandlerAbortControllers.get(request.id) === abortController) {
|
|
42922
|
+
this._requestHandlerAbortControllers.delete(request.id);
|
|
42923
|
+
}
|
|
41343
42924
|
});
|
|
41344
42925
|
}
|
|
41345
42926
|
_onprogress(notification) {
|
|
@@ -42818,7 +44399,7 @@ async function run30() {
|
|
|
42818
44399
|
if (wantsHelp()) {
|
|
42819
44400
|
console.log([
|
|
42820
44401
|
"",
|
|
42821
|
-
"Usage: specialists db <setup|backfill>",
|
|
44402
|
+
"Usage: specialists db <setup|backfill|vacuum|prune>",
|
|
42822
44403
|
"",
|
|
42823
44404
|
"Provision the shared observability SQLite database (human-only).",
|
|
42824
44405
|
"",
|
|
@@ -42827,6 +44408,9 @@ async function run30() {
|
|
|
42827
44408
|
" init Alias for setup",
|
|
42828
44409
|
" backfill Backfill specialist_jobs from .specialists/jobs/*/status.json",
|
|
42829
44410
|
" Use --events to also replay events.jsonl",
|
|
44411
|
+
" vacuum Run SQLite VACUUM (refuses when active jobs running/starting)",
|
|
44412
|
+
" prune Prune old rows: requires --before <iso|duration>, dry-run by default",
|
|
44413
|
+
" Use --apply to execute; --include-epics to also prune epic_runs",
|
|
42830
44414
|
"",
|
|
42831
44415
|
"Notes:",
|
|
42832
44416
|
" - TTY required (blocked in agent/non-interactive sessions)",
|
|
@@ -42837,6 +44421,9 @@ async function run30() {
|
|
|
42837
44421
|
" specialists db setup",
|
|
42838
44422
|
" specialists db backfill",
|
|
42839
44423
|
" specialists db backfill --events",
|
|
44424
|
+
" specialists db vacuum",
|
|
44425
|
+
" specialists db prune --before 30d --dry-run",
|
|
44426
|
+
" specialists db prune --before 2026-01-01T00:00:00Z --apply --include-epics",
|
|
42840
44427
|
" sp db setup",
|
|
42841
44428
|
" sp db backfill",
|
|
42842
44429
|
""
|
|
@@ -43508,7 +45095,7 @@ async function run30() {
|
|
|
43508
45095
|
if (wantsHelp()) {
|
|
43509
45096
|
console.log([
|
|
43510
45097
|
"",
|
|
43511
|
-
"Usage: specialists doctor",
|
|
45098
|
+
"Usage: specialists doctor [orphans]",
|
|
43512
45099
|
"",
|
|
43513
45100
|
"Diagnose bootstrap and runtime problems.",
|
|
43514
45101
|
"",
|
|
@@ -43525,15 +45112,19 @@ async function run30() {
|
|
|
43525
45112
|
" - prints fix hints for failing checks",
|
|
43526
45113
|
" - auto-creates missing runtime directories when possible",
|
|
43527
45114
|
"",
|
|
45115
|
+
"Subcommands:",
|
|
45116
|
+
" orphans Read-only orphan scan: membership/jobs/epics/worktree pointers",
|
|
45117
|
+
"",
|
|
43528
45118
|
"Examples:",
|
|
43529
45119
|
" specialists doctor",
|
|
45120
|
+
" specialists doctor orphans",
|
|
43530
45121
|
""
|
|
43531
45122
|
].join(`
|
|
43532
45123
|
`));
|
|
43533
45124
|
return;
|
|
43534
45125
|
}
|
|
43535
45126
|
const { run: handler } = await Promise.resolve().then(() => (init_doctor(), exports_doctor));
|
|
43536
|
-
return handler();
|
|
45127
|
+
return handler(process.argv.slice(3));
|
|
43537
45128
|
}
|
|
43538
45129
|
if (sub === "setup") {
|
|
43539
45130
|
if (wantsHelp()) {
|