@jaggerxtrm/specialists 3.10.0 → 3.12.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/README.md +3 -0
- package/config/hooks/specialists-session-start.mjs +33 -1
- package/config/mandatory-rules/changelog-conventions.md +21 -0
- package/config/mandatory-rules/changelog-keeper-scope.md +50 -0
- package/config/mandatory-rules/gitnexus-required.md +6 -1
- package/config/mandatory-rules/sync-docs-scope-discipline.md +40 -0
- package/config/skills/releasing/SKILL.md +82 -0
- package/config/skills/specialists-creator/SKILL.md +84 -10
- package/config/skills/specialists-creator/scripts/validate-specialist.ts +1 -1
- package/config/skills/update-specialists/SKILL.md +41 -7
- package/config/skills/using-kpi/SKILL.md +150 -0
- package/config/skills/using-script-specialists/SKILL.md +208 -0
- package/config/skills/using-specialists-v2/SKILL.md +162 -28
- package/config/skills/using-specialists-v3/SKILL.md +284 -0
- package/config/skills/using-specialists-v3/evals/evals.json +89 -0
- package/config/specialists/changelog-drafter.specialist.json +62 -0
- package/config/specialists/changelog-keeper.specialist.json +79 -0
- package/config/specialists/code-sanity.specialist.json +106 -0
- package/config/specialists/debugger.specialist.json +4 -4
- package/config/specialists/executor.specialist.json +4 -4
- package/config/specialists/explorer.specialist.json +14 -4
- package/config/specialists/memory-processor.specialist.json +4 -4
- package/config/specialists/node-coordinator.specialist.json +3 -3
- package/config/specialists/overthinker.specialist.json +3 -3
- package/config/specialists/planner.specialist.json +4 -4
- package/config/specialists/researcher.specialist.json +3 -3
- package/config/specialists/reviewer.specialist.json +4 -4
- package/config/specialists/security-auditor.specialist.json +68 -0
- package/config/specialists/specialists-creator.specialist.json +6 -5
- package/config/specialists/sync-docs.specialist.json +15 -18
- package/config/specialists/test-runner.specialist.json +3 -3
- package/config/specialists/xt-merge.specialist.json +4 -4
- package/dist/index.js +3323 -1004
- package/dist/lib.js +480 -135
- package/dist/types/cli/clean.d.ts.map +1 -1
- package/dist/types/cli/config.d.ts.map +1 -1
- package/dist/types/cli/db.d.ts.map +1 -1
- package/dist/types/cli/doctor.d.ts.map +1 -1
- package/dist/types/cli/feed.d.ts.map +1 -1
- package/dist/types/cli/help.d.ts.map +1 -1
- package/dist/types/cli/init.d.ts.map +1 -1
- package/dist/types/cli/list.d.ts +4 -0
- package/dist/types/cli/list.d.ts.map +1 -1
- package/dist/types/cli/merge.d.ts +4 -2
- package/dist/types/cli/merge.d.ts.map +1 -1
- package/dist/types/cli/node.d.ts.map +1 -1
- package/dist/types/cli/prune-stale-defaults.d.ts +2 -0
- package/dist/types/cli/prune-stale-defaults.d.ts.map +1 -0
- package/dist/types/cli/ps.d.ts.map +1 -1
- package/dist/types/cli/result.d.ts.map +1 -1
- package/dist/types/cli/run.d.ts.map +1 -1
- package/dist/types/cli/script.d.ts.map +1 -1
- package/dist/types/cli/serve-hot-reload.d.ts +13 -0
- package/dist/types/cli/serve-hot-reload.d.ts.map +1 -0
- package/dist/types/cli/serve.d.ts +28 -0
- package/dist/types/cli/serve.d.ts.map +1 -1
- package/dist/types/cli/status.d.ts.map +1 -1
- package/dist/types/cli/stop.d.ts.map +1 -1
- package/dist/types/cli/version-check.d.ts +17 -0
- package/dist/types/cli/version-check.d.ts.map +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/pi/session.d.ts +10 -0
- package/dist/types/pi/session.d.ts.map +1 -1
- package/dist/types/specialist/canonical-asset-resolver.d.ts +6 -0
- package/dist/types/specialist/canonical-asset-resolver.d.ts.map +1 -0
- package/dist/types/specialist/drift-detector.d.ts +39 -0
- package/dist/types/specialist/drift-detector.d.ts.map +1 -0
- package/dist/types/specialist/epic-lifecycle.d.ts.map +1 -1
- package/dist/types/specialist/epic-readiness.d.ts.map +1 -1
- package/dist/types/specialist/epic-reconciler.d.ts.map +1 -1
- package/dist/types/specialist/loader.d.ts +2 -1
- package/dist/types/specialist/loader.d.ts.map +1 -1
- package/dist/types/specialist/mandatory-rules.d.ts.map +1 -1
- package/dist/types/specialist/manifest-resolver.d.ts +55 -0
- package/dist/types/specialist/manifest-resolver.d.ts.map +1 -0
- package/dist/types/specialist/node-contract.d.ts +2 -2
- package/dist/types/specialist/observability-sqlite.d.ts +43 -0
- package/dist/types/specialist/observability-sqlite.d.ts.map +1 -1
- package/dist/types/specialist/payload-measure.d.ts +19 -0
- package/dist/types/specialist/payload-measure.d.ts.map +1 -0
- package/dist/types/specialist/porcelain-parser.d.ts +2 -0
- package/dist/types/specialist/porcelain-parser.d.ts.map +1 -0
- package/dist/types/specialist/resolution-diagnostics.d.ts +36 -0
- package/dist/types/specialist/resolution-diagnostics.d.ts.map +1 -0
- package/dist/types/specialist/runner.d.ts +8 -0
- package/dist/types/specialist/runner.d.ts.map +1 -1
- package/dist/types/specialist/schema.d.ts +27 -0
- package/dist/types/specialist/schema.d.ts.map +1 -1
- package/dist/types/specialist/script-runner.d.ts +44 -1
- package/dist/types/specialist/script-runner.d.ts.map +1 -1
- package/dist/types/specialist/supervisor.d.ts +4 -0
- package/dist/types/specialist/supervisor.d.ts.map +1 -1
- package/dist/types/specialist/timeline-events.d.ts +29 -1
- package/dist/types/specialist/timeline-events.d.ts.map +1 -1
- package/dist/types/specialist/timeline-query.d.ts.map +1 -1
- package/dist/types/specialist/tool-catalog.d.ts +126 -0
- package/dist/types/specialist/tool-catalog.d.ts.map +1 -0
- package/dist/types/tools/specialist/feed_specialist.tool.d.ts +2 -2
- package/dist/types/tools/specialist/use_specialist.tool.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/lib.js
CHANGED
|
@@ -6906,7 +6906,8 @@ var require_public_api = __commonJS((exports) => {
|
|
|
6906
6906
|
|
|
6907
6907
|
// src/specialist/script-runner.ts
|
|
6908
6908
|
import { spawn } from "node:child_process";
|
|
6909
|
-
import { randomUUID } from "node:crypto";
|
|
6909
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6910
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
6910
6911
|
|
|
6911
6912
|
// src/specialist/templateEngine.ts
|
|
6912
6913
|
function renderTemplate(template, variables) {
|
|
@@ -7044,7 +7045,8 @@ function migrateToV2(db) {
|
|
|
7044
7045
|
status_json TEXT NOT NULL,
|
|
7045
7046
|
bead_id TEXT,
|
|
7046
7047
|
updated_at_ms INTEGER NOT NULL,
|
|
7047
|
-
last_output TEXT
|
|
7048
|
+
last_output TEXT,
|
|
7049
|
+
startup_payload_json TEXT
|
|
7048
7050
|
);
|
|
7049
7051
|
INSERT OR IGNORE INTO specialist_jobs_v2
|
|
7050
7052
|
SELECT
|
|
@@ -7054,7 +7056,8 @@ function migrateToV2(db) {
|
|
|
7054
7056
|
status_json,
|
|
7055
7057
|
JSON_EXTRACT(status_json, '$.bead_id'),
|
|
7056
7058
|
updated_at_ms,
|
|
7057
|
-
last_output
|
|
7059
|
+
last_output,
|
|
7060
|
+
startup_payload_json
|
|
7058
7061
|
FROM specialist_jobs;
|
|
7059
7062
|
DROP TABLE IF EXISTS specialist_jobs;
|
|
7060
7063
|
ALTER TABLE specialist_jobs_v2 RENAME TO specialist_jobs;
|
|
@@ -7081,7 +7084,8 @@ function migrateToV3(db) {
|
|
|
7081
7084
|
status TEXT NOT NULL,
|
|
7082
7085
|
status_json TEXT NOT NULL,
|
|
7083
7086
|
updated_at_ms INTEGER NOT NULL,
|
|
7084
|
-
last_output TEXT
|
|
7087
|
+
last_output TEXT,
|
|
7088
|
+
startup_payload_json TEXT
|
|
7085
7089
|
);
|
|
7086
7090
|
INSERT OR IGNORE INTO specialist_jobs_v3
|
|
7087
7091
|
SELECT
|
|
@@ -7093,7 +7097,8 @@ function migrateToV3(db) {
|
|
|
7093
7097
|
COALESCE(JSON_EXTRACT(status_json, '$.status'), 'starting'),
|
|
7094
7098
|
status_json,
|
|
7095
7099
|
updated_at_ms,
|
|
7096
|
-
last_output
|
|
7100
|
+
last_output,
|
|
7101
|
+
startup_payload_json
|
|
7097
7102
|
FROM specialist_jobs;
|
|
7098
7103
|
DROP TABLE IF EXISTS specialist_jobs;
|
|
7099
7104
|
ALTER TABLE specialist_jobs_v3 RENAME TO specialist_jobs;
|
|
@@ -7108,6 +7113,15 @@ function migrateToV3(db) {
|
|
|
7108
7113
|
function migrateToV11(db) {
|
|
7109
7114
|
const hasV11 = db.query("SELECT 1 FROM schema_version WHERE version = 11 LIMIT 1").get();
|
|
7110
7115
|
if (hasV11) {
|
|
7116
|
+
const metricsColumns = new Set(db.query("PRAGMA table_info(specialist_job_metrics)").all().map((column) => column.name).filter((name) => typeof name === "string" && name.length > 0));
|
|
7117
|
+
for (const column of [
|
|
7118
|
+
{ name: "active_runtime_ms", definition: "INTEGER" },
|
|
7119
|
+
{ name: "waiting_ms", definition: "INTEGER" }
|
|
7120
|
+
]) {
|
|
7121
|
+
if (!metricsColumns.has(column.name)) {
|
|
7122
|
+
db.run(`ALTER TABLE specialist_job_metrics ADD COLUMN ${column.name} ${column.definition}`);
|
|
7123
|
+
}
|
|
7124
|
+
}
|
|
7111
7125
|
db.run("CREATE INDEX IF NOT EXISTS idx_job_metrics_spec_model_updated ON specialist_job_metrics(specialist, model, updated_at_ms DESC)");
|
|
7112
7126
|
db.run("CREATE INDEX IF NOT EXISTS idx_job_metrics_updated ON specialist_job_metrics(updated_at_ms DESC)");
|
|
7113
7127
|
return;
|
|
@@ -7126,6 +7140,8 @@ function migrateToV11(db) {
|
|
|
7126
7140
|
started_at_ms INTEGER,
|
|
7127
7141
|
completed_at_ms INTEGER,
|
|
7128
7142
|
elapsed_ms INTEGER,
|
|
7143
|
+
active_runtime_ms INTEGER,
|
|
7144
|
+
waiting_ms INTEGER,
|
|
7129
7145
|
total_turns INTEGER NOT NULL DEFAULT 0,
|
|
7130
7146
|
total_tools INTEGER NOT NULL DEFAULT 0,
|
|
7131
7147
|
tool_call_counts_json TEXT NOT NULL,
|
|
@@ -7294,7 +7310,8 @@ function initSchema(db) {
|
|
|
7294
7310
|
{ name: "chain_root_bead_id", definition: "TEXT" },
|
|
7295
7311
|
{ name: "epic_id", definition: "TEXT" },
|
|
7296
7312
|
{ name: "status", definition: "TEXT NOT NULL DEFAULT 'starting'" },
|
|
7297
|
-
{ name: "last_output", definition: "TEXT" }
|
|
7313
|
+
{ name: "last_output", definition: "TEXT" },
|
|
7314
|
+
{ name: "startup_payload_json", definition: "TEXT" }
|
|
7298
7315
|
].filter(({ name }) => !specialistJobsColumns.has(name));
|
|
7299
7316
|
for (const missingColumn of missingSpecialistJobsColumns) {
|
|
7300
7317
|
db.run(`ALTER TABLE specialist_jobs ADD COLUMN ${missingColumn.name} ${missingColumn.definition}`);
|
|
@@ -7316,7 +7333,8 @@ function initSchema(db) {
|
|
|
7316
7333
|
status TEXT NOT NULL,
|
|
7317
7334
|
status_json TEXT NOT NULL,
|
|
7318
7335
|
updated_at_ms INTEGER NOT NULL,
|
|
7319
|
-
last_output TEXT
|
|
7336
|
+
last_output TEXT,
|
|
7337
|
+
startup_payload_json TEXT
|
|
7320
7338
|
);
|
|
7321
7339
|
INSERT OR IGNORE INTO specialist_jobs_new
|
|
7322
7340
|
SELECT
|
|
@@ -7333,7 +7351,8 @@ function initSchema(db) {
|
|
|
7333
7351
|
COALESCE(status, JSON_EXTRACT(status_json, '$.status'), 'starting'),
|
|
7334
7352
|
status_json,
|
|
7335
7353
|
updated_at_ms,
|
|
7336
|
-
last_output
|
|
7354
|
+
last_output,
|
|
7355
|
+
startup_payload_json
|
|
7337
7356
|
FROM specialist_jobs;
|
|
7338
7357
|
DROP TABLE IF EXISTS specialist_jobs;
|
|
7339
7358
|
ALTER TABLE specialist_jobs_new RENAME TO specialist_jobs;
|
|
@@ -7456,6 +7475,7 @@ function migrateToV8(db) {
|
|
|
7456
7475
|
}
|
|
7457
7476
|
db.run("CREATE INDEX IF NOT EXISTS idx_jobs_chain ON specialist_jobs(chain_id) WHERE chain_id IS NOT NULL");
|
|
7458
7477
|
db.run("CREATE INDEX IF NOT EXISTS idx_jobs_epic ON specialist_jobs(epic_id) WHERE epic_id IS NOT NULL");
|
|
7478
|
+
db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_jobs_active_bead_specialist ON specialist_jobs(bead_id, specialist) WHERE bead_id IS NOT NULL AND status IN ('starting', 'running')");
|
|
7459
7479
|
db.run(`
|
|
7460
7480
|
CREATE TABLE IF NOT EXISTS epic_runs (
|
|
7461
7481
|
epic_id TEXT PRIMARY KEY,
|
|
@@ -7552,6 +7572,37 @@ function migrateToV10(db) {
|
|
|
7552
7572
|
VALUES (10, strftime('%s', 'now') * 1000);
|
|
7553
7573
|
`);
|
|
7554
7574
|
}
|
|
7575
|
+
var STALE_CLAIM_AGE_MS = 60000;
|
|
7576
|
+
function defaultIsPidAlive(pid) {
|
|
7577
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
7578
|
+
return false;
|
|
7579
|
+
try {
|
|
7580
|
+
process.kill(pid, 0);
|
|
7581
|
+
return true;
|
|
7582
|
+
} catch {
|
|
7583
|
+
return false;
|
|
7584
|
+
}
|
|
7585
|
+
}
|
|
7586
|
+
function claimJobStartWithStore(store, status, event, options = {}) {
|
|
7587
|
+
const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
|
|
7588
|
+
const nowMs = options.nowMs ?? Date.now;
|
|
7589
|
+
const staleAgeMs = options.staleClaimAgeMs ?? STALE_CLAIM_AGE_MS;
|
|
7590
|
+
return withRetry(() => store.transaction(() => {
|
|
7591
|
+
const existing = store.findActiveJob(status.bead_id ?? null, status.specialist);
|
|
7592
|
+
if (existing?.job_id && existing.job_id !== status.id) {
|
|
7593
|
+
const updatedAtMs = existing.updated_at_ms ?? 0;
|
|
7594
|
+
const isStale = updatedAtMs > 0 && nowMs() - updatedAtMs > staleAgeMs && !isPidAlive(existing.pid);
|
|
7595
|
+
if (isStale && store.cancelStaleClaim) {
|
|
7596
|
+
store.cancelStaleClaim(existing.job_id);
|
|
7597
|
+
} else {
|
|
7598
|
+
return { ok: false, existingJobId: existing.job_id, existingStatus: existing.status ?? "starting" };
|
|
7599
|
+
}
|
|
7600
|
+
}
|
|
7601
|
+
store.writeStatusRow(status);
|
|
7602
|
+
store.writeEventRow(status.id, status.specialist, status.bead_id, event);
|
|
7603
|
+
return { ok: true };
|
|
7604
|
+
}), "claimJobStart");
|
|
7605
|
+
}
|
|
7555
7606
|
|
|
7556
7607
|
class SqliteClient {
|
|
7557
7608
|
db;
|
|
@@ -7566,8 +7617,8 @@ class SqliteClient {
|
|
|
7566
7617
|
writeStatusRow(status, lastOutput) {
|
|
7567
7618
|
const statusJson = JSON.stringify(status);
|
|
7568
7619
|
this.db.run(`
|
|
7569
|
-
INSERT INTO specialist_jobs (job_id, specialist, worktree_column, bead_id, node_id, chain_kind, chain_id, chain_root_job_id, chain_root_bead_id, epic_id, status, status_json, updated_at_ms, last_output)
|
|
7570
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7620
|
+
INSERT INTO specialist_jobs (job_id, specialist, worktree_column, bead_id, node_id, chain_kind, chain_id, chain_root_job_id, chain_root_bead_id, epic_id, status, status_json, updated_at_ms, last_output, startup_payload_json)
|
|
7621
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7571
7622
|
ON CONFLICT(job_id) DO UPDATE SET
|
|
7572
7623
|
specialist = excluded.specialist,
|
|
7573
7624
|
worktree_column = excluded.worktree_column,
|
|
@@ -7581,7 +7632,8 @@ class SqliteClient {
|
|
|
7581
7632
|
status = excluded.status,
|
|
7582
7633
|
status_json = excluded.status_json,
|
|
7583
7634
|
updated_at_ms = excluded.updated_at_ms,
|
|
7584
|
-
last_output = COALESCE(excluded.last_output, specialist_jobs.last_output)
|
|
7635
|
+
last_output = COALESCE(excluded.last_output, specialist_jobs.last_output),
|
|
7636
|
+
startup_payload_json = COALESCE(excluded.startup_payload_json, specialist_jobs.startup_payload_json);
|
|
7585
7637
|
`, [
|
|
7586
7638
|
status.id,
|
|
7587
7639
|
status.specialist,
|
|
@@ -7596,7 +7648,8 @@ class SqliteClient {
|
|
|
7596
7648
|
status.status,
|
|
7597
7649
|
statusJson,
|
|
7598
7650
|
Date.now(),
|
|
7599
|
-
lastOutput ?? null
|
|
7651
|
+
lastOutput ?? null,
|
|
7652
|
+
status.startup_payload_json ?? null
|
|
7600
7653
|
]);
|
|
7601
7654
|
}
|
|
7602
7655
|
writeEpicRunRow(epic) {
|
|
@@ -7642,6 +7695,36 @@ class SqliteClient {
|
|
|
7642
7695
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
7643
7696
|
`, [jobId, seq, specialist, beadId ?? null, event.t, event.type, eventJson]);
|
|
7644
7697
|
}
|
|
7698
|
+
claimJobStart(status, event) {
|
|
7699
|
+
return claimJobStartWithStore({
|
|
7700
|
+
transaction: (callback) => this.db.transaction(callback)(),
|
|
7701
|
+
findActiveJob: (beadId, specialist) => this.db.query(`
|
|
7702
|
+
SELECT
|
|
7703
|
+
job_id,
|
|
7704
|
+
status,
|
|
7705
|
+
updated_at_ms,
|
|
7706
|
+
CAST(JSON_EXTRACT(status_json, '$.pid') AS INTEGER) AS pid
|
|
7707
|
+
FROM specialist_jobs
|
|
7708
|
+
WHERE bead_id = ?
|
|
7709
|
+
AND specialist = ?
|
|
7710
|
+
AND status IN ('starting', 'running')
|
|
7711
|
+
ORDER BY updated_at_ms DESC
|
|
7712
|
+
LIMIT 1
|
|
7713
|
+
`).get(beadId, specialist),
|
|
7714
|
+
writeStatusRow: (nextStatus) => this.writeStatusRow(nextStatus),
|
|
7715
|
+
writeEventRow: (jobId, specialist, beadId, nextEvent) => this.writeEventRow(jobId, specialist, beadId, nextEvent),
|
|
7716
|
+
cancelStaleClaim: (jobId) => {
|
|
7717
|
+
const nowMs = Date.now();
|
|
7718
|
+
this.db.run(`
|
|
7719
|
+
UPDATE specialist_jobs
|
|
7720
|
+
SET status = 'cancelled',
|
|
7721
|
+
status_json = JSON_PATCH(status_json, JSON_OBJECT('status', 'cancelled', 'cancelled_reason', 'orphan-claim-stale')),
|
|
7722
|
+
updated_at_ms = ?
|
|
7723
|
+
WHERE job_id = ?
|
|
7724
|
+
`, [nowMs, jobId]);
|
|
7725
|
+
}
|
|
7726
|
+
}, status, event);
|
|
7727
|
+
}
|
|
7645
7728
|
writeResultRow(jobId, output) {
|
|
7646
7729
|
this.db.run(`
|
|
7647
7730
|
INSERT INTO specialist_results (job_id, output, updated_at_ms)
|
|
@@ -8102,6 +8185,15 @@ class SqliteClient {
|
|
|
8102
8185
|
return statuses;
|
|
8103
8186
|
}, "listStatuses");
|
|
8104
8187
|
}
|
|
8188
|
+
removeJobs(jobIds) {
|
|
8189
|
+
return withRetry(() => {
|
|
8190
|
+
if (jobIds.length === 0)
|
|
8191
|
+
return 0;
|
|
8192
|
+
const placeholders = jobIds.map(() => "?").join(", ");
|
|
8193
|
+
const result = this.db.query(`DELETE FROM specialist_jobs WHERE job_id IN (${placeholders})`).run(...jobIds);
|
|
8194
|
+
return result.changes ?? 0;
|
|
8195
|
+
}, "removeJobs");
|
|
8196
|
+
}
|
|
8105
8197
|
readEpicRun(epicId) {
|
|
8106
8198
|
return withRetry(() => {
|
|
8107
8199
|
const row = this.db.query("SELECT epic_id, status, status_json, updated_at_ms FROM epic_runs WHERE epic_id = ? LIMIT 1").get(epicId);
|
|
@@ -8131,7 +8223,6 @@ class SqliteClient {
|
|
|
8131
8223
|
SELECT chain_id, epic_id, chain_root_bead_id, chain_root_job_id, updated_at_ms
|
|
8132
8224
|
FROM epic_chain_membership
|
|
8133
8225
|
WHERE epic_id = ?
|
|
8134
|
-
AND (chain_root_job_id IS NULL OR chain_root_job_id != chain_id)
|
|
8135
8226
|
ORDER BY updated_at_ms DESC
|
|
8136
8227
|
`).all(epicId);
|
|
8137
8228
|
}, "listEpicChains");
|
|
@@ -8149,6 +8240,16 @@ class SqliteClient {
|
|
|
8149
8240
|
return removable;
|
|
8150
8241
|
}, "deleteEpicChainMembership");
|
|
8151
8242
|
}
|
|
8243
|
+
listReferencedChainRootJobIds() {
|
|
8244
|
+
return withRetry(() => {
|
|
8245
|
+
const rows = this.db.query(`
|
|
8246
|
+
SELECT DISTINCT chain_root_job_id
|
|
8247
|
+
FROM epic_chain_membership
|
|
8248
|
+
WHERE chain_root_job_id IS NOT NULL AND chain_root_job_id != ''
|
|
8249
|
+
`).all();
|
|
8250
|
+
return rows.map((row) => row.chain_root_job_id).filter((jobId) => typeof jobId === "string" && jobId.length > 0);
|
|
8251
|
+
}, "listReferencedChainRootJobIds");
|
|
8252
|
+
}
|
|
8152
8253
|
listEpicChainsWithLatestJob(epicId) {
|
|
8153
8254
|
return withRetry(() => {
|
|
8154
8255
|
const rows = this.db.query(`
|
|
@@ -8226,6 +8327,18 @@ class SqliteClient {
|
|
|
8226
8327
|
return rows.map((row) => row.job_id).filter((jobId) => typeof jobId === "string" && jobId.length > 0);
|
|
8227
8328
|
}, "listChainJobIds");
|
|
8228
8329
|
}
|
|
8330
|
+
listLiveJobsForBead(beadId) {
|
|
8331
|
+
return withRetry(() => {
|
|
8332
|
+
const rows = this.db.query(`
|
|
8333
|
+
SELECT job_id
|
|
8334
|
+
FROM specialist_jobs
|
|
8335
|
+
WHERE bead_id = ?
|
|
8336
|
+
AND status IN ('starting', 'running', 'waiting')
|
|
8337
|
+
ORDER BY updated_at_ms ASC
|
|
8338
|
+
`).all(beadId);
|
|
8339
|
+
return rows.map((row) => row.job_id).filter((jobId) => typeof jobId === "string" && jobId.length > 0);
|
|
8340
|
+
}, "listLiveJobsForBead");
|
|
8341
|
+
}
|
|
8229
8342
|
resolveChainEpicLinkByJobId(jobId) {
|
|
8230
8343
|
return withRetry(() => {
|
|
8231
8344
|
const row = this.db.query(`
|
|
@@ -8305,7 +8418,7 @@ class SqliteClient {
|
|
|
8305
8418
|
aggregateJobMetrics(jobId) {
|
|
8306
8419
|
return withRetry(() => {
|
|
8307
8420
|
const jobRow = this.db.query(`
|
|
8308
|
-
SELECT job_id, specialist, status, chain_kind, chain_id, bead_id, node_id, epic_id, updated_at_ms
|
|
8421
|
+
SELECT job_id, specialist, status, chain_kind, chain_id, bead_id, node_id, epic_id, updated_at_ms, startup_payload_json
|
|
8309
8422
|
FROM specialist_jobs
|
|
8310
8423
|
WHERE job_id = ?
|
|
8311
8424
|
`).get(jobId);
|
|
@@ -8323,9 +8436,22 @@ class SqliteClient {
|
|
|
8323
8436
|
let runCompleteJson = null;
|
|
8324
8437
|
let model = null;
|
|
8325
8438
|
let elapsedMs = null;
|
|
8439
|
+
let activeRuntimeMs = 0;
|
|
8440
|
+
let waitingMs = 0;
|
|
8441
|
+
let phase = null;
|
|
8442
|
+
let phaseStartedAtMs = null;
|
|
8443
|
+
const closePhase = (endAtMs) => {
|
|
8444
|
+
if (phase === null || phaseStartedAtMs === null || endAtMs < phaseStartedAtMs)
|
|
8445
|
+
return;
|
|
8446
|
+
const durationMs = endAtMs - phaseStartedAtMs;
|
|
8447
|
+
if (phase === "running") {
|
|
8448
|
+
activeRuntimeMs += durationMs;
|
|
8449
|
+
} else {
|
|
8450
|
+
waitingMs += durationMs;
|
|
8451
|
+
}
|
|
8452
|
+
};
|
|
8326
8453
|
for (const event of events) {
|
|
8327
8454
|
startedAtMs = startedAtMs === null ? event.t : Math.min(startedAtMs, event.t);
|
|
8328
|
-
completedAtMs = completedAtMs === null ? event.t : Math.max(completedAtMs, event.t);
|
|
8329
8455
|
if (event.type === "tool") {
|
|
8330
8456
|
totalTools += 1;
|
|
8331
8457
|
toolCallCounts[event.tool] = (toolCallCounts[event.tool] ?? 0) + 1;
|
|
@@ -8343,16 +8469,45 @@ class SqliteClient {
|
|
|
8343
8469
|
tokenTrajectory.push({ t: event.t, source: event.source, token_usage: event.token_usage });
|
|
8344
8470
|
continue;
|
|
8345
8471
|
}
|
|
8472
|
+
if (event.type === "run_start") {
|
|
8473
|
+
phase = "running";
|
|
8474
|
+
phaseStartedAtMs = event.t;
|
|
8475
|
+
continue;
|
|
8476
|
+
}
|
|
8477
|
+
if (event.type === "status_change") {
|
|
8478
|
+
if (event.status === "running" || event.status === "waiting") {
|
|
8479
|
+
closePhase(event.t);
|
|
8480
|
+
phase = event.status;
|
|
8481
|
+
phaseStartedAtMs = event.t;
|
|
8482
|
+
continue;
|
|
8483
|
+
}
|
|
8484
|
+
if (event.status === "done" || event.status === "error" || event.status === "cancelled") {
|
|
8485
|
+
closePhase(event.t);
|
|
8486
|
+
phase = null;
|
|
8487
|
+
phaseStartedAtMs = null;
|
|
8488
|
+
}
|
|
8489
|
+
continue;
|
|
8490
|
+
}
|
|
8346
8491
|
if (event.type === "run_complete") {
|
|
8492
|
+
closePhase(event.t);
|
|
8493
|
+
completedAtMs = event.t;
|
|
8347
8494
|
runCompleteJson = JSON.stringify(event);
|
|
8348
8495
|
model = event.model ?? model;
|
|
8349
8496
|
elapsedMs = Math.round(event.elapsed_s * 1000);
|
|
8497
|
+
phase = null;
|
|
8498
|
+
phaseStartedAtMs = null;
|
|
8350
8499
|
continue;
|
|
8351
8500
|
}
|
|
8352
8501
|
if (event.type === "stale_warning" && event.reason === "tool_duration") {
|
|
8353
8502
|
stallGaps.push({ t: event.t, tool: event.tool ?? null, silence_ms: event.silence_ms, threshold_ms: event.threshold_ms });
|
|
8354
8503
|
}
|
|
8355
8504
|
}
|
|
8505
|
+
if (startedAtMs !== null && completedAtMs === null) {
|
|
8506
|
+
completedAtMs = events.length > 0 ? events[events.length - 1].t : startedAtMs;
|
|
8507
|
+
}
|
|
8508
|
+
if (elapsedMs === null && startedAtMs !== null && completedAtMs !== null) {
|
|
8509
|
+
elapsedMs = Math.max(0, completedAtMs - startedAtMs);
|
|
8510
|
+
}
|
|
8356
8511
|
const record = {
|
|
8357
8512
|
job_id: jobRow.job_id,
|
|
8358
8513
|
specialist: jobRow.specialist,
|
|
@@ -8366,6 +8521,8 @@ class SqliteClient {
|
|
|
8366
8521
|
started_at_ms: startedAtMs,
|
|
8367
8522
|
completed_at_ms: completedAtMs,
|
|
8368
8523
|
elapsed_ms: elapsedMs,
|
|
8524
|
+
active_runtime_ms: activeRuntimeMs,
|
|
8525
|
+
waiting_ms: waitingMs,
|
|
8369
8526
|
total_turns: totalTurns,
|
|
8370
8527
|
total_tools: totalTools,
|
|
8371
8528
|
tool_call_counts_json: stringifyJson(toolCallCounts),
|
|
@@ -8373,15 +8530,16 @@ class SqliteClient {
|
|
|
8373
8530
|
context_trajectory_json: stringifyJson(contextTrajectory),
|
|
8374
8531
|
stall_gaps_json: stringifyJson(stallGaps),
|
|
8375
8532
|
run_complete_json: runCompleteJson,
|
|
8533
|
+
startup_payload_json: jobRow.startup_payload_json ?? null,
|
|
8376
8534
|
updated_at_ms: jobRow.updated_at_ms
|
|
8377
8535
|
};
|
|
8378
8536
|
this.db.run(`
|
|
8379
8537
|
INSERT INTO specialist_job_metrics (
|
|
8380
8538
|
job_id, specialist, model, status, chain_kind, chain_id, bead_id, node_id, epic_id,
|
|
8381
|
-
started_at_ms, completed_at_ms, elapsed_ms, total_turns, total_tools,
|
|
8539
|
+
started_at_ms, completed_at_ms, elapsed_ms, active_runtime_ms, waiting_ms, total_turns, total_tools,
|
|
8382
8540
|
tool_call_counts_json, token_trajectory_json, context_trajectory_json, stall_gaps_json,
|
|
8383
8541
|
run_complete_json, updated_at_ms
|
|
8384
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8542
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8385
8543
|
ON CONFLICT(job_id) DO UPDATE SET
|
|
8386
8544
|
specialist = excluded.specialist,
|
|
8387
8545
|
model = excluded.model,
|
|
@@ -8394,6 +8552,8 @@ class SqliteClient {
|
|
|
8394
8552
|
started_at_ms = excluded.started_at_ms,
|
|
8395
8553
|
completed_at_ms = excluded.completed_at_ms,
|
|
8396
8554
|
elapsed_ms = excluded.elapsed_ms,
|
|
8555
|
+
active_runtime_ms = excluded.active_runtime_ms,
|
|
8556
|
+
waiting_ms = excluded.waiting_ms,
|
|
8397
8557
|
total_turns = excluded.total_turns,
|
|
8398
8558
|
total_tools = excluded.total_tools,
|
|
8399
8559
|
tool_call_counts_json = excluded.tool_call_counts_json,
|
|
@@ -8415,6 +8575,8 @@ class SqliteClient {
|
|
|
8415
8575
|
record.started_at_ms,
|
|
8416
8576
|
record.completed_at_ms,
|
|
8417
8577
|
record.elapsed_ms,
|
|
8578
|
+
record.active_runtime_ms,
|
|
8579
|
+
record.waiting_ms,
|
|
8418
8580
|
record.total_turns,
|
|
8419
8581
|
record.total_tools,
|
|
8420
8582
|
record.tool_call_counts_json,
|
|
@@ -8447,6 +8609,29 @@ class SqliteClient {
|
|
|
8447
8609
|
return this.db.query(`SELECT * FROM specialist_job_metrics ${where} ORDER BY updated_at_ms DESC, job_id DESC`).all(...params);
|
|
8448
8610
|
}, "listJobMetrics");
|
|
8449
8611
|
}
|
|
8612
|
+
listElapsedMsBySpecialist(sinceMs, limitPerSpecialist = 200) {
|
|
8613
|
+
return withRetry(() => {
|
|
8614
|
+
const rows = this.db.query(`
|
|
8615
|
+
WITH ranked AS (
|
|
8616
|
+
SELECT specialist, elapsed_ms,
|
|
8617
|
+
ROW_NUMBER() OVER (PARTITION BY specialist ORDER BY updated_at_ms DESC) AS rn
|
|
8618
|
+
FROM specialist_job_metrics
|
|
8619
|
+
WHERE status = 'completed' AND updated_at_ms >= ? AND elapsed_ms IS NOT NULL
|
|
8620
|
+
)
|
|
8621
|
+
SELECT specialist, elapsed_ms
|
|
8622
|
+
FROM ranked
|
|
8623
|
+
WHERE rn <= ?
|
|
8624
|
+
ORDER BY specialist, rn
|
|
8625
|
+
`).all(sinceMs, limitPerSpecialist);
|
|
8626
|
+
const bySpecialist = {};
|
|
8627
|
+
for (const row of rows) {
|
|
8628
|
+
if (!row.specialist || typeof row.elapsed_ms !== "number" || !Number.isFinite(row.elapsed_ms))
|
|
8629
|
+
continue;
|
|
8630
|
+
(bySpecialist[row.specialist] ??= []).push(row.elapsed_ms);
|
|
8631
|
+
}
|
|
8632
|
+
return bySpecialist;
|
|
8633
|
+
}, "listElapsedMsBySpecialist");
|
|
8634
|
+
}
|
|
8450
8635
|
readResult(jobId) {
|
|
8451
8636
|
return withRetry(() => {
|
|
8452
8637
|
const row = this.db.query("SELECT output FROM specialist_results WHERE job_id = ? LIMIT 1").get(jobId);
|
|
@@ -8847,27 +9032,72 @@ function createObservabilitySqliteClient(cwd = process.cwd()) {
|
|
|
8847
9032
|
}
|
|
8848
9033
|
|
|
8849
9034
|
// src/specialist/script-runner.ts
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
9035
|
+
class CompatGuardError extends Error {
|
|
9036
|
+
field;
|
|
9037
|
+
constructor(field, message) {
|
|
9038
|
+
super(message);
|
|
9039
|
+
this.field = field;
|
|
9040
|
+
this.name = "CompatGuardError";
|
|
9041
|
+
}
|
|
9042
|
+
}
|
|
9043
|
+
function hasUnsubstitutedVariables(template, variables) {
|
|
9044
|
+
const matches = template.match(/\$([a-zA-Z_][a-zA-Z0-9_]*)/g) ?? [];
|
|
9045
|
+
for (const match of matches) {
|
|
9046
|
+
const key = match.slice(1);
|
|
9047
|
+
if (variables[key] === undefined)
|
|
9048
|
+
return key;
|
|
9049
|
+
}
|
|
9050
|
+
return null;
|
|
8853
9051
|
}
|
|
8854
|
-
function compatGuard(spec) {
|
|
9052
|
+
function compatGuard(spec, trust) {
|
|
8855
9053
|
const execution = spec.specialist.execution;
|
|
8856
9054
|
if (execution.interactive)
|
|
8857
|
-
throw new
|
|
9055
|
+
throw new CompatGuardError("execution.interactive", "interactive specialists are not allowed");
|
|
8858
9056
|
if (execution.requires_worktree)
|
|
8859
|
-
throw new
|
|
9057
|
+
throw new CompatGuardError("execution.requires_worktree", "worktree specialists are not allowed");
|
|
8860
9058
|
if (execution.permission_required !== "READ_ONLY")
|
|
8861
|
-
throw new
|
|
8862
|
-
|
|
8863
|
-
|
|
9059
|
+
throw new CompatGuardError("execution.permission_required", "permission_required must be READ_ONLY");
|
|
9060
|
+
const hasScripts = (spec.specialist.skills?.scripts?.length ?? 0) > 0;
|
|
9061
|
+
if (hasScripts && !trust?.allowLocalScripts) {
|
|
9062
|
+
throw new CompatGuardError("skills.scripts", "scripts not allowed (enable with --allow-local-scripts)");
|
|
9063
|
+
}
|
|
9064
|
+
const hasPaths = (spec.specialist.skills?.paths?.length ?? 0) > 0;
|
|
9065
|
+
const hasSkillInherit = Boolean(spec.specialist.prompt.skill_inherit);
|
|
9066
|
+
if (hasPaths && !trust?.allowSkills) {
|
|
9067
|
+
throw new CompatGuardError("skills.paths", "skills not allowed (enable with --allow-skills)");
|
|
9068
|
+
}
|
|
9069
|
+
if (hasSkillInherit && !trust?.allowSkills) {
|
|
9070
|
+
throw new CompatGuardError("prompt.skill_inherit", "skills not allowed (enable with --allow-skills)");
|
|
9071
|
+
}
|
|
9072
|
+
if (hasPaths && trust?.allowSkills && trust.allowSkillsRoots && trust.allowSkillsRoots.length > 0) {
|
|
9073
|
+
const paths = spec.specialist.skills?.paths ?? [];
|
|
9074
|
+
for (const path of paths) {
|
|
9075
|
+
const allowed = trust.allowSkillsRoots.some((root) => path.startsWith(root));
|
|
9076
|
+
if (!allowed) {
|
|
9077
|
+
throw new CompatGuardError("skills.paths", `skill path '${path}' not under any --allow-skills-roots entry`);
|
|
9078
|
+
}
|
|
9079
|
+
}
|
|
9080
|
+
}
|
|
9081
|
+
}
|
|
9082
|
+
function computeSkillSources(spec) {
|
|
9083
|
+
const paths = spec.specialist.skills?.paths ?? [];
|
|
9084
|
+
const sources = [];
|
|
9085
|
+
for (const path of paths) {
|
|
9086
|
+
try {
|
|
9087
|
+
const content = readFileSync2(path);
|
|
9088
|
+
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
9089
|
+
sources.push({ path, sha256 });
|
|
9090
|
+
} catch {
|
|
9091
|
+
sources.push({ path, sha256: "unreadable" });
|
|
9092
|
+
}
|
|
9093
|
+
}
|
|
9094
|
+
return sources;
|
|
8864
9095
|
}
|
|
8865
9096
|
function renderTaskTemplate(template, variables) {
|
|
8866
|
-
const
|
|
8867
|
-
const missing = hasUnsubstitutedVariables(output);
|
|
9097
|
+
const missing = hasUnsubstitutedVariables(template, variables);
|
|
8868
9098
|
if (missing)
|
|
8869
9099
|
throw new Error(`Missing template variable: ${missing}`);
|
|
8870
|
-
return
|
|
9100
|
+
return renderTemplate(template, variables);
|
|
8871
9101
|
}
|
|
8872
9102
|
function mapErrorType(message) {
|
|
8873
9103
|
if (message.includes("Specialist not found"))
|
|
@@ -8897,59 +9127,32 @@ function textFromMessage(message) {
|
|
|
8897
9127
|
return "";
|
|
8898
9128
|
return message.content.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("");
|
|
8899
9129
|
}
|
|
8900
|
-
function
|
|
8901
|
-
|
|
8902
|
-
const
|
|
8903
|
-
if (
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
continue;
|
|
8910
|
-
}
|
|
8911
|
-
if (event.type === "message_end") {
|
|
8912
|
-
const text = textFromMessage(event.message);
|
|
9130
|
+
function extractAssistantTextFromEvent(event) {
|
|
9131
|
+
if (event.type === "message_end") {
|
|
9132
|
+
const text = textFromMessage(event.message);
|
|
9133
|
+
if (text)
|
|
9134
|
+
return text;
|
|
9135
|
+
}
|
|
9136
|
+
if (event.type === "agent_end" && Array.isArray(event.messages)) {
|
|
9137
|
+
for (let j = event.messages.length - 1;j >= 0; j--) {
|
|
9138
|
+
const text = textFromMessage(event.messages[j]);
|
|
8913
9139
|
if (text)
|
|
8914
9140
|
return text;
|
|
8915
9141
|
}
|
|
8916
|
-
if (event.type === "agent_end" && Array.isArray(event.messages)) {
|
|
8917
|
-
for (let j = event.messages.length - 1;j >= 0; j--) {
|
|
8918
|
-
const text = textFromMessage(event.messages[j]);
|
|
8919
|
-
if (text)
|
|
8920
|
-
return text;
|
|
8921
|
-
}
|
|
8922
|
-
}
|
|
8923
|
-
if (event.type === "assistant" && typeof event.data?.text === "string")
|
|
8924
|
-
return event.data.text;
|
|
8925
|
-
const legacyContent = event.data?.content?.[0]?.text;
|
|
8926
|
-
if (typeof legacyContent === "string")
|
|
8927
|
-
return legacyContent;
|
|
8928
9142
|
}
|
|
8929
|
-
|
|
9143
|
+
if (event.type === "assistant" && typeof event.data?.text === "string")
|
|
9144
|
+
return event.data.text;
|
|
9145
|
+
const legacyContent = event.data?.content?.[0]?.text;
|
|
9146
|
+
if (typeof legacyContent === "string")
|
|
9147
|
+
return legacyContent;
|
|
9148
|
+
return;
|
|
8930
9149
|
}
|
|
8931
9150
|
function stripMarkdownFences(text) {
|
|
8932
9151
|
const trimmed = text.trim();
|
|
8933
9152
|
const fenced = trimmed.match(/^```(?:json|JSON)?\s*\n?([\s\S]*?)\n?```\s*$/);
|
|
8934
9153
|
return fenced ? fenced[1].trim() : trimmed;
|
|
8935
9154
|
}
|
|
8936
|
-
function
|
|
8937
|
-
for (let i = lines.length - 1;i >= 0; i--) {
|
|
8938
|
-
const line = lines[i].trim();
|
|
8939
|
-
if (!line)
|
|
8940
|
-
continue;
|
|
8941
|
-
try {
|
|
8942
|
-
const event = JSON.parse(line);
|
|
8943
|
-
const errMsg = event.message?.errorMessage;
|
|
8944
|
-
if (typeof errMsg === "string" && errMsg.length > 0)
|
|
8945
|
-
return errMsg;
|
|
8946
|
-
} catch {
|
|
8947
|
-
continue;
|
|
8948
|
-
}
|
|
8949
|
-
}
|
|
8950
|
-
return null;
|
|
8951
|
-
}
|
|
8952
|
-
function writeTraceRow(client, specialist, model, traceId, output, durationMs) {
|
|
9155
|
+
function writeTraceRow(client, specialist, model, traceId, output, durationMs, skillSources, onAuditFailure) {
|
|
8953
9156
|
if (!client)
|
|
8954
9157
|
return;
|
|
8955
9158
|
const status = {
|
|
@@ -8960,106 +9163,231 @@ function writeTraceRow(client, specialist, model, traceId, output, durationMs) {
|
|
|
8960
9163
|
started_at_ms: Date.now() - durationMs,
|
|
8961
9164
|
elapsed_s: durationMs / 1000,
|
|
8962
9165
|
last_event_at_ms: Date.now(),
|
|
8963
|
-
surface: "script_specialist"
|
|
9166
|
+
surface: "script_specialist",
|
|
9167
|
+
...skillSources && skillSources.length > 0 ? { skill_sources: skillSources } : {}
|
|
8964
9168
|
};
|
|
8965
|
-
|
|
8966
|
-
|
|
9169
|
+
try {
|
|
9170
|
+
client.upsertStatus(status);
|
|
9171
|
+
client.upsertResult(traceId, output);
|
|
9172
|
+
} catch (error) {
|
|
9173
|
+
onAuditFailure?.(error);
|
|
9174
|
+
}
|
|
9175
|
+
}
|
|
9176
|
+
var DEFAULT_PENDING_LINE_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
9177
|
+
var DEFAULT_ASSISTANT_TEXT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
9178
|
+
var DEFAULT_STDERR_LIMIT_BYTES = 1 * 1024 * 1024;
|
|
9179
|
+
function resolveAssistantTextLimitBytes(spec) {
|
|
9180
|
+
return spec.specialist.execution.stdout_limit_bytes ?? resolveEnvAssistantTextLimitBytes() ?? DEFAULT_ASSISTANT_TEXT_LIMIT_BYTES;
|
|
9181
|
+
}
|
|
9182
|
+
function resolveEnvAssistantTextLimitBytes() {
|
|
9183
|
+
const raw = process.env.SPECIALISTS_SCRIPT_STDOUT_LIMIT_BYTES;
|
|
9184
|
+
if (raw === undefined)
|
|
9185
|
+
return;
|
|
9186
|
+
const envLimit = Number(raw);
|
|
9187
|
+
if (!Number.isFinite(envLimit) || envLimit <= 0)
|
|
9188
|
+
return;
|
|
9189
|
+
process.stderr.write(`warning: SPECIALISTS_SCRIPT_STDOUT_LIMIT_BYTES is deprecated; applies to assistant text cap
|
|
9190
|
+
`);
|
|
9191
|
+
return Math.floor(envLimit);
|
|
8967
9192
|
}
|
|
8968
9193
|
function openObservabilityClient(options) {
|
|
8969
9194
|
const dbPath = options.observabilityDbPath ?? options.projectDir;
|
|
8970
9195
|
return createObservabilitySqliteClient(dbPath);
|
|
8971
9196
|
}
|
|
9197
|
+
function resolveScriptSpecialistName(name) {
|
|
9198
|
+
if (name === "changelog-keeper")
|
|
9199
|
+
return "changelog-drafter";
|
|
9200
|
+
return name;
|
|
9201
|
+
}
|
|
8972
9202
|
async function runScriptSpecialist(input, options) {
|
|
8973
9203
|
const traceId = randomUUID();
|
|
8974
9204
|
const startedAt = Date.now();
|
|
8975
9205
|
try {
|
|
8976
|
-
const
|
|
8977
|
-
|
|
9206
|
+
const resolvedSpecialist = resolveScriptSpecialistName(input.specialist);
|
|
9207
|
+
const spec = await options.loader.get(resolvedSpecialist);
|
|
9208
|
+
compatGuard(spec, options.trust);
|
|
9209
|
+
const skillSources = options.trust?.allowSkills ? computeSkillSources(spec) : undefined;
|
|
8978
9210
|
const template = input.template ?? spec.specialist.prompt.task_template;
|
|
8979
9211
|
const prompt = renderTaskTemplate(template, input.variables ?? {});
|
|
8980
|
-
|
|
9212
|
+
if (process.env.SPECIALISTS_SCRIPT_STUB_OUTPUT) {
|
|
9213
|
+
return {
|
|
9214
|
+
success: true,
|
|
9215
|
+
output: prompt,
|
|
9216
|
+
meta: {
|
|
9217
|
+
specialist: resolvedSpecialist,
|
|
9218
|
+
requested_specialist: input.requested_specialist ?? input.specialist,
|
|
9219
|
+
resolved_specialist: resolvedSpecialist,
|
|
9220
|
+
model: "stub",
|
|
9221
|
+
duration_ms: Date.now() - startedAt,
|
|
9222
|
+
trace_id: traceId
|
|
9223
|
+
}
|
|
9224
|
+
};
|
|
9225
|
+
}
|
|
8981
9226
|
const timeoutMs = input.timeout_ms ?? spec.specialist.execution.timeout_ms ?? 120000;
|
|
9227
|
+
const modelCandidates = collectModelCandidates(input, spec, options);
|
|
9228
|
+
const assistantTextLimitBytes = resolveAssistantTextLimitBytes(spec);
|
|
9229
|
+
const attempts = [];
|
|
9230
|
+
for (const model of modelCandidates) {
|
|
9231
|
+
const attempt = await runSingleAttempt(prompt, model, input.thinking_level ?? spec.specialist.execution.thinking_level, timeoutMs, assistantTextLimitBytes, options);
|
|
9232
|
+
attempts.push(attempt);
|
|
9233
|
+
const parsed = classifyAttempt(attempt);
|
|
9234
|
+
if (parsed.retryable)
|
|
9235
|
+
continue;
|
|
9236
|
+
const durationMs2 = Date.now() - startedAt;
|
|
9237
|
+
const observability2 = openObservabilityClient(options);
|
|
9238
|
+
if (input.trace !== false && observability2)
|
|
9239
|
+
writeTraceRow(observability2, resolvedSpecialist, model, traceId, parsed.text, durationMs2, skillSources, options.onAuditFailure);
|
|
9240
|
+
if (parsed.kind === "success") {
|
|
9241
|
+
let parsed_json;
|
|
9242
|
+
if (spec.specialist.execution.response_format === "json") {
|
|
9243
|
+
try {
|
|
9244
|
+
parsed_json = JSON.parse(stripMarkdownFences(parsed.text));
|
|
9245
|
+
const required = Array.isArray(spec.specialist.prompt.output_schema?.required) ? spec.specialist.prompt.output_schema.required.filter((value) => typeof value === "string") : [];
|
|
9246
|
+
for (const key of required) {
|
|
9247
|
+
if (parsed_json === null || typeof parsed_json !== "object" || !(key in parsed_json))
|
|
9248
|
+
throw new Error(`Missing required output field: ${key}`);
|
|
9249
|
+
}
|
|
9250
|
+
} catch (error) {
|
|
9251
|
+
return { success: false, error: error instanceof Error ? error.message : String(error), error_type: "invalid_json", meta: { specialist: resolvedSpecialist, requested_specialist: input.requested_specialist ?? input.specialist, resolved_specialist: resolvedSpecialist, model, duration_ms: durationMs2, trace_id: traceId } };
|
|
9252
|
+
}
|
|
9253
|
+
}
|
|
9254
|
+
return { success: true, output: parsed.text, parsed_json, meta: { specialist: resolvedSpecialist, requested_specialist: input.requested_specialist ?? input.specialist, resolved_specialist: resolvedSpecialist, model, duration_ms: durationMs2, trace_id: traceId } };
|
|
9255
|
+
}
|
|
9256
|
+
return { success: false, error: parsed.error, error_type: parsed.errorType, meta: { specialist: resolvedSpecialist, requested_specialist: input.requested_specialist ?? input.specialist, resolved_specialist: resolvedSpecialist, model, duration_ms: durationMs2, trace_id: traceId } };
|
|
9257
|
+
}
|
|
9258
|
+
const lastAttempt = attempts.at(-1);
|
|
9259
|
+
const durationMs = Date.now() - startedAt;
|
|
9260
|
+
const observability = openObservabilityClient(options);
|
|
9261
|
+
if (input.trace !== false && observability)
|
|
9262
|
+
writeTraceRow(observability, resolvedSpecialist, modelCandidates.at(-1) ?? "unknown", traceId, lastAttempt?.text ?? "", durationMs, skillSources, options.onAuditFailure);
|
|
9263
|
+
return { success: false, error: lastAttempt?.stderr || "pi produced no assistant text", error_type: "internal", meta: { specialist: resolvedSpecialist, requested_specialist: input.requested_specialist ?? input.specialist, resolved_specialist: resolvedSpecialist, model: modelCandidates.at(-1) ?? "unknown", duration_ms: durationMs, trace_id: traceId } };
|
|
9264
|
+
} catch (error) {
|
|
9265
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9266
|
+
const resolvedSpecialist = resolveScriptSpecialistName(input.specialist);
|
|
9267
|
+
return { success: false, error: message, error_type: mapErrorType(message), meta: { specialist: resolvedSpecialist, requested_specialist: input.requested_specialist ?? input.specialist, resolved_specialist: resolvedSpecialist, duration_ms: Date.now() - startedAt, trace_id: traceId } };
|
|
9268
|
+
}
|
|
9269
|
+
}
|
|
9270
|
+
function collectModelCandidates(input, spec, options) {
|
|
9271
|
+
const candidates = [input.model_override, spec.specialist.execution.model, spec.specialist.execution.fallback_model, options.fallbackModel].filter((value) => typeof value === "string" && value.length > 0);
|
|
9272
|
+
return [...new Set(candidates)];
|
|
9273
|
+
}
|
|
9274
|
+
function runSingleAttempt(prompt, model, thinkingLevel, timeoutMs, assistantTextLimitBytes, options) {
|
|
9275
|
+
return new Promise((resolve, reject) => {
|
|
8982
9276
|
const args = ["--mode", "json", "--no-session", "--no-extensions", "--no-tools", "--model", model];
|
|
8983
|
-
const thinkingLevel = input.thinking_level ?? spec.specialist.execution.thinking_level;
|
|
8984
9277
|
if (thinkingLevel)
|
|
8985
9278
|
args.push("--thinking", thinkingLevel);
|
|
8986
9279
|
args.push(prompt);
|
|
8987
9280
|
const pi = spawn("pi", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
8988
9281
|
options.onChild?.(pi);
|
|
8989
|
-
const chunks = [];
|
|
8990
9282
|
let stderr = "";
|
|
8991
9283
|
let timedOut = false;
|
|
8992
9284
|
let outputTooLarge = false;
|
|
8993
|
-
|
|
8994
|
-
let
|
|
9285
|
+
let outputTooLargeReason;
|
|
9286
|
+
let pending = "";
|
|
9287
|
+
let assistantText = "";
|
|
9288
|
+
let pendingBytes = 0;
|
|
9289
|
+
let stderrBytes = 0;
|
|
8995
9290
|
const timer = setTimeout(() => {
|
|
8996
9291
|
timedOut = true;
|
|
8997
9292
|
pi.kill("SIGTERM");
|
|
8998
9293
|
setTimeout(() => pi.kill("SIGKILL"), 2000);
|
|
8999
9294
|
}, timeoutMs);
|
|
9000
9295
|
pi.stdout.on("data", (chunk) => {
|
|
9296
|
+
if (outputTooLarge)
|
|
9297
|
+
return;
|
|
9001
9298
|
const buffer = Buffer.from(chunk);
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
if (
|
|
9299
|
+
pending += buffer.toString("utf-8");
|
|
9300
|
+
pendingBytes += buffer.length;
|
|
9301
|
+
if (pendingBytes > DEFAULT_PENDING_LINE_LIMIT_BYTES) {
|
|
9005
9302
|
outputTooLarge = true;
|
|
9303
|
+
outputTooLargeReason = "malformed_line_too_large";
|
|
9006
9304
|
pi.kill("SIGTERM");
|
|
9007
9305
|
setTimeout(() => pi.kill("SIGKILL"), 2000);
|
|
9306
|
+
return;
|
|
9008
9307
|
}
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
}
|
|
9029
|
-
if (exitCode !== 0) {
|
|
9030
|
-
return { success: false, error: stderr || `pi exit ${exitCode}`, error_type: mapErrorType(stderr), meta: { specialist: input.specialist, model, duration_ms: durationMs, trace_id: traceId } };
|
|
9031
|
-
}
|
|
9032
|
-
if (!text) {
|
|
9033
|
-
const piError = extractPiErrorMessage(stdout.split(/\r?\n/));
|
|
9034
|
-
if (piError) {
|
|
9035
|
-
return { success: false, error: piError, error_type: mapErrorType(piError), meta: { specialist: input.specialist, model, duration_ms: durationMs, trace_id: traceId } };
|
|
9036
|
-
}
|
|
9037
|
-
return { success: false, error: "pi produced no assistant text", error_type: "internal", meta: { specialist: input.specialist, model, duration_ms: durationMs, trace_id: traceId } };
|
|
9038
|
-
}
|
|
9039
|
-
let parsed_json;
|
|
9040
|
-
if (spec.specialist.execution.response_format === "json") {
|
|
9041
|
-
try {
|
|
9042
|
-
parsed_json = JSON.parse(stripMarkdownFences(text));
|
|
9043
|
-
const required = Array.isArray(spec.specialist.prompt.output_schema?.required) ? spec.specialist.prompt.output_schema.required.filter((value) => typeof value === "string") : [];
|
|
9044
|
-
for (const key of required) {
|
|
9045
|
-
if (parsed_json === null || typeof parsed_json !== "object" || !(key in parsed_json)) {
|
|
9046
|
-
throw new Error(`Missing required output field: ${key}`);
|
|
9308
|
+
const lines = pending.split(/\r?\n/);
|
|
9309
|
+
pending = lines.pop() ?? "";
|
|
9310
|
+
pendingBytes = Buffer.byteLength(pending);
|
|
9311
|
+
for (const rawLine of lines) {
|
|
9312
|
+
const line = rawLine.trim();
|
|
9313
|
+
if (!line)
|
|
9314
|
+
continue;
|
|
9315
|
+
try {
|
|
9316
|
+
const event = JSON.parse(line);
|
|
9317
|
+
const nextAssistantText = extractAssistantTextFromEvent(event);
|
|
9318
|
+
if (nextAssistantText !== undefined) {
|
|
9319
|
+
if (Buffer.byteLength(nextAssistantText, "utf8") > assistantTextLimitBytes) {
|
|
9320
|
+
outputTooLarge = true;
|
|
9321
|
+
outputTooLargeReason = "assistant_text_too_large";
|
|
9322
|
+
pi.kill("SIGTERM");
|
|
9323
|
+
setTimeout(() => pi.kill("SIGKILL"), 2000);
|
|
9324
|
+
return;
|
|
9325
|
+
}
|
|
9326
|
+
assistantText = nextAssistantText;
|
|
9047
9327
|
}
|
|
9328
|
+
} catch {
|
|
9329
|
+
continue;
|
|
9048
9330
|
}
|
|
9049
|
-
} catch (error) {
|
|
9050
|
-
return { success: false, error: error instanceof Error ? error.message : String(error), error_type: "invalid_json", meta: { specialist: input.specialist, model, duration_ms: durationMs, trace_id: traceId } };
|
|
9051
9331
|
}
|
|
9052
|
-
}
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9332
|
+
});
|
|
9333
|
+
pi.stderr.on("data", (chunk) => {
|
|
9334
|
+
if (outputTooLarge)
|
|
9335
|
+
return;
|
|
9336
|
+
const text = String(chunk);
|
|
9337
|
+
stderr += text;
|
|
9338
|
+
stderrBytes += Buffer.byteLength(text, "utf8");
|
|
9339
|
+
if (stderrBytes > DEFAULT_STDERR_LIMIT_BYTES) {
|
|
9340
|
+
outputTooLarge = true;
|
|
9341
|
+
outputTooLargeReason = "stderr_too_large";
|
|
9342
|
+
stderr = stderr.slice(0, DEFAULT_STDERR_LIMIT_BYTES);
|
|
9343
|
+
pi.kill("SIGTERM");
|
|
9344
|
+
setTimeout(() => pi.kill("SIGKILL"), 2000);
|
|
9345
|
+
}
|
|
9346
|
+
});
|
|
9347
|
+
pi.on("error", reject);
|
|
9348
|
+
pi.on("close", (code) => {
|
|
9349
|
+
clearTimeout(timer);
|
|
9350
|
+
resolve({
|
|
9351
|
+
model,
|
|
9352
|
+
text: assistantText,
|
|
9353
|
+
stderr,
|
|
9354
|
+
exitCode: code ?? 0,
|
|
9355
|
+
timedOut,
|
|
9356
|
+
outputTooLarge,
|
|
9357
|
+
outputTooLargeReason
|
|
9358
|
+
});
|
|
9359
|
+
});
|
|
9360
|
+
});
|
|
9361
|
+
}
|
|
9362
|
+
function classifyAttempt(attempt) {
|
|
9363
|
+
if (attempt.outputTooLarge) {
|
|
9364
|
+
if (attempt.outputTooLargeReason === "assistant_text_too_large")
|
|
9365
|
+
return { retryable: false, kind: "failure", error: "assistant message too large", errorType: "output_too_large", text: attempt.text };
|
|
9366
|
+
if (attempt.outputTooLargeReason === "stderr_too_large")
|
|
9367
|
+
return { retryable: false, kind: "failure", error: "stderr too large", errorType: "output_too_large", text: attempt.text };
|
|
9368
|
+
if (attempt.outputTooLargeReason === "malformed_line_too_large")
|
|
9369
|
+
return { retryable: false, kind: "failure", error: "malformed line too large", errorType: "output_too_large", text: attempt.text };
|
|
9370
|
+
return { retryable: false, kind: "failure", error: "output exceeded cap", errorType: "output_too_large", text: attempt.text };
|
|
9371
|
+
}
|
|
9372
|
+
if (attempt.timedOut)
|
|
9373
|
+
return { retryable: false, kind: "failure", error: attempt.stderr || "timed out", errorType: "timeout", text: attempt.text };
|
|
9374
|
+
const retryable = isRetryableModelFailure(attempt.stderr, attempt.text);
|
|
9375
|
+
if (attempt.exitCode !== 0) {
|
|
9376
|
+
const errorType = mapErrorType(attempt.stderr);
|
|
9377
|
+
return { retryable, kind: "failure", error: attempt.stderr || `pi exit ${attempt.exitCode}`, errorType, text: attempt.text };
|
|
9378
|
+
}
|
|
9379
|
+
if (!attempt.text) {
|
|
9380
|
+
return { retryable, kind: "failure", error: attempt.stderr || "pi produced no assistant text", errorType: mapErrorType(attempt.stderr), text: attempt.text };
|
|
9381
|
+
}
|
|
9382
|
+
return { retryable: false, kind: "success", error: "", errorType: "internal", text: attempt.text };
|
|
9383
|
+
}
|
|
9384
|
+
function isRetryableModelFailure(stderr, text) {
|
|
9385
|
+
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();
|
|
9058
9386
|
}
|
|
9059
9387
|
// src/specialist/loader.ts
|
|
9060
9388
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
9061
9389
|
import { basename, join as join2 } from "node:path";
|
|
9062
|
-
import { existsSync as
|
|
9390
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
9063
9391
|
|
|
9064
9392
|
// node_modules/yaml/dist/index.js
|
|
9065
9393
|
var composer = require_composer();
|
|
@@ -12937,6 +13265,7 @@ var ExecutionSchema = objectType({
|
|
|
12937
13265
|
stall_timeout_ms: numberType().optional(),
|
|
12938
13266
|
max_retries: numberType().int().min(0).default(0),
|
|
12939
13267
|
interactive: booleanType().default(false),
|
|
13268
|
+
stdout_limit_bytes: numberType().int().positive().optional(),
|
|
12940
13269
|
response_format: enumType(["text", "json", "markdown"]).default("text"),
|
|
12941
13270
|
output_type: enumType(["codegen", "analysis", "review", "synthesis", "orchestration", "workflow", "research", "custom"]).default("custom"),
|
|
12942
13271
|
permission_required: enumType(["READ_ONLY", "LOW", "MEDIUM", "HIGH"]).default("READ_ONLY"),
|
|
@@ -13087,6 +13416,20 @@ ${result.warnings.map((w) => ` ⚠ ${w}`).join(`
|
|
|
13087
13416
|
return SpecialistSchema.parseAsync(raw);
|
|
13088
13417
|
}
|
|
13089
13418
|
|
|
13419
|
+
// src/specialist/canonical-asset-resolver.ts
|
|
13420
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
13421
|
+
import { fileURLToPath } from "node:url";
|
|
13422
|
+
function resolveCanonicalAssetDir(relativePath) {
|
|
13423
|
+
const configPath = `config/${relativePath}`;
|
|
13424
|
+
let resolved = fileURLToPath(new URL(`../${configPath}`, import.meta.url));
|
|
13425
|
+
if (existsSync2(resolved))
|
|
13426
|
+
return resolved;
|
|
13427
|
+
resolved = fileURLToPath(new URL(`../../${configPath}`, import.meta.url));
|
|
13428
|
+
if (existsSync2(resolved))
|
|
13429
|
+
return resolved;
|
|
13430
|
+
return null;
|
|
13431
|
+
}
|
|
13432
|
+
|
|
13090
13433
|
// src/specialist/loader.ts
|
|
13091
13434
|
class SpecialistLoader {
|
|
13092
13435
|
cache = new Map;
|
|
@@ -13101,11 +13444,12 @@ class SpecialistLoader {
|
|
|
13101
13444
|
{ path: join2(this.projectDir, ".specialists", "default"), scope: "default", source: "default-mirror" },
|
|
13102
13445
|
{ path: join2(this.projectDir, ".specialists", "default", "specialists"), scope: "default", source: "legacy" },
|
|
13103
13446
|
{ path: join2(this.projectDir, "config", "specialists"), scope: "package", source: "package-fallback" },
|
|
13447
|
+
{ path: resolveCanonicalAssetDir("specialists") ?? "", scope: "package", source: "package-live" },
|
|
13104
13448
|
{ path: join2(this.projectDir, "specialists"), scope: "default", source: "legacy" },
|
|
13105
13449
|
{ path: join2(this.projectDir, ".claude", "specialists"), scope: "default", source: "legacy" },
|
|
13106
13450
|
{ path: join2(this.projectDir, ".agent-forge", "specialists"), scope: "default", source: "legacy" }
|
|
13107
13451
|
];
|
|
13108
|
-
return dirs.filter((d) =>
|
|
13452
|
+
return dirs.filter((d) => d.path && existsSync3(d.path));
|
|
13109
13453
|
}
|
|
13110
13454
|
toJson(content, isYaml) {
|
|
13111
13455
|
if (!isYaml)
|
|
@@ -13114,11 +13458,11 @@ class SpecialistLoader {
|
|
|
13114
13458
|
}
|
|
13115
13459
|
resolveSpecialistPath(dirPath, specialistName) {
|
|
13116
13460
|
const jsonPath = join2(dirPath, `${specialistName}.specialist.json`);
|
|
13117
|
-
if (
|
|
13461
|
+
if (existsSync3(jsonPath)) {
|
|
13118
13462
|
return { filePath: jsonPath, deprecatedYaml: false };
|
|
13119
13463
|
}
|
|
13120
13464
|
const yamlPath = join2(dirPath, `${specialistName}.specialist.yaml`);
|
|
13121
|
-
if (
|
|
13465
|
+
if (existsSync3(yamlPath)) {
|
|
13122
13466
|
return { filePath: yamlPath, deprecatedYaml: true };
|
|
13123
13467
|
}
|
|
13124
13468
|
return null;
|
|
@@ -13159,6 +13503,7 @@ class SpecialistLoader {
|
|
|
13159
13503
|
thinking_level: spec.specialist.execution.thinking_level,
|
|
13160
13504
|
skills: spec.specialist.skills?.paths ?? [],
|
|
13161
13505
|
scripts: spec.specialist.skills?.scripts ?? [],
|
|
13506
|
+
mandatoryRuleTemplateSets: spec.specialist.mandatory_rules?.template_sets ?? [],
|
|
13162
13507
|
scope: dir.scope,
|
|
13163
13508
|
source: dir.source,
|
|
13164
13509
|
filePath: resolved.filePath,
|