@jaggerxtrm/specialists 3.6.10 → 3.6.11
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/skills/using-specialists/SKILL.md +57 -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 +1 -1
- package/config/specialists/sync-docs.specialist.json +2 -2
- package/dist/index.js +1656 -261
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18966,7 +18966,7 @@ function resolveCurrentBranch(cwd = process.cwd()) {
|
|
|
18966
18966
|
var init_job_root = () => {};
|
|
18967
18967
|
|
|
18968
18968
|
// src/specialist/observability-sqlite.ts
|
|
18969
|
-
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
18969
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, statSync } from "fs";
|
|
18970
18970
|
import { join as join5 } from "path";
|
|
18971
18971
|
function loadBunDatabase() {
|
|
18972
18972
|
if (_probed)
|
|
@@ -19508,7 +19508,9 @@ function migrateToV10(db) {
|
|
|
19508
19508
|
|
|
19509
19509
|
class SqliteClient {
|
|
19510
19510
|
db;
|
|
19511
|
+
dbPath;
|
|
19511
19512
|
constructor(dbPath) {
|
|
19513
|
+
this.dbPath = dbPath;
|
|
19512
19514
|
const Ctor = loadBunDatabase();
|
|
19513
19515
|
this.db = new Ctor(dbPath);
|
|
19514
19516
|
this.db.run(`PRAGMA busy_timeout=${BUSY_TIMEOUT_MS}`);
|
|
@@ -20331,6 +20333,242 @@ class SqliteClient {
|
|
|
20331
20333
|
transaction();
|
|
20332
20334
|
}, "invalidateMemoriesCache");
|
|
20333
20335
|
}
|
|
20336
|
+
hasActiveJobs(statuses = ["running", "starting"]) {
|
|
20337
|
+
return this.listActiveJobs(statuses).length > 0;
|
|
20338
|
+
}
|
|
20339
|
+
listActiveJobs(statuses = ["running", "starting"]) {
|
|
20340
|
+
return withRetry(() => {
|
|
20341
|
+
if (statuses.length === 0)
|
|
20342
|
+
return [];
|
|
20343
|
+
const placeholders = statuses.map(() => "?").join(", ");
|
|
20344
|
+
return this.db.query(`
|
|
20345
|
+
SELECT job_id, specialist, status
|
|
20346
|
+
FROM specialist_jobs
|
|
20347
|
+
WHERE status IN (${placeholders})
|
|
20348
|
+
ORDER BY updated_at_ms DESC
|
|
20349
|
+
`).all(...statuses);
|
|
20350
|
+
}, "listActiveJobs");
|
|
20351
|
+
}
|
|
20352
|
+
getDatabaseSizeBytes() {
|
|
20353
|
+
try {
|
|
20354
|
+
return statSync(this.dbPath).size;
|
|
20355
|
+
} catch {
|
|
20356
|
+
return 0;
|
|
20357
|
+
}
|
|
20358
|
+
}
|
|
20359
|
+
vacuumDatabase() {
|
|
20360
|
+
return withRetry(() => {
|
|
20361
|
+
const beforeBytes = this.getDatabaseSizeBytes();
|
|
20362
|
+
this.db.run("VACUUM");
|
|
20363
|
+
const afterBytes = this.getDatabaseSizeBytes();
|
|
20364
|
+
return { beforeBytes, afterBytes };
|
|
20365
|
+
}, "vacuumDatabase");
|
|
20366
|
+
}
|
|
20367
|
+
pruneObservabilityData(options) {
|
|
20368
|
+
return withRetry(() => {
|
|
20369
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
20370
|
+
const eventsRetentionMs = options.eventsRetentionMs ?? 30 * 24 * 60 * 60 * 1000;
|
|
20371
|
+
const eventsCutoffMs = nowMs - eventsRetentionMs;
|
|
20372
|
+
const terminalStatuses = ["done", "error", "stopped"];
|
|
20373
|
+
const activeStatuses = ["running", "starting", "waiting"];
|
|
20374
|
+
const skippedActiveChainJobs = this.db.query(`
|
|
20375
|
+
SELECT COUNT(*) AS count
|
|
20376
|
+
FROM specialist_jobs stale
|
|
20377
|
+
WHERE stale.updated_at_ms < ?
|
|
20378
|
+
AND stale.status IN (${terminalStatuses.map(() => "?").join(", ")})
|
|
20379
|
+
AND stale.chain_id IS NOT NULL
|
|
20380
|
+
AND EXISTS (
|
|
20381
|
+
SELECT 1
|
|
20382
|
+
FROM specialist_jobs active
|
|
20383
|
+
WHERE active.chain_id = stale.chain_id
|
|
20384
|
+
AND active.status IN (${activeStatuses.map(() => "?").join(", ")})
|
|
20385
|
+
)
|
|
20386
|
+
`).get(options.beforeMs, ...terminalStatuses, ...activeStatuses)?.count ?? 0;
|
|
20387
|
+
const resultCandidates = this.db.query(`
|
|
20388
|
+
SELECT COUNT(*) AS count
|
|
20389
|
+
FROM specialist_results results
|
|
20390
|
+
LEFT JOIN specialist_jobs jobs ON jobs.job_id = results.job_id
|
|
20391
|
+
WHERE results.updated_at_ms < ?
|
|
20392
|
+
AND (
|
|
20393
|
+
jobs.job_id IS NULL
|
|
20394
|
+
OR jobs.chain_id IS NULL
|
|
20395
|
+
OR NOT EXISTS (
|
|
20396
|
+
SELECT 1
|
|
20397
|
+
FROM specialist_jobs active
|
|
20398
|
+
WHERE active.chain_id = jobs.chain_id
|
|
20399
|
+
AND active.status IN (${activeStatuses.map(() => "?").join(", ")})
|
|
20400
|
+
)
|
|
20401
|
+
)
|
|
20402
|
+
`).get(options.beforeMs, ...activeStatuses)?.count ?? 0;
|
|
20403
|
+
const jobCandidates = this.db.query(`
|
|
20404
|
+
SELECT COUNT(*) AS count
|
|
20405
|
+
FROM specialist_jobs stale
|
|
20406
|
+
WHERE stale.updated_at_ms < ?
|
|
20407
|
+
AND stale.status IN (${terminalStatuses.map(() => "?").join(", ")})
|
|
20408
|
+
AND (
|
|
20409
|
+
stale.chain_id IS NULL
|
|
20410
|
+
OR NOT EXISTS (
|
|
20411
|
+
SELECT 1
|
|
20412
|
+
FROM specialist_jobs active
|
|
20413
|
+
WHERE active.chain_id = stale.chain_id
|
|
20414
|
+
AND active.status IN (${activeStatuses.map(() => "?").join(", ")})
|
|
20415
|
+
)
|
|
20416
|
+
)
|
|
20417
|
+
`).get(options.beforeMs, ...terminalStatuses, ...activeStatuses)?.count ?? 0;
|
|
20418
|
+
const eventsCandidates = this.db.query("SELECT COUNT(*) AS count FROM specialist_events WHERE t < ?").get(eventsCutoffMs)?.count ?? 0;
|
|
20419
|
+
const epicCandidates = options.includeEpics ? this.db.query(`
|
|
20420
|
+
SELECT COUNT(*) AS count
|
|
20421
|
+
FROM epic_runs epic
|
|
20422
|
+
WHERE epic.updated_at_ms < ?
|
|
20423
|
+
AND epic.status IN ('merged', 'failed', 'abandoned')
|
|
20424
|
+
AND NOT EXISTS (
|
|
20425
|
+
SELECT 1
|
|
20426
|
+
FROM epic_chain_membership membership
|
|
20427
|
+
WHERE membership.epic_id = epic.epic_id
|
|
20428
|
+
)
|
|
20429
|
+
`).get(options.beforeMs)?.count ?? 0 : 0;
|
|
20430
|
+
if (!options.apply) {
|
|
20431
|
+
return {
|
|
20432
|
+
dryRun: true,
|
|
20433
|
+
beforeMs: options.beforeMs,
|
|
20434
|
+
eventsCutoffMs,
|
|
20435
|
+
includeEpics: options.includeEpics,
|
|
20436
|
+
deletedEvents: eventsCandidates,
|
|
20437
|
+
deletedResults: resultCandidates,
|
|
20438
|
+
deletedJobs: jobCandidates,
|
|
20439
|
+
deletedEpicRuns: epicCandidates,
|
|
20440
|
+
skippedActiveChainJobs
|
|
20441
|
+
};
|
|
20442
|
+
}
|
|
20443
|
+
const deleteResults = this.db.query(`
|
|
20444
|
+
DELETE FROM specialist_results
|
|
20445
|
+
WHERE updated_at_ms < ?
|
|
20446
|
+
AND (
|
|
20447
|
+
job_id NOT IN (SELECT job_id FROM specialist_jobs WHERE chain_id IS NOT NULL)
|
|
20448
|
+
OR job_id IN (
|
|
20449
|
+
SELECT jobs.job_id
|
|
20450
|
+
FROM specialist_jobs jobs
|
|
20451
|
+
WHERE jobs.chain_id IS NULL
|
|
20452
|
+
OR NOT EXISTS (
|
|
20453
|
+
SELECT 1
|
|
20454
|
+
FROM specialist_jobs active
|
|
20455
|
+
WHERE active.chain_id = jobs.chain_id
|
|
20456
|
+
AND active.status IN (${activeStatuses.map(() => "?").join(", ")})
|
|
20457
|
+
)
|
|
20458
|
+
)
|
|
20459
|
+
)
|
|
20460
|
+
`);
|
|
20461
|
+
const deletedResults = deleteResults.run(options.beforeMs, ...activeStatuses).changes ?? 0;
|
|
20462
|
+
const deleteEvents = this.db.query("DELETE FROM specialist_events WHERE t < ?");
|
|
20463
|
+
const deletedEvents = deleteEvents.run(eventsCutoffMs).changes ?? 0;
|
|
20464
|
+
const deleteJobs = this.db.query(`
|
|
20465
|
+
DELETE FROM specialist_jobs
|
|
20466
|
+
WHERE updated_at_ms < ?
|
|
20467
|
+
AND status IN (${terminalStatuses.map(() => "?").join(", ")})
|
|
20468
|
+
AND (
|
|
20469
|
+
chain_id IS NULL
|
|
20470
|
+
OR NOT EXISTS (
|
|
20471
|
+
SELECT 1
|
|
20472
|
+
FROM specialist_jobs active
|
|
20473
|
+
WHERE active.chain_id = specialist_jobs.chain_id
|
|
20474
|
+
AND active.status IN (${activeStatuses.map(() => "?").join(", ")})
|
|
20475
|
+
)
|
|
20476
|
+
)
|
|
20477
|
+
`);
|
|
20478
|
+
const deletedJobs = deleteJobs.run(options.beforeMs, ...terminalStatuses, ...activeStatuses).changes ?? 0;
|
|
20479
|
+
let deletedEpicRuns = 0;
|
|
20480
|
+
if (options.includeEpics) {
|
|
20481
|
+
const deleteEpics = this.db.query(`
|
|
20482
|
+
DELETE FROM epic_runs
|
|
20483
|
+
WHERE updated_at_ms < ?
|
|
20484
|
+
AND status IN ('merged', 'failed', 'abandoned')
|
|
20485
|
+
AND NOT EXISTS (
|
|
20486
|
+
SELECT 1
|
|
20487
|
+
FROM epic_chain_membership membership
|
|
20488
|
+
WHERE membership.epic_id = epic_runs.epic_id
|
|
20489
|
+
)
|
|
20490
|
+
`);
|
|
20491
|
+
deletedEpicRuns = deleteEpics.run(options.beforeMs).changes ?? 0;
|
|
20492
|
+
}
|
|
20493
|
+
return {
|
|
20494
|
+
dryRun: false,
|
|
20495
|
+
beforeMs: options.beforeMs,
|
|
20496
|
+
eventsCutoffMs,
|
|
20497
|
+
includeEpics: options.includeEpics,
|
|
20498
|
+
deletedEvents,
|
|
20499
|
+
deletedResults,
|
|
20500
|
+
deletedJobs,
|
|
20501
|
+
deletedEpicRuns,
|
|
20502
|
+
skippedActiveChainJobs
|
|
20503
|
+
};
|
|
20504
|
+
}, "pruneObservabilityData");
|
|
20505
|
+
}
|
|
20506
|
+
scanOrphans() {
|
|
20507
|
+
return withRetry(() => {
|
|
20508
|
+
const findings = [];
|
|
20509
|
+
const chainMembershipWithoutJobs = this.db.query(`
|
|
20510
|
+
SELECT membership.chain_id, membership.epic_id
|
|
20511
|
+
FROM epic_chain_membership membership
|
|
20512
|
+
LEFT JOIN specialist_jobs jobs ON jobs.chain_id = membership.chain_id
|
|
20513
|
+
WHERE jobs.job_id IS NULL
|
|
20514
|
+
`).all();
|
|
20515
|
+
for (const row of chainMembershipWithoutJobs) {
|
|
20516
|
+
findings.push({
|
|
20517
|
+
kind: "orphan",
|
|
20518
|
+
code: "chain_membership_without_jobs",
|
|
20519
|
+
message: `chain ${row.chain_id} has epic membership but no jobs`,
|
|
20520
|
+
details: { chain_id: row.chain_id, epic_id: row.epic_id }
|
|
20521
|
+
});
|
|
20522
|
+
}
|
|
20523
|
+
const epicsWithoutChains = this.db.query(`
|
|
20524
|
+
SELECT epic.epic_id, epic.status
|
|
20525
|
+
FROM epic_runs epic
|
|
20526
|
+
LEFT JOIN epic_chain_membership membership ON membership.epic_id = epic.epic_id
|
|
20527
|
+
WHERE membership.chain_id IS NULL
|
|
20528
|
+
`).all();
|
|
20529
|
+
for (const row of epicsWithoutChains) {
|
|
20530
|
+
findings.push({
|
|
20531
|
+
kind: "orphan",
|
|
20532
|
+
code: "epic_without_chains",
|
|
20533
|
+
message: `epic ${row.epic_id} has no chain membership`,
|
|
20534
|
+
details: { epic_id: row.epic_id, status: row.status }
|
|
20535
|
+
});
|
|
20536
|
+
}
|
|
20537
|
+
const jobEpicWithoutMembership = this.db.query(`
|
|
20538
|
+
SELECT jobs.job_id, jobs.epic_id, jobs.chain_id
|
|
20539
|
+
FROM specialist_jobs jobs
|
|
20540
|
+
LEFT JOIN epic_chain_membership membership
|
|
20541
|
+
ON membership.chain_id = jobs.chain_id
|
|
20542
|
+
AND membership.epic_id = jobs.epic_id
|
|
20543
|
+
WHERE jobs.epic_id IS NOT NULL
|
|
20544
|
+
AND (jobs.chain_id IS NULL OR membership.chain_id IS NULL)
|
|
20545
|
+
`).all();
|
|
20546
|
+
for (const row of jobEpicWithoutMembership) {
|
|
20547
|
+
findings.push({
|
|
20548
|
+
kind: "integrity-violation",
|
|
20549
|
+
code: "job_epic_without_membership",
|
|
20550
|
+
message: `job ${row.job_id} references epic without chain membership link`,
|
|
20551
|
+
details: { job_id: row.job_id, epic_id: row.epic_id, chain_id: row.chain_id ?? null }
|
|
20552
|
+
});
|
|
20553
|
+
}
|
|
20554
|
+
const worktreeRows = this.db.query(`
|
|
20555
|
+
SELECT DISTINCT job_id, worktree_column
|
|
20556
|
+
FROM specialist_jobs
|
|
20557
|
+
WHERE worktree_column IS NOT NULL AND worktree_column != ''
|
|
20558
|
+
`).all();
|
|
20559
|
+
for (const row of worktreeRows) {
|
|
20560
|
+
if (existsSync4(row.worktree_column))
|
|
20561
|
+
continue;
|
|
20562
|
+
findings.push({
|
|
20563
|
+
kind: "stale-pointer",
|
|
20564
|
+
code: "worktree_missing_on_disk",
|
|
20565
|
+
message: `job ${row.job_id} points to missing worktree path`,
|
|
20566
|
+
details: { job_id: row.job_id, worktree_path: row.worktree_column }
|
|
20567
|
+
});
|
|
20568
|
+
}
|
|
20569
|
+
return findings;
|
|
20570
|
+
}, "scanOrphans");
|
|
20571
|
+
}
|
|
20334
20572
|
close() {
|
|
20335
20573
|
this.db.close();
|
|
20336
20574
|
}
|
|
@@ -20944,11 +21182,21 @@ class SpecialistRunner {
|
|
|
20944
21182
|
const preScriptOutput = formatScriptOutput(preResults);
|
|
20945
21183
|
const resolvedPrompt = this.resolvePromptWithBeadContext(options, beadsClient);
|
|
20946
21184
|
const beadVariables = options.inputBeadId ? { bead_context: resolvedPrompt, bead_id: options.inputBeadId } : {};
|
|
20947
|
-
const
|
|
21185
|
+
const lineageVariables = {
|
|
21186
|
+
...options.reusedFromJobId ? { reused_from_job_id: options.reusedFromJobId } : {},
|
|
21187
|
+
...options.worktreeOwnerJobId ? { worktree_owner_job_id: options.worktreeOwnerJobId } : {}
|
|
21188
|
+
};
|
|
21189
|
+
const beadTemplateVariables = {
|
|
21190
|
+
prompt: resolvedPrompt,
|
|
21191
|
+
bead_id: options.inputBeadId ?? "",
|
|
21192
|
+
...lineageVariables
|
|
21193
|
+
};
|
|
20948
21194
|
const variables = {
|
|
20949
21195
|
prompt: resolvedPrompt,
|
|
20950
21196
|
cwd: runCwd,
|
|
20951
21197
|
pre_script_output: preScriptOutput,
|
|
21198
|
+
bead_id: options.inputBeadId ?? "",
|
|
21199
|
+
...lineageVariables,
|
|
20952
21200
|
...options.variables ?? {},
|
|
20953
21201
|
...beadVariables
|
|
20954
21202
|
};
|
|
@@ -20961,7 +21209,7 @@ class SpecialistRunner {
|
|
|
20961
21209
|
estimated_tokens: Math.ceil(renderedTask.length / 4),
|
|
20962
21210
|
system_prompt_present: !!prompt.system
|
|
20963
21211
|
});
|
|
20964
|
-
let agentsMd =
|
|
21212
|
+
let agentsMd = renderTemplate(prompt.system ?? "", beadTemplateVariables);
|
|
20965
21213
|
{
|
|
20966
21214
|
const sanitizedBeadId = options.inputBeadId ? sanitizeBeadIdForPrompt(options.inputBeadId) : "";
|
|
20967
21215
|
const beadInstructions = sanitizedBeadId ? `
|
|
@@ -21713,12 +21961,13 @@ function mapCallbackEventToTimelineEvent(callbackEvent, context) {
|
|
|
21713
21961
|
return null;
|
|
21714
21962
|
}
|
|
21715
21963
|
}
|
|
21716
|
-
function createRunStartEvent(specialist, beadId) {
|
|
21964
|
+
function createRunStartEvent(specialist, beadId, startupSnapshot) {
|
|
21717
21965
|
return {
|
|
21718
21966
|
t: Date.now(),
|
|
21719
21967
|
type: TIMELINE_EVENT_TYPES.RUN_START,
|
|
21720
21968
|
specialist,
|
|
21721
|
-
bead_id: beadId
|
|
21969
|
+
bead_id: beadId,
|
|
21970
|
+
...startupSnapshot ? { startup_snapshot: startupSnapshot } : {}
|
|
21722
21971
|
};
|
|
21723
21972
|
}
|
|
21724
21973
|
function createMetaEvent(model, backend) {
|
|
@@ -21938,6 +22187,27 @@ function evaluateEpicMergeReadiness(input) {
|
|
|
21938
22187
|
summary: `Epic ${input.epicId} is merge-ready and all chains are terminal.`
|
|
21939
22188
|
};
|
|
21940
22189
|
}
|
|
22190
|
+
function appendEpicTransitionAudit(statusJson, entry) {
|
|
22191
|
+
const fallback = {
|
|
22192
|
+
transitions: []
|
|
22193
|
+
};
|
|
22194
|
+
let parsed = fallback;
|
|
22195
|
+
if (statusJson) {
|
|
22196
|
+
try {
|
|
22197
|
+
const candidate = JSON.parse(statusJson);
|
|
22198
|
+
if (candidate && typeof candidate === "object") {
|
|
22199
|
+
parsed = candidate;
|
|
22200
|
+
}
|
|
22201
|
+
} catch {
|
|
22202
|
+
parsed = fallback;
|
|
22203
|
+
}
|
|
22204
|
+
}
|
|
22205
|
+
const previous = Array.isArray(parsed.transitions) ? parsed.transitions.filter((item) => Boolean(item) && typeof item === "object") : [];
|
|
22206
|
+
return JSON.stringify({
|
|
22207
|
+
...parsed,
|
|
22208
|
+
transitions: [...previous, entry]
|
|
22209
|
+
});
|
|
22210
|
+
}
|
|
21941
22211
|
function summarizeEpicTransition(epicId, from, to) {
|
|
21942
22212
|
return `Epic ${epicId}: ${from} -> ${to}`;
|
|
21943
22213
|
}
|
|
@@ -21954,6 +22224,72 @@ var init_epic_lifecycle = __esm(() => {
|
|
|
21954
22224
|
};
|
|
21955
22225
|
});
|
|
21956
22226
|
|
|
22227
|
+
// src/specialist/process-liveness.ts
|
|
22228
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
22229
|
+
function isValidPid(pid) {
|
|
22230
|
+
return Number.isInteger(pid) && pid > 0;
|
|
22231
|
+
}
|
|
22232
|
+
function isAliveBySignal(pid) {
|
|
22233
|
+
try {
|
|
22234
|
+
process.kill(pid, 0);
|
|
22235
|
+
return true;
|
|
22236
|
+
} catch {
|
|
22237
|
+
return false;
|
|
22238
|
+
}
|
|
22239
|
+
}
|
|
22240
|
+
function parseBootTimeMs() {
|
|
22241
|
+
try {
|
|
22242
|
+
const procStat = readFileSync4("/proc/stat", "utf-8");
|
|
22243
|
+
const bootLine = procStat.split(`
|
|
22244
|
+
`).find((line) => line.startsWith("btime "));
|
|
22245
|
+
if (!bootLine)
|
|
22246
|
+
return;
|
|
22247
|
+
const bootSeconds = Number.parseInt(bootLine.split(/\s+/)[1] ?? "", 10);
|
|
22248
|
+
if (!Number.isFinite(bootSeconds) || bootSeconds <= 0)
|
|
22249
|
+
return;
|
|
22250
|
+
return bootSeconds * 1000;
|
|
22251
|
+
} catch {
|
|
22252
|
+
return;
|
|
22253
|
+
}
|
|
22254
|
+
}
|
|
22255
|
+
function parseProcessStartTimeMs(pid) {
|
|
22256
|
+
try {
|
|
22257
|
+
const statRaw = readFileSync4(`/proc/${pid}/stat`, "utf-8");
|
|
22258
|
+
const closeParenIndex = statRaw.lastIndexOf(")");
|
|
22259
|
+
if (closeParenIndex < 0)
|
|
22260
|
+
return;
|
|
22261
|
+
const suffix = statRaw.slice(closeParenIndex + 1).trim();
|
|
22262
|
+
const fields = suffix.split(/\s+/);
|
|
22263
|
+
const startTimeTicksText = fields[PROC_STAT_START_TIME_INDEX - 2];
|
|
22264
|
+
if (!startTimeTicksText)
|
|
22265
|
+
return;
|
|
22266
|
+
const startTimeTicks = Number.parseInt(startTimeTicksText, 10);
|
|
22267
|
+
if (!Number.isFinite(startTimeTicks) || startTimeTicks < 0)
|
|
22268
|
+
return;
|
|
22269
|
+
const bootTimeMs = parseBootTimeMs();
|
|
22270
|
+
if (bootTimeMs === undefined)
|
|
22271
|
+
return;
|
|
22272
|
+
const ticksPerSecond = 100;
|
|
22273
|
+
return bootTimeMs + Math.floor(startTimeTicks * 1000 / ticksPerSecond);
|
|
22274
|
+
} catch {
|
|
22275
|
+
return;
|
|
22276
|
+
}
|
|
22277
|
+
}
|
|
22278
|
+
function isProcessAlive(pid, startTimeMs) {
|
|
22279
|
+
if (!isValidPid(pid))
|
|
22280
|
+
return false;
|
|
22281
|
+
if (!isAliveBySignal(pid))
|
|
22282
|
+
return false;
|
|
22283
|
+
if (startTimeMs === undefined)
|
|
22284
|
+
return true;
|
|
22285
|
+
const actualStartTimeMs = parseProcessStartTimeMs(pid);
|
|
22286
|
+
if (actualStartTimeMs === undefined)
|
|
22287
|
+
return true;
|
|
22288
|
+
return Math.abs(actualStartTimeMs - startTimeMs) <= 2000;
|
|
22289
|
+
}
|
|
22290
|
+
var PROC_STAT_START_TIME_INDEX = 21;
|
|
22291
|
+
var init_process_liveness = () => {};
|
|
22292
|
+
|
|
21957
22293
|
// src/specialist/epic-readiness.ts
|
|
21958
22294
|
function parseReviewerVerdict(output) {
|
|
21959
22295
|
if (!output)
|
|
@@ -21984,7 +22320,19 @@ function evaluateChainReadiness(chainId, jobs, chainRootBeadId) {
|
|
|
21984
22320
|
}
|
|
21985
22321
|
const orderedJobs = [...jobs].sort((a, b) => a.started_at_ms - b.started_at_ms);
|
|
21986
22322
|
const activeJobs = orderedJobs.filter((job) => ACTIVE_JOB_STATUSES.has(job.status));
|
|
22323
|
+
const deadActiveJobs = activeJobs.filter((job) => job.pid !== undefined && !isProcessAlive(job.pid, job.started_at_ms));
|
|
21987
22324
|
const hasActiveJobs = activeJobs.length > 0;
|
|
22325
|
+
if (deadActiveJobs.length > 0) {
|
|
22326
|
+
return {
|
|
22327
|
+
chain_id: chainId,
|
|
22328
|
+
chain_root_bead_id: chainRootBeadId,
|
|
22329
|
+
state: "failed",
|
|
22330
|
+
reviewer_verdict: "missing",
|
|
22331
|
+
blocking_reason: `Active chain jobs appear dead: ${deadActiveJobs.map((job) => job.id).join(", ")}`,
|
|
22332
|
+
has_active_jobs: false,
|
|
22333
|
+
job_ids: orderedJobs.map((job) => job.id)
|
|
22334
|
+
};
|
|
22335
|
+
}
|
|
21988
22336
|
if (hasActiveJobs) {
|
|
21989
22337
|
return {
|
|
21990
22338
|
chain_id: chainId,
|
|
@@ -22151,6 +22499,7 @@ function loadEpicReadinessSummary(sqlite, epicId) {
|
|
|
22151
22499
|
id: status.id,
|
|
22152
22500
|
specialist: status.specialist,
|
|
22153
22501
|
status: status.status,
|
|
22502
|
+
pid: status.pid,
|
|
22154
22503
|
started_at_ms: status.started_at_ms,
|
|
22155
22504
|
result_text: sqlite.readResult(status.id) ?? undefined
|
|
22156
22505
|
}));
|
|
@@ -22193,6 +22542,7 @@ function syncEpicStateFromReadiness(sqlite, summary) {
|
|
|
22193
22542
|
var ACTIVE_JOB_STATUSES, TERMINAL_JOB_STATUSES, REVIEWER_VERDICT_REGEX;
|
|
22194
22543
|
var init_epic_readiness = __esm(() => {
|
|
22195
22544
|
init_epic_lifecycle();
|
|
22545
|
+
init_process_liveness();
|
|
22196
22546
|
ACTIVE_JOB_STATUSES = new Set(["starting", "running", "waiting"]);
|
|
22197
22547
|
TERMINAL_JOB_STATUSES = new Set(["done", "error"]);
|
|
22198
22548
|
REVIEWER_VERDICT_REGEX = /Verdict:\s*(PASS|PARTIAL|FAIL)/i;
|
|
@@ -22271,10 +22621,10 @@ import {
|
|
|
22271
22621
|
mkdirSync as mkdirSync3,
|
|
22272
22622
|
openSync,
|
|
22273
22623
|
readdirSync,
|
|
22274
|
-
readFileSync as
|
|
22624
|
+
readFileSync as readFileSync5,
|
|
22275
22625
|
renameSync,
|
|
22276
22626
|
rmSync,
|
|
22277
|
-
statSync,
|
|
22627
|
+
statSync as statSync2,
|
|
22278
22628
|
writeFileSync as writeFileSync3,
|
|
22279
22629
|
writeSync
|
|
22280
22630
|
} from "fs";
|
|
@@ -22685,7 +23035,7 @@ class Supervisor {
|
|
|
22685
23035
|
if (!existsSync7(path))
|
|
22686
23036
|
return null;
|
|
22687
23037
|
try {
|
|
22688
|
-
const status = JSON.parse(
|
|
23038
|
+
const status = JSON.parse(readFileSync5(path, "utf-8"));
|
|
22689
23039
|
return this.withComputedLiveness(status);
|
|
22690
23040
|
} catch {
|
|
22691
23041
|
return null;
|
|
@@ -22727,7 +23077,7 @@ class Supervisor {
|
|
|
22727
23077
|
if (!existsSync7(path))
|
|
22728
23078
|
continue;
|
|
22729
23079
|
try {
|
|
22730
|
-
const status = JSON.parse(
|
|
23080
|
+
const status = JSON.parse(readFileSync5(path, "utf-8"));
|
|
22731
23081
|
jobs.push(this.withComputedLiveness(status));
|
|
22732
23082
|
} catch {}
|
|
22733
23083
|
}
|
|
@@ -22807,7 +23157,7 @@ class Supervisor {
|
|
|
22807
23157
|
for (const entry of readdirSync(this.resolvedJobsDir)) {
|
|
22808
23158
|
const dir = join8(this.resolvedJobsDir, entry);
|
|
22809
23159
|
try {
|
|
22810
|
-
const stat2 =
|
|
23160
|
+
const stat2 = statSync2(dir);
|
|
22811
23161
|
if (!stat2.isDirectory())
|
|
22812
23162
|
continue;
|
|
22813
23163
|
if (stat2.mtimeMs < cutoff)
|
|
@@ -22828,7 +23178,7 @@ class Supervisor {
|
|
|
22828
23178
|
if (!existsSync7(statusPath))
|
|
22829
23179
|
continue;
|
|
22830
23180
|
try {
|
|
22831
|
-
const s = JSON.parse(
|
|
23181
|
+
const s = JSON.parse(readFileSync5(statusPath, "utf-8"));
|
|
22832
23182
|
if (s.status === "running" || s.status === "starting") {
|
|
22833
23183
|
if (!s.pid)
|
|
22834
23184
|
continue;
|
|
@@ -22890,6 +23240,32 @@ class Supervisor {
|
|
|
22890
23240
|
mkdirSync3(dir, { recursive: true });
|
|
22891
23241
|
mkdirSync3(this.readyDir(), { recursive: true });
|
|
22892
23242
|
const nodeId = runOptions.variables?.node_id ?? runOptions.variables?.SPECIALISTS_NODE_ID;
|
|
23243
|
+
const variablesKeys = Object.keys(runOptions.variables ?? {});
|
|
23244
|
+
const activatedSkills = (runOptions.variables?.activated_skills ?? runOptions.variables?.skills_activated ?? "").split(",").map((skill) => skill.trim()).filter((skill) => skill.length > 0);
|
|
23245
|
+
const startupContext = {
|
|
23246
|
+
job_id: id,
|
|
23247
|
+
specialist_name: runOptions.name,
|
|
23248
|
+
...runOptions.inputBeadId ? { bead_id: runOptions.inputBeadId } : {},
|
|
23249
|
+
...runOptions.reusedFromJobId ? { reused_from_job_id: runOptions.reusedFromJobId } : {},
|
|
23250
|
+
...runOptions.worktreeOwnerJobId ? { worktree_owner_job_id: runOptions.worktreeOwnerJobId } : {},
|
|
23251
|
+
...runOptions.worktreeOwnerJobId || runOptions.workingDirectory ? {
|
|
23252
|
+
chain_id: runOptions.worktreeOwnerJobId ?? id,
|
|
23253
|
+
chain_root_job_id: runOptions.worktreeOwnerJobId ?? id
|
|
23254
|
+
} : {},
|
|
23255
|
+
...runOptions.variables?.chain_root_bead_id ? { chain_root_bead_id: runOptions.variables.chain_root_bead_id } : {},
|
|
23256
|
+
...runOptions.workingDirectory ? { worktree_path: runOptions.workingDirectory } : {},
|
|
23257
|
+
...runOptions.workingDirectory ? { branch: resolveCurrentBranch(runOptions.workingDirectory) } : { branch: resolveCurrentBranch() },
|
|
23258
|
+
variables_keys: variablesKeys,
|
|
23259
|
+
reviewed_job_id_present: variablesKeys.includes("reviewed_job_id"),
|
|
23260
|
+
reused_worktree_awareness_present: variablesKeys.includes("reused_worktree_awareness"),
|
|
23261
|
+
bead_context_present: variablesKeys.includes("bead_context"),
|
|
23262
|
+
...activatedSkills.length > 0 ? {
|
|
23263
|
+
skills: {
|
|
23264
|
+
count: activatedSkills.length,
|
|
23265
|
+
activated: activatedSkills
|
|
23266
|
+
}
|
|
23267
|
+
} : {}
|
|
23268
|
+
};
|
|
22893
23269
|
const initialStatus = {
|
|
22894
23270
|
id,
|
|
22895
23271
|
specialist: runOptions.name,
|
|
@@ -22908,7 +23284,8 @@ class Supervisor {
|
|
|
22908
23284
|
chain_root_job_id: runOptions.worktreeOwnerJobId ?? id
|
|
22909
23285
|
} : { chain_kind: "prep" },
|
|
22910
23286
|
...runOptions.epicId ? { epic_id: runOptions.epicId } : {},
|
|
22911
|
-
...runOptions.workingDirectory ? { branch: resolveCurrentBranch(runOptions.workingDirectory) } : { branch: resolveCurrentBranch() }
|
|
23287
|
+
...runOptions.workingDirectory ? { branch: resolveCurrentBranch(runOptions.workingDirectory) } : { branch: resolveCurrentBranch() },
|
|
23288
|
+
startup_context: startupContext
|
|
22912
23289
|
};
|
|
22913
23290
|
this.writeStatusFileOnly(id, initialStatus);
|
|
22914
23291
|
const statusWatchdogPid = startDetachedStatusWatchdog(this.statusPath(id), process.pid);
|
|
@@ -22978,7 +23355,7 @@ class Supervisor {
|
|
|
22978
23355
|
appendTimelineEvent(createStatusChangeEvent("waiting", previousStatus));
|
|
22979
23356
|
}
|
|
22980
23357
|
};
|
|
22981
|
-
const runStartEvent = appendTimelineEventFileOnly(createRunStartEvent(runOptions.name, runOptions.inputBeadId));
|
|
23358
|
+
const runStartEvent = appendTimelineEventFileOnly(createRunStartEvent(runOptions.name, runOptions.inputBeadId, statusSnapshot.startup_context));
|
|
22982
23359
|
try {
|
|
22983
23360
|
this.withSqliteOperation("upsertStatusWithEvent:run_start", (client) => client.upsertStatusWithEvent(statusSnapshot, runStartEvent));
|
|
22984
23361
|
} catch (error2) {
|
|
@@ -23283,6 +23660,14 @@ ${appendError}
|
|
|
23283
23660
|
return;
|
|
23284
23661
|
}
|
|
23285
23662
|
})();
|
|
23663
|
+
if (eventType === "memory_injection" && memoryInjection) {
|
|
23664
|
+
setStatus({
|
|
23665
|
+
startup_context: {
|
|
23666
|
+
...statusSnapshot.startup_context ?? {},
|
|
23667
|
+
memory_injection: memoryInjection
|
|
23668
|
+
}
|
|
23669
|
+
});
|
|
23670
|
+
}
|
|
23286
23671
|
const timelineEvent = mapCallbackEventToTimelineEvent(eventType, {
|
|
23287
23672
|
tool: toolState?.tool,
|
|
23288
23673
|
toolCallId,
|
|
@@ -23731,7 +24116,7 @@ __export(exports_list, {
|
|
|
23731
24116
|
ArgParseError: () => ArgParseError
|
|
23732
24117
|
});
|
|
23733
24118
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
23734
|
-
import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as
|
|
24119
|
+
import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
23735
24120
|
import { join as join9 } from "path";
|
|
23736
24121
|
import readline from "readline";
|
|
23737
24122
|
function permissionBadge(permission) {
|
|
@@ -23764,7 +24149,7 @@ function toLiveJob(status) {
|
|
|
23764
24149
|
}
|
|
23765
24150
|
function readJobStatus(statusPath) {
|
|
23766
24151
|
try {
|
|
23767
|
-
return JSON.parse(
|
|
24152
|
+
return JSON.parse(readFileSync6(statusPath, "utf-8"));
|
|
23768
24153
|
} catch {
|
|
23769
24154
|
return null;
|
|
23770
24155
|
}
|
|
@@ -24333,7 +24718,7 @@ var exports_init = {};
|
|
|
24333
24718
|
__export(exports_init, {
|
|
24334
24719
|
run: () => run6
|
|
24335
24720
|
});
|
|
24336
|
-
import { copyFileSync, cpSync, existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as
|
|
24721
|
+
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
24722
|
import { spawnSync as spawnSync9 } from "child_process";
|
|
24338
24723
|
import { basename as basename3, dirname as dirname5, join as join10, relative, resolve as resolve4 } from "path";
|
|
24339
24724
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -24375,7 +24760,7 @@ function loadJson(path, fallback) {
|
|
|
24375
24760
|
if (!existsSync9(path))
|
|
24376
24761
|
return structuredClone(fallback);
|
|
24377
24762
|
try {
|
|
24378
|
-
return JSON.parse(
|
|
24763
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
24379
24764
|
} catch {
|
|
24380
24765
|
return structuredClone(fallback);
|
|
24381
24766
|
}
|
|
@@ -24718,7 +25103,7 @@ function ensureProjectMcp(cwd) {
|
|
|
24718
25103
|
}
|
|
24719
25104
|
function ensureGitignore(cwd) {
|
|
24720
25105
|
const gitignorePath = join10(cwd, ".gitignore");
|
|
24721
|
-
const existing = existsSync9(gitignorePath) ?
|
|
25106
|
+
const existing = existsSync9(gitignorePath) ? readFileSync7(gitignorePath, "utf-8") : "";
|
|
24722
25107
|
let added = 0;
|
|
24723
25108
|
const lines = existing.split(`
|
|
24724
25109
|
`);
|
|
@@ -24765,7 +25150,7 @@ function ensureObservabilityDb(cwd) {
|
|
|
24765
25150
|
function ensureAgentsMd(cwd) {
|
|
24766
25151
|
const agentsPath = join10(cwd, "AGENTS.md");
|
|
24767
25152
|
if (existsSync9(agentsPath)) {
|
|
24768
|
-
const existing =
|
|
25153
|
+
const existing = readFileSync7(agentsPath, "utf-8");
|
|
24769
25154
|
if (existing.includes(AGENTS_MARKER)) {
|
|
24770
25155
|
skip("AGENTS.md already has Specialists section");
|
|
24771
25156
|
} else {
|
|
@@ -24783,7 +25168,7 @@ function readJsonObject(path) {
|
|
|
24783
25168
|
if (!existsSync9(path))
|
|
24784
25169
|
return {};
|
|
24785
25170
|
try {
|
|
24786
|
-
return JSON.parse(
|
|
25171
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
24787
25172
|
} catch {
|
|
24788
25173
|
return {};
|
|
24789
25174
|
}
|
|
@@ -25056,35 +25441,77 @@ var exports_db = {};
|
|
|
25056
25441
|
__export(exports_db, {
|
|
25057
25442
|
run: () => run8
|
|
25058
25443
|
});
|
|
25059
|
-
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as
|
|
25060
|
-
import { join as join11 } from "path";
|
|
25444
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
25445
|
+
import { dirname as dirname6, join as join11, resolve as resolve5 } from "path";
|
|
25446
|
+
function formatBytes(bytes) {
|
|
25447
|
+
if (bytes < 1024)
|
|
25448
|
+
return `${bytes} B`;
|
|
25449
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
25450
|
+
let value = bytes;
|
|
25451
|
+
let unitIndex = -1;
|
|
25452
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
25453
|
+
value /= 1024;
|
|
25454
|
+
unitIndex += 1;
|
|
25455
|
+
}
|
|
25456
|
+
return `${value.toFixed(2)} ${units[unitIndex]}`;
|
|
25457
|
+
}
|
|
25458
|
+
function parseIsoDate(input2) {
|
|
25459
|
+
const parsed = Date.parse(input2);
|
|
25460
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
25461
|
+
}
|
|
25462
|
+
function parseDuration(input2) {
|
|
25463
|
+
const match = input2.trim().toLowerCase().match(/^(\d+)([smhdw])$/);
|
|
25464
|
+
if (!match)
|
|
25465
|
+
return null;
|
|
25466
|
+
const amount = Number(match[1]);
|
|
25467
|
+
const unit = match[2];
|
|
25468
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
25469
|
+
return null;
|
|
25470
|
+
const multipliers = {
|
|
25471
|
+
s: 1000,
|
|
25472
|
+
m: 60 * 1000,
|
|
25473
|
+
h: 60 * 60 * 1000,
|
|
25474
|
+
d: DAY_MS,
|
|
25475
|
+
w: 7 * DAY_MS
|
|
25476
|
+
};
|
|
25477
|
+
return amount * multipliers[unit];
|
|
25478
|
+
}
|
|
25479
|
+
function parseBeforeArgument(raw) {
|
|
25480
|
+
const durationMs = parseDuration(raw);
|
|
25481
|
+
if (durationMs !== null)
|
|
25482
|
+
return Date.now() - durationMs;
|
|
25483
|
+
const isoMs = parseIsoDate(raw);
|
|
25484
|
+
if (isoMs !== null)
|
|
25485
|
+
return isoMs;
|
|
25486
|
+
throw new Error(`Invalid --before value '${raw}'. Use ISO date or duration like 7d.`);
|
|
25487
|
+
}
|
|
25061
25488
|
function printDbHelp() {
|
|
25062
25489
|
console.log([
|
|
25063
25490
|
"",
|
|
25064
|
-
"Usage: specialists db <setup|backfill>",
|
|
25491
|
+
"Usage: specialists db <setup|backfill|vacuum|prune>",
|
|
25065
25492
|
"",
|
|
25066
|
-
"Human-only commands for
|
|
25493
|
+
"Human-only commands for shared observability SQLite database.",
|
|
25067
25494
|
"",
|
|
25068
25495
|
"Commands:",
|
|
25069
|
-
" setup
|
|
25070
|
-
" init
|
|
25071
|
-
" backfill
|
|
25072
|
-
"
|
|
25496
|
+
" setup Provision database file + schema + .gitignore entries",
|
|
25497
|
+
" init Alias for setup",
|
|
25498
|
+
" backfill [--events] Import historical .specialists/jobs/*/status.json rows",
|
|
25499
|
+
" vacuum Run SQLite VACUUM (refuses when running/starting jobs exist)",
|
|
25500
|
+
" prune --before <iso|duration> Prune old rows (default dry-run)",
|
|
25501
|
+
" [--dry-run] [--apply] [--include-epics]",
|
|
25073
25502
|
"",
|
|
25074
25503
|
"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)",
|
|
25504
|
+
" - prune keeps specialist_events last 30 days always",
|
|
25505
|
+
" - prune removes specialist_results and terminal specialist_jobs older than --before",
|
|
25506
|
+
" - prune never touches active-chain jobs",
|
|
25507
|
+
" - prune never touches epic_runs unless --include-epics",
|
|
25081
25508
|
"",
|
|
25082
25509
|
"Examples:",
|
|
25083
25510
|
" specialists db setup",
|
|
25084
|
-
" specialists db backfill",
|
|
25085
25511
|
" specialists db backfill --events",
|
|
25086
|
-
"
|
|
25087
|
-
"
|
|
25512
|
+
" specialists db vacuum",
|
|
25513
|
+
" specialists db prune --before 30d --dry-run",
|
|
25514
|
+
" specialists db prune --before 2026-01-01T00:00:00Z --apply --include-epics",
|
|
25088
25515
|
""
|
|
25089
25516
|
].join(`
|
|
25090
25517
|
`));
|
|
@@ -25094,7 +25521,7 @@ function assertHumanInteractiveTerminal(commandName) {
|
|
|
25094
25521
|
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
25522
|
if (!inAgentSession)
|
|
25096
25523
|
return;
|
|
25097
|
-
console.error(`specialists db ${commandName} requires
|
|
25524
|
+
console.error(`specialists db ${commandName} requires interactive terminal. user-only setup command.`);
|
|
25098
25525
|
process.exit(1);
|
|
25099
25526
|
}
|
|
25100
25527
|
function printSetupResult(created, gitignoreUpdated, location) {
|
|
@@ -25123,9 +25550,48 @@ function parseBackfillOptions(argv) {
|
|
|
25123
25550
|
}
|
|
25124
25551
|
return { importEvents };
|
|
25125
25552
|
}
|
|
25553
|
+
function parsePruneOptions(argv) {
|
|
25554
|
+
let beforeValue = null;
|
|
25555
|
+
let apply = false;
|
|
25556
|
+
let dryRun = true;
|
|
25557
|
+
let includeEpics = false;
|
|
25558
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
25559
|
+
const argument = argv[index];
|
|
25560
|
+
if (argument === "--before") {
|
|
25561
|
+
const value = argv[index + 1];
|
|
25562
|
+
if (!value)
|
|
25563
|
+
throw new Error("Missing value for --before");
|
|
25564
|
+
beforeValue = value;
|
|
25565
|
+
index += 1;
|
|
25566
|
+
continue;
|
|
25567
|
+
}
|
|
25568
|
+
if (argument === "--apply") {
|
|
25569
|
+
apply = true;
|
|
25570
|
+
dryRun = false;
|
|
25571
|
+
continue;
|
|
25572
|
+
}
|
|
25573
|
+
if (argument === "--dry-run") {
|
|
25574
|
+
dryRun = true;
|
|
25575
|
+
apply = false;
|
|
25576
|
+
continue;
|
|
25577
|
+
}
|
|
25578
|
+
if (argument === "--include-epics") {
|
|
25579
|
+
includeEpics = true;
|
|
25580
|
+
continue;
|
|
25581
|
+
}
|
|
25582
|
+
throw new Error(`Unknown option for db prune: '${argument}'`);
|
|
25583
|
+
}
|
|
25584
|
+
if (!beforeValue)
|
|
25585
|
+
throw new Error("Missing required --before for db prune");
|
|
25586
|
+
return {
|
|
25587
|
+
beforeMs: parseBeforeArgument(beforeValue),
|
|
25588
|
+
apply: apply && !dryRun,
|
|
25589
|
+
includeEpics
|
|
25590
|
+
};
|
|
25591
|
+
}
|
|
25126
25592
|
function parseStatusFile(jobDirectoryPath, fallbackJobId) {
|
|
25127
25593
|
const statusPath = join11(jobDirectoryPath, "status.json");
|
|
25128
|
-
const statusRaw =
|
|
25594
|
+
const statusRaw = readFileSync8(statusPath, "utf-8");
|
|
25129
25595
|
const parsed = JSON.parse(statusRaw);
|
|
25130
25596
|
const jobId = typeof parsed.id === "string" && parsed.id.length > 0 ? parsed.id : fallbackJobId;
|
|
25131
25597
|
const specialist = typeof parsed.specialist === "string" && parsed.specialist.length > 0 ? parsed.specialist : "unknown";
|
|
@@ -25142,7 +25608,7 @@ function parseStatusFile(jobDirectoryPath, fallbackJobId) {
|
|
|
25142
25608
|
function replayEvents(eventsPath, sqliteClient, status) {
|
|
25143
25609
|
if (!existsSync10(eventsPath))
|
|
25144
25610
|
return 0;
|
|
25145
|
-
const rawContent =
|
|
25611
|
+
const rawContent = readFileSync8(eventsPath, "utf-8");
|
|
25146
25612
|
const lines = rawContent.split(`
|
|
25147
25613
|
`).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
25148
25614
|
let importedEvents = 0;
|
|
@@ -25239,6 +25705,231 @@ ${bold7("specialists db backfill")}
|
|
|
25239
25705
|
}
|
|
25240
25706
|
console.log("");
|
|
25241
25707
|
}
|
|
25708
|
+
function runVacuum() {
|
|
25709
|
+
const sqliteClient = createObservabilitySqliteClient();
|
|
25710
|
+
if (!sqliteClient) {
|
|
25711
|
+
throw new Error("Failed to initialize observability SQLite schema. Run `specialists db setup` first and ensure sqlite3 is installed.");
|
|
25712
|
+
}
|
|
25713
|
+
try {
|
|
25714
|
+
const activeJobs = sqliteClient.listActiveJobs(["running", "starting"]);
|
|
25715
|
+
if (activeJobs.length > 0) {
|
|
25716
|
+
const listing = activeJobs.slice(0, 5).map((job) => `${job.job_id}:${job.status}`).join(", ");
|
|
25717
|
+
throw new Error(`Refusing vacuum while active jobs exist (${activeJobs.length}): ${listing}`);
|
|
25718
|
+
}
|
|
25719
|
+
const { beforeBytes, afterBytes } = sqliteClient.vacuumDatabase();
|
|
25720
|
+
const savedBytes = Math.max(0, beforeBytes - afterBytes);
|
|
25721
|
+
console.log(`
|
|
25722
|
+
${bold7("specialists db vacuum")}
|
|
25723
|
+
`);
|
|
25724
|
+
console.log(` ${green5("\u2713")} before: ${formatBytes(beforeBytes)} (${beforeBytes} bytes)`);
|
|
25725
|
+
console.log(` ${green5("\u2713")} after: ${formatBytes(afterBytes)} (${afterBytes} bytes)`);
|
|
25726
|
+
console.log(` ${green5("\u2713")} saved: ${formatBytes(savedBytes)} (${savedBytes} bytes)`);
|
|
25727
|
+
console.log("");
|
|
25728
|
+
} finally {
|
|
25729
|
+
sqliteClient.close();
|
|
25730
|
+
}
|
|
25731
|
+
}
|
|
25732
|
+
function runPrune(options) {
|
|
25733
|
+
const sqliteClient = createObservabilitySqliteClient();
|
|
25734
|
+
if (!sqliteClient) {
|
|
25735
|
+
throw new Error("Failed to initialize observability SQLite schema. Run `specialists db setup` first and ensure sqlite3 is installed.");
|
|
25736
|
+
}
|
|
25737
|
+
try {
|
|
25738
|
+
const report = sqliteClient.pruneObservabilityData({
|
|
25739
|
+
beforeMs: options.beforeMs,
|
|
25740
|
+
includeEpics: options.includeEpics,
|
|
25741
|
+
apply: options.apply
|
|
25742
|
+
});
|
|
25743
|
+
console.log(`
|
|
25744
|
+
${bold7("specialists db prune")}
|
|
25745
|
+
`);
|
|
25746
|
+
console.log(` ${report.dryRun ? yellow6("\u25CB dry-run") : green5("\u2713 applied")}`);
|
|
25747
|
+
console.log(` ${green5("\u2713")} before: ${new Date(report.beforeMs).toISOString()}`);
|
|
25748
|
+
console.log(` ${green5("\u2713")} events cutoff (fixed 30d): ${new Date(report.eventsCutoffMs).toISOString()}`);
|
|
25749
|
+
console.log(` ${green5("\u2713")} specialist_events: ${report.deletedEvents}`);
|
|
25750
|
+
console.log(` ${green5("\u2713")} specialist_results: ${report.deletedResults}`);
|
|
25751
|
+
console.log(` ${green5("\u2713")} specialist_jobs: ${report.deletedJobs}`);
|
|
25752
|
+
console.log(` ${report.includeEpics ? green5("\u2713") : yellow6("\u25CB")} epic_runs: ${report.deletedEpicRuns} ${report.includeEpics ? "" : "(skipped, use --include-epics)"}`);
|
|
25753
|
+
console.log(` ${yellow6("\u25CB")} skipped active-chain jobs: ${report.skippedActiveChainJobs}`);
|
|
25754
|
+
console.log("");
|
|
25755
|
+
} finally {
|
|
25756
|
+
sqliteClient.close();
|
|
25757
|
+
}
|
|
25758
|
+
}
|
|
25759
|
+
function parseBenchmarkExportOptions(argv) {
|
|
25760
|
+
const defaultOutput = resolve5(process.cwd(), ".specialists/benchmarks/executor-benchmark-rows.jsonl");
|
|
25761
|
+
let outputPath = defaultOutput;
|
|
25762
|
+
let epicId;
|
|
25763
|
+
let includePrepJobs = false;
|
|
25764
|
+
for (let i = 0;i < argv.length; i += 1) {
|
|
25765
|
+
const argument = argv[i];
|
|
25766
|
+
if (argument === "--output" && argv[i + 1]) {
|
|
25767
|
+
outputPath = resolve5(process.cwd(), argv[i + 1]);
|
|
25768
|
+
i += 1;
|
|
25769
|
+
continue;
|
|
25770
|
+
}
|
|
25771
|
+
if (argument === "--epic" && argv[i + 1]) {
|
|
25772
|
+
epicId = argv[i + 1];
|
|
25773
|
+
i += 1;
|
|
25774
|
+
continue;
|
|
25775
|
+
}
|
|
25776
|
+
if (argument === "--include-prep") {
|
|
25777
|
+
includePrepJobs = true;
|
|
25778
|
+
continue;
|
|
25779
|
+
}
|
|
25780
|
+
throw new Error(`Unknown option for db benchmark-export: '${argument}'`);
|
|
25781
|
+
}
|
|
25782
|
+
return { outputPath, epicId, includePrepJobs };
|
|
25783
|
+
}
|
|
25784
|
+
function parseReviewerVerdict2(output2) {
|
|
25785
|
+
if (!output2)
|
|
25786
|
+
return "MISSING";
|
|
25787
|
+
const match = output2.match(/Verdict:\s*(PASS|PARTIAL|FAIL)/i);
|
|
25788
|
+
if (!match?.[1])
|
|
25789
|
+
return "MISSING";
|
|
25790
|
+
return match[1].toUpperCase();
|
|
25791
|
+
}
|
|
25792
|
+
function parseReviewerScore(output2) {
|
|
25793
|
+
if (!output2)
|
|
25794
|
+
return null;
|
|
25795
|
+
const match = output2.match(/(?:Reviewer\s+)?Score(?:\s*\(0-100\))?\s*[:=]\s*(\d+(?:\.\d+)?)/i);
|
|
25796
|
+
return match?.[1] ? Number(match[1]) : null;
|
|
25797
|
+
}
|
|
25798
|
+
function parseGateResult(output2, key) {
|
|
25799
|
+
if (!output2)
|
|
25800
|
+
return null;
|
|
25801
|
+
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;
|
|
25802
|
+
const match = output2.match(regex);
|
|
25803
|
+
if (!match?.[1])
|
|
25804
|
+
return null;
|
|
25805
|
+
const normalized = match[1].toLowerCase();
|
|
25806
|
+
return normalized === "true" || normalized === "pass";
|
|
25807
|
+
}
|
|
25808
|
+
function readLatestRunCompleteEvent(events) {
|
|
25809
|
+
for (let index = events.length - 1;index >= 0; index -= 1) {
|
|
25810
|
+
const event = events[index];
|
|
25811
|
+
if (event?.type === "run_complete") {
|
|
25812
|
+
return event;
|
|
25813
|
+
}
|
|
25814
|
+
}
|
|
25815
|
+
return null;
|
|
25816
|
+
}
|
|
25817
|
+
function inferFailureNotes(input2) {
|
|
25818
|
+
const notes = [];
|
|
25819
|
+
if (input2.runComplete?.status === "ERROR") {
|
|
25820
|
+
notes.push(`run_complete_status=ERROR${input2.runComplete.error ? `: ${input2.runComplete.error}` : ""}`);
|
|
25821
|
+
}
|
|
25822
|
+
if (input2.runComplete?.status === "CANCELLED") {
|
|
25823
|
+
notes.push("run_complete_status=CANCELLED");
|
|
25824
|
+
}
|
|
25825
|
+
if (input2.runComplete?.exit_reason) {
|
|
25826
|
+
notes.push(`exit_reason=${input2.runComplete.exit_reason}`);
|
|
25827
|
+
}
|
|
25828
|
+
if (input2.runComplete?.finish_reason) {
|
|
25829
|
+
notes.push(`finish_reason=${input2.runComplete.finish_reason}`);
|
|
25830
|
+
}
|
|
25831
|
+
if (input2.status.error) {
|
|
25832
|
+
notes.push(`status_error=${input2.status.error}`);
|
|
25833
|
+
}
|
|
25834
|
+
if (input2.reviewerVerdict !== "PASS" && input2.hasLaterExecutorInChain) {
|
|
25835
|
+
notes.push("fix_loop_rerun_detected_after_non_pass_review");
|
|
25836
|
+
}
|
|
25837
|
+
if (!input2.runComplete) {
|
|
25838
|
+
notes.push("missing_run_complete_event_fallback_to_status_metrics");
|
|
25839
|
+
}
|
|
25840
|
+
return notes;
|
|
25841
|
+
}
|
|
25842
|
+
function runBenchmarkExport(options) {
|
|
25843
|
+
const sqliteClient = createObservabilitySqliteClient();
|
|
25844
|
+
if (!sqliteClient) {
|
|
25845
|
+
throw new Error("Failed to initialize observability SQLite schema. Run `specialists db setup` first and ensure sqlite3 is installed.");
|
|
25846
|
+
}
|
|
25847
|
+
try {
|
|
25848
|
+
const statuses = sqliteClient.listStatuses().filter((status) => options.includePrepJobs || status.chain_kind === "chain").filter((status) => !options.epicId || status.epic_id === options.epicId);
|
|
25849
|
+
const byChain = new Map;
|
|
25850
|
+
for (const status of statuses) {
|
|
25851
|
+
const chainId = status.chain_id ?? `job:${status.id}`;
|
|
25852
|
+
const group = byChain.get(chainId) ?? [];
|
|
25853
|
+
group.push(status);
|
|
25854
|
+
byChain.set(chainId, group);
|
|
25855
|
+
}
|
|
25856
|
+
const rows = [];
|
|
25857
|
+
for (const chainStatuses of byChain.values()) {
|
|
25858
|
+
const ordered = [...chainStatuses].sort((a, b) => a.started_at_ms - b.started_at_ms);
|
|
25859
|
+
const executorStatuses = ordered.filter((status) => status.specialist === "executor");
|
|
25860
|
+
const reviewerStatuses = ordered.filter((status) => status.specialist === "reviewer");
|
|
25861
|
+
executorStatuses.forEach((executorStatus, executorIndex) => {
|
|
25862
|
+
const nextExecutor = executorStatuses[executorIndex + 1];
|
|
25863
|
+
const reviewer = reviewerStatuses.find((candidate) => {
|
|
25864
|
+
if (candidate.started_at_ms < executorStatus.started_at_ms)
|
|
25865
|
+
return false;
|
|
25866
|
+
if (!nextExecutor)
|
|
25867
|
+
return true;
|
|
25868
|
+
return candidate.started_at_ms < nextExecutor.started_at_ms;
|
|
25869
|
+
}) ?? null;
|
|
25870
|
+
const runComplete = readLatestRunCompleteEvent(sqliteClient.readEvents(executorStatus.id));
|
|
25871
|
+
const reviewerOutput = reviewer ? sqliteClient.readResult(reviewer.id) : null;
|
|
25872
|
+
const reviewerVerdict = parseReviewerVerdict2(reviewerOutput);
|
|
25873
|
+
const totalTokens = runComplete?.token_usage?.total_tokens ?? runComplete?.metrics?.token_usage?.total_tokens ?? executorStatus.metrics?.token_usage?.total_tokens ?? null;
|
|
25874
|
+
const costUsd = runComplete?.token_usage?.cost_usd ?? runComplete?.metrics?.token_usage?.cost_usd ?? executorStatus.metrics?.token_usage?.cost_usd ?? null;
|
|
25875
|
+
const elapsedMs = runComplete ? Math.round(runComplete.elapsed_s * 1000) : typeof executorStatus.elapsed_s === "number" ? Math.round(executorStatus.elapsed_s * 1000) : null;
|
|
25876
|
+
const hasLaterExecutorInChain = Boolean(nextExecutor);
|
|
25877
|
+
const failureNotes = inferFailureNotes({
|
|
25878
|
+
status: executorStatus,
|
|
25879
|
+
runComplete,
|
|
25880
|
+
reviewerVerdict,
|
|
25881
|
+
hasLaterExecutorInChain
|
|
25882
|
+
});
|
|
25883
|
+
rows.push({
|
|
25884
|
+
task_id: executorStatus.chain_root_bead_id ?? executorStatus.bead_id ?? "unknown_task",
|
|
25885
|
+
model_id: executorStatus.model ?? null,
|
|
25886
|
+
executor_job_id: executorStatus.id,
|
|
25887
|
+
reviewer_job_id: reviewer?.id ?? null,
|
|
25888
|
+
lint_pass: parseGateResult(reviewerOutput, "lint"),
|
|
25889
|
+
tsc_pass: parseGateResult(reviewerOutput, "tsc"),
|
|
25890
|
+
reviewer_verdict: reviewerVerdict,
|
|
25891
|
+
reviewer_score_if_present: parseReviewerScore(reviewerOutput),
|
|
25892
|
+
total_tokens: totalTokens,
|
|
25893
|
+
cost_usd: costUsd,
|
|
25894
|
+
elapsed_ms: elapsedMs,
|
|
25895
|
+
failure_notes: failureNotes,
|
|
25896
|
+
source_of_truth: {
|
|
25897
|
+
task_id: "specialist_jobs.chain_root_bead_id fallback bead_id",
|
|
25898
|
+
model_id: "specialist_jobs.status_json.model",
|
|
25899
|
+
executor_job_id: "specialist_jobs.job_id",
|
|
25900
|
+
reviewer_job_id: "specialist_jobs.job_id where specialist=reviewer in same chain window",
|
|
25901
|
+
lint_pass: "reviewer specialist_results.output regex parse; null when absent",
|
|
25902
|
+
tsc_pass: "reviewer specialist_results.output regex parse; null when absent",
|
|
25903
|
+
reviewer_verdict: "reviewer specialist_results.output Verdict: PASS|PARTIAL|FAIL",
|
|
25904
|
+
reviewer_score_if_present: "reviewer specialist_results.output score regex; null when absent",
|
|
25905
|
+
total_tokens: runComplete ? "specialist_events.type=run_complete.token_usage.total_tokens" : "status_json.metrics.token_usage.total_tokens fallback",
|
|
25906
|
+
cost_usd: runComplete ? "specialist_events.type=run_complete.token_usage.cost_usd" : "status_json.metrics.token_usage.cost_usd fallback",
|
|
25907
|
+
elapsed_ms: runComplete ? "specialist_events.type=run_complete.elapsed_s * 1000" : "status_json.elapsed_s * 1000 fallback",
|
|
25908
|
+
failure_notes: "run_complete.error/status + status_json.error + chain sequencing heuristics"
|
|
25909
|
+
}
|
|
25910
|
+
});
|
|
25911
|
+
});
|
|
25912
|
+
}
|
|
25913
|
+
rows.sort((a, b) => a.task_id.localeCompare(b.task_id) || a.executor_job_id.localeCompare(b.executor_job_id));
|
|
25914
|
+
const outputDirectory = dirname6(options.outputPath);
|
|
25915
|
+
mkdirSync5(outputDirectory, { recursive: true });
|
|
25916
|
+
const jsonl = rows.map((row) => JSON.stringify(row)).join(`
|
|
25917
|
+
`);
|
|
25918
|
+
writeFileSync5(options.outputPath, rows.length > 0 ? `${jsonl}
|
|
25919
|
+
` : "", "utf-8");
|
|
25920
|
+
console.log(`
|
|
25921
|
+
${bold7("specialists db benchmark-export")}
|
|
25922
|
+
`);
|
|
25923
|
+
console.log(` ${green5("\u2713")} rows exported: ${rows.length}`);
|
|
25924
|
+
console.log(` ${green5("\u2713")} output: ${options.outputPath}`);
|
|
25925
|
+
if (options.epicId) {
|
|
25926
|
+
console.log(` ${green5("\u2713")} epic filter: ${options.epicId}`);
|
|
25927
|
+
}
|
|
25928
|
+
console.log("");
|
|
25929
|
+
} finally {
|
|
25930
|
+
sqliteClient.close();
|
|
25931
|
+
}
|
|
25932
|
+
}
|
|
25242
25933
|
function runSetup() {
|
|
25243
25934
|
const location = resolveObservabilityDbLocation(process.cwd());
|
|
25244
25935
|
if (isPathInsideJobsDirectory(location.dbPath, location.gitRoot)) {
|
|
@@ -25270,17 +25961,32 @@ async function run8(argv = process.argv.slice(3)) {
|
|
|
25270
25961
|
runBackfill(options);
|
|
25271
25962
|
return;
|
|
25272
25963
|
}
|
|
25964
|
+
if (subcommand === "vacuum") {
|
|
25965
|
+
runVacuum();
|
|
25966
|
+
return;
|
|
25967
|
+
}
|
|
25968
|
+
if (subcommand === "prune") {
|
|
25969
|
+
const options = parsePruneOptions(argv.slice(1));
|
|
25970
|
+
runPrune(options);
|
|
25971
|
+
return;
|
|
25972
|
+
}
|
|
25973
|
+
if (subcommand === "benchmark-export") {
|
|
25974
|
+
const options = parseBenchmarkExportOptions(argv.slice(1));
|
|
25975
|
+
runBenchmarkExport(options);
|
|
25976
|
+
return;
|
|
25977
|
+
}
|
|
25273
25978
|
console.error(`Unknown db subcommand: '${subcommand}'`);
|
|
25274
25979
|
printDbHelp();
|
|
25275
25980
|
process.exit(1);
|
|
25276
25981
|
}
|
|
25277
|
-
var bold7 = (s) => `\x1B[1m${s}\x1B[0m`, green5 = (s) => `\x1B[32m${s}\x1B[0m`, yellow6 = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
25982
|
+
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
25983
|
var init_db = __esm(() => {
|
|
25279
25984
|
init_observability_db();
|
|
25280
25985
|
init_job_root();
|
|
25281
25986
|
init_observability_sqlite();
|
|
25282
25987
|
init_chain_identity();
|
|
25283
25988
|
init_timeline_events();
|
|
25989
|
+
DAY_MS = 24 * 60 * 60 * 1000;
|
|
25284
25990
|
});
|
|
25285
25991
|
|
|
25286
25992
|
// src/cli/validate.ts
|
|
@@ -25410,7 +26116,7 @@ __export(exports_edit, {
|
|
|
25410
26116
|
run: () => run10
|
|
25411
26117
|
});
|
|
25412
26118
|
import { spawnSync as spawnSync10 } from "child_process";
|
|
25413
|
-
import { existsSync as existsSync12, readdirSync as readdirSync5, readFileSync as
|
|
26119
|
+
import { existsSync as existsSync12, readdirSync as readdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
|
|
25414
26120
|
import { join as join13 } from "path";
|
|
25415
26121
|
function loadPresets() {
|
|
25416
26122
|
const paths = [
|
|
@@ -25420,7 +26126,7 @@ function loadPresets() {
|
|
|
25420
26126
|
for (const p of paths) {
|
|
25421
26127
|
if (existsSync12(p)) {
|
|
25422
26128
|
try {
|
|
25423
|
-
const data = JSON.parse(
|
|
26129
|
+
const data = JSON.parse(readFileSync9(p, "utf-8"));
|
|
25424
26130
|
return data;
|
|
25425
26131
|
} catch {
|
|
25426
26132
|
return {};
|
|
@@ -25773,7 +26479,7 @@ function getRawValue(args, resolvedPath) {
|
|
|
25773
26479
|
if (!MULTILINE_FILE_PATHS.has(resolvedPath.normalizedPath)) {
|
|
25774
26480
|
fail(`Error: --file is only supported for: ${Array.from(MULTILINE_FILE_PATHS).join(", ")}`);
|
|
25775
26481
|
}
|
|
25776
|
-
return
|
|
26482
|
+
return readFileSync9(args.filePath, "utf-8");
|
|
25777
26483
|
}
|
|
25778
26484
|
function getAtPath(root, segments) {
|
|
25779
26485
|
let current = root;
|
|
@@ -25883,7 +26589,7 @@ async function run10() {
|
|
|
25883
26589
|
}
|
|
25884
26590
|
const targets2 = await resolveTargets(args);
|
|
25885
26591
|
for (const target of targets2) {
|
|
25886
|
-
const raw =
|
|
26592
|
+
const raw = readFileSync9(target.filePath, "utf-8");
|
|
25887
26593
|
const doc2 = JSON.parse(raw);
|
|
25888
26594
|
for (const [fieldPath, fieldValue] of Object.entries(preset.fields)) {
|
|
25889
26595
|
const resolved = resolvePath2(fieldPath);
|
|
@@ -25897,7 +26603,7 @@ async function run10() {
|
|
|
25897
26603
|
printDryRun(target.filePath, raw, updated);
|
|
25898
26604
|
continue;
|
|
25899
26605
|
}
|
|
25900
|
-
|
|
26606
|
+
writeFileSync6(target.filePath, updated, "utf-8");
|
|
25901
26607
|
const fieldList = Object.keys(preset.fields).map((f) => yellow8(f)).join(", ");
|
|
25902
26608
|
console.log(`${green7("\u2713")} ${bold9(target.name)}: applied preset ${bold9(args.preset)} (${fieldList})`);
|
|
25903
26609
|
}
|
|
@@ -25909,7 +26615,7 @@ async function run10() {
|
|
|
25909
26615
|
fail("Error: no specialists found");
|
|
25910
26616
|
}
|
|
25911
26617
|
for (const target of targets) {
|
|
25912
|
-
const raw =
|
|
26618
|
+
const raw = readFileSync9(target.filePath, "utf-8");
|
|
25913
26619
|
let doc2;
|
|
25914
26620
|
try {
|
|
25915
26621
|
const parsed = JSON.parse(raw);
|
|
@@ -25933,7 +26639,7 @@ async function run10() {
|
|
|
25933
26639
|
printDryRun(target.filePath, raw, updated);
|
|
25934
26640
|
continue;
|
|
25935
26641
|
}
|
|
25936
|
-
|
|
26642
|
+
writeFileSync6(target.filePath, updated, "utf-8");
|
|
25937
26643
|
console.log(`${green7("\u2713")} ${bold9(target.name)}: ${yellow8(resolvedPath.normalizedPath)} = ${formatOutputValue(nextValue)}` + dim7(` (${target.filePath})`));
|
|
25938
26644
|
}
|
|
25939
26645
|
}
|
|
@@ -26052,8 +26758,8 @@ var init_config = __esm(() => {
|
|
|
26052
26758
|
});
|
|
26053
26759
|
|
|
26054
26760
|
// src/specialist/worktree.ts
|
|
26055
|
-
import { existsSync as existsSync13, symlinkSync as symlinkSync2, mkdirSync as
|
|
26056
|
-
import { join as join14, resolve as
|
|
26761
|
+
import { existsSync as existsSync13, symlinkSync as symlinkSync2, mkdirSync as mkdirSync6 } from "fs";
|
|
26762
|
+
import { join as join14, resolve as resolve6 } from "path";
|
|
26057
26763
|
import { spawnSync as spawnSync11, execFileSync as execFileSync2 } from "child_process";
|
|
26058
26764
|
function deriveBranchName(beadId, specialistName) {
|
|
26059
26765
|
return `feature/${beadId}-${slugify(specialistName)}`;
|
|
@@ -26086,11 +26792,11 @@ function provisionWorktree(options) {
|
|
|
26086
26792
|
const branch = deriveBranchName(options.beadId, options.specialistName);
|
|
26087
26793
|
const existingPath = findExistingWorktree(branch, cwd);
|
|
26088
26794
|
if (existingPath) {
|
|
26089
|
-
return { branch, worktreePath:
|
|
26795
|
+
return { branch, worktreePath: resolve6(existingPath), reused: true };
|
|
26090
26796
|
}
|
|
26091
26797
|
const worktreeBase = options.worktreeBase ?? join14(commonRoot, ".worktrees", options.beadId);
|
|
26092
26798
|
const worktreeName = deriveWorktreeName(options.beadId, options.specialistName);
|
|
26093
|
-
const worktreePath =
|
|
26799
|
+
const worktreePath = resolve6(join14(worktreeBase, worktreeName));
|
|
26094
26800
|
createWorktreeViaBd(worktreePath, branch, commonRoot);
|
|
26095
26801
|
symlinkPiNpmCache(commonRoot, worktreePath);
|
|
26096
26802
|
return { branch, worktreePath, reused: false };
|
|
@@ -26101,7 +26807,7 @@ function symlinkPiNpmCache(commonRoot, worktreePath) {
|
|
|
26101
26807
|
if (!existsSync13(source) || existsSync13(target))
|
|
26102
26808
|
return;
|
|
26103
26809
|
try {
|
|
26104
|
-
|
|
26810
|
+
mkdirSync6(join14(worktreePath, ".pi"), { recursive: true });
|
|
26105
26811
|
symlinkSync2(source, target);
|
|
26106
26812
|
} catch {}
|
|
26107
26813
|
}
|
|
@@ -26161,9 +26867,10 @@ __export(exports_merge, {
|
|
|
26161
26867
|
executePublicationPlan: () => executePublicationPlan,
|
|
26162
26868
|
evaluateMergeWorthiness: () => evaluateMergeWorthiness,
|
|
26163
26869
|
ensureTerminalJobs: () => ensureTerminalJobs,
|
|
26164
|
-
checkEpicUnresolvedGuard: () => checkEpicUnresolvedGuard
|
|
26870
|
+
checkEpicUnresolvedGuard: () => checkEpicUnresolvedGuard,
|
|
26871
|
+
assertMainRepoCleanForMerge: () => assertMainRepoCleanForMerge
|
|
26165
26872
|
});
|
|
26166
|
-
import { existsSync as existsSync14, readdirSync as readdirSync6, readFileSync as
|
|
26873
|
+
import { existsSync as existsSync14, readdirSync as readdirSync6, readFileSync as readFileSync10 } from "fs";
|
|
26167
26874
|
import { spawnSync as spawnSync12 } from "child_process";
|
|
26168
26875
|
import { join as join15 } from "path";
|
|
26169
26876
|
function parseOptions(argv) {
|
|
@@ -26353,7 +27060,7 @@ function readAllJobStatuses() {
|
|
|
26353
27060
|
const statusPath = join15(jobsDir, entry.name, "status.json");
|
|
26354
27061
|
if (!existsSync14(statusPath))
|
|
26355
27062
|
continue;
|
|
26356
|
-
const parsed = readJson(
|
|
27063
|
+
const parsed = readJson(readFileSync10(statusPath, "utf-8"));
|
|
26357
27064
|
if (!parsed || typeof parsed !== "object")
|
|
26358
27065
|
continue;
|
|
26359
27066
|
statuses.push(parsed);
|
|
@@ -26480,6 +27187,29 @@ function readChangedFilesForLastMerge(cwd = process.cwd()) {
|
|
|
26480
27187
|
return diff.stdout.split(`
|
|
26481
27188
|
`).map((line) => line.trim()).filter(Boolean);
|
|
26482
27189
|
}
|
|
27190
|
+
function isMergeDirtyIgnored(path) {
|
|
27191
|
+
return MERGE_DIRTY_IGNORE_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
27192
|
+
}
|
|
27193
|
+
function assertMainRepoCleanForMerge(cwd) {
|
|
27194
|
+
const status = runCommand("git", ["status", "--porcelain", "--untracked-files=no"], cwd);
|
|
27195
|
+
if (status.status !== 0) {
|
|
27196
|
+
throw new Error(`Unable to read git status in '${cwd}'.`);
|
|
27197
|
+
}
|
|
27198
|
+
const dirty = status.stdout.split(`
|
|
27199
|
+
`).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
27200
|
+
const match = /^..\s(.+?)(?:\s->\s(.+))?$/.exec(line);
|
|
27201
|
+
const path = match ? match[2] ?? match[1] ?? "" : line.slice(3).trim();
|
|
27202
|
+
return path.trim();
|
|
27203
|
+
}).filter((path) => path && !isMergeDirtyIgnored(path));
|
|
27204
|
+
if (dirty.length === 0)
|
|
27205
|
+
return;
|
|
27206
|
+
const list = dirty.map((path) => `- ${path}`).join(`
|
|
27207
|
+
`);
|
|
27208
|
+
throw new Error(`Refusing merge: main repo '${cwd}' has uncommitted changes that could cause spurious conflicts.
|
|
27209
|
+
` + `Dirty files (tracked, non-dist):
|
|
27210
|
+
${list}
|
|
27211
|
+
` + `Resolve by committing, stashing, or reverting these changes, then retry merge.`);
|
|
27212
|
+
}
|
|
26483
27213
|
function parseNameStatusLine(line) {
|
|
26484
27214
|
const trimmed = line.trim();
|
|
26485
27215
|
if (!trimmed)
|
|
@@ -26648,6 +27378,7 @@ function printUsageAndExit(message) {
|
|
|
26648
27378
|
}
|
|
26649
27379
|
function runMergePlan(targets, options) {
|
|
26650
27380
|
const mainRepoRoot = resolveMainWorktreeRoot();
|
|
27381
|
+
assertMainRepoCleanForMerge(mainRepoRoot);
|
|
26651
27382
|
const mergedSteps = [];
|
|
26652
27383
|
for (const target of targets) {
|
|
26653
27384
|
rebaseBranchOntoMaster(target.branch, target.worktreePath);
|
|
@@ -26740,13 +27471,17 @@ async function run12() {
|
|
|
26740
27471
|
const mergedSteps = runMergePlan(targets, { rebuild: options.rebuild });
|
|
26741
27472
|
printSummary(mergedSteps, options.rebuild);
|
|
26742
27473
|
}
|
|
26743
|
-
var TERMINAL_STATUSES, NOISE_PATH_PREFIXES;
|
|
27474
|
+
var TERMINAL_STATUSES, NOISE_PATH_PREFIXES, MERGE_DIRTY_IGNORE_PREFIXES;
|
|
26744
27475
|
var init_merge = __esm(() => {
|
|
26745
27476
|
init_job_root();
|
|
26746
27477
|
init_observability_sqlite();
|
|
26747
27478
|
init_epic_lifecycle();
|
|
26748
27479
|
TERMINAL_STATUSES = new Set(["done", "error", "cancelled"]);
|
|
26749
27480
|
NOISE_PATH_PREFIXES = [".xtrm/reports/", ".wolf/", ".specialists/jobs/"];
|
|
27481
|
+
MERGE_DIRTY_IGNORE_PREFIXES = [
|
|
27482
|
+
...NOISE_PATH_PREFIXES,
|
|
27483
|
+
"dist/"
|
|
27484
|
+
];
|
|
26750
27485
|
});
|
|
26751
27486
|
|
|
26752
27487
|
// src/cli/format-helpers.ts
|
|
@@ -26976,7 +27711,7 @@ __export(exports_run, {
|
|
|
26976
27711
|
run: () => run13
|
|
26977
27712
|
});
|
|
26978
27713
|
import { join as join16 } from "path";
|
|
26979
|
-
import { readFileSync as
|
|
27714
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
26980
27715
|
import { randomBytes } from "crypto";
|
|
26981
27716
|
import { spawn as cpSpawn, execSync as execSync3 } from "child_process";
|
|
26982
27717
|
async function parseArgs6(argv) {
|
|
@@ -27091,13 +27826,13 @@ async function parseArgs6(argv) {
|
|
|
27091
27826
|
process.exit(1);
|
|
27092
27827
|
}
|
|
27093
27828
|
if (!prompt && !beadId && !process.stdin.isTTY) {
|
|
27094
|
-
prompt = await new Promise((
|
|
27829
|
+
prompt = await new Promise((resolve7) => {
|
|
27095
27830
|
let buf = "";
|
|
27096
27831
|
process.stdin.setEncoding("utf-8");
|
|
27097
27832
|
process.stdin.on("data", (chunk) => {
|
|
27098
27833
|
buf += chunk;
|
|
27099
27834
|
});
|
|
27100
|
-
process.stdin.on("end", () =>
|
|
27835
|
+
process.stdin.on("end", () => resolve7(buf.trim()));
|
|
27101
27836
|
});
|
|
27102
27837
|
}
|
|
27103
27838
|
if (!prompt && !beadId && !reuseJobId) {
|
|
@@ -27255,7 +27990,7 @@ function startEventTailer(jobId, jobsDir, mode, specialist, beadId) {
|
|
|
27255
27990
|
const drain = () => {
|
|
27256
27991
|
let content;
|
|
27257
27992
|
try {
|
|
27258
|
-
content =
|
|
27993
|
+
content = readFileSync11(eventsPath, "utf-8");
|
|
27259
27994
|
} catch {
|
|
27260
27995
|
return;
|
|
27261
27996
|
}
|
|
@@ -27314,6 +28049,23 @@ function formatFooterModel(backend, model) {
|
|
|
27314
28049
|
function shellQuote(value) {
|
|
27315
28050
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
27316
28051
|
}
|
|
28052
|
+
function extractReviewedJobIdOverride(prompt) {
|
|
28053
|
+
const match = prompt.match(/(?:^|\n)\s*reviewed_job_id\s*:\s*([^\n]+)/i);
|
|
28054
|
+
const candidate = match?.[1]?.trim();
|
|
28055
|
+
return candidate ? candidate : undefined;
|
|
28056
|
+
}
|
|
28057
|
+
function buildReusedWorktreeAwarenessBlock(options) {
|
|
28058
|
+
const owner = options.worktreeOwnerJobId ?? options.reusedFromJobId;
|
|
28059
|
+
return [
|
|
28060
|
+
"## Reused workspace awareness (from --job)",
|
|
28061
|
+
`You are entering an existing worktree reused from job: ${options.reusedFromJobId}.`,
|
|
28062
|
+
`Worktree chain owner job: ${owner}.`,
|
|
28063
|
+
"Workspace may contain uncommitted edits, staged changes, generated files, or partial fixes from prior handoff steps.",
|
|
28064
|
+
"Before edits, run and inspect: git status --short --branch, git diff --stat, git diff --cached --stat.",
|
|
28065
|
+
"Treat existing tree state as real input context \u2014 do not assume clean baseline."
|
|
28066
|
+
].join(`
|
|
28067
|
+
`);
|
|
28068
|
+
}
|
|
27317
28069
|
async function run13() {
|
|
27318
28070
|
const args = await parseArgs6(process.argv.slice(3));
|
|
27319
28071
|
const loader = new SpecialistLoader;
|
|
@@ -27339,7 +28091,7 @@ async function run13() {
|
|
|
27339
28091
|
const latestPath = join16(jobsDir2, "latest");
|
|
27340
28092
|
const oldLatest = (() => {
|
|
27341
28093
|
try {
|
|
27342
|
-
return
|
|
28094
|
+
return readFileSync11(latestPath, "utf-8").trim();
|
|
27343
28095
|
} catch {
|
|
27344
28096
|
return "";
|
|
27345
28097
|
}
|
|
@@ -27367,7 +28119,7 @@ async function run13() {
|
|
|
27367
28119
|
while (Date.now() < deadline) {
|
|
27368
28120
|
await new Promise((r) => setTimeout(r, 100));
|
|
27369
28121
|
try {
|
|
27370
|
-
const current =
|
|
28122
|
+
const current = readFileSync11(latestPath, "utf-8").trim();
|
|
27371
28123
|
if (current && current !== oldLatest) {
|
|
27372
28124
|
jobId2 = current;
|
|
27373
28125
|
break;
|
|
@@ -27449,10 +28201,19 @@ async function run13() {
|
|
|
27449
28201
|
} else if (args.epicId) {
|
|
27450
28202
|
epicId = args.epicId;
|
|
27451
28203
|
}
|
|
28204
|
+
variables = {
|
|
28205
|
+
...variables ?? {},
|
|
28206
|
+
reused_worktree_awareness: ""
|
|
28207
|
+
};
|
|
27452
28208
|
if (args.reuseJobId) {
|
|
28209
|
+
const reviewedJobId = extractReviewedJobIdOverride(prompt) ?? args.reuseJobId;
|
|
27453
28210
|
variables = {
|
|
27454
28211
|
...variables ?? {},
|
|
27455
|
-
reviewed_job_id:
|
|
28212
|
+
reviewed_job_id: reviewedJobId,
|
|
28213
|
+
reused_worktree_awareness: buildReusedWorktreeAwarenessBlock({
|
|
28214
|
+
reusedFromJobId: args.reuseJobId,
|
|
28215
|
+
worktreeOwnerJobId
|
|
28216
|
+
})
|
|
27456
28217
|
};
|
|
27457
28218
|
}
|
|
27458
28219
|
if (!prompt && !effectiveBeadId) {
|
|
@@ -27613,7 +28374,7 @@ var init_node_resolve = __esm(() => {
|
|
|
27613
28374
|
});
|
|
27614
28375
|
|
|
27615
28376
|
// src/specialist/job-control.ts
|
|
27616
|
-
import { existsSync as existsSync15, readFileSync as
|
|
28377
|
+
import { existsSync as existsSync15, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
|
|
27617
28378
|
import { join as join17 } from "path";
|
|
27618
28379
|
|
|
27619
28380
|
class JobControl {
|
|
@@ -27645,8 +28406,8 @@ class JobControl {
|
|
|
27645
28406
|
}
|
|
27646
28407
|
};
|
|
27647
28408
|
let resolveJobId;
|
|
27648
|
-
const jobIdPromise = new Promise((
|
|
27649
|
-
resolveJobId =
|
|
28409
|
+
const jobIdPromise = new Promise((resolve7) => {
|
|
28410
|
+
resolveJobId = resolve7;
|
|
27650
28411
|
});
|
|
27651
28412
|
this.supervisor = new Supervisor({
|
|
27652
28413
|
runner: this.runner,
|
|
@@ -27692,7 +28453,7 @@ class JobControl {
|
|
|
27692
28453
|
if (!existsSync15(resultPath))
|
|
27693
28454
|
return null;
|
|
27694
28455
|
try {
|
|
27695
|
-
return
|
|
28456
|
+
return readFileSync12(resultPath, "utf-8");
|
|
27696
28457
|
} catch {
|
|
27697
28458
|
return null;
|
|
27698
28459
|
}
|
|
@@ -27711,7 +28472,7 @@ class JobControl {
|
|
|
27711
28472
|
if (deadline !== undefined && Date.now() >= deadline) {
|
|
27712
28473
|
throw new Error(`Timed out waiting for terminal status for job ${jobId}`);
|
|
27713
28474
|
}
|
|
27714
|
-
await new Promise((
|
|
28475
|
+
await new Promise((resolve7) => setTimeout(resolve7, backoffMs));
|
|
27715
28476
|
backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
|
|
27716
28477
|
}
|
|
27717
28478
|
}
|
|
@@ -27725,7 +28486,7 @@ class JobControl {
|
|
|
27725
28486
|
}
|
|
27726
28487
|
const jsonLine = `${JSON.stringify(payload)}
|
|
27727
28488
|
`;
|
|
27728
|
-
|
|
28489
|
+
writeFileSync7(status.fifo_path, jsonLine, { flag: "a" });
|
|
27729
28490
|
}
|
|
27730
28491
|
resultPath(jobId) {
|
|
27731
28492
|
return join17(this.jobsDir, jobId, "result.txt");
|
|
@@ -27890,7 +28651,7 @@ function hashOutput(output2, salt) {
|
|
|
27890
28651
|
return createHash3("sha256").update(value).digest("hex");
|
|
27891
28652
|
}
|
|
27892
28653
|
function sleep2(ms) {
|
|
27893
|
-
return new Promise((
|
|
28654
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
27894
28655
|
}
|
|
27895
28656
|
function toContextHealth(contextPct) {
|
|
27896
28657
|
if (contextPct === null)
|
|
@@ -29803,10 +30564,10 @@ var exports_node = {};
|
|
|
29803
30564
|
__export(exports_node, {
|
|
29804
30565
|
handleNodeCommand: () => handleNodeCommand
|
|
29805
30566
|
});
|
|
29806
|
-
import { existsSync as existsSync16, readFileSync as
|
|
30567
|
+
import { existsSync as existsSync16, readFileSync as readFileSync13, readdirSync as readdirSync7 } from "fs";
|
|
29807
30568
|
import { randomUUID } from "crypto";
|
|
29808
30569
|
import { spawnSync as spawnSync14 } from "child_process";
|
|
29809
|
-
import { basename as basename4, join as join18, resolve as
|
|
30570
|
+
import { basename as basename4, join as join18, resolve as resolve7 } from "path";
|
|
29810
30571
|
function parseNodeArgs(argv) {
|
|
29811
30572
|
const command = argv[0];
|
|
29812
30573
|
const supportedCommands = new Set(["run", "list", "promote", "members", "memory", "stop", "spawn-member", "create-bead", "complete", "wait-phase"]);
|
|
@@ -29990,7 +30751,7 @@ function toNodeName(filePath) {
|
|
|
29990
30751
|
function discoverNodeConfigs(cwd) {
|
|
29991
30752
|
const discoveredByName = new Map;
|
|
29992
30753
|
for (const directory of NODE_DISCOVERY_DIRS) {
|
|
29993
|
-
const absoluteDir =
|
|
30754
|
+
const absoluteDir = resolve7(cwd, directory.path);
|
|
29994
30755
|
if (!existsSync16(absoluteDir))
|
|
29995
30756
|
continue;
|
|
29996
30757
|
const files = readdirSync7(absoluteDir).filter((fileName) => fileName.endsWith(NODE_CONFIG_SUFFIX));
|
|
@@ -30005,7 +30766,7 @@ function discoverNodeConfigs(cwd) {
|
|
|
30005
30766
|
return [...discoveredByName.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
30006
30767
|
}
|
|
30007
30768
|
function resolveNodeConfigPath(cwd, input2) {
|
|
30008
|
-
const explicitPath =
|
|
30769
|
+
const explicitPath = resolve7(cwd, input2);
|
|
30009
30770
|
if (existsSync16(explicitPath)) {
|
|
30010
30771
|
return explicitPath;
|
|
30011
30772
|
}
|
|
@@ -30101,7 +30862,7 @@ async function handleNodeRun(args) {
|
|
|
30101
30862
|
throw new Error("Observability SQLite DB is unavailable. Run: specialists db setup");
|
|
30102
30863
|
}
|
|
30103
30864
|
try {
|
|
30104
|
-
const rawConfig = args.inlineJson ? args.inlineJson :
|
|
30865
|
+
const rawConfig = args.inlineJson ? args.inlineJson : readFileSync13(resolveNodeConfigPath(process.cwd(), args.nodeConfigInput), "utf-8");
|
|
30105
30866
|
const config2 = parseNodeConfig(rawConfig);
|
|
30106
30867
|
const loader = new SpecialistLoader;
|
|
30107
30868
|
const runner = new SpecialistRunner({
|
|
@@ -30550,7 +31311,7 @@ async function handleNodeAction(args) {
|
|
|
30550
31311
|
if (deadline !== null && Date.now() >= deadline) {
|
|
30551
31312
|
throw new Error(`Timed out waiting for phase '${args.phaseId}' members: ${memberKeys.join(", ")}`);
|
|
30552
31313
|
}
|
|
30553
|
-
await new Promise((
|
|
31314
|
+
await new Promise((resolve8) => setTimeout(resolve8, 500));
|
|
30554
31315
|
}
|
|
30555
31316
|
} finally {
|
|
30556
31317
|
sqliteClient.close();
|
|
@@ -30604,46 +31365,254 @@ async function handleNodeCommand(argv) {
|
|
|
30604
31365
|
await handleNodeMembers(parsed);
|
|
30605
31366
|
return;
|
|
30606
31367
|
}
|
|
30607
|
-
if (parsed.command === "memory") {
|
|
30608
|
-
await handleNodeMemory(parsed);
|
|
30609
|
-
return;
|
|
31368
|
+
if (parsed.command === "memory") {
|
|
31369
|
+
await handleNodeMemory(parsed);
|
|
31370
|
+
return;
|
|
31371
|
+
}
|
|
31372
|
+
if (parsed.command === "stop") {
|
|
31373
|
+
await handleNodeStop(parsed);
|
|
31374
|
+
return;
|
|
31375
|
+
}
|
|
31376
|
+
if (parsed.command === "spawn-member" || parsed.command === "create-bead" || parsed.command === "complete" || parsed.command === "wait-phase") {
|
|
31377
|
+
await handleNodeAction(parsed);
|
|
31378
|
+
return;
|
|
31379
|
+
}
|
|
31380
|
+
emitNodeCommandError(`Unsupported node command: ${parsed.command}`, parsed.jsonMode);
|
|
31381
|
+
}
|
|
31382
|
+
var NODE_CONFIG_SUFFIX = ".node.json", NODE_DISCOVERY_DIRS;
|
|
31383
|
+
var init_node = __esm(() => {
|
|
31384
|
+
init_loader();
|
|
31385
|
+
init_runner();
|
|
31386
|
+
init_circuitBreaker();
|
|
31387
|
+
init_hooks();
|
|
31388
|
+
init_observability_sqlite();
|
|
31389
|
+
init_beads();
|
|
31390
|
+
init_supervisor();
|
|
31391
|
+
init_job_root();
|
|
31392
|
+
init_node_resolve();
|
|
31393
|
+
init_node_supervisor();
|
|
31394
|
+
NODE_DISCOVERY_DIRS = [
|
|
31395
|
+
{ path: ".specialists/default/nodes", source: "default" },
|
|
31396
|
+
{ path: "config/nodes", source: "project" }
|
|
31397
|
+
];
|
|
31398
|
+
});
|
|
31399
|
+
|
|
31400
|
+
// src/specialist/epic-reconciler.ts
|
|
31401
|
+
import { mkdirSync as mkdirSync7, openSync as openSync2, readFileSync as readFileSync14, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "fs";
|
|
31402
|
+
import { join as join19 } from "path";
|
|
31403
|
+
function buildEpicLockPath(epicId) {
|
|
31404
|
+
const location = resolveObservabilityDbLocation();
|
|
31405
|
+
const lockDir = join19(location.dbDirectory, "locks");
|
|
31406
|
+
mkdirSync7(lockDir, { recursive: true });
|
|
31407
|
+
return join19(lockDir, `epic-${epicId}.lock`);
|
|
31408
|
+
}
|
|
31409
|
+
function withEpicAdvisoryLock(epicId, action) {
|
|
31410
|
+
const lockPath = buildEpicLockPath(epicId);
|
|
31411
|
+
let lockFd = null;
|
|
31412
|
+
try {
|
|
31413
|
+
lockFd = openSync2(lockPath, "wx");
|
|
31414
|
+
writeFileSync8(lockPath, JSON.stringify({ epic_id: epicId, pid: process.pid, created_at_ms: Date.now() }));
|
|
31415
|
+
} catch {
|
|
31416
|
+
let holder = "unknown";
|
|
31417
|
+
try {
|
|
31418
|
+
holder = readFileSync14(lockPath, "utf-8");
|
|
31419
|
+
} catch {
|
|
31420
|
+
holder = "unknown";
|
|
31421
|
+
}
|
|
31422
|
+
throw new Error(`Epic advisory lock busy for ${epicId}. Holder: ${holder}`);
|
|
31423
|
+
}
|
|
31424
|
+
try {
|
|
31425
|
+
return action();
|
|
31426
|
+
} finally {
|
|
31427
|
+
if (lockFd !== null) {
|
|
31428
|
+
try {
|
|
31429
|
+
rmSync2(lockPath, { force: true });
|
|
31430
|
+
} catch {}
|
|
31431
|
+
}
|
|
31432
|
+
}
|
|
31433
|
+
}
|
|
31434
|
+
function hasRedirectMarkers(statusJson) {
|
|
31435
|
+
if (!statusJson)
|
|
31436
|
+
return false;
|
|
31437
|
+
try {
|
|
31438
|
+
const parsed = JSON.parse(statusJson);
|
|
31439
|
+
return Object.keys(parsed).some((key) => key.toLowerCase().includes("redirect"));
|
|
31440
|
+
} catch {
|
|
31441
|
+
return false;
|
|
31442
|
+
}
|
|
31443
|
+
}
|
|
31444
|
+
function clearRedirectMarkers(statusJson) {
|
|
31445
|
+
if (!statusJson)
|
|
31446
|
+
return statusJson;
|
|
31447
|
+
try {
|
|
31448
|
+
const parsed = JSON.parse(statusJson);
|
|
31449
|
+
const cleanedEntries = Object.entries(parsed).filter(([key]) => !key.toLowerCase().includes("redirect"));
|
|
31450
|
+
return JSON.stringify(Object.fromEntries(cleanedEntries));
|
|
31451
|
+
} catch {
|
|
31452
|
+
return statusJson;
|
|
31453
|
+
}
|
|
31454
|
+
}
|
|
31455
|
+
function collectEpicJobs(sqlite, epicId) {
|
|
31456
|
+
const chainIds = new Set(sqlite.listEpicChains(epicId).map((chain) => chain.chain_id));
|
|
31457
|
+
return sqlite.listStatuses().filter((status) => status.epic_id === epicId || (status.chain_id ? chainIds.has(status.chain_id) : false));
|
|
31458
|
+
}
|
|
31459
|
+
function detectStaleChainRefs(sqlite, epicId) {
|
|
31460
|
+
return sqlite.listEpicChains(epicId).map((chain) => chain.chain_id).filter((chainId) => sqlite.listChainJobIds(chainId).length === 0);
|
|
31461
|
+
}
|
|
31462
|
+
function detectDeadBlockingJobs(jobs) {
|
|
31463
|
+
return jobs.filter((job) => ACTIVE_JOB_STATUSES2.has(job.status) && isJobDead(job)).map((job) => job.id);
|
|
31464
|
+
}
|
|
31465
|
+
function detectIntegrityFlags(sqlite, epicId, jobs) {
|
|
31466
|
+
const chainIds = new Set(sqlite.listEpicChains(epicId).map((chain) => chain.chain_id));
|
|
31467
|
+
const flags = [];
|
|
31468
|
+
for (const job of jobs) {
|
|
31469
|
+
if (job.chain_kind === "chain" && !job.chain_id) {
|
|
31470
|
+
flags.push(`job:${job.id}:chain_kind=chain missing chain_id`);
|
|
31471
|
+
}
|
|
31472
|
+
if (job.chain_id && !chainIds.has(job.chain_id) && job.epic_id === epicId) {
|
|
31473
|
+
flags.push(`job:${job.id}:references chain ${job.chain_id} missing from epic membership`);
|
|
31474
|
+
}
|
|
31475
|
+
if (job.chain_id && chainIds.has(job.chain_id) && job.epic_id && job.epic_id !== epicId) {
|
|
31476
|
+
flags.push(`job:${job.id}:chain ${job.chain_id} linked to epic ${job.epic_id}, expected ${epicId}`);
|
|
31477
|
+
}
|
|
31478
|
+
}
|
|
31479
|
+
return flags;
|
|
31480
|
+
}
|
|
31481
|
+
function markDeadJobsAsError(sqlite, jobs) {
|
|
31482
|
+
const deadBlockingIds = detectDeadBlockingJobs(jobs);
|
|
31483
|
+
const now = Date.now();
|
|
31484
|
+
for (const jobId of deadBlockingIds) {
|
|
31485
|
+
const current = jobs.find((job) => job.id === jobId);
|
|
31486
|
+
if (!current)
|
|
31487
|
+
continue;
|
|
31488
|
+
sqlite.upsertStatus({
|
|
31489
|
+
...current,
|
|
31490
|
+
status: "error",
|
|
31491
|
+
error: current.error ?? "epic reconciler: detected dead pid/tmux for active job",
|
|
31492
|
+
last_event_at_ms: now
|
|
31493
|
+
});
|
|
31494
|
+
}
|
|
31495
|
+
return deadBlockingIds;
|
|
31496
|
+
}
|
|
31497
|
+
function syncEpicState(sqlite, epicId, apply) {
|
|
31498
|
+
const epicRun = sqlite.readEpicRun(epicId);
|
|
31499
|
+
const jobs = collectEpicJobs(sqlite, epicId);
|
|
31500
|
+
const readinessBefore = loadEpicReadinessSummary(sqlite, epicId);
|
|
31501
|
+
const drift = {
|
|
31502
|
+
stale_chain_refs: detectStaleChainRefs(sqlite, epicId),
|
|
31503
|
+
dead_jobs_blocking_readiness: detectDeadBlockingJobs(jobs),
|
|
31504
|
+
integrity_flags: detectIntegrityFlags(sqlite, epicId, jobs),
|
|
31505
|
+
stale_redirect_markers: epicRun && hasRedirectMarkers(epicRun.status_json) ? [epicId] : []
|
|
31506
|
+
};
|
|
31507
|
+
let deadJobsMarkedError = [];
|
|
31508
|
+
let readinessResynced = false;
|
|
31509
|
+
let redirectMarkersCleared = false;
|
|
31510
|
+
if (apply) {
|
|
31511
|
+
if (drift.dead_jobs_blocking_readiness.length > 0) {
|
|
31512
|
+
deadJobsMarkedError = markDeadJobsAsError(sqlite, jobs);
|
|
31513
|
+
}
|
|
31514
|
+
const readinessNext = loadEpicReadinessSummary(sqlite, epicId);
|
|
31515
|
+
const synced = syncEpicStateFromReadiness(sqlite, readinessNext);
|
|
31516
|
+
readinessResynced = synced.status !== readinessNext.persisted_state;
|
|
31517
|
+
if (epicRun && drift.stale_redirect_markers.length > 0) {
|
|
31518
|
+
const cleaned = clearRedirectMarkers(epicRun.status_json);
|
|
31519
|
+
if (cleaned && cleaned !== epicRun.status_json) {
|
|
31520
|
+
sqlite.upsertEpicRun({
|
|
31521
|
+
...epicRun,
|
|
31522
|
+
status_json: cleaned,
|
|
31523
|
+
updated_at_ms: Date.now()
|
|
31524
|
+
});
|
|
31525
|
+
redirectMarkersCleared = true;
|
|
31526
|
+
}
|
|
31527
|
+
}
|
|
31528
|
+
}
|
|
31529
|
+
const readinessAfter = loadEpicReadinessSummary(sqlite, epicId);
|
|
31530
|
+
return {
|
|
31531
|
+
epic_id: epicId,
|
|
31532
|
+
apply,
|
|
31533
|
+
drift,
|
|
31534
|
+
repairs: {
|
|
31535
|
+
dead_jobs_marked_error: deadJobsMarkedError,
|
|
31536
|
+
readiness_resynced: readinessResynced,
|
|
31537
|
+
redirect_markers_cleared: redirectMarkersCleared
|
|
31538
|
+
},
|
|
31539
|
+
readiness_before: readinessBefore,
|
|
31540
|
+
readiness_after: readinessAfter
|
|
31541
|
+
};
|
|
31542
|
+
}
|
|
31543
|
+
function listLiveMemberJobIds(sqlite, epicId) {
|
|
31544
|
+
return collectEpicJobs(sqlite, epicId).filter((job) => ACTIVE_JOB_STATUSES2.has(job.status) && !isJobDead(job)).map((job) => job.id);
|
|
31545
|
+
}
|
|
31546
|
+
function buildAbandonedStatusJson(epic, epicId, fromState, reason, forced) {
|
|
31547
|
+
const base = appendEpicTransitionAudit(epic?.status_json, {
|
|
31548
|
+
from: fromState,
|
|
31549
|
+
to: "abandoned",
|
|
31550
|
+
at_ms: Date.now(),
|
|
31551
|
+
reason,
|
|
31552
|
+
trigger: "sp epic abandon",
|
|
31553
|
+
forced
|
|
31554
|
+
});
|
|
31555
|
+
try {
|
|
31556
|
+
const parsed = JSON.parse(base);
|
|
31557
|
+
return JSON.stringify({
|
|
31558
|
+
...parsed,
|
|
31559
|
+
epic_id: epicId,
|
|
31560
|
+
status: "abandoned",
|
|
31561
|
+
reason,
|
|
31562
|
+
forced
|
|
31563
|
+
});
|
|
31564
|
+
} catch {
|
|
31565
|
+
return base;
|
|
31566
|
+
}
|
|
31567
|
+
}
|
|
31568
|
+
function abandonEpic(sqlite, epicId, reason, force) {
|
|
31569
|
+
const epic = sqlite.readEpicRun(epicId);
|
|
31570
|
+
const fromState = epic?.status ?? "open";
|
|
31571
|
+
if (fromState === "merged") {
|
|
31572
|
+
throw new Error(`Epic ${epicId} already merged. Abandon blocked.`);
|
|
30610
31573
|
}
|
|
30611
|
-
if (
|
|
30612
|
-
|
|
30613
|
-
return;
|
|
31574
|
+
if (fromState === "failed" || fromState === "abandoned") {
|
|
31575
|
+
throw new Error(`Epic ${epicId} already terminal in state '${fromState}'.`);
|
|
30614
31576
|
}
|
|
30615
|
-
|
|
30616
|
-
|
|
30617
|
-
|
|
31577
|
+
const liveMemberJobIds = listLiveMemberJobIds(sqlite, epicId);
|
|
31578
|
+
if (!force && liveMemberJobIds.length > 0) {
|
|
31579
|
+
throw new Error(`Epic ${epicId} has live members: ${liveMemberJobIds.join(", ")}. Retry with --force to abandon anyway.`);
|
|
30618
31580
|
}
|
|
30619
|
-
|
|
31581
|
+
const statusJson = buildAbandonedStatusJson(epic, epicId, fromState, reason, force);
|
|
31582
|
+
sqlite.upsertEpicRun({
|
|
31583
|
+
epic_id: epicId,
|
|
31584
|
+
status: "abandoned",
|
|
31585
|
+
status_json: statusJson,
|
|
31586
|
+
updated_at_ms: Date.now()
|
|
31587
|
+
});
|
|
31588
|
+
return {
|
|
31589
|
+
epic_id: epicId,
|
|
31590
|
+
from_state: fromState,
|
|
31591
|
+
to_state: "abandoned",
|
|
31592
|
+
forced: force,
|
|
31593
|
+
reason,
|
|
31594
|
+
live_member_job_ids: liveMemberJobIds
|
|
31595
|
+
};
|
|
30620
31596
|
}
|
|
30621
|
-
var
|
|
30622
|
-
var
|
|
30623
|
-
|
|
30624
|
-
|
|
30625
|
-
|
|
30626
|
-
init_hooks();
|
|
30627
|
-
init_observability_sqlite();
|
|
30628
|
-
init_beads();
|
|
31597
|
+
var ACTIVE_JOB_STATUSES2;
|
|
31598
|
+
var init_epic_reconciler = __esm(() => {
|
|
31599
|
+
init_epic_lifecycle();
|
|
31600
|
+
init_epic_readiness();
|
|
31601
|
+
init_observability_db();
|
|
30629
31602
|
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
|
-
];
|
|
31603
|
+
ACTIVE_JOB_STATUSES2 = new Set(["starting", "running", "waiting"]);
|
|
30637
31604
|
});
|
|
30638
31605
|
|
|
30639
31606
|
// src/cli/epic.ts
|
|
30640
31607
|
var exports_epic = {};
|
|
30641
31608
|
__export(exports_epic, {
|
|
31609
|
+
handleEpicSyncCommand: () => handleEpicSyncCommand,
|
|
30642
31610
|
handleEpicStatusCommand: () => handleEpicStatusCommand,
|
|
30643
31611
|
handleEpicResolveCommand: () => handleEpicResolveCommand,
|
|
30644
31612
|
handleEpicMergeCommand: () => handleEpicMergeCommand,
|
|
30645
31613
|
handleEpicListCommand: () => handleEpicListCommand,
|
|
30646
|
-
handleEpicCommand: () => handleEpicCommand
|
|
31614
|
+
handleEpicCommand: () => handleEpicCommand,
|
|
31615
|
+
handleEpicAbandonCommand: () => handleEpicAbandonCommand
|
|
30647
31616
|
});
|
|
30648
31617
|
import { spawnSync as spawnSync15 } from "child_process";
|
|
30649
31618
|
function runCommand2(command, args, cwd = process.cwd()) {
|
|
@@ -30743,6 +31712,58 @@ function parseResolveOptions(argv) {
|
|
|
30743
31712
|
}
|
|
30744
31713
|
return { epicId, dryRun, json };
|
|
30745
31714
|
}
|
|
31715
|
+
function parseSyncOptions(argv) {
|
|
31716
|
+
const epicId = parseEpicId(argv);
|
|
31717
|
+
let apply = false;
|
|
31718
|
+
let json = false;
|
|
31719
|
+
for (const argument of argv) {
|
|
31720
|
+
if (argument === "--apply") {
|
|
31721
|
+
apply = true;
|
|
31722
|
+
continue;
|
|
31723
|
+
}
|
|
31724
|
+
if (argument === "--json") {
|
|
31725
|
+
json = true;
|
|
31726
|
+
continue;
|
|
31727
|
+
}
|
|
31728
|
+
if (argument.startsWith("-") && argument !== "--apply" && argument !== "--json") {
|
|
31729
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
31730
|
+
}
|
|
31731
|
+
}
|
|
31732
|
+
return { epicId, apply, json };
|
|
31733
|
+
}
|
|
31734
|
+
function parseAbandonOptions(argv) {
|
|
31735
|
+
const epicId = parseEpicId(argv);
|
|
31736
|
+
let reason = "";
|
|
31737
|
+
let force = false;
|
|
31738
|
+
let json = false;
|
|
31739
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
31740
|
+
const argument = argv[index];
|
|
31741
|
+
if (argument === "--force") {
|
|
31742
|
+
force = true;
|
|
31743
|
+
continue;
|
|
31744
|
+
}
|
|
31745
|
+
if (argument === "--json") {
|
|
31746
|
+
json = true;
|
|
31747
|
+
continue;
|
|
31748
|
+
}
|
|
31749
|
+
if (argument === "--reason") {
|
|
31750
|
+
const value = argv[index + 1];
|
|
31751
|
+
if (!value || value.startsWith("-")) {
|
|
31752
|
+
throw new Error("Missing value for --reason");
|
|
31753
|
+
}
|
|
31754
|
+
reason = value.trim();
|
|
31755
|
+
index += 1;
|
|
31756
|
+
continue;
|
|
31757
|
+
}
|
|
31758
|
+
if (argument.startsWith("-") && argument !== "--force" && argument !== "--json") {
|
|
31759
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
31760
|
+
}
|
|
31761
|
+
}
|
|
31762
|
+
if (reason.length === 0) {
|
|
31763
|
+
throw new Error("Missing required --reason <text>");
|
|
31764
|
+
}
|
|
31765
|
+
return { epicId, reason, force, json };
|
|
31766
|
+
}
|
|
30746
31767
|
function readEpicChildrenFromBeads(epicId) {
|
|
30747
31768
|
const result = runCommand2("bd", ["children", epicId]);
|
|
30748
31769
|
if (result.status !== 0) {
|
|
@@ -31106,6 +32127,93 @@ async function handleEpicMergeCommand(argv) {
|
|
|
31106
32127
|
process.exit(1);
|
|
31107
32128
|
}
|
|
31108
32129
|
}
|
|
32130
|
+
async function handleEpicSyncCommand(argv) {
|
|
32131
|
+
let options;
|
|
32132
|
+
try {
|
|
32133
|
+
options = parseSyncOptions(argv);
|
|
32134
|
+
} catch (error2) {
|
|
32135
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
32136
|
+
console.error(message);
|
|
32137
|
+
console.error("Usage: specialists epic sync <epic-id> [--apply] [--json]");
|
|
32138
|
+
process.exit(1);
|
|
32139
|
+
}
|
|
32140
|
+
const sqlite = createObservabilitySqliteClient();
|
|
32141
|
+
if (!sqlite) {
|
|
32142
|
+
const message = "Observability SQLite database not available. Run `sp db setup` first.";
|
|
32143
|
+
if (options.json) {
|
|
32144
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
32145
|
+
} else {
|
|
32146
|
+
console.error(message);
|
|
32147
|
+
}
|
|
32148
|
+
process.exit(1);
|
|
32149
|
+
}
|
|
32150
|
+
try {
|
|
32151
|
+
const result = withEpicAdvisoryLock(options.epicId, () => syncEpicState(sqlite, options.epicId, options.apply));
|
|
32152
|
+
if (options.json) {
|
|
32153
|
+
console.log(JSON.stringify(result, null, 2));
|
|
32154
|
+
return;
|
|
32155
|
+
}
|
|
32156
|
+
console.log("");
|
|
32157
|
+
console.log(`Epic ${result.epic_id} sync (${result.apply ? "apply" : "dry-run"})`);
|
|
32158
|
+
console.log(` stale_chain_refs: ${result.drift.stale_chain_refs.length}`);
|
|
32159
|
+
console.log(` dead_jobs_blocking_readiness: ${result.drift.dead_jobs_blocking_readiness.length}`);
|
|
32160
|
+
console.log(` integrity_flags: ${result.drift.integrity_flags.length}`);
|
|
32161
|
+
console.log(` stale_redirect_markers: ${result.drift.stale_redirect_markers.length}`);
|
|
32162
|
+
if (result.apply) {
|
|
32163
|
+
console.log(` repaired_dead_jobs: ${result.repairs.dead_jobs_marked_error.length}`);
|
|
32164
|
+
console.log(` readiness_resynced: ${result.repairs.readiness_resynced}`);
|
|
32165
|
+
console.log(` redirect_markers_cleared: ${result.repairs.redirect_markers_cleared}`);
|
|
32166
|
+
}
|
|
32167
|
+
console.log(` readiness_before: ${result.readiness_before.readiness_state}`);
|
|
32168
|
+
console.log(` readiness_after: ${result.readiness_after.readiness_state}`);
|
|
32169
|
+
console.log("");
|
|
32170
|
+
} finally {
|
|
32171
|
+
sqlite.close();
|
|
32172
|
+
}
|
|
32173
|
+
}
|
|
32174
|
+
async function handleEpicAbandonCommand(argv) {
|
|
32175
|
+
let options;
|
|
32176
|
+
try {
|
|
32177
|
+
options = parseAbandonOptions(argv);
|
|
32178
|
+
} catch (error2) {
|
|
32179
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
32180
|
+
console.error(message);
|
|
32181
|
+
console.error("Usage: specialists epic abandon <epic-id> --reason <text> [--force] [--json]");
|
|
32182
|
+
process.exit(1);
|
|
32183
|
+
}
|
|
32184
|
+
const sqlite = createObservabilitySqliteClient();
|
|
32185
|
+
if (!sqlite) {
|
|
32186
|
+
const message = "Observability SQLite database not available. Run `sp db setup` first.";
|
|
32187
|
+
if (options.json) {
|
|
32188
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
32189
|
+
} else {
|
|
32190
|
+
console.error(message);
|
|
32191
|
+
}
|
|
32192
|
+
process.exit(1);
|
|
32193
|
+
}
|
|
32194
|
+
try {
|
|
32195
|
+
const result = withEpicAdvisoryLock(options.epicId, () => abandonEpic(sqlite, options.epicId, options.reason, options.force));
|
|
32196
|
+
if (options.json) {
|
|
32197
|
+
console.log(JSON.stringify(result, null, 2));
|
|
32198
|
+
return;
|
|
32199
|
+
}
|
|
32200
|
+
console.log(`Epic ${result.epic_id}: ${result.from_state} -> ${result.to_state}`);
|
|
32201
|
+
console.log(`Reason: ${result.reason}`);
|
|
32202
|
+
if (result.forced) {
|
|
32203
|
+
console.log("Mode: forced");
|
|
32204
|
+
}
|
|
32205
|
+
} catch (error2) {
|
|
32206
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
32207
|
+
if (options.json) {
|
|
32208
|
+
console.log(JSON.stringify({ epic_id: options.epicId, error: message }, null, 2));
|
|
32209
|
+
} else {
|
|
32210
|
+
console.error(message);
|
|
32211
|
+
}
|
|
32212
|
+
process.exit(1);
|
|
32213
|
+
} finally {
|
|
32214
|
+
sqlite.close();
|
|
32215
|
+
}
|
|
32216
|
+
}
|
|
31109
32217
|
async function handleEpicStatusCommand(argv) {
|
|
31110
32218
|
let options;
|
|
31111
32219
|
try {
|
|
@@ -31190,12 +32298,14 @@ async function handleEpicCommand(argv) {
|
|
|
31190
32298
|
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
31191
32299
|
console.log([
|
|
31192
32300
|
"",
|
|
31193
|
-
"Usage: specialists epic <list|status|resolve|merge> [options]",
|
|
32301
|
+
"Usage: specialists epic <list|status|resolve|sync|abandon|merge> [options]",
|
|
31194
32302
|
"",
|
|
31195
32303
|
"Commands:",
|
|
31196
32304
|
" list [--unresolved] [--json] List epics with lifecycle and readiness summary",
|
|
31197
32305
|
" status <epic-id> [--json] Show epic state, chain statuses, and merge readiness",
|
|
31198
32306
|
" resolve <epic-id> [--dry-run] [--json] Transition epic from open to resolving",
|
|
32307
|
+
" sync <epic-id> [--apply] [--json] Reconcile epic drift (dry-run by default)",
|
|
32308
|
+
" abandon <epic-id> --reason <text> [--force] [--json] Transition epic to abandoned",
|
|
31199
32309
|
" merge <epic-id> [--rebuild] [--pr] [--json] Publish epic-owned chains in dependency order",
|
|
31200
32310
|
"",
|
|
31201
32311
|
"Epic lifecycle states:",
|
|
@@ -31215,6 +32325,9 @@ async function handleEpicCommand(argv) {
|
|
|
31215
32325
|
" specialists epic list --unresolved --json",
|
|
31216
32326
|
" specialists epic resolve unitAI-3f7b",
|
|
31217
32327
|
" specialists epic status unitAI-3f7b --json",
|
|
32328
|
+
" specialists epic sync unitAI-3f7b",
|
|
32329
|
+
" specialists epic sync unitAI-3f7b --apply",
|
|
32330
|
+
' specialists epic abandon unitAI-3f7b --reason "scope changed"',
|
|
31218
32331
|
" specialists epic merge unitAI-3f7b --rebuild",
|
|
31219
32332
|
" specialists epic merge unitAI-3f7b --pr",
|
|
31220
32333
|
""
|
|
@@ -31230,6 +32343,14 @@ async function handleEpicCommand(argv) {
|
|
|
31230
32343
|
await handleEpicResolveCommand(argv.slice(1));
|
|
31231
32344
|
return;
|
|
31232
32345
|
}
|
|
32346
|
+
if (subcommand === "sync") {
|
|
32347
|
+
await handleEpicSyncCommand(argv.slice(1));
|
|
32348
|
+
return;
|
|
32349
|
+
}
|
|
32350
|
+
if (subcommand === "abandon") {
|
|
32351
|
+
await handleEpicAbandonCommand(argv.slice(1));
|
|
32352
|
+
return;
|
|
32353
|
+
}
|
|
31233
32354
|
if (subcommand === "merge") {
|
|
31234
32355
|
await handleEpicMergeCommand(argv.slice(1));
|
|
31235
32356
|
return;
|
|
@@ -31239,12 +32360,13 @@ async function handleEpicCommand(argv) {
|
|
|
31239
32360
|
return;
|
|
31240
32361
|
}
|
|
31241
32362
|
console.error(`Unknown epic subcommand: ${subcommand}`);
|
|
31242
|
-
console.error("Usage: specialists epic <list|status|resolve|merge>");
|
|
32363
|
+
console.error("Usage: specialists epic <list|status|resolve|sync|abandon|merge>");
|
|
31243
32364
|
process.exit(1);
|
|
31244
32365
|
}
|
|
31245
32366
|
var RUNNING_STATUSES;
|
|
31246
32367
|
var init_epic = __esm(() => {
|
|
31247
32368
|
init_epic_lifecycle();
|
|
32369
|
+
init_epic_reconciler();
|
|
31248
32370
|
init_observability_sqlite();
|
|
31249
32371
|
init_merge();
|
|
31250
32372
|
RUNNING_STATUSES = new Set(["starting", "running", "waiting", "degraded"]);
|
|
@@ -31256,8 +32378,8 @@ __export(exports_status, {
|
|
|
31256
32378
|
run: () => run14
|
|
31257
32379
|
});
|
|
31258
32380
|
import { spawnSync as spawnSync16 } from "child_process";
|
|
31259
|
-
import { existsSync as existsSync17, readFileSync as
|
|
31260
|
-
import { join as
|
|
32381
|
+
import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
|
|
32382
|
+
import { join as join20 } from "path";
|
|
31261
32383
|
function ok2(msg) {
|
|
31262
32384
|
console.log(` ${green8("\u2713")} ${msg}`);
|
|
31263
32385
|
}
|
|
@@ -31348,10 +32470,10 @@ function countJobEvents(sqliteClient, jobsDir, jobId) {
|
|
|
31348
32470
|
} catch (error2) {
|
|
31349
32471
|
console.warn(`SQLite events read failed for job ${jobId}; falling back to events.jsonl`, error2);
|
|
31350
32472
|
}
|
|
31351
|
-
const eventsFile =
|
|
32473
|
+
const eventsFile = join20(jobsDir, jobId, "events.jsonl");
|
|
31352
32474
|
if (!existsSync17(eventsFile))
|
|
31353
32475
|
return 0;
|
|
31354
|
-
const raw =
|
|
32476
|
+
const raw = readFileSync15(eventsFile, "utf-8").trim();
|
|
31355
32477
|
if (!raw)
|
|
31356
32478
|
return 0;
|
|
31357
32479
|
return raw.split(`
|
|
@@ -31382,10 +32504,10 @@ function getLatestContextSnapshot(sqliteClient, jobsDir, jobId) {
|
|
|
31382
32504
|
} catch (error2) {
|
|
31383
32505
|
console.warn(`SQLite events read failed for job ${jobId}; falling back to events.jsonl`, error2);
|
|
31384
32506
|
}
|
|
31385
|
-
const eventsFile =
|
|
32507
|
+
const eventsFile = join20(jobsDir, jobId, "events.jsonl");
|
|
31386
32508
|
if (!existsSync17(eventsFile))
|
|
31387
32509
|
return null;
|
|
31388
|
-
const lines =
|
|
32510
|
+
const lines = readFileSync15(eventsFile, "utf-8").split(`
|
|
31389
32511
|
`);
|
|
31390
32512
|
for (let index = lines.length - 1;index >= 0; index -= 1) {
|
|
31391
32513
|
const line = lines[index].trim();
|
|
@@ -31490,7 +32612,7 @@ async function run14() {
|
|
|
31490
32612
|
`).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean)) : new Set;
|
|
31491
32613
|
const bdInstalled = isInstalled2("bd");
|
|
31492
32614
|
const bdVersion = bdInstalled ? cmd("bd", ["--version"]) : null;
|
|
31493
|
-
const beadsPresent = existsSync17(
|
|
32615
|
+
const beadsPresent = existsSync17(join20(process.cwd(), ".beads"));
|
|
31494
32616
|
const specialistsBin = cmd("which", ["specialists"]);
|
|
31495
32617
|
const jobsDir = resolveJobsDir();
|
|
31496
32618
|
let jobs = [];
|
|
@@ -31651,8 +32773,8 @@ __export(exports_ps, {
|
|
|
31651
32773
|
run: () => run15
|
|
31652
32774
|
});
|
|
31653
32775
|
import { spawnSync as spawnSync17 } from "child_process";
|
|
31654
|
-
import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as
|
|
31655
|
-
import { join as
|
|
32776
|
+
import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync16 } from "fs";
|
|
32777
|
+
import { join as join21 } from "path";
|
|
31656
32778
|
function parseArgs7(argv) {
|
|
31657
32779
|
let nodeId;
|
|
31658
32780
|
const positional = [];
|
|
@@ -31686,21 +32808,21 @@ function readStatusesFromFiles(jobsDir) {
|
|
|
31686
32808
|
return [];
|
|
31687
32809
|
const statuses = [];
|
|
31688
32810
|
for (const entry of readdirSync8(jobsDir)) {
|
|
31689
|
-
const statusPath =
|
|
32811
|
+
const statusPath = join21(jobsDir, entry, "status.json");
|
|
31690
32812
|
if (!existsSync18(statusPath))
|
|
31691
32813
|
continue;
|
|
31692
32814
|
try {
|
|
31693
|
-
statuses.push(JSON.parse(
|
|
32815
|
+
statuses.push(JSON.parse(readFileSync16(statusPath, "utf-8")));
|
|
31694
32816
|
} catch {}
|
|
31695
32817
|
}
|
|
31696
32818
|
return statuses.sort((a, b) => b.started_at_ms - a.started_at_ms);
|
|
31697
32819
|
}
|
|
31698
32820
|
function readLastToolEventFromFile(jobsDir, jobId) {
|
|
31699
|
-
const eventsPath =
|
|
32821
|
+
const eventsPath = join21(jobsDir, jobId, "events.jsonl");
|
|
31700
32822
|
if (!existsSync18(eventsPath))
|
|
31701
32823
|
return;
|
|
31702
32824
|
try {
|
|
31703
|
-
const lines =
|
|
32825
|
+
const lines = readFileSync16(eventsPath, "utf-8").split(`
|
|
31704
32826
|
`);
|
|
31705
32827
|
for (let index = lines.length - 1;index >= 0; index -= 1) {
|
|
31706
32828
|
const line = lines[index]?.trim();
|
|
@@ -32458,8 +33580,8 @@ var exports_result = {};
|
|
|
32458
33580
|
__export(exports_result, {
|
|
32459
33581
|
run: () => run16
|
|
32460
33582
|
});
|
|
32461
|
-
import { existsSync as existsSync19, readFileSync as
|
|
32462
|
-
import { join as
|
|
33583
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
|
|
33584
|
+
import { join as join22 } from "path";
|
|
32463
33585
|
function parseArgs8(argv) {
|
|
32464
33586
|
let jobId;
|
|
32465
33587
|
let nodeId;
|
|
@@ -32543,9 +33665,92 @@ function resolveJobIdFromNodeMember(sqliteClient, nodeId, memberKey) {
|
|
|
32543
33665
|
}
|
|
32544
33666
|
return member.job_id;
|
|
32545
33667
|
}
|
|
33668
|
+
function readTimelineEventsForResult(sqliteClient, jobsDir, jobId) {
|
|
33669
|
+
if (sqliteClient) {
|
|
33670
|
+
try {
|
|
33671
|
+
return sqliteClient.readEvents(jobId);
|
|
33672
|
+
} catch {}
|
|
33673
|
+
}
|
|
33674
|
+
const eventsPath = join22(jobsDir, jobId, "events.jsonl");
|
|
33675
|
+
if (!existsSync19(eventsPath))
|
|
33676
|
+
return [];
|
|
33677
|
+
return readFileSync17(eventsPath, "utf-8").split(`
|
|
33678
|
+
`).map((line) => line.trim()).filter(Boolean).map((line) => parseTimelineEvent(line)).filter((event) => event !== null);
|
|
33679
|
+
}
|
|
33680
|
+
function deriveStartupSnapshot(status, events) {
|
|
33681
|
+
const runStartEvent = events.find((event) => event.type === "run_start");
|
|
33682
|
+
const startupFromEvent = runStartEvent?.type === "run_start" ? runStartEvent.startup_snapshot ?? null : null;
|
|
33683
|
+
const memoryMeta = events.find((event) => event.type === "meta" && !!event.memory_injection);
|
|
33684
|
+
const memoryInjection = memoryMeta?.type === "meta" ? memoryMeta.memory_injection : undefined;
|
|
33685
|
+
const merged = {
|
|
33686
|
+
...startupFromEvent ?? {},
|
|
33687
|
+
...status.startup_context ?? {},
|
|
33688
|
+
...memoryInjection ? { memory_injection: memoryInjection } : {}
|
|
33689
|
+
};
|
|
33690
|
+
if (!merged.job_id)
|
|
33691
|
+
merged.job_id = status.id;
|
|
33692
|
+
if (!merged.specialist_name)
|
|
33693
|
+
merged.specialist_name = status.specialist;
|
|
33694
|
+
if (!merged.bead_id && status.bead_id)
|
|
33695
|
+
merged.bead_id = status.bead_id;
|
|
33696
|
+
if (!merged.reused_from_job_id && status.reused_from_job_id)
|
|
33697
|
+
merged.reused_from_job_id = status.reused_from_job_id;
|
|
33698
|
+
if (!merged.worktree_owner_job_id && status.worktree_owner_job_id)
|
|
33699
|
+
merged.worktree_owner_job_id = status.worktree_owner_job_id;
|
|
33700
|
+
if (!merged.chain_id && status.chain_id)
|
|
33701
|
+
merged.chain_id = status.chain_id;
|
|
33702
|
+
if (!merged.chain_root_job_id && status.chain_root_job_id)
|
|
33703
|
+
merged.chain_root_job_id = status.chain_root_job_id;
|
|
33704
|
+
if (!merged.chain_root_bead_id && status.chain_root_bead_id)
|
|
33705
|
+
merged.chain_root_bead_id = status.chain_root_bead_id;
|
|
33706
|
+
if (!merged.worktree_path && status.worktree_path)
|
|
33707
|
+
merged.worktree_path = status.worktree_path;
|
|
33708
|
+
if (!merged.branch && status.branch)
|
|
33709
|
+
merged.branch = status.branch;
|
|
33710
|
+
return Object.keys(merged).length > 0 ? merged : null;
|
|
33711
|
+
}
|
|
33712
|
+
function formatStartupSnapshot(snapshot) {
|
|
33713
|
+
if (!snapshot)
|
|
33714
|
+
return null;
|
|
33715
|
+
const lines = [`
|
|
33716
|
+
--- startup context ---`];
|
|
33717
|
+
const push = (key, value) => {
|
|
33718
|
+
if (value === undefined || value === null)
|
|
33719
|
+
return;
|
|
33720
|
+
lines.push(`${key}: ${Array.isArray(value) ? value.join(", ") : String(value)}`);
|
|
33721
|
+
};
|
|
33722
|
+
push("job_id", snapshot.job_id);
|
|
33723
|
+
push("specialist_name", snapshot.specialist_name);
|
|
33724
|
+
push("bead_id", snapshot.bead_id);
|
|
33725
|
+
push("reused_from_job_id", snapshot.reused_from_job_id);
|
|
33726
|
+
push("worktree_owner_job_id", snapshot.worktree_owner_job_id);
|
|
33727
|
+
push("chain_id", snapshot.chain_id);
|
|
33728
|
+
push("chain_root_job_id", snapshot.chain_root_job_id);
|
|
33729
|
+
push("chain_root_bead_id", snapshot.chain_root_bead_id);
|
|
33730
|
+
push("worktree_path", snapshot.worktree_path);
|
|
33731
|
+
push("branch", snapshot.branch);
|
|
33732
|
+
push("variables_keys", snapshot.variables_keys);
|
|
33733
|
+
push("reviewed_job_id_present", snapshot.reviewed_job_id_present);
|
|
33734
|
+
push("reused_worktree_awareness_present", snapshot.reused_worktree_awareness_present);
|
|
33735
|
+
push("bead_context_present", snapshot.bead_context_present);
|
|
33736
|
+
if (snapshot.memory_injection) {
|
|
33737
|
+
push("memory.static_tokens", snapshot.memory_injection.static_tokens);
|
|
33738
|
+
push("memory.memory_tokens", snapshot.memory_injection.memory_tokens);
|
|
33739
|
+
push("memory.gitnexus_tokens", snapshot.memory_injection.gitnexus_tokens);
|
|
33740
|
+
push("memory.total_tokens", snapshot.memory_injection.total_tokens);
|
|
33741
|
+
}
|
|
33742
|
+
if (snapshot.skills) {
|
|
33743
|
+
push("skills.count", snapshot.skills.count);
|
|
33744
|
+
push("skills.activated", snapshot.skills.activated);
|
|
33745
|
+
}
|
|
33746
|
+
lines.push("---");
|
|
33747
|
+
return `${lines.join(`
|
|
33748
|
+
`)}
|
|
33749
|
+
`;
|
|
33750
|
+
}
|
|
32546
33751
|
async function run16() {
|
|
32547
33752
|
const args = parseArgs8(process.argv.slice(3));
|
|
32548
|
-
const emitJson = (status, output2, error2) => {
|
|
33753
|
+
const emitJson = (status, output2, error2, startupContext = null) => {
|
|
32549
33754
|
console.log(JSON.stringify({
|
|
32550
33755
|
job: status ? {
|
|
32551
33756
|
id: status.id,
|
|
@@ -32555,17 +33760,20 @@ async function run16() {
|
|
|
32555
33760
|
backend: status.backend ?? null,
|
|
32556
33761
|
bead_id: status.bead_id ?? null,
|
|
32557
33762
|
metrics: status.metrics ?? null,
|
|
33763
|
+
startup_context: startupContext,
|
|
32558
33764
|
error: status.error ?? null
|
|
32559
33765
|
} : null,
|
|
32560
33766
|
output: output2,
|
|
33767
|
+
startup_context: startupContext,
|
|
32561
33768
|
error: error2
|
|
32562
33769
|
}, null, 2));
|
|
32563
33770
|
};
|
|
32564
|
-
const jobsDir =
|
|
33771
|
+
const jobsDir = join22(process.cwd(), ".specialists", "jobs");
|
|
32565
33772
|
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
32566
33773
|
const sqliteClient = createObservabilitySqliteClient();
|
|
32567
|
-
const emitHumanResult = (output2, status, trailingFooter) => {
|
|
32568
|
-
|
|
33774
|
+
const emitHumanResult = (output2, status, startupContext, trailingFooter) => {
|
|
33775
|
+
const startupBlock = formatStartupSnapshot(startupContext);
|
|
33776
|
+
process.stdout.write(startupBlock ? `${startupBlock}${output2}` : output2);
|
|
32569
33777
|
const tokenSummaryParts = formatTokenUsageSummary(status.metrics?.token_usage).filter((part) => !part.startsWith("cost="));
|
|
32570
33778
|
const formattedCost = formatCostUsd(status.metrics?.token_usage?.cost_usd);
|
|
32571
33779
|
if (tokenSummaryParts.length === 0 && !formattedCost) {
|
|
@@ -32594,7 +33802,7 @@ async function run16() {
|
|
|
32594
33802
|
const resolvedNodeId = args.nodeId ? resolveNodeRefWithClient(args.nodeId, sqliteClient) : resolveSingleActiveNodeRef(sqliteClient);
|
|
32595
33803
|
return resolveJobIdFromNodeMember(sqliteClient, resolvedNodeId, args.memberKey);
|
|
32596
33804
|
})();
|
|
32597
|
-
const resultPath =
|
|
33805
|
+
const resultPath = join22(jobsDir, jobId, "result.txt");
|
|
32598
33806
|
const readResultOutput = () => {
|
|
32599
33807
|
try {
|
|
32600
33808
|
const sqliteResult = sqliteClient?.readResult(jobId) ?? null;
|
|
@@ -32606,7 +33814,7 @@ async function run16() {
|
|
|
32606
33814
|
if (!existsSync19(resultPath)) {
|
|
32607
33815
|
return null;
|
|
32608
33816
|
}
|
|
32609
|
-
return
|
|
33817
|
+
return readFileSync17(resultPath, "utf-8");
|
|
32610
33818
|
};
|
|
32611
33819
|
if (args.wait) {
|
|
32612
33820
|
const startMs = Date.now();
|
|
@@ -32621,6 +33829,7 @@ async function run16() {
|
|
|
32621
33829
|
process.exit(1);
|
|
32622
33830
|
}
|
|
32623
33831
|
if (status2.status === "done") {
|
|
33832
|
+
const startupContext2 = deriveStartupSnapshot(status2, readTimelineEventsForResult(sqliteClient, jobsDir, jobId));
|
|
32624
33833
|
const output3 = readResultOutput();
|
|
32625
33834
|
if (!output3) {
|
|
32626
33835
|
if (args.json) {
|
|
@@ -32631,16 +33840,17 @@ async function run16() {
|
|
|
32631
33840
|
process.exit(1);
|
|
32632
33841
|
}
|
|
32633
33842
|
if (args.json) {
|
|
32634
|
-
emitJson(status2, output3, null);
|
|
33843
|
+
emitJson(status2, output3, null, startupContext2);
|
|
32635
33844
|
} else {
|
|
32636
|
-
emitHumanResult(output3, status2);
|
|
33845
|
+
emitHumanResult(output3, status2, startupContext2);
|
|
32637
33846
|
}
|
|
32638
33847
|
return;
|
|
32639
33848
|
}
|
|
32640
33849
|
if (status2.status === "error") {
|
|
33850
|
+
const startupContext2 = deriveStartupSnapshot(status2, readTimelineEventsForResult(sqliteClient, jobsDir, jobId));
|
|
32641
33851
|
const message = `Job ${jobId} failed: ${status2.error ?? "unknown error"}`;
|
|
32642
33852
|
if (args.json) {
|
|
32643
|
-
emitJson(status2, null, message);
|
|
33853
|
+
emitJson(status2, null, message, startupContext2);
|
|
32644
33854
|
} else {
|
|
32645
33855
|
process.stderr.write(`${red3(`Job ${jobId} failed:`)} ${status2.error ?? "unknown error"}
|
|
32646
33856
|
`);
|
|
@@ -32652,7 +33862,8 @@ async function run16() {
|
|
|
32652
33862
|
if (elapsedSecs >= args.timeout) {
|
|
32653
33863
|
const timeoutMessage = `Timeout: job ${jobId} did not complete within ${args.timeout}s`;
|
|
32654
33864
|
if (args.json) {
|
|
32655
|
-
|
|
33865
|
+
const startupContext2 = deriveStartupSnapshot(status2, readTimelineEventsForResult(sqliteClient, jobsDir, jobId));
|
|
33866
|
+
emitJson(status2, null, timeoutMessage, startupContext2);
|
|
32656
33867
|
} else {
|
|
32657
33868
|
process.stderr.write(`${timeoutMessage}
|
|
32658
33869
|
`);
|
|
@@ -32673,11 +33884,12 @@ async function run16() {
|
|
|
32673
33884
|
process.exit(1);
|
|
32674
33885
|
}
|
|
32675
33886
|
if (status.status === "running" || status.status === "starting") {
|
|
33887
|
+
const startupContext2 = deriveStartupSnapshot(status, readTimelineEventsForResult(sqliteClient, jobsDir, jobId));
|
|
32676
33888
|
const output3 = readResultOutput();
|
|
32677
33889
|
if (!output3) {
|
|
32678
33890
|
const message = `Job ${jobId} is still ${status.status}. Use 'specialists feed --job ${jobId}' to follow.`;
|
|
32679
33891
|
if (args.json) {
|
|
32680
|
-
emitJson(status, null, message);
|
|
33892
|
+
emitJson(status, null, message, startupContext2);
|
|
32681
33893
|
} else {
|
|
32682
33894
|
process.stderr.write(`${dim10(message)}
|
|
32683
33895
|
`);
|
|
@@ -32685,20 +33897,21 @@ async function run16() {
|
|
|
32685
33897
|
process.exit(1);
|
|
32686
33898
|
}
|
|
32687
33899
|
if (args.json) {
|
|
32688
|
-
emitJson(status, output3, null);
|
|
33900
|
+
emitJson(status, output3, null, startupContext2);
|
|
32689
33901
|
} else {
|
|
32690
33902
|
process.stderr.write(`${dim10(`Job ${jobId} is currently ${status.status}. Showing last completed output while it continues.`)}
|
|
32691
33903
|
`);
|
|
32692
|
-
emitHumanResult(output3, status);
|
|
33904
|
+
emitHumanResult(output3, status, startupContext2);
|
|
32693
33905
|
}
|
|
32694
33906
|
return;
|
|
32695
33907
|
}
|
|
32696
33908
|
if (status.status === "waiting") {
|
|
33909
|
+
const startupContext2 = deriveStartupSnapshot(status, readTimelineEventsForResult(sqliteClient, jobsDir, jobId));
|
|
32697
33910
|
const output3 = readResultOutput();
|
|
32698
33911
|
if (!output3) {
|
|
32699
33912
|
const message = `Job ${jobId} is waiting for input. Use: specialists resume ${jobId} "..."`;
|
|
32700
33913
|
if (args.json) {
|
|
32701
|
-
emitJson(status, null, message);
|
|
33914
|
+
emitJson(status, null, message, startupContext2);
|
|
32702
33915
|
} else {
|
|
32703
33916
|
process.stderr.write(`${dim10(message)}
|
|
32704
33917
|
`);
|
|
@@ -32709,16 +33922,17 @@ async function run16() {
|
|
|
32709
33922
|
--- Session is waiting for your input. Use: specialists resume ${jobId} "..." ---
|
|
32710
33923
|
`;
|
|
32711
33924
|
if (args.json) {
|
|
32712
|
-
emitJson(status, `${output3}${waitingFooter}`, null);
|
|
33925
|
+
emitJson(status, `${output3}${waitingFooter}`, null, startupContext2);
|
|
32713
33926
|
} else {
|
|
32714
|
-
emitHumanResult(output3, status, waitingFooter);
|
|
33927
|
+
emitHumanResult(output3, status, startupContext2, waitingFooter);
|
|
32715
33928
|
}
|
|
32716
33929
|
return;
|
|
32717
33930
|
}
|
|
32718
33931
|
if (status.status === "error") {
|
|
33932
|
+
const startupContext2 = deriveStartupSnapshot(status, readTimelineEventsForResult(sqliteClient, jobsDir, jobId));
|
|
32719
33933
|
const message = `Job ${jobId} failed: ${status.error ?? "unknown error"}`;
|
|
32720
33934
|
if (args.json) {
|
|
32721
|
-
emitJson(status, null, message);
|
|
33935
|
+
emitJson(status, null, message, startupContext2);
|
|
32722
33936
|
} else {
|
|
32723
33937
|
process.stderr.write(`${red3(`Job ${jobId} failed:`)} ${status.error ?? "unknown error"}
|
|
32724
33938
|
`);
|
|
@@ -32734,11 +33948,12 @@ async function run16() {
|
|
|
32734
33948
|
}
|
|
32735
33949
|
process.exit(1);
|
|
32736
33950
|
}
|
|
33951
|
+
const startupContext = deriveStartupSnapshot(status, readTimelineEventsForResult(sqliteClient, jobsDir, jobId));
|
|
32737
33952
|
if (args.json) {
|
|
32738
|
-
emitJson(status, output2, null);
|
|
33953
|
+
emitJson(status, output2, null, startupContext);
|
|
32739
33954
|
return;
|
|
32740
33955
|
}
|
|
32741
|
-
emitHumanResult(output2, status);
|
|
33956
|
+
emitHumanResult(output2, status, startupContext);
|
|
32742
33957
|
} catch (error2) {
|
|
32743
33958
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
32744
33959
|
if (args.json) {
|
|
@@ -32756,13 +33971,14 @@ var dim10 = (s) => `\x1B[2m${s}\x1B[0m`, red3 = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
|
32756
33971
|
var init_result = __esm(() => {
|
|
32757
33972
|
init_supervisor();
|
|
32758
33973
|
init_observability_sqlite();
|
|
33974
|
+
init_timeline_events();
|
|
32759
33975
|
init_node_resolve();
|
|
32760
33976
|
init_format_helpers();
|
|
32761
33977
|
});
|
|
32762
33978
|
|
|
32763
33979
|
// src/specialist/timeline-query.ts
|
|
32764
|
-
import { existsSync as existsSync20, readdirSync as readdirSync9, readFileSync as
|
|
32765
|
-
import { basename as basename5, join as
|
|
33980
|
+
import { existsSync as existsSync20, readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
|
|
33981
|
+
import { basename as basename5, join as join23 } from "path";
|
|
32766
33982
|
function readJobEvents(jobDir) {
|
|
32767
33983
|
const jobId = basename5(jobDir);
|
|
32768
33984
|
try {
|
|
@@ -32772,10 +33988,10 @@ function readJobEvents(jobDir) {
|
|
|
32772
33988
|
return sqliteEvents;
|
|
32773
33989
|
}
|
|
32774
33990
|
} catch {}
|
|
32775
|
-
const eventsPath =
|
|
33991
|
+
const eventsPath = join23(jobDir, "events.jsonl");
|
|
32776
33992
|
if (!existsSync20(eventsPath))
|
|
32777
33993
|
return [];
|
|
32778
|
-
const content =
|
|
33994
|
+
const content = readFileSync18(eventsPath, "utf-8");
|
|
32779
33995
|
const lines = content.split(`
|
|
32780
33996
|
`).filter(Boolean);
|
|
32781
33997
|
const events = [];
|
|
@@ -32788,7 +34004,7 @@ function readJobEvents(jobDir) {
|
|
|
32788
34004
|
return events;
|
|
32789
34005
|
}
|
|
32790
34006
|
function readJobEventsById(jobsDir, jobId) {
|
|
32791
|
-
return readJobEvents(
|
|
34007
|
+
return readJobEvents(join23(jobsDir, jobId));
|
|
32792
34008
|
}
|
|
32793
34009
|
function readAllJobEvents(jobsDir) {
|
|
32794
34010
|
if (!existsSync20(jobsDir))
|
|
@@ -32796,7 +34012,7 @@ function readAllJobEvents(jobsDir) {
|
|
|
32796
34012
|
const batches = [];
|
|
32797
34013
|
const entries = readdirSync9(jobsDir);
|
|
32798
34014
|
for (const entry of entries) {
|
|
32799
|
-
const jobDir =
|
|
34015
|
+
const jobDir = join23(jobsDir, entry);
|
|
32800
34016
|
try {
|
|
32801
34017
|
const stat2 = __require("fs").statSync(jobDir);
|
|
32802
34018
|
if (!stat2.isDirectory())
|
|
@@ -32805,12 +34021,12 @@ function readAllJobEvents(jobsDir) {
|
|
|
32805
34021
|
continue;
|
|
32806
34022
|
}
|
|
32807
34023
|
const jobId = entry;
|
|
32808
|
-
const statusPath =
|
|
34024
|
+
const statusPath = join23(jobDir, "status.json");
|
|
32809
34025
|
let specialist = "unknown";
|
|
32810
34026
|
let beadId;
|
|
32811
34027
|
if (existsSync20(statusPath)) {
|
|
32812
34028
|
try {
|
|
32813
|
-
const status = JSON.parse(
|
|
34029
|
+
const status = JSON.parse(readFileSync18(statusPath, "utf-8"));
|
|
32814
34030
|
specialist = status.specialist ?? "unknown";
|
|
32815
34031
|
beadId = status.bead_id;
|
|
32816
34032
|
} catch {}
|
|
@@ -32908,12 +34124,12 @@ __export(exports_feed, {
|
|
|
32908
34124
|
import {
|
|
32909
34125
|
closeSync as closeSync2,
|
|
32910
34126
|
existsSync as existsSync21,
|
|
32911
|
-
openSync as
|
|
32912
|
-
readFileSync as
|
|
34127
|
+
openSync as openSync3,
|
|
34128
|
+
readFileSync as readFileSync19,
|
|
32913
34129
|
readdirSync as readdirSync10,
|
|
32914
|
-
statSync as
|
|
34130
|
+
statSync as statSync3
|
|
32915
34131
|
} from "fs";
|
|
32916
|
-
import { join as
|
|
34132
|
+
import { join as join24 } from "path";
|
|
32917
34133
|
function getHumanEventKey(event) {
|
|
32918
34134
|
switch (event.type) {
|
|
32919
34135
|
case "meta":
|
|
@@ -32981,6 +34197,50 @@ function formatWaitingBanner(jobId, specialist) {
|
|
|
32981
34197
|
const prefix = magenta3(bold10("WAIT"));
|
|
32982
34198
|
return `${prefix} ${specialist} (${jobId}) is waiting for input. Use: specialists resume ${jobId} "..."`;
|
|
32983
34199
|
}
|
|
34200
|
+
function formatStartupContextLine(event) {
|
|
34201
|
+
if (event.type === "run_start") {
|
|
34202
|
+
const snapshot = event.startup_snapshot;
|
|
34203
|
+
if (!snapshot)
|
|
34204
|
+
return null;
|
|
34205
|
+
const parts = [];
|
|
34206
|
+
if (snapshot.job_id)
|
|
34207
|
+
parts.push(`job=${snapshot.job_id}`);
|
|
34208
|
+
if (snapshot.specialist_name)
|
|
34209
|
+
parts.push(`specialist=${snapshot.specialist_name}`);
|
|
34210
|
+
if (snapshot.bead_id)
|
|
34211
|
+
parts.push(`bead=${snapshot.bead_id}`);
|
|
34212
|
+
if (snapshot.reused_from_job_id)
|
|
34213
|
+
parts.push(`reused=${snapshot.reused_from_job_id}`);
|
|
34214
|
+
if (snapshot.worktree_owner_job_id)
|
|
34215
|
+
parts.push(`owner=${snapshot.worktree_owner_job_id}`);
|
|
34216
|
+
if (snapshot.chain_id)
|
|
34217
|
+
parts.push(`chain=${snapshot.chain_id}`);
|
|
34218
|
+
if (snapshot.chain_root_job_id)
|
|
34219
|
+
parts.push(`chain_root_job=${snapshot.chain_root_job_id}`);
|
|
34220
|
+
if (snapshot.chain_root_bead_id)
|
|
34221
|
+
parts.push(`chain_root_bead=${snapshot.chain_root_bead_id}`);
|
|
34222
|
+
if (snapshot.worktree_path)
|
|
34223
|
+
parts.push(`worktree=${snapshot.worktree_path}`);
|
|
34224
|
+
if (snapshot.branch)
|
|
34225
|
+
parts.push(`branch=${snapshot.branch}`);
|
|
34226
|
+
if (snapshot.variables_keys)
|
|
34227
|
+
parts.push(`vars=[${snapshot.variables_keys.join(",")}]`);
|
|
34228
|
+
if (snapshot.reviewed_job_id_present !== undefined)
|
|
34229
|
+
parts.push(`reviewed_present=${snapshot.reviewed_job_id_present}`);
|
|
34230
|
+
if (snapshot.reused_worktree_awareness_present !== undefined)
|
|
34231
|
+
parts.push(`reuse_awareness_present=${snapshot.reused_worktree_awareness_present}`);
|
|
34232
|
+
if (snapshot.bead_context_present !== undefined)
|
|
34233
|
+
parts.push(`bead_context_present=${snapshot.bead_context_present}`);
|
|
34234
|
+
if (snapshot.skills)
|
|
34235
|
+
parts.push(`skills=${snapshot.skills.count}`);
|
|
34236
|
+
return parts.length > 0 ? dim8(` \u21B3 startup ${parts.join(" ")}`) : null;
|
|
34237
|
+
}
|
|
34238
|
+
if (event.type === "meta" && event.memory_injection) {
|
|
34239
|
+
const mem = event.memory_injection;
|
|
34240
|
+
return dim8(` \u21B3 memory static=${mem.static_tokens} dynamic=${mem.memory_tokens} gitnexus=${mem.gitnexus_tokens} total=${mem.total_tokens}`);
|
|
34241
|
+
}
|
|
34242
|
+
return null;
|
|
34243
|
+
}
|
|
32984
34244
|
function parseSince(value) {
|
|
32985
34245
|
if (value.includes("T") || value.includes("-")) {
|
|
32986
34246
|
return new Date(value).getTime();
|
|
@@ -33007,8 +34267,8 @@ function parseCursor(value, defaultJobId) {
|
|
|
33007
34267
|
function readFileFresh(filePath) {
|
|
33008
34268
|
let fd = null;
|
|
33009
34269
|
try {
|
|
33010
|
-
fd =
|
|
33011
|
-
return
|
|
34270
|
+
fd = openSync3(filePath, "r");
|
|
34271
|
+
return readFileSync19(fd, "utf-8");
|
|
33012
34272
|
} catch {
|
|
33013
34273
|
return null;
|
|
33014
34274
|
} finally {
|
|
@@ -33025,7 +34285,7 @@ function readStatusJson(sqliteClient, jobsDir, jobId) {
|
|
|
33025
34285
|
} catch (error2) {
|
|
33026
34286
|
console.warn(`SQLite status read failed for job ${jobId}; falling back to status.json`, error2);
|
|
33027
34287
|
}
|
|
33028
|
-
const statusPath =
|
|
34288
|
+
const statusPath = join24(jobsDir, jobId, "status.json");
|
|
33029
34289
|
const raw = readFileFresh(statusPath);
|
|
33030
34290
|
if (!raw)
|
|
33031
34291
|
return null;
|
|
@@ -33195,6 +34455,9 @@ function printSnapshot(sqliteClient, merged, options, jobsDir) {
|
|
|
33195
34455
|
contextPct: meta.contextPct,
|
|
33196
34456
|
colorize
|
|
33197
34457
|
}));
|
|
34458
|
+
const startupContextLine = formatStartupContextLine(event);
|
|
34459
|
+
if (startupContextLine)
|
|
34460
|
+
console.log(startupContextLine);
|
|
33198
34461
|
}
|
|
33199
34462
|
}
|
|
33200
34463
|
function compareMergedEvents(a, b) {
|
|
@@ -33235,9 +34498,9 @@ function listMatchingJobIds(sqliteClient, jobsDir, options) {
|
|
|
33235
34498
|
return [];
|
|
33236
34499
|
const jobIds = [];
|
|
33237
34500
|
for (const entry of readdirSync10(jobsDir)) {
|
|
33238
|
-
const jobDir =
|
|
34501
|
+
const jobDir = join24(jobsDir, entry);
|
|
33239
34502
|
try {
|
|
33240
|
-
if (!
|
|
34503
|
+
if (!statSync3(jobDir).isDirectory())
|
|
33241
34504
|
continue;
|
|
33242
34505
|
} catch {
|
|
33243
34506
|
continue;
|
|
@@ -33269,7 +34532,7 @@ function readJobEventsFresh(sqliteClient, jobsDir, jobId) {
|
|
|
33269
34532
|
} catch (error2) {
|
|
33270
34533
|
console.warn(`SQLite events read failed for job ${jobId}; falling back to events.jsonl`, error2);
|
|
33271
34534
|
}
|
|
33272
|
-
const eventsPath =
|
|
34535
|
+
const eventsPath = join24(jobsDir, jobId, "events.jsonl");
|
|
33273
34536
|
const content = readFileFresh(eventsPath);
|
|
33274
34537
|
if (!content)
|
|
33275
34538
|
return [];
|
|
@@ -33345,7 +34608,7 @@ async function followMerged(sqliteClient, jobsDir, options) {
|
|
|
33345
34608
|
}
|
|
33346
34609
|
const lastPrintedEventKey = new Map;
|
|
33347
34610
|
const seenMetaKey = new Map;
|
|
33348
|
-
await new Promise((
|
|
34611
|
+
await new Promise((resolve8) => {
|
|
33349
34612
|
const interval = setInterval(() => {
|
|
33350
34613
|
const batches = filteredBatches();
|
|
33351
34614
|
for (const jobId of listMatchingJobIds(sqliteClient, jobsDir, options)) {
|
|
@@ -33415,11 +34678,14 @@ async function followMerged(sqliteClient, jobsDir, options) {
|
|
|
33415
34678
|
contextPct: meta.contextPct,
|
|
33416
34679
|
colorize
|
|
33417
34680
|
}));
|
|
34681
|
+
const startupContextLine = formatStartupContextLine(event);
|
|
34682
|
+
if (startupContextLine)
|
|
34683
|
+
console.log(startupContextLine);
|
|
33418
34684
|
}
|
|
33419
34685
|
}
|
|
33420
34686
|
if (!options.forever && trackedJobs.size > 0 && completedJobs.size === trackedJobs.size) {
|
|
33421
34687
|
clearInterval(interval);
|
|
33422
|
-
|
|
34688
|
+
resolve8();
|
|
33423
34689
|
}
|
|
33424
34690
|
}, 500);
|
|
33425
34691
|
});
|
|
@@ -33428,7 +34694,7 @@ async function run17() {
|
|
|
33428
34694
|
const options = parseArgs9(process.argv.slice(3));
|
|
33429
34695
|
const sqliteClient = createObservabilitySqliteClient();
|
|
33430
34696
|
try {
|
|
33431
|
-
const jobsDir =
|
|
34697
|
+
const jobsDir = join24(process.cwd(), ".specialists", "jobs");
|
|
33432
34698
|
if (!existsSync21(jobsDir)) {
|
|
33433
34699
|
console.log(dim8("No jobs directory found."));
|
|
33434
34700
|
return;
|
|
@@ -33468,8 +34734,8 @@ var exports_poll = {};
|
|
|
33468
34734
|
__export(exports_poll, {
|
|
33469
34735
|
run: () => run18
|
|
33470
34736
|
});
|
|
33471
|
-
import { existsSync as existsSync22, readFileSync as
|
|
33472
|
-
import { join as
|
|
34737
|
+
import { existsSync as existsSync22, readFileSync as readFileSync20 } from "fs";
|
|
34738
|
+
import { join as join25 } from "path";
|
|
33473
34739
|
function parseArgs10(argv) {
|
|
33474
34740
|
let jobId;
|
|
33475
34741
|
let cursor = 0;
|
|
@@ -33506,19 +34772,19 @@ function parseArgs10(argv) {
|
|
|
33506
34772
|
return { jobId, cursor, outputCursor };
|
|
33507
34773
|
}
|
|
33508
34774
|
function readJobState(jobsDir, jobId, cursor, outputCursor) {
|
|
33509
|
-
const jobDir =
|
|
33510
|
-
const statusPath =
|
|
34775
|
+
const jobDir = join25(jobsDir, jobId);
|
|
34776
|
+
const statusPath = join25(jobDir, "status.json");
|
|
33511
34777
|
let status = null;
|
|
33512
34778
|
if (existsSync22(statusPath)) {
|
|
33513
34779
|
try {
|
|
33514
|
-
status = JSON.parse(
|
|
34780
|
+
status = JSON.parse(readFileSync20(statusPath, "utf-8"));
|
|
33515
34781
|
} catch {}
|
|
33516
34782
|
}
|
|
33517
|
-
const resultPath =
|
|
34783
|
+
const resultPath = join25(jobDir, "result.txt");
|
|
33518
34784
|
let fullOutput = "";
|
|
33519
34785
|
if (existsSync22(resultPath)) {
|
|
33520
34786
|
try {
|
|
33521
|
-
fullOutput =
|
|
34787
|
+
fullOutput = readFileSync20(resultPath, "utf-8");
|
|
33522
34788
|
} catch {}
|
|
33523
34789
|
}
|
|
33524
34790
|
const events = readJobEventsById(jobsDir, jobId);
|
|
@@ -33550,8 +34816,8 @@ function readJobState(jobsDir, jobId, cursor, outputCursor) {
|
|
|
33550
34816
|
}
|
|
33551
34817
|
async function run18() {
|
|
33552
34818
|
const { jobId, cursor, outputCursor } = parseArgs10(process.argv.slice(3));
|
|
33553
|
-
const jobsDir =
|
|
33554
|
-
const jobDir =
|
|
34819
|
+
const jobsDir = join25(process.cwd(), ".specialists", "jobs");
|
|
34820
|
+
const jobDir = join25(jobsDir, jobId);
|
|
33555
34821
|
if (!existsSync22(jobDir)) {
|
|
33556
34822
|
const result2 = {
|
|
33557
34823
|
job_id: jobId,
|
|
@@ -33583,7 +34849,7 @@ var exports_steer = {};
|
|
|
33583
34849
|
__export(exports_steer, {
|
|
33584
34850
|
run: () => run19
|
|
33585
34851
|
});
|
|
33586
|
-
import { writeFileSync as
|
|
34852
|
+
import { writeFileSync as writeFileSync9 } from "fs";
|
|
33587
34853
|
async function run19() {
|
|
33588
34854
|
const jobId = process.argv[3];
|
|
33589
34855
|
const message = process.argv[4];
|
|
@@ -33614,7 +34880,7 @@ async function run19() {
|
|
|
33614
34880
|
try {
|
|
33615
34881
|
const payload = JSON.stringify({ type: "steer", message }) + `
|
|
33616
34882
|
`;
|
|
33617
|
-
|
|
34883
|
+
writeFileSync9(status.fifo_path, payload, { flag: "a" });
|
|
33618
34884
|
process.stdout.write(`${green10("\u2713")} Steer message sent to job ${jobId}
|
|
33619
34885
|
`);
|
|
33620
34886
|
} catch (err) {
|
|
@@ -33637,7 +34903,7 @@ var exports_resume = {};
|
|
|
33637
34903
|
__export(exports_resume, {
|
|
33638
34904
|
run: () => run20
|
|
33639
34905
|
});
|
|
33640
|
-
import { writeFileSync as
|
|
34906
|
+
import { writeFileSync as writeFileSync10 } from "fs";
|
|
33641
34907
|
async function run20() {
|
|
33642
34908
|
const jobId = process.argv[3];
|
|
33643
34909
|
const task = process.argv[4];
|
|
@@ -33668,7 +34934,7 @@ async function run20() {
|
|
|
33668
34934
|
try {
|
|
33669
34935
|
const payload = JSON.stringify({ type: "resume", task }) + `
|
|
33670
34936
|
`;
|
|
33671
|
-
|
|
34937
|
+
writeFileSync10(status.fifo_path, payload, { flag: "a" });
|
|
33672
34938
|
process.stdout.write(`${green11("\u2713")} Resume sent to job ${jobId}
|
|
33673
34939
|
`);
|
|
33674
34940
|
process.stdout.write(` Use 'specialists feed ${jobId} --follow' to watch the response.
|
|
@@ -33700,15 +34966,15 @@ async function run21() {
|
|
|
33700
34966
|
}
|
|
33701
34967
|
|
|
33702
34968
|
// src/specialist/worktree-gc.ts
|
|
33703
|
-
import { existsSync as existsSync23, readdirSync as readdirSync11, readFileSync as
|
|
33704
|
-
import { join as
|
|
34969
|
+
import { existsSync as existsSync23, readdirSync as readdirSync11, readFileSync as readFileSync21 } from "fs";
|
|
34970
|
+
import { join as join26 } from "path";
|
|
33705
34971
|
import { spawnSync as spawnSync18 } from "child_process";
|
|
33706
34972
|
function readJobStatus2(jobDir) {
|
|
33707
|
-
const statusPath =
|
|
34973
|
+
const statusPath = join26(jobDir, "status.json");
|
|
33708
34974
|
if (!existsSync23(statusPath))
|
|
33709
34975
|
return null;
|
|
33710
34976
|
try {
|
|
33711
|
-
return JSON.parse(
|
|
34977
|
+
return JSON.parse(readFileSync21(statusPath, "utf-8"));
|
|
33712
34978
|
} catch {
|
|
33713
34979
|
return null;
|
|
33714
34980
|
}
|
|
@@ -33726,7 +34992,7 @@ function collectWorktreeGcCandidates(jobsDir) {
|
|
|
33726
34992
|
for (const entry of readdirSync11(jobsDir, { withFileTypes: true })) {
|
|
33727
34993
|
if (!entry.isDirectory())
|
|
33728
34994
|
continue;
|
|
33729
|
-
const status = readJobStatus2(
|
|
34995
|
+
const status = readJobStatus2(join26(jobsDir, entry.name));
|
|
33730
34996
|
if (!status)
|
|
33731
34997
|
continue;
|
|
33732
34998
|
if (isActive(status.status))
|
|
@@ -33784,11 +35050,11 @@ __export(exports_clean, {
|
|
|
33784
35050
|
import {
|
|
33785
35051
|
existsSync as existsSync24,
|
|
33786
35052
|
readdirSync as readdirSync12,
|
|
33787
|
-
readFileSync as
|
|
33788
|
-
rmSync as
|
|
33789
|
-
statSync as
|
|
35053
|
+
readFileSync as readFileSync22,
|
|
35054
|
+
rmSync as rmSync3,
|
|
35055
|
+
statSync as statSync4
|
|
33790
35056
|
} from "fs";
|
|
33791
|
-
import { join as
|
|
35057
|
+
import { join as join27 } from "path";
|
|
33792
35058
|
function parseTtlDaysFromEnvironment() {
|
|
33793
35059
|
const rawValue = process.env.SPECIALISTS_JOB_TTL_DAYS ?? process.env.JOB_TTL_DAYS;
|
|
33794
35060
|
if (!rawValue)
|
|
@@ -33844,8 +35110,8 @@ function readDirectorySizeBytes(directoryPath) {
|
|
|
33844
35110
|
let totalBytes = 0;
|
|
33845
35111
|
const entries = readdirSync12(directoryPath, { withFileTypes: true });
|
|
33846
35112
|
for (const entry of entries) {
|
|
33847
|
-
const entryPath =
|
|
33848
|
-
const stats =
|
|
35113
|
+
const entryPath = join27(directoryPath, entry.name);
|
|
35114
|
+
const stats = statSync4(entryPath);
|
|
33849
35115
|
if (stats.isDirectory()) {
|
|
33850
35116
|
totalBytes += readDirectorySizeBytes(entryPath);
|
|
33851
35117
|
continue;
|
|
@@ -33857,7 +35123,7 @@ function readDirectorySizeBytes(directoryPath) {
|
|
|
33857
35123
|
function containsProtectedSqliteArtifact(directoryPath) {
|
|
33858
35124
|
const entries = readdirSync12(directoryPath, { withFileTypes: true });
|
|
33859
35125
|
for (const entry of entries) {
|
|
33860
|
-
const entryPath =
|
|
35126
|
+
const entryPath = join27(directoryPath, entry.name);
|
|
33861
35127
|
if (entry.isDirectory()) {
|
|
33862
35128
|
if (containsProtectedSqliteArtifact(entryPath))
|
|
33863
35129
|
return true;
|
|
@@ -33872,21 +35138,21 @@ function containsProtectedSqliteArtifact(directoryPath) {
|
|
|
33872
35138
|
function readCompletedJobDirectory(baseDirectory, entry) {
|
|
33873
35139
|
if (!entry.isDirectory())
|
|
33874
35140
|
return null;
|
|
33875
|
-
const directoryPath =
|
|
35141
|
+
const directoryPath = join27(baseDirectory, entry.name);
|
|
33876
35142
|
if (containsProtectedSqliteArtifact(directoryPath))
|
|
33877
35143
|
return null;
|
|
33878
|
-
const statusFilePath =
|
|
35144
|
+
const statusFilePath = join27(directoryPath, "status.json");
|
|
33879
35145
|
if (!existsSync24(statusFilePath))
|
|
33880
35146
|
return null;
|
|
33881
35147
|
let statusData;
|
|
33882
35148
|
try {
|
|
33883
|
-
statusData = JSON.parse(
|
|
35149
|
+
statusData = JSON.parse(readFileSync22(statusFilePath, "utf-8"));
|
|
33884
35150
|
} catch {
|
|
33885
35151
|
return null;
|
|
33886
35152
|
}
|
|
33887
35153
|
if (!COMPLETED_STATUSES.has(statusData.status))
|
|
33888
35154
|
return null;
|
|
33889
|
-
const directoryStats =
|
|
35155
|
+
const directoryStats = statSync4(directoryPath);
|
|
33890
35156
|
return {
|
|
33891
35157
|
id: entry.name,
|
|
33892
35158
|
directoryPath,
|
|
@@ -33923,7 +35189,7 @@ function selectJobsToRemove(completedJobs, options) {
|
|
|
33923
35189
|
const cutoffMs = Date.now() - ttlDays * MS_PER_DAY;
|
|
33924
35190
|
return jobsByNewest.filter((job) => job.modifiedAtMs < cutoffMs);
|
|
33925
35191
|
}
|
|
33926
|
-
function
|
|
35192
|
+
function formatBytes2(bytes) {
|
|
33927
35193
|
if (bytes < 1024)
|
|
33928
35194
|
return `${bytes} B`;
|
|
33929
35195
|
const units = ["KB", "MB", "GB", "TB"];
|
|
@@ -33938,7 +35204,7 @@ function formatBytes(bytes) {
|
|
|
33938
35204
|
function renderSummary(removedCount, freedBytes, dryRun) {
|
|
33939
35205
|
const action = dryRun ? "Would remove" : "Removed";
|
|
33940
35206
|
const noun = removedCount === 1 ? "directory" : "directories";
|
|
33941
|
-
return `${action} ${removedCount} job ${noun} (${
|
|
35207
|
+
return `${action} ${removedCount} job ${noun} (${formatBytes2(freedBytes)} freed)`;
|
|
33942
35208
|
}
|
|
33943
35209
|
function printDryRunPlan(jobs) {
|
|
33944
35210
|
if (jobs.length === 0)
|
|
@@ -33992,7 +35258,7 @@ async function run22() {
|
|
|
33992
35258
|
return;
|
|
33993
35259
|
}
|
|
33994
35260
|
for (const job of jobsToRemove) {
|
|
33995
|
-
|
|
35261
|
+
rmSync3(job.directoryPath, { recursive: true, force: true });
|
|
33996
35262
|
}
|
|
33997
35263
|
console.log(renderSummary(jobsToRemove.length, freedBytes, false));
|
|
33998
35264
|
if (worktreeCandidates.length > 0) {
|
|
@@ -34115,6 +35381,10 @@ async function run23() {
|
|
|
34115
35381
|
const guard = checkEpicUnresolvedGuard(beadId);
|
|
34116
35382
|
if (guard.blocked && guard.epicId && guard.epicStatus && isEpicUnresolvedState(guard.epicStatus)) {
|
|
34117
35383
|
console.log(`Chain ${beadId} belongs to unresolved epic ${guard.epicId} (${guard.epicStatus}).`);
|
|
35384
|
+
if (guard.epicStatus === "open") {
|
|
35385
|
+
console.log(`Epic ${guard.epicId} still open. Run: sp epic resolve ${guard.epicId}`);
|
|
35386
|
+
process.exit(1);
|
|
35387
|
+
}
|
|
34118
35388
|
console.log(`Redirecting session close publication to epic merge (${options.pr ? "PR mode" : "direct mode"}).`);
|
|
34119
35389
|
const args = ["merge", guard.epicId, ...options.rebuild ? ["--rebuild"] : [], ...options.pr ? ["--pr"] : []];
|
|
34120
35390
|
await handleEpicMergeCommand(args);
|
|
@@ -34137,10 +35407,51 @@ __export(exports_stop, {
|
|
|
34137
35407
|
function resolveTerminalStatus(jobId) {
|
|
34138
35408
|
return hasRunCompleteEvent(jobId) ? "done" : "cancelled";
|
|
34139
35409
|
}
|
|
35410
|
+
function parseStopArgs(argv) {
|
|
35411
|
+
let jobId;
|
|
35412
|
+
let force = false;
|
|
35413
|
+
for (const token of argv) {
|
|
35414
|
+
if (token === "--force") {
|
|
35415
|
+
force = true;
|
|
35416
|
+
continue;
|
|
35417
|
+
}
|
|
35418
|
+
if (!token.startsWith("-") && !jobId) {
|
|
35419
|
+
jobId = token;
|
|
35420
|
+
continue;
|
|
35421
|
+
}
|
|
35422
|
+
throw new Error(`Unknown option: ${token}`);
|
|
35423
|
+
}
|
|
35424
|
+
return { jobId, force };
|
|
35425
|
+
}
|
|
35426
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
35427
|
+
const deadline = Date.now() + timeoutMs;
|
|
35428
|
+
while (Date.now() < deadline) {
|
|
35429
|
+
if (!isProcessAlive(pid))
|
|
35430
|
+
return true;
|
|
35431
|
+
await new Promise((resolve8) => setTimeout(resolve8, 100));
|
|
35432
|
+
}
|
|
35433
|
+
return !isProcessAlive(pid);
|
|
35434
|
+
}
|
|
35435
|
+
function tryKillProcessGroup(pid) {
|
|
35436
|
+
try {
|
|
35437
|
+
process.kill(-pid, "SIGKILL");
|
|
35438
|
+
} catch (err) {
|
|
35439
|
+
if (err.code !== "ESRCH")
|
|
35440
|
+
throw err;
|
|
35441
|
+
}
|
|
35442
|
+
}
|
|
34140
35443
|
async function run24() {
|
|
34141
|
-
|
|
35444
|
+
let parsed;
|
|
35445
|
+
try {
|
|
35446
|
+
parsed = parseStopArgs(process.argv.slice(3));
|
|
35447
|
+
} catch (err) {
|
|
35448
|
+
console.error(err.message);
|
|
35449
|
+
console.error("Usage: specialists|sp stop <job-id> [--force]");
|
|
35450
|
+
process.exit(1);
|
|
35451
|
+
}
|
|
35452
|
+
const { jobId, force } = parsed;
|
|
34142
35453
|
if (!jobId) {
|
|
34143
|
-
console.error("Usage: specialists|sp stop <job-id>");
|
|
35454
|
+
console.error("Usage: specialists|sp stop <job-id> [--force]");
|
|
34144
35455
|
process.exit(1);
|
|
34145
35456
|
}
|
|
34146
35457
|
const jobsDir = resolveJobsDir(process.cwd());
|
|
@@ -34161,33 +35472,53 @@ async function run24() {
|
|
|
34161
35472
|
`);
|
|
34162
35473
|
process.exit(1);
|
|
34163
35474
|
}
|
|
35475
|
+
const pid = status.pid;
|
|
34164
35476
|
const tmuxSession = status.tmux_session;
|
|
34165
|
-
const
|
|
34166
|
-
|
|
34167
|
-
|
|
34168
|
-
|
|
34169
|
-
process.stdout.write(`${green12("\u2713")} Marked ${jobId} as
|
|
34170
|
-
`);
|
|
34171
|
-
if (tmuxSession) {
|
|
34172
|
-
killTmuxSession(tmuxSession);
|
|
34173
|
-
process.stdout.write(`${dim11(` tmux session ${tmuxSession} killed`)}
|
|
35477
|
+
const isAlreadyDead = !isProcessAlive(pid, status.started_at_ms);
|
|
35478
|
+
if (force && isAlreadyDead) {
|
|
35479
|
+
supervisor.updateJobStatus(jobId, "error");
|
|
35480
|
+
tryKillProcessGroup(pid);
|
|
35481
|
+
process.stdout.write(`${green12("\u2713")} Marked ${jobId} as error (PID ${pid} already dead)
|
|
34174
35482
|
`);
|
|
34175
|
-
|
|
34176
|
-
|
|
34177
|
-
|
|
34178
|
-
|
|
35483
|
+
} else {
|
|
35484
|
+
const terminalStatus = resolveTerminalStatus(jobId);
|
|
35485
|
+
supervisor.updateJobStatus(jobId, terminalStatus);
|
|
35486
|
+
try {
|
|
35487
|
+
process.kill(pid, "SIGTERM");
|
|
35488
|
+
process.stdout.write(`${green12("\u2713")} Marked ${jobId} as ${terminalStatus} and sent SIGTERM to PID ${pid}
|
|
34179
35489
|
`);
|
|
34180
|
-
if (
|
|
34181
|
-
|
|
34182
|
-
|
|
35490
|
+
if (force) {
|
|
35491
|
+
const exited = await waitForProcessExit(pid, 5000);
|
|
35492
|
+
if (!exited) {
|
|
35493
|
+
supervisor.updateJobStatus(jobId, "error");
|
|
35494
|
+
tryKillProcessGroup(pid);
|
|
35495
|
+
process.stderr.write(`${red6("Force stop:")} PID ${pid} ignored SIGTERM, marked ${jobId} as error and killed process group.
|
|
34183
35496
|
`);
|
|
35497
|
+
}
|
|
34184
35498
|
}
|
|
34185
|
-
}
|
|
34186
|
-
|
|
35499
|
+
} catch (err) {
|
|
35500
|
+
if (err.code === "ESRCH") {
|
|
35501
|
+
if (force) {
|
|
35502
|
+
supervisor.updateJobStatus(jobId, "error");
|
|
35503
|
+
tryKillProcessGroup(pid);
|
|
35504
|
+
process.stdout.write(`${green12("\u2713")} Marked ${jobId} as error (PID ${pid} already gone)
|
|
34187
35505
|
`);
|
|
34188
|
-
|
|
35506
|
+
} else {
|
|
35507
|
+
process.stderr.write(`${red6(`Process ${pid} not found.`)} Job may have already completed.
|
|
35508
|
+
`);
|
|
35509
|
+
}
|
|
35510
|
+
} else {
|
|
35511
|
+
process.stderr.write(`${red6("Error:")} ${err.message}
|
|
35512
|
+
`);
|
|
35513
|
+
process.exit(1);
|
|
35514
|
+
}
|
|
34189
35515
|
}
|
|
34190
35516
|
}
|
|
35517
|
+
if (tmuxSession) {
|
|
35518
|
+
killTmuxSession(tmuxSession);
|
|
35519
|
+
process.stdout.write(`${dim11(` tmux session ${tmuxSession} killed`)}
|
|
35520
|
+
`);
|
|
35521
|
+
}
|
|
34191
35522
|
} finally {
|
|
34192
35523
|
await supervisor.dispose();
|
|
34193
35524
|
}
|
|
@@ -34197,6 +35528,7 @@ var init_stop = __esm(() => {
|
|
|
34197
35528
|
init_supervisor();
|
|
34198
35529
|
init_job_root();
|
|
34199
35530
|
init_observability_sqlite();
|
|
35531
|
+
init_process_liveness();
|
|
34200
35532
|
init_tmux_utils();
|
|
34201
35533
|
});
|
|
34202
35534
|
|
|
@@ -34206,15 +35538,15 @@ __export(exports_attach, {
|
|
|
34206
35538
|
run: () => run25
|
|
34207
35539
|
});
|
|
34208
35540
|
import { execFileSync as execFileSync3, spawnSync as spawnSync20 } from "child_process";
|
|
34209
|
-
import { readFileSync as
|
|
34210
|
-
import { join as
|
|
35541
|
+
import { readFileSync as readFileSync23 } from "fs";
|
|
35542
|
+
import { join as join28 } from "path";
|
|
34211
35543
|
function exitWithError(message) {
|
|
34212
35544
|
console.error(message);
|
|
34213
35545
|
process.exit(1);
|
|
34214
35546
|
}
|
|
34215
35547
|
function readStatus(statusPath, jobId) {
|
|
34216
35548
|
try {
|
|
34217
|
-
return JSON.parse(
|
|
35549
|
+
return JSON.parse(readFileSync23(statusPath, "utf-8"));
|
|
34218
35550
|
} catch (error2) {
|
|
34219
35551
|
if (error2 && typeof error2 === "object" && "code" in error2 && error2.code === "ENOENT") {
|
|
34220
35552
|
exitWithError(`Job \`${jobId}\` not found. Run \`specialists status\` to see active jobs.`);
|
|
@@ -34228,8 +35560,8 @@ async function run25() {
|
|
|
34228
35560
|
if (!jobId) {
|
|
34229
35561
|
exitWithError("Usage: specialists attach <job-id>");
|
|
34230
35562
|
}
|
|
34231
|
-
const jobsDir =
|
|
34232
|
-
const statusPath =
|
|
35563
|
+
const jobsDir = join28(process.cwd(), ".specialists", "jobs");
|
|
35564
|
+
const statusPath = join28(jobsDir, jobId, "status.json");
|
|
34233
35565
|
const status = readStatus(statusPath, jobId);
|
|
34234
35566
|
if (status.status === "done" || status.status === "error") {
|
|
34235
35567
|
exitWithError(`Job \`${jobId}\` has already completed (status: ${status.status}). Use \`specialists result ${jobId}\` to read output.`);
|
|
@@ -34490,8 +35822,8 @@ __export(exports_doctor, {
|
|
|
34490
35822
|
});
|
|
34491
35823
|
import { createHash as createHash4 } from "crypto";
|
|
34492
35824
|
import { spawnSync as spawnSync21 } from "child_process";
|
|
34493
|
-
import { existsSync as existsSync25, lstatSync as lstatSync2, mkdirSync as
|
|
34494
|
-
import { dirname as
|
|
35825
|
+
import { existsSync as existsSync25, lstatSync as lstatSync2, mkdirSync as mkdirSync8, readdirSync as readdirSync13, readFileSync as readFileSync24, readlinkSync as readlinkSync2, writeFileSync as writeFileSync11 } from "fs";
|
|
35826
|
+
import { dirname as dirname7, join as join29, relative as relative2, resolve as resolve8 } from "path";
|
|
34495
35827
|
function ok3(msg) {
|
|
34496
35828
|
console.log(` ${green14("\u2713")} ${msg}`);
|
|
34497
35829
|
}
|
|
@@ -34523,7 +35855,7 @@ function loadJson2(path) {
|
|
|
34523
35855
|
if (!existsSync25(path))
|
|
34524
35856
|
return null;
|
|
34525
35857
|
try {
|
|
34526
|
-
return JSON.parse(
|
|
35858
|
+
return JSON.parse(readFileSync24(path, "utf8"));
|
|
34527
35859
|
} catch {
|
|
34528
35860
|
return null;
|
|
34529
35861
|
}
|
|
@@ -34566,7 +35898,7 @@ function checkBd() {
|
|
|
34566
35898
|
return false;
|
|
34567
35899
|
}
|
|
34568
35900
|
ok3(`bd installed ${dim13(sp("bd", ["--version"]).stdout || "")}`);
|
|
34569
|
-
if (existsSync25(
|
|
35901
|
+
if (existsSync25(join29(CWD, ".beads")))
|
|
34570
35902
|
ok3(".beads/ present in project");
|
|
34571
35903
|
else
|
|
34572
35904
|
warn3(".beads/ not found in project");
|
|
@@ -34586,7 +35918,7 @@ function checkHooks() {
|
|
|
34586
35918
|
section3("Claude Code hooks (2 expected)");
|
|
34587
35919
|
let allPresent = true;
|
|
34588
35920
|
for (const name of HOOK_NAMES) {
|
|
34589
|
-
const canonicalPath =
|
|
35921
|
+
const canonicalPath = join29(HOOKS_DIR, name);
|
|
34590
35922
|
if (!existsSync25(canonicalPath)) {
|
|
34591
35923
|
fail4(`${relative2(CWD, canonicalPath)} ${red7("missing")}`);
|
|
34592
35924
|
fix("specialists init");
|
|
@@ -34594,10 +35926,10 @@ function checkHooks() {
|
|
|
34594
35926
|
} else {
|
|
34595
35927
|
ok3(relative2(CWD, canonicalPath));
|
|
34596
35928
|
}
|
|
34597
|
-
const claudeHookPath =
|
|
35929
|
+
const claudeHookPath = join29(CLAUDE_HOOKS_DIR, name);
|
|
34598
35930
|
const symlinkState = isSymlinkTo(claudeHookPath, canonicalPath);
|
|
34599
35931
|
if (symlinkState.ok) {
|
|
34600
|
-
ok3(`${relative2(CWD, claudeHookPath)} -> ${relative2(
|
|
35932
|
+
ok3(`${relative2(CWD, claudeHookPath)} -> ${relative2(dirname7(claudeHookPath), canonicalPath)}`);
|
|
34601
35933
|
continue;
|
|
34602
35934
|
}
|
|
34603
35935
|
allPresent = false;
|
|
@@ -34649,14 +35981,14 @@ function checkMCP() {
|
|
|
34649
35981
|
}
|
|
34650
35982
|
function hashFile(path) {
|
|
34651
35983
|
const hash = createHash4("sha256");
|
|
34652
|
-
hash.update(
|
|
35984
|
+
hash.update(readFileSync24(path));
|
|
34653
35985
|
return hash.digest("hex");
|
|
34654
35986
|
}
|
|
34655
35987
|
function collectFileHashes(rootDir) {
|
|
34656
35988
|
const hashes = new Map;
|
|
34657
35989
|
const visit2 = (dir) => {
|
|
34658
35990
|
for (const entry of readdirSync13(dir, { withFileTypes: true })) {
|
|
34659
|
-
const fullPath =
|
|
35991
|
+
const fullPath = join29(dir, entry.name);
|
|
34660
35992
|
if (entry.isDirectory()) {
|
|
34661
35993
|
visit2(fullPath);
|
|
34662
35994
|
continue;
|
|
@@ -34684,8 +36016,8 @@ function isSymlinkTo(linkPath, expectedTargetPath) {
|
|
|
34684
36016
|
return { ok: false, reason: "not-symlink" };
|
|
34685
36017
|
try {
|
|
34686
36018
|
const rawTarget = readlinkSync2(linkPath);
|
|
34687
|
-
const resolvedTarget =
|
|
34688
|
-
const resolvedExpected =
|
|
36019
|
+
const resolvedTarget = resolve8(dirname7(linkPath), rawTarget);
|
|
36020
|
+
const resolvedExpected = resolve8(expectedTargetPath);
|
|
34689
36021
|
if (resolvedTarget !== resolvedExpected) {
|
|
34690
36022
|
return { ok: false, reason: "wrong-target", target: rawTarget };
|
|
34691
36023
|
}
|
|
@@ -34743,7 +36075,7 @@ function checkSkillDrift() {
|
|
|
34743
36075
|
}
|
|
34744
36076
|
let linksOk = true;
|
|
34745
36077
|
for (const scope of ["claude", "pi"]) {
|
|
34746
|
-
const activeRoot =
|
|
36078
|
+
const activeRoot = join29(XTRM_ACTIVE_SKILLS_DIR, scope);
|
|
34747
36079
|
if (!existsSync25(activeRoot)) {
|
|
34748
36080
|
fail4(`${relative2(CWD, activeRoot)}/ missing`);
|
|
34749
36081
|
fix("specialists init --sync-skills");
|
|
@@ -34752,8 +36084,8 @@ function checkSkillDrift() {
|
|
|
34752
36084
|
}
|
|
34753
36085
|
const defaultSkills = readdirSync13(XTRM_DEFAULT_SKILLS_DIR, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
34754
36086
|
for (const skillName of defaultSkills) {
|
|
34755
|
-
const activeLinkPath =
|
|
34756
|
-
const expectedTarget =
|
|
36087
|
+
const activeLinkPath = join29(activeRoot, skillName);
|
|
36088
|
+
const expectedTarget = join29(XTRM_DEFAULT_SKILLS_DIR, skillName);
|
|
34757
36089
|
const state = isSymlinkTo(activeLinkPath, expectedTarget);
|
|
34758
36090
|
if (state.ok)
|
|
34759
36091
|
continue;
|
|
@@ -34772,14 +36104,14 @@ function checkSkillDrift() {
|
|
|
34772
36104
|
}
|
|
34773
36105
|
}
|
|
34774
36106
|
const skillRootChecks = [
|
|
34775
|
-
{ root:
|
|
34776
|
-
{ root:
|
|
36107
|
+
{ root: join29(CLAUDE_DIR, "skills"), expected: ACTIVE_CLAUDE_SKILLS_DIR },
|
|
36108
|
+
{ root: join29(PI_DIR, "skills"), expected: ACTIVE_PI_SKILLS_DIR }
|
|
34777
36109
|
];
|
|
34778
36110
|
let rootLinksOk = true;
|
|
34779
36111
|
for (const check2 of skillRootChecks) {
|
|
34780
36112
|
const state = isSymlinkTo(check2.root, check2.expected);
|
|
34781
36113
|
if (state.ok) {
|
|
34782
|
-
ok3(`${relative2(CWD, check2.root)} -> ${relative2(
|
|
36114
|
+
ok3(`${relative2(CWD, check2.root)} -> ${relative2(dirname7(check2.root), check2.expected)}`);
|
|
34783
36115
|
continue;
|
|
34784
36116
|
}
|
|
34785
36117
|
rootLinksOk = false;
|
|
@@ -34799,9 +36131,9 @@ function checkSkillDrift() {
|
|
|
34799
36131
|
}
|
|
34800
36132
|
function checkRuntimeDirs() {
|
|
34801
36133
|
section3(".specialists/ runtime directories");
|
|
34802
|
-
const rootDir =
|
|
34803
|
-
const jobsDir =
|
|
34804
|
-
const readyDir =
|
|
36134
|
+
const rootDir = join29(CWD, ".specialists");
|
|
36135
|
+
const jobsDir = join29(rootDir, "jobs");
|
|
36136
|
+
const readyDir = join29(rootDir, "ready");
|
|
34805
36137
|
let allOk = true;
|
|
34806
36138
|
if (!existsSync25(rootDir)) {
|
|
34807
36139
|
warn3(".specialists/ not found in current project");
|
|
@@ -34812,7 +36144,7 @@ function checkRuntimeDirs() {
|
|
|
34812
36144
|
for (const [subDir, label] of [[jobsDir, "jobs"], [readyDir, "ready"]]) {
|
|
34813
36145
|
if (!existsSync25(subDir)) {
|
|
34814
36146
|
warn3(`.specialists/${label}/ missing \u2014 auto-creating`);
|
|
34815
|
-
|
|
36147
|
+
mkdirSync8(subDir, { recursive: true });
|
|
34816
36148
|
ok3(`.specialists/${label}/ created`);
|
|
34817
36149
|
} else {
|
|
34818
36150
|
ok3(`.specialists/${label}/ present`);
|
|
@@ -34843,10 +36175,10 @@ function compareVersions(left, right) {
|
|
|
34843
36175
|
}
|
|
34844
36176
|
function setStatusError(statusPath) {
|
|
34845
36177
|
try {
|
|
34846
|
-
const raw =
|
|
36178
|
+
const raw = readFileSync24(statusPath, "utf8");
|
|
34847
36179
|
const status = JSON.parse(raw);
|
|
34848
36180
|
status.status = "error";
|
|
34849
|
-
|
|
36181
|
+
writeFileSync11(statusPath, `${JSON.stringify(status, null, 2)}
|
|
34850
36182
|
`, "utf8");
|
|
34851
36183
|
} catch {}
|
|
34852
36184
|
}
|
|
@@ -34865,11 +36197,11 @@ function cleanupProcesses(jobsDir, dryRun) {
|
|
|
34865
36197
|
zombieJobIds: []
|
|
34866
36198
|
};
|
|
34867
36199
|
for (const jobId of entries) {
|
|
34868
|
-
const statusPath =
|
|
36200
|
+
const statusPath = join29(jobsDir, jobId, "status.json");
|
|
34869
36201
|
if (!existsSync25(statusPath))
|
|
34870
36202
|
continue;
|
|
34871
36203
|
try {
|
|
34872
|
-
const status = JSON.parse(
|
|
36204
|
+
const status = JSON.parse(readFileSync24(statusPath, "utf8"));
|
|
34873
36205
|
result.total += 1;
|
|
34874
36206
|
if (status.status !== "running" && status.status !== "starting")
|
|
34875
36207
|
continue;
|
|
@@ -34900,9 +36232,52 @@ function renderProcessSummary(result, dryRun) {
|
|
|
34900
36232
|
const action = dryRun ? "would be marked error" : "marked error";
|
|
34901
36233
|
return `${result.zombies} zombie job${result.zombies === 1 ? "" : "s"} found (${result.updated} ${action})`;
|
|
34902
36234
|
}
|
|
36235
|
+
function runDoctorOrphans() {
|
|
36236
|
+
const sqliteClient = createObservabilitySqliteClient();
|
|
36237
|
+
if (!sqliteClient) {
|
|
36238
|
+
console.log(`
|
|
36239
|
+
${bold13("specialists doctor orphans")}
|
|
36240
|
+
`);
|
|
36241
|
+
fail4("observability SQLite not available");
|
|
36242
|
+
fix("specialists db setup");
|
|
36243
|
+
console.log("");
|
|
36244
|
+
process.exit(1);
|
|
36245
|
+
}
|
|
36246
|
+
try {
|
|
36247
|
+
const findings = sqliteClient.scanOrphans();
|
|
36248
|
+
const byKind = {
|
|
36249
|
+
orphan: findings.filter((item) => item.kind === "orphan"),
|
|
36250
|
+
stalePointer: findings.filter((item) => item.kind === "stale-pointer"),
|
|
36251
|
+
integrity: findings.filter((item) => item.kind === "integrity-violation")
|
|
36252
|
+
};
|
|
36253
|
+
console.log(`
|
|
36254
|
+
${bold13("specialists doctor orphans")}
|
|
36255
|
+
`);
|
|
36256
|
+
if (findings.length === 0) {
|
|
36257
|
+
ok3("No orphan/stale/integrity findings");
|
|
36258
|
+
console.log("");
|
|
36259
|
+
return;
|
|
36260
|
+
}
|
|
36261
|
+
const renderGroup = (label, rows) => {
|
|
36262
|
+
if (rows.length === 0)
|
|
36263
|
+
return;
|
|
36264
|
+
console.log(` ${yellow12("\u25CB")} ${label}: ${rows.length}`);
|
|
36265
|
+
for (const row of rows) {
|
|
36266
|
+
console.log(` - [${row.code}] ${row.message}`);
|
|
36267
|
+
}
|
|
36268
|
+
};
|
|
36269
|
+
renderGroup("orphan", byKind.orphan);
|
|
36270
|
+
renderGroup("stale-pointer", byKind.stalePointer);
|
|
36271
|
+
renderGroup("integrity-violation", byKind.integrity);
|
|
36272
|
+
console.log("");
|
|
36273
|
+
process.exit(1);
|
|
36274
|
+
} finally {
|
|
36275
|
+
sqliteClient.close();
|
|
36276
|
+
}
|
|
36277
|
+
}
|
|
34903
36278
|
function checkZombieJobs() {
|
|
34904
36279
|
section3("Background jobs");
|
|
34905
|
-
const jobsDir =
|
|
36280
|
+
const jobsDir = join29(CWD, ".specialists", "jobs");
|
|
34906
36281
|
if (!existsSync25(jobsDir)) {
|
|
34907
36282
|
hint("No .specialists/jobs/ \u2014 skipping");
|
|
34908
36283
|
return true;
|
|
@@ -34921,7 +36296,16 @@ function checkZombieJobs() {
|
|
|
34921
36296
|
}
|
|
34922
36297
|
return result.zombies === 0;
|
|
34923
36298
|
}
|
|
34924
|
-
async function run27() {
|
|
36299
|
+
async function run27(argv = process.argv.slice(3)) {
|
|
36300
|
+
const subcommand = argv[0];
|
|
36301
|
+
if (subcommand === "orphans") {
|
|
36302
|
+
runDoctorOrphans();
|
|
36303
|
+
return;
|
|
36304
|
+
}
|
|
36305
|
+
if (subcommand && subcommand !== "--help" && subcommand !== "-h") {
|
|
36306
|
+
console.error(`Unknown doctor subcommand: '${subcommand}'`);
|
|
36307
|
+
process.exit(1);
|
|
36308
|
+
}
|
|
34925
36309
|
console.log(`
|
|
34926
36310
|
${bold13("specialists doctor")}
|
|
34927
36311
|
`);
|
|
@@ -34946,20 +36330,21 @@ ${bold13("specialists doctor")}
|
|
|
34946
36330
|
}
|
|
34947
36331
|
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
36332
|
var init_doctor = __esm(() => {
|
|
36333
|
+
init_observability_sqlite();
|
|
34949
36334
|
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 =
|
|
36335
|
+
CLAUDE_DIR = join29(CWD, ".claude");
|
|
36336
|
+
PI_DIR = join29(CWD, ".pi");
|
|
36337
|
+
XTRM_SKILLS_DIR = join29(CWD, ".xtrm", "skills");
|
|
36338
|
+
XTRM_DEFAULT_SKILLS_DIR = join29(XTRM_SKILLS_DIR, "default");
|
|
36339
|
+
XTRM_ACTIVE_SKILLS_DIR = join29(XTRM_SKILLS_DIR, "active");
|
|
36340
|
+
ACTIVE_CLAUDE_SKILLS_DIR = join29(XTRM_ACTIVE_SKILLS_DIR, "claude");
|
|
36341
|
+
ACTIVE_PI_SKILLS_DIR = join29(XTRM_ACTIVE_SKILLS_DIR, "pi");
|
|
36342
|
+
CONFIG_SKILLS_DIR = join29(CWD, "config", "skills");
|
|
36343
|
+
SPECIALISTS_DIR = join29(CWD, ".specialists");
|
|
36344
|
+
HOOKS_DIR = join29(CWD, ".xtrm", "hooks", "specialists");
|
|
36345
|
+
CLAUDE_HOOKS_DIR = join29(CLAUDE_DIR, "hooks");
|
|
36346
|
+
SETTINGS_FILE = join29(CLAUDE_DIR, "settings.json");
|
|
36347
|
+
MCP_FILE2 = join29(CWD, ".mcp.json");
|
|
34963
36348
|
HOOK_NAMES = [
|
|
34964
36349
|
"specialists-complete.mjs",
|
|
34965
36350
|
"specialists-session-start.mjs"
|
|
@@ -42818,7 +44203,7 @@ async function run30() {
|
|
|
42818
44203
|
if (wantsHelp()) {
|
|
42819
44204
|
console.log([
|
|
42820
44205
|
"",
|
|
42821
|
-
"Usage: specialists db <setup|backfill>",
|
|
44206
|
+
"Usage: specialists db <setup|backfill|vacuum|prune>",
|
|
42822
44207
|
"",
|
|
42823
44208
|
"Provision the shared observability SQLite database (human-only).",
|
|
42824
44209
|
"",
|
|
@@ -42827,6 +44212,9 @@ async function run30() {
|
|
|
42827
44212
|
" init Alias for setup",
|
|
42828
44213
|
" backfill Backfill specialist_jobs from .specialists/jobs/*/status.json",
|
|
42829
44214
|
" Use --events to also replay events.jsonl",
|
|
44215
|
+
" vacuum Run SQLite VACUUM (refuses when active jobs running/starting)",
|
|
44216
|
+
" prune Prune old rows: requires --before <iso|duration>, dry-run by default",
|
|
44217
|
+
" Use --apply to execute; --include-epics to also prune epic_runs",
|
|
42830
44218
|
"",
|
|
42831
44219
|
"Notes:",
|
|
42832
44220
|
" - TTY required (blocked in agent/non-interactive sessions)",
|
|
@@ -42837,6 +44225,9 @@ async function run30() {
|
|
|
42837
44225
|
" specialists db setup",
|
|
42838
44226
|
" specialists db backfill",
|
|
42839
44227
|
" specialists db backfill --events",
|
|
44228
|
+
" specialists db vacuum",
|
|
44229
|
+
" specialists db prune --before 30d --dry-run",
|
|
44230
|
+
" specialists db prune --before 2026-01-01T00:00:00Z --apply --include-epics",
|
|
42840
44231
|
" sp db setup",
|
|
42841
44232
|
" sp db backfill",
|
|
42842
44233
|
""
|
|
@@ -43508,7 +44899,7 @@ async function run30() {
|
|
|
43508
44899
|
if (wantsHelp()) {
|
|
43509
44900
|
console.log([
|
|
43510
44901
|
"",
|
|
43511
|
-
"Usage: specialists doctor",
|
|
44902
|
+
"Usage: specialists doctor [orphans]",
|
|
43512
44903
|
"",
|
|
43513
44904
|
"Diagnose bootstrap and runtime problems.",
|
|
43514
44905
|
"",
|
|
@@ -43525,15 +44916,19 @@ async function run30() {
|
|
|
43525
44916
|
" - prints fix hints for failing checks",
|
|
43526
44917
|
" - auto-creates missing runtime directories when possible",
|
|
43527
44918
|
"",
|
|
44919
|
+
"Subcommands:",
|
|
44920
|
+
" orphans Read-only orphan scan: membership/jobs/epics/worktree pointers",
|
|
44921
|
+
"",
|
|
43528
44922
|
"Examples:",
|
|
43529
44923
|
" specialists doctor",
|
|
44924
|
+
" specialists doctor orphans",
|
|
43530
44925
|
""
|
|
43531
44926
|
].join(`
|
|
43532
44927
|
`));
|
|
43533
44928
|
return;
|
|
43534
44929
|
}
|
|
43535
44930
|
const { run: handler } = await Promise.resolve().then(() => (init_doctor(), exports_doctor));
|
|
43536
|
-
return handler();
|
|
44931
|
+
return handler(process.argv.slice(3));
|
|
43537
44932
|
}
|
|
43538
44933
|
if (sub === "setup") {
|
|
43539
44934
|
if (wantsHelp()) {
|