@jaggerxtrm/specialists 3.14.1 → 3.15.0
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/LICENSE +21 -0
- package/README.md +24 -3
- package/config/catalog/gitnexus.json +12 -0
- package/config/catalog/index.json +59 -0
- package/config/catalog/native.json +12 -0
- package/config/catalog/serena.json +12 -0
- package/config/mandatory-rules/README.md +7 -6
- package/config/mandatory-rules/code-quality-defaults.md +5 -0
- package/config/mandatory-rules/diagnose-loop.md +13 -0
- package/config/mandatory-rules/gitnexus-required.md +1 -0
- package/config/mandatory-rules/research-tool-routing.md +12 -0
- package/config/mandatory-rules/security-review-defaults.md +9 -0
- package/config/mandatory-rules/serena-cheatsheet.md +16 -4
- package/config/presets.json +1 -1
- package/config/skills/memory-audit-transaction/SKILL.md +196 -0
- package/config/skills/memory-audit-transaction/scripts/pre-bulk-export.sh +58 -0
- package/config/skills/using-specialists/SKILL.md +13 -12
- package/config/skills/using-specialists-auto/SKILL.md +137 -0
- package/config/skills/using-specialists-v2/SKILL.md +14 -21
- package/config/skills/using-specialists-v3/SKILL.md +399 -27
- package/config/specialists/changelog-drafter.specialist.json +3 -2
- package/config/specialists/changelog-keeper.specialist.json +1 -1
- package/config/specialists/code-sanity.specialist.json +3 -5
- package/config/specialists/debugger.specialist.json +4 -8
- package/config/specialists/executor.specialist.json +6 -8
- package/config/specialists/explorer.specialist.json +7 -8
- package/config/specialists/memory-processor.specialist.json +14 -7
- package/config/specialists/node-coordinator.specialist.json +2 -2
- package/config/specialists/overthinker.specialist.json +7 -10
- package/config/specialists/planner.specialist.json +3 -4
- package/config/specialists/researcher.specialist.json +15 -19
- package/config/specialists/reviewer.specialist.json +4 -8
- package/config/specialists/security-auditor.specialist.json +3 -8
- package/config/specialists/specialists-creator.specialist.json +4 -2
- package/config/specialists/test-runner.specialist.json +10 -10
- package/config/specialists/xt-merge.specialist.json +10 -4
- package/dist/asset-contract.json +205 -0
- package/dist/index.js +1990 -704
- package/dist/lib.js +99 -17
- package/dist/types/cli/clean.d.ts.map +1 -1
- package/dist/types/cli/doctor.d.ts +1 -0
- package/dist/types/cli/doctor.d.ts.map +1 -1
- package/dist/types/cli/edit.d.ts.map +1 -1
- package/dist/types/cli/epic.d.ts +0 -1
- package/dist/types/cli/epic.d.ts.map +1 -1
- package/dist/types/cli/feed.d.ts.map +1 -1
- package/dist/types/cli/finalize.d.ts +2 -0
- package/dist/types/cli/finalize.d.ts.map +1 -0
- package/dist/types/cli/format-helpers.d.ts.map +1 -1
- package/dist/types/cli/init.d.ts.map +1 -1
- package/dist/types/cli/list-rules.d.ts.map +1 -1
- package/dist/types/cli/merge.d.ts +4 -3
- package/dist/types/cli/merge.d.ts.map +1 -1
- package/dist/types/cli/ps.d.ts.map +1 -1
- package/dist/types/cli/quickstart.d.ts.map +1 -1
- package/dist/types/cli/run.d.ts +1 -0
- package/dist/types/cli/run.d.ts.map +1 -1
- package/dist/types/pi/session.d.ts.map +1 -1
- package/dist/types/specialist/epic-lifecycle.d.ts +5 -5
- package/dist/types/specialist/epic-lifecycle.d.ts.map +1 -1
- package/dist/types/specialist/epic-readiness.d.ts +1 -1
- package/dist/types/specialist/epic-readiness.d.ts.map +1 -1
- package/dist/types/specialist/jobRegistry.d.ts +5 -0
- package/dist/types/specialist/jobRegistry.d.ts.map +1 -1
- package/dist/types/specialist/observability-sqlite.d.ts +8 -0
- package/dist/types/specialist/observability-sqlite.d.ts.map +1 -1
- package/dist/types/specialist/process-health.d.ts +77 -0
- package/dist/types/specialist/process-health.d.ts.map +1 -0
- package/dist/types/specialist/runner.d.ts.map +1 -1
- package/dist/types/specialist/schema.d.ts +162 -0
- package/dist/types/specialist/schema.d.ts.map +1 -1
- package/dist/types/specialist/script-runner.d.ts +31 -1
- package/dist/types/specialist/script-runner.d.ts.map +1 -1
- package/dist/types/specialist/supervisor.d.ts +8 -0
- package/dist/types/specialist/supervisor.d.ts.map +1 -1
- package/dist/types/specialist/timeline-query.d.ts +1 -1
- package/dist/types/specialist/timeline-query.d.ts.map +1 -1
- package/dist/types/specialist/worktree.d.ts.map +1 -1
- package/package.json +32 -7
- package/config/benchmarks/executor-benchmark-matrix.json +0 -25
- package/config/mandatory-rules/debugger-trace-first.md +0 -5
- package/config/skills/using-specialists/evals/evals.json +0 -68
- package/config/skills/using-specialists-v3/evals/evals.json +0 -89
package/dist/index.js
CHANGED
|
@@ -17571,7 +17571,8 @@ var init_schema = __esm(() => {
|
|
|
17571
17571
|
extensions: objectType({
|
|
17572
17572
|
serena: booleanType().optional(),
|
|
17573
17573
|
gitnexus: booleanType().optional()
|
|
17574
|
-
}).passthrough().optional()
|
|
17574
|
+
}).passthrough().optional(),
|
|
17575
|
+
expected_output_keys: arrayType(stringType()).optional()
|
|
17575
17576
|
}).passthrough();
|
|
17576
17577
|
PromptSchema2 = objectType({
|
|
17577
17578
|
system: stringType().optional(),
|
|
@@ -17801,6 +17802,22 @@ var init_loader = __esm(() => {
|
|
|
17801
17802
|
init_canonical_asset_resolver();
|
|
17802
17803
|
});
|
|
17803
17804
|
|
|
17805
|
+
// src/specialist/job-file-output.ts
|
|
17806
|
+
function normalizeMode(value) {
|
|
17807
|
+
return (value ?? "").trim().toLowerCase();
|
|
17808
|
+
}
|
|
17809
|
+
function detectJobFileOutputMode(env = process.env) {
|
|
17810
|
+
const normalized = normalizeMode(env.SPECIALISTS_JOB_FILE_OUTPUT);
|
|
17811
|
+
if (normalized === "on" || normalized === "1" || normalized === "true")
|
|
17812
|
+
return "on";
|
|
17813
|
+
if (normalized === "off" || normalized === "0" || normalized === "false")
|
|
17814
|
+
return "off";
|
|
17815
|
+
return "off";
|
|
17816
|
+
}
|
|
17817
|
+
function isJobFileOutputEnabled(env = process.env) {
|
|
17818
|
+
return detectJobFileOutputMode(env) === "on";
|
|
17819
|
+
}
|
|
17820
|
+
|
|
17804
17821
|
// src/specialist/templateEngine.ts
|
|
17805
17822
|
function renderTemplate(template, variables) {
|
|
17806
17823
|
return template.replace(/\$([a-zA-Z_][a-zA-Z0-9_]*)/g, (match, key) => {
|
|
@@ -18019,12 +18036,21 @@ import { isAbsolute, resolve, sep, join as join2, dirname } from "path";
|
|
|
18019
18036
|
function loadSharedToolCatalogIndex() {
|
|
18020
18037
|
if (cachedToolCatalogIndex)
|
|
18021
18038
|
return cachedToolCatalogIndex;
|
|
18039
|
+
const overridePath = resolve(process.cwd(), ".specialists", "catalog", "index.json");
|
|
18022
18040
|
try {
|
|
18023
|
-
|
|
18024
|
-
cachedToolCatalogIndex = loadToolCatalogIndex(readFileSync(indexPath, "utf8"));
|
|
18041
|
+
cachedToolCatalogIndex = loadToolCatalogIndex(readFileSync(overridePath, "utf8"));
|
|
18025
18042
|
return cachedToolCatalogIndex;
|
|
18026
18043
|
} catch {
|
|
18027
|
-
|
|
18044
|
+
try {
|
|
18045
|
+
const canonicalDir = resolveCanonicalAssetDir("catalog");
|
|
18046
|
+
if (!canonicalDir)
|
|
18047
|
+
return;
|
|
18048
|
+
const canonicalPath = resolve(canonicalDir, "index.json");
|
|
18049
|
+
cachedToolCatalogIndex = loadToolCatalogIndex(readFileSync(canonicalPath, "utf8"));
|
|
18050
|
+
return cachedToolCatalogIndex;
|
|
18051
|
+
} catch {
|
|
18052
|
+
return;
|
|
18053
|
+
}
|
|
18028
18054
|
}
|
|
18029
18055
|
}
|
|
18030
18056
|
function probeExtensionHealth(packageName) {
|
|
@@ -18370,6 +18396,10 @@ class PiAgentSession {
|
|
|
18370
18396
|
"--no-extensions",
|
|
18371
18397
|
...providerArgs,
|
|
18372
18398
|
"--no-session",
|
|
18399
|
+
"--offline",
|
|
18400
|
+
"--no-context-files",
|
|
18401
|
+
"--no-prompt-templates",
|
|
18402
|
+
"--no-themes",
|
|
18373
18403
|
...extraArgs
|
|
18374
18404
|
];
|
|
18375
18405
|
const toolsFlag = resolvePermissionTools({
|
|
@@ -18429,7 +18459,8 @@ class PiAgentSession {
|
|
|
18429
18459
|
this.proc = spawn("pi", args, {
|
|
18430
18460
|
stdio: ["pipe", "pipe", "pipe"],
|
|
18431
18461
|
cwd: sessionCwd,
|
|
18432
|
-
env: worktreeBoundary ? { ...baseEnv, [WORKTREE_BOUNDARY_ENV_KEY]: worktreeBoundary } : baseEnv
|
|
18462
|
+
env: worktreeBoundary ? { ...baseEnv, [WORKTREE_BOUNDARY_ENV_KEY]: worktreeBoundary } : baseEnv,
|
|
18463
|
+
detached: true
|
|
18433
18464
|
});
|
|
18434
18465
|
const donePromise = new Promise((resolve2, reject) => {
|
|
18435
18466
|
this._doneResolve = resolve2;
|
|
@@ -18854,14 +18885,17 @@ class PiAgentSession {
|
|
|
18854
18885
|
this._clearStallTimer();
|
|
18855
18886
|
this.proc?.stdin?.end();
|
|
18856
18887
|
if (this.proc) {
|
|
18888
|
+
const proc = this.proc;
|
|
18857
18889
|
await new Promise((resolve2) => {
|
|
18858
|
-
|
|
18890
|
+
proc.on("close", () => resolve2());
|
|
18859
18891
|
setTimeout(() => {
|
|
18860
|
-
if (
|
|
18861
|
-
|
|
18892
|
+
if (proc.exitCode === null && proc.pid != null) {
|
|
18893
|
+
try {
|
|
18894
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
18895
|
+
} catch {}
|
|
18862
18896
|
}
|
|
18863
18897
|
resolve2();
|
|
18864
|
-
},
|
|
18898
|
+
}, 8000);
|
|
18865
18899
|
});
|
|
18866
18900
|
}
|
|
18867
18901
|
}
|
|
@@ -18882,8 +18916,17 @@ class PiAgentSession {
|
|
|
18882
18916
|
entry.reject(killError);
|
|
18883
18917
|
}
|
|
18884
18918
|
this._pendingRequests.clear();
|
|
18885
|
-
this.proc
|
|
18919
|
+
const proc = this.proc;
|
|
18886
18920
|
this.proc = undefined;
|
|
18921
|
+
proc?.kill();
|
|
18922
|
+
const pid = proc?.pid;
|
|
18923
|
+
if (pid != null) {
|
|
18924
|
+
setTimeout(() => {
|
|
18925
|
+
try {
|
|
18926
|
+
process.kill(-pid, "SIGKILL");
|
|
18927
|
+
} catch {}
|
|
18928
|
+
}, 8000).unref();
|
|
18929
|
+
}
|
|
18887
18930
|
this._doneReject?.(killError);
|
|
18888
18931
|
}
|
|
18889
18932
|
getStderr() {
|
|
@@ -18919,6 +18962,7 @@ class PiAgentSession {
|
|
|
18919
18962
|
var SessionKilledError, StallTimeoutError, TEST_COMMAND_STALL_TIMEOUT_MS = 300000, GITNEXUS_IMPACT_STALL_TIMEOUT_MS = 300000, TEST_COMMAND_PATTERNS, cachedToolCatalogIndex, WRITE_BOUNDARY_TOOL_NAMES, WORKTREE_BOUNDARY_ENV_KEY = "SPECIALISTS_WORKTREE_BOUNDARY";
|
|
18920
18963
|
var init_session = __esm(() => {
|
|
18921
18964
|
init_backendMap();
|
|
18965
|
+
init_canonical_asset_resolver();
|
|
18922
18966
|
init_manifest_resolver();
|
|
18923
18967
|
init_tool_catalog();
|
|
18924
18968
|
SessionKilledError = class SessionKilledError extends Error {
|
|
@@ -19877,22 +19921,25 @@ class SqliteClient {
|
|
|
19877
19921
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
19878
19922
|
`, [jobId, seq, specialist, beadId ?? null, event.t, event.type, eventJson]);
|
|
19879
19923
|
}
|
|
19924
|
+
findActiveJob(beadId, specialist) {
|
|
19925
|
+
return this.db.query(`
|
|
19926
|
+
SELECT
|
|
19927
|
+
job_id,
|
|
19928
|
+
status,
|
|
19929
|
+
updated_at_ms,
|
|
19930
|
+
CAST(JSON_EXTRACT(status_json, '$.pid') AS INTEGER) AS pid
|
|
19931
|
+
FROM specialist_jobs
|
|
19932
|
+
WHERE bead_id = ?
|
|
19933
|
+
AND specialist = ?
|
|
19934
|
+
AND status IN ('starting', 'running', 'waiting')
|
|
19935
|
+
ORDER BY updated_at_ms DESC
|
|
19936
|
+
LIMIT 1
|
|
19937
|
+
`).get(beadId, specialist);
|
|
19938
|
+
}
|
|
19880
19939
|
claimJobStart(status, event) {
|
|
19881
19940
|
return claimJobStartWithStore({
|
|
19882
19941
|
transaction: (callback) => this.db.transaction(callback)(),
|
|
19883
|
-
findActiveJob: (beadId, specialist) => this.
|
|
19884
|
-
SELECT
|
|
19885
|
-
job_id,
|
|
19886
|
-
status,
|
|
19887
|
-
updated_at_ms,
|
|
19888
|
-
CAST(JSON_EXTRACT(status_json, '$.pid') AS INTEGER) AS pid
|
|
19889
|
-
FROM specialist_jobs
|
|
19890
|
-
WHERE bead_id = ?
|
|
19891
|
-
AND specialist = ?
|
|
19892
|
-
AND status IN ('starting', 'running')
|
|
19893
|
-
ORDER BY updated_at_ms DESC
|
|
19894
|
-
LIMIT 1
|
|
19895
|
-
`).get(beadId, specialist),
|
|
19942
|
+
findActiveJob: (beadId, specialist) => this.findActiveJob(beadId, specialist),
|
|
19896
19943
|
writeStatusRow: (nextStatus) => this.writeStatusRow(nextStatus),
|
|
19897
19944
|
writeEventRow: (jobId, specialist, beadId, nextEvent) => this.writeEventRow(jobId, specialist, beadId, nextEvent),
|
|
19898
19945
|
cancelStaleClaim: (jobId) => {
|
|
@@ -20097,6 +20144,21 @@ class SqliteClient {
|
|
|
20097
20144
|
this.writeStatusRow(status);
|
|
20098
20145
|
}, "upsertStatus");
|
|
20099
20146
|
}
|
|
20147
|
+
markSpecialistJobCancelled(jobId, reason) {
|
|
20148
|
+
withRetry(() => {
|
|
20149
|
+
const transaction = this.db.transaction(() => {
|
|
20150
|
+
const nowMs = Date.now();
|
|
20151
|
+
this.db.run(`
|
|
20152
|
+
UPDATE specialist_jobs
|
|
20153
|
+
SET status = 'cancelled',
|
|
20154
|
+
status_json = JSON_PATCH(status_json, JSON_OBJECT('status', 'cancelled', 'cancelled_reason', ?)),
|
|
20155
|
+
updated_at_ms = ?
|
|
20156
|
+
WHERE job_id = ?
|
|
20157
|
+
`, [reason, nowMs, jobId]);
|
|
20158
|
+
});
|
|
20159
|
+
transaction();
|
|
20160
|
+
}, "markSpecialistJobCancelled");
|
|
20161
|
+
}
|
|
20100
20162
|
upsertEpicRun(epic) {
|
|
20101
20163
|
withRetry(() => {
|
|
20102
20164
|
this.writeEpicRunRow(epic);
|
|
@@ -20597,6 +20659,16 @@ class SqliteClient {
|
|
|
20597
20659
|
}
|
|
20598
20660
|
}, "readLatestToolEvent");
|
|
20599
20661
|
}
|
|
20662
|
+
getLastActivityTimestampMs(jobId) {
|
|
20663
|
+
return withRetry(() => {
|
|
20664
|
+
const row = this.db.query(`
|
|
20665
|
+
SELECT MAX(t) AS last_activity_ms
|
|
20666
|
+
FROM specialist_events
|
|
20667
|
+
WHERE job_id = ? AND type IN ('tool', 'think')
|
|
20668
|
+
`).get(jobId);
|
|
20669
|
+
return typeof row?.last_activity_ms === "number" ? row.last_activity_ms : null;
|
|
20670
|
+
}, "getLastActivityTimestampMs");
|
|
20671
|
+
}
|
|
20600
20672
|
aggregateJobMetrics(jobId) {
|
|
20601
20673
|
return withRetry(() => {
|
|
20602
20674
|
const jobRow = this.db.query(`
|
|
@@ -21901,6 +21973,40 @@ function runScript(command, cwd) {
|
|
|
21901
21973
|
return { name: scriptName, output: e.stdout ?? e.message ?? "", exitCode: e.status ?? 1 };
|
|
21902
21974
|
}
|
|
21903
21975
|
}
|
|
21976
|
+
function buildReviewedGitnexusSummary(opts) {
|
|
21977
|
+
const jobId = opts.reusedFromJobId?.trim();
|
|
21978
|
+
if (!jobId)
|
|
21979
|
+
return "";
|
|
21980
|
+
try {
|
|
21981
|
+
const client = createObservabilitySqliteClient(opts.cwd);
|
|
21982
|
+
if (!client)
|
|
21983
|
+
return "";
|
|
21984
|
+
const events = client.readEvents(jobId);
|
|
21985
|
+
let runComplete = null;
|
|
21986
|
+
for (let i = events.length - 1;i >= 0; i -= 1) {
|
|
21987
|
+
const ev = events[i];
|
|
21988
|
+
if (ev?.type === "run_complete") {
|
|
21989
|
+
runComplete = ev;
|
|
21990
|
+
break;
|
|
21991
|
+
}
|
|
21992
|
+
}
|
|
21993
|
+
if (!runComplete?.gitnexus_summary)
|
|
21994
|
+
return "";
|
|
21995
|
+
const gs = runComplete.gitnexus_summary;
|
|
21996
|
+
const files = (gs.files_touched ?? []).slice(0, 20).join(", ") || "(none)";
|
|
21997
|
+
const symbols = (gs.symbols_analyzed ?? []).slice(0, 20).join(", ") || "(none)";
|
|
21998
|
+
const risk = gs.highest_risk ?? "UNKNOWN";
|
|
21999
|
+
const invocations = gs.tool_invocations ?? 0;
|
|
22000
|
+
return `<gitnexus_summary reviewed_job_id="${jobId}">
|
|
22001
|
+
files_touched: ${files}
|
|
22002
|
+
symbols_analyzed: ${symbols}
|
|
22003
|
+
highest_risk: ${risk}
|
|
22004
|
+
tool_invocations: ${invocations}
|
|
22005
|
+
</gitnexus_summary>`;
|
|
22006
|
+
} catch {
|
|
22007
|
+
return "";
|
|
22008
|
+
}
|
|
22009
|
+
}
|
|
21904
22010
|
function formatScriptOutput(results) {
|
|
21905
22011
|
const withOutput = results.filter((r) => r.output.trim());
|
|
21906
22012
|
if (withOutput.length === 0)
|
|
@@ -21972,7 +22078,7 @@ function validateBeforeRun(spec, permissionLevel) {
|
|
|
21972
22078
|
}
|
|
21973
22079
|
} else {
|
|
21974
22080
|
const binary = run.split(" ")[0];
|
|
21975
|
-
if (!commandExists(binary)) {
|
|
22081
|
+
if (binary && !SHELL_BUILTINS.has(binary) && !commandExists(binary)) {
|
|
21976
22082
|
errors5.push(` \u2717 skills.scripts: command not found on PATH: ${binary}`);
|
|
21977
22083
|
}
|
|
21978
22084
|
}
|
|
@@ -22394,6 +22500,13 @@ ${buildBeadBoundaryInstruction(runCwd, options.worktreeBoundary)}`.trim() : this
|
|
|
22394
22500
|
...options.reusedFromJobId ? { reused_from_job_id: options.reusedFromJobId } : {},
|
|
22395
22501
|
...options.worktreeOwnerJobId ? { worktree_owner_job_id: options.worktreeOwnerJobId } : {}
|
|
22396
22502
|
};
|
|
22503
|
+
const gitnexusSummary = buildReviewedGitnexusSummary({
|
|
22504
|
+
reusedFromJobId: options.reusedFromJobId,
|
|
22505
|
+
cwd: runCwd
|
|
22506
|
+
});
|
|
22507
|
+
if (gitnexusSummary) {
|
|
22508
|
+
lineageVariables.gitnexus_summary = gitnexusSummary;
|
|
22509
|
+
}
|
|
22397
22510
|
const beadTemplateVariables = {
|
|
22398
22511
|
prompt: resolvedPrompt,
|
|
22399
22512
|
bead_id: options.inputBeadId ?? "",
|
|
@@ -22806,7 +22919,7 @@ ${outputContractWarnings.map((msg) => ` \u26A0 ${msg}`).join(`
|
|
|
22806
22919
|
`)}
|
|
22807
22920
|
`);
|
|
22808
22921
|
}
|
|
22809
|
-
if (output_file) {
|
|
22922
|
+
if (output_file && isJobFileOutputEnabled()) {
|
|
22810
22923
|
await writeFile(output_file, output, "utf-8").catch(() => {});
|
|
22811
22924
|
}
|
|
22812
22925
|
await hooks.emit("post_execute", invocationId, metadata.name, metadata.version, {
|
|
@@ -22848,13 +22961,42 @@ ${outputContractWarnings.map((msg) => ` \u26A0 ${msg}`).join(`
|
|
|
22848
22961
|
return jobId;
|
|
22849
22962
|
}
|
|
22850
22963
|
}
|
|
22851
|
-
var PERMISSION_GATED_TOOLS, RETRY_BASE_DELAY_MS = 1000, RETRY_MAX_JITTER = 0.2, BASE_OUTPUT_SCHEMA, IMPACT_REPORT_SCHEMA, OUTPUT_TYPE_SCHEMA_EXTENSIONS, OUTPUT_TYPE_GUIDANCE;
|
|
22964
|
+
var SHELL_BUILTINS, PERMISSION_GATED_TOOLS, RETRY_BASE_DELAY_MS = 1000, RETRY_MAX_JITTER = 0.2, BASE_OUTPUT_SCHEMA, IMPACT_REPORT_SCHEMA, OUTPUT_TYPE_SCHEMA_EXTENSIONS, OUTPUT_TYPE_GUIDANCE;
|
|
22852
22965
|
var init_runner = __esm(() => {
|
|
22853
22966
|
init_session();
|
|
22854
22967
|
init_circuitBreaker();
|
|
22855
22968
|
init_mandatory_rules();
|
|
22969
|
+
init_observability_sqlite();
|
|
22856
22970
|
init_beads();
|
|
22857
22971
|
init_memory_retrieval();
|
|
22972
|
+
SHELL_BUILTINS = new Set([
|
|
22973
|
+
"if",
|
|
22974
|
+
"then",
|
|
22975
|
+
"else",
|
|
22976
|
+
"elif",
|
|
22977
|
+
"fi",
|
|
22978
|
+
"for",
|
|
22979
|
+
"while",
|
|
22980
|
+
"until",
|
|
22981
|
+
"do",
|
|
22982
|
+
"done",
|
|
22983
|
+
"case",
|
|
22984
|
+
"esac",
|
|
22985
|
+
"select",
|
|
22986
|
+
"in",
|
|
22987
|
+
"function",
|
|
22988
|
+
"return",
|
|
22989
|
+
"break",
|
|
22990
|
+
"continue",
|
|
22991
|
+
":",
|
|
22992
|
+
".",
|
|
22993
|
+
"true",
|
|
22994
|
+
"false",
|
|
22995
|
+
"[",
|
|
22996
|
+
"[[",
|
|
22997
|
+
"{",
|
|
22998
|
+
"("
|
|
22999
|
+
]);
|
|
22858
23000
|
PERMISSION_GATED_TOOLS = {
|
|
22859
23001
|
bash: ["LOW", "MEDIUM", "HIGH"],
|
|
22860
23002
|
edit: ["MEDIUM", "HIGH"],
|
|
@@ -23355,6 +23497,7 @@ var RULE_TIERS, SPEC_TIERS;
|
|
|
23355
23497
|
var init_list_rules = __esm(() => {
|
|
23356
23498
|
init_mandatory_rules();
|
|
23357
23499
|
RULE_TIERS = [
|
|
23500
|
+
{ rel: ".specialists/user/mandatory-rules", tier: "user" },
|
|
23358
23501
|
{ rel: ".specialists/mandatory-rules", tier: "overlay" },
|
|
23359
23502
|
{ rel: ".specialists/default/mandatory-rules", tier: "default" },
|
|
23360
23503
|
{ rel: "config/mandatory-rules", tier: "config" }
|
|
@@ -23366,22 +23509,6 @@ var init_list_rules = __esm(() => {
|
|
|
23366
23509
|
];
|
|
23367
23510
|
});
|
|
23368
23511
|
|
|
23369
|
-
// src/specialist/job-file-output.ts
|
|
23370
|
-
function normalizeMode(value) {
|
|
23371
|
-
return (value ?? "").trim().toLowerCase();
|
|
23372
|
-
}
|
|
23373
|
-
function detectJobFileOutputMode(env = process.env) {
|
|
23374
|
-
const normalized = normalizeMode(env.SPECIALISTS_JOB_FILE_OUTPUT);
|
|
23375
|
-
if (normalized === "on" || normalized === "1" || normalized === "true")
|
|
23376
|
-
return "on";
|
|
23377
|
-
if (normalized === "off" || normalized === "0" || normalized === "false")
|
|
23378
|
-
return "off";
|
|
23379
|
-
return "off";
|
|
23380
|
-
}
|
|
23381
|
-
function isJobFileOutputEnabled(env = process.env) {
|
|
23382
|
-
return detectJobFileOutputMode(env) === "on";
|
|
23383
|
-
}
|
|
23384
|
-
|
|
23385
23512
|
// src/specialist/timeline-events.ts
|
|
23386
23513
|
function summarizeToolResult(resultContent) {
|
|
23387
23514
|
if (!resultContent)
|
|
@@ -23746,25 +23873,24 @@ function resolveChainId(status) {
|
|
|
23746
23873
|
return;
|
|
23747
23874
|
}
|
|
23748
23875
|
function evaluateEpicMergeReadiness(input) {
|
|
23749
|
-
const isEligibleState = input.epicStatus === "merge_ready";
|
|
23750
23876
|
const blockingChains = input.chainStatuses.filter((chain) => chain.hasRunningJob).map((chain) => chain.chainId);
|
|
23751
|
-
const isReady =
|
|
23752
|
-
if (!
|
|
23877
|
+
const isReady = blockingChains.length === 0;
|
|
23878
|
+
if (!isReady) {
|
|
23753
23879
|
return {
|
|
23754
23880
|
epicId: input.epicId,
|
|
23755
23881
|
epicStatus: input.epicStatus,
|
|
23756
23882
|
isReady,
|
|
23757
23883
|
blockingChains,
|
|
23758
|
-
summary: `Epic ${input.epicId} is ${
|
|
23884
|
+
summary: `Epic ${input.epicId} is blocked by active chains: ${blockingChains.join(", ")}.`
|
|
23759
23885
|
};
|
|
23760
23886
|
}
|
|
23761
|
-
if (
|
|
23887
|
+
if (input.epicStatus === "merged" || input.epicStatus === "abandoned") {
|
|
23762
23888
|
return {
|
|
23763
23889
|
epicId: input.epicId,
|
|
23764
23890
|
epicStatus: input.epicStatus,
|
|
23765
23891
|
isReady,
|
|
23766
23892
|
blockingChains,
|
|
23767
|
-
summary: `Epic ${input.epicId} is
|
|
23893
|
+
summary: `Epic ${input.epicId} is ${input.epicStatus}; live chains are terminal.`
|
|
23768
23894
|
};
|
|
23769
23895
|
}
|
|
23770
23896
|
return {
|
|
@@ -23772,7 +23898,7 @@ function evaluateEpicMergeReadiness(input) {
|
|
|
23772
23898
|
epicStatus: input.epicStatus,
|
|
23773
23899
|
isReady,
|
|
23774
23900
|
blockingChains,
|
|
23775
|
-
summary: `Epic ${input.epicId} is
|
|
23901
|
+
summary: `Epic ${input.epicId} is live-ready and all chains are terminal.`
|
|
23776
23902
|
};
|
|
23777
23903
|
}
|
|
23778
23904
|
function appendEpicTransitionAudit(statusJson, entry) {
|
|
@@ -23796,9 +23922,6 @@ function appendEpicTransitionAudit(statusJson, entry) {
|
|
|
23796
23922
|
transitions: [...previous, entry]
|
|
23797
23923
|
});
|
|
23798
23924
|
}
|
|
23799
|
-
function summarizeEpicTransition(epicId, from, to) {
|
|
23800
|
-
return `Epic ${epicId}: ${from} -> ${to}`;
|
|
23801
|
-
}
|
|
23802
23925
|
var EPIC_TERMINAL_STATES, VALID_EPIC_TRANSITIONS;
|
|
23803
23926
|
var init_epic_lifecycle = __esm(() => {
|
|
23804
23927
|
EPIC_TERMINAL_STATES = ["merged", "failed", "abandoned"];
|
|
@@ -23990,79 +24113,55 @@ function evaluatePrepReadiness(prepJobs) {
|
|
|
23990
24113
|
blocker_job_ids: [...running, ...failed].map((job) => job.id)
|
|
23991
24114
|
};
|
|
23992
24115
|
}
|
|
23993
|
-
function
|
|
23994
|
-
|
|
23995
|
-
return "merged";
|
|
23996
|
-
if (persistedState === "abandoned")
|
|
23997
|
-
return "abandoned";
|
|
23998
|
-
const hasBlockingPrep = prep.running > 0;
|
|
23999
|
-
const hasFailedPrep = prep.failed > 0;
|
|
24116
|
+
function deriveEpicReadinessState(prep, chains) {
|
|
24117
|
+
const hasActivePrep = prep.running > 0;
|
|
24000
24118
|
const hasPendingChain = chains.some((chain) => chain.state === "pending");
|
|
24001
24119
|
const hasBlockedChain = chains.some((chain) => chain.state === "blocked");
|
|
24120
|
+
const hasFailedPrep = prep.failed > 0;
|
|
24002
24121
|
const hasFailedChain = chains.some((chain) => chain.state === "failed");
|
|
24003
24122
|
const allChainsPass = chains.length === 0 || chains.every((chain) => chain.state === "pass");
|
|
24123
|
+
if (hasActivePrep || hasPendingChain)
|
|
24124
|
+
return "resolving";
|
|
24004
24125
|
if (hasFailedPrep || hasFailedChain)
|
|
24005
24126
|
return "failed";
|
|
24006
|
-
if (persistedState === "failed" && allChainsPass)
|
|
24007
|
-
return "merge_ready";
|
|
24008
|
-
if (persistedState === "failed")
|
|
24009
|
-
return "failed";
|
|
24010
|
-
if (hasBlockingPrep || hasPendingChain)
|
|
24011
|
-
return persistedState === "resolving" ? "resolving" : "unresolved";
|
|
24012
24127
|
if (hasBlockedChain)
|
|
24013
|
-
return
|
|
24128
|
+
return "blocked";
|
|
24014
24129
|
if (allChainsPass)
|
|
24015
24130
|
return "merge_ready";
|
|
24016
24131
|
return "blocked";
|
|
24017
24132
|
}
|
|
24018
|
-
function
|
|
24133
|
+
function deriveEpicNextState(persistedState, readinessState) {
|
|
24019
24134
|
if (persistedState === "merged" || persistedState === "abandoned")
|
|
24020
24135
|
return persistedState;
|
|
24021
|
-
if (readinessState === "
|
|
24022
|
-
|
|
24023
|
-
|
|
24024
|
-
|
|
24025
|
-
|
|
24026
|
-
return transitionEpicState("resolving", "failed");
|
|
24027
|
-
}
|
|
24028
|
-
return persistedState;
|
|
24029
|
-
}
|
|
24030
|
-
if (readinessState === "merge_ready") {
|
|
24031
|
-
if (persistedState === "failed")
|
|
24032
|
-
return "merge_ready";
|
|
24033
|
-
if (persistedState === "open")
|
|
24034
|
-
return transitionEpicState("open", "resolving");
|
|
24035
|
-
if (persistedState === "resolving")
|
|
24036
|
-
return transitionEpicState("resolving", "merge_ready");
|
|
24037
|
-
return persistedState;
|
|
24038
|
-
}
|
|
24039
|
-
if (persistedState === "open" && (readinessState === "unresolved" || readinessState === "resolving" || readinessState === "blocked")) {
|
|
24040
|
-
return transitionEpicState("open", "resolving");
|
|
24041
|
-
}
|
|
24042
|
-
if (persistedState === "merge_ready" && (readinessState === "unresolved" || readinessState === "resolving" || readinessState === "blocked")) {
|
|
24043
|
-
return transitionEpicState("merge_ready", "resolving");
|
|
24044
|
-
}
|
|
24045
|
-
return persistedState;
|
|
24136
|
+
if (readinessState === "merge_ready")
|
|
24137
|
+
return "merge_ready";
|
|
24138
|
+
if (readinessState === "failed")
|
|
24139
|
+
return "failed";
|
|
24140
|
+
return "resolving";
|
|
24046
24141
|
}
|
|
24047
|
-
function buildSummaryLine(epicId, readinessState, prep, chains) {
|
|
24142
|
+
function buildSummaryLine(epicId, persistedState, readinessState, prep, chains) {
|
|
24048
24143
|
const chainPass = chains.filter((chain) => chain.state === "pass").length;
|
|
24049
24144
|
const chainTotal = chains.length;
|
|
24050
|
-
const blockedChains = chains.filter((chain) => chain.state === "blocked" || chain.state === "pending").map((chain) => chain.chain_id);
|
|
24145
|
+
const blockedChains = chains.filter((chain) => chain.state === "blocked" || chain.state === "pending" || chain.state === "failed").map((chain) => chain.chain_id);
|
|
24146
|
+
const failedChains = chains.filter((chain) => chain.state === "failed").map((chain) => chain.chain_id);
|
|
24051
24147
|
const prepSegment = `prep done=${prep.done}/${prep.total} running=${prep.running} failed=${prep.failed}`;
|
|
24052
24148
|
const chainSegment = `chains pass=${chainPass}/${chainTotal}`;
|
|
24053
|
-
|
|
24054
|
-
|
|
24055
|
-
|
|
24056
|
-
|
|
24149
|
+
const stateSegment = persistedState === readinessState ? readinessState : `${readinessState} (stored=${persistedState})`;
|
|
24150
|
+
const segments = [prepSegment, chainSegment];
|
|
24151
|
+
if (blockedChains.length > 0)
|
|
24152
|
+
segments.push(`blocked=${blockedChains.join(", ")}`);
|
|
24153
|
+
if (failedChains.length > 0)
|
|
24154
|
+
segments.push(`failed=${failedChains.join(", ")}`);
|
|
24155
|
+
return `Epic ${epicId}: ${stateSegment} (${segments.join("; ")})`;
|
|
24057
24156
|
}
|
|
24058
24157
|
function evaluateEpicReadinessSummary(input) {
|
|
24059
24158
|
const prep = evaluatePrepReadiness(input.prepJobs);
|
|
24060
24159
|
const chains = input.chainInputs.map((chain) => evaluateChainReadiness(chain.chain_id, chain.jobs, chain.chain_root_bead_id));
|
|
24061
|
-
const readinessState =
|
|
24062
|
-
const nextState =
|
|
24160
|
+
const readinessState = deriveEpicReadinessState(prep, chains);
|
|
24161
|
+
const nextState = deriveEpicNextState(input.persistedState, readinessState);
|
|
24063
24162
|
const blockers = [
|
|
24064
24163
|
...prep.blocker_job_ids.map((jobId) => `prep:${jobId}`),
|
|
24065
|
-
...chains.filter((chain) => chain.state === "pending" || chain.state === "blocked").map((chain) => `chain:${chain.chain_id}`)
|
|
24164
|
+
...chains.filter((chain) => chain.state === "pending" || chain.state === "blocked" || chain.state === "failed").map((chain) => `chain:${chain.chain_id}`)
|
|
24066
24165
|
];
|
|
24067
24166
|
return {
|
|
24068
24167
|
epic_id: input.epicId,
|
|
@@ -24073,7 +24172,7 @@ function evaluateEpicReadinessSummary(input) {
|
|
|
24073
24172
|
prep,
|
|
24074
24173
|
chains,
|
|
24075
24174
|
blockers,
|
|
24076
|
-
summary: buildSummaryLine(input.epicId, readinessState, prep, chains)
|
|
24175
|
+
summary: buildSummaryLine(input.epicId, input.persistedState, readinessState, prep, chains)
|
|
24077
24176
|
};
|
|
24078
24177
|
}
|
|
24079
24178
|
function loadEpicReadinessSummary(sqlite, epicId) {
|
|
@@ -24113,7 +24212,7 @@ function loadEpicReadinessSummary(sqlite, epicId) {
|
|
|
24113
24212
|
function syncEpicStateFromReadiness(sqlite, summary) {
|
|
24114
24213
|
const now = Date.now();
|
|
24115
24214
|
const existing = sqlite.readEpicRun(summary.epic_id);
|
|
24116
|
-
const
|
|
24215
|
+
const recoveredFromLegacyFailure = summary.persisted_state === "failed" && summary.readiness_state === "merge_ready";
|
|
24117
24216
|
const nextRecord = {
|
|
24118
24217
|
epic_id: summary.epic_id,
|
|
24119
24218
|
status: summary.next_state,
|
|
@@ -24128,21 +24227,20 @@ function syncEpicStateFromReadiness(sqlite, summary) {
|
|
|
24128
24227
|
chains: summary.chains,
|
|
24129
24228
|
summary: summary.summary,
|
|
24130
24229
|
evaluated_at_ms: now,
|
|
24131
|
-
note:
|
|
24230
|
+
note: recoveredFromLegacyFailure ? "derived readiness healed legacy failed row after live chains turned pass" : undefined
|
|
24132
24231
|
})
|
|
24133
24232
|
};
|
|
24134
|
-
if (summary.can_transition ||
|
|
24233
|
+
if (summary.can_transition || recoveredFromLegacyFailure || !existing) {
|
|
24135
24234
|
sqlite.upsertEpicRun(nextRecord);
|
|
24136
24235
|
}
|
|
24137
24236
|
return nextRecord;
|
|
24138
24237
|
}
|
|
24139
24238
|
var ACTIVE_JOB_STATUSES, TERMINAL_JOB_STATUSES, REVIEWER_VERDICT_REGEX;
|
|
24140
24239
|
var init_epic_readiness = __esm(() => {
|
|
24141
|
-
init_epic_lifecycle();
|
|
24142
24240
|
init_process_liveness();
|
|
24143
24241
|
ACTIVE_JOB_STATUSES = new Set(["starting", "running", "waiting"]);
|
|
24144
24242
|
TERMINAL_JOB_STATUSES = new Set(["done", "error"]);
|
|
24145
|
-
REVIEWER_VERDICT_REGEX = /Verdict:\s*(PASS|PARTIAL|FAIL)
|
|
24243
|
+
REVIEWER_VERDICT_REGEX = /Verdict:\s*\**\s*(PASS|PARTIAL|FAIL)\s*\**/i;
|
|
24146
24244
|
});
|
|
24147
24245
|
|
|
24148
24246
|
// src/specialist/chain-identity.ts
|
|
@@ -24419,8 +24517,20 @@ function runAutoCommitCheckpoint(options) {
|
|
|
24419
24517
|
return { status: "failed", reason: String(error2) };
|
|
24420
24518
|
}
|
|
24421
24519
|
}
|
|
24520
|
+
function gitnexusHasEmbeddings(cwd) {
|
|
24521
|
+
try {
|
|
24522
|
+
const metaPath = join9(cwd, ".gitnexus", "meta.json");
|
|
24523
|
+
const raw = readFileSync8(metaPath, "utf-8");
|
|
24524
|
+
const meta = JSON.parse(raw);
|
|
24525
|
+
return typeof meta.stats?.embeddings === "number" && meta.stats.embeddings > 0;
|
|
24526
|
+
} catch {
|
|
24527
|
+
return false;
|
|
24528
|
+
}
|
|
24529
|
+
}
|
|
24422
24530
|
function startDetachedGitnexusAnalyze(cwd) {
|
|
24423
|
-
const
|
|
24531
|
+
const baseArgs = ["gitnexus", "analyze", "--skip-agents-md", "--no-stats"];
|
|
24532
|
+
const args = gitnexusHasEmbeddings(cwd) ? [...baseArgs, "--embeddings"] : baseArgs;
|
|
24533
|
+
const child = spawn2("npx", args, {
|
|
24424
24534
|
cwd,
|
|
24425
24535
|
detached: true,
|
|
24426
24536
|
stdio: "ignore"
|
|
@@ -24707,6 +24817,55 @@ class Supervisor {
|
|
|
24707
24817
|
return [];
|
|
24708
24818
|
}
|
|
24709
24819
|
}
|
|
24820
|
+
listChainJobIds(chainId) {
|
|
24821
|
+
try {
|
|
24822
|
+
if (this.isDisposed) {
|
|
24823
|
+
throw this.createDisposedSqliteError("listChainJobIds");
|
|
24824
|
+
}
|
|
24825
|
+
return this.withSqliteOperation("listChainJobIds", (client) => client.listChainJobIds(chainId)) ?? [];
|
|
24826
|
+
} catch (error2) {
|
|
24827
|
+
console.warn(`[supervisor] SQLite listChainJobIds failed: ${String(error2)}`);
|
|
24828
|
+
return [];
|
|
24829
|
+
}
|
|
24830
|
+
}
|
|
24831
|
+
readResult(id) {
|
|
24832
|
+
try {
|
|
24833
|
+
if (this.isDisposed) {
|
|
24834
|
+
throw this.createDisposedSqliteError("readResult");
|
|
24835
|
+
}
|
|
24836
|
+
const sqliteResult = this.withSqliteOperation("readResult", (client) => client.readResult(id));
|
|
24837
|
+
if (sqliteResult)
|
|
24838
|
+
return sqliteResult;
|
|
24839
|
+
} catch (error2) {
|
|
24840
|
+
if (!(error2 instanceof Error && error2.message.includes("supervisor is disposed"))) {
|
|
24841
|
+
console.warn(`[supervisor] SQLite readResult failed, falling back to file state: ${String(error2)}`);
|
|
24842
|
+
}
|
|
24843
|
+
}
|
|
24844
|
+
const path = this.resultPath(id);
|
|
24845
|
+
if (!existsSync10(path))
|
|
24846
|
+
return null;
|
|
24847
|
+
try {
|
|
24848
|
+
return readFileSync8(path, "utf-8");
|
|
24849
|
+
} catch {
|
|
24850
|
+
return null;
|
|
24851
|
+
}
|
|
24852
|
+
}
|
|
24853
|
+
finalizeWaitingJob(id) {
|
|
24854
|
+
const currentStatus = this.readStatus(id);
|
|
24855
|
+
if (!currentStatus)
|
|
24856
|
+
return null;
|
|
24857
|
+
if (currentStatus.status !== "waiting")
|
|
24858
|
+
return currentStatus;
|
|
24859
|
+
if (currentStatus.fifo_path) {
|
|
24860
|
+
writeFileSync3(currentStatus.fifo_path, JSON.stringify({ type: "close" }) + `
|
|
24861
|
+
`, { flag: "a" });
|
|
24862
|
+
}
|
|
24863
|
+
const finalized = this.updateJobStatus(id, "done");
|
|
24864
|
+
if (!finalized)
|
|
24865
|
+
return null;
|
|
24866
|
+
this.aggregateJobMetricsBestEffort(id);
|
|
24867
|
+
return finalized;
|
|
24868
|
+
}
|
|
24710
24869
|
emitMetaEvent(jobId, model, backend) {
|
|
24711
24870
|
if (this.isDisposed)
|
|
24712
24871
|
return;
|
|
@@ -25165,8 +25324,24 @@ class Supervisor {
|
|
|
25165
25324
|
}));
|
|
25166
25325
|
};
|
|
25167
25326
|
const shouldAutoCloseReadOnlyKeepAlive = (output) => isReadOnlySpecialist && TERMINAL_COMPLIANCE_VERDICT_REGEX.test(output);
|
|
25327
|
+
const shouldAutoFinalizeKeepAlive = (output) => PASS_COMPLIANCE_VERDICT_REGEX.test(output);
|
|
25168
25328
|
const shouldWriteExternalBeadNotes = runOptions.beadsWriteNotes ?? true;
|
|
25169
25329
|
let skipFinalKeepAliveInputBeadAppend = false;
|
|
25330
|
+
let lastGitnexusAnalyzedSha;
|
|
25331
|
+
const triggerGitnexusAnalyzeIfNeeded = (sha, source) => {
|
|
25332
|
+
if (!isGitnexusAnalyzeRequired(runOptions.permissionRequired))
|
|
25333
|
+
return;
|
|
25334
|
+
if (sha && lastGitnexusAnalyzedSha === sha)
|
|
25335
|
+
return;
|
|
25336
|
+
try {
|
|
25337
|
+
startDetachedGitnexusAnalyze(runOptions.workingDirectory ?? process.cwd());
|
|
25338
|
+
appendTimelineEvent(createMetaEvent("gitnexus_analyze_started", source));
|
|
25339
|
+
if (sha)
|
|
25340
|
+
lastGitnexusAnalyzedSha = sha;
|
|
25341
|
+
} catch (err) {
|
|
25342
|
+
appendTimelineEvent(createMetaEvent("gitnexus_analyze_start_failed", `${source}: ${String(err?.message ?? err)}`));
|
|
25343
|
+
}
|
|
25344
|
+
};
|
|
25170
25345
|
const appendResultToInputBead = (params) => {
|
|
25171
25346
|
const inputBeadId = runOptions.inputBeadId;
|
|
25172
25347
|
const shouldAppendResultToInputBead = Boolean(shouldWriteExternalBeadNotes && inputBeadId && this.opts.beadsClient);
|
|
@@ -25189,12 +25364,14 @@ class Supervisor {
|
|
|
25189
25364
|
const appendError = `[bead-append-failed] ${appendResult.error ?? "Unknown error"}`;
|
|
25190
25365
|
appendTimelineEvent(createMetaEvent("bead_append_failed", appendError));
|
|
25191
25366
|
setStatus({ current_event: "bead_append_failed", last_event_at_ms: Date.now() });
|
|
25192
|
-
|
|
25193
|
-
|
|
25367
|
+
if (this.isJobFileOutputEnabled) {
|
|
25368
|
+
try {
|
|
25369
|
+
appendFileSync(this.resultPath(id), `
|
|
25194
25370
|
|
|
25195
25371
|
${appendError}
|
|
25196
25372
|
`, "utf-8");
|
|
25197
|
-
|
|
25373
|
+
} catch {}
|
|
25374
|
+
}
|
|
25198
25375
|
return false;
|
|
25199
25376
|
};
|
|
25200
25377
|
const applyAutoCommitCheckpoint = (target, autoCommitPolicy2) => {
|
|
@@ -25225,6 +25402,7 @@ ${appendError}
|
|
|
25225
25402
|
commit_sha: autoCommitResult.sha,
|
|
25226
25403
|
committed_files: autoCommitResult.files
|
|
25227
25404
|
}));
|
|
25405
|
+
triggerGitnexusAnalyzeIfNeeded(autoCommitResult.sha, "checkpoint");
|
|
25228
25406
|
};
|
|
25229
25407
|
const handleResumeTurn = async (task) => {
|
|
25230
25408
|
if (!resumeFn)
|
|
@@ -25236,8 +25414,10 @@ ${appendError}
|
|
|
25236
25414
|
try {
|
|
25237
25415
|
const output = await resumeFn(task);
|
|
25238
25416
|
latestOutput = output;
|
|
25239
|
-
|
|
25240
|
-
|
|
25417
|
+
if (this.isJobFileOutputEnabled) {
|
|
25418
|
+
mkdirSync4(this.jobDir(id), { recursive: true });
|
|
25419
|
+
writeFileSync3(this.resultPath(id), output, "utf-8");
|
|
25420
|
+
}
|
|
25241
25421
|
try {
|
|
25242
25422
|
this.withSqliteOperation("upsertResult:resume_turn", (client) => client.upsertResult(id, output));
|
|
25243
25423
|
} catch (error2) {
|
|
@@ -25249,7 +25429,9 @@ ${appendError}
|
|
|
25249
25429
|
beadId: statusSnapshot.bead_id,
|
|
25250
25430
|
output
|
|
25251
25431
|
});
|
|
25252
|
-
const
|
|
25432
|
+
const passFinalize = shouldAutoFinalizeKeepAlive(output);
|
|
25433
|
+
const readOnlyClose = shouldAutoCloseReadOnlyKeepAlive(output);
|
|
25434
|
+
const isWaitingTurn = !readOnlyClose && !passFinalize;
|
|
25253
25435
|
applyAutoCommitCheckpoint(isWaitingTurn ? "waiting" : "terminal", autoCommitPolicy);
|
|
25254
25436
|
appendResultToInputBead({
|
|
25255
25437
|
output,
|
|
@@ -25676,6 +25858,8 @@ ${appendError}
|
|
|
25676
25858
|
if (keepAliveSession) {
|
|
25677
25859
|
if (shouldAutoCloseReadOnlyKeepAlive(finalResult.output)) {
|
|
25678
25860
|
await closeKeepAliveSession();
|
|
25861
|
+
} else if (shouldAutoFinalizeKeepAlive(finalResult.output)) {
|
|
25862
|
+
await closeKeepAliveSession();
|
|
25679
25863
|
} else {
|
|
25680
25864
|
appendResultToInputBead({
|
|
25681
25865
|
output: finalResult.output,
|
|
@@ -25791,24 +25975,7 @@ ${appendError}
|
|
|
25791
25975
|
throw new Error("[supervisor] SQLite upsertStatusWithEventAndResult failed: database client unavailable");
|
|
25792
25976
|
}
|
|
25793
25977
|
this.aggregateJobMetricsBestEffort(id);
|
|
25794
|
-
|
|
25795
|
-
try {
|
|
25796
|
-
startDetachedGitnexusAnalyze(runOptions.workingDirectory ?? process.cwd());
|
|
25797
|
-
appendTimelineEventFileOnly({
|
|
25798
|
-
t: Date.now(),
|
|
25799
|
-
type: TIMELINE_EVENT_TYPES.META,
|
|
25800
|
-
model: "gitnexus_analyze_started",
|
|
25801
|
-
backend: "supervisor"
|
|
25802
|
-
});
|
|
25803
|
-
} catch (err) {
|
|
25804
|
-
appendTimelineEventFileOnly({
|
|
25805
|
-
t: Date.now(),
|
|
25806
|
-
type: TIMELINE_EVENT_TYPES.META,
|
|
25807
|
-
model: "gitnexus_analyze_start_failed",
|
|
25808
|
-
backend: String(err?.message ?? err)
|
|
25809
|
-
});
|
|
25810
|
-
}
|
|
25811
|
-
}
|
|
25978
|
+
triggerGitnexusAnalyzeIfNeeded(statusSnapshot.last_auto_commit_sha, "terminal");
|
|
25812
25979
|
this.writeReadyMarker(id);
|
|
25813
25980
|
return id;
|
|
25814
25981
|
} catch (err) {
|
|
@@ -25897,7 +26064,7 @@ ${appendError}
|
|
|
25897
26064
|
}
|
|
25898
26065
|
}
|
|
25899
26066
|
}
|
|
25900
|
-
var JOB_TTL_DAYS, STALL_DETECTION_DEFAULTS, GITNEXUS_RISK_ORDER, MODEL_CONTEXT_WINDOWS, TERMINAL_COMPLIANCE_VERDICT_REGEX, AUTO_COMMIT_NOISE_PREFIXES, STATUS_WATCHDOG_INTERVAL_MS = 5000, STATUS_WATCHDOG_STALE_AFTER_MS = 30000;
|
|
26067
|
+
var JOB_TTL_DAYS, STALL_DETECTION_DEFAULTS, GITNEXUS_RISK_ORDER, MODEL_CONTEXT_WINDOWS, TERMINAL_COMPLIANCE_VERDICT_REGEX, PASS_COMPLIANCE_VERDICT_REGEX, AUTO_COMMIT_NOISE_PREFIXES, STATUS_WATCHDOG_INTERVAL_MS = 5000, STATUS_WATCHDOG_STALE_AFTER_MS = 30000;
|
|
25901
26068
|
var init_supervisor = __esm(() => {
|
|
25902
26069
|
init_job_root();
|
|
25903
26070
|
init_timeline_events();
|
|
@@ -25925,8 +26092,9 @@ var init_supervisor = __esm(() => {
|
|
|
25925
26092
|
{ matcher: (model) => model.includes("qwen3.5") || model.includes("glm-5"), windowTokens: 128000 },
|
|
25926
26093
|
{ matcher: (model) => model.includes("claude"), windowTokens: 200000 }
|
|
25927
26094
|
];
|
|
25928
|
-
TERMINAL_COMPLIANCE_VERDICT_REGEX = /## Compliance Verdict[\s\S]*?- Verdict
|
|
25929
|
-
|
|
26095
|
+
TERMINAL_COMPLIANCE_VERDICT_REGEX = /## Compliance Verdict[\s\S]*?- Verdict:\s*\**\s*(PASS|PARTIAL|FAIL)\s*\**/i;
|
|
26096
|
+
PASS_COMPLIANCE_VERDICT_REGEX = /## Compliance Verdict[\s\S]*?- Verdict:\s*\**\s*PASS\s*\**/i;
|
|
26097
|
+
AUTO_COMMIT_NOISE_PREFIXES = [".xtrm/", ".wolf/", ".specialists/jobs/", ".beads/", ".pi/"];
|
|
25930
26098
|
});
|
|
25931
26099
|
|
|
25932
26100
|
// src/cli/list.ts
|
|
@@ -26652,8 +26820,20 @@ function assertXtrmPrerequisites(cwd) {
|
|
|
26652
26820
|
const hasXtCli = isInstalled("xt");
|
|
26653
26821
|
if (hasXtrmDir && hasXtCli)
|
|
26654
26822
|
return;
|
|
26655
|
-
|
|
26656
|
-
|
|
26823
|
+
if (!hasXtCli) {
|
|
26824
|
+
console.error("specialists init: missing xt CLI.");
|
|
26825
|
+
console.error("1. Install xtrm-tools globally: npm install -g xtrm-tools");
|
|
26826
|
+
console.error("2. Run xt install");
|
|
26827
|
+
console.error("3. Run xt init in this repo");
|
|
26828
|
+
console.error("4. Verify xt is available: xt --version");
|
|
26829
|
+
process.exit(1);
|
|
26830
|
+
}
|
|
26831
|
+
if (!hasXtrmDir) {
|
|
26832
|
+
console.error("specialists init: missing .xtrm/ in this repo.");
|
|
26833
|
+
console.error("1. Run xt init in this repo");
|
|
26834
|
+
console.error("2. Verify xt is available: xt --version");
|
|
26835
|
+
process.exit(1);
|
|
26836
|
+
}
|
|
26657
26837
|
}
|
|
26658
26838
|
function warnMissingOptionalPrerequisites() {
|
|
26659
26839
|
const optionalTools = [
|
|
@@ -27103,22 +27283,51 @@ function ensureObservabilityDb(cwd) {
|
|
|
27103
27283
|
}
|
|
27104
27284
|
ensureGitignoreHasObservabilityDbEntries(location.gitRoot);
|
|
27105
27285
|
}
|
|
27286
|
+
function extractSpecialistsBlockSpan(existing) {
|
|
27287
|
+
const start = existing.indexOf("<!-- specialists:start -->");
|
|
27288
|
+
if (start === -1)
|
|
27289
|
+
return null;
|
|
27290
|
+
const endMarker = "<!-- specialists:end -->";
|
|
27291
|
+
const endMarkerIndex = existing.indexOf(endMarker, start);
|
|
27292
|
+
if (endMarkerIndex === -1)
|
|
27293
|
+
return null;
|
|
27294
|
+
return { start, end: endMarkerIndex + endMarker.length };
|
|
27295
|
+
}
|
|
27106
27296
|
function ensureAgentsMd(cwd) {
|
|
27107
27297
|
const agentsPath = join11(cwd, "AGENTS.md");
|
|
27108
|
-
if (existsSync12(agentsPath)) {
|
|
27109
|
-
const existing = readFileSync10(agentsPath, "utf-8");
|
|
27110
|
-
if (existing.includes(AGENTS_MARKER)) {
|
|
27111
|
-
skip("AGENTS.md already has Specialists section");
|
|
27112
|
-
} else {
|
|
27113
|
-
writeFileSync4(agentsPath, existing.trimEnd() + `
|
|
27114
|
-
|
|
27115
|
-
` + AGENTS_BLOCK, "utf-8");
|
|
27116
|
-
ok("appended Specialists section to AGENTS.md");
|
|
27117
|
-
}
|
|
27118
|
-
} else {
|
|
27298
|
+
if (!existsSync12(agentsPath)) {
|
|
27119
27299
|
writeFileSync4(agentsPath, AGENTS_BLOCK, "utf-8");
|
|
27120
27300
|
ok("created AGENTS.md with Specialists section");
|
|
27301
|
+
return;
|
|
27121
27302
|
}
|
|
27303
|
+
const existing = readFileSync10(agentsPath, "utf-8");
|
|
27304
|
+
const span = extractSpecialistsBlockSpan(existing);
|
|
27305
|
+
if (span) {
|
|
27306
|
+
const next = existing.slice(0, span.start) + AGENTS_BLOCK + existing.slice(span.end);
|
|
27307
|
+
if (next === existing) {
|
|
27308
|
+
skip("AGENTS.md already has Specialists section");
|
|
27309
|
+
return;
|
|
27310
|
+
}
|
|
27311
|
+
writeFileSync4(agentsPath, next, "utf-8");
|
|
27312
|
+
ok("updated Specialists section in AGENTS.md");
|
|
27313
|
+
return;
|
|
27314
|
+
}
|
|
27315
|
+
if (existing.includes(AGENTS_MARKER)) {
|
|
27316
|
+
const markerIndex = existing.indexOf(AGENTS_MARKER);
|
|
27317
|
+
const nextH2Match = /^## /m.exec(existing.slice(markerIndex + AGENTS_MARKER.length));
|
|
27318
|
+
const nextH2Index = nextH2Match ? markerIndex + AGENTS_MARKER.length + nextH2Match.index : existing.length;
|
|
27319
|
+
const next = existing.slice(0, markerIndex).trimEnd() + `
|
|
27320
|
+
|
|
27321
|
+
` + AGENTS_BLOCK + (nextH2Index < existing.length ? `
|
|
27322
|
+
` + existing.slice(nextH2Index) : "");
|
|
27323
|
+
writeFileSync4(agentsPath, next, "utf-8");
|
|
27324
|
+
ok("migrated Specialists section in AGENTS.md");
|
|
27325
|
+
return;
|
|
27326
|
+
}
|
|
27327
|
+
writeFileSync4(agentsPath, existing.trimEnd() + `
|
|
27328
|
+
|
|
27329
|
+
` + AGENTS_BLOCK, "utf-8");
|
|
27330
|
+
ok("appended Specialists section to AGENTS.md");
|
|
27122
27331
|
}
|
|
27123
27332
|
function readJsonObject(path) {
|
|
27124
27333
|
if (!existsSync12(path))
|
|
@@ -27315,6 +27524,7 @@ var init_init = __esm(() => {
|
|
|
27315
27524
|
init_memory_retrieval();
|
|
27316
27525
|
init_canonical_asset_resolver();
|
|
27317
27526
|
AGENTS_BLOCK = `
|
|
27527
|
+
<!-- specialists:start -->
|
|
27318
27528
|
## Specialists
|
|
27319
27529
|
|
|
27320
27530
|
Use CLI commands via Bash to run and monitor specialists:
|
|
@@ -27340,6 +27550,7 @@ Canonical tracked flow:
|
|
|
27340
27550
|
5. Close/update bead with outcome
|
|
27341
27551
|
|
|
27342
27552
|
Add custom specialists to \`.specialists/user/\` to extend defaults.
|
|
27553
|
+
<!-- specialists:end -->
|
|
27343
27554
|
`.trimStart();
|
|
27344
27555
|
GITIGNORE_ENTRIES = [
|
|
27345
27556
|
".specialists/jobs/",
|
|
@@ -28373,6 +28584,37 @@ function resolveScriptSpecialistName(name) {
|
|
|
28373
28584
|
return "changelog-drafter";
|
|
28374
28585
|
return name;
|
|
28375
28586
|
}
|
|
28587
|
+
function collectRequiredOutputKeys(spec) {
|
|
28588
|
+
const keys = new Set;
|
|
28589
|
+
const declared = spec.specialist.execution.expected_output_keys;
|
|
28590
|
+
if (Array.isArray(declared)) {
|
|
28591
|
+
for (const value of declared) {
|
|
28592
|
+
if (typeof value === "string" && value.length > 0)
|
|
28593
|
+
keys.add(value);
|
|
28594
|
+
}
|
|
28595
|
+
}
|
|
28596
|
+
if (spec.specialist.execution.response_format === "json") {
|
|
28597
|
+
const required2 = spec.specialist.prompt.output_schema?.required;
|
|
28598
|
+
if (Array.isArray(required2)) {
|
|
28599
|
+
for (const value of required2) {
|
|
28600
|
+
if (typeof value === "string" && value.length > 0)
|
|
28601
|
+
keys.add(value);
|
|
28602
|
+
}
|
|
28603
|
+
}
|
|
28604
|
+
}
|
|
28605
|
+
return Array.from(keys);
|
|
28606
|
+
}
|
|
28607
|
+
function detectTemplateFieldMisuse(template, specPrompt) {
|
|
28608
|
+
if (!specPrompt)
|
|
28609
|
+
return null;
|
|
28610
|
+
if (template.length > TEMPLATE_FIELD_MISUSE_MAX_LEN)
|
|
28611
|
+
return null;
|
|
28612
|
+
if (!TEMPLATE_FIELD_IDENTIFIER_RE.test(template))
|
|
28613
|
+
return null;
|
|
28614
|
+
if (!Object.prototype.hasOwnProperty.call(specPrompt, template))
|
|
28615
|
+
return null;
|
|
28616
|
+
return template;
|
|
28617
|
+
}
|
|
28376
28618
|
async function runScriptSpecialist(input2, options) {
|
|
28377
28619
|
const traceId = randomUUID();
|
|
28378
28620
|
const startedAt = Date.now();
|
|
@@ -28383,6 +28625,25 @@ async function runScriptSpecialist(input2, options) {
|
|
|
28383
28625
|
const skillPaths = options.trust?.allowSkills ? collectSkillPaths(spec) : [];
|
|
28384
28626
|
const skillSources = options.trust?.allowSkills ? computeSkillSources(spec) : undefined;
|
|
28385
28627
|
const template = input2.template ?? spec.specialist.prompt.task_template;
|
|
28628
|
+
if (input2.template !== undefined) {
|
|
28629
|
+
const misusedField = detectTemplateFieldMisuse(input2.template, spec.specialist.prompt);
|
|
28630
|
+
if (misusedField !== null) {
|
|
28631
|
+
const modelCandidates2 = collectModelCandidates(input2, spec, options);
|
|
28632
|
+
return {
|
|
28633
|
+
success: false,
|
|
28634
|
+
error: `template field misuse: input.template equals spec.prompt.${misusedField} key name (${input2.template.length} chars). The 'template' input field expects the literal template body, not a spec key. To use the spec's default, omit 'template'; to use a non-default template body, pass its full text inline.`,
|
|
28635
|
+
error_type: "template_field_misuse",
|
|
28636
|
+
meta: {
|
|
28637
|
+
specialist: resolvedSpecialist,
|
|
28638
|
+
requested_specialist: input2.requested_specialist ?? input2.specialist,
|
|
28639
|
+
resolved_specialist: resolvedSpecialist,
|
|
28640
|
+
model: modelCandidates2[0],
|
|
28641
|
+
duration_ms: Date.now() - startedAt,
|
|
28642
|
+
trace_id: traceId
|
|
28643
|
+
}
|
|
28644
|
+
};
|
|
28645
|
+
}
|
|
28646
|
+
}
|
|
28386
28647
|
const prompt = applyOutputContract(renderTaskTemplate(template, input2.variables ?? {}), spec);
|
|
28387
28648
|
const modelCandidates = collectModelCandidates(input2, spec, options);
|
|
28388
28649
|
const promptLimitBytes = resolvePromptLimitBytes(spec);
|
|
@@ -28432,11 +28693,12 @@ async function runScriptSpecialist(input2, options) {
|
|
|
28432
28693
|
writeTraceRow(observability2, resolvedSpecialist, model, traceId, parsed.text, durationMs2, skillSources, options.onAuditFailure);
|
|
28433
28694
|
if (parsed.kind === "success") {
|
|
28434
28695
|
let parsed_json;
|
|
28435
|
-
|
|
28696
|
+
const expectedKeys = collectRequiredOutputKeys(spec);
|
|
28697
|
+
const shouldParseJson = spec.specialist.execution.response_format === "json" || expectedKeys.length > 0;
|
|
28698
|
+
if (shouldParseJson) {
|
|
28436
28699
|
try {
|
|
28437
28700
|
parsed_json = JSON.parse(stripMarkdownFences(parsed.text));
|
|
28438
|
-
|
|
28439
|
-
for (const key of required2) {
|
|
28701
|
+
for (const key of expectedKeys) {
|
|
28440
28702
|
if (parsed_json === null || typeof parsed_json !== "object" || !(key in parsed_json))
|
|
28441
28703
|
throw new Error(`Missing required output field: ${key}`);
|
|
28442
28704
|
}
|
|
@@ -28586,7 +28848,7 @@ function classifyAttempt(attempt) {
|
|
|
28586
28848
|
function isRetryableModelFailure(stderr, text) {
|
|
28587
28849
|
return stderr.includes("0 tokens") || stderr.includes("quota") || stderr.includes("rate limit") || stderr.includes("403") || stderr.includes("401") || stderr.includes("insufficient_quota") || !text && !stderr.trim();
|
|
28588
28850
|
}
|
|
28589
|
-
var CompatGuardError, DEFAULT_PENDING_LINE_LIMIT_BYTES, DEFAULT_ASSISTANT_TEXT_LIMIT_BYTES, DEFAULT_STDERR_LIMIT_BYTES, DEFAULT_PROMPT_LIMIT_BYTES;
|
|
28851
|
+
var CompatGuardError, DEFAULT_PENDING_LINE_LIMIT_BYTES, DEFAULT_ASSISTANT_TEXT_LIMIT_BYTES, DEFAULT_STDERR_LIMIT_BYTES, DEFAULT_PROMPT_LIMIT_BYTES, TEMPLATE_FIELD_MISUSE_MAX_LEN = 30, TEMPLATE_FIELD_IDENTIFIER_RE;
|
|
28590
28852
|
var init_script_runner = __esm(() => {
|
|
28591
28853
|
init_observability_sqlite();
|
|
28592
28854
|
CompatGuardError = class CompatGuardError extends Error {
|
|
@@ -28601,6 +28863,7 @@ var init_script_runner = __esm(() => {
|
|
|
28601
28863
|
DEFAULT_ASSISTANT_TEXT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
28602
28864
|
DEFAULT_STDERR_LIMIT_BYTES = 1 * 1024 * 1024;
|
|
28603
28865
|
DEFAULT_PROMPT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
28866
|
+
TEMPLATE_FIELD_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
28604
28867
|
});
|
|
28605
28868
|
|
|
28606
28869
|
// src/cli/validate.ts
|
|
@@ -29203,12 +29466,22 @@ function createUserFork(source, targetName) {
|
|
|
29203
29466
|
}
|
|
29204
29467
|
async function resolveTargets(args) {
|
|
29205
29468
|
const loader = new SpecialistLoader;
|
|
29206
|
-
const
|
|
29469
|
+
const listedSpecialists = await loader.list();
|
|
29470
|
+
const allSpecialists = listedSpecialists.filter((specialist) => specialist.scope !== "package");
|
|
29207
29471
|
if (args.all) {
|
|
29208
29472
|
return allSpecialists;
|
|
29209
29473
|
}
|
|
29210
29474
|
const match = allSpecialists.find((specialist) => specialist.name === args.name && (args.scope === undefined || specialist.scope === args.scope));
|
|
29211
29475
|
if (!match) {
|
|
29476
|
+
const packageMatch = args.scope === undefined ? listedSpecialists.find((specialist) => specialist.name === args.name && specialist.scope === "package") : undefined;
|
|
29477
|
+
if (packageMatch) {
|
|
29478
|
+
fail(`Error: specialist "${args.name}" lives in [package] tier and cannot be edited directly.
|
|
29479
|
+
` + ` Fork to user tier first:
|
|
29480
|
+
|
|
29481
|
+
` + ` ${yellow8(`specialists edit ${args.name} --fork-from ${args.name}`)}
|
|
29482
|
+
|
|
29483
|
+
` + ` Then re-run your edit command.`);
|
|
29484
|
+
}
|
|
29212
29485
|
const hint = args.scope ? ` (scope: ${args.scope})` : "";
|
|
29213
29486
|
fail(`Error: specialist "${args.name}" not found${hint}
|
|
29214
29487
|
Run ${yellow8("specialists list")} to see available specialists`);
|
|
@@ -29686,7 +29959,7 @@ var init_config = __esm(() => {
|
|
|
29686
29959
|
});
|
|
29687
29960
|
|
|
29688
29961
|
// src/specialist/worktree.ts
|
|
29689
|
-
import { existsSync as existsSync16, symlinkSync as symlinkSync2, mkdirSync as mkdirSync8 } from "fs";
|
|
29962
|
+
import { existsSync as existsSync16, symlinkSync as symlinkSync2, mkdirSync as mkdirSync8, rmSync as rmSync2 } from "fs";
|
|
29690
29963
|
import { join as join16, resolve as resolve9 } from "path";
|
|
29691
29964
|
import { spawnSync as spawnSync11, execFileSync as execFileSync2 } from "child_process";
|
|
29692
29965
|
function deriveBranchName(beadId, specialistName) {
|
|
@@ -29726,9 +29999,52 @@ function provisionWorktree(options) {
|
|
|
29726
29999
|
const worktreeName = deriveWorktreeName(options.beadId, options.specialistName);
|
|
29727
30000
|
const worktreePath = resolve9(join16(worktreeBase, worktreeName));
|
|
29728
30001
|
createWorktreeViaBd(worktreePath, branch, commonRoot);
|
|
30002
|
+
normalizeParentHooksPath(commonRoot);
|
|
30003
|
+
try {
|
|
30004
|
+
rmSync2(join16(worktreePath, ".beads"), { recursive: true, force: true });
|
|
30005
|
+
markBeadsSkipWorktree(worktreePath);
|
|
30006
|
+
} catch {}
|
|
29729
30007
|
symlinkPiNpmCache(commonRoot, worktreePath);
|
|
29730
30008
|
return { branch, worktreePath, reused: false };
|
|
29731
30009
|
}
|
|
30010
|
+
function normalizeParentHooksPath(mainRepoRoot) {
|
|
30011
|
+
try {
|
|
30012
|
+
const result = spawnSync11("git", ["-C", mainRepoRoot, "config", "--get", "core.hooksPath"], {
|
|
30013
|
+
stdio: "pipe",
|
|
30014
|
+
encoding: "utf8"
|
|
30015
|
+
});
|
|
30016
|
+
if (result.status !== 0)
|
|
30017
|
+
return;
|
|
30018
|
+
const current = (result.stdout ?? "").trim();
|
|
30019
|
+
if (!current)
|
|
30020
|
+
return;
|
|
30021
|
+
if (resolve9(current) === current)
|
|
30022
|
+
return;
|
|
30023
|
+
if (current !== ".beads/hooks" && current !== "./.beads/hooks")
|
|
30024
|
+
return;
|
|
30025
|
+
const absolute = join16(mainRepoRoot, ".beads", "hooks");
|
|
30026
|
+
spawnSync11("git", ["-C", mainRepoRoot, "config", "core.hooksPath", absolute], { stdio: "pipe" });
|
|
30027
|
+
} catch {}
|
|
30028
|
+
}
|
|
30029
|
+
function markBeadsSkipWorktree(worktreePath) {
|
|
30030
|
+
try {
|
|
30031
|
+
const trackedResult = spawnSync11("git", ["-C", worktreePath, "ls-files", "--", ".beads"], {
|
|
30032
|
+
cwd: worktreePath,
|
|
30033
|
+
stdio: "pipe",
|
|
30034
|
+
encoding: "utf8"
|
|
30035
|
+
});
|
|
30036
|
+
if (trackedResult.status !== 0)
|
|
30037
|
+
return;
|
|
30038
|
+
const trackedPaths = (trackedResult.stdout ?? "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
30039
|
+
if (trackedPaths.length === 0)
|
|
30040
|
+
return;
|
|
30041
|
+
spawnSync11("git", ["-C", worktreePath, "update-index", "--skip-worktree", "--", ...trackedPaths], {
|
|
30042
|
+
cwd: worktreePath,
|
|
30043
|
+
stdio: "pipe",
|
|
30044
|
+
encoding: "utf8"
|
|
30045
|
+
});
|
|
30046
|
+
} catch {}
|
|
30047
|
+
}
|
|
29732
30048
|
function symlinkPiNpmCache(commonRoot, worktreePath) {
|
|
29733
30049
|
const source = join16(commonRoot, ".pi", "npm");
|
|
29734
30050
|
const target = join16(worktreePath, ".pi", "npm");
|
|
@@ -29775,7 +30091,7 @@ var init_worktree = __esm(() => {
|
|
|
29775
30091
|
});
|
|
29776
30092
|
|
|
29777
30093
|
// src/specialist/epic-reconciler.ts
|
|
29778
|
-
import { mkdirSync as mkdirSync9, openSync as openSync2, readFileSync as readFileSync15, rmSync as
|
|
30094
|
+
import { mkdirSync as mkdirSync9, openSync as openSync2, readFileSync as readFileSync15, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
29779
30095
|
import { join as join17 } from "path";
|
|
29780
30096
|
function buildEpicLockPath(epicId) {
|
|
29781
30097
|
const location = resolveObservabilityDbLocation();
|
|
@@ -29803,7 +30119,7 @@ function withEpicAdvisoryLock(epicId, action) {
|
|
|
29803
30119
|
} finally {
|
|
29804
30120
|
if (lockFd !== null) {
|
|
29805
30121
|
try {
|
|
29806
|
-
|
|
30122
|
+
rmSync3(lockPath, { force: true });
|
|
29807
30123
|
} catch {}
|
|
29808
30124
|
}
|
|
29809
30125
|
}
|
|
@@ -30015,11 +30331,25 @@ import { join as join18 } from "path";
|
|
|
30015
30331
|
function parseOptions(argv) {
|
|
30016
30332
|
let target = "";
|
|
30017
30333
|
let rebuild = false;
|
|
30018
|
-
|
|
30334
|
+
let targetBranch = "";
|
|
30335
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
30336
|
+
const argument = argv[index];
|
|
30019
30337
|
if (argument === "--rebuild") {
|
|
30020
30338
|
rebuild = true;
|
|
30021
30339
|
continue;
|
|
30022
30340
|
}
|
|
30341
|
+
if (argument === "--target-branch") {
|
|
30342
|
+
const branchName = argv[index + 1];
|
|
30343
|
+
if (!branchName || branchName.startsWith("-")) {
|
|
30344
|
+
throw new Error("Missing value for --target-branch");
|
|
30345
|
+
}
|
|
30346
|
+
if (targetBranch) {
|
|
30347
|
+
throw new Error("Only one target branch is supported");
|
|
30348
|
+
}
|
|
30349
|
+
targetBranch = branchName;
|
|
30350
|
+
index += 1;
|
|
30351
|
+
continue;
|
|
30352
|
+
}
|
|
30023
30353
|
if (argument.startsWith("-")) {
|
|
30024
30354
|
throw new Error(`Unknown option: ${argument}`);
|
|
30025
30355
|
}
|
|
@@ -30031,7 +30361,7 @@ function parseOptions(argv) {
|
|
|
30031
30361
|
if (!target) {
|
|
30032
30362
|
throw new Error("Missing merge target");
|
|
30033
30363
|
}
|
|
30034
|
-
return { target, rebuild };
|
|
30364
|
+
return { target, rebuild, targetBranch: targetBranch ? validateTargetBranchRef(targetBranch) : undefined };
|
|
30035
30365
|
}
|
|
30036
30366
|
function runCommand(command, args, cwd = process.cwd()) {
|
|
30037
30367
|
return spawnSync12(command, args, {
|
|
@@ -30040,7 +30370,17 @@ function runCommand(command, args, cwd = process.cwd()) {
|
|
|
30040
30370
|
stdio: ["ignore", "pipe", "pipe"]
|
|
30041
30371
|
});
|
|
30042
30372
|
}
|
|
30043
|
-
function
|
|
30373
|
+
function validateTargetBranchRef(targetBranch, cwd = process.cwd()) {
|
|
30374
|
+
const verification = runCommand("git", ["rev-parse", "--verify", `${targetBranch}^{commit}`], cwd);
|
|
30375
|
+
if (verification.status !== 0) {
|
|
30376
|
+
const detail = verification.stderr.trim() || verification.stdout.trim() || "unknown git ref error";
|
|
30377
|
+
throw new Error(`Invalid --target-branch '${targetBranch}': ${detail}`);
|
|
30378
|
+
}
|
|
30379
|
+
return targetBranch;
|
|
30380
|
+
}
|
|
30381
|
+
function resolveDefaultBranchName(cwd = process.cwd(), overrideBranch) {
|
|
30382
|
+
if (overrideBranch)
|
|
30383
|
+
return overrideBranch;
|
|
30044
30384
|
const symbolicRef = runCommand("git", ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], cwd);
|
|
30045
30385
|
if (symbolicRef.status === 0) {
|
|
30046
30386
|
const remoteHeadRef = symbolicRef.stdout.trim();
|
|
@@ -30170,7 +30510,7 @@ function checkEpicUnresolvedGuard(chainRootBeadId) {
|
|
|
30170
30510
|
return {
|
|
30171
30511
|
blocked: false,
|
|
30172
30512
|
epicId: membership.epicId,
|
|
30173
|
-
message: `Warning: unable to verify epic ${membership.epicId}
|
|
30513
|
+
message: `Warning: unable to verify epic ${membership.epicId} readiness (observability DB unavailable). Proceeding with chain merge.`
|
|
30174
30514
|
};
|
|
30175
30515
|
}
|
|
30176
30516
|
try {
|
|
@@ -30186,12 +30526,25 @@ function checkEpicUnresolvedGuard(chainRootBeadId) {
|
|
|
30186
30526
|
if (!isEpicUnresolvedState(status)) {
|
|
30187
30527
|
return { blocked: false, epicId: membership.epicId, epicStatus: status };
|
|
30188
30528
|
}
|
|
30529
|
+
const summary = loadEpicReadinessSummary(sqliteClient, membership.epicId);
|
|
30530
|
+
const chain = summary.chains.find((entry) => entry.chain_root_bead_id === chainRootBeadId || entry.chain_id === chainRootBeadId);
|
|
30531
|
+
if (!chain) {
|
|
30532
|
+
return {
|
|
30533
|
+
blocked: true,
|
|
30534
|
+
epicId: membership.epicId,
|
|
30535
|
+
epicStatus: status,
|
|
30536
|
+
message: `Chain ${chainRootBeadId} belongs to epic ${membership.epicId} but has no derived readiness record. Use 'sp epic status ${membership.epicId}' to inspect migration state.`
|
|
30537
|
+
};
|
|
30538
|
+
}
|
|
30539
|
+
if (chain.state === "pass") {
|
|
30540
|
+
return { blocked: false, epicId: membership.epicId, epicStatus: status };
|
|
30541
|
+
}
|
|
30189
30542
|
return {
|
|
30190
30543
|
blocked: true,
|
|
30191
30544
|
epicId: membership.epicId,
|
|
30192
30545
|
epicStatus: status,
|
|
30193
|
-
message: `Chain ${chainRootBeadId}
|
|
30194
|
-
Use 'sp epic
|
|
30546
|
+
message: `Chain ${chainRootBeadId} blocked by derived readiness: ${chain.blocking_reason ?? chain.state}.
|
|
30547
|
+
Use 'sp epic status ${membership.epicId}' to inspect epic state.`
|
|
30195
30548
|
};
|
|
30196
30549
|
} finally {
|
|
30197
30550
|
sqliteClient.close();
|
|
@@ -30466,8 +30819,8 @@ function parseNameStatusLine(line) {
|
|
|
30466
30819
|
function isNoisePath(path) {
|
|
30467
30820
|
return NOISE_PATH_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
30468
30821
|
}
|
|
30469
|
-
function isBranchAlreadyPublished(branch, cwd = process.cwd()) {
|
|
30470
|
-
const baseBranch = resolveDefaultBranchName(cwd);
|
|
30822
|
+
function isBranchAlreadyPublished(branch, cwd = process.cwd(), targetBranch) {
|
|
30823
|
+
const baseBranch = resolveDefaultBranchName(cwd, targetBranch);
|
|
30471
30824
|
const ancestorCheck = runCommand("git", ["merge-base", "--is-ancestor", branch, baseBranch], cwd);
|
|
30472
30825
|
if (ancestorCheck.status === 0) {
|
|
30473
30826
|
return true;
|
|
@@ -30478,8 +30831,8 @@ function isBranchAlreadyPublished(branch, cwd = process.cwd()) {
|
|
|
30478
30831
|
}
|
|
30479
30832
|
return cherryPickCount.stdout.trim() === "0";
|
|
30480
30833
|
}
|
|
30481
|
-
function previewBranchMergeDelta(branch, cwd = process.cwd()) {
|
|
30482
|
-
const baseBranch = resolveDefaultBranchName(cwd);
|
|
30834
|
+
function previewBranchMergeDelta(branch, cwd = process.cwd(), targetBranch) {
|
|
30835
|
+
const baseBranch = resolveDefaultBranchName(cwd, targetBranch);
|
|
30483
30836
|
const mergeBase = runCommand("git", ["merge-base", baseBranch, branch], cwd);
|
|
30484
30837
|
if (mergeBase.status !== 0) {
|
|
30485
30838
|
throw new Error(`Unable to compute merge base for '${baseBranch}' and '${branch}'.`);
|
|
@@ -30503,12 +30856,12 @@ function previewBranchMergeDelta(branch, cwd = process.cwd()) {
|
|
|
30503
30856
|
substantiveFiles
|
|
30504
30857
|
};
|
|
30505
30858
|
}
|
|
30506
|
-
function evaluateMergeWorthiness(preview, branch, cwd = process.cwd()) {
|
|
30859
|
+
function evaluateMergeWorthiness(preview, branch, cwd = process.cwd(), targetBranch) {
|
|
30507
30860
|
if (preview.files.length === 0) {
|
|
30508
|
-
return isBranchAlreadyPublished(branch, cwd) ? { shouldMerge: false, reason: "already-published" } : { shouldMerge: false, reason: "empty-delta" };
|
|
30861
|
+
return isBranchAlreadyPublished(branch, cwd, targetBranch) ? { shouldMerge: false, reason: "already-published" } : { shouldMerge: false, reason: "empty-delta" };
|
|
30509
30862
|
}
|
|
30510
30863
|
if (preview.substantiveFiles.length === 0) {
|
|
30511
|
-
return isBranchAlreadyPublished(branch, cwd) ? { shouldMerge: false, reason: "already-published" } : { shouldMerge: false, reason: "noise-only-delta" };
|
|
30864
|
+
return isBranchAlreadyPublished(branch, cwd, targetBranch) ? { shouldMerge: false, reason: "already-published" } : { shouldMerge: false, reason: "noise-only-delta" };
|
|
30512
30865
|
}
|
|
30513
30866
|
return { shouldMerge: true, reason: "ok" };
|
|
30514
30867
|
}
|
|
@@ -30525,9 +30878,9 @@ function throwWorthinessBlockError(target, preview, decision) {
|
|
|
30525
30878
|
throw new Error(`Refusing merge for '${target.branch}': ${reason}.
|
|
30526
30879
|
` + `Diagnostics: ${summary}`);
|
|
30527
30880
|
}
|
|
30528
|
-
function assertBranchMergeWorthiness(target, cwd = process.cwd()) {
|
|
30529
|
-
const preview = previewBranchMergeDelta(target.branch, cwd);
|
|
30530
|
-
const decision = evaluateMergeWorthiness(preview, target.branch, cwd);
|
|
30881
|
+
function assertBranchMergeWorthiness(target, cwd = process.cwd(), targetBranch) {
|
|
30882
|
+
const preview = previewBranchMergeDelta(target.branch, cwd, targetBranch);
|
|
30883
|
+
const decision = evaluateMergeWorthiness(preview, target.branch, cwd, targetBranch);
|
|
30531
30884
|
if (decision.reason === "already-published" || decision.shouldMerge)
|
|
30532
30885
|
return decision;
|
|
30533
30886
|
throwWorthinessBlockError(target, preview, decision);
|
|
@@ -30553,8 +30906,8 @@ function getCurrentHeadBranch(cwd = process.cwd()) {
|
|
|
30553
30906
|
function tryAbortRebase(cwd = process.cwd()) {
|
|
30554
30907
|
runCommand("git", ["rebase", "--abort"], cwd);
|
|
30555
30908
|
}
|
|
30556
|
-
function rebaseBranchOntoMaster(branch, worktreePath) {
|
|
30557
|
-
const baseBranch = resolveDefaultBranchName(worktreePath);
|
|
30909
|
+
function rebaseBranchOntoMaster(branch, worktreePath, targetBranch) {
|
|
30910
|
+
const baseBranch = resolveDefaultBranchName(worktreePath, targetBranch);
|
|
30558
30911
|
const checkedOutBranch = getCurrentHeadBranch(worktreePath);
|
|
30559
30912
|
if (checkedOutBranch !== branch) {
|
|
30560
30913
|
throw new Error(`Expected branch '${branch}' in worktree '${worktreePath}', found '${checkedOutBranch}'.`);
|
|
@@ -30588,6 +30941,11 @@ ${conflicts.map((file) => `- ${file}`).join(`
|
|
|
30588
30941
|
throw new Error(`Merge conflict while merging '${branch}'.${context}`);
|
|
30589
30942
|
}
|
|
30590
30943
|
function runTypecheckGate(cwd = process.cwd()) {
|
|
30944
|
+
const hasTypeScriptConfig = existsSync17(join18(cwd, "tsconfig.json")) || readdirSync6(cwd).some((entry) => entry.startsWith("tsconfig") && entry.endsWith(".json"));
|
|
30945
|
+
if (!hasTypeScriptConfig) {
|
|
30946
|
+
console.log("TypeScript gate: skipped (no tsconfig)");
|
|
30947
|
+
return;
|
|
30948
|
+
}
|
|
30591
30949
|
const tsc = runCommand("bunx", ["tsc", "--noEmit"], cwd);
|
|
30592
30950
|
if (tsc.status === 0)
|
|
30593
30951
|
return;
|
|
@@ -30623,7 +30981,7 @@ function printSummary(steps, rebuild) {
|
|
|
30623
30981
|
}
|
|
30624
30982
|
function printUsageAndExit(message) {
|
|
30625
30983
|
console.error(message);
|
|
30626
|
-
console.error("Usage: specialists|sp merge <target-bead-id> [--rebuild]");
|
|
30984
|
+
console.error("Usage: specialists|sp merge <target-bead-id> [--rebuild] [--target-branch <name>]");
|
|
30627
30985
|
process.exit(1);
|
|
30628
30986
|
}
|
|
30629
30987
|
function syncEpicStateAfterMerge(target) {
|
|
@@ -30641,6 +30999,7 @@ function syncEpicStateAfterMerge(target) {
|
|
|
30641
30999
|
}
|
|
30642
31000
|
function runMergePlan(targets, options) {
|
|
30643
31001
|
const mainRepoRoot = resolveMainWorktreeRoot();
|
|
31002
|
+
const targetBranch = options.targetBranch ? validateTargetBranchRef(options.targetBranch, mainRepoRoot) : undefined;
|
|
30644
31003
|
const shelved = options.mode === "direct" ? (() => {
|
|
30645
31004
|
const dirtyState = classifyMainRepoDirtyState(targets.map((target) => target.branch), mainRepoRoot);
|
|
30646
31005
|
if (dirtyState.overlappingPaths.length > 0) {
|
|
@@ -30654,7 +31013,7 @@ ${formatDirtyConflictMessage(dirtyState.overlappingPaths)}
|
|
|
30654
31013
|
const mergedSteps = [];
|
|
30655
31014
|
try {
|
|
30656
31015
|
for (const target of targets) {
|
|
30657
|
-
const worthiness = assertBranchMergeWorthiness(target, mainRepoRoot);
|
|
31016
|
+
const worthiness = assertBranchMergeWorthiness(target, mainRepoRoot, targetBranch);
|
|
30658
31017
|
if (worthiness.reason === "already-published") {
|
|
30659
31018
|
mergedSteps.push({
|
|
30660
31019
|
beadId: target.beadId,
|
|
@@ -30663,7 +31022,7 @@ ${formatDirtyConflictMessage(dirtyState.overlappingPaths)}
|
|
|
30663
31022
|
});
|
|
30664
31023
|
continue;
|
|
30665
31024
|
}
|
|
30666
|
-
rebaseBranchOntoMaster(target.branch, target.worktreePath);
|
|
31025
|
+
rebaseBranchOntoMaster(target.branch, target.worktreePath, targetBranch);
|
|
30667
31026
|
mergeBranch(target.branch, mainRepoRoot);
|
|
30668
31027
|
runTypecheckGate(mainRepoRoot);
|
|
30669
31028
|
syncEpicStateAfterMerge(target);
|
|
@@ -30755,18 +31114,21 @@ async function run13() {
|
|
|
30755
31114
|
printUsageAndExit(message);
|
|
30756
31115
|
}
|
|
30757
31116
|
const targets = resolveMergeTargets(options.target);
|
|
30758
|
-
const mergedSteps = runMergePlan(targets, { rebuild: options.rebuild });
|
|
31117
|
+
const mergedSteps = runMergePlan(targets, { rebuild: options.rebuild, targetBranch: options.targetBranch });
|
|
30759
31118
|
printSummary(mergedSteps, options.rebuild);
|
|
30760
31119
|
}
|
|
30761
31120
|
var TERMINAL_STATUSES, NOISE_PATH_PREFIXES, MERGE_DIRTY_IGNORE_PREFIXES;
|
|
30762
31121
|
var init_merge = __esm(() => {
|
|
30763
31122
|
init_observability_sqlite();
|
|
31123
|
+
init_epic_readiness();
|
|
30764
31124
|
init_epic_reconciler();
|
|
30765
31125
|
init_epic_lifecycle();
|
|
30766
31126
|
TERMINAL_STATUSES = new Set(["done", "error", "cancelled"]);
|
|
30767
31127
|
NOISE_PATH_PREFIXES = [".xtrm/reports/", ".wolf/", ".specialists/jobs/"];
|
|
30768
31128
|
MERGE_DIRTY_IGNORE_PREFIXES = [
|
|
30769
31129
|
...NOISE_PATH_PREFIXES,
|
|
31130
|
+
".beads/",
|
|
31131
|
+
".xtrm/skills/active/",
|
|
30770
31132
|
"dist/"
|
|
30771
31133
|
];
|
|
30772
31134
|
});
|
|
@@ -30864,15 +31226,37 @@ function formatEventLine(event, options) {
|
|
|
30864
31226
|
const detailParts = [];
|
|
30865
31227
|
let detail = "";
|
|
30866
31228
|
if (event.type === "meta") {
|
|
30867
|
-
|
|
30868
|
-
|
|
30869
|
-
|
|
30870
|
-
|
|
31229
|
+
if (event.model === "gitnexus_analyze_started") {
|
|
31230
|
+
detailParts.push("gitnexus=analyze_started");
|
|
31231
|
+
detailParts.push(`source=${event.backend}`);
|
|
31232
|
+
} else if (event.model === "gitnexus_analyze_start_failed") {
|
|
31233
|
+
detailParts.push("gitnexus=analyze_start_failed");
|
|
31234
|
+
detailParts.push(`reason=${event.backend}`);
|
|
31235
|
+
} else {
|
|
31236
|
+
detailParts.push(`model=${event.model}`);
|
|
31237
|
+
detailParts.push(`backend=${event.backend}`);
|
|
31238
|
+
if (event.source)
|
|
31239
|
+
detailParts.push(`source=${event.source}`);
|
|
31240
|
+
}
|
|
30871
31241
|
} else if (event.type === "tool") {
|
|
30872
31242
|
detail = formatToolDetail(event);
|
|
30873
31243
|
} else if (event.type === "error") {
|
|
30874
31244
|
detailParts.push(`source=${event.source}`);
|
|
30875
31245
|
detailParts.push(`error=${event.error_message}`);
|
|
31246
|
+
} else if (event.type === "auto_commit_success" || event.type === "auto_commit_skipped" || event.type === "auto_commit_failed") {
|
|
31247
|
+
const status = event.type.replace("auto_commit_", "");
|
|
31248
|
+
detailParts.push(`status=${status}`);
|
|
31249
|
+
if (event.commit_sha)
|
|
31250
|
+
detailParts.push(`commit=${event.commit_sha.slice(0, 12)}`);
|
|
31251
|
+
if (event.committed_files) {
|
|
31252
|
+
detailParts.push(`files=${event.committed_files.length}`);
|
|
31253
|
+
if (event.committed_files.length > 0) {
|
|
31254
|
+
const filePreview = event.committed_files.slice(0, 3).join(",");
|
|
31255
|
+
detailParts.push(`paths=${filePreview}${event.committed_files.length > 3 ? ",\u2026" : ""}`);
|
|
31256
|
+
}
|
|
31257
|
+
}
|
|
31258
|
+
if (event.reason)
|
|
31259
|
+
detailParts.push(`reason=${event.reason}`);
|
|
30876
31260
|
} else if (event.type === "run_complete") {
|
|
30877
31261
|
detailParts.push(`status=${event.status}`);
|
|
30878
31262
|
detailParts.push(`elapsed=${formatElapsed(event.elapsed_s)}`);
|
|
@@ -30997,17 +31381,21 @@ var init_format_helpers = __esm(() => {
|
|
|
30997
31381
|
turn_summary: "TURN+",
|
|
30998
31382
|
compaction: "CMPCT",
|
|
30999
31383
|
retry: "RETRY",
|
|
31000
|
-
error: "ERROR"
|
|
31384
|
+
error: "ERROR",
|
|
31385
|
+
auto_commit_success: "AUTO+",
|
|
31386
|
+
auto_commit_skipped: "AUTO-",
|
|
31387
|
+
auto_commit_failed: "AUTO!"
|
|
31001
31388
|
};
|
|
31002
31389
|
});
|
|
31003
31390
|
|
|
31004
31391
|
// src/cli/run.ts
|
|
31005
31392
|
var exports_run = {};
|
|
31006
31393
|
__export(exports_run, {
|
|
31007
|
-
run: () => run14
|
|
31394
|
+
run: () => run14,
|
|
31395
|
+
buildInjectedReviewerDiffVariables: () => buildInjectedReviewerDiffVariables
|
|
31008
31396
|
});
|
|
31009
31397
|
import { join as join19 } from "path";
|
|
31010
|
-
import { readFileSync as readFileSync17 } from "fs";
|
|
31398
|
+
import { existsSync as existsSync18, readFileSync as readFileSync17, readdirSync as readdirSync7, statSync as statSync3, writeFileSync as writeFileSync8 } from "fs";
|
|
31011
31399
|
import { randomBytes } from "crypto";
|
|
31012
31400
|
import { spawn as cpSpawn, execSync as execSync4 } from "child_process";
|
|
31013
31401
|
async function parseArgs7(argv) {
|
|
@@ -31182,12 +31570,58 @@ function resolveEpicIdForBead(sqliteClient, beadId) {
|
|
|
31182
31570
|
return;
|
|
31183
31571
|
return bead.parent;
|
|
31184
31572
|
}
|
|
31185
|
-
function
|
|
31186
|
-
const
|
|
31187
|
-
if (
|
|
31573
|
+
function ensureObservabilityDb2(cwd = process.cwd()) {
|
|
31574
|
+
const existing = createObservabilitySqliteClient(cwd);
|
|
31575
|
+
if (existing) {
|
|
31576
|
+
existing.close();
|
|
31188
31577
|
return;
|
|
31189
|
-
|
|
31190
|
-
|
|
31578
|
+
}
|
|
31579
|
+
const location = resolveObservabilityDbLocation(cwd);
|
|
31580
|
+
const bootstrapped = createObservabilitySqliteClientAtPath(location.dbPath);
|
|
31581
|
+
if (!bootstrapped)
|
|
31582
|
+
return;
|
|
31583
|
+
bootstrapped.close();
|
|
31584
|
+
}
|
|
31585
|
+
function resolveNewestJobIdFromDb(cwd, jobsDir, specialist, previousLatest, minStartedAtMs) {
|
|
31586
|
+
const sqliteClient = createObservabilitySqliteClient(cwd);
|
|
31587
|
+
if (!sqliteClient)
|
|
31588
|
+
return "";
|
|
31589
|
+
try {
|
|
31590
|
+
const newest = sqliteClient.listStatuses().filter((status) => {
|
|
31591
|
+
if (status.specialist !== specialist || status.id === previousLatest || status.started_at_ms < minStartedAtMs)
|
|
31592
|
+
return false;
|
|
31593
|
+
return existsSync18(join19(jobsDir, status.id, "status.json"));
|
|
31594
|
+
}).sort((left, right) => right.started_at_ms - left.started_at_ms || left.id.localeCompare(right.id))[0];
|
|
31595
|
+
return newest?.id ?? "";
|
|
31596
|
+
} catch {
|
|
31597
|
+
return "";
|
|
31598
|
+
} finally {
|
|
31599
|
+
sqliteClient.close();
|
|
31600
|
+
}
|
|
31601
|
+
}
|
|
31602
|
+
function resolveNewestJobIdFromJobsDir(jobsDir, previousLatest, minMtimeMs) {
|
|
31603
|
+
try {
|
|
31604
|
+
const entries = readdirSync7(jobsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^[a-f0-9]{6}$/.test(entry.name) && entry.name !== previousLatest).map((entry) => {
|
|
31605
|
+
const dirPath = join19(jobsDir, entry.name);
|
|
31606
|
+
const statusPath = join19(dirPath, "status.json");
|
|
31607
|
+
const stats = statSync3(dirPath);
|
|
31608
|
+
const statusStats = statSync3(statusPath);
|
|
31609
|
+
return {
|
|
31610
|
+
id: entry.name,
|
|
31611
|
+
mtimeMs: Math.max(stats.mtimeMs, statusStats.mtimeMs)
|
|
31612
|
+
};
|
|
31613
|
+
}).filter((entry) => entry.mtimeMs >= minMtimeMs).sort((left, right) => right.mtimeMs - left.mtimeMs || left.id.localeCompare(right.id));
|
|
31614
|
+
return entries[0]?.id ?? "";
|
|
31615
|
+
} catch {
|
|
31616
|
+
return "";
|
|
31617
|
+
}
|
|
31618
|
+
}
|
|
31619
|
+
function assertNoStaleBaseSiblings(beadId, forceStaleBase) {
|
|
31620
|
+
const sqliteClient = createObservabilitySqliteClient();
|
|
31621
|
+
if (!sqliteClient)
|
|
31622
|
+
return;
|
|
31623
|
+
try {
|
|
31624
|
+
const epicId = resolveEpicIdForBead(sqliteClient, beadId);
|
|
31191
31625
|
if (!epicId)
|
|
31192
31626
|
return;
|
|
31193
31627
|
const siblingChains = sqliteClient.listEpicChainsWithLatestJob(epicId).filter((chain) => chain.chain_root_bead_id !== beadId && Boolean(chain.branch));
|
|
@@ -31417,7 +31851,7 @@ function buildInjectedReviewerDiffVariables(cwd, maxFiles = 20) {
|
|
|
31417
31851
|
for (const src of sources) {
|
|
31418
31852
|
const stat2 = read(src.statCmd);
|
|
31419
31853
|
const files = read(src.namesCmd).split(`
|
|
31420
|
-
`).map((line) => line.trim()).filter(Boolean).slice(0, maxFiles);
|
|
31854
|
+
`).map((line) => line.trim()).filter(Boolean).slice(0, maxFiles).filter((file) => !AUTO_COMMIT_NOISE_PREFIXES.some((prefix) => file.startsWith(prefix)));
|
|
31421
31855
|
if (files.length === 0)
|
|
31422
31856
|
continue;
|
|
31423
31857
|
let remaining = MAX_TOTAL_HUNKS_CHARS;
|
|
@@ -31457,6 +31891,7 @@ ${truncated}` : `### ${file}
|
|
|
31457
31891
|
}
|
|
31458
31892
|
async function run14() {
|
|
31459
31893
|
const args = await parseArgs7(process.argv.slice(3));
|
|
31894
|
+
ensureObservabilityDb2(process.cwd());
|
|
31460
31895
|
const loader = new SpecialistLoader;
|
|
31461
31896
|
const specialist = await loader.get(args.name).catch((err) => {
|
|
31462
31897
|
process.stderr.write(`Error: ${err?.message ?? err}
|
|
@@ -31475,6 +31910,25 @@ async function run14() {
|
|
|
31475
31910
|
`);
|
|
31476
31911
|
process.exit(1);
|
|
31477
31912
|
}
|
|
31913
|
+
if (args.beadId && !args.reuseJobId) {
|
|
31914
|
+
const sqliteClient = createObservabilitySqliteClient();
|
|
31915
|
+
if (sqliteClient) {
|
|
31916
|
+
try {
|
|
31917
|
+
const existing = sqliteClient.findActiveJob(args.beadId, args.name);
|
|
31918
|
+
if (existing?.job_id) {
|
|
31919
|
+
process.stderr.write(`Error: existing ${existing.status ?? "unknown"} job '${existing.job_id}' already targets bead '${args.beadId}' specialist '${args.name}'.
|
|
31920
|
+
` + `To resume the keep-alive session: specialists run ${args.name} --job ${existing.job_id} ...
|
|
31921
|
+
` + `To inspect: specialists ps ${existing.job_id} --json
|
|
31922
|
+
` + `To cancel: specialists stop ${existing.job_id}
|
|
31923
|
+
`);
|
|
31924
|
+
sqliteClient.close();
|
|
31925
|
+
process.exit(1);
|
|
31926
|
+
}
|
|
31927
|
+
} finally {
|
|
31928
|
+
sqliteClient.close();
|
|
31929
|
+
}
|
|
31930
|
+
}
|
|
31931
|
+
}
|
|
31478
31932
|
if (args.background) {
|
|
31479
31933
|
const jobsDir2 = resolveJobsDir();
|
|
31480
31934
|
const latestPath = join19(jobsDir2, "latest");
|
|
@@ -31486,27 +31940,50 @@ async function run14() {
|
|
|
31486
31940
|
}
|
|
31487
31941
|
})();
|
|
31488
31942
|
const cwd = process.cwd();
|
|
31943
|
+
const launchStartedAt = Date.now();
|
|
31489
31944
|
const innerArgs = process.argv.slice(2).filter((a) => a !== "--background");
|
|
31490
31945
|
const cmd = `${process.execPath} ${process.argv[1]} ${innerArgs.map(shellQuote2).join(" ")}`;
|
|
31946
|
+
const tmuxCmd = `/bin/bash -lc ${shellQuote2(`cd ${shellQuote2(cwd)} && exec ${cmd}`)}`;
|
|
31491
31947
|
let childPid;
|
|
31948
|
+
let childExitCode;
|
|
31949
|
+
let childExitPromise;
|
|
31950
|
+
let handoffPath;
|
|
31492
31951
|
if (isTmuxAvailable()) {
|
|
31493
31952
|
const suffix = randomBytes(3).toString("hex");
|
|
31494
31953
|
const sessionName = buildSessionName(args.name, suffix);
|
|
31495
|
-
|
|
31954
|
+
handoffPath = join19(jobsDir2, `.bg-job-id-${sessionName}`);
|
|
31955
|
+
createTmuxSession(sessionName, cwd, tmuxCmd, { [JOB_ID_HANDOFF_PATH_ENV]: handoffPath });
|
|
31496
31956
|
} else {
|
|
31497
31957
|
const child = cpSpawn(process.execPath, [process.argv[1], ...innerArgs], {
|
|
31498
31958
|
detached: true,
|
|
31499
|
-
stdio: "ignore",
|
|
31959
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
31500
31960
|
cwd,
|
|
31501
31961
|
env: process.env
|
|
31502
31962
|
});
|
|
31963
|
+
const childStderr = child.stderr;
|
|
31964
|
+
if (childStderr) {
|
|
31965
|
+
childStderr.setEncoding("utf8");
|
|
31966
|
+
childStderr.on("data", (chunk) => {
|
|
31967
|
+
process.stderr.write(chunk);
|
|
31968
|
+
});
|
|
31969
|
+
}
|
|
31970
|
+
childExitPromise = new Promise((resolve10) => {
|
|
31971
|
+
child.on("exit", (code) => {
|
|
31972
|
+
childExitCode = code ?? 1;
|
|
31973
|
+
resolve10();
|
|
31974
|
+
});
|
|
31975
|
+
});
|
|
31503
31976
|
child.unref();
|
|
31504
31977
|
childPid = child.pid;
|
|
31505
31978
|
}
|
|
31506
|
-
const
|
|
31979
|
+
const pollTimeoutMs = isTmuxAvailable() ? 15000 : 5000;
|
|
31980
|
+
const deadline = Date.now() + pollTimeoutMs;
|
|
31507
31981
|
let jobId2 = "";
|
|
31508
31982
|
while (Date.now() < deadline) {
|
|
31509
|
-
await
|
|
31983
|
+
await Promise.race([
|
|
31984
|
+
new Promise((r) => setTimeout(r, 100)),
|
|
31985
|
+
childExitPromise
|
|
31986
|
+
]);
|
|
31510
31987
|
try {
|
|
31511
31988
|
const current = readFileSync17(latestPath, "utf-8").trim();
|
|
31512
31989
|
if (current && current !== oldLatest) {
|
|
@@ -31514,6 +31991,26 @@ async function run14() {
|
|
|
31514
31991
|
break;
|
|
31515
31992
|
}
|
|
31516
31993
|
} catch {}
|
|
31994
|
+
if (!jobId2 && handoffPath) {
|
|
31995
|
+
try {
|
|
31996
|
+
const handoff = readFileSync17(handoffPath, "utf-8").trim();
|
|
31997
|
+
if (/^[a-f0-9]{6}$/.test(handoff)) {
|
|
31998
|
+
jobId2 = handoff;
|
|
31999
|
+
break;
|
|
32000
|
+
}
|
|
32001
|
+
} catch {}
|
|
32002
|
+
}
|
|
32003
|
+
if (childExitCode !== undefined)
|
|
32004
|
+
break;
|
|
32005
|
+
}
|
|
32006
|
+
if (!jobId2 && childExitCode !== undefined && childExitCode !== 0) {
|
|
32007
|
+
process.exit(childExitCode);
|
|
32008
|
+
}
|
|
32009
|
+
if (!jobId2) {
|
|
32010
|
+
jobId2 = resolveNewestJobIdFromDb(cwd, jobsDir2, args.name, oldLatest, launchStartedAt - 1000);
|
|
32011
|
+
}
|
|
32012
|
+
if (!jobId2) {
|
|
32013
|
+
jobId2 = resolveNewestJobIdFromJobsDir(jobsDir2, oldLatest, launchStartedAt - 1000);
|
|
31517
32014
|
}
|
|
31518
32015
|
if (jobId2) {
|
|
31519
32016
|
process.stdout.write(`${jobId2}
|
|
@@ -31640,6 +32137,13 @@ async function run14() {
|
|
|
31640
32137
|
onJobStarted: ({ id }) => {
|
|
31641
32138
|
process.stderr.write(dim9(`[job started: ${id}]
|
|
31642
32139
|
`));
|
|
32140
|
+
const handoffPath = process.env[JOB_ID_HANDOFF_PATH_ENV];
|
|
32141
|
+
if (handoffPath) {
|
|
32142
|
+
try {
|
|
32143
|
+
writeFileSync8(handoffPath, `${id}
|
|
32144
|
+
`, "utf-8");
|
|
32145
|
+
} catch {}
|
|
32146
|
+
}
|
|
31643
32147
|
if (args.outputMode !== "raw") {
|
|
31644
32148
|
stopTailer = startEventTailer(id, jobsDir, args.outputMode, args.name, effectiveBeadId);
|
|
31645
32149
|
}
|
|
@@ -31701,7 +32205,7 @@ ${green9("\u2713")} ${footer}
|
|
|
31701
32205
|
`));
|
|
31702
32206
|
process.exit(0);
|
|
31703
32207
|
}
|
|
31704
|
-
var bold11 = (s) => `\x1B[1m${s}\x1B[0m`, dim9 = (s) => `\x1B[2m${s}\x1B[0m`, green9 = (s) => `\x1B[32m${s}\x1B[0m`, cyan6 = (s) => `\x1B[36m${s}\x1B[0m`, BLOCKED_JOB_REUSE_STATUSES;
|
|
32208
|
+
var bold11 = (s) => `\x1B[1m${s}\x1B[0m`, dim9 = (s) => `\x1B[2m${s}\x1B[0m`, green9 = (s) => `\x1B[32m${s}\x1B[0m`, cyan6 = (s) => `\x1B[36m${s}\x1B[0m`, JOB_ID_HANDOFF_PATH_ENV = "SPECIALISTS_BG_JOB_ID_PATH", BLOCKED_JOB_REUSE_STATUSES;
|
|
31705
32209
|
var init_run = __esm(() => {
|
|
31706
32210
|
init_loader();
|
|
31707
32211
|
init_runner();
|
|
@@ -31712,6 +32216,7 @@ var init_run = __esm(() => {
|
|
|
31712
32216
|
init_job_root();
|
|
31713
32217
|
init_worktree();
|
|
31714
32218
|
init_observability_sqlite();
|
|
32219
|
+
init_observability_db();
|
|
31715
32220
|
init_merge();
|
|
31716
32221
|
init_format_helpers();
|
|
31717
32222
|
init_tmux_utils();
|
|
@@ -31769,7 +32274,7 @@ var init_node_resolve = __esm(() => {
|
|
|
31769
32274
|
});
|
|
31770
32275
|
|
|
31771
32276
|
// src/specialist/job-control.ts
|
|
31772
|
-
import { existsSync as
|
|
32277
|
+
import { existsSync as existsSync19, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
|
|
31773
32278
|
import { join as join20 } from "path";
|
|
31774
32279
|
|
|
31775
32280
|
class JobControl {
|
|
@@ -31845,7 +32350,7 @@ class JobControl {
|
|
|
31845
32350
|
return sqliteResult;
|
|
31846
32351
|
} catch {}
|
|
31847
32352
|
const resultPath = this.resultPath(jobId);
|
|
31848
|
-
if (!
|
|
32353
|
+
if (!existsSync19(resultPath))
|
|
31849
32354
|
return null;
|
|
31850
32355
|
try {
|
|
31851
32356
|
return readFileSync18(resultPath, "utf-8");
|
|
@@ -31881,7 +32386,7 @@ class JobControl {
|
|
|
31881
32386
|
}
|
|
31882
32387
|
const jsonLine = `${JSON.stringify(payload)}
|
|
31883
32388
|
`;
|
|
31884
|
-
|
|
32389
|
+
writeFileSync9(status.fifo_path, jsonLine, { flag: "a" });
|
|
31885
32390
|
}
|
|
31886
32391
|
resultPath(jobId) {
|
|
31887
32392
|
return join20(this.jobsDir, jobId, "result.txt");
|
|
@@ -33959,7 +34464,7 @@ var exports_node = {};
|
|
|
33959
34464
|
__export(exports_node, {
|
|
33960
34465
|
handleNodeCommand: () => handleNodeCommand
|
|
33961
34466
|
});
|
|
33962
|
-
import { existsSync as
|
|
34467
|
+
import { existsSync as existsSync20, readFileSync as readFileSync19, readdirSync as readdirSync8 } from "fs";
|
|
33963
34468
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
33964
34469
|
import { spawnSync as spawnSync14 } from "child_process";
|
|
33965
34470
|
import { basename as basename5, join as join21, resolve as resolve10 } from "path";
|
|
@@ -34152,9 +34657,9 @@ function discoverNodeConfigs(cwd) {
|
|
|
34152
34657
|
const discoveredByName = new Map;
|
|
34153
34658
|
for (const directory of NODE_DISCOVERY_DIRS) {
|
|
34154
34659
|
const absoluteDir = resolve10(cwd, directory.path);
|
|
34155
|
-
if (!
|
|
34660
|
+
if (!existsSync20(absoluteDir))
|
|
34156
34661
|
continue;
|
|
34157
|
-
const files =
|
|
34662
|
+
const files = readdirSync8(absoluteDir).filter((fileName) => fileName.endsWith(NODE_CONFIG_SUFFIX));
|
|
34158
34663
|
for (const fileName of files) {
|
|
34159
34664
|
const path = join21(absoluteDir, fileName);
|
|
34160
34665
|
const name = toNodeName(fileName);
|
|
@@ -34174,7 +34679,7 @@ function getNodeDiscoverySummary() {
|
|
|
34174
34679
|
}
|
|
34175
34680
|
function resolveNodeConfigPath(cwd, input2) {
|
|
34176
34681
|
const explicitPath = resolve10(cwd, input2);
|
|
34177
|
-
if (
|
|
34682
|
+
if (existsSync20(explicitPath)) {
|
|
34178
34683
|
return { name: toNodeName(explicitPath), path: explicitPath, source: "repo" };
|
|
34179
34684
|
}
|
|
34180
34685
|
const normalizedName = input2.endsWith(NODE_CONFIG_SUFFIX) ? input2.slice(0, -NODE_CONFIG_SUFFIX.length) : input2;
|
|
@@ -34824,7 +35329,6 @@ var exports_epic = {};
|
|
|
34824
35329
|
__export(exports_epic, {
|
|
34825
35330
|
handleEpicSyncCommand: () => handleEpicSyncCommand,
|
|
34826
35331
|
handleEpicStatusCommand: () => handleEpicStatusCommand,
|
|
34827
|
-
handleEpicResolveCommand: () => handleEpicResolveCommand,
|
|
34828
35332
|
handleEpicMergeCommand: () => handleEpicMergeCommand,
|
|
34829
35333
|
handleEpicListCommand: () => handleEpicListCommand,
|
|
34830
35334
|
handleEpicCommand: () => handleEpicCommand,
|
|
@@ -34858,7 +35362,9 @@ function parseMergeOptions(argv) {
|
|
|
34858
35362
|
let rebuild = false;
|
|
34859
35363
|
let json = false;
|
|
34860
35364
|
let pr = false;
|
|
34861
|
-
|
|
35365
|
+
let targetBranch = "";
|
|
35366
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
35367
|
+
const argument = argv[index];
|
|
34862
35368
|
if (argument === "--rebuild") {
|
|
34863
35369
|
rebuild = true;
|
|
34864
35370
|
continue;
|
|
@@ -34871,11 +35377,23 @@ function parseMergeOptions(argv) {
|
|
|
34871
35377
|
pr = true;
|
|
34872
35378
|
continue;
|
|
34873
35379
|
}
|
|
35380
|
+
if (argument === "--target-branch") {
|
|
35381
|
+
const branchName = argv[index + 1];
|
|
35382
|
+
if (!branchName || branchName.startsWith("-")) {
|
|
35383
|
+
throw new Error("Missing value for --target-branch");
|
|
35384
|
+
}
|
|
35385
|
+
if (targetBranch) {
|
|
35386
|
+
throw new Error("Only one target branch is supported");
|
|
35387
|
+
}
|
|
35388
|
+
targetBranch = branchName;
|
|
35389
|
+
index += 1;
|
|
35390
|
+
continue;
|
|
35391
|
+
}
|
|
34874
35392
|
if (argument.startsWith("-") && argument !== "--rebuild" && argument !== "--json" && argument !== "--pr") {
|
|
34875
35393
|
throw new Error(`Unknown option: ${argument}`);
|
|
34876
35394
|
}
|
|
34877
35395
|
}
|
|
34878
|
-
return { epicId, rebuild, json, pr };
|
|
35396
|
+
return { epicId, rebuild, json, pr, targetBranch: targetBranch || undefined };
|
|
34879
35397
|
}
|
|
34880
35398
|
function parseListOptions(argv) {
|
|
34881
35399
|
let unresolvedOnly = false;
|
|
@@ -34909,25 +35427,6 @@ function parseStatusOptions(argv) {
|
|
|
34909
35427
|
}
|
|
34910
35428
|
return { epicId, json };
|
|
34911
35429
|
}
|
|
34912
|
-
function parseResolveOptions(argv) {
|
|
34913
|
-
const epicId = parseEpicId(argv);
|
|
34914
|
-
let dryRun = false;
|
|
34915
|
-
let json = false;
|
|
34916
|
-
for (const argument of argv) {
|
|
34917
|
-
if (argument === "--dry-run") {
|
|
34918
|
-
dryRun = true;
|
|
34919
|
-
continue;
|
|
34920
|
-
}
|
|
34921
|
-
if (argument === "--json") {
|
|
34922
|
-
json = true;
|
|
34923
|
-
continue;
|
|
34924
|
-
}
|
|
34925
|
-
if (argument.startsWith("-") && argument !== "--dry-run" && argument !== "--json") {
|
|
34926
|
-
throw new Error(`Unknown option: ${argument}`);
|
|
34927
|
-
}
|
|
34928
|
-
}
|
|
34929
|
-
return { epicId, dryRun, json };
|
|
34930
|
-
}
|
|
34931
35430
|
function parseSyncOptions(argv) {
|
|
34932
35431
|
const epicId = parseEpicId(argv);
|
|
34933
35432
|
let apply = false;
|
|
@@ -35072,12 +35571,9 @@ function gatherEpicContext(options) {
|
|
|
35072
35571
|
}
|
|
35073
35572
|
function validateEpicMergeReadiness(context) {
|
|
35074
35573
|
const epicState = context.epicRecord?.status ?? "open";
|
|
35075
|
-
if (
|
|
35574
|
+
if (epicState === "merged" || epicState === "abandoned") {
|
|
35076
35575
|
throw new Error(`Epic ${context.epicId} is already in terminal state '${epicState}'. No further merges allowed.`);
|
|
35077
35576
|
}
|
|
35078
|
-
if (epicState !== "resolving" && epicState !== "merge_ready") {
|
|
35079
|
-
throw new Error(`Epic ${context.epicId} is in state '${epicState}'. Must be 'resolving' or 'merge_ready' before publication.`);
|
|
35080
|
-
}
|
|
35081
35577
|
const chainStatuses = [...context.chainJobStatuses.entries()].map(([chainId, status]) => ({
|
|
35082
35578
|
chainId,
|
|
35083
35579
|
hasRunningJob: status.hasRunningJob
|
|
@@ -35115,11 +35611,12 @@ function updateEpicState(epicId, fromState, toState) {
|
|
|
35115
35611
|
sqlite.close();
|
|
35116
35612
|
}
|
|
35117
35613
|
}
|
|
35118
|
-
function mergeEpicChains(context, rebuild, pr) {
|
|
35614
|
+
function mergeEpicChains(context, rebuild, pr, targetBranch) {
|
|
35119
35615
|
return executePublicationPlan(context.chainTargets, {
|
|
35120
35616
|
rebuild,
|
|
35121
35617
|
mode: pr ? "pr" : "direct",
|
|
35122
|
-
publicationLabel: `epic-${context.epicId}
|
|
35618
|
+
publicationLabel: `epic-${context.epicId}`,
|
|
35619
|
+
targetBranch
|
|
35123
35620
|
});
|
|
35124
35621
|
}
|
|
35125
35622
|
function printEpicMergeSummary(result, rebuild, pr) {
|
|
@@ -35203,75 +35700,6 @@ async function handleEpicListCommand(argv) {
|
|
|
35203
35700
|
sqlite.close();
|
|
35204
35701
|
}
|
|
35205
35702
|
}
|
|
35206
|
-
async function handleEpicResolveCommand(argv) {
|
|
35207
|
-
let options;
|
|
35208
|
-
try {
|
|
35209
|
-
options = parseResolveOptions(argv);
|
|
35210
|
-
} catch (error2) {
|
|
35211
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
35212
|
-
console.error(message);
|
|
35213
|
-
console.error("Usage: specialists epic resolve <epic-id> [--dry-run] [--json]");
|
|
35214
|
-
process.exit(1);
|
|
35215
|
-
}
|
|
35216
|
-
const sqlite = createObservabilitySqliteClient();
|
|
35217
|
-
if (!sqlite) {
|
|
35218
|
-
const message = "Observability SQLite database not available. Run `sp db setup` first.";
|
|
35219
|
-
if (options.json) {
|
|
35220
|
-
console.log(JSON.stringify({ error: message }, null, 2));
|
|
35221
|
-
} else {
|
|
35222
|
-
console.error(message);
|
|
35223
|
-
}
|
|
35224
|
-
process.exit(1);
|
|
35225
|
-
}
|
|
35226
|
-
try {
|
|
35227
|
-
const now = Date.now();
|
|
35228
|
-
const existing = sqlite.readEpicRun(options.epicId);
|
|
35229
|
-
const fromState = existing?.status ?? "open";
|
|
35230
|
-
let toState;
|
|
35231
|
-
try {
|
|
35232
|
-
toState = transitionEpicState(fromState, "resolving");
|
|
35233
|
-
} catch (error2) {
|
|
35234
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
35235
|
-
if (options.json) {
|
|
35236
|
-
console.log(JSON.stringify({ epic_id: options.epicId, from_state: fromState, error: message }, null, 2));
|
|
35237
|
-
} else {
|
|
35238
|
-
console.error(`Resolve blocked: ${message}`);
|
|
35239
|
-
}
|
|
35240
|
-
process.exit(1);
|
|
35241
|
-
return;
|
|
35242
|
-
}
|
|
35243
|
-
if (!options.dryRun) {
|
|
35244
|
-
sqlite.upsertEpicRun({
|
|
35245
|
-
epic_id: options.epicId,
|
|
35246
|
-
status: toState,
|
|
35247
|
-
status_json: JSON.stringify({
|
|
35248
|
-
epic_id: options.epicId,
|
|
35249
|
-
status: toState,
|
|
35250
|
-
previous_status: fromState,
|
|
35251
|
-
transitioned_at_ms: now
|
|
35252
|
-
}),
|
|
35253
|
-
updated_at_ms: now
|
|
35254
|
-
});
|
|
35255
|
-
}
|
|
35256
|
-
const transitionSummary = summarizeEpicTransition(options.epicId, fromState, toState);
|
|
35257
|
-
if (options.json) {
|
|
35258
|
-
console.log(JSON.stringify({
|
|
35259
|
-
epic_id: options.epicId,
|
|
35260
|
-
from_state: fromState,
|
|
35261
|
-
to_state: toState,
|
|
35262
|
-
dry_run: options.dryRun,
|
|
35263
|
-
summary: transitionSummary
|
|
35264
|
-
}, null, 2));
|
|
35265
|
-
return;
|
|
35266
|
-
}
|
|
35267
|
-
console.log(transitionSummary);
|
|
35268
|
-
if (options.dryRun) {
|
|
35269
|
-
console.log("(dry-run: no state persisted)");
|
|
35270
|
-
}
|
|
35271
|
-
} finally {
|
|
35272
|
-
sqlite.close();
|
|
35273
|
-
}
|
|
35274
|
-
}
|
|
35275
35703
|
async function handleEpicMergeCommand(argv) {
|
|
35276
35704
|
let options;
|
|
35277
35705
|
try {
|
|
@@ -35280,7 +35708,7 @@ async function handleEpicMergeCommand(argv) {
|
|
|
35280
35708
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
35281
35709
|
console.error(message);
|
|
35282
35710
|
console.error("");
|
|
35283
|
-
console.error("Usage: specialists epic merge <epic-id> [--rebuild] [--pr] [--json]");
|
|
35711
|
+
console.error("Usage: specialists epic merge <epic-id> [--rebuild] [--pr] [--json] [--target-branch <name>]");
|
|
35284
35712
|
process.exit(1);
|
|
35285
35713
|
}
|
|
35286
35714
|
let context;
|
|
@@ -35308,20 +35736,12 @@ async function handleEpicMergeCommand(argv) {
|
|
|
35308
35736
|
process.exit(1);
|
|
35309
35737
|
}
|
|
35310
35738
|
const fromState = currentState;
|
|
35311
|
-
if (currentState === "resolving") {
|
|
35312
|
-
const nextState = transitionEpicState(currentState, "merge_ready");
|
|
35313
|
-
updateEpicState(context.epicId, currentState, nextState);
|
|
35314
|
-
if (!options.json) {
|
|
35315
|
-
console.log(summarizeEpicTransition(context.epicId, currentState, nextState));
|
|
35316
|
-
}
|
|
35317
|
-
currentState = nextState;
|
|
35318
|
-
}
|
|
35319
35739
|
let mergedChains = [];
|
|
35320
35740
|
let mergeError;
|
|
35321
35741
|
let toState = currentState;
|
|
35322
35742
|
let pullRequestUrl;
|
|
35323
35743
|
try {
|
|
35324
|
-
const publicationResult = mergeEpicChains(context, options.rebuild, options.pr);
|
|
35744
|
+
const publicationResult = mergeEpicChains(context, options.rebuild, options.pr, options.targetBranch);
|
|
35325
35745
|
mergedChains = publicationResult.steps;
|
|
35326
35746
|
pullRequestUrl = publicationResult.pullRequestUrl;
|
|
35327
35747
|
toState = options.pr ? currentState : transitionEpicState(currentState, "merged");
|
|
@@ -35488,12 +35908,7 @@ async function handleEpicStatusCommand(argv) {
|
|
|
35488
35908
|
}
|
|
35489
35909
|
console.log("");
|
|
35490
35910
|
console.log(`Epic: ${options.epicId}`);
|
|
35491
|
-
|
|
35492
|
-
console.log(`State: ${epicRecord.status}`);
|
|
35493
|
-
console.log(`Updated: ${new Date(epicRecord.updated_at_ms).toISOString()}`);
|
|
35494
|
-
} else {
|
|
35495
|
-
console.log("State: (not tracked in SQLite, defaults to open)");
|
|
35496
|
-
}
|
|
35911
|
+
console.log(`State: ${epicRecord?.status ?? "(derived)"}`);
|
|
35497
35912
|
console.log(`Readiness: ${readiness.isReady ? "ready" : "blocked"}`);
|
|
35498
35913
|
console.log(`Summary: ${readiness.summary}`);
|
|
35499
35914
|
console.log("");
|
|
@@ -35522,22 +35937,21 @@ async function handleEpicCommand(argv) {
|
|
|
35522
35937
|
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
35523
35938
|
console.log([
|
|
35524
35939
|
"",
|
|
35525
|
-
"Usage: specialists epic <list|status|
|
|
35940
|
+
"Usage: specialists epic <list|status|sync|abandon|merge> [options]",
|
|
35526
35941
|
"",
|
|
35527
35942
|
"Commands:",
|
|
35528
|
-
" list [--unresolved] [--json] List epics with
|
|
35529
|
-
" status <epic-id> [--json] Show
|
|
35530
|
-
"
|
|
35531
|
-
" sync <epic-id> [--apply] [--json] Reconcile epic drift (dry-run by default)",
|
|
35943
|
+
" list [--unresolved] [--json] List epics with readiness summary",
|
|
35944
|
+
" status <epic-id> [--json] Show derived readiness and chain status",
|
|
35945
|
+
" sync <epic-id> [--apply] [--json] Reconcile epic drift (dry-run by default)",
|
|
35532
35946
|
" abandon <epic-id> --reason <text> [--force] [--json] Transition epic to abandoned",
|
|
35533
35947
|
" merge <epic-id> [--rebuild] [--pr] [--json] Publish epic-owned chains in dependency order",
|
|
35534
35948
|
"",
|
|
35535
|
-
"Epic
|
|
35536
|
-
"
|
|
35537
|
-
"
|
|
35949
|
+
"Epic readiness:",
|
|
35950
|
+
" status reflects derived readiness from live chain state",
|
|
35951
|
+
" persisted epic state is compatibility metadata only",
|
|
35538
35952
|
"",
|
|
35539
35953
|
"Merge behavior:",
|
|
35540
|
-
" - Requires
|
|
35954
|
+
" - Requires derived readiness: ready chains only",
|
|
35541
35955
|
" - All chain jobs must be terminal before publication",
|
|
35542
35956
|
" - Chains merged in topological dependency order",
|
|
35543
35957
|
" - Use --pr to publish via pull request instead of direct merge",
|
|
@@ -35547,13 +35961,13 @@ async function handleEpicCommand(argv) {
|
|
|
35547
35961
|
"Examples:",
|
|
35548
35962
|
" specialists epic list",
|
|
35549
35963
|
" specialists epic list --unresolved --json",
|
|
35550
|
-
" specialists epic resolve unitAI-3f7b",
|
|
35551
35964
|
" specialists epic status unitAI-3f7b --json",
|
|
35552
35965
|
" specialists epic sync unitAI-3f7b",
|
|
35553
35966
|
" specialists epic sync unitAI-3f7b --apply",
|
|
35554
35967
|
' specialists epic abandon unitAI-3f7b --reason "scope changed"',
|
|
35555
35968
|
" specialists epic merge unitAI-3f7b --rebuild",
|
|
35556
35969
|
" specialists epic merge unitAI-3f7b --pr",
|
|
35970
|
+
" specialists epic merge unitAI-3f7b --target-branch feature/foo",
|
|
35557
35971
|
""
|
|
35558
35972
|
].join(`
|
|
35559
35973
|
`));
|
|
@@ -35563,10 +35977,6 @@ async function handleEpicCommand(argv) {
|
|
|
35563
35977
|
await handleEpicListCommand(argv.slice(1));
|
|
35564
35978
|
return;
|
|
35565
35979
|
}
|
|
35566
|
-
if (subcommand === "resolve") {
|
|
35567
|
-
await handleEpicResolveCommand(argv.slice(1));
|
|
35568
|
-
return;
|
|
35569
|
-
}
|
|
35570
35980
|
if (subcommand === "sync") {
|
|
35571
35981
|
await handleEpicSyncCommand(argv.slice(1));
|
|
35572
35982
|
return;
|
|
@@ -35584,7 +35994,7 @@ async function handleEpicCommand(argv) {
|
|
|
35584
35994
|
return;
|
|
35585
35995
|
}
|
|
35586
35996
|
console.error(`Unknown epic subcommand: ${subcommand}`);
|
|
35587
|
-
console.error("Usage: specialists epic <list|status|
|
|
35997
|
+
console.error("Usage: specialists epic <list|status|sync|abandon|merge>");
|
|
35588
35998
|
process.exit(1);
|
|
35589
35999
|
}
|
|
35590
36000
|
var RUNNING_STATUSES;
|
|
@@ -35598,7 +36008,7 @@ var init_epic = __esm(() => {
|
|
|
35598
36008
|
|
|
35599
36009
|
// src/cli/version-check.ts
|
|
35600
36010
|
import { spawnSync as spawnSync16 } from "child_process";
|
|
35601
|
-
import { existsSync as
|
|
36011
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync20, writeFileSync as writeFileSync10 } from "fs";
|
|
35602
36012
|
import { dirname as dirname9, join as join22 } from "path";
|
|
35603
36013
|
import { createRequire as createRequire3 } from "module";
|
|
35604
36014
|
function readBundledPackageVersion(requireFn = require3) {
|
|
@@ -35621,7 +36031,7 @@ function shouldRunVersionCheck() {
|
|
|
35621
36031
|
return true;
|
|
35622
36032
|
}
|
|
35623
36033
|
function readCache() {
|
|
35624
|
-
if (!
|
|
36034
|
+
if (!existsSync21(CACHE_PATH))
|
|
35625
36035
|
return null;
|
|
35626
36036
|
try {
|
|
35627
36037
|
return JSON.parse(readFileSync20(CACHE_PATH, "utf8"));
|
|
@@ -35634,7 +36044,7 @@ function readCachedVersionCheck() {
|
|
|
35634
36044
|
}
|
|
35635
36045
|
function writeCache(cache) {
|
|
35636
36046
|
mkdirSync10(dirname9(CACHE_PATH), { recursive: true });
|
|
35637
|
-
|
|
36047
|
+
writeFileSync10(CACHE_PATH, `${JSON.stringify(cache, null, 2)}
|
|
35638
36048
|
`, "utf8");
|
|
35639
36049
|
}
|
|
35640
36050
|
function isFresh(cache) {
|
|
@@ -35731,7 +36141,7 @@ __export(exports_status, {
|
|
|
35731
36141
|
detectJobOutputMode: () => detectJobOutputMode
|
|
35732
36142
|
});
|
|
35733
36143
|
import { spawnSync as spawnSync17 } from "child_process";
|
|
35734
|
-
import { existsSync as
|
|
36144
|
+
import { existsSync as existsSync22, readFileSync as readFileSync21 } from "fs";
|
|
35735
36145
|
import { join as join23 } from "path";
|
|
35736
36146
|
function ok2(msg) {
|
|
35737
36147
|
console.log(` ${green8("\u2713")} ${msg}`);
|
|
@@ -35830,7 +36240,7 @@ function countJobEvents(sqliteClient, jobsDir, jobId) {
|
|
|
35830
36240
|
return 0;
|
|
35831
36241
|
}
|
|
35832
36242
|
const eventsFile = join23(jobsDir, jobId, "events.jsonl");
|
|
35833
|
-
if (!
|
|
36243
|
+
if (!existsSync22(eventsFile))
|
|
35834
36244
|
return 0;
|
|
35835
36245
|
const raw = readFileSync21(eventsFile, "utf-8").trim();
|
|
35836
36246
|
if (!raw)
|
|
@@ -35867,7 +36277,7 @@ function getLatestContextSnapshot(sqliteClient, jobsDir, jobId) {
|
|
|
35867
36277
|
return null;
|
|
35868
36278
|
}
|
|
35869
36279
|
const eventsFile = join23(jobsDir, jobId, "events.jsonl");
|
|
35870
|
-
if (!
|
|
36280
|
+
if (!existsSync22(eventsFile))
|
|
35871
36281
|
return null;
|
|
35872
36282
|
const lines = readFileSync21(eventsFile, "utf-8").split(`
|
|
35873
36283
|
`);
|
|
@@ -35974,12 +36384,12 @@ async function run15() {
|
|
|
35974
36384
|
`).slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean)) : new Set;
|
|
35975
36385
|
const bdInstalled = isInstalled2("bd");
|
|
35976
36386
|
const bdVersion = bdInstalled ? cmd("bd", ["--version"]) : null;
|
|
35977
|
-
const beadsPresent =
|
|
36387
|
+
const beadsPresent = existsSync22(join23(process.cwd(), ".beads"));
|
|
35978
36388
|
const specialistsBin = cmd("which", ["specialists"]);
|
|
35979
36389
|
const jobsDir = resolveJobsDir();
|
|
35980
36390
|
const jobFileOutputMode = detectJobFileOutputMode();
|
|
35981
36391
|
let jobs = [];
|
|
35982
|
-
if (
|
|
36392
|
+
if (existsSync22(jobsDir)) {
|
|
35983
36393
|
supervisor = new Supervisor({
|
|
35984
36394
|
runner: null,
|
|
35985
36395
|
runOptions: null,
|
|
@@ -36145,14 +36555,346 @@ var init_status = __esm(() => {
|
|
|
36145
36555
|
init_version_check();
|
|
36146
36556
|
});
|
|
36147
36557
|
|
|
36558
|
+
// src/specialist/process-health.ts
|
|
36559
|
+
import { existsSync as existsSync23, readdirSync as readdirSync9, readFileSync as readFileSync22, readlinkSync as readlinkSync2 } from "fs";
|
|
36560
|
+
import { join as join24 } from "path";
|
|
36561
|
+
function parseThreshold(raw, fallback) {
|
|
36562
|
+
if (!raw)
|
|
36563
|
+
return fallback;
|
|
36564
|
+
const parsed = Number(raw);
|
|
36565
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
36566
|
+
return fallback;
|
|
36567
|
+
return parsed;
|
|
36568
|
+
}
|
|
36569
|
+
function getProcessHealthThresholds(env = process.env) {
|
|
36570
|
+
const warnPct = parseThreshold(env.SPECIALISTS_HEALTH_WARN_PCT, DEFAULT_WARN_PCT);
|
|
36571
|
+
const refusePct = parseThreshold(env.SPECIALISTS_HEALTH_REFUSE_PCT, DEFAULT_REFUSE_PCT);
|
|
36572
|
+
return { warnPct, refusePct };
|
|
36573
|
+
}
|
|
36574
|
+
function readMemAvailableBytes(meminfoPath) {
|
|
36575
|
+
try {
|
|
36576
|
+
const content = readFileSync22(meminfoPath, "utf-8");
|
|
36577
|
+
const match = /^MemAvailable:\s+(\d+)\s+kB$/m.exec(content);
|
|
36578
|
+
if (!match)
|
|
36579
|
+
return 0;
|
|
36580
|
+
return Number(match[1]) * 1024;
|
|
36581
|
+
} catch {
|
|
36582
|
+
return 0;
|
|
36583
|
+
}
|
|
36584
|
+
}
|
|
36585
|
+
function readProcStringOrNull(path) {
|
|
36586
|
+
try {
|
|
36587
|
+
return readFileSync22(path, "utf-8");
|
|
36588
|
+
} catch {
|
|
36589
|
+
return null;
|
|
36590
|
+
}
|
|
36591
|
+
}
|
|
36592
|
+
function readProcCwdOrNull(pid, procRoot) {
|
|
36593
|
+
try {
|
|
36594
|
+
return readlinkSync2(join24(procRoot, String(pid), "cwd"));
|
|
36595
|
+
} catch {
|
|
36596
|
+
return null;
|
|
36597
|
+
}
|
|
36598
|
+
}
|
|
36599
|
+
function parseStat(stat2) {
|
|
36600
|
+
const closeParen = stat2.lastIndexOf(")");
|
|
36601
|
+
if (closeParen < 0)
|
|
36602
|
+
return null;
|
|
36603
|
+
const fields = stat2.slice(closeParen + 2).trim().replace(/\s+/g, " ").split(" ");
|
|
36604
|
+
const ppid = Number(fields[1]);
|
|
36605
|
+
const utimeTicks = Number(fields[11]);
|
|
36606
|
+
const stimeTicks = Number(fields[12]);
|
|
36607
|
+
const startTimeTicks = Number(fields[19]);
|
|
36608
|
+
if (![ppid, utimeTicks, stimeTicks, startTimeTicks].every(Number.isFinite))
|
|
36609
|
+
return null;
|
|
36610
|
+
return { ppid, utimeTicks, stimeTicks, startTimeTicks };
|
|
36611
|
+
}
|
|
36612
|
+
function readProcUptimeSecondsOrNull(procRoot) {
|
|
36613
|
+
try {
|
|
36614
|
+
const match = /^(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)$/.exec(readFileSync22(join24(procRoot, "uptime"), "utf-8").trim());
|
|
36615
|
+
if (!match)
|
|
36616
|
+
return null;
|
|
36617
|
+
return Number(match[1]);
|
|
36618
|
+
} catch {
|
|
36619
|
+
return null;
|
|
36620
|
+
}
|
|
36621
|
+
}
|
|
36622
|
+
function readProcessLiveness(pid, procRoot) {
|
|
36623
|
+
if (!existsSync23(join24(procRoot, String(pid))))
|
|
36624
|
+
return "dead";
|
|
36625
|
+
try {
|
|
36626
|
+
process.kill(pid, 0);
|
|
36627
|
+
return "alive";
|
|
36628
|
+
} catch (error2) {
|
|
36629
|
+
if (error2 instanceof Error && "code" in error2 && error2.code === "ESRCH")
|
|
36630
|
+
return "dead";
|
|
36631
|
+
return "alive";
|
|
36632
|
+
}
|
|
36633
|
+
}
|
|
36634
|
+
function readProcessSnapshot(pid, procRoot, uptimeSeconds) {
|
|
36635
|
+
const basePath = join24(procRoot, String(pid));
|
|
36636
|
+
const cmdlineRaw = readProcStringOrNull(join24(basePath, "cmdline"));
|
|
36637
|
+
if (!cmdlineRaw)
|
|
36638
|
+
return null;
|
|
36639
|
+
const cmdline = cmdlineRaw.replace(/\0/g, " ").trim();
|
|
36640
|
+
if (!cmdline)
|
|
36641
|
+
return null;
|
|
36642
|
+
const comm = (readProcStringOrNull(join24(basePath, "comm")) ?? "").trim();
|
|
36643
|
+
const stat2 = readProcStringOrNull(join24(basePath, "stat"));
|
|
36644
|
+
const parsedStat = stat2 ? parseStat(stat2) : null;
|
|
36645
|
+
if (!parsedStat)
|
|
36646
|
+
return null;
|
|
36647
|
+
const status = readProcStringOrNull(join24(basePath, "status")) ?? "";
|
|
36648
|
+
const rssMatch = /VmRSS:\s+(\d+)\s+kB/m.exec(status);
|
|
36649
|
+
const rssBytes = rssMatch ? Number(rssMatch[1]) * 1024 : 0;
|
|
36650
|
+
const cwd = readProcCwdOrNull(pid, procRoot);
|
|
36651
|
+
const cpuSeconds = (parsedStat.utimeTicks + parsedStat.stimeTicks) / CLOCK_TICKS_PER_SECOND;
|
|
36652
|
+
const ageSeconds = Math.max(0, uptimeSeconds - parsedStat.startTimeTicks / CLOCK_TICKS_PER_SECOND);
|
|
36653
|
+
const cpuPct = ageSeconds > 0 ? cpuSeconds / ageSeconds * 100 : 0;
|
|
36654
|
+
return { pid, ppid: parsedStat.ppid, cmdline, comm, cwd, rssBytes, cpuPct, ageSeconds };
|
|
36655
|
+
}
|
|
36656
|
+
function listPids(procRoot) {
|
|
36657
|
+
if (!existsSync23(procRoot))
|
|
36658
|
+
return [];
|
|
36659
|
+
const pids = [];
|
|
36660
|
+
for (const entry of readdirSync9(procRoot, { withFileTypes: true })) {
|
|
36661
|
+
if (!entry.isDirectory())
|
|
36662
|
+
continue;
|
|
36663
|
+
const pid = Number(entry.name);
|
|
36664
|
+
if (Number.isInteger(pid) && pid > 0)
|
|
36665
|
+
pids.push(pid);
|
|
36666
|
+
}
|
|
36667
|
+
return pids;
|
|
36668
|
+
}
|
|
36669
|
+
function getWorktreeFromCwd(cwd) {
|
|
36670
|
+
if (!cwd)
|
|
36671
|
+
return null;
|
|
36672
|
+
const marker = "/.worktrees/";
|
|
36673
|
+
const index = cwd.indexOf(marker);
|
|
36674
|
+
if (index < 0)
|
|
36675
|
+
return null;
|
|
36676
|
+
const tail = cwd.slice(index + marker.length);
|
|
36677
|
+
const slash = tail.indexOf("/");
|
|
36678
|
+
return cwd.slice(0, index + marker.length + (slash < 0 ? tail.length : slash));
|
|
36679
|
+
}
|
|
36680
|
+
function basename6(command) {
|
|
36681
|
+
return command.split("/").pop() ?? command;
|
|
36682
|
+
}
|
|
36683
|
+
function isShellWrapper(command) {
|
|
36684
|
+
return ["sh", "bash", "zsh", "fish"].includes(basename6(command));
|
|
36685
|
+
}
|
|
36686
|
+
function isSpecialistRunCommand(cmdline) {
|
|
36687
|
+
const tokens = cmdline.split(/\s+/).filter(Boolean);
|
|
36688
|
+
if (tokens.length === 0 || isShellWrapper(tokens[0]))
|
|
36689
|
+
return false;
|
|
36690
|
+
const commandIndex = tokens.findIndex((token) => ["specialists", "sp"].includes(basename6(token)));
|
|
36691
|
+
return commandIndex >= 0 && tokens[commandIndex + 1] === "run";
|
|
36692
|
+
}
|
|
36693
|
+
function isPiAgentProcess(snapshot) {
|
|
36694
|
+
return snapshot.cmdline.includes("pi-coding-agent");
|
|
36695
|
+
}
|
|
36696
|
+
function isPiOrphanCandidate(snapshot) {
|
|
36697
|
+
return snapshot.comm === "pi" || isPiAgentProcess(snapshot);
|
|
36698
|
+
}
|
|
36699
|
+
function isDeletedCwdToolProcess(snapshot) {
|
|
36700
|
+
if (!snapshot.cwd?.includes("(deleted)"))
|
|
36701
|
+
return false;
|
|
36702
|
+
return isPiOrphanCandidate(snapshot) || isSpecialistRunCommand(snapshot.cmdline) || snapshot.cmdline.includes("gitnexus") || snapshot.cmdline.includes("serena") || snapshot.cmdline.includes("tsserver");
|
|
36703
|
+
}
|
|
36704
|
+
function classifyProcess(snapshot) {
|
|
36705
|
+
const { cmdline, ppid } = snapshot;
|
|
36706
|
+
if (cmdline.includes("dolt sql-server"))
|
|
36707
|
+
return "dolt";
|
|
36708
|
+
if (cmdline.includes("serena") && (cmdline.includes("language-server") || cmdline.includes("lsp")))
|
|
36709
|
+
return "serena-lsp";
|
|
36710
|
+
if (isDeletedCwdToolProcess(snapshot))
|
|
36711
|
+
return "orphan";
|
|
36712
|
+
if ((isPiOrphanCandidate(snapshot) || cmdline.includes("gitnexus") && cmdline.includes("mcp")) && ppid === 1)
|
|
36713
|
+
return "orphan";
|
|
36714
|
+
if (isPiAgentProcess(snapshot) || isSpecialistRunCommand(cmdline))
|
|
36715
|
+
return "specialist";
|
|
36716
|
+
return null;
|
|
36717
|
+
}
|
|
36718
|
+
function getOrphanReason(snapshot) {
|
|
36719
|
+
if (snapshot.cmdline.includes("dolt sql-server"))
|
|
36720
|
+
return "dolt-worktree-local";
|
|
36721
|
+
if (isDeletedCwdToolProcess(snapshot))
|
|
36722
|
+
return "deleted-worktree-process";
|
|
36723
|
+
if (snapshot.cmdline.includes("gitnexus") && snapshot.cmdline.includes("mcp") && snapshot.ppid === 1)
|
|
36724
|
+
return "gitnexus-orphan";
|
|
36725
|
+
if (isPiOrphanCandidate(snapshot) && snapshot.ppid === 1)
|
|
36726
|
+
return "pi-orphan";
|
|
36727
|
+
return null;
|
|
36728
|
+
}
|
|
36729
|
+
function toProcessHealthProcess(snapshot, kind) {
|
|
36730
|
+
return {
|
|
36731
|
+
pid: snapshot.pid,
|
|
36732
|
+
ppid: snapshot.ppid,
|
|
36733
|
+
kind,
|
|
36734
|
+
role: kind,
|
|
36735
|
+
cmdline: snapshot.cmdline,
|
|
36736
|
+
cwd: snapshot.cwd,
|
|
36737
|
+
rssBytes: snapshot.rssBytes,
|
|
36738
|
+
cpuPct: snapshot.cpuPct,
|
|
36739
|
+
ageSeconds: snapshot.ageSeconds,
|
|
36740
|
+
worktree: getWorktreeFromCwd(snapshot.cwd)
|
|
36741
|
+
};
|
|
36742
|
+
}
|
|
36743
|
+
function collectProcessHealth(options = {}) {
|
|
36744
|
+
const procRoot = options.procRoot ?? "/proc";
|
|
36745
|
+
const meminfoPath = options.meminfoPath ?? "/proc/meminfo";
|
|
36746
|
+
const uptimeSeconds = readProcUptimeSecondsOrNull(procRoot) ?? (options.nowMs ?? Date.now()) / 1000;
|
|
36747
|
+
const thresholds = getProcessHealthThresholds();
|
|
36748
|
+
const memAvailableBytes = readMemAvailableBytes(meminfoPath);
|
|
36749
|
+
const processes = [];
|
|
36750
|
+
for (const pid of listPids(procRoot)) {
|
|
36751
|
+
const snapshot = readProcessSnapshot(pid, procRoot, uptimeSeconds);
|
|
36752
|
+
if (!snapshot)
|
|
36753
|
+
continue;
|
|
36754
|
+
const kind = classifyProcess(snapshot);
|
|
36755
|
+
if (!kind)
|
|
36756
|
+
continue;
|
|
36757
|
+
const process3 = toProcessHealthProcess(snapshot, kind);
|
|
36758
|
+
if (kind === "orphan")
|
|
36759
|
+
process3.reason = getOrphanReason(snapshot) ?? undefined;
|
|
36760
|
+
processes.push(process3);
|
|
36761
|
+
}
|
|
36762
|
+
const specialistProcesses = processes.filter((process3) => process3.kind === "specialist");
|
|
36763
|
+
const doltProcesses = processes.filter((process3) => process3.kind === "dolt");
|
|
36764
|
+
const serenaProcesses = processes.filter((process3) => process3.kind === "serena-lsp");
|
|
36765
|
+
const orphanProcesses = processes.filter((process3) => process3.kind === "orphan");
|
|
36766
|
+
const serenaMap = new Map;
|
|
36767
|
+
for (const process3 of serenaProcesses) {
|
|
36768
|
+
const workspace = process3.worktree ?? process3.cwd ?? "unknown";
|
|
36769
|
+
if (!serenaMap.has(workspace))
|
|
36770
|
+
serenaMap.set(workspace, []);
|
|
36771
|
+
serenaMap.get(workspace).push(process3);
|
|
36772
|
+
}
|
|
36773
|
+
const warnLimitBytes = Math.floor(memAvailableBytes * (thresholds.warnPct / 100));
|
|
36774
|
+
const refuseLimitBytes = Math.floor(memAvailableBytes * (thresholds.refusePct / 100));
|
|
36775
|
+
const totalRssBytes = processes.reduce((sum, process3) => sum + process3.rssBytes, 0);
|
|
36776
|
+
const totalCpuPct = processes.reduce((sum, process3) => sum + process3.cpuPct, 0);
|
|
36777
|
+
const thresholdPct = memAvailableBytes > 0 ? totalRssBytes / memAvailableBytes * 100 : 0;
|
|
36778
|
+
const statusReasons = [];
|
|
36779
|
+
if (thresholdPct >= thresholds.refusePct)
|
|
36780
|
+
statusReasons.push(`rss >= refuse threshold (${thresholds.refusePct}%)`);
|
|
36781
|
+
else if (thresholdPct >= thresholds.warnPct)
|
|
36782
|
+
statusReasons.push(`rss >= warn threshold (${thresholds.warnPct}%)`);
|
|
36783
|
+
if (doltProcesses.length > 1)
|
|
36784
|
+
statusReasons.push(`dolt sql-server count ${doltProcesses.length} > expected 1`);
|
|
36785
|
+
if (orphanProcesses.length > 0)
|
|
36786
|
+
statusReasons.push(`orphan process count ${orphanProcesses.length} > 0`);
|
|
36787
|
+
const status = thresholdPct >= thresholds.refusePct ? "REFUSE" : statusReasons.length > 0 ? "WARN" : "OK";
|
|
36788
|
+
return {
|
|
36789
|
+
status,
|
|
36790
|
+
statusReasons,
|
|
36791
|
+
memAvailableBytes,
|
|
36792
|
+
totalRssBytes,
|
|
36793
|
+
totalCpuPct,
|
|
36794
|
+
specialistCount: specialistProcesses.length,
|
|
36795
|
+
doltCount: doltProcesses.length,
|
|
36796
|
+
serenaLspCount: serenaProcesses.length,
|
|
36797
|
+
orphanCount: orphanProcesses.length,
|
|
36798
|
+
thresholdPct,
|
|
36799
|
+
warnPct: thresholds.warnPct,
|
|
36800
|
+
refusePct: thresholds.refusePct,
|
|
36801
|
+
warnLimitBytes,
|
|
36802
|
+
refuseLimitBytes,
|
|
36803
|
+
specialistProcesses,
|
|
36804
|
+
doltProcesses,
|
|
36805
|
+
serenaWorkspaces: [...serenaMap.entries()].map(([workspace, workspaceProcesses]) => ({
|
|
36806
|
+
workspace,
|
|
36807
|
+
count: workspaceProcesses.length,
|
|
36808
|
+
rssBytes: workspaceProcesses.reduce((sum, process3) => sum + process3.rssBytes, 0),
|
|
36809
|
+
processes: workspaceProcesses
|
|
36810
|
+
})).sort((left, right) => right.rssBytes - left.rssBytes),
|
|
36811
|
+
orphanProcesses
|
|
36812
|
+
};
|
|
36813
|
+
}
|
|
36814
|
+
function withReason(process3, reason) {
|
|
36815
|
+
return { ...process3, reason };
|
|
36816
|
+
}
|
|
36817
|
+
function hasDeletedCwd(process3) {
|
|
36818
|
+
return Boolean(process3.cwd?.includes("(deleted)"));
|
|
36819
|
+
}
|
|
36820
|
+
function collectOrphanProcesses(options = {}) {
|
|
36821
|
+
const health = collectProcessHealth(options);
|
|
36822
|
+
const reaped = new Map;
|
|
36823
|
+
for (const process3 of health.orphanProcesses)
|
|
36824
|
+
reaped.set(process3.pid, process3);
|
|
36825
|
+
for (const process3 of health.doltProcesses) {
|
|
36826
|
+
if (hasDeletedCwd(process3))
|
|
36827
|
+
reaped.set(process3.pid, withReason(process3, "dolt-worktree-local"));
|
|
36828
|
+
}
|
|
36829
|
+
for (const process3 of health.specialistProcesses) {
|
|
36830
|
+
if (hasDeletedCwd(process3))
|
|
36831
|
+
reaped.set(process3.pid, withReason(process3, "deleted-worktree-process"));
|
|
36832
|
+
}
|
|
36833
|
+
for (const workspace of health.serenaWorkspaces) {
|
|
36834
|
+
for (const process3 of workspace.processes) {
|
|
36835
|
+
if (hasDeletedCwd(process3))
|
|
36836
|
+
reaped.set(process3.pid, withReason(process3, "deleted-worktree-process"));
|
|
36837
|
+
}
|
|
36838
|
+
}
|
|
36839
|
+
return [...reaped.values()].sort((left, right) => left.pid - right.pid);
|
|
36840
|
+
}
|
|
36841
|
+
function collectStaleSpecialistJobs(options = {}) {
|
|
36842
|
+
const procRoot = options.procRoot ?? "/proc";
|
|
36843
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
36844
|
+
const minKeepAliveAgeMs = options.minKeepAliveAgeMs ?? 30 * 60 * 1000;
|
|
36845
|
+
const observabilityClient = options.observabilityClient ?? createObservabilitySqliteClient();
|
|
36846
|
+
const statuses = observabilityClient?.listStatuses() ?? [];
|
|
36847
|
+
const staleStatuses = statuses.filter((status) => ["starting", "running", "waiting"].includes(status.status));
|
|
36848
|
+
const uptimeSeconds = readProcUptimeSecondsOrNull(procRoot) ?? nowMs / 1000;
|
|
36849
|
+
const candidates = [];
|
|
36850
|
+
for (const status of staleStatuses) {
|
|
36851
|
+
const pid = status.pid;
|
|
36852
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
36853
|
+
continue;
|
|
36854
|
+
const ageMs = Math.max(0, nowMs - (status.updated_at_ms ?? nowMs));
|
|
36855
|
+
const snapshot = readProcessSnapshot(pid, procRoot, uptimeSeconds);
|
|
36856
|
+
if (!snapshot) {
|
|
36857
|
+
if (readProcessLiveness(pid, procRoot) === "dead" && ageMs >= minKeepAliveAgeMs) {
|
|
36858
|
+
candidates.push({ jobId: status.id, pid, beadId: status.bead_id ?? null, specialist: status.specialist, cwd: null, ageMs, reason: "dead-pid" });
|
|
36859
|
+
continue;
|
|
36860
|
+
}
|
|
36861
|
+
const basePath = join24(procRoot, String(pid));
|
|
36862
|
+
const cmdlineRaw = readProcStringOrNull(join24(basePath, "cmdline"));
|
|
36863
|
+
const statRaw = readProcStringOrNull(join24(basePath, "stat"));
|
|
36864
|
+
const parsedStat = statRaw ? parseStat(statRaw) : null;
|
|
36865
|
+
if (status.status === "waiting" && parsedStat?.ppid === 1 && cmdlineRaw && isSpecialistRunCommand(cmdlineRaw.replace(/\0/g, " "))) {
|
|
36866
|
+
candidates.push({ jobId: status.id, pid, beadId: status.bead_id ?? null, specialist: status.specialist, cwd: readProcCwdOrNull(pid, procRoot), ageMs, reason: "orphaned-keep-alive" });
|
|
36867
|
+
}
|
|
36868
|
+
continue;
|
|
36869
|
+
}
|
|
36870
|
+
if (status.status === "waiting" && snapshot.ppid === 1 && isSpecialistRunCommand(snapshot.cmdline) && ageMs >= minKeepAliveAgeMs) {
|
|
36871
|
+
candidates.push({ jobId: status.id, pid, beadId: status.bead_id ?? null, specialist: status.specialist, cwd: snapshot.cwd, ageMs, reason: "orphaned-keep-alive" });
|
|
36872
|
+
continue;
|
|
36873
|
+
}
|
|
36874
|
+
if (status.status !== "running" && status.status !== "waiting")
|
|
36875
|
+
continue;
|
|
36876
|
+
if (ageMs < minKeepAliveAgeMs)
|
|
36877
|
+
continue;
|
|
36878
|
+
const lastActivityMs = observabilityClient?.getLastActivityTimestampMs?.(status.id) ?? null;
|
|
36879
|
+
if (lastActivityMs !== null && nowMs - lastActivityMs < minKeepAliveAgeMs)
|
|
36880
|
+
continue;
|
|
36881
|
+
candidates.push({ jobId: status.id, pid, beadId: status.bead_id ?? null, specialist: status.specialist, cwd: snapshot.cwd, ageMs, reason: "dead-toolchain" });
|
|
36882
|
+
}
|
|
36883
|
+
return candidates.sort((left, right) => left.pid - right.pid);
|
|
36884
|
+
}
|
|
36885
|
+
var DEFAULT_WARN_PCT = 70, DEFAULT_REFUSE_PCT = 85, CLOCK_TICKS_PER_SECOND = 100;
|
|
36886
|
+
var init_process_health = __esm(() => {
|
|
36887
|
+
init_observability_sqlite();
|
|
36888
|
+
});
|
|
36889
|
+
|
|
36148
36890
|
// src/cli/ps.ts
|
|
36149
36891
|
var exports_ps = {};
|
|
36150
36892
|
__export(exports_ps, {
|
|
36151
36893
|
run: () => run16
|
|
36152
36894
|
});
|
|
36153
36895
|
import { spawnSync as spawnSync18 } from "child_process";
|
|
36154
|
-
import { existsSync as
|
|
36155
|
-
import { join as
|
|
36896
|
+
import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync23 } from "fs";
|
|
36897
|
+
import { join as join25 } from "path";
|
|
36156
36898
|
function loadBeadIdsForCurrentUser() {
|
|
36157
36899
|
const ids = new Set;
|
|
36158
36900
|
try {
|
|
@@ -36182,12 +36924,30 @@ function parseSinceArg(value) {
|
|
|
36182
36924
|
return Date.now() - ms;
|
|
36183
36925
|
}
|
|
36184
36926
|
function parseArgs8(argv) {
|
|
36927
|
+
const allowedBooleanFlags = new Set([
|
|
36928
|
+
"--json",
|
|
36929
|
+
"--all",
|
|
36930
|
+
"--follow",
|
|
36931
|
+
"-f",
|
|
36932
|
+
"--include-terminal",
|
|
36933
|
+
"--include-merged",
|
|
36934
|
+
"--include-cleaned",
|
|
36935
|
+
"--active",
|
|
36936
|
+
"--running",
|
|
36937
|
+
"--mine",
|
|
36938
|
+
"--health"
|
|
36939
|
+
]);
|
|
36940
|
+
const valueFlags = new Set(["--node", "--bead", "--since"]);
|
|
36185
36941
|
let nodeId;
|
|
36186
36942
|
let beadFilter;
|
|
36187
36943
|
let sinceMs;
|
|
36188
36944
|
const positional = [];
|
|
36189
36945
|
for (let i = 0;i < argv.length; i += 1) {
|
|
36190
36946
|
const token = argv[i];
|
|
36947
|
+
if (token.startsWith("-") && !allowedBooleanFlags.has(token) && !valueFlags.has(token)) {
|
|
36948
|
+
const hint = token === "--ps" ? " Did you mean `sp clean --ps`?" : "";
|
|
36949
|
+
throw new Error(`Unknown ps option: ${token}.${hint}`);
|
|
36950
|
+
}
|
|
36191
36951
|
if (token === "--node" && argv[i + 1]) {
|
|
36192
36952
|
nodeId = argv[i + 1];
|
|
36193
36953
|
i += 1;
|
|
@@ -36213,39 +36973,44 @@ function parseArgs8(argv) {
|
|
|
36213
36973
|
all: argv.includes("--all"),
|
|
36214
36974
|
follow: argv.includes("--follow") || argv.includes("-f"),
|
|
36215
36975
|
includeTerminal,
|
|
36216
|
-
|
|
36976
|
+
includeCleaned: argv.includes("--include-cleaned"),
|
|
36977
|
+
active: argv.includes("--active"),
|
|
36978
|
+
running: argv.includes("--running") || argv.includes("--active"),
|
|
36217
36979
|
mine: argv.includes("--mine"),
|
|
36980
|
+
health: argv.includes("--health"),
|
|
36218
36981
|
beadFilter,
|
|
36219
36982
|
sinceMs,
|
|
36220
36983
|
nodeId,
|
|
36221
36984
|
inspectId: positional[0]
|
|
36222
36985
|
};
|
|
36223
36986
|
}
|
|
36224
|
-
function
|
|
36225
|
-
|
|
36226
|
-
|
|
36227
|
-
|
|
36987
|
+
function isPsCleaned(job) {
|
|
36988
|
+
const typed = job;
|
|
36989
|
+
return Boolean(typed.ps_hidden_at ?? typed.ps_hidden_from_dashboard_at);
|
|
36990
|
+
}
|
|
36991
|
+
function isDefaultActionableTerminal(job) {
|
|
36992
|
+
return job.status === "error" || job.status === "cancelled";
|
|
36228
36993
|
}
|
|
36229
36994
|
function readStatusesFromFiles(jobsDir) {
|
|
36230
|
-
if (!
|
|
36995
|
+
if (!existsSync24(jobsDir))
|
|
36231
36996
|
return [];
|
|
36232
36997
|
const statuses = [];
|
|
36233
|
-
for (const entry of
|
|
36234
|
-
const statusPath =
|
|
36235
|
-
if (!
|
|
36998
|
+
for (const entry of readdirSync10(jobsDir)) {
|
|
36999
|
+
const statusPath = join25(jobsDir, entry, "status.json");
|
|
37000
|
+
if (!existsSync24(statusPath))
|
|
36236
37001
|
continue;
|
|
36237
37002
|
try {
|
|
36238
|
-
statuses.push(JSON.parse(
|
|
37003
|
+
statuses.push(JSON.parse(readFileSync23(statusPath, "utf-8")));
|
|
36239
37004
|
} catch {}
|
|
36240
37005
|
}
|
|
36241
37006
|
return statuses.sort((a, b) => b.started_at_ms - a.started_at_ms);
|
|
36242
37007
|
}
|
|
36243
37008
|
function readLastToolEventFromFile(jobsDir, jobId) {
|
|
36244
|
-
const eventsPath =
|
|
36245
|
-
if (!
|
|
37009
|
+
const eventsPath = join25(jobsDir, jobId, "events.jsonl");
|
|
37010
|
+
if (!existsSync24(eventsPath))
|
|
36246
37011
|
return;
|
|
36247
37012
|
try {
|
|
36248
|
-
const lines =
|
|
37013
|
+
const lines = readFileSync23(eventsPath, "utf-8").split(`
|
|
36249
37014
|
`);
|
|
36250
37015
|
for (let index = lines.length - 1;index >= 0; index -= 1) {
|
|
36251
37016
|
const line = lines[index]?.trim();
|
|
@@ -36536,7 +37301,7 @@ function statusLabel(status) {
|
|
|
36536
37301
|
}
|
|
36537
37302
|
function epicStateLabel(state) {
|
|
36538
37303
|
if (state === "merge_ready")
|
|
36539
|
-
return green8("
|
|
37304
|
+
return green8("pass");
|
|
36540
37305
|
if (state === "merged")
|
|
36541
37306
|
return dim8("merged");
|
|
36542
37307
|
if (state === "failed")
|
|
@@ -36544,10 +37309,10 @@ function epicStateLabel(state) {
|
|
|
36544
37309
|
if (state === "blocked")
|
|
36545
37310
|
return yellow10("blocked");
|
|
36546
37311
|
if (state === "resolving")
|
|
36547
|
-
return cyan5("
|
|
37312
|
+
return cyan5("merge_ready");
|
|
36548
37313
|
if (state === "abandoned")
|
|
36549
37314
|
return dim8("abandoned");
|
|
36550
|
-
return magenta3("
|
|
37315
|
+
return magenta3("no pass yet");
|
|
36551
37316
|
}
|
|
36552
37317
|
function withPidLiveness(statuses) {
|
|
36553
37318
|
return statuses.map((job) => ({
|
|
@@ -36704,6 +37469,50 @@ function renderTreeNodes(nodes, beadTitles, prefix, renderedJobIds) {
|
|
|
36704
37469
|
}
|
|
36705
37470
|
}
|
|
36706
37471
|
}
|
|
37472
|
+
function formatProcessRow(process3) {
|
|
37473
|
+
const cwd = process3.cwd ?? "--";
|
|
37474
|
+
const rssMb = `${(process3.rssBytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
37475
|
+
const cpu = `${process3.cpuPct.toFixed(1)}%`;
|
|
37476
|
+
const age = `${Math.floor(process3.ageSeconds / 60)}m`;
|
|
37477
|
+
return ` ${String(process3.pid).padEnd(7)} ${process3.role.padEnd(14)} ${rssMb.padEnd(8)} ${cpu.padEnd(7)} ${age.padEnd(5)} ${cwd}`;
|
|
37478
|
+
}
|
|
37479
|
+
function renderProcessHealthBlock(report, includeDetails) {
|
|
37480
|
+
const percent = report.thresholdPct.toFixed(1);
|
|
37481
|
+
const severity = report.status === "REFUSE" ? red2("REFUSE") : report.status === "WARN" ? yellow10("WARN") : green8("OK");
|
|
37482
|
+
console.log(bold10(cyan5("System health")));
|
|
37483
|
+
console.log(` ${severity} rss=${(report.totalRssBytes / (1024 * 1024)).toFixed(1)}MB avail=${(report.memAvailableBytes / (1024 * 1024)).toFixed(1)}MB used=${percent}% warn=${report.warnPct}% refuse=${report.refusePct}% cpu=${report.totalCpuPct.toFixed(1)}%`);
|
|
37484
|
+
console.log(` specialists=${report.specialistCount} dolt=${report.doltCount} serena-lsp=${report.serenaLspCount} orphans=${report.orphanCount}`);
|
|
37485
|
+
if (report.statusReasons.length > 0)
|
|
37486
|
+
console.log(` alerts=${report.statusReasons.join("; ")}`);
|
|
37487
|
+
if (!includeDetails) {
|
|
37488
|
+
console.log("");
|
|
37489
|
+
return;
|
|
37490
|
+
}
|
|
37491
|
+
if (report.doltProcesses.length > 0) {
|
|
37492
|
+
console.log(bold10(" Dolt sql-server"));
|
|
37493
|
+
for (const process3 of report.doltProcesses)
|
|
37494
|
+
console.log(formatProcessRow(process3));
|
|
37495
|
+
}
|
|
37496
|
+
if (report.serenaWorkspaces.length > 0) {
|
|
37497
|
+
console.log(bold10(" Serena LSP"));
|
|
37498
|
+
for (const workspace of report.serenaWorkspaces) {
|
|
37499
|
+
console.log(` ${workspace.workspace} \xB7 ${workspace.count} procs \xB7 ${(workspace.rssBytes / (1024 * 1024)).toFixed(1)}MB`);
|
|
37500
|
+
for (const process3 of workspace.processes)
|
|
37501
|
+
console.log(formatProcessRow(process3));
|
|
37502
|
+
}
|
|
37503
|
+
}
|
|
37504
|
+
if (report.specialistProcesses.length > 0) {
|
|
37505
|
+
console.log(bold10(" Specialists"));
|
|
37506
|
+
for (const process3 of report.specialistProcesses)
|
|
37507
|
+
console.log(formatProcessRow(process3));
|
|
37508
|
+
}
|
|
37509
|
+
if (report.orphanProcesses.length > 0) {
|
|
37510
|
+
console.log(bold10(" Orphans"));
|
|
37511
|
+
for (const process3 of report.orphanProcesses)
|
|
37512
|
+
console.log(formatProcessRow(process3));
|
|
37513
|
+
}
|
|
37514
|
+
console.log("");
|
|
37515
|
+
}
|
|
36707
37516
|
function resolveEpicReadinessMap(jobs, includeTerminal) {
|
|
36708
37517
|
const epicIds = new Set(jobs.map((job) => job.epic_id).filter((epicId) => Boolean(epicId)));
|
|
36709
37518
|
const sqlite = createObservabilitySqliteClient();
|
|
@@ -36730,12 +37539,12 @@ function resolveEpicReadinessMap(jobs, includeTerminal) {
|
|
|
36730
37539
|
sqlite.close();
|
|
36731
37540
|
}
|
|
36732
37541
|
}
|
|
36733
|
-
function renderHuman(jobs, nodes, trees, all, includeTerminal, epicReadiness) {
|
|
37542
|
+
function renderHuman(jobs, nodes, trees, all, includeTerminal, epicReadiness, health, includeHealthDetails) {
|
|
36734
37543
|
const beadTitles = buildBeadTitleCache(jobs);
|
|
36735
37544
|
const renderedJobIds = new Set;
|
|
36736
37545
|
const epicGroups = buildEpicGroups(jobs, epicReadiness);
|
|
36737
37546
|
const renderedEpicIds = new Set(epicGroups.map((epic) => epic.epic_id));
|
|
36738
|
-
|
|
37547
|
+
renderProcessHealthBlock(health, includeHealthDetails);
|
|
36739
37548
|
for (const epic of epicGroups) {
|
|
36740
37549
|
const prepCount = epic.prep_jobs.length;
|
|
36741
37550
|
const chainCount = epic.chains.length;
|
|
@@ -36744,9 +37553,9 @@ function renderHuman(jobs, nodes, trees, all, includeTerminal, epicReadiness) {
|
|
|
36744
37553
|
const persistedState = readiness?.persisted_state ?? "open";
|
|
36745
37554
|
const prepSummary = readiness?.prep ? `prep ${readiness.prep.done}/${readiness.prep.total} done${readiness.prep.running > 0 ? ` ${readiness.prep.running} running` : ""}${readiness.prep.failed > 0 ? ` ${readiness.prep.failed} failed` : ""}` : `prep ${prepCount}`;
|
|
36746
37555
|
const chainSummary = readiness?.chains ? `chains ${readiness.chains.filter((chain) => chain.state === "pass").length}/${readiness.chains.length} pass` : `chains ${chainCount}`;
|
|
36747
|
-
const epicBanner = bold10(cyan5(`\u250F\u2501 EPIC ${epic.epic_id} \u2501 ${
|
|
37556
|
+
const epicBanner = bold10(cyan5(`\u250F\u2501 EPIC ${epic.epic_id} \u2501 ${epicStateLabel(readiness?.readiness_state)} \u2501 ${prepSummary} \u2501 ${chainSummary}`));
|
|
36748
37557
|
console.log(epicBanner);
|
|
36749
|
-
console.log(` ${dim8(`
|
|
37558
|
+
console.log(` ${dim8(`derived:${readinessState}`)} \xB7 ${dim8(`stored:${persistedState}`)}`);
|
|
36750
37559
|
console.log(` ${bold10("Prep")}`);
|
|
36751
37560
|
if (epic.prep_jobs.length === 0) {
|
|
36752
37561
|
console.log(dim8(" (none)"));
|
|
@@ -36891,7 +37700,7 @@ ${job.id} ${job.specialist} ${getStatusIcon(toJobNode(job))} ${statusLabel(job
|
|
|
36891
37700
|
console.log(`
|
|
36892
37701
|
${dim8(inspectActions.join(" | "))}`);
|
|
36893
37702
|
}
|
|
36894
|
-
function renderJson(jobs, nodes, trees, _all, epicReadiness, args) {
|
|
37703
|
+
function renderJson(jobs, nodes, trees, _all, epicReadiness, args, health) {
|
|
36895
37704
|
console.log(JSON.stringify({
|
|
36896
37705
|
generated_at_ms: Date.now(),
|
|
36897
37706
|
include_terminal: args.includeTerminal,
|
|
@@ -36929,7 +37738,8 @@ function renderJson(jobs, nodes, trees, _all, epicReadiness, args) {
|
|
|
36929
37738
|
nodes,
|
|
36930
37739
|
trees,
|
|
36931
37740
|
epics: buildEpicGroups(jobs, epicReadiness),
|
|
36932
|
-
epic_readiness: Object.fromEntries([...epicReadiness.entries()].map(([epicId, summary]) => [epicId, summary]))
|
|
37741
|
+
epic_readiness: Object.fromEntries([...epicReadiness.entries()].map(([epicId, summary]) => [epicId, summary])),
|
|
37742
|
+
process_health: health
|
|
36933
37743
|
}, null, 2));
|
|
36934
37744
|
}
|
|
36935
37745
|
function dedupeStatusesById(statuses) {
|
|
@@ -36963,30 +37773,31 @@ function render(args) {
|
|
|
36963
37773
|
return false;
|
|
36964
37774
|
if (mineBeadIds && (!job.bead_id || !mineBeadIds.has(job.bead_id)))
|
|
36965
37775
|
return false;
|
|
36966
|
-
const
|
|
36967
|
-
if (epicTerminal && !args.includeTerminal && !args.all)
|
|
36968
|
-
return false;
|
|
37776
|
+
const cleaned = isPsCleaned(job);
|
|
36969
37777
|
if (args.all)
|
|
36970
37778
|
return true;
|
|
36971
|
-
if (
|
|
37779
|
+
if (cleaned && !args.includeCleaned)
|
|
36972
37780
|
return false;
|
|
36973
|
-
if (
|
|
37781
|
+
if (cleaned && args.includeCleaned && TERMINAL_STATES.includes(job.status))
|
|
36974
37782
|
return true;
|
|
36975
|
-
if (
|
|
36976
|
-
return false;
|
|
36977
|
-
if (!TERMINAL_STATES.includes(job.status))
|
|
37783
|
+
if (job.is_dead)
|
|
36978
37784
|
return false;
|
|
36979
|
-
if (
|
|
37785
|
+
if (ACTIVE_STATES.includes(job.status))
|
|
37786
|
+
return true;
|
|
37787
|
+
if (args.active)
|
|
36980
37788
|
return false;
|
|
36981
|
-
|
|
37789
|
+
if (args.includeTerminal && TERMINAL_STATES.includes(job.status))
|
|
37790
|
+
return true;
|
|
37791
|
+
return isDefaultActionableTerminal(job);
|
|
36982
37792
|
});
|
|
36983
37793
|
const nodes = groupByNode(visibleStatuses);
|
|
36984
37794
|
const trees = groupByTree(visibleStatuses);
|
|
37795
|
+
const health = collectProcessHealth();
|
|
36985
37796
|
if (args.json) {
|
|
36986
|
-
renderJson(visibleStatuses, nodes, trees, args.all, epicReadiness, args);
|
|
37797
|
+
renderJson(visibleStatuses, nodes, trees, args.all, epicReadiness, args, health);
|
|
36987
37798
|
return;
|
|
36988
37799
|
}
|
|
36989
|
-
renderHuman(visibleStatuses, nodes, trees, args.all, args.includeTerminal, epicReadiness);
|
|
37800
|
+
renderHuman(visibleStatuses, nodes, trees, args.all, args.includeTerminal, epicReadiness, health, args.health);
|
|
36990
37801
|
}
|
|
36991
37802
|
function renderBuffered(args) {
|
|
36992
37803
|
const lines = [];
|
|
@@ -37046,6 +37857,7 @@ var init_ps = __esm(() => {
|
|
|
37046
37857
|
init_timeline_events();
|
|
37047
37858
|
init_node_resolve();
|
|
37048
37859
|
init_epic_readiness();
|
|
37860
|
+
init_process_health();
|
|
37049
37861
|
ACTIVE_STATES = ["starting", "running", "waiting"];
|
|
37050
37862
|
TERMINAL_STATES = ["done", "error", "cancelled"];
|
|
37051
37863
|
BEAD_TITLE_CACHE = new Map;
|
|
@@ -37064,8 +37876,8 @@ var exports_result = {};
|
|
|
37064
37876
|
__export(exports_result, {
|
|
37065
37877
|
run: () => run17
|
|
37066
37878
|
});
|
|
37067
|
-
import { existsSync as
|
|
37068
|
-
import { join as
|
|
37879
|
+
import { existsSync as existsSync25, readFileSync as readFileSync24 } from "fs";
|
|
37880
|
+
import { join as join26 } from "path";
|
|
37069
37881
|
function parseArgs9(argv) {
|
|
37070
37882
|
let jobId;
|
|
37071
37883
|
let nodeId;
|
|
@@ -37155,10 +37967,10 @@ function readTimelineEventsForResult(sqliteClient, jobsDir, jobId) {
|
|
|
37155
37967
|
return sqliteClient.readEvents(jobId);
|
|
37156
37968
|
} catch {}
|
|
37157
37969
|
}
|
|
37158
|
-
const eventsPath =
|
|
37159
|
-
if (!
|
|
37970
|
+
const eventsPath = join26(jobsDir, jobId, "events.jsonl");
|
|
37971
|
+
if (!existsSync25(eventsPath))
|
|
37160
37972
|
return [];
|
|
37161
|
-
return
|
|
37973
|
+
return readFileSync24(eventsPath, "utf-8").split(`
|
|
37162
37974
|
`).map((line) => line.trim()).filter(Boolean).map((line) => parseTimelineEvent(line)).filter((event) => event !== null);
|
|
37163
37975
|
}
|
|
37164
37976
|
function deriveStartupSnapshot(status, events) {
|
|
@@ -37277,7 +38089,7 @@ async function run17() {
|
|
|
37277
38089
|
error: error2
|
|
37278
38090
|
}, null, 2));
|
|
37279
38091
|
};
|
|
37280
|
-
const jobsDir =
|
|
38092
|
+
const jobsDir = join26(process.cwd(), ".specialists", "jobs");
|
|
37281
38093
|
const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
|
|
37282
38094
|
const sqliteClient = createObservabilitySqliteClient();
|
|
37283
38095
|
const emitHumanResult = (output2, status, startupContext, trailingFooter) => {
|
|
@@ -37312,7 +38124,7 @@ async function run17() {
|
|
|
37312
38124
|
const resolvedNodeId = args.nodeId ? resolveNodeRefWithClient(args.nodeId, sqliteClient) : resolveSingleActiveNodeRef(sqliteClient);
|
|
37313
38125
|
return resolveJobIdFromNodeMember(sqliteClient, resolvedNodeId, args.memberKey);
|
|
37314
38126
|
})();
|
|
37315
|
-
const resultPath =
|
|
38127
|
+
const resultPath = join26(jobsDir, jobId, "result.txt");
|
|
37316
38128
|
const readResultOutput = () => {
|
|
37317
38129
|
try {
|
|
37318
38130
|
const sqliteResult = sqliteClient?.readResult(jobId) ?? null;
|
|
@@ -37321,8 +38133,8 @@ async function run17() {
|
|
|
37321
38133
|
} catch (error2) {
|
|
37322
38134
|
console.warn(`SQLite result read failed for job ${jobId}; falling back to result.txt`, error2);
|
|
37323
38135
|
}
|
|
37324
|
-
if (
|
|
37325
|
-
return
|
|
38136
|
+
if (existsSync25(resultPath)) {
|
|
38137
|
+
return readFileSync24(resultPath, "utf-8");
|
|
37326
38138
|
}
|
|
37327
38139
|
try {
|
|
37328
38140
|
const events2 = readTimelineEventsForResult(sqliteClient, jobsDir, jobId);
|
|
@@ -37507,10 +38319,10 @@ var init_result = __esm(() => {
|
|
|
37507
38319
|
});
|
|
37508
38320
|
|
|
37509
38321
|
// src/specialist/timeline-query.ts
|
|
37510
|
-
import { existsSync as
|
|
37511
|
-
import { basename as
|
|
38322
|
+
import { existsSync as existsSync26, readdirSync as readdirSync11, readFileSync as readFileSync25 } from "fs";
|
|
38323
|
+
import { basename as basename7, join as join27 } from "path";
|
|
37512
38324
|
function readJobEvents(jobDir) {
|
|
37513
|
-
const jobId =
|
|
38325
|
+
const jobId = basename7(jobDir);
|
|
37514
38326
|
try {
|
|
37515
38327
|
const sqliteEvents = createObservabilitySqliteClient()?.readEvents(jobId) ?? [];
|
|
37516
38328
|
if (sqliteEvents.length > 0) {
|
|
@@ -37520,10 +38332,10 @@ function readJobEvents(jobDir) {
|
|
|
37520
38332
|
} catch {}
|
|
37521
38333
|
if (process.env.SPECIALISTS_JOB_FILE_OUTPUT !== "on")
|
|
37522
38334
|
return [];
|
|
37523
|
-
const eventsPath =
|
|
37524
|
-
if (!
|
|
38335
|
+
const eventsPath = join27(jobDir, "events.jsonl");
|
|
38336
|
+
if (!existsSync26(eventsPath))
|
|
37525
38337
|
return [];
|
|
37526
|
-
const content =
|
|
38338
|
+
const content = readFileSync25(eventsPath, "utf-8");
|
|
37527
38339
|
const lines = content.split(`
|
|
37528
38340
|
`).filter(Boolean);
|
|
37529
38341
|
const events = [];
|
|
@@ -37535,9 +38347,21 @@ function readJobEvents(jobDir) {
|
|
|
37535
38347
|
events.sort(compareTimelineEvents);
|
|
37536
38348
|
return events;
|
|
37537
38349
|
}
|
|
37538
|
-
function readAllJobEvents(jobsDir) {
|
|
38350
|
+
function readAllJobEvents(jobsDir, jobId) {
|
|
37539
38351
|
const sqliteClient = createObservabilitySqliteClient();
|
|
37540
38352
|
try {
|
|
38353
|
+
if (jobId !== undefined && sqliteClient) {
|
|
38354
|
+
const events = sqliteClient.readEvents(jobId);
|
|
38355
|
+
if (events.length === 0)
|
|
38356
|
+
return [];
|
|
38357
|
+
const status = typeof sqliteClient.getStatus === "function" ? sqliteClient.getStatus(jobId) : undefined;
|
|
38358
|
+
return [{
|
|
38359
|
+
jobId,
|
|
38360
|
+
specialist: status?.specialist ?? "unknown",
|
|
38361
|
+
beadId: status?.bead_id,
|
|
38362
|
+
events
|
|
38363
|
+
}];
|
|
38364
|
+
}
|
|
37541
38365
|
const statuses = typeof sqliteClient?.listStatuses === "function" ? sqliteClient.listStatuses() : [];
|
|
37542
38366
|
if (statuses.length > 0 && sqliteClient) {
|
|
37543
38367
|
return statuses.flatMap((status) => {
|
|
@@ -37555,12 +38379,12 @@ function readAllJobEvents(jobsDir) {
|
|
|
37555
38379
|
} catch {}
|
|
37556
38380
|
if (process.env.SPECIALISTS_JOB_FILE_OUTPUT !== "on")
|
|
37557
38381
|
return [];
|
|
37558
|
-
if (!
|
|
38382
|
+
if (!existsSync26(jobsDir))
|
|
37559
38383
|
return [];
|
|
37560
38384
|
const batches = [];
|
|
37561
|
-
const entries =
|
|
38385
|
+
const entries = readdirSync11(jobsDir);
|
|
37562
38386
|
for (const entry of entries) {
|
|
37563
|
-
const jobDir =
|
|
38387
|
+
const jobDir = join27(jobsDir, entry);
|
|
37564
38388
|
try {
|
|
37565
38389
|
const stat2 = __require("fs").statSync(jobDir);
|
|
37566
38390
|
if (!stat2.isDirectory())
|
|
@@ -37568,20 +38392,20 @@ function readAllJobEvents(jobsDir) {
|
|
|
37568
38392
|
} catch {
|
|
37569
38393
|
continue;
|
|
37570
38394
|
}
|
|
37571
|
-
const
|
|
37572
|
-
const statusPath =
|
|
38395
|
+
const jobId2 = entry;
|
|
38396
|
+
const statusPath = join27(jobDir, "status.json");
|
|
37573
38397
|
let specialist = "unknown";
|
|
37574
38398
|
let beadId;
|
|
37575
|
-
if (
|
|
38399
|
+
if (existsSync26(statusPath)) {
|
|
37576
38400
|
try {
|
|
37577
|
-
const status = JSON.parse(
|
|
38401
|
+
const status = JSON.parse(readFileSync25(statusPath, "utf-8"));
|
|
37578
38402
|
specialist = status.specialist ?? "unknown";
|
|
37579
38403
|
beadId = status.bead_id;
|
|
37580
38404
|
} catch {}
|
|
37581
38405
|
}
|
|
37582
38406
|
const events = readJobEvents(jobDir);
|
|
37583
38407
|
if (events.length > 0) {
|
|
37584
|
-
batches.push({ jobId, specialist, beadId, events });
|
|
38408
|
+
batches.push({ jobId: jobId2, specialist, beadId, events });
|
|
37585
38409
|
}
|
|
37586
38410
|
}
|
|
37587
38411
|
return batches;
|
|
@@ -37626,14 +38450,9 @@ function filterTimelineEvents(merged, filter) {
|
|
|
37626
38450
|
return result;
|
|
37627
38451
|
}
|
|
37628
38452
|
function queryTimeline(jobsDir, filter = {}) {
|
|
37629
|
-
|
|
37630
|
-
|
|
37631
|
-
|
|
37632
|
-
}
|
|
37633
|
-
if (filter.specialist !== undefined) {
|
|
37634
|
-
batches = batches.filter((b) => b.specialist === filter.specialist);
|
|
37635
|
-
}
|
|
37636
|
-
const merged = mergeTimelineEvents(batches);
|
|
38453
|
+
const batches = readAllJobEvents(jobsDir, filter.jobId);
|
|
38454
|
+
const filteredBatches = filter.specialist !== undefined ? batches.filter((b) => b.specialist === filter.specialist) : batches;
|
|
38455
|
+
const merged = mergeTimelineEvents(filteredBatches);
|
|
37637
38456
|
return filterTimelineEvents(merged, filter);
|
|
37638
38457
|
}
|
|
37639
38458
|
var init_timeline_query = __esm(() => {
|
|
@@ -37671,13 +38490,13 @@ __export(exports_feed, {
|
|
|
37671
38490
|
});
|
|
37672
38491
|
import {
|
|
37673
38492
|
closeSync as closeSync2,
|
|
37674
|
-
existsSync as
|
|
38493
|
+
existsSync as existsSync27,
|
|
37675
38494
|
openSync as openSync3,
|
|
37676
|
-
readFileSync as
|
|
37677
|
-
readdirSync as
|
|
37678
|
-
statSync as
|
|
38495
|
+
readFileSync as readFileSync26,
|
|
38496
|
+
readdirSync as readdirSync12,
|
|
38497
|
+
statSync as statSync4
|
|
37679
38498
|
} from "fs";
|
|
37680
|
-
import { join as
|
|
38499
|
+
import { join as join28 } from "path";
|
|
37681
38500
|
function getHumanEventKey(event) {
|
|
37682
38501
|
switch (event.type) {
|
|
37683
38502
|
case "meta":
|
|
@@ -37839,7 +38658,7 @@ function readFileFresh(filePath) {
|
|
|
37839
38658
|
let fd = null;
|
|
37840
38659
|
try {
|
|
37841
38660
|
fd = openSync3(filePath, "r");
|
|
37842
|
-
return
|
|
38661
|
+
return readFileSync26(fd, "utf-8");
|
|
37843
38662
|
} catch {
|
|
37844
38663
|
return null;
|
|
37845
38664
|
} finally {
|
|
@@ -37856,7 +38675,7 @@ function readStatusJson(sqliteClient, jobsDir, jobId) {
|
|
|
37856
38675
|
} catch (error2) {
|
|
37857
38676
|
console.warn(`SQLite status read failed for job ${jobId}; falling back to status.json`, error2);
|
|
37858
38677
|
}
|
|
37859
|
-
const statusPath =
|
|
38678
|
+
const statusPath = join28(jobsDir, jobId, "status.json");
|
|
37860
38679
|
const raw = readFileFresh(statusPath);
|
|
37861
38680
|
if (!raw)
|
|
37862
38681
|
return null;
|
|
@@ -37866,13 +38685,15 @@ function readStatusJson(sqliteClient, jobsDir, jobId) {
|
|
|
37866
38685
|
return null;
|
|
37867
38686
|
}
|
|
37868
38687
|
}
|
|
37869
|
-
function isTerminalJobStatus(sqliteClient, jobsDir, jobId) {
|
|
37870
|
-
const status = readStatusJson(sqliteClient, jobsDir, jobId);
|
|
37871
|
-
return status?.status === "done" || status?.status === "error" || status?.status === "cancelled";
|
|
37872
|
-
}
|
|
37873
38688
|
function isKeepAliveJobStatus(status) {
|
|
37874
38689
|
return status?.status === "waiting";
|
|
37875
38690
|
}
|
|
38691
|
+
function isTerminalStatus(status) {
|
|
38692
|
+
return status?.status === "done" || status?.status === "error" || status?.status === "cancelled";
|
|
38693
|
+
}
|
|
38694
|
+
function isTerminalEquivalentForFollow(status, isGlobalFollow) {
|
|
38695
|
+
return isTerminalStatus(status ?? null) || isGlobalFollow && isKeepAliveJobStatus(status ?? null);
|
|
38696
|
+
}
|
|
37876
38697
|
function isJobCompleteForFollow(sqliteClient, jobsDir, jobId, events) {
|
|
37877
38698
|
const status = readStatusJson(sqliteClient, jobsDir, jobId);
|
|
37878
38699
|
if (isKeepAliveJobStatus(status)) {
|
|
@@ -37978,8 +38799,13 @@ function parseArgs10(argv) {
|
|
|
37978
38799
|
}
|
|
37979
38800
|
function printSnapshot(sqliteClient, merged, options, jobsDir) {
|
|
37980
38801
|
if (merged.length === 0) {
|
|
37981
|
-
if (!options.json)
|
|
37982
|
-
|
|
38802
|
+
if (!options.json) {
|
|
38803
|
+
if (options.jobId && sqliteClient) {
|
|
38804
|
+
console.log(dim8(`job ${options.jobId} not found in .specialists/db/observability.db`));
|
|
38805
|
+
} else {
|
|
38806
|
+
console.log(dim8("No events found."));
|
|
38807
|
+
}
|
|
38808
|
+
}
|
|
37983
38809
|
return;
|
|
37984
38810
|
}
|
|
37985
38811
|
const colorMap = new JobColorMap;
|
|
@@ -38065,13 +38891,13 @@ function filterMergedEventsByNode(sqliteClient, jobsDir, merged, nodeId) {
|
|
|
38065
38891
|
});
|
|
38066
38892
|
}
|
|
38067
38893
|
function listMatchingJobIds(sqliteClient, jobsDir, options) {
|
|
38068
|
-
if (!
|
|
38894
|
+
if (!existsSync27(jobsDir))
|
|
38069
38895
|
return [];
|
|
38070
38896
|
const jobIds = [];
|
|
38071
|
-
for (const entry of
|
|
38072
|
-
const jobDir =
|
|
38897
|
+
for (const entry of readdirSync12(jobsDir)) {
|
|
38898
|
+
const jobDir = join28(jobsDir, entry);
|
|
38073
38899
|
try {
|
|
38074
|
-
if (!
|
|
38900
|
+
if (!statSync4(jobDir).isDirectory())
|
|
38075
38901
|
continue;
|
|
38076
38902
|
} catch {
|
|
38077
38903
|
continue;
|
|
@@ -38106,7 +38932,7 @@ function readJobEventsFresh(sqliteClient, jobsDir, jobId) {
|
|
|
38106
38932
|
} catch (error2) {
|
|
38107
38933
|
console.warn(`SQLite events read failed for job ${jobId}; falling back to events.jsonl`, error2);
|
|
38108
38934
|
}
|
|
38109
|
-
const eventsPath =
|
|
38935
|
+
const eventsPath = join28(jobsDir, jobId, "events.jsonl");
|
|
38110
38936
|
const content = readFileFresh(eventsPath);
|
|
38111
38937
|
if (!content)
|
|
38112
38938
|
return [];
|
|
@@ -38130,10 +38956,10 @@ function readJobEventsIncremental(sqliteClient, jobsDir, jobId, afterSeq, fileCa
|
|
|
38130
38956
|
} catch (error2) {
|
|
38131
38957
|
console.warn(`SQLite incremental events read failed for job ${jobId}; falling back to events.jsonl`, error2);
|
|
38132
38958
|
}
|
|
38133
|
-
const eventsPath =
|
|
38959
|
+
const eventsPath = join28(jobsDir, jobId, "events.jsonl");
|
|
38134
38960
|
let stats;
|
|
38135
38961
|
try {
|
|
38136
|
-
stats =
|
|
38962
|
+
stats = statSync4(eventsPath);
|
|
38137
38963
|
} catch {
|
|
38138
38964
|
return [];
|
|
38139
38965
|
}
|
|
@@ -38167,7 +38993,11 @@ async function followMerged(sqliteClient, jobsDir, options) {
|
|
|
38167
38993
|
const fileEventCache = new Map;
|
|
38168
38994
|
const initialMatchingJobIds = listMatchingJobIds(sqliteClient, jobsDir, options);
|
|
38169
38995
|
const hasInitialMatchingJobs = initialMatchingJobIds.length > 0;
|
|
38170
|
-
const
|
|
38996
|
+
const isGlobalFollow = options.jobId === undefined;
|
|
38997
|
+
const trackedJobs = new Set(initialMatchingJobIds.filter((jobId) => {
|
|
38998
|
+
const status = readStatusJson(sqliteClient, jobsDir, jobId);
|
|
38999
|
+
return !isTerminalStatus(status) && !(isGlobalFollow && isKeepAliveJobStatus(status));
|
|
39000
|
+
}));
|
|
38171
39001
|
const completedJobs = new Set;
|
|
38172
39002
|
const filteredBatches = () => readFilteredBatchesFresh(sqliteClient, jobsDir, options);
|
|
38173
39003
|
const initial = filterMergedEventsByCursor(filterMergedEventsByNode(sqliteClient, jobsDir, queryTimeline(jobsDir, {
|
|
@@ -38213,14 +39043,11 @@ async function followMerged(sqliteClient, jobsDir, options) {
|
|
|
38213
39043
|
for (const jobId of currentJobIds) {
|
|
38214
39044
|
const status = readStatusJson(sqliteClient, jobsDir, jobId);
|
|
38215
39045
|
statusByJobId.set(jobId, status);
|
|
38216
|
-
|
|
38217
|
-
if (!isKeepAliveJobStatus(status) && isTerminal2) {
|
|
39046
|
+
if (isTerminalEquivalentForFollow(status, isGlobalFollow)) {
|
|
38218
39047
|
completedJobs.add(jobId);
|
|
38219
39048
|
continue;
|
|
38220
39049
|
}
|
|
38221
|
-
|
|
38222
|
-
trackedJobs.add(jobId);
|
|
38223
|
-
}
|
|
39050
|
+
trackedJobs.add(jobId);
|
|
38224
39051
|
}
|
|
38225
39052
|
const newEvents = [];
|
|
38226
39053
|
for (const jobId of currentJobIds) {
|
|
@@ -38240,8 +39067,7 @@ async function followMerged(sqliteClient, jobsDir, options) {
|
|
|
38240
39067
|
if (isKeepAliveJobStatus(status ?? null)) {
|
|
38241
39068
|
continue;
|
|
38242
39069
|
}
|
|
38243
|
-
|
|
38244
|
-
if (events.some(isRunCompleteEvent) || isTerminal2) {
|
|
39070
|
+
if (events.some(isRunCompleteEvent) || isTerminalStatus(status ?? null) || isGlobalFollow && isKeepAliveJobStatus(status ?? null)) {
|
|
38245
39071
|
completedJobs.add(jobId);
|
|
38246
39072
|
}
|
|
38247
39073
|
}
|
|
@@ -38290,7 +39116,7 @@ async function followMerged(sqliteClient, jobsDir, options) {
|
|
|
38290
39116
|
if (!options.forever && trackedJobs.size > 0) {
|
|
38291
39117
|
const allTrackedTerminal = [...trackedJobs].every((jobId) => {
|
|
38292
39118
|
const status = statusByJobId.get(jobId) ?? readStatusJson(sqliteClient, jobsDir, jobId);
|
|
38293
|
-
return status
|
|
39119
|
+
return isTerminalEquivalentForFollow(status, isGlobalFollow);
|
|
38294
39120
|
});
|
|
38295
39121
|
if (completedJobs.size === trackedJobs.size || allTrackedTerminal) {
|
|
38296
39122
|
clearInterval(interval);
|
|
@@ -38304,9 +39130,13 @@ async function run18() {
|
|
|
38304
39130
|
const options = parseArgs10(process.argv.slice(3));
|
|
38305
39131
|
const sqliteClient = createObservabilitySqliteClient();
|
|
38306
39132
|
try {
|
|
38307
|
-
const jobsDir =
|
|
38308
|
-
if (!
|
|
38309
|
-
|
|
39133
|
+
const jobsDir = join28(process.cwd(), ".specialists", "jobs");
|
|
39134
|
+
if (!existsSync27(jobsDir)) {
|
|
39135
|
+
if (options.jobId && sqliteClient) {
|
|
39136
|
+
console.log(dim8(`job ${options.jobId} not found in .specialists/db/observability.db`));
|
|
39137
|
+
} else {
|
|
39138
|
+
console.log(dim8("No jobs directory found."));
|
|
39139
|
+
}
|
|
38310
39140
|
return;
|
|
38311
39141
|
}
|
|
38312
39142
|
const resolvedOptions = {
|
|
@@ -38344,7 +39174,7 @@ var exports_steer = {};
|
|
|
38344
39174
|
__export(exports_steer, {
|
|
38345
39175
|
run: () => run19
|
|
38346
39176
|
});
|
|
38347
|
-
import { writeFileSync as
|
|
39177
|
+
import { writeFileSync as writeFileSync11 } from "fs";
|
|
38348
39178
|
async function run19() {
|
|
38349
39179
|
const jobId = process.argv[3];
|
|
38350
39180
|
const message = process.argv[4];
|
|
@@ -38375,7 +39205,7 @@ async function run19() {
|
|
|
38375
39205
|
try {
|
|
38376
39206
|
const payload = JSON.stringify({ type: "steer", message }) + `
|
|
38377
39207
|
`;
|
|
38378
|
-
|
|
39208
|
+
writeFileSync11(status.fifo_path, payload, { flag: "a" });
|
|
38379
39209
|
process.stdout.write(`${green10("\u2713")} Steer message sent to job ${jobId}
|
|
38380
39210
|
`);
|
|
38381
39211
|
} catch (err) {
|
|
@@ -38398,7 +39228,7 @@ var exports_resume = {};
|
|
|
38398
39228
|
__export(exports_resume, {
|
|
38399
39229
|
run: () => run20
|
|
38400
39230
|
});
|
|
38401
|
-
import { writeFileSync as
|
|
39231
|
+
import { writeFileSync as writeFileSync12 } from "fs";
|
|
38402
39232
|
async function run20() {
|
|
38403
39233
|
const jobId = process.argv[3];
|
|
38404
39234
|
const task = process.argv[4];
|
|
@@ -38415,9 +39245,9 @@ async function run20() {
|
|
|
38415
39245
|
process.exit(1);
|
|
38416
39246
|
}
|
|
38417
39247
|
if (status.status !== "waiting") {
|
|
38418
|
-
process.stderr.write(`${red5("Error:")} Job ${jobId} is
|
|
39248
|
+
process.stderr.write(`${red5("Error:")} Job ${jobId} is already finalized (${status.status}).
|
|
38419
39249
|
`);
|
|
38420
|
-
process.stderr.write(`resume
|
|
39250
|
+
process.stderr.write(`resume only works for true waiting jobs. Finalized work is terminal; use sp ps to inspect chain state.
|
|
38421
39251
|
`);
|
|
38422
39252
|
process.exit(1);
|
|
38423
39253
|
}
|
|
@@ -38429,7 +39259,7 @@ async function run20() {
|
|
|
38429
39259
|
try {
|
|
38430
39260
|
const payload = JSON.stringify({ type: "resume", task }) + `
|
|
38431
39261
|
`;
|
|
38432
|
-
|
|
39262
|
+
writeFileSync12(status.fifo_path, payload, { flag: "a" });
|
|
38433
39263
|
process.stdout.write(`${green11("\u2713")} Resume sent to job ${jobId}
|
|
38434
39264
|
`);
|
|
38435
39265
|
process.stdout.write(` Use 'specialists feed ${jobId} --follow' to watch the response.
|
|
@@ -38461,15 +39291,15 @@ async function run21() {
|
|
|
38461
39291
|
}
|
|
38462
39292
|
|
|
38463
39293
|
// src/specialist/worktree-gc.ts
|
|
38464
|
-
import { existsSync as
|
|
38465
|
-
import { join as
|
|
39294
|
+
import { existsSync as existsSync28, readdirSync as readdirSync13, readFileSync as readFileSync27 } from "fs";
|
|
39295
|
+
import { join as join29 } from "path";
|
|
38466
39296
|
import { spawnSync as spawnSync19 } from "child_process";
|
|
38467
39297
|
function readJobStatus2(jobDir) {
|
|
38468
|
-
const statusPath =
|
|
38469
|
-
if (!
|
|
39298
|
+
const statusPath = join29(jobDir, "status.json");
|
|
39299
|
+
if (!existsSync28(statusPath))
|
|
38470
39300
|
return null;
|
|
38471
39301
|
try {
|
|
38472
|
-
return JSON.parse(
|
|
39302
|
+
return JSON.parse(readFileSync27(statusPath, "utf-8"));
|
|
38473
39303
|
} catch {
|
|
38474
39304
|
return null;
|
|
38475
39305
|
}
|
|
@@ -38491,7 +39321,7 @@ function collectWorktreeGcCandidates(jobsDir) {
|
|
|
38491
39321
|
const worktreePath = status.worktree_path;
|
|
38492
39322
|
if (!worktreePath)
|
|
38493
39323
|
return null;
|
|
38494
|
-
if (!
|
|
39324
|
+
if (!existsSync28(worktreePath))
|
|
38495
39325
|
return null;
|
|
38496
39326
|
return {
|
|
38497
39327
|
jobId: status.id,
|
|
@@ -38503,13 +39333,13 @@ function collectWorktreeGcCandidates(jobsDir) {
|
|
|
38503
39333
|
}
|
|
38504
39334
|
if (!getFileFallbackEnabled())
|
|
38505
39335
|
return [];
|
|
38506
|
-
if (!
|
|
39336
|
+
if (!existsSync28(jobsDir))
|
|
38507
39337
|
return [];
|
|
38508
39338
|
const candidates = [];
|
|
38509
|
-
for (const entry of
|
|
39339
|
+
for (const entry of readdirSync13(jobsDir, { withFileTypes: true })) {
|
|
38510
39340
|
if (!entry.isDirectory())
|
|
38511
39341
|
continue;
|
|
38512
|
-
const status = readJobStatus2(
|
|
39342
|
+
const status = readJobStatus2(join29(jobsDir, entry.name));
|
|
38513
39343
|
if (!status)
|
|
38514
39344
|
continue;
|
|
38515
39345
|
if (isActive(status.status))
|
|
@@ -38519,7 +39349,7 @@ function collectWorktreeGcCandidates(jobsDir) {
|
|
|
38519
39349
|
const { worktree_path: worktreePath, branch } = status;
|
|
38520
39350
|
if (!worktreePath)
|
|
38521
39351
|
continue;
|
|
38522
|
-
if (!
|
|
39352
|
+
if (!existsSync28(worktreePath))
|
|
38523
39353
|
continue;
|
|
38524
39354
|
candidates.push({
|
|
38525
39355
|
jobId: status.id,
|
|
@@ -38565,8 +39395,32 @@ var exports_clean = {};
|
|
|
38565
39395
|
__export(exports_clean, {
|
|
38566
39396
|
run: () => run22
|
|
38567
39397
|
});
|
|
38568
|
-
import { existsSync as
|
|
38569
|
-
import { join as
|
|
39398
|
+
import { existsSync as existsSync29, readFileSync as readFileSync28, readdirSync as readdirSync14, rmSync as rmSync4, statSync as statSync5 } from "fs";
|
|
39399
|
+
import { join as join30 } from "path";
|
|
39400
|
+
function parseDuration2(raw) {
|
|
39401
|
+
const match = /^(\d+)(ms|s|m|h|d)$/i.exec(raw.trim());
|
|
39402
|
+
if (!match)
|
|
39403
|
+
return null;
|
|
39404
|
+
const amount = Number(match[1]);
|
|
39405
|
+
const unit = match[2].toLowerCase();
|
|
39406
|
+
const multipliers = {
|
|
39407
|
+
ms: 1,
|
|
39408
|
+
s: 1000,
|
|
39409
|
+
m: 60000,
|
|
39410
|
+
h: 3600000,
|
|
39411
|
+
d: 86400000
|
|
39412
|
+
};
|
|
39413
|
+
return amount * multipliers[unit];
|
|
39414
|
+
}
|
|
39415
|
+
function parseBeforeArgument2(raw) {
|
|
39416
|
+
const durationMs = parseDuration2(raw);
|
|
39417
|
+
if (durationMs !== null)
|
|
39418
|
+
return Date.now() - durationMs;
|
|
39419
|
+
const isoMs = Date.parse(raw);
|
|
39420
|
+
if (Number.isFinite(isoMs))
|
|
39421
|
+
return isoMs;
|
|
39422
|
+
throw new Error(`Invalid --before value '${raw}'. Use ISO date or duration like 7d.`);
|
|
39423
|
+
}
|
|
38570
39424
|
function parseTtlDaysFromEnvironment() {
|
|
38571
39425
|
const rawValue = process.env.SPECIALISTS_JOB_TTL_DAYS ?? process.env.JOB_TTL_DAYS;
|
|
38572
39426
|
if (!rawValue)
|
|
@@ -38583,6 +39437,11 @@ function parseOptions2(argv) {
|
|
|
38583
39437
|
let aggressivePrune = false;
|
|
38584
39438
|
let staleProcessesOnly = false;
|
|
38585
39439
|
let staleAfterHours = DEFAULT_STALE_AFTER_HOURS;
|
|
39440
|
+
let reapOrphans = false;
|
|
39441
|
+
let psDashboard = false;
|
|
39442
|
+
let observability = false;
|
|
39443
|
+
let observabilityBeforeMs = null;
|
|
39444
|
+
let includeEpics = false;
|
|
38586
39445
|
for (let index = 0;index < argv.length; index += 1) {
|
|
38587
39446
|
const argument = argv[index];
|
|
38588
39447
|
if (argument === "--all") {
|
|
@@ -38597,6 +39456,34 @@ function parseOptions2(argv) {
|
|
|
38597
39456
|
staleProcessesOnly = true;
|
|
38598
39457
|
continue;
|
|
38599
39458
|
}
|
|
39459
|
+
if (argument === "--reap-orphans") {
|
|
39460
|
+
reapOrphans = true;
|
|
39461
|
+
continue;
|
|
39462
|
+
}
|
|
39463
|
+
if (argument === "--ps") {
|
|
39464
|
+
psDashboard = true;
|
|
39465
|
+
continue;
|
|
39466
|
+
}
|
|
39467
|
+
if (argument === "--observability") {
|
|
39468
|
+
observability = true;
|
|
39469
|
+
continue;
|
|
39470
|
+
}
|
|
39471
|
+
if (argument === "--include-epics") {
|
|
39472
|
+
includeEpics = true;
|
|
39473
|
+
continue;
|
|
39474
|
+
}
|
|
39475
|
+
if (argument === "--before") {
|
|
39476
|
+
const value = argv[index + 1];
|
|
39477
|
+
if (!value)
|
|
39478
|
+
throw new Error("Missing value for --before");
|
|
39479
|
+
observabilityBeforeMs = parseBeforeArgument2(value);
|
|
39480
|
+
index += 1;
|
|
39481
|
+
continue;
|
|
39482
|
+
}
|
|
39483
|
+
if (argument.startsWith("--before=")) {
|
|
39484
|
+
observabilityBeforeMs = parseBeforeArgument2(argument.slice("--before=".length));
|
|
39485
|
+
continue;
|
|
39486
|
+
}
|
|
38600
39487
|
if (argument === "--aggressive-prune") {
|
|
38601
39488
|
aggressivePrune = true;
|
|
38602
39489
|
continue;
|
|
@@ -38644,20 +39531,35 @@ function parseOptions2(argv) {
|
|
|
38644
39531
|
if (staleProcessesOnly && (removeAllCompleted || keepRecentCount !== null)) {
|
|
38645
39532
|
throw new Error("--processes cannot be combined with --all or --keep");
|
|
38646
39533
|
}
|
|
38647
|
-
|
|
39534
|
+
if (reapOrphans && (removeAllCompleted || keepRecentCount !== null || staleProcessesOnly || psDashboard)) {
|
|
39535
|
+
throw new Error("--reap-orphans cannot be combined with --all, --keep, --processes, or --ps");
|
|
39536
|
+
}
|
|
39537
|
+
if (psDashboard && (removeAllCompleted || keepRecentCount !== null || staleProcessesOnly || aggressivePrune || observability)) {
|
|
39538
|
+
throw new Error("--ps cannot be combined with --all, --keep, --processes, --aggressive-prune, or --observability");
|
|
39539
|
+
}
|
|
39540
|
+
if (observability && (removeAllCompleted || keepRecentCount !== null || staleProcessesOnly || reapOrphans || psDashboard || aggressivePrune)) {
|
|
39541
|
+
throw new Error("--observability cannot be combined with --all, --keep, --processes, --reap-orphans, --ps, or --aggressive-prune");
|
|
39542
|
+
}
|
|
39543
|
+
if (!observability && (observabilityBeforeMs !== null || includeEpics)) {
|
|
39544
|
+
throw new Error("--before and --include-epics require --observability");
|
|
39545
|
+
}
|
|
39546
|
+
if (observability && observabilityBeforeMs === null) {
|
|
39547
|
+
throw new Error("--observability requires --before <iso|duration>");
|
|
39548
|
+
}
|
|
39549
|
+
return { removeAllCompleted, dryRun, keepRecentCount, aggressivePrune, staleProcessesOnly, staleAfterHours, reapOrphans, psDashboard, observability, observabilityBeforeMs, includeEpics };
|
|
38648
39550
|
}
|
|
38649
39551
|
function readDirectorySizeBytes(directoryPath) {
|
|
38650
39552
|
let totalBytes = 0;
|
|
38651
|
-
for (const entry of
|
|
38652
|
-
const entryPath =
|
|
38653
|
-
const stats =
|
|
39553
|
+
for (const entry of readdirSync14(directoryPath, { withFileTypes: true })) {
|
|
39554
|
+
const entryPath = join30(directoryPath, entry.name);
|
|
39555
|
+
const stats = statSync5(entryPath);
|
|
38654
39556
|
totalBytes += stats.isDirectory() ? readDirectorySizeBytes(entryPath) : stats.size;
|
|
38655
39557
|
}
|
|
38656
39558
|
return totalBytes;
|
|
38657
39559
|
}
|
|
38658
39560
|
function containsProtectedSqliteArtifact(directoryPath) {
|
|
38659
|
-
for (const entry of
|
|
38660
|
-
const entryPath =
|
|
39561
|
+
for (const entry of readdirSync14(directoryPath, { withFileTypes: true })) {
|
|
39562
|
+
const entryPath = join30(directoryPath, entry.name);
|
|
38661
39563
|
if (entry.isDirectory()) {
|
|
38662
39564
|
if (containsProtectedSqliteArtifact(entryPath))
|
|
38663
39565
|
return true;
|
|
@@ -38678,15 +39580,15 @@ function getJobTimestamps(status) {
|
|
|
38678
39580
|
function readCompletedJobDirectory(baseDirectory, entry) {
|
|
38679
39581
|
if (!entry.isDirectory())
|
|
38680
39582
|
return null;
|
|
38681
|
-
const directoryPath =
|
|
39583
|
+
const directoryPath = join30(baseDirectory, entry.name);
|
|
38682
39584
|
if (containsProtectedSqliteArtifact(directoryPath))
|
|
38683
39585
|
return null;
|
|
38684
|
-
const statusFilePath =
|
|
38685
|
-
if (!
|
|
39586
|
+
const statusFilePath = join30(directoryPath, "status.json");
|
|
39587
|
+
if (!existsSync29(statusFilePath))
|
|
38686
39588
|
return null;
|
|
38687
39589
|
let statusData;
|
|
38688
39590
|
try {
|
|
38689
|
-
statusData = JSON.parse(
|
|
39591
|
+
statusData = JSON.parse(readFileSync28(statusFilePath, "utf-8"));
|
|
38690
39592
|
} catch {
|
|
38691
39593
|
return null;
|
|
38692
39594
|
}
|
|
@@ -38700,8 +39602,8 @@ function collectCompletedJobs(jobsDirectoryPath) {
|
|
|
38700
39602
|
const statuses = sqliteClient?.listStatuses() ?? [];
|
|
38701
39603
|
if (statuses.length > 0) {
|
|
38702
39604
|
return statuses.filter((status) => COMPLETED_STATUSES.has(status.status)).map((status) => {
|
|
38703
|
-
const directoryPath =
|
|
38704
|
-
if (!
|
|
39605
|
+
const directoryPath = join30(jobsDirectoryPath, status.id);
|
|
39606
|
+
if (!existsSync29(directoryPath) || containsProtectedSqliteArtifact(directoryPath))
|
|
38705
39607
|
return null;
|
|
38706
39608
|
const { createdAtMs, completedAtMs } = getJobTimestamps(status);
|
|
38707
39609
|
return { id: status.id, directoryPath, completedAtMs, createdAtMs, sizeBytes: readDirectorySizeBytes(directoryPath) };
|
|
@@ -38709,7 +39611,7 @@ function collectCompletedJobs(jobsDirectoryPath) {
|
|
|
38709
39611
|
}
|
|
38710
39612
|
if (process.env.SPECIALISTS_JOB_FILE_OUTPUT !== "on")
|
|
38711
39613
|
return [];
|
|
38712
|
-
return
|
|
39614
|
+
return readdirSync14(jobsDirectoryPath, { withFileTypes: true }).map((entry) => readCompletedJobDirectory(jobsDirectoryPath, entry)).filter((job) => job !== null);
|
|
38713
39615
|
}
|
|
38714
39616
|
function selectJobsToRemove(completedJobs, options, protectedJobIds) {
|
|
38715
39617
|
const jobsByNewest = [...completedJobs].sort((left, right) => {
|
|
@@ -38781,6 +39683,17 @@ function renderSummary(removedCount, freedBytes, dryRun) {
|
|
|
38781
39683
|
const noun = removedCount === 1 ? "directory" : "directories";
|
|
38782
39684
|
return `${action} ${removedCount} job ${noun} (${formatBytes2(freedBytes)} freed)`;
|
|
38783
39685
|
}
|
|
39686
|
+
function printObservabilityPruneReport(report) {
|
|
39687
|
+
console.log(report.dryRun ? "Would prune observability SQLite rows:" : "Pruned observability SQLite rows:");
|
|
39688
|
+
console.log(` before: ${new Date(report.beforeMs).toISOString()}`);
|
|
39689
|
+
console.log(` events cutoff: ${new Date(report.eventsCutoffMs).toISOString()}`);
|
|
39690
|
+
console.log(` specialist_events: ${report.deletedEvents}`);
|
|
39691
|
+
console.log(` specialist_results: ${report.deletedResults}`);
|
|
39692
|
+
console.log(` specialist_jobs: ${report.deletedJobs}`);
|
|
39693
|
+
console.log(` extracted jobs: ${report.extractedJobs}`);
|
|
39694
|
+
console.log(` epic_runs: ${report.deletedEpicRuns}${report.includeEpics ? "" : " (skipped, use --include-epics)"}`);
|
|
39695
|
+
console.log(` skipped active-chain jobs: ${report.skippedActiveChainJobs}`);
|
|
39696
|
+
}
|
|
38784
39697
|
function printDryRunPlan(jobs) {
|
|
38785
39698
|
if (jobs.length === 0)
|
|
38786
39699
|
return;
|
|
@@ -38812,15 +39725,142 @@ function printWorktreeGcSummary(removed, skipped) {
|
|
|
38812
39725
|
}
|
|
38813
39726
|
function printUsageAndExit2(message) {
|
|
38814
39727
|
console.error(message);
|
|
38815
|
-
console.error("Usage: specialists|sp clean [--all] [--keep <n>] [--aggressive-prune] [--processes [--stale-after <hours>]] [--dry-run]");
|
|
39728
|
+
console.error("Usage: specialists|sp clean [--all] [--keep <n>] [--aggressive-prune] [--processes [--stale-after <hours>]] [--reap-orphans] [--observability --before <iso|duration> [--include-epics]] [--dry-run]");
|
|
38816
39729
|
process.exit(1);
|
|
38817
39730
|
}
|
|
39731
|
+
function findOrphanProcesses() {
|
|
39732
|
+
return collectOrphanProcesses().map((process3) => ({
|
|
39733
|
+
pid: process3.pid,
|
|
39734
|
+
ppid: process3.ppid,
|
|
39735
|
+
comm: process3.cmdline.split(" ")[0] ?? process3.role,
|
|
39736
|
+
cmdline: process3.cmdline,
|
|
39737
|
+
cwd: process3.cwd,
|
|
39738
|
+
reason: process3.reason ?? "pi-orphan"
|
|
39739
|
+
}));
|
|
39740
|
+
}
|
|
39741
|
+
async function killOrphanProcesses(orphans, dryRun) {
|
|
39742
|
+
if (dryRun)
|
|
39743
|
+
return orphans.length;
|
|
39744
|
+
let killed = 0;
|
|
39745
|
+
for (const orphan of orphans) {
|
|
39746
|
+
try {
|
|
39747
|
+
process.kill(orphan.pid, "SIGTERM");
|
|
39748
|
+
} catch {}
|
|
39749
|
+
}
|
|
39750
|
+
if (orphans.length > 0)
|
|
39751
|
+
await new Promise((resolve11) => setTimeout(resolve11, 1500));
|
|
39752
|
+
for (const orphan of orphans) {
|
|
39753
|
+
try {
|
|
39754
|
+
process.kill(orphan.pid, 0);
|
|
39755
|
+
try {
|
|
39756
|
+
process.kill(orphan.pid, "SIGKILL");
|
|
39757
|
+
} catch {}
|
|
39758
|
+
} catch {}
|
|
39759
|
+
killed += 1;
|
|
39760
|
+
}
|
|
39761
|
+
return killed;
|
|
39762
|
+
}
|
|
39763
|
+
function printOrphanPlan(orphans) {
|
|
39764
|
+
if (orphans.length === 0) {
|
|
39765
|
+
console.log("No orphan/stale leaked processes found.");
|
|
39766
|
+
return;
|
|
39767
|
+
}
|
|
39768
|
+
const action = "Would reap";
|
|
39769
|
+
console.log(`${action} ${orphans.length} orphan/stale leaked process(es):`);
|
|
39770
|
+
for (const orphan of orphans) {
|
|
39771
|
+
const cwdSuffix = orphan.cwd ? ` cwd=${orphan.cwd}` : "";
|
|
39772
|
+
console.log(` - pid=${orphan.pid} ppid=${orphan.ppid} reason=${orphan.reason} comm=${orphan.comm}${cwdSuffix}`);
|
|
39773
|
+
}
|
|
39774
|
+
}
|
|
39775
|
+
function printStaleJobPlan(jobs) {
|
|
39776
|
+
if (jobs.length === 0) {
|
|
39777
|
+
console.log("No stale specialist jobs found.");
|
|
39778
|
+
return;
|
|
39779
|
+
}
|
|
39780
|
+
console.log(`Would reap ${jobs.length} stale specialist job(s):`);
|
|
39781
|
+
for (const job of jobs) {
|
|
39782
|
+
const ageMinutes = Math.round(job.ageMs / 60000);
|
|
39783
|
+
const bead = job.beadId ? ` bead=${job.beadId}` : "";
|
|
39784
|
+
const cwd = job.cwd ? ` cwd=${job.cwd}` : "";
|
|
39785
|
+
console.log(` - job=${job.jobId} pid=${job.pid}${bead} age=${ageMinutes}m reason=${job.reason}${cwd}`);
|
|
39786
|
+
}
|
|
39787
|
+
}
|
|
39788
|
+
async function reapStaleSpecialistJobs(jobs, dryRun) {
|
|
39789
|
+
const sqliteClient = createObservabilitySqliteClient();
|
|
39790
|
+
if (!sqliteClient)
|
|
39791
|
+
return 0;
|
|
39792
|
+
if (dryRun)
|
|
39793
|
+
return jobs.length;
|
|
39794
|
+
let reapedCount = 0;
|
|
39795
|
+
for (const job of jobs) {
|
|
39796
|
+
if (job.reason === "orphaned-keep-alive") {
|
|
39797
|
+
try {
|
|
39798
|
+
process.kill(job.pid, "SIGTERM");
|
|
39799
|
+
} catch {}
|
|
39800
|
+
await new Promise((resolve11) => setTimeout(resolve11, 500));
|
|
39801
|
+
try {
|
|
39802
|
+
process.kill(job.pid, 0);
|
|
39803
|
+
try {
|
|
39804
|
+
process.kill(job.pid, "SIGKILL");
|
|
39805
|
+
} catch {}
|
|
39806
|
+
} catch {}
|
|
39807
|
+
}
|
|
39808
|
+
sqliteClient.markSpecialistJobCancelled(job.jobId, `cleanup: stale-reaper:${job.reason}`);
|
|
39809
|
+
reapedCount += 1;
|
|
39810
|
+
}
|
|
39811
|
+
return reapedCount;
|
|
39812
|
+
}
|
|
39813
|
+
function printOrphanSummary(killedCount) {
|
|
39814
|
+
if (killedCount === 0)
|
|
39815
|
+
return;
|
|
39816
|
+
const noun = killedCount === 1 ? "process" : "processes";
|
|
39817
|
+
console.log(`Reaped ${killedCount} orphan/stale leaked ${noun}.`);
|
|
39818
|
+
}
|
|
39819
|
+
function printStaleJobSummary(reapedCount) {
|
|
39820
|
+
const noun = reapedCount === 1 ? "job" : "jobs";
|
|
39821
|
+
console.log(`Reaped ${reapedCount} stale specialist ${noun}.`);
|
|
39822
|
+
}
|
|
38818
39823
|
function deleteJobDirectories(jobs) {
|
|
38819
39824
|
for (const job of jobs) {
|
|
38820
|
-
|
|
39825
|
+
rmSync4(job.directoryPath, { recursive: true, force: true });
|
|
38821
39826
|
}
|
|
38822
39827
|
return jobs.length;
|
|
38823
39828
|
}
|
|
39829
|
+
function isPsHidden(status) {
|
|
39830
|
+
const typed = status;
|
|
39831
|
+
return Boolean(typed.ps_hidden_at ?? typed.ps_hidden_from_dashboard_at);
|
|
39832
|
+
}
|
|
39833
|
+
function selectPsDashboardCleanCandidates(statuses) {
|
|
39834
|
+
return statuses.filter((status) => COMPLETED_STATUSES.has(status.status) && !isPsHidden(status));
|
|
39835
|
+
}
|
|
39836
|
+
function printPsDashboardCleanPlan(statuses) {
|
|
39837
|
+
if (statuses.length === 0) {
|
|
39838
|
+
console.log("No terminal ps rows to hide.");
|
|
39839
|
+
return;
|
|
39840
|
+
}
|
|
39841
|
+
console.log(`Would hide ${statuses.length} terminal row(s) from default ps:`);
|
|
39842
|
+
for (const status of statuses) {
|
|
39843
|
+
const bead = status.bead_id ? ` bead=${status.bead_id}` : "";
|
|
39844
|
+
const branch = status.branch ? ` branch=${status.branch}` : "";
|
|
39845
|
+
console.log(` - ${status.id} ${status.specialist} status=${status.status}${bead}${branch}`);
|
|
39846
|
+
}
|
|
39847
|
+
}
|
|
39848
|
+
function hidePsDashboardRows(statuses, dryRun) {
|
|
39849
|
+
const sqliteClient = createObservabilitySqliteClient();
|
|
39850
|
+
if (!sqliteClient)
|
|
39851
|
+
return 0;
|
|
39852
|
+
if (dryRun)
|
|
39853
|
+
return statuses.length;
|
|
39854
|
+
const now = Date.now();
|
|
39855
|
+
for (const status of statuses) {
|
|
39856
|
+
sqliteClient.upsertStatus({
|
|
39857
|
+
...status,
|
|
39858
|
+
ps_hidden_at: now,
|
|
39859
|
+
ps_hidden_reason: "sp clean --ps"
|
|
39860
|
+
});
|
|
39861
|
+
}
|
|
39862
|
+
return statuses.length;
|
|
39863
|
+
}
|
|
38824
39864
|
function removeStaleProcesses(statuses, dryRun) {
|
|
38825
39865
|
const sqliteClient = createObservabilitySqliteClient();
|
|
38826
39866
|
if (!sqliteClient)
|
|
@@ -38849,13 +39889,54 @@ async function run22() {
|
|
|
38849
39889
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
38850
39890
|
printUsageAndExit2(message);
|
|
38851
39891
|
}
|
|
38852
|
-
|
|
38853
|
-
|
|
38854
|
-
|
|
39892
|
+
if (options.reapOrphans) {
|
|
39893
|
+
const orphans = findOrphanProcesses();
|
|
39894
|
+
const staleJobs = collectStaleSpecialistJobs();
|
|
39895
|
+
if (options.dryRun) {
|
|
39896
|
+
printOrphanPlan(orphans);
|
|
39897
|
+
printStaleJobPlan(staleJobs);
|
|
39898
|
+
return;
|
|
39899
|
+
}
|
|
39900
|
+
if (orphans.length === 0 && staleJobs.length === 0) {
|
|
39901
|
+
console.log("No orphan/stale leaked processes found.");
|
|
39902
|
+
return;
|
|
39903
|
+
}
|
|
39904
|
+
printOrphanPlan(orphans);
|
|
39905
|
+
printStaleJobPlan(staleJobs);
|
|
39906
|
+
const killedCount = await killOrphanProcesses(orphans, false);
|
|
39907
|
+
const staleCount = await reapStaleSpecialistJobs(staleJobs, false);
|
|
39908
|
+
printOrphanSummary(killedCount);
|
|
39909
|
+
printStaleJobSummary(staleCount);
|
|
38855
39910
|
return;
|
|
38856
39911
|
}
|
|
39912
|
+
const jobsDirectoryPath = resolveJobsDir();
|
|
38857
39913
|
const sqliteClient = createObservabilitySqliteClient();
|
|
38858
39914
|
const statuses = sqliteClient?.listStatuses() ?? [];
|
|
39915
|
+
if (options.observability) {
|
|
39916
|
+
if (!sqliteClient)
|
|
39917
|
+
throw new Error("Failed to initialize observability SQLite schema. Run `specialists db setup` first.");
|
|
39918
|
+
const report = sqliteClient.pruneObservabilityData({
|
|
39919
|
+
beforeMs: options.observabilityBeforeMs,
|
|
39920
|
+
includeEpics: options.includeEpics,
|
|
39921
|
+
apply: !options.dryRun
|
|
39922
|
+
});
|
|
39923
|
+
printObservabilityPruneReport(report);
|
|
39924
|
+
return;
|
|
39925
|
+
}
|
|
39926
|
+
if (options.psDashboard) {
|
|
39927
|
+
const candidates = selectPsDashboardCleanCandidates(statuses);
|
|
39928
|
+
if (options.dryRun) {
|
|
39929
|
+
printPsDashboardCleanPlan(candidates);
|
|
39930
|
+
return;
|
|
39931
|
+
}
|
|
39932
|
+
const hiddenCount = hidePsDashboardRows(candidates, false);
|
|
39933
|
+
console.log(`Hid ${hiddenCount} terminal row(s) from default ps.`);
|
|
39934
|
+
return;
|
|
39935
|
+
}
|
|
39936
|
+
if (!existsSync29(jobsDirectoryPath)) {
|
|
39937
|
+
console.log("No jobs directory found.");
|
|
39938
|
+
return;
|
|
39939
|
+
}
|
|
38859
39940
|
const worktreeCandidates = collectWorktreeGcCandidates(jobsDirectoryPath);
|
|
38860
39941
|
const protectedJobIds = options.keepRecentCount !== null && !options.aggressivePrune && sqliteClient ? new Set(sqliteClient.listReferencedChainRootJobIds()) : new Set;
|
|
38861
39942
|
if (options.staleProcessesOnly) {
|
|
@@ -38894,6 +39975,7 @@ var MS_PER_DAY = 86400000, DEFAULT_TTL_DAYS = 7, DEFAULT_STALE_AFTER_HOURS = 24,
|
|
|
38894
39975
|
var init_clean = __esm(() => {
|
|
38895
39976
|
init_job_root();
|
|
38896
39977
|
init_observability_sqlite();
|
|
39978
|
+
init_process_health();
|
|
38897
39979
|
init_worktree_gc();
|
|
38898
39980
|
COMPLETED_STATUSES = new Set(["done", "error", "cancelled"]);
|
|
38899
39981
|
STALE_PROCESS_STATUSES = new Set(["running", "starting", "waiting"]);
|
|
@@ -39095,7 +40177,7 @@ async function run24() {
|
|
|
39095
40177
|
process.exit(1);
|
|
39096
40178
|
}
|
|
39097
40179
|
if (status.status === "done" || status.status === "error" || status.status === "cancelled") {
|
|
39098
|
-
process.stderr.write(`${dim11(`Job ${jobId}
|
|
40180
|
+
process.stderr.write(`${dim11(`Job ${jobId} already finalized (${status.status}).`)}
|
|
39099
40181
|
`);
|
|
39100
40182
|
return;
|
|
39101
40183
|
}
|
|
@@ -39183,21 +40265,127 @@ var init_stop = __esm(() => {
|
|
|
39183
40265
|
init_tmux_utils();
|
|
39184
40266
|
});
|
|
39185
40267
|
|
|
40268
|
+
// src/cli/finalize.ts
|
|
40269
|
+
var exports_finalize = {};
|
|
40270
|
+
__export(exports_finalize, {
|
|
40271
|
+
run: () => run25
|
|
40272
|
+
});
|
|
40273
|
+
function createFinalizeSupervisor(jobsDir) {
|
|
40274
|
+
const runner = {
|
|
40275
|
+
run: async () => {
|
|
40276
|
+
throw new Error("finalize supervisor runner is not used");
|
|
40277
|
+
}
|
|
40278
|
+
};
|
|
40279
|
+
const runOptions = {};
|
|
40280
|
+
return new Supervisor({ runner, runOptions, jobsDir });
|
|
40281
|
+
}
|
|
40282
|
+
function parseFinalizeArgs(argv) {
|
|
40283
|
+
const jobId = argv.find((token) => !token.startsWith("-"));
|
|
40284
|
+
return { jobId };
|
|
40285
|
+
}
|
|
40286
|
+
function findReviewerPassInChain(supervisor, chainId) {
|
|
40287
|
+
const jobIds = supervisor.listChainJobIds(chainId);
|
|
40288
|
+
for (const id of jobIds) {
|
|
40289
|
+
const status = supervisor.readStatus(id);
|
|
40290
|
+
if (!status || status.specialist !== "reviewer")
|
|
40291
|
+
continue;
|
|
40292
|
+
const output2 = supervisor.readResult(id) ?? "";
|
|
40293
|
+
if (PASS_COMPLIANCE_VERDICT_REGEX2.test(output2)) {
|
|
40294
|
+
return { reviewerJobId: id };
|
|
40295
|
+
}
|
|
40296
|
+
}
|
|
40297
|
+
return null;
|
|
40298
|
+
}
|
|
40299
|
+
async function run25() {
|
|
40300
|
+
const parsed = parseFinalizeArgs(process.argv.slice(3));
|
|
40301
|
+
const jobId = parsed.jobId;
|
|
40302
|
+
if (!jobId) {
|
|
40303
|
+
console.error("Usage: specialists|sp finalize <job-id>");
|
|
40304
|
+
process.exit(1);
|
|
40305
|
+
}
|
|
40306
|
+
const jobsDir = resolveJobsDir();
|
|
40307
|
+
const supervisor = createFinalizeSupervisor(jobsDir);
|
|
40308
|
+
try {
|
|
40309
|
+
const status = supervisor.readStatus(jobId);
|
|
40310
|
+
if (!status) {
|
|
40311
|
+
console.error(`No job found: ${jobId}`);
|
|
40312
|
+
process.exit(1);
|
|
40313
|
+
}
|
|
40314
|
+
const chainId = status.chain_id ?? status.chain_root_job_id;
|
|
40315
|
+
if (!chainId) {
|
|
40316
|
+
process.stderr.write(`${red7("Error:")} Job ${jobId} has no chain identity (chain_id missing).
|
|
40317
|
+
`);
|
|
40318
|
+
process.exit(1);
|
|
40319
|
+
}
|
|
40320
|
+
const reviewerPass = findReviewerPassInChain(supervisor, chainId);
|
|
40321
|
+
if (!reviewerPass) {
|
|
40322
|
+
process.stderr.write(`${red7("Error:")} No reviewer with PASS compliance verdict found in chain ${chainId}.
|
|
40323
|
+
`);
|
|
40324
|
+
process.stderr.write(`${dim12("finalize only closes keep-alive chains after reviewer PASS.")}
|
|
40325
|
+
`);
|
|
40326
|
+
process.exit(1);
|
|
40327
|
+
}
|
|
40328
|
+
const chainJobIds = supervisor.listChainJobIds(chainId);
|
|
40329
|
+
const finalized = [];
|
|
40330
|
+
const skipped = [];
|
|
40331
|
+
for (const id of chainJobIds) {
|
|
40332
|
+
const memberStatus = supervisor.readStatus(id);
|
|
40333
|
+
if (!memberStatus) {
|
|
40334
|
+
skipped.push({ id, reason: "missing" });
|
|
40335
|
+
continue;
|
|
40336
|
+
}
|
|
40337
|
+
if (memberStatus.status !== "waiting") {
|
|
40338
|
+
skipped.push({ id, reason: memberStatus.status });
|
|
40339
|
+
continue;
|
|
40340
|
+
}
|
|
40341
|
+
const result = supervisor.finalizeWaitingJob(id);
|
|
40342
|
+
if (result) {
|
|
40343
|
+
finalized.push(id);
|
|
40344
|
+
} else {
|
|
40345
|
+
skipped.push({ id, reason: "finalize-failed" });
|
|
40346
|
+
}
|
|
40347
|
+
}
|
|
40348
|
+
if (finalized.length === 0) {
|
|
40349
|
+
process.stderr.write(`${red7("Error:")} No waiting keep-alive jobs to finalize in chain ${chainId}.
|
|
40350
|
+
`);
|
|
40351
|
+
process.exit(1);
|
|
40352
|
+
}
|
|
40353
|
+
process.stdout.write(`${green13("\u2713")} Finalized chain ${chainId} (reviewer PASS: ${reviewerPass.reviewerJobId})
|
|
40354
|
+
`);
|
|
40355
|
+
for (const id of finalized) {
|
|
40356
|
+
process.stdout.write(` ${green13("\u2713")} ${id}
|
|
40357
|
+
`);
|
|
40358
|
+
}
|
|
40359
|
+
for (const { id, reason } of skipped) {
|
|
40360
|
+
process.stdout.write(` ${dim12(`\xB7 ${id} (${reason})`)}
|
|
40361
|
+
`);
|
|
40362
|
+
}
|
|
40363
|
+
} finally {
|
|
40364
|
+
await supervisor.dispose();
|
|
40365
|
+
}
|
|
40366
|
+
}
|
|
40367
|
+
var green13 = (s) => `\x1B[32m${s}\x1B[0m`, red7 = (s) => `\x1B[31m${s}\x1B[0m`, dim12 = (s) => `\x1B[2m${s}\x1B[0m`, PASS_COMPLIANCE_VERDICT_REGEX2;
|
|
40368
|
+
var init_finalize = __esm(() => {
|
|
40369
|
+
init_supervisor();
|
|
40370
|
+
init_job_root();
|
|
40371
|
+
PASS_COMPLIANCE_VERDICT_REGEX2 = /## Compliance Verdict[\s\S]*?- Verdict:\s*\**\s*PASS\s*\**/i;
|
|
40372
|
+
});
|
|
40373
|
+
|
|
39186
40374
|
// src/cli/attach.ts
|
|
39187
40375
|
var exports_attach = {};
|
|
39188
40376
|
__export(exports_attach, {
|
|
39189
|
-
run: () =>
|
|
40377
|
+
run: () => run26
|
|
39190
40378
|
});
|
|
39191
40379
|
import { execFileSync as execFileSync3, spawnSync as spawnSync21 } from "child_process";
|
|
39192
|
-
import { readFileSync as
|
|
39193
|
-
import { join as
|
|
40380
|
+
import { readFileSync as readFileSync29 } from "fs";
|
|
40381
|
+
import { join as join31 } from "path";
|
|
39194
40382
|
function exitWithError(message) {
|
|
39195
40383
|
console.error(message);
|
|
39196
40384
|
process.exit(1);
|
|
39197
40385
|
}
|
|
39198
40386
|
function readStatus(statusPath, jobId) {
|
|
39199
40387
|
try {
|
|
39200
|
-
return JSON.parse(
|
|
40388
|
+
return JSON.parse(readFileSync29(statusPath, "utf-8"));
|
|
39201
40389
|
} catch (error2) {
|
|
39202
40390
|
if (error2 && typeof error2 === "object" && "code" in error2 && error2.code === "ENOENT") {
|
|
39203
40391
|
exitWithError(`Job \`${jobId}\` not found. Run \`specialists status\` to see active jobs in current mode.`);
|
|
@@ -39206,13 +40394,13 @@ function readStatus(statusPath, jobId) {
|
|
|
39206
40394
|
exitWithError(`Failed to read status for job \`${jobId}\`: ${details}`);
|
|
39207
40395
|
}
|
|
39208
40396
|
}
|
|
39209
|
-
async function
|
|
40397
|
+
async function run26() {
|
|
39210
40398
|
const [jobId] = process.argv.slice(3);
|
|
39211
40399
|
if (!jobId) {
|
|
39212
40400
|
exitWithError("Usage: specialists attach <job-id> (normal runtime is DB-backed; job files are legacy/operator-only)");
|
|
39213
40401
|
}
|
|
39214
|
-
const jobsDir =
|
|
39215
|
-
const statusPath =
|
|
40402
|
+
const jobsDir = join31(process.cwd(), ".specialists", "jobs");
|
|
40403
|
+
const statusPath = join31(jobsDir, jobId, "status.json");
|
|
39216
40404
|
const status = readStatus(statusPath, jobId);
|
|
39217
40405
|
if (status.status === "done" || status.status === "error") {
|
|
39218
40406
|
exitWithError(`Job \`${jobId}\` has already completed (status: ${status.status}). Use \`specialists result ${jobId}\` to read output.`);
|
|
@@ -39234,15 +40422,15 @@ async function run25() {
|
|
|
39234
40422
|
var init_attach = () => {};
|
|
39235
40423
|
|
|
39236
40424
|
// src/specialist/drift-detector.ts
|
|
39237
|
-
import { existsSync as
|
|
39238
|
-
import { join as
|
|
40425
|
+
import { existsSync as existsSync30, readFileSync as readFileSync30, readdirSync as readdirSync15, rmSync as rmSync5 } from "fs";
|
|
40426
|
+
import { join as join32, resolve as resolve11, relative as relative3 } from "path";
|
|
39239
40427
|
function listFiles(root) {
|
|
39240
|
-
if (!
|
|
40428
|
+
if (!existsSync30(root))
|
|
39241
40429
|
return [];
|
|
39242
40430
|
const out = [];
|
|
39243
40431
|
const visit2 = (dir) => {
|
|
39244
|
-
for (const entry of
|
|
39245
|
-
const full =
|
|
40432
|
+
for (const entry of readdirSync15(dir, { withFileTypes: true })) {
|
|
40433
|
+
const full = join32(dir, entry.name);
|
|
39246
40434
|
if (entry.isDirectory()) {
|
|
39247
40435
|
visit2(full);
|
|
39248
40436
|
continue;
|
|
@@ -39277,14 +40465,14 @@ function detectDriftForRepo(repoRoot) {
|
|
|
39277
40465
|
{ scope: "user", dir: resolve11(repoRoot, ".specialists/user") }
|
|
39278
40466
|
];
|
|
39279
40467
|
for (const { scope, dir } of scopes) {
|
|
39280
|
-
if (!
|
|
40468
|
+
if (!existsSync30(dir))
|
|
39281
40469
|
continue;
|
|
39282
40470
|
for (const file of listFiles(dir)) {
|
|
39283
40471
|
const rel = relPath(file, dir);
|
|
39284
|
-
const canonicalPath =
|
|
39285
|
-
if (!
|
|
40472
|
+
const canonicalPath = join32(asset.canonicalDir, rel);
|
|
40473
|
+
if (!existsSync30(canonicalPath))
|
|
39286
40474
|
continue;
|
|
39287
|
-
const bytesEqual =
|
|
40475
|
+
const bytesEqual = readFileSync30(file).equals(readFileSync30(canonicalPath));
|
|
39288
40476
|
findings.push(makeFinding(repoRoot, asset.kind, scope, file, canonicalPath, bytesEqual));
|
|
39289
40477
|
}
|
|
39290
40478
|
}
|
|
@@ -39303,12 +40491,12 @@ function detectDriftUnderRoot(root) {
|
|
|
39303
40491
|
repos.push({ root: dir, findings });
|
|
39304
40492
|
return;
|
|
39305
40493
|
}
|
|
39306
|
-
for (const entry of
|
|
40494
|
+
for (const entry of readdirSync15(dir, { withFileTypes: true })) {
|
|
39307
40495
|
if (!entry.isDirectory())
|
|
39308
40496
|
continue;
|
|
39309
40497
|
if (entry.name === "node_modules" || entry.name === ".git")
|
|
39310
40498
|
continue;
|
|
39311
|
-
visit2(
|
|
40499
|
+
visit2(join32(dir, entry.name));
|
|
39312
40500
|
}
|
|
39313
40501
|
};
|
|
39314
40502
|
visit2(resolve11(root));
|
|
@@ -39330,7 +40518,7 @@ function pruneStaleDefaults(repoRoot, dryRun) {
|
|
|
39330
40518
|
const targets = detectDriftForRepo(repoRoot).filter((f) => f.scope === "default" && f.bytes_equal === true).map((f) => f.path);
|
|
39331
40519
|
if (!dryRun) {
|
|
39332
40520
|
for (const target of targets)
|
|
39333
|
-
|
|
40521
|
+
rmSync5(target, { recursive: true, force: true });
|
|
39334
40522
|
}
|
|
39335
40523
|
return targets;
|
|
39336
40524
|
}
|
|
@@ -39348,7 +40536,7 @@ var init_drift_detector = __esm(() => {
|
|
|
39348
40536
|
// src/cli/prune-stale-defaults.ts
|
|
39349
40537
|
var exports_prune_stale_defaults = {};
|
|
39350
40538
|
__export(exports_prune_stale_defaults, {
|
|
39351
|
-
run: () =>
|
|
40539
|
+
run: () => run27
|
|
39352
40540
|
});
|
|
39353
40541
|
import { resolve as resolve12 } from "path";
|
|
39354
40542
|
function parseArgs11(argv) {
|
|
@@ -39382,7 +40570,7 @@ function printHelp() {
|
|
|
39382
40570
|
console.log(" --dry-run List stale default snapshots without pruning");
|
|
39383
40571
|
console.log(" --root Repo root to scan");
|
|
39384
40572
|
}
|
|
39385
|
-
async function
|
|
40573
|
+
async function run27(argv = process.argv.slice(3)) {
|
|
39386
40574
|
const { dryRun, root, help } = parseArgs11(argv);
|
|
39387
40575
|
if (help) {
|
|
39388
40576
|
printHelp();
|
|
@@ -39409,33 +40597,39 @@ var init_prune_stale_defaults = __esm(() => {
|
|
|
39409
40597
|
// src/cli/quickstart.ts
|
|
39410
40598
|
var exports_quickstart = {};
|
|
39411
40599
|
__export(exports_quickstart, {
|
|
39412
|
-
run: () =>
|
|
40600
|
+
run: () => run28
|
|
39413
40601
|
});
|
|
39414
40602
|
function section2(title) {
|
|
39415
40603
|
const bar = "\u2500".repeat(60);
|
|
39416
40604
|
return `
|
|
39417
40605
|
${bold12(cyan7(title))}
|
|
39418
|
-
${
|
|
40606
|
+
${dim13(bar)}`;
|
|
39419
40607
|
}
|
|
39420
40608
|
function cmd2(s) {
|
|
39421
40609
|
return yellow11(s);
|
|
39422
40610
|
}
|
|
39423
40611
|
function flag(s) {
|
|
39424
|
-
return
|
|
40612
|
+
return green14(s);
|
|
39425
40613
|
}
|
|
39426
|
-
async function
|
|
40614
|
+
async function run28() {
|
|
39427
40615
|
const lines = [
|
|
39428
40616
|
"",
|
|
39429
40617
|
bold12("specialists \xB7 Quick Start Guide"),
|
|
39430
|
-
|
|
39431
|
-
|
|
40618
|
+
dim13("One MCP server. Multiple AI backends. Intelligent orchestration."),
|
|
40619
|
+
dim13("Tip: sp is a shorter alias \u2014 sp run, sp list, sp feed etc. work identically."),
|
|
39432
40620
|
""
|
|
39433
40621
|
];
|
|
39434
40622
|
lines.push(section2("1. Installation"));
|
|
39435
40623
|
lines.push("");
|
|
40624
|
+
lines.push(` ${bold12("Prerequisite: Bun")} ${cmd2("bun --version")} # verify Bun >=1.0.0`);
|
|
40625
|
+
lines.push(` ${cmd2("curl -fsSL https://bun.sh/install | bash")} # install Bun if missing`);
|
|
40626
|
+
lines.push(` ${cmd2("npm install -g xtrm-tools")} # install runtime prerequisite`);
|
|
40627
|
+
lines.push(` ${cmd2("xt install")} # install xtrm-managed assets`);
|
|
40628
|
+
lines.push(` ${cmd2("xt init")} # initialize .xtrm/ in this repo`);
|
|
40629
|
+
lines.push(` ${dim13("sp list, sp doctor --check-drift, sp prune-stale-defaults are Category A and do NOT require xt or .xtrm/")}`);
|
|
39436
40630
|
lines.push(` ${cmd2("npm install -g @jaggerxtrm/specialists")} # install globally`);
|
|
39437
|
-
lines.push(` ${cmd2("
|
|
39438
|
-
lines.push(` ${
|
|
40631
|
+
lines.push(` ${cmd2("sp init")} # project setup:`);
|
|
40632
|
+
lines.push(` ${dim13(" # creates dirs, wires MCP + hooks, injects context")}`);
|
|
39439
40633
|
lines.push("");
|
|
39440
40634
|
lines.push(` Verify everything is healthy:`);
|
|
39441
40635
|
lines.push(` ${cmd2("specialists status")} # shows pi, beads, MCP, active jobs`);
|
|
@@ -39443,13 +40637,14 @@ async function run27() {
|
|
|
39443
40637
|
lines.push(section2("2. Initialize a Project"));
|
|
39444
40638
|
lines.push("");
|
|
39445
40639
|
lines.push(` Run once per project root:`);
|
|
39446
|
-
lines.push(` ${cmd2("
|
|
40640
|
+
lines.push(` ${cmd2("sp init")} # creates .specialists/, wires MCP + AGENTS.md`);
|
|
40641
|
+
lines.push(` ${dim13(" # requires xt init first so .xtrm/ already exists")}`);
|
|
39447
40642
|
lines.push("");
|
|
39448
40643
|
lines.push(` What this creates:`);
|
|
39449
|
-
lines.push(` ${
|
|
39450
|
-
lines.push(` ${
|
|
39451
|
-
lines.push(` ${
|
|
39452
|
-
lines.push(` ${
|
|
40644
|
+
lines.push(` ${dim13(".specialists/default/")} \u2014 canonical specialists (from init)`);
|
|
40645
|
+
lines.push(` ${dim13(".specialists/user/")} \u2014 custom .specialist.json files`);
|
|
40646
|
+
lines.push(` ${dim13(".specialists/jobs|ready")} \u2014 runtime data \u2014 gitignored`);
|
|
40647
|
+
lines.push(` ${dim13("AGENTS.md")} \u2014 context block injected into Claude sessions`);
|
|
39453
40648
|
lines.push("");
|
|
39454
40649
|
lines.push(section2("3. Discover Specialists"));
|
|
39455
40650
|
lines.push("");
|
|
@@ -39466,17 +40661,17 @@ async function run27() {
|
|
|
39466
40661
|
lines.push(section2("4. Running a Specialist"));
|
|
39467
40662
|
lines.push("");
|
|
39468
40663
|
lines.push(` ${bold12("Foreground")} (streams output to stdout):`);
|
|
39469
|
-
lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${
|
|
40664
|
+
lines.push(` ${cmd2("specialists run code-review")} ${flag("--prompt")} ${dim13('"Review src/api.ts for security issues"')}`);
|
|
39470
40665
|
lines.push("");
|
|
39471
40666
|
lines.push(` ${bold12("Tracked run")} (linked to a beads issue for workflow integration):`);
|
|
39472
|
-
lines.push(` ${cmd2("specialists run code-review")} ${flag("--bead")} ${
|
|
39473
|
-
lines.push(` ${
|
|
40667
|
+
lines.push(` ${cmd2("specialists run code-review")} ${flag("--bead")} ${dim13("unitAI-abc")}`);
|
|
40668
|
+
lines.push(` ${dim13(" # uses bead description as prompt, tracks result in issue")}`);
|
|
39474
40669
|
lines.push("");
|
|
39475
40670
|
lines.push(` Override model for one run:`);
|
|
39476
|
-
lines.push(` ${cmd2("specialists run code-review")} ${flag("--model")} ${
|
|
40671
|
+
lines.push(` ${cmd2("specialists run code-review")} ${flag("--model")} ${dim13("anthropic/claude-opus-4-6")} ${flag("--prompt")} ${dim13('"..."')}`);
|
|
39477
40672
|
lines.push("");
|
|
39478
40673
|
lines.push(` Run without beads issue tracking:`);
|
|
39479
|
-
lines.push(` ${cmd2("specialists run code-review")} ${flag("--no-beads")} ${flag("--prompt")} ${
|
|
40674
|
+
lines.push(` ${cmd2("specialists run code-review")} ${flag("--no-beads")} ${flag("--prompt")} ${dim13('"..."')}`);
|
|
39480
40675
|
lines.push("");
|
|
39481
40676
|
lines.push(` Pipe a prompt from stdin:`);
|
|
39482
40677
|
lines.push(` ${cmd2("cat my-brief.md | specialists run code-review")}`);
|
|
@@ -39484,7 +40679,7 @@ async function run27() {
|
|
|
39484
40679
|
lines.push(section2("5. Async Job Lifecycle"));
|
|
39485
40680
|
lines.push("");
|
|
39486
40681
|
lines.push(` ${bold12("MCP pattern")}: ${cmd2("use_specialist")} (foreground, returns result directly)`);
|
|
39487
|
-
lines.push(` ${bold12("CLI pattern")}: ${cmd2('specialists run <name> --prompt "..."')} prints ${
|
|
40682
|
+
lines.push(` ${bold12("CLI pattern")}: ${cmd2('specialists run <name> --prompt "..."')} prints ${dim13("[job started: <id>]")} to stderr`);
|
|
39488
40683
|
lines.push(` ${bold12("Shell pattern")}: ${cmd2('specialists run <name> --prompt "..." &')} for native backgrounding`);
|
|
39489
40684
|
lines.push("");
|
|
39490
40685
|
lines.push(` ${bold12("Watch progress")} \u2014 stream events as they arrive:`);
|
|
@@ -39496,11 +40691,11 @@ async function run27() {
|
|
|
39496
40691
|
lines.push("");
|
|
39497
40692
|
lines.push(` ${bold12("Steer a running job")} \u2014 redirect the agent mid-run without cancelling:`);
|
|
39498
40693
|
lines.push(` ${cmd2("specialists steer job_a1b2c3d4")} ${flag('"focus only on supervisor.ts"')}`);
|
|
39499
|
-
lines.push(` ${
|
|
40694
|
+
lines.push(` ${dim13(" # delivered after current tool calls finish, before the next LLM call")}`);
|
|
39500
40695
|
lines.push("");
|
|
39501
40696
|
lines.push(` ${bold12("Keep-alive multi-turn")} \u2014 start with ${flag("--keep-alive")}, then follow up:`);
|
|
39502
40697
|
lines.push(` ${cmd2("specialists run debugger")} ${flag("--bead unitAI-abc --keep-alive")}`);
|
|
39503
|
-
lines.push(` ${
|
|
40698
|
+
lines.push(` ${dim13(" # \u2192 status: waiting after first turn")}`);
|
|
39504
40699
|
lines.push(` ${cmd2("specialists result a1b2c3")} # read first turn`);
|
|
39505
40700
|
lines.push(` ${cmd2("specialists follow-up a1b2c3")} ${flag('"now write the fix"')} # next turn, same Pi context`);
|
|
39506
40701
|
lines.push(` ${cmd2("specialists feed a1b2c3")} ${flag("--follow")} # watch response`);
|
|
@@ -39508,23 +40703,23 @@ async function run27() {
|
|
|
39508
40703
|
lines.push(` ${bold12("Cancel a job")}:`);
|
|
39509
40704
|
lines.push(` ${cmd2("specialists stop job_a1b2c3d4")} # sends SIGTERM to the agent process`);
|
|
39510
40705
|
lines.push("");
|
|
39511
|
-
lines.push(` ${bold12("Job files")} in ${
|
|
39512
|
-
lines.push(` ${
|
|
39513
|
-
lines.push(` ${
|
|
39514
|
-
lines.push(` ${
|
|
39515
|
-
lines.push(` ${
|
|
40706
|
+
lines.push(` ${bold12("Job files")} in ${dim13(".specialists/jobs/<job-id>/")}:`);
|
|
40707
|
+
lines.push(` ${dim13("status.json")} \u2014 id, specialist, status, pid, started_at, elapsed_s, current_tool`);
|
|
40708
|
+
lines.push(` ${dim13("events.jsonl")} \u2014 one JSON event per line (tool_use, text, agent_end, error \u2026)`);
|
|
40709
|
+
lines.push(` ${dim13("result.txt")} \u2014 final output (written when status=done)`);
|
|
40710
|
+
lines.push(` ${dim13("steer.pipe")} \u2014 named FIFO for mid-run steering (removed on job completion)`);
|
|
39516
40711
|
lines.push("");
|
|
39517
40712
|
lines.push(section2("6. Editing Specialists"));
|
|
39518
40713
|
lines.push("");
|
|
39519
40714
|
lines.push(` Change a field without opening the YAML manually:`);
|
|
39520
|
-
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${
|
|
39521
|
-
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--description")} ${
|
|
39522
|
-
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--timeout")} ${
|
|
39523
|
-
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--permission")} ${
|
|
39524
|
-
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--tags")} ${
|
|
40715
|
+
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${dim13("anthropic/claude-sonnet-4-6")}`);
|
|
40716
|
+
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--description")} ${dim13('"Updated description"')}`);
|
|
40717
|
+
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--timeout")} ${dim13("120000")}`);
|
|
40718
|
+
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--permission")} ${dim13("HIGH")}`);
|
|
40719
|
+
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--tags")} ${dim13("analysis,security,review")}`);
|
|
39525
40720
|
lines.push("");
|
|
39526
40721
|
lines.push(` Preview without writing:`);
|
|
39527
|
-
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${
|
|
40722
|
+
lines.push(` ${cmd2("specialists edit code-review")} ${flag("--model")} ${dim13("...")} ${flag("--dry-run")}`);
|
|
39528
40723
|
lines.push("");
|
|
39529
40724
|
lines.push(section2("7. .specialist.json Schema"));
|
|
39530
40725
|
lines.push("");
|
|
@@ -39571,12 +40766,12 @@ async function run27() {
|
|
|
39571
40766
|
" priority: 2 # 0=critical \u2026 4=backlog"
|
|
39572
40767
|
];
|
|
39573
40768
|
for (const l of schemaLines) {
|
|
39574
|
-
lines.push(` ${
|
|
40769
|
+
lines.push(` ${dim13(l)}`);
|
|
39575
40770
|
}
|
|
39576
40771
|
lines.push("");
|
|
39577
40772
|
lines.push(section2("8. Hook System"));
|
|
39578
40773
|
lines.push("");
|
|
39579
|
-
lines.push(` Specialists emits lifecycle events to ${
|
|
40774
|
+
lines.push(` Specialists emits lifecycle events to ${dim13(".specialists/trace.jsonl")}:`);
|
|
39580
40775
|
lines.push("");
|
|
39581
40776
|
lines.push(` ${bold12("Hook point")} ${bold12("When fired")}`);
|
|
39582
40777
|
lines.push(` ${yellow11("specialist:start")} before the agent session begins`);
|
|
@@ -39585,7 +40780,7 @@ async function run27() {
|
|
|
39585
40780
|
lines.push(` ${yellow11("specialist:error")} on failure or timeout`);
|
|
39586
40781
|
lines.push("");
|
|
39587
40782
|
lines.push(` Each event line in trace.jsonl:`);
|
|
39588
|
-
lines.push(` ${
|
|
40783
|
+
lines.push(` ${dim13('{"t":"<ISO>","hook":"specialist:done","specialist":"code-review","durationMs":4120}')}`);
|
|
39589
40784
|
lines.push("");
|
|
39590
40785
|
lines.push(` Tail the trace file to observe all activity:`);
|
|
39591
40786
|
lines.push(` ${cmd2("tail -f .specialists/trace.jsonl | jq .")}`);
|
|
@@ -39610,7 +40805,7 @@ async function run27() {
|
|
|
39610
40805
|
lines.push("");
|
|
39611
40806
|
lines.push(` ${bold12("Tracked run with beads integration:")}`);
|
|
39612
40807
|
lines.push(` ${cmd2("specialists run deep-analysis --bead unitAI-abc")}`);
|
|
39613
|
-
lines.push(` ${
|
|
40808
|
+
lines.push(` ${dim13(" # prompt from bead, result tracked in bead")}`);
|
|
39614
40809
|
lines.push("");
|
|
39615
40810
|
lines.push(` ${bold12("Steer a job mid-run:")}`);
|
|
39616
40811
|
lines.push(` ${cmd2('specialists steer <job-id> "focus only on the auth module"')}`);
|
|
@@ -39625,20 +40820,21 @@ async function run27() {
|
|
|
39625
40820
|
lines.push(` ${bold12("Override model for a single run:")}`);
|
|
39626
40821
|
lines.push(` ${cmd2('specialists run code-review --model anthropic/claude-opus-4-6 --prompt "..."')}`);
|
|
39627
40822
|
lines.push("");
|
|
39628
|
-
lines.push(
|
|
39629
|
-
lines.push(` ${
|
|
39630
|
-
lines.push(` ${
|
|
40823
|
+
lines.push(dim13("\u2500".repeat(62)));
|
|
40824
|
+
lines.push(` ${dim13("specialists help")} command list ${dim13("specialists <cmd> --help")} per-command flags`);
|
|
40825
|
+
lines.push(` ${dim13("specialists status")} health check ${dim13("specialists models")} available models`);
|
|
39631
40826
|
lines.push("");
|
|
39632
40827
|
console.log(lines.join(`
|
|
39633
40828
|
`));
|
|
39634
40829
|
}
|
|
39635
|
-
var bold12 = (s) => `\x1B[1m${s}\x1B[0m`,
|
|
40830
|
+
var bold12 = (s) => `\x1B[1m${s}\x1B[0m`, dim13 = (s) => `\x1B[2m${s}\x1B[0m`, yellow11 = (s) => `\x1B[33m${s}\x1B[0m`, cyan7 = (s) => `\x1B[36m${s}\x1B[0m`, blue4 = (s) => `\x1B[34m${s}\x1B[0m`, green14 = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
39636
40831
|
|
|
39637
40832
|
// src/cli/doctor.ts
|
|
39638
40833
|
var exports_doctor = {};
|
|
39639
40834
|
__export(exports_doctor, {
|
|
39640
40835
|
setStatusError: () => setStatusError,
|
|
39641
|
-
run: () =>
|
|
40836
|
+
run: () => run29,
|
|
40837
|
+
resolvePackageAssetDir: () => resolvePackageAssetDir,
|
|
39642
40838
|
renderProcessSummary: () => renderProcessSummary,
|
|
39643
40839
|
parseVersionTuple: () => parseVersionTuple,
|
|
39644
40840
|
compareVersions: () => compareVersions2,
|
|
@@ -39646,22 +40842,22 @@ __export(exports_doctor, {
|
|
|
39646
40842
|
});
|
|
39647
40843
|
import { createHash as createHash5 } from "crypto";
|
|
39648
40844
|
import { spawnSync as spawnSync22 } from "child_process";
|
|
39649
|
-
import { existsSync as
|
|
39650
|
-
import { dirname as dirname10, join as
|
|
40845
|
+
import { existsSync as existsSync31, lstatSync as lstatSync2, mkdirSync as mkdirSync11, readdirSync as readdirSync16, readFileSync as readFileSync31, readlinkSync as readlinkSync3, writeFileSync as writeFileSync13 } from "fs";
|
|
40846
|
+
import { dirname as dirname10, join as join33, relative as relative4, resolve as resolve13 } from "path";
|
|
39651
40847
|
function ok3(msg) {
|
|
39652
|
-
console.log(` ${
|
|
40848
|
+
console.log(` ${green15("\u2713")} ${msg}`);
|
|
39653
40849
|
}
|
|
39654
40850
|
function warn3(msg) {
|
|
39655
40851
|
console.log(` ${yellow12("\u25CB")} ${msg}`);
|
|
39656
40852
|
}
|
|
39657
40853
|
function fail4(msg) {
|
|
39658
|
-
console.log(` ${
|
|
40854
|
+
console.log(` ${red8("\u2717")} ${msg}`);
|
|
39659
40855
|
}
|
|
39660
40856
|
function fix(msg) {
|
|
39661
|
-
console.log(` ${
|
|
40857
|
+
console.log(` ${dim14("\u2192 fix:")} ${yellow12(msg)}`);
|
|
39662
40858
|
}
|
|
39663
40859
|
function hint(msg) {
|
|
39664
|
-
console.log(` ${
|
|
40860
|
+
console.log(` ${dim14(msg)}`);
|
|
39665
40861
|
}
|
|
39666
40862
|
function section3(label) {
|
|
39667
40863
|
const line = "\u2500".repeat(Math.max(0, 38 - label.length));
|
|
@@ -39676,10 +40872,10 @@ function isInstalled3(bin) {
|
|
|
39676
40872
|
return spawnSync22("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
|
|
39677
40873
|
}
|
|
39678
40874
|
function loadJson2(path) {
|
|
39679
|
-
if (!
|
|
40875
|
+
if (!existsSync31(path))
|
|
39680
40876
|
return null;
|
|
39681
40877
|
try {
|
|
39682
|
-
return JSON.parse(
|
|
40878
|
+
return JSON.parse(readFileSync31(path, "utf8"));
|
|
39683
40879
|
} catch {
|
|
39684
40880
|
return null;
|
|
39685
40881
|
}
|
|
@@ -39701,7 +40897,7 @@ function checkPi() {
|
|
|
39701
40897
|
fix("pi config (add at least one API key)");
|
|
39702
40898
|
return false;
|
|
39703
40899
|
}
|
|
39704
|
-
ok3(`pi ${vStr} \u2014 ${providers.size} provider${providers.size > 1 ? "s" : ""} active ${
|
|
40900
|
+
ok3(`pi ${vStr} \u2014 ${providers.size} provider${providers.size > 1 ? "s" : ""} active ${dim14(`(${[...providers].join(", ")})`)}`);
|
|
39705
40901
|
return true;
|
|
39706
40902
|
}
|
|
39707
40903
|
function checkSpAlias() {
|
|
@@ -39721,8 +40917,8 @@ function checkBd() {
|
|
|
39721
40917
|
fix("install beads (bd) first");
|
|
39722
40918
|
return false;
|
|
39723
40919
|
}
|
|
39724
|
-
ok3(`bd installed ${
|
|
39725
|
-
if (
|
|
40920
|
+
ok3(`bd installed ${dim14(sp("bd", ["--version"]).stdout || "")}`);
|
|
40921
|
+
if (existsSync31(join33(CWD, ".beads")))
|
|
39726
40922
|
ok3(".beads/ present in project");
|
|
39727
40923
|
else
|
|
39728
40924
|
warn3(".beads/ not found in project");
|
|
@@ -39735,22 +40931,22 @@ function checkXt() {
|
|
|
39735
40931
|
fix("install xtrm-tools first");
|
|
39736
40932
|
return false;
|
|
39737
40933
|
}
|
|
39738
|
-
ok3(`xt installed ${
|
|
40934
|
+
ok3(`xt installed ${dim14(sp("xt", ["--version"]).stdout || "")}`);
|
|
39739
40935
|
return true;
|
|
39740
40936
|
}
|
|
39741
40937
|
function checkHooks() {
|
|
39742
40938
|
section3("Claude Code hooks (2 expected)");
|
|
39743
40939
|
let allPresent = true;
|
|
39744
40940
|
for (const name of HOOK_NAMES) {
|
|
39745
|
-
const canonicalPath =
|
|
39746
|
-
if (!
|
|
39747
|
-
fail4(`${relative4(CWD, canonicalPath)} ${
|
|
40941
|
+
const canonicalPath = join33(HOOKS_DIR, name);
|
|
40942
|
+
if (!existsSync31(canonicalPath)) {
|
|
40943
|
+
fail4(`${relative4(CWD, canonicalPath)} ${red8("missing")}`);
|
|
39748
40944
|
fix("specialists init");
|
|
39749
40945
|
allPresent = false;
|
|
39750
40946
|
} else {
|
|
39751
40947
|
ok3(relative4(CWD, canonicalPath));
|
|
39752
40948
|
}
|
|
39753
|
-
const claudeHookPath =
|
|
40949
|
+
const claudeHookPath = join33(CLAUDE_HOOKS_DIR, name);
|
|
39754
40950
|
const symlinkState = isSymlinkTo(claudeHookPath, canonicalPath);
|
|
39755
40951
|
if (symlinkState.ok) {
|
|
39756
40952
|
ok3(`${relative4(CWD, claudeHookPath)} -> ${relative4(dirname10(claudeHookPath), canonicalPath)}`);
|
|
@@ -39825,14 +41021,14 @@ function checkVersion() {
|
|
|
39825
41021
|
}
|
|
39826
41022
|
function hashFile(path) {
|
|
39827
41023
|
const hash = createHash5("sha256");
|
|
39828
|
-
hash.update(
|
|
41024
|
+
hash.update(readFileSync31(path));
|
|
39829
41025
|
return hash.digest("hex");
|
|
39830
41026
|
}
|
|
39831
41027
|
function collectFileHashes(rootDir) {
|
|
39832
41028
|
const hashes = new Map;
|
|
39833
41029
|
const visit2 = (dir) => {
|
|
39834
|
-
for (const entry of
|
|
39835
|
-
const fullPath =
|
|
41030
|
+
for (const entry of readdirSync16(dir, { withFileTypes: true })) {
|
|
41031
|
+
const fullPath = join33(dir, entry.name);
|
|
39836
41032
|
if (entry.isDirectory()) {
|
|
39837
41033
|
visit2(fullPath);
|
|
39838
41034
|
continue;
|
|
@@ -39843,12 +41039,12 @@ function collectFileHashes(rootDir) {
|
|
|
39843
41039
|
hashes.set(relPath2, hashFile(fullPath));
|
|
39844
41040
|
}
|
|
39845
41041
|
};
|
|
39846
|
-
if (
|
|
41042
|
+
if (existsSync31(rootDir))
|
|
39847
41043
|
visit2(rootDir);
|
|
39848
41044
|
return hashes;
|
|
39849
41045
|
}
|
|
39850
41046
|
function isSymlinkTo(linkPath, expectedTargetPath) {
|
|
39851
|
-
if (!
|
|
41047
|
+
if (!existsSync31(linkPath))
|
|
39852
41048
|
return { ok: false, reason: "missing" };
|
|
39853
41049
|
let stats;
|
|
39854
41050
|
try {
|
|
@@ -39859,7 +41055,7 @@ function isSymlinkTo(linkPath, expectedTargetPath) {
|
|
|
39859
41055
|
if (!stats.isSymbolicLink())
|
|
39860
41056
|
return { ok: false, reason: "not-symlink" };
|
|
39861
41057
|
try {
|
|
39862
|
-
const rawTarget =
|
|
41058
|
+
const rawTarget = readlinkSync3(linkPath);
|
|
39863
41059
|
const resolvedTarget = resolve13(dirname10(linkPath), rawTarget);
|
|
39864
41060
|
const resolvedExpected = resolve13(expectedTargetPath);
|
|
39865
41061
|
if (resolvedTarget !== resolvedExpected) {
|
|
@@ -39870,19 +41066,23 @@ function isSymlinkTo(linkPath, expectedTargetPath) {
|
|
|
39870
41066
|
return { ok: false, reason: "broken" };
|
|
39871
41067
|
}
|
|
39872
41068
|
}
|
|
41069
|
+
function resolvePackageAssetDir(relativePath) {
|
|
41070
|
+
return resolveCanonicalAssetDir(relativePath) ?? (existsSync31(join33(CWD, "config", relativePath)) ? join33(CWD, "config", relativePath) : null);
|
|
41071
|
+
}
|
|
39873
41072
|
function checkSkillDrift() {
|
|
39874
|
-
section3("
|
|
39875
|
-
|
|
39876
|
-
|
|
39877
|
-
|
|
41073
|
+
section3("Category A package-live skill sync");
|
|
41074
|
+
const canonicalSkillsDir = resolvePackageAssetDir("skills");
|
|
41075
|
+
if (!canonicalSkillsDir) {
|
|
41076
|
+
fail4("package canonical skills source missing");
|
|
41077
|
+
fix("restore config/skills/ or install package assets");
|
|
39878
41078
|
return false;
|
|
39879
41079
|
}
|
|
39880
|
-
if (!
|
|
41080
|
+
if (!existsSync31(XTRM_DEFAULT_SKILLS_DIR)) {
|
|
39881
41081
|
fail4(".xtrm/skills/default/ missing");
|
|
39882
41082
|
fix("specialists init --sync-skills");
|
|
39883
41083
|
return false;
|
|
39884
41084
|
}
|
|
39885
|
-
const canonicalHashes = collectFileHashes(
|
|
41085
|
+
const canonicalHashes = collectFileHashes(canonicalSkillsDir);
|
|
39886
41086
|
const defaultHashes = collectFileHashes(XTRM_DEFAULT_SKILLS_DIR);
|
|
39887
41087
|
const drifted = [];
|
|
39888
41088
|
const missingInDefault = [];
|
|
@@ -39901,10 +41101,10 @@ function checkSkillDrift() {
|
|
|
39901
41101
|
extraInDefault.push(relPath2);
|
|
39902
41102
|
}
|
|
39903
41103
|
if (drifted.length === 0 && missingInDefault.length === 0 && extraInDefault.length === 0) {
|
|
39904
|
-
ok3(
|
|
41104
|
+
ok3(`${relative4(CWD, canonicalSkillsDir)} and .xtrm/skills/default/ are in sync`);
|
|
39905
41105
|
} else {
|
|
39906
41106
|
if (drifted.length > 0) {
|
|
39907
|
-
fail4(`${drifted.length} drifted file${drifted.length === 1 ? "" : "s"} between
|
|
41107
|
+
fail4(`${drifted.length} drifted file${drifted.length === 1 ? "" : "s"} between ${relative4(CWD, canonicalSkillsDir)} and .xtrm/skills/default`);
|
|
39908
41108
|
hint(`example: ${drifted.slice(0, 3).join(", ")}${drifted.length > 3 ? ", ..." : ""}`);
|
|
39909
41109
|
}
|
|
39910
41110
|
if (missingInDefault.length > 0) {
|
|
@@ -39917,39 +41117,47 @@ function checkSkillDrift() {
|
|
|
39917
41117
|
}
|
|
39918
41118
|
fix("specialists init --sync-skills");
|
|
39919
41119
|
}
|
|
41120
|
+
const defaultSkills = readdirSync16(XTRM_DEFAULT_SKILLS_DIR, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
39920
41121
|
let linksOk = true;
|
|
39921
|
-
for (const
|
|
39922
|
-
const
|
|
39923
|
-
|
|
39924
|
-
|
|
39925
|
-
|
|
39926
|
-
linksOk = false;
|
|
41122
|
+
for (const skillName of defaultSkills) {
|
|
41123
|
+
const activeLinkPath = join33(XTRM_ACTIVE_SKILLS_DIR, skillName);
|
|
41124
|
+
const expectedTarget = join33(XTRM_DEFAULT_SKILLS_DIR, skillName);
|
|
41125
|
+
const state = isSymlinkTo(activeLinkPath, expectedTarget);
|
|
41126
|
+
if (state.ok)
|
|
39927
41127
|
continue;
|
|
41128
|
+
linksOk = false;
|
|
41129
|
+
const relLink = relative4(CWD, activeLinkPath);
|
|
41130
|
+
if (state.reason === "missing") {
|
|
41131
|
+
fail4(`${relLink} missing`);
|
|
41132
|
+
} else if (state.reason === "not-symlink") {
|
|
41133
|
+
fail4(`${relLink} is not a symlink`);
|
|
41134
|
+
} else if (state.reason === "wrong-target") {
|
|
41135
|
+
fail4(`${relLink} points to ${state.target ?? "unknown target"}`);
|
|
41136
|
+
} else {
|
|
41137
|
+
fail4(`${relLink} is broken`);
|
|
39928
41138
|
}
|
|
39929
|
-
|
|
39930
|
-
|
|
39931
|
-
|
|
39932
|
-
|
|
39933
|
-
|
|
39934
|
-
|
|
39935
|
-
|
|
39936
|
-
|
|
39937
|
-
|
|
39938
|
-
|
|
39939
|
-
|
|
39940
|
-
|
|
39941
|
-
|
|
39942
|
-
}
|
|
39943
|
-
|
|
39944
|
-
}
|
|
39945
|
-
fail4(`${relLink} is broken`);
|
|
39946
|
-
}
|
|
39947
|
-
fix("specialists init --sync-skills");
|
|
41139
|
+
fix("specialists init --sync-skills");
|
|
41140
|
+
}
|
|
41141
|
+
const legacyActiveRoots = [
|
|
41142
|
+
{ scope: "claude", root: join33(XTRM_ACTIVE_SKILLS_DIR, "claude") },
|
|
41143
|
+
{ scope: "pi", root: join33(XTRM_ACTIVE_SKILLS_DIR, "pi") }
|
|
41144
|
+
];
|
|
41145
|
+
for (const { root } of legacyActiveRoots) {
|
|
41146
|
+
if (!existsSync31(root))
|
|
41147
|
+
continue;
|
|
41148
|
+
if (isSymlinkTo(root, XTRM_ACTIVE_SKILLS_DIR).ok)
|
|
41149
|
+
continue;
|
|
41150
|
+
const relRoot = relative4(CWD, root);
|
|
41151
|
+
if (lstatSync2(root).isDirectory()) {
|
|
41152
|
+
warn3(`${relRoot}/ legacy scoped layout found`);
|
|
41153
|
+
} else {
|
|
41154
|
+
warn3(`${relRoot} legacy scoped layout found`);
|
|
39948
41155
|
}
|
|
41156
|
+
fix("specialists init --sync-skills");
|
|
39949
41157
|
}
|
|
39950
41158
|
const skillRootChecks = [
|
|
39951
|
-
{ root:
|
|
39952
|
-
{ root:
|
|
41159
|
+
{ root: join33(CLAUDE_DIR, "skills"), expected: XTRM_ACTIVE_SKILLS_DIR },
|
|
41160
|
+
{ root: join33(PI_DIR, "skills"), expected: XTRM_ACTIVE_SKILLS_DIR }
|
|
39953
41161
|
];
|
|
39954
41162
|
let rootLinksOk = true;
|
|
39955
41163
|
for (const check2 of skillRootChecks) {
|
|
@@ -39973,13 +41181,15 @@ function checkSkillDrift() {
|
|
|
39973
41181
|
}
|
|
39974
41182
|
return drifted.length === 0 && missingInDefault.length === 0 && linksOk && rootLinksOk;
|
|
39975
41183
|
}
|
|
39976
|
-
function checkManagedMirror(label,
|
|
39977
|
-
|
|
39978
|
-
|
|
41184
|
+
function checkManagedMirror(label, canonicalRelativePath, mirrorDir, fixHint) {
|
|
41185
|
+
const sourceDir = resolvePackageAssetDir(canonicalRelativePath);
|
|
41186
|
+
const sourceLabel = sourceDir ? relative4(CWD, sourceDir) : `package canonical ${canonicalRelativePath}`;
|
|
41187
|
+
if (!sourceDir) {
|
|
41188
|
+
warn3(`${label} source missing: package canonical ${canonicalRelativePath}`);
|
|
39979
41189
|
fix(fixHint);
|
|
39980
41190
|
return false;
|
|
39981
41191
|
}
|
|
39982
|
-
if (!
|
|
41192
|
+
if (!existsSync31(mirrorDir)) {
|
|
39983
41193
|
fail4(`${label} mirror missing: ${relative4(CWD, mirrorDir)}`);
|
|
39984
41194
|
fix(fixHint);
|
|
39985
41195
|
return false;
|
|
@@ -39990,7 +41200,7 @@ function checkManagedMirror(label, sourceDir, mirrorDir, fixHint) {
|
|
|
39990
41200
|
const missing = [...sourceHashes.keys()].filter((relPath2) => !mirrorHashes.has(relPath2));
|
|
39991
41201
|
const extra = [...mirrorHashes.keys()].filter((relPath2) => !sourceHashes.has(relPath2));
|
|
39992
41202
|
if (drifted.length === 0 && missing.length === 0 && extra.length === 0) {
|
|
39993
|
-
ok3(`${label} mirror in sync`);
|
|
41203
|
+
ok3(`${label} mirror in sync against ${sourceLabel}`);
|
|
39994
41204
|
return true;
|
|
39995
41205
|
}
|
|
39996
41206
|
if (drifted.length > 0) {
|
|
@@ -40009,33 +41219,33 @@ function checkManagedMirror(label, sourceDir, mirrorDir, fixHint) {
|
|
|
40009
41219
|
return false;
|
|
40010
41220
|
}
|
|
40011
41221
|
function checkManagedAssetMirrors() {
|
|
40012
|
-
section3("
|
|
40013
|
-
const specialistsOk = checkManagedMirror("specialists",
|
|
40014
|
-
const rulesOk = checkManagedMirror("mandatory-rules",
|
|
40015
|
-
const nodesOk = checkManagedMirror("nodes",
|
|
41222
|
+
section3("Category B xtrm-managed asset mirrors");
|
|
41223
|
+
const specialistsOk = checkManagedMirror("specialists", "specialists", DEFAULT_SPECIALISTS_DIR, "specialists init --sync-defaults");
|
|
41224
|
+
const rulesOk = checkManagedMirror("mandatory-rules", "mandatory-rules", join33(DEFAULT_SPECIALISTS_DIR, "mandatory-rules"), "specialists init --sync-defaults");
|
|
41225
|
+
const nodesOk = checkManagedMirror("nodes", "nodes", join33(DEFAULT_SPECIALISTS_DIR, "nodes"), "specialists init --sync-defaults");
|
|
40016
41226
|
return specialistsOk && rulesOk && nodesOk;
|
|
40017
41227
|
}
|
|
40018
41228
|
function checkUserOverlayDrift() {
|
|
40019
41229
|
section3("User specialist overlays");
|
|
40020
|
-
if (!
|
|
41230
|
+
if (!existsSync31(USER_SPECIALISTS_DIR)) {
|
|
40021
41231
|
ok3("no user overlays present");
|
|
40022
41232
|
return true;
|
|
40023
41233
|
}
|
|
40024
|
-
const overlays =
|
|
41234
|
+
const overlays = readdirSync16(USER_SPECIALISTS_DIR).filter((name) => name.endsWith(".specialist.json"));
|
|
40025
41235
|
if (overlays.length === 0) {
|
|
40026
41236
|
ok3("no user overlays present");
|
|
40027
41237
|
return true;
|
|
40028
41238
|
}
|
|
40029
41239
|
let allOk = true;
|
|
40030
41240
|
for (const name of overlays) {
|
|
40031
|
-
const userPath =
|
|
40032
|
-
const defaultPath =
|
|
41241
|
+
const userPath = join33(USER_SPECIALISTS_DIR, name);
|
|
41242
|
+
const defaultPath = join33(DEFAULT_SPECIALISTS_DIR, name);
|
|
40033
41243
|
const userSpec = loadJson2(userPath);
|
|
40034
41244
|
if (!userSpec) {
|
|
40035
41245
|
warn3(`${name}: failed to parse \u2014 skipping drift check`);
|
|
40036
41246
|
continue;
|
|
40037
41247
|
}
|
|
40038
|
-
if (!
|
|
41248
|
+
if (!existsSync31(defaultPath)) {
|
|
40039
41249
|
ok3(`${name}: user-only overlay (no default to drift from)`);
|
|
40040
41250
|
continue;
|
|
40041
41251
|
}
|
|
@@ -40063,18 +41273,18 @@ function checkUserOverlayDrift() {
|
|
|
40063
41273
|
}
|
|
40064
41274
|
function checkRuntimeDirs() {
|
|
40065
41275
|
section3(".specialists/ runtime directories");
|
|
40066
|
-
const rootDir =
|
|
40067
|
-
const jobsDir =
|
|
40068
|
-
const readyDir =
|
|
41276
|
+
const rootDir = join33(CWD, ".specialists");
|
|
41277
|
+
const jobsDir = join33(rootDir, "jobs");
|
|
41278
|
+
const readyDir = join33(rootDir, "ready");
|
|
40069
41279
|
let allOk = true;
|
|
40070
|
-
if (!
|
|
41280
|
+
if (!existsSync31(rootDir)) {
|
|
40071
41281
|
warn3(".specialists/ not found in current project");
|
|
40072
41282
|
fix("specialists init");
|
|
40073
41283
|
allOk = false;
|
|
40074
41284
|
} else {
|
|
40075
41285
|
ok3(".specialists/ present");
|
|
40076
41286
|
for (const [subDir, label] of [[jobsDir, "jobs"], [readyDir, "ready"]]) {
|
|
40077
|
-
if (!
|
|
41287
|
+
if (!existsSync31(subDir)) {
|
|
40078
41288
|
warn3(`.specialists/${label}/ missing \u2014 auto-creating`);
|
|
40079
41289
|
mkdirSync11(subDir, { recursive: true });
|
|
40080
41290
|
ok3(`.specialists/${label}/ created`);
|
|
@@ -40088,8 +41298,8 @@ function checkRuntimeDirs() {
|
|
|
40088
41298
|
function checkClaudeMdFragments() {
|
|
40089
41299
|
section3("CLAUDE.md fragments");
|
|
40090
41300
|
const projectRoot = process.cwd();
|
|
40091
|
-
const claudeMd =
|
|
40092
|
-
if (!
|
|
41301
|
+
const claudeMd = join33(projectRoot, "CLAUDE.md");
|
|
41302
|
+
if (!existsSync31(claudeMd)) {
|
|
40093
41303
|
warn3("No CLAUDE.md in project root \u2014 skipping fragment check");
|
|
40094
41304
|
return true;
|
|
40095
41305
|
}
|
|
@@ -40215,10 +41425,10 @@ function compareVersions2(left, right) {
|
|
|
40215
41425
|
}
|
|
40216
41426
|
function setStatusError(statusPath) {
|
|
40217
41427
|
try {
|
|
40218
|
-
const raw =
|
|
41428
|
+
const raw = readFileSync31(statusPath, "utf8");
|
|
40219
41429
|
const status = JSON.parse(raw);
|
|
40220
41430
|
status.status = "error";
|
|
40221
|
-
|
|
41431
|
+
writeFileSync13(statusPath, `${JSON.stringify(status, null, 2)}
|
|
40222
41432
|
`, "utf8");
|
|
40223
41433
|
} catch {}
|
|
40224
41434
|
}
|
|
@@ -40257,7 +41467,7 @@ function cleanupProcesses(jobsDir, dryRun) {
|
|
|
40257
41467
|
}
|
|
40258
41468
|
let entries;
|
|
40259
41469
|
try {
|
|
40260
|
-
entries =
|
|
41470
|
+
entries = readdirSync16(jobsDir);
|
|
40261
41471
|
} catch {
|
|
40262
41472
|
entries = [];
|
|
40263
41473
|
}
|
|
@@ -40269,11 +41479,11 @@ function cleanupProcesses(jobsDir, dryRun) {
|
|
|
40269
41479
|
zombieJobIds: []
|
|
40270
41480
|
};
|
|
40271
41481
|
for (const jobId of entries) {
|
|
40272
|
-
const statusPath =
|
|
40273
|
-
if (!
|
|
41482
|
+
const statusPath = join33(jobsDir, jobId, "status.json");
|
|
41483
|
+
if (!existsSync31(statusPath))
|
|
40274
41484
|
continue;
|
|
40275
41485
|
try {
|
|
40276
|
-
const status = JSON.parse(
|
|
41486
|
+
const status = JSON.parse(readFileSync31(statusPath, "utf8"));
|
|
40277
41487
|
result.total += 1;
|
|
40278
41488
|
if (status.status !== "running" && status.status !== "starting")
|
|
40279
41489
|
continue;
|
|
@@ -40358,8 +41568,8 @@ function resolveWatchdogMode() {
|
|
|
40358
41568
|
function checkZombieJobs() {
|
|
40359
41569
|
section3("Background jobs");
|
|
40360
41570
|
hint(`watchdog mode: ${resolveWatchdogMode()}`);
|
|
40361
|
-
const jobsDir =
|
|
40362
|
-
if (!
|
|
41571
|
+
const jobsDir = join33(CWD, ".specialists", "jobs");
|
|
41572
|
+
if (!existsSync31(jobsDir)) {
|
|
40363
41573
|
hint("No .specialists/jobs/ \u2014 skipping");
|
|
40364
41574
|
return true;
|
|
40365
41575
|
}
|
|
@@ -40369,7 +41579,7 @@ function checkZombieJobs() {
|
|
|
40369
41579
|
return true;
|
|
40370
41580
|
}
|
|
40371
41581
|
for (const jobId of result.zombieJobIds) {
|
|
40372
|
-
warn3(`${jobId} ${yellow12("ZOMBIE")} ${
|
|
41582
|
+
warn3(`${jobId} ${yellow12("ZOMBIE")} ${dim14("pid not found for running job")}`);
|
|
40373
41583
|
fix(`Edit .specialists/jobs/${jobId}/status.json \u2192 set "status": "error"`);
|
|
40374
41584
|
}
|
|
40375
41585
|
if (result.zombies === 0) {
|
|
@@ -40377,7 +41587,7 @@ function checkZombieJobs() {
|
|
|
40377
41587
|
}
|
|
40378
41588
|
return result.zombies === 0;
|
|
40379
41589
|
}
|
|
40380
|
-
async function
|
|
41590
|
+
async function run29(argv = process.argv.slice(3)) {
|
|
40381
41591
|
const subcommand = argv[0];
|
|
40382
41592
|
if (subcommand === "orphans") {
|
|
40383
41593
|
runDoctorOrphans();
|
|
@@ -40411,37 +41621,32 @@ ${bold13("specialists doctor")}
|
|
|
40411
41621
|
const allOk = piOk && spOk && bdOk && xtOk && hooksOk && mcpOk && versionOk && skillDriftOk && mirrorOk && userOverlayOk && dirsOk && jobsOk && fragmentsOk;
|
|
40412
41622
|
console.log("");
|
|
40413
41623
|
if (allOk) {
|
|
40414
|
-
console.log(` ${
|
|
41624
|
+
console.log(` ${green15("\u2713")} ${bold13("All checks passed")} \u2014 specialists is healthy`);
|
|
40415
41625
|
} else {
|
|
40416
41626
|
console.log(` ${yellow12("\u25CB")} ${bold13("Some checks failed")} \u2014 follow the fix hints above`);
|
|
40417
|
-
console.log(` ${
|
|
41627
|
+
console.log(` ${dim14("specialists init fixes hook + MCP registration; specialists init --sync-skills fixes skill drift/symlink issues; specialists init --sync-defaults fixes managed mirrors.")}`);
|
|
40418
41628
|
}
|
|
40419
41629
|
console.log("");
|
|
40420
41630
|
}
|
|
40421
|
-
var bold13 = (s) => `\x1B[1m${s}\x1B[0m`,
|
|
41631
|
+
var bold13 = (s) => `\x1B[1m${s}\x1B[0m`, dim14 = (s) => `\x1B[2m${s}\x1B[0m`, green15 = (s) => `\x1B[32m${s}\x1B[0m`, yellow12 = (s) => `\x1B[33m${s}\x1B[0m`, red8 = (s) => `\x1B[31m${s}\x1B[0m`, CWD, CLAUDE_DIR, PI_DIR, XTRM_SKILLS_DIR, XTRM_DEFAULT_SKILLS_DIR, XTRM_ACTIVE_SKILLS_DIR, SPECIALISTS_DIR, DEFAULT_SPECIALISTS_DIR, USER_SPECIALISTS_DIR, HOOKS_DIR, CLAUDE_HOOKS_DIR, SETTINGS_FILE, MCP_FILE2, HOOK_NAMES;
|
|
40422
41632
|
var init_doctor = __esm(() => {
|
|
40423
41633
|
init_observability_sqlite();
|
|
41634
|
+
init_canonical_asset_resolver();
|
|
40424
41635
|
init_drift_detector();
|
|
40425
41636
|
init_version_check();
|
|
40426
41637
|
CWD = process.cwd();
|
|
40427
|
-
CLAUDE_DIR =
|
|
40428
|
-
PI_DIR =
|
|
40429
|
-
XTRM_SKILLS_DIR =
|
|
40430
|
-
XTRM_DEFAULT_SKILLS_DIR =
|
|
40431
|
-
XTRM_ACTIVE_SKILLS_DIR =
|
|
40432
|
-
|
|
40433
|
-
|
|
40434
|
-
|
|
40435
|
-
|
|
40436
|
-
|
|
40437
|
-
|
|
40438
|
-
|
|
40439
|
-
DEFAULT_SPECIALISTS_DIR = join32(SPECIALISTS_DIR, "default");
|
|
40440
|
-
USER_SPECIALISTS_DIR = join32(SPECIALISTS_DIR, "user");
|
|
40441
|
-
HOOKS_DIR = join32(CWD, ".xtrm", "hooks", "specialists");
|
|
40442
|
-
CLAUDE_HOOKS_DIR = join32(CLAUDE_DIR, "hooks");
|
|
40443
|
-
SETTINGS_FILE = join32(CLAUDE_DIR, "settings.json");
|
|
40444
|
-
MCP_FILE2 = join32(CWD, ".mcp.json");
|
|
41638
|
+
CLAUDE_DIR = join33(CWD, ".claude");
|
|
41639
|
+
PI_DIR = join33(CWD, ".pi");
|
|
41640
|
+
XTRM_SKILLS_DIR = join33(CWD, ".xtrm", "skills");
|
|
41641
|
+
XTRM_DEFAULT_SKILLS_DIR = join33(XTRM_SKILLS_DIR, "default");
|
|
41642
|
+
XTRM_ACTIVE_SKILLS_DIR = join33(XTRM_SKILLS_DIR, "active");
|
|
41643
|
+
SPECIALISTS_DIR = join33(CWD, ".specialists");
|
|
41644
|
+
DEFAULT_SPECIALISTS_DIR = join33(SPECIALISTS_DIR, "default");
|
|
41645
|
+
USER_SPECIALISTS_DIR = join33(SPECIALISTS_DIR, "user");
|
|
41646
|
+
HOOKS_DIR = join33(CWD, ".xtrm", "hooks", "specialists");
|
|
41647
|
+
CLAUDE_HOOKS_DIR = join33(CLAUDE_DIR, "hooks");
|
|
41648
|
+
SETTINGS_FILE = join33(CLAUDE_DIR, "settings.json");
|
|
41649
|
+
MCP_FILE2 = join33(CWD, ".mcp.json");
|
|
40445
41650
|
HOOK_NAMES = [
|
|
40446
41651
|
"specialists-complete.mjs",
|
|
40447
41652
|
"specialists-session-start.mjs"
|
|
@@ -40451,9 +41656,9 @@ var init_doctor = __esm(() => {
|
|
|
40451
41656
|
// src/cli/setup.ts
|
|
40452
41657
|
var exports_setup = {};
|
|
40453
41658
|
__export(exports_setup, {
|
|
40454
|
-
run: () =>
|
|
41659
|
+
run: () => run30
|
|
40455
41660
|
});
|
|
40456
|
-
async function
|
|
41661
|
+
async function run30() {
|
|
40457
41662
|
console.log("");
|
|
40458
41663
|
console.log(yellow13("\u26A0 DEPRECATED: `specialists setup` is deprecated"));
|
|
40459
41664
|
console.log("");
|
|
@@ -40468,26 +41673,26 @@ async function run29() {
|
|
|
40468
41673
|
console.log(" Options:");
|
|
40469
41674
|
console.log(" --force-workflow Overwrite existing workflow blocks");
|
|
40470
41675
|
console.log("");
|
|
40471
|
-
console.log(` ${
|
|
41676
|
+
console.log(` ${dim15("Run: specialists init --help for full details")}`);
|
|
40472
41677
|
console.log("");
|
|
40473
41678
|
}
|
|
40474
|
-
var bold14 = (s) => `\x1B[1m${s}\x1B[0m`, yellow13 = (s) => `\x1B[33m${s}\x1B[0m`,
|
|
41679
|
+
var bold14 = (s) => `\x1B[1m${s}\x1B[0m`, yellow13 = (s) => `\x1B[33m${s}\x1B[0m`, dim15 = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
40475
41680
|
|
|
40476
41681
|
// src/cli/serve-hot-reload.ts
|
|
40477
|
-
import { existsSync as
|
|
40478
|
-
import { join as
|
|
41682
|
+
import { existsSync as existsSync32, readdirSync as readdirSync17, statSync as statSync6, watch as fsWatch } from "fs";
|
|
41683
|
+
import { join as join34 } from "path";
|
|
40479
41684
|
function specialistNameFromFile(file) {
|
|
40480
41685
|
const match = file.match(/^(.+)\.specialist\.(json|yaml)$/);
|
|
40481
41686
|
return match ? match[1] : null;
|
|
40482
41687
|
}
|
|
40483
41688
|
function snapshotMtimes(dir) {
|
|
40484
41689
|
const out = new Map;
|
|
40485
|
-
if (!
|
|
41690
|
+
if (!existsSync32(dir))
|
|
40486
41691
|
return out;
|
|
40487
|
-
const entries =
|
|
41692
|
+
const entries = readdirSync17(dir).filter((name) => specialistNameFromFile(name) !== null);
|
|
40488
41693
|
for (const name of entries) {
|
|
40489
41694
|
try {
|
|
40490
|
-
out.set(name,
|
|
41695
|
+
out.set(name, statSync6(join34(dir, name)).mtimeMs);
|
|
40491
41696
|
} catch {}
|
|
40492
41697
|
}
|
|
40493
41698
|
return out;
|
|
@@ -40544,7 +41749,7 @@ function createUserDirWatcher(opts) {
|
|
|
40544
41749
|
for (const file of changed)
|
|
40545
41750
|
queue(file);
|
|
40546
41751
|
}, opts.pollMs);
|
|
40547
|
-
} else if (
|
|
41752
|
+
} else if (existsSync32(opts.userDir)) {
|
|
40548
41753
|
try {
|
|
40549
41754
|
watcher = fsWatch(opts.userDir, { persistent: false }, (_eventType, filename) => {
|
|
40550
41755
|
queue(filename ? String(filename) : null);
|
|
@@ -40576,7 +41781,7 @@ var init_serve_hot_reload = () => {};
|
|
|
40576
41781
|
var exports_serve = {};
|
|
40577
41782
|
__export(exports_serve, {
|
|
40578
41783
|
startServe: () => startServe,
|
|
40579
|
-
run: () =>
|
|
41784
|
+
run: () => run31,
|
|
40580
41785
|
recordAuditFailure: () => recordAuditFailure,
|
|
40581
41786
|
evaluateReadiness: () => evaluateReadiness2,
|
|
40582
41787
|
createReadinessState: () => createReadinessState,
|
|
@@ -40587,9 +41792,9 @@ import { randomUUID as randomUUID3 } from "crypto";
|
|
|
40587
41792
|
import { once } from "events";
|
|
40588
41793
|
import { spawnSync as spawnSync23 } from "child_process";
|
|
40589
41794
|
import { access, readdir as readdir2, readFile as readFile5, constants } from "fs/promises";
|
|
40590
|
-
import { existsSync as
|
|
41795
|
+
import { existsSync as existsSync33 } from "fs";
|
|
40591
41796
|
import { homedir as homedir3 } from "os";
|
|
40592
|
-
import { join as
|
|
41797
|
+
import { join as join35 } from "path";
|
|
40593
41798
|
function createReadinessState() {
|
|
40594
41799
|
return { shuttingDown: false, auditFailures: [], dbWriteFailuresTotal: 0 };
|
|
40595
41800
|
}
|
|
@@ -40605,7 +41810,7 @@ function pruneAuditFailures(state, now = Date.now()) {
|
|
|
40605
41810
|
}
|
|
40606
41811
|
}
|
|
40607
41812
|
async function checkUserDirSpecs(userDir) {
|
|
40608
|
-
if (!
|
|
41813
|
+
if (!existsSync33(userDir))
|
|
40609
41814
|
return "empty";
|
|
40610
41815
|
const entries = await readdir2(userDir).catch(() => []);
|
|
40611
41816
|
const specFiles = entries.filter((name) => name.endsWith(".specialist.json") || name.endsWith(".specialist.yaml"));
|
|
@@ -40614,7 +41819,7 @@ async function checkUserDirSpecs(userDir) {
|
|
|
40614
41819
|
let validCount = 0;
|
|
40615
41820
|
for (const file of specFiles) {
|
|
40616
41821
|
try {
|
|
40617
|
-
const content = await readFile5(
|
|
41822
|
+
const content = await readFile5(join35(userDir, file), "utf-8");
|
|
40618
41823
|
const json = file.endsWith(".json") ? content : null;
|
|
40619
41824
|
if (!json)
|
|
40620
41825
|
continue;
|
|
@@ -40632,7 +41837,7 @@ async function evaluateReadiness2(opts) {
|
|
|
40632
41837
|
if (opts.state.auditFailures.length > opts.auditFailureThreshold) {
|
|
40633
41838
|
return { ready: false, reason: "degraded:audit" };
|
|
40634
41839
|
}
|
|
40635
|
-
const piConfigPath = opts.piConfigPath ??
|
|
41840
|
+
const piConfigPath = opts.piConfigPath ?? join35(homedir3(), ".pi", "agent", "auth.json");
|
|
40636
41841
|
try {
|
|
40637
41842
|
await access(piConfigPath, constants.R_OK);
|
|
40638
41843
|
} catch {
|
|
@@ -40653,7 +41858,7 @@ async function evaluateReadiness2(opts) {
|
|
|
40653
41858
|
warning = canaryFailure;
|
|
40654
41859
|
}
|
|
40655
41860
|
}
|
|
40656
|
-
const userDir =
|
|
41861
|
+
const userDir = join35(opts.projectDir, ".specialists", "user");
|
|
40657
41862
|
const userDirResult = await checkUserDirSpecs(userDir);
|
|
40658
41863
|
if (userDirResult === "empty")
|
|
40659
41864
|
return { ready: false, reason: "empty_user_dir" };
|
|
@@ -40774,7 +41979,7 @@ async function startServe(argv = process.argv.slice(3)) {
|
|
|
40774
41979
|
return createObservabilitySqliteClient(args.projectDir);
|
|
40775
41980
|
})();
|
|
40776
41981
|
const readinessState = createReadinessState();
|
|
40777
|
-
const userDir =
|
|
41982
|
+
const userDir = join35(args.projectDir, ".specialists", "user");
|
|
40778
41983
|
const hotReload = createUserDirWatcher({ loader, userDir, pollMs: args.reloadPollMs });
|
|
40779
41984
|
let active = 0;
|
|
40780
41985
|
const children = new Set;
|
|
@@ -40939,7 +42144,7 @@ async function startServe(argv = process.argv.slice(3)) {
|
|
|
40939
42144
|
console.log(`sp serve listening on ${args.port}`);
|
|
40940
42145
|
return { server, args, db, readinessState };
|
|
40941
42146
|
}
|
|
40942
|
-
async function
|
|
42147
|
+
async function run31(argv = process.argv.slice(3)) {
|
|
40943
42148
|
await startServe(argv);
|
|
40944
42149
|
}
|
|
40945
42150
|
var AUDIT_WINDOW_MS = 60000, DEFAULT_REQUIRED_PI_FLAGS;
|
|
@@ -40957,7 +42162,7 @@ var init_serve = __esm(() => {
|
|
|
40957
42162
|
var exports_script = {};
|
|
40958
42163
|
__export(exports_script, {
|
|
40959
42164
|
scriptCli: () => scriptCli,
|
|
40960
|
-
run: () =>
|
|
42165
|
+
run: () => run32,
|
|
40961
42166
|
parseArgs: () => parseArgs13,
|
|
40962
42167
|
mapExitCode: () => mapExitCode
|
|
40963
42168
|
});
|
|
@@ -41074,7 +42279,7 @@ function runUnderLock(lockPath, argv) {
|
|
|
41074
42279
|
return 75;
|
|
41075
42280
|
return flock.status ?? 1;
|
|
41076
42281
|
}
|
|
41077
|
-
async function
|
|
42282
|
+
async function run32(argv = process.argv.slice(3)) {
|
|
41078
42283
|
const args = parseArgs13(argv);
|
|
41079
42284
|
if (args.singleInstance && !process.env.SP_SCRIPT_NO_LOCK) {
|
|
41080
42285
|
process.exit(runUnderLock(args.singleInstance, argv));
|
|
@@ -41098,13 +42303,13 @@ var init_script = __esm(() => {
|
|
|
41098
42303
|
// src/cli/help.ts
|
|
41099
42304
|
var exports_help = {};
|
|
41100
42305
|
__export(exports_help, {
|
|
41101
|
-
run: () =>
|
|
42306
|
+
run: () => run33
|
|
41102
42307
|
});
|
|
41103
42308
|
function formatCommands(entries) {
|
|
41104
42309
|
const width = Math.max(...entries.map(([cmd3]) => cmd3.length));
|
|
41105
42310
|
return entries.map(([cmd3, desc]) => ` ${cmd3.padEnd(width)} ${desc}`);
|
|
41106
42311
|
}
|
|
41107
|
-
async function
|
|
42312
|
+
async function run33() {
|
|
41108
42313
|
const lines = [
|
|
41109
42314
|
"",
|
|
41110
42315
|
"Specialists lets you run project-scoped specialist agents with a bead-first workflow.",
|
|
@@ -41113,7 +42318,7 @@ async function run32() {
|
|
|
41113
42318
|
" specialists|sp [command]",
|
|
41114
42319
|
" specialists|sp [command] --help",
|
|
41115
42320
|
"",
|
|
41116
|
-
|
|
42321
|
+
dim16(" sp is a shorter alias \u2014 sp run, sp list, sp feed etc. all work identically."),
|
|
41117
42322
|
"",
|
|
41118
42323
|
bold15("Common flows:"),
|
|
41119
42324
|
"",
|
|
@@ -41195,13 +42400,13 @@ async function run32() {
|
|
|
41195
42400
|
" specialists init --help Bootstrap behavior and workflow injection",
|
|
41196
42401
|
" specialists feed --help Job event streaming details",
|
|
41197
42402
|
"",
|
|
41198
|
-
|
|
42403
|
+
dim16("Project model: specialists are project-only; user-scope discovery is deprecated."),
|
|
41199
42404
|
""
|
|
41200
42405
|
];
|
|
41201
42406
|
console.log(lines.join(`
|
|
41202
42407
|
`));
|
|
41203
42408
|
}
|
|
41204
|
-
var bold15 = (s) => `\x1B[1m${s}\x1B[0m`,
|
|
42409
|
+
var bold15 = (s) => `\x1B[1m${s}\x1B[0m`, dim16 = (s) => `\x1B[2m${s}\x1B[0m`, CORE_COMMANDS, EXTENDED_COMMANDS, WORKTREE_COMMANDS;
|
|
41205
42410
|
var init_help = __esm(() => {
|
|
41206
42411
|
CORE_COMMANDS = [
|
|
41207
42412
|
["init", "Bootstrap a project: dirs, workflow injection, project MCP registration"],
|
|
@@ -41218,7 +42423,7 @@ var init_help = __esm(() => {
|
|
|
41218
42423
|
["epic", "Epic lifecycle management: list/status/resolve wave-bound chain groups"],
|
|
41219
42424
|
["feed", "Tail job events; use -f to follow all jobs"],
|
|
41220
42425
|
["result", "Print final output of a completed job; --wait polls until done, --timeout <ms> sets a limit"],
|
|
41221
|
-
["clean", "
|
|
42426
|
+
["clean", "Clean job dirs, dashboard history (--ps), and orphan processes"],
|
|
41222
42427
|
["merge", "Publish one standalone chain (refuses unresolved epic chains)"],
|
|
41223
42428
|
["end", "Session-close publish helper; chain-aware and epic-aware with optional --pr mode"],
|
|
41224
42429
|
["epic list", "Enumerate epics with lifecycle state and merge readiness summary"],
|
|
@@ -41231,7 +42436,7 @@ var init_help = __esm(() => {
|
|
|
41231
42436
|
["attach", "Attach terminal to a running background job tmux session"],
|
|
41232
42437
|
["report", "Generate/show/list/diff session reports in .xtrm/reports/"],
|
|
41233
42438
|
["status", "Show health, MCP state, and active jobs"],
|
|
41234
|
-
["ps", "Show
|
|
42439
|
+
["ps", "Show actionable dashboard (active + unresolved terminal problems); --json, --all, --follow, --active, --health, --include-terminal, --include-cleaned"],
|
|
41235
42440
|
["doctor", "Diagnose installation/runtime problems; --check-drift reports stale .specialists/default/ snapshots"],
|
|
41236
42441
|
["prune-stale-defaults", "Prune redundant .specialists/default/ snapshots (byte-identical to package canonical); --dry-run, --root <path>"],
|
|
41237
42442
|
["quickstart", "Full getting-started guide"],
|
|
@@ -48756,12 +49961,21 @@ process.on("uncaughtException", (err) => {
|
|
|
48756
49961
|
console.error("[specialists] [ERROR] Fatal error:", err);
|
|
48757
49962
|
process.exit(1);
|
|
48758
49963
|
});
|
|
49964
|
+
if (typeof globalThis.Bun === "undefined") {
|
|
49965
|
+
console.error([
|
|
49966
|
+
"[specialists] [ERROR] Bun runtime required (>=1.0.0).",
|
|
49967
|
+
"[specialists] Install Bun: https://bun.sh/install",
|
|
49968
|
+
"[specialists] Example: curl -fsSL https://bun.sh/install | bash"
|
|
49969
|
+
].join(`
|
|
49970
|
+
`));
|
|
49971
|
+
process.exit(1);
|
|
49972
|
+
}
|
|
48759
49973
|
var sub = process.argv[2];
|
|
48760
49974
|
var next = process.argv[3];
|
|
48761
49975
|
function wantsHelp() {
|
|
48762
49976
|
return next === "--help" || next === "-h";
|
|
48763
49977
|
}
|
|
48764
|
-
async function
|
|
49978
|
+
async function run34() {
|
|
48765
49979
|
if (sub === "install") {
|
|
48766
49980
|
if (wantsHelp()) {
|
|
48767
49981
|
console.log([
|
|
@@ -48893,6 +50107,12 @@ async function run33() {
|
|
|
48893
50107
|
" \u2022 installs hooks to .claude/hooks/ and wires .claude/settings.json",
|
|
48894
50108
|
" \u2022 syncs skills into .xtrm/skills/default/ and wires active symlinks",
|
|
48895
50109
|
"",
|
|
50110
|
+
"Prereq:",
|
|
50111
|
+
" Bun >=1.0.0 required before xtrm-tools setup.",
|
|
50112
|
+
" Install order: Bun -> xtrm-tools -> xt install -> xt init ->",
|
|
50113
|
+
" @jaggerxtrm/specialists -> sp init.",
|
|
50114
|
+
" sp list, sp doctor --check-drift, sp prune-stale-defaults do not require xt.",
|
|
50115
|
+
"",
|
|
48896
50116
|
"Options:",
|
|
48897
50117
|
" --sync-defaults Also copy canonical specialists to .specialists/default/.",
|
|
48898
50118
|
" Human-only: rewrites default specialist YAML files.",
|
|
@@ -49207,31 +50427,29 @@ async function run33() {
|
|
|
49207
50427
|
if (wantsHelp()) {
|
|
49208
50428
|
console.log([
|
|
49209
50429
|
"",
|
|
49210
|
-
"Usage: specialists epic <list|status|
|
|
50430
|
+
"Usage: specialists epic <list|status|sync|abandon|merge> [options]",
|
|
49211
50431
|
"",
|
|
49212
50432
|
"Epic lifecycle management for wave-bound chain groups.",
|
|
49213
50433
|
"",
|
|
49214
50434
|
"Commands:",
|
|
49215
|
-
" list [--unresolved] [--json] Enumerate epics with
|
|
49216
|
-
" status <epic-id> [--json] Show
|
|
49217
|
-
"
|
|
50435
|
+
" list [--unresolved] [--json] Enumerate epics with readiness",
|
|
50436
|
+
" status <epic-id> [--json] Show derived readiness and chain status",
|
|
50437
|
+
" sync <epic-id> [--apply] [--json] Reconcile epic drift (dry-run by default)",
|
|
50438
|
+
" abandon <epic-id> --reason <text> [--force] [--json] Transition epic to abandoned",
|
|
49218
50439
|
" merge <epic-id> [--rebuild] [--pr] [--json] Publish epic chains (direct merge or PR mode)",
|
|
49219
50440
|
"",
|
|
49220
50441
|
"Options:",
|
|
49221
|
-
" --unresolved Filter list to
|
|
49222
|
-
" --dry-run Preview transition without persisting",
|
|
50442
|
+
" --unresolved Filter list to open epics only",
|
|
49223
50443
|
" --json Machine-readable JSON output",
|
|
49224
50444
|
"",
|
|
49225
|
-
"
|
|
49226
|
-
"
|
|
49227
|
-
"
|
|
50445
|
+
"Readiness:",
|
|
50446
|
+
" status is derived from live chain readiness",
|
|
50447
|
+
" persisted lifecycle state is compatibility metadata only",
|
|
49228
50448
|
"",
|
|
49229
50449
|
"Examples:",
|
|
49230
50450
|
" specialists epic list",
|
|
49231
50451
|
" specialists epic list --unresolved",
|
|
49232
50452
|
" specialists epic status unitAI-epic1",
|
|
49233
|
-
" specialists epic resolve unitAI-epic1",
|
|
49234
|
-
" specialists epic resolve unitAI-epic1 --dry-run",
|
|
49235
50453
|
" specialists epic merge unitAI-epic1 --pr",
|
|
49236
50454
|
""
|
|
49237
50455
|
].join(`
|
|
@@ -49277,14 +50495,18 @@ async function run33() {
|
|
|
49277
50495
|
"",
|
|
49278
50496
|
"Usage: specialists ps [options]",
|
|
49279
50497
|
"",
|
|
49280
|
-
"Process dashboard \u2014 shows active
|
|
49281
|
-
"
|
|
49282
|
-
"and next-action hints on every row.",
|
|
50498
|
+
"Process dashboard \u2014 shows active jobs plus unresolved terminal problems.",
|
|
50499
|
+
"Cleaned dashboard history and dead jobs are filtered by default. Includes",
|
|
50500
|
+
"context%, bead title, and next-action hints on every row.",
|
|
49283
50501
|
"",
|
|
49284
50502
|
"Options:",
|
|
49285
|
-
" --json
|
|
49286
|
-
" --all
|
|
49287
|
-
" --follow, -f
|
|
50503
|
+
" --json Structured JSON output with trees[].children[] schema",
|
|
50504
|
+
" --all Include every row, including cleaned/dead/terminal history",
|
|
50505
|
+
" --follow, -f Live-refresh view with spinner animation",
|
|
50506
|
+
" --health Show detailed process health tables (default is aggregate only)",
|
|
50507
|
+
" --active Show active jobs only; hide unresolved terminal problems",
|
|
50508
|
+
" --include-terminal Include terminal history that has not been cleaned",
|
|
50509
|
+
" --include-cleaned Include rows hidden by sp clean --ps",
|
|
49288
50510
|
"",
|
|
49289
50511
|
"Output columns:",
|
|
49290
50512
|
" st Status icon: \u25C9 running, \u25D0 waiting/starting, \u25CB done/error",
|
|
@@ -49302,9 +50524,17 @@ async function run33() {
|
|
|
49302
50524
|
"",
|
|
49303
50525
|
"Node refs accept any unique prefix.",
|
|
49304
50526
|
"",
|
|
50527
|
+
"Dashboard cleanup:",
|
|
50528
|
+
" sp clean --ps --dry-run previews terminal rows to hide from default ps.",
|
|
50529
|
+
" sp clean --ps hides terminal rows without deleting DB history or changing status.",
|
|
50530
|
+
" Use --include-cleaned or --all to audit cleaned rows later.",
|
|
50531
|
+
"",
|
|
49305
50532
|
"Examples:",
|
|
49306
|
-
" specialists ps Active
|
|
49307
|
-
" specialists ps --
|
|
50533
|
+
" specialists ps Active + unresolved terminal problems",
|
|
50534
|
+
" specialists ps --active Active jobs only",
|
|
50535
|
+
" specialists ps --include-terminal Include uncleaned terminal history",
|
|
50536
|
+
" specialists ps --include-cleaned Show rows hidden by sp clean --ps",
|
|
50537
|
+
" specialists ps --all Full audit view including cleaned/dead/history",
|
|
49308
50538
|
" specialists ps --json Machine-readable tree output",
|
|
49309
50539
|
" specialists ps --node research Filter dashboard to one node run",
|
|
49310
50540
|
" specialists ps --follow Live dashboard with auto-refresh",
|
|
@@ -49465,24 +50695,52 @@ async function run33() {
|
|
|
49465
50695
|
console.log([
|
|
49466
50696
|
"",
|
|
49467
50697
|
"Usage: specialists clean [--all] [--keep <n>] [--dry-run]",
|
|
50698
|
+
" specialists clean --ps [--dry-run]",
|
|
50699
|
+
" specialists clean --reap-orphans [--dry-run]",
|
|
50700
|
+
" specialists clean --observability --before <iso|duration> [--include-epics] [--dry-run]",
|
|
49468
50701
|
"",
|
|
49469
|
-
"
|
|
50702
|
+
"Clean specialist runtime artifacts and dashboard visibility.",
|
|
49470
50703
|
"",
|
|
49471
50704
|
"Default behavior:",
|
|
49472
|
-
" - removes done/error
|
|
50705
|
+
" - removes done/error job directories older than SPECIALISTS_JOB_TTL_DAYS",
|
|
49473
50706
|
" - TTL defaults to 7 days if env is unset",
|
|
49474
50707
|
" - never removes SQLite artifacts (*.db, *.db-wal, *.db-shm)",
|
|
50708
|
+
" - never prunes observability.db rows unless --observability is explicit",
|
|
50709
|
+
"",
|
|
50710
|
+
"Dashboard cleanup:",
|
|
50711
|
+
" - --ps soft-hides terminal rows from default sp ps",
|
|
50712
|
+
" - --ps does not delete SQLite rows or change job status",
|
|
50713
|
+
" - sp ps --include-cleaned / --all restore audit visibility",
|
|
50714
|
+
"",
|
|
50715
|
+
"Observability cleanup:",
|
|
50716
|
+
" - --observability prunes terminal SQLite rows via the DB prune path",
|
|
50717
|
+
" - requires --before <iso|duration>; examples: 30d, 2026-01-01T00:00:00Z",
|
|
50718
|
+
" - preserves active/waiting/running jobs and memories_* tables",
|
|
49475
50719
|
"",
|
|
49476
50720
|
"Options:",
|
|
49477
|
-
" --all
|
|
49478
|
-
" --keep <n>
|
|
49479
|
-
" --dry-run
|
|
50721
|
+
" --all Remove all done/error job directories regardless of age",
|
|
50722
|
+
" --keep <n> Keep only the N most recent done/error job directories",
|
|
50723
|
+
" --dry-run Preview filesystem, dashboard, or process cleanup",
|
|
50724
|
+
" --ps Hide terminal rows from default ps without deleting DB history",
|
|
50725
|
+
" --observability Prune terminal observability.db rows (requires --before)",
|
|
50726
|
+
" --before <value> Cutoff for --observability (ISO date or duration like 30d)",
|
|
50727
|
+
" --include-epics Also prune eligible terminal epic_runs with --observability",
|
|
50728
|
+
" --reap-orphans Reap orphan/stale leaked tool processes; detects",
|
|
50729
|
+
" dead-pid (PID gone), orphaned-keep-alive (PID alive,",
|
|
50730
|
+
" ppid=1, status=waiting), dead-toolchain (PID alive but",
|
|
50731
|
+
" no tool/think events in last 30min); all require 30min",
|
|
50732
|
+
" min-age threshold",
|
|
49480
50733
|
"",
|
|
49481
50734
|
"Examples:",
|
|
49482
50735
|
" specialists clean",
|
|
49483
50736
|
" specialists clean --all",
|
|
49484
50737
|
" specialists clean --keep 20",
|
|
49485
50738
|
" specialists clean --dry-run",
|
|
50739
|
+
" specialists clean --ps --dry-run",
|
|
50740
|
+
" specialists clean --ps",
|
|
50741
|
+
" specialists clean --observability --before 30d --dry-run",
|
|
50742
|
+
" specialists clean --observability --before 30d",
|
|
50743
|
+
" specialists clean --reap-orphans --dry-run",
|
|
49486
50744
|
""
|
|
49487
50745
|
].join(`
|
|
49488
50746
|
`));
|
|
@@ -49495,7 +50753,7 @@ async function run33() {
|
|
|
49495
50753
|
if (wantsHelp()) {
|
|
49496
50754
|
console.log([
|
|
49497
50755
|
"",
|
|
49498
|
-
"Usage: specialists merge <target-bead-id> [--rebuild]",
|
|
50756
|
+
"Usage: specialists merge <target-bead-id> [--target-branch <name>] [--rebuild]",
|
|
49499
50757
|
"",
|
|
49500
50758
|
"Publish a chain root bead branch. Epic publication belongs to `sp epic merge`.",
|
|
49501
50759
|
"",
|
|
@@ -49504,6 +50762,9 @@ async function run33() {
|
|
|
49504
50762
|
" - unresolved epic member: refuses and points to `sp epic merge <epic-id>`",
|
|
49505
50763
|
" - runs `bunx tsc --noEmit` after each merge and stops on failure",
|
|
49506
50764
|
" - stops on first merge conflict and reports conflicting files",
|
|
50765
|
+
" - --target-branch overrides origin/HEAD as rebase target; useful for",
|
|
50766
|
+
" chains forked from non-main branches",
|
|
50767
|
+
" - ignores dirty .beads/issues.jsonl and .xtrm/skills/active/**",
|
|
49507
50768
|
" - NOTE: for epic publication with lifecycle management, use `sp epic merge`",
|
|
49508
50769
|
"",
|
|
49509
50770
|
"Options:",
|
|
@@ -49574,6 +50835,28 @@ async function run33() {
|
|
|
49574
50835
|
const { run: handler } = await Promise.resolve().then(() => (init_stop(), exports_stop));
|
|
49575
50836
|
return handler();
|
|
49576
50837
|
}
|
|
50838
|
+
if (sub === "finalize") {
|
|
50839
|
+
if (wantsHelp()) {
|
|
50840
|
+
console.log([
|
|
50841
|
+
"",
|
|
50842
|
+
"Usage: specialists finalize <job-id>",
|
|
50843
|
+
"",
|
|
50844
|
+
"Finalize waiting keep-alive job after reviewer PASS.",
|
|
50845
|
+
"Reads SQLite-first compliance verdict from observability.db specialist_results.",
|
|
50846
|
+
"Falls back to result.txt when SPECIALISTS_JOB_FILE_OUTPUT=on.",
|
|
50847
|
+
"Accepts any chain member job-id and finalizes full keep-alive chain.",
|
|
50848
|
+
"Refuses non-waiting or non-PASS jobs.",
|
|
50849
|
+
"",
|
|
50850
|
+
"Examples:",
|
|
50851
|
+
" specialists finalize job_a1b2c3d4",
|
|
50852
|
+
""
|
|
50853
|
+
].join(`
|
|
50854
|
+
`));
|
|
50855
|
+
return;
|
|
50856
|
+
}
|
|
50857
|
+
const { run: handler } = await Promise.resolve().then(() => (init_finalize(), exports_finalize));
|
|
50858
|
+
return handler();
|
|
50859
|
+
}
|
|
49577
50860
|
if (sub === "attach") {
|
|
49578
50861
|
if (wantsHelp()) {
|
|
49579
50862
|
process.stdout.write([
|
|
@@ -49635,7 +50918,10 @@ async function run33() {
|
|
|
49635
50918
|
"",
|
|
49636
50919
|
"Subcommands:",
|
|
49637
50920
|
" orphans Read-only orphan scan: membership/jobs/epics/worktree pointers",
|
|
49638
|
-
" --check-drift, --drift
|
|
50921
|
+
" --check-drift, --drift Compare .specialists/default/ snapshots against package canonical",
|
|
50922
|
+
" Category A (specialists runtime) and Category B",
|
|
50923
|
+
" (filesystem skills/hooks) are distinct; doctor",
|
|
50924
|
+
" covers Category A only",
|
|
49639
50925
|
"",
|
|
49640
50926
|
"Examples:",
|
|
49641
50927
|
" specialists doctor",
|
|
@@ -49667,7 +50953,7 @@ async function run33() {
|
|
|
49667
50953
|
if (wantsHelp()) {
|
|
49668
50954
|
console.log([
|
|
49669
50955
|
"",
|
|
49670
|
-
"Usage: specialists serve [--port <n>] [--concurrency <n>] [--shutdown-grace-ms <n>] [--project-dir <path>]
|
|
50956
|
+
"Usage: specialists serve [--port <n>] [--concurrency <n>] [--shutdown-grace-ms <n>] [--project-dir <path>]",
|
|
49671
50957
|
"",
|
|
49672
50958
|
"HTTP wrapper for script-class specialists.",
|
|
49673
50959
|
"",
|
|
@@ -49723,7 +51009,7 @@ Run 'specialists help' to see available commands.`);
|
|
|
49723
51009
|
const server = new SpecialistsServer;
|
|
49724
51010
|
await server.start();
|
|
49725
51011
|
}
|
|
49726
|
-
|
|
51012
|
+
run34().catch((error2) => {
|
|
49727
51013
|
logger.error(`Fatal error: ${error2}`);
|
|
49728
51014
|
process.exit(1);
|
|
49729
51015
|
});
|