@jaggerxtrm/specialists 3.10.0 → 3.13.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/bead-id-verbatim.md +14 -0
- 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/per-turn-handoff-schema.md +16 -0
- 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 +100 -10
- package/config/skills/specialists-creator/scripts/validate-specialist.ts +1 -1
- package/config/skills/update-specialists/SKILL.md +192 -325
- package/config/skills/using-kpi/SKILL.md +236 -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 +562 -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 +80 -0
- package/config/specialists/code-sanity.specialist.json +108 -0
- package/config/specialists/debugger.specialist.json +7 -5
- package/config/specialists/executor.specialist.json +7 -5
- package/config/specialists/explorer.specialist.json +16 -5
- package/config/specialists/memory-processor.specialist.json +4 -4
- package/config/specialists/node-coordinator.specialist.json +3 -3
- package/config/specialists/overthinker.specialist.json +5 -4
- package/config/specialists/planner.specialist.json +7 -5
- package/config/specialists/researcher.specialist.json +5 -4
- package/config/specialists/reviewer.specialist.json +7 -5
- package/config/specialists/security-auditor.specialist.json +111 -0
- package/config/specialists/specialists-creator.specialist.json +6 -5
- package/config/specialists/sync-docs.specialist.json +18 -19
- package/config/specialists/test-runner.specialist.json +5 -4
- package/config/specialists/xt-merge.specialist.json +4 -4
- package/dist/index.js +3379 -1168
- package/dist/lib.js +518 -154
- 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 +20 -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 +5 -0
- 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 +4 -4
- package/config/specialists/.serena/project.yml +0 -151
package/dist/lib.js
CHANGED
|
@@ -555,6 +555,8 @@ var require_Alias = __commonJS((exports) => {
|
|
|
555
555
|
});
|
|
556
556
|
}
|
|
557
557
|
resolve(doc, ctx) {
|
|
558
|
+
if (ctx?.maxAliasCount === 0)
|
|
559
|
+
throw new ReferenceError("Alias resolution is disabled");
|
|
558
560
|
let nodes;
|
|
559
561
|
if (ctx?.aliasResolveCache) {
|
|
560
562
|
nodes = ctx.aliasResolveCache;
|
|
@@ -1334,6 +1336,7 @@ var require_stringify = __commonJS((exports) => {
|
|
|
1334
1336
|
nullStr: "null",
|
|
1335
1337
|
simpleKeys: false,
|
|
1336
1338
|
singleQuote: null,
|
|
1339
|
+
trailingComma: false,
|
|
1337
1340
|
trueStr: "true",
|
|
1338
1341
|
verifyAliasOrder: true
|
|
1339
1342
|
}, doc.schema.toStringOptions, options);
|
|
@@ -1603,18 +1606,18 @@ var require_merge = __commonJS((exports) => {
|
|
|
1603
1606
|
};
|
|
1604
1607
|
var isMergeKey = (ctx, key) => (merge.identify(key) || identity.isScalar(key) && (!key.type || key.type === Scalar.Scalar.PLAIN) && merge.identify(key.value)) && ctx?.doc.schema.tags.some((tag) => tag.tag === merge.tag && tag.default);
|
|
1605
1608
|
function addMergeToJSMap(ctx, map, value) {
|
|
1606
|
-
|
|
1607
|
-
if (identity.isSeq(
|
|
1608
|
-
for (const it of
|
|
1609
|
+
const source = resolveAliasValue(ctx, value);
|
|
1610
|
+
if (identity.isSeq(source))
|
|
1611
|
+
for (const it of source.items)
|
|
1609
1612
|
mergeValue(ctx, map, it);
|
|
1610
|
-
else if (Array.isArray(
|
|
1611
|
-
for (const it of
|
|
1613
|
+
else if (Array.isArray(source))
|
|
1614
|
+
for (const it of source)
|
|
1612
1615
|
mergeValue(ctx, map, it);
|
|
1613
1616
|
else
|
|
1614
|
-
mergeValue(ctx, map,
|
|
1617
|
+
mergeValue(ctx, map, source);
|
|
1615
1618
|
}
|
|
1616
1619
|
function mergeValue(ctx, map, value) {
|
|
1617
|
-
const source = ctx
|
|
1620
|
+
const source = resolveAliasValue(ctx, value);
|
|
1618
1621
|
if (!identity.isMap(source))
|
|
1619
1622
|
throw new Error("Merge sources must be maps or map aliases");
|
|
1620
1623
|
const srcMap = source.toJSON(null, ctx, Map);
|
|
@@ -1635,6 +1638,9 @@ var require_merge = __commonJS((exports) => {
|
|
|
1635
1638
|
}
|
|
1636
1639
|
return map;
|
|
1637
1640
|
}
|
|
1641
|
+
function resolveAliasValue(ctx, value) {
|
|
1642
|
+
return ctx && identity.isAlias(value) ? value.resolve(ctx.doc, ctx) : value;
|
|
1643
|
+
}
|
|
1638
1644
|
exports.addMergeToJSMap = addMergeToJSMap;
|
|
1639
1645
|
exports.isMergeKey = isMergeKey;
|
|
1640
1646
|
exports.merge = merge;
|
|
@@ -1842,13 +1848,20 @@ ${indent}${line}` : `
|
|
|
1842
1848
|
if (comment)
|
|
1843
1849
|
reqNewline = true;
|
|
1844
1850
|
let str = stringify.stringify(item, itemCtx, () => comment = null);
|
|
1845
|
-
|
|
1851
|
+
reqNewline || (reqNewline = lines.length > linesAtValue || str.includes(`
|
|
1852
|
+
`));
|
|
1853
|
+
if (i < items.length - 1) {
|
|
1846
1854
|
str += ",";
|
|
1855
|
+
} else if (ctx.options.trailingComma) {
|
|
1856
|
+
if (ctx.options.lineWidth > 0) {
|
|
1857
|
+
reqNewline || (reqNewline = lines.reduce((sum, line) => sum + line.length + 2, 2) + (str.length + 2) > ctx.options.lineWidth);
|
|
1858
|
+
}
|
|
1859
|
+
if (reqNewline) {
|
|
1860
|
+
str += ",";
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1847
1863
|
if (comment)
|
|
1848
1864
|
str += stringifyComment.lineComment(str, itemIndent, commentString(comment));
|
|
1849
|
-
if (!reqNewline && (lines.length > linesAtValue || str.includes(`
|
|
1850
|
-
`)))
|
|
1851
|
-
reqNewline = true;
|
|
1852
1865
|
lines.push(str);
|
|
1853
1866
|
linesAtValue = lines.length;
|
|
1854
1867
|
}
|
|
@@ -2203,7 +2216,7 @@ var require_stringifyNumber = __commonJS((exports) => {
|
|
|
2203
2216
|
if (!isFinite(num))
|
|
2204
2217
|
return isNaN(num) ? ".nan" : num < 0 ? "-.inf" : ".inf";
|
|
2205
2218
|
let n = Object.is(value, -0) ? "-0" : JSON.stringify(value);
|
|
2206
|
-
if (!format && minFractionDigits && (!tag || tag === "tag:yaml.org,2002:float") &&
|
|
2219
|
+
if (!format && minFractionDigits && (!tag || tag === "tag:yaml.org,2002:float") && /^-?\d/.test(n) && !n.includes("e")) {
|
|
2207
2220
|
let i = n.indexOf(".");
|
|
2208
2221
|
if (i < 0) {
|
|
2209
2222
|
i = n.length;
|
|
@@ -4428,7 +4441,7 @@ var require_resolve_flow_scalar = __commonJS((exports) => {
|
|
|
4428
4441
|
while (next === " " || next === "\t")
|
|
4429
4442
|
next = source[++i + 1];
|
|
4430
4443
|
} else if (next === "x" || next === "u" || next === "U") {
|
|
4431
|
-
const length =
|
|
4444
|
+
const length = next === "x" ? 2 : next === "u" ? 4 : 8;
|
|
4432
4445
|
res += parseCharCode(source, i + 1, length, onError);
|
|
4433
4446
|
i += length;
|
|
4434
4447
|
} else {
|
|
@@ -4497,12 +4510,13 @@ var require_resolve_flow_scalar = __commonJS((exports) => {
|
|
|
4497
4510
|
const cc = source.substr(offset, length);
|
|
4498
4511
|
const ok = cc.length === length && /^[0-9a-fA-F]+$/.test(cc);
|
|
4499
4512
|
const code = ok ? parseInt(cc, 16) : NaN;
|
|
4500
|
-
|
|
4513
|
+
try {
|
|
4514
|
+
return String.fromCodePoint(code);
|
|
4515
|
+
} catch {
|
|
4501
4516
|
const raw = source.substr(offset - 2, length + 2);
|
|
4502
4517
|
onError(offset - 2, "BAD_DQ_ESCAPE", `Invalid escape sequence ${raw}`);
|
|
4503
4518
|
return raw;
|
|
4504
4519
|
}
|
|
4505
|
-
return String.fromCodePoint(code);
|
|
4506
4520
|
}
|
|
4507
4521
|
exports.resolveFlowScalar = resolveFlowScalar;
|
|
4508
4522
|
});
|
|
@@ -4643,17 +4657,22 @@ var require_compose_node = __commonJS((exports) => {
|
|
|
4643
4657
|
case "block-map":
|
|
4644
4658
|
case "block-seq":
|
|
4645
4659
|
case "flow-collection":
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4660
|
+
try {
|
|
4661
|
+
node = composeCollection.composeCollection(CN, ctx, token, props, onError);
|
|
4662
|
+
if (anchor)
|
|
4663
|
+
node.anchor = anchor.source.substring(1);
|
|
4664
|
+
} catch (error) {
|
|
4665
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4666
|
+
onError(token, "RESOURCE_EXHAUSTION", message);
|
|
4667
|
+
}
|
|
4649
4668
|
break;
|
|
4650
4669
|
default: {
|
|
4651
4670
|
const message = token.type === "error" ? token.message : `Unsupported token (type: ${token.type})`;
|
|
4652
4671
|
onError(token, "UNEXPECTED_TOKEN", message);
|
|
4653
|
-
node = composeEmptyNode(ctx, token.offset, undefined, null, props, onError);
|
|
4654
4672
|
isSrcToken = false;
|
|
4655
4673
|
}
|
|
4656
4674
|
}
|
|
4675
|
+
node ?? (node = composeEmptyNode(ctx, token.offset, undefined, null, props, onError));
|
|
4657
4676
|
if (anchor && node.anchor === "")
|
|
4658
4677
|
onError(anchor, "BAD_ALIAS", "Anchor cannot be an empty string");
|
|
4659
4678
|
if (atKey && ctx.options.stringKeys && (!identity.isScalar(node) || typeof node.value !== "string" || node.tag && node.tag !== "tag:yaml.org,2002:str")) {
|
|
@@ -6906,7 +6925,8 @@ var require_public_api = __commonJS((exports) => {
|
|
|
6906
6925
|
|
|
6907
6926
|
// src/specialist/script-runner.ts
|
|
6908
6927
|
import { spawn } from "node:child_process";
|
|
6909
|
-
import { randomUUID } from "node:crypto";
|
|
6928
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6929
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
6910
6930
|
|
|
6911
6931
|
// src/specialist/templateEngine.ts
|
|
6912
6932
|
function renderTemplate(template, variables) {
|
|
@@ -7044,7 +7064,8 @@ function migrateToV2(db) {
|
|
|
7044
7064
|
status_json TEXT NOT NULL,
|
|
7045
7065
|
bead_id TEXT,
|
|
7046
7066
|
updated_at_ms INTEGER NOT NULL,
|
|
7047
|
-
last_output TEXT
|
|
7067
|
+
last_output TEXT,
|
|
7068
|
+
startup_payload_json TEXT
|
|
7048
7069
|
);
|
|
7049
7070
|
INSERT OR IGNORE INTO specialist_jobs_v2
|
|
7050
7071
|
SELECT
|
|
@@ -7054,7 +7075,8 @@ function migrateToV2(db) {
|
|
|
7054
7075
|
status_json,
|
|
7055
7076
|
JSON_EXTRACT(status_json, '$.bead_id'),
|
|
7056
7077
|
updated_at_ms,
|
|
7057
|
-
last_output
|
|
7078
|
+
last_output,
|
|
7079
|
+
startup_payload_json
|
|
7058
7080
|
FROM specialist_jobs;
|
|
7059
7081
|
DROP TABLE IF EXISTS specialist_jobs;
|
|
7060
7082
|
ALTER TABLE specialist_jobs_v2 RENAME TO specialist_jobs;
|
|
@@ -7081,7 +7103,8 @@ function migrateToV3(db) {
|
|
|
7081
7103
|
status TEXT NOT NULL,
|
|
7082
7104
|
status_json TEXT NOT NULL,
|
|
7083
7105
|
updated_at_ms INTEGER NOT NULL,
|
|
7084
|
-
last_output TEXT
|
|
7106
|
+
last_output TEXT,
|
|
7107
|
+
startup_payload_json TEXT
|
|
7085
7108
|
);
|
|
7086
7109
|
INSERT OR IGNORE INTO specialist_jobs_v3
|
|
7087
7110
|
SELECT
|
|
@@ -7093,7 +7116,8 @@ function migrateToV3(db) {
|
|
|
7093
7116
|
COALESCE(JSON_EXTRACT(status_json, '$.status'), 'starting'),
|
|
7094
7117
|
status_json,
|
|
7095
7118
|
updated_at_ms,
|
|
7096
|
-
last_output
|
|
7119
|
+
last_output,
|
|
7120
|
+
startup_payload_json
|
|
7097
7121
|
FROM specialist_jobs;
|
|
7098
7122
|
DROP TABLE IF EXISTS specialist_jobs;
|
|
7099
7123
|
ALTER TABLE specialist_jobs_v3 RENAME TO specialist_jobs;
|
|
@@ -7108,6 +7132,15 @@ function migrateToV3(db) {
|
|
|
7108
7132
|
function migrateToV11(db) {
|
|
7109
7133
|
const hasV11 = db.query("SELECT 1 FROM schema_version WHERE version = 11 LIMIT 1").get();
|
|
7110
7134
|
if (hasV11) {
|
|
7135
|
+
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));
|
|
7136
|
+
for (const column of [
|
|
7137
|
+
{ name: "active_runtime_ms", definition: "INTEGER" },
|
|
7138
|
+
{ name: "waiting_ms", definition: "INTEGER" }
|
|
7139
|
+
]) {
|
|
7140
|
+
if (!metricsColumns.has(column.name)) {
|
|
7141
|
+
db.run(`ALTER TABLE specialist_job_metrics ADD COLUMN ${column.name} ${column.definition}`);
|
|
7142
|
+
}
|
|
7143
|
+
}
|
|
7111
7144
|
db.run("CREATE INDEX IF NOT EXISTS idx_job_metrics_spec_model_updated ON specialist_job_metrics(specialist, model, updated_at_ms DESC)");
|
|
7112
7145
|
db.run("CREATE INDEX IF NOT EXISTS idx_job_metrics_updated ON specialist_job_metrics(updated_at_ms DESC)");
|
|
7113
7146
|
return;
|
|
@@ -7126,6 +7159,8 @@ function migrateToV11(db) {
|
|
|
7126
7159
|
started_at_ms INTEGER,
|
|
7127
7160
|
completed_at_ms INTEGER,
|
|
7128
7161
|
elapsed_ms INTEGER,
|
|
7162
|
+
active_runtime_ms INTEGER,
|
|
7163
|
+
waiting_ms INTEGER,
|
|
7129
7164
|
total_turns INTEGER NOT NULL DEFAULT 0,
|
|
7130
7165
|
total_tools INTEGER NOT NULL DEFAULT 0,
|
|
7131
7166
|
tool_call_counts_json TEXT NOT NULL,
|
|
@@ -7294,7 +7329,8 @@ function initSchema(db) {
|
|
|
7294
7329
|
{ name: "chain_root_bead_id", definition: "TEXT" },
|
|
7295
7330
|
{ name: "epic_id", definition: "TEXT" },
|
|
7296
7331
|
{ name: "status", definition: "TEXT NOT NULL DEFAULT 'starting'" },
|
|
7297
|
-
{ name: "last_output", definition: "TEXT" }
|
|
7332
|
+
{ name: "last_output", definition: "TEXT" },
|
|
7333
|
+
{ name: "startup_payload_json", definition: "TEXT" }
|
|
7298
7334
|
].filter(({ name }) => !specialistJobsColumns.has(name));
|
|
7299
7335
|
for (const missingColumn of missingSpecialistJobsColumns) {
|
|
7300
7336
|
db.run(`ALTER TABLE specialist_jobs ADD COLUMN ${missingColumn.name} ${missingColumn.definition}`);
|
|
@@ -7316,7 +7352,8 @@ function initSchema(db) {
|
|
|
7316
7352
|
status TEXT NOT NULL,
|
|
7317
7353
|
status_json TEXT NOT NULL,
|
|
7318
7354
|
updated_at_ms INTEGER NOT NULL,
|
|
7319
|
-
last_output TEXT
|
|
7355
|
+
last_output TEXT,
|
|
7356
|
+
startup_payload_json TEXT
|
|
7320
7357
|
);
|
|
7321
7358
|
INSERT OR IGNORE INTO specialist_jobs_new
|
|
7322
7359
|
SELECT
|
|
@@ -7333,7 +7370,8 @@ function initSchema(db) {
|
|
|
7333
7370
|
COALESCE(status, JSON_EXTRACT(status_json, '$.status'), 'starting'),
|
|
7334
7371
|
status_json,
|
|
7335
7372
|
updated_at_ms,
|
|
7336
|
-
last_output
|
|
7373
|
+
last_output,
|
|
7374
|
+
startup_payload_json
|
|
7337
7375
|
FROM specialist_jobs;
|
|
7338
7376
|
DROP TABLE IF EXISTS specialist_jobs;
|
|
7339
7377
|
ALTER TABLE specialist_jobs_new RENAME TO specialist_jobs;
|
|
@@ -7456,6 +7494,7 @@ function migrateToV8(db) {
|
|
|
7456
7494
|
}
|
|
7457
7495
|
db.run("CREATE INDEX IF NOT EXISTS idx_jobs_chain ON specialist_jobs(chain_id) WHERE chain_id IS NOT NULL");
|
|
7458
7496
|
db.run("CREATE INDEX IF NOT EXISTS idx_jobs_epic ON specialist_jobs(epic_id) WHERE epic_id IS NOT NULL");
|
|
7497
|
+
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
7498
|
db.run(`
|
|
7460
7499
|
CREATE TABLE IF NOT EXISTS epic_runs (
|
|
7461
7500
|
epic_id TEXT PRIMARY KEY,
|
|
@@ -7552,6 +7591,37 @@ function migrateToV10(db) {
|
|
|
7552
7591
|
VALUES (10, strftime('%s', 'now') * 1000);
|
|
7553
7592
|
`);
|
|
7554
7593
|
}
|
|
7594
|
+
var STALE_CLAIM_AGE_MS = 60000;
|
|
7595
|
+
function defaultIsPidAlive(pid) {
|
|
7596
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
7597
|
+
return false;
|
|
7598
|
+
try {
|
|
7599
|
+
process.kill(pid, 0);
|
|
7600
|
+
return true;
|
|
7601
|
+
} catch {
|
|
7602
|
+
return false;
|
|
7603
|
+
}
|
|
7604
|
+
}
|
|
7605
|
+
function claimJobStartWithStore(store, status, event, options = {}) {
|
|
7606
|
+
const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
|
|
7607
|
+
const nowMs = options.nowMs ?? Date.now;
|
|
7608
|
+
const staleAgeMs = options.staleClaimAgeMs ?? STALE_CLAIM_AGE_MS;
|
|
7609
|
+
return withRetry(() => store.transaction(() => {
|
|
7610
|
+
const existing = store.findActiveJob(status.bead_id ?? null, status.specialist);
|
|
7611
|
+
if (existing?.job_id && existing.job_id !== status.id) {
|
|
7612
|
+
const updatedAtMs = existing.updated_at_ms ?? 0;
|
|
7613
|
+
const isStale = updatedAtMs > 0 && nowMs() - updatedAtMs > staleAgeMs && !isPidAlive(existing.pid);
|
|
7614
|
+
if (isStale && store.cancelStaleClaim) {
|
|
7615
|
+
store.cancelStaleClaim(existing.job_id);
|
|
7616
|
+
} else {
|
|
7617
|
+
return { ok: false, existingJobId: existing.job_id, existingStatus: existing.status ?? "starting" };
|
|
7618
|
+
}
|
|
7619
|
+
}
|
|
7620
|
+
store.writeStatusRow(status);
|
|
7621
|
+
store.writeEventRow(status.id, status.specialist, status.bead_id, event);
|
|
7622
|
+
return { ok: true };
|
|
7623
|
+
}), "claimJobStart");
|
|
7624
|
+
}
|
|
7555
7625
|
|
|
7556
7626
|
class SqliteClient {
|
|
7557
7627
|
db;
|
|
@@ -7566,8 +7636,8 @@ class SqliteClient {
|
|
|
7566
7636
|
writeStatusRow(status, lastOutput) {
|
|
7567
7637
|
const statusJson = JSON.stringify(status);
|
|
7568
7638
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7639
|
+
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)
|
|
7640
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7571
7641
|
ON CONFLICT(job_id) DO UPDATE SET
|
|
7572
7642
|
specialist = excluded.specialist,
|
|
7573
7643
|
worktree_column = excluded.worktree_column,
|
|
@@ -7581,7 +7651,8 @@ class SqliteClient {
|
|
|
7581
7651
|
status = excluded.status,
|
|
7582
7652
|
status_json = excluded.status_json,
|
|
7583
7653
|
updated_at_ms = excluded.updated_at_ms,
|
|
7584
|
-
last_output = COALESCE(excluded.last_output, specialist_jobs.last_output)
|
|
7654
|
+
last_output = COALESCE(excluded.last_output, specialist_jobs.last_output),
|
|
7655
|
+
startup_payload_json = COALESCE(excluded.startup_payload_json, specialist_jobs.startup_payload_json);
|
|
7585
7656
|
`, [
|
|
7586
7657
|
status.id,
|
|
7587
7658
|
status.specialist,
|
|
@@ -7596,7 +7667,8 @@ class SqliteClient {
|
|
|
7596
7667
|
status.status,
|
|
7597
7668
|
statusJson,
|
|
7598
7669
|
Date.now(),
|
|
7599
|
-
lastOutput ?? null
|
|
7670
|
+
lastOutput ?? null,
|
|
7671
|
+
status.startup_payload_json ?? null
|
|
7600
7672
|
]);
|
|
7601
7673
|
}
|
|
7602
7674
|
writeEpicRunRow(epic) {
|
|
@@ -7642,6 +7714,36 @@ class SqliteClient {
|
|
|
7642
7714
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
7643
7715
|
`, [jobId, seq, specialist, beadId ?? null, event.t, event.type, eventJson]);
|
|
7644
7716
|
}
|
|
7717
|
+
claimJobStart(status, event) {
|
|
7718
|
+
return claimJobStartWithStore({
|
|
7719
|
+
transaction: (callback) => this.db.transaction(callback)(),
|
|
7720
|
+
findActiveJob: (beadId, specialist) => this.db.query(`
|
|
7721
|
+
SELECT
|
|
7722
|
+
job_id,
|
|
7723
|
+
status,
|
|
7724
|
+
updated_at_ms,
|
|
7725
|
+
CAST(JSON_EXTRACT(status_json, '$.pid') AS INTEGER) AS pid
|
|
7726
|
+
FROM specialist_jobs
|
|
7727
|
+
WHERE bead_id = ?
|
|
7728
|
+
AND specialist = ?
|
|
7729
|
+
AND status IN ('starting', 'running')
|
|
7730
|
+
ORDER BY updated_at_ms DESC
|
|
7731
|
+
LIMIT 1
|
|
7732
|
+
`).get(beadId, specialist),
|
|
7733
|
+
writeStatusRow: (nextStatus) => this.writeStatusRow(nextStatus),
|
|
7734
|
+
writeEventRow: (jobId, specialist, beadId, nextEvent) => this.writeEventRow(jobId, specialist, beadId, nextEvent),
|
|
7735
|
+
cancelStaleClaim: (jobId) => {
|
|
7736
|
+
const nowMs = Date.now();
|
|
7737
|
+
this.db.run(`
|
|
7738
|
+
UPDATE specialist_jobs
|
|
7739
|
+
SET status = 'cancelled',
|
|
7740
|
+
status_json = JSON_PATCH(status_json, JSON_OBJECT('status', 'cancelled', 'cancelled_reason', 'orphan-claim-stale')),
|
|
7741
|
+
updated_at_ms = ?
|
|
7742
|
+
WHERE job_id = ?
|
|
7743
|
+
`, [nowMs, jobId]);
|
|
7744
|
+
}
|
|
7745
|
+
}, status, event);
|
|
7746
|
+
}
|
|
7645
7747
|
writeResultRow(jobId, output) {
|
|
7646
7748
|
this.db.run(`
|
|
7647
7749
|
INSERT INTO specialist_results (job_id, output, updated_at_ms)
|
|
@@ -8102,6 +8204,15 @@ class SqliteClient {
|
|
|
8102
8204
|
return statuses;
|
|
8103
8205
|
}, "listStatuses");
|
|
8104
8206
|
}
|
|
8207
|
+
removeJobs(jobIds) {
|
|
8208
|
+
return withRetry(() => {
|
|
8209
|
+
if (jobIds.length === 0)
|
|
8210
|
+
return 0;
|
|
8211
|
+
const placeholders = jobIds.map(() => "?").join(", ");
|
|
8212
|
+
const result = this.db.query(`DELETE FROM specialist_jobs WHERE job_id IN (${placeholders})`).run(...jobIds);
|
|
8213
|
+
return result.changes ?? 0;
|
|
8214
|
+
}, "removeJobs");
|
|
8215
|
+
}
|
|
8105
8216
|
readEpicRun(epicId) {
|
|
8106
8217
|
return withRetry(() => {
|
|
8107
8218
|
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 +8242,6 @@ class SqliteClient {
|
|
|
8131
8242
|
SELECT chain_id, epic_id, chain_root_bead_id, chain_root_job_id, updated_at_ms
|
|
8132
8243
|
FROM epic_chain_membership
|
|
8133
8244
|
WHERE epic_id = ?
|
|
8134
|
-
AND (chain_root_job_id IS NULL OR chain_root_job_id != chain_id)
|
|
8135
8245
|
ORDER BY updated_at_ms DESC
|
|
8136
8246
|
`).all(epicId);
|
|
8137
8247
|
}, "listEpicChains");
|
|
@@ -8149,6 +8259,16 @@ class SqliteClient {
|
|
|
8149
8259
|
return removable;
|
|
8150
8260
|
}, "deleteEpicChainMembership");
|
|
8151
8261
|
}
|
|
8262
|
+
listReferencedChainRootJobIds() {
|
|
8263
|
+
return withRetry(() => {
|
|
8264
|
+
const rows = this.db.query(`
|
|
8265
|
+
SELECT DISTINCT chain_root_job_id
|
|
8266
|
+
FROM epic_chain_membership
|
|
8267
|
+
WHERE chain_root_job_id IS NOT NULL AND chain_root_job_id != ''
|
|
8268
|
+
`).all();
|
|
8269
|
+
return rows.map((row) => row.chain_root_job_id).filter((jobId) => typeof jobId === "string" && jobId.length > 0);
|
|
8270
|
+
}, "listReferencedChainRootJobIds");
|
|
8271
|
+
}
|
|
8152
8272
|
listEpicChainsWithLatestJob(epicId) {
|
|
8153
8273
|
return withRetry(() => {
|
|
8154
8274
|
const rows = this.db.query(`
|
|
@@ -8226,6 +8346,18 @@ class SqliteClient {
|
|
|
8226
8346
|
return rows.map((row) => row.job_id).filter((jobId) => typeof jobId === "string" && jobId.length > 0);
|
|
8227
8347
|
}, "listChainJobIds");
|
|
8228
8348
|
}
|
|
8349
|
+
listLiveJobsForBead(beadId) {
|
|
8350
|
+
return withRetry(() => {
|
|
8351
|
+
const rows = this.db.query(`
|
|
8352
|
+
SELECT job_id
|
|
8353
|
+
FROM specialist_jobs
|
|
8354
|
+
WHERE bead_id = ?
|
|
8355
|
+
AND status IN ('starting', 'running', 'waiting')
|
|
8356
|
+
ORDER BY updated_at_ms ASC
|
|
8357
|
+
`).all(beadId);
|
|
8358
|
+
return rows.map((row) => row.job_id).filter((jobId) => typeof jobId === "string" && jobId.length > 0);
|
|
8359
|
+
}, "listLiveJobsForBead");
|
|
8360
|
+
}
|
|
8229
8361
|
resolveChainEpicLinkByJobId(jobId) {
|
|
8230
8362
|
return withRetry(() => {
|
|
8231
8363
|
const row = this.db.query(`
|
|
@@ -8305,7 +8437,7 @@ class SqliteClient {
|
|
|
8305
8437
|
aggregateJobMetrics(jobId) {
|
|
8306
8438
|
return withRetry(() => {
|
|
8307
8439
|
const jobRow = this.db.query(`
|
|
8308
|
-
SELECT job_id, specialist, status, chain_kind, chain_id, bead_id, node_id, epic_id, updated_at_ms
|
|
8440
|
+
SELECT job_id, specialist, status, chain_kind, chain_id, bead_id, node_id, epic_id, updated_at_ms, startup_payload_json
|
|
8309
8441
|
FROM specialist_jobs
|
|
8310
8442
|
WHERE job_id = ?
|
|
8311
8443
|
`).get(jobId);
|
|
@@ -8323,9 +8455,22 @@ class SqliteClient {
|
|
|
8323
8455
|
let runCompleteJson = null;
|
|
8324
8456
|
let model = null;
|
|
8325
8457
|
let elapsedMs = null;
|
|
8458
|
+
let activeRuntimeMs = 0;
|
|
8459
|
+
let waitingMs = 0;
|
|
8460
|
+
let phase = null;
|
|
8461
|
+
let phaseStartedAtMs = null;
|
|
8462
|
+
const closePhase = (endAtMs) => {
|
|
8463
|
+
if (phase === null || phaseStartedAtMs === null || endAtMs < phaseStartedAtMs)
|
|
8464
|
+
return;
|
|
8465
|
+
const durationMs = endAtMs - phaseStartedAtMs;
|
|
8466
|
+
if (phase === "running") {
|
|
8467
|
+
activeRuntimeMs += durationMs;
|
|
8468
|
+
} else {
|
|
8469
|
+
waitingMs += durationMs;
|
|
8470
|
+
}
|
|
8471
|
+
};
|
|
8326
8472
|
for (const event of events) {
|
|
8327
8473
|
startedAtMs = startedAtMs === null ? event.t : Math.min(startedAtMs, event.t);
|
|
8328
|
-
completedAtMs = completedAtMs === null ? event.t : Math.max(completedAtMs, event.t);
|
|
8329
8474
|
if (event.type === "tool") {
|
|
8330
8475
|
totalTools += 1;
|
|
8331
8476
|
toolCallCounts[event.tool] = (toolCallCounts[event.tool] ?? 0) + 1;
|
|
@@ -8343,16 +8488,45 @@ class SqliteClient {
|
|
|
8343
8488
|
tokenTrajectory.push({ t: event.t, source: event.source, token_usage: event.token_usage });
|
|
8344
8489
|
continue;
|
|
8345
8490
|
}
|
|
8491
|
+
if (event.type === "run_start") {
|
|
8492
|
+
phase = "running";
|
|
8493
|
+
phaseStartedAtMs = event.t;
|
|
8494
|
+
continue;
|
|
8495
|
+
}
|
|
8496
|
+
if (event.type === "status_change") {
|
|
8497
|
+
if (event.status === "running" || event.status === "waiting") {
|
|
8498
|
+
closePhase(event.t);
|
|
8499
|
+
phase = event.status;
|
|
8500
|
+
phaseStartedAtMs = event.t;
|
|
8501
|
+
continue;
|
|
8502
|
+
}
|
|
8503
|
+
if (event.status === "done" || event.status === "error" || event.status === "cancelled") {
|
|
8504
|
+
closePhase(event.t);
|
|
8505
|
+
phase = null;
|
|
8506
|
+
phaseStartedAtMs = null;
|
|
8507
|
+
}
|
|
8508
|
+
continue;
|
|
8509
|
+
}
|
|
8346
8510
|
if (event.type === "run_complete") {
|
|
8511
|
+
closePhase(event.t);
|
|
8512
|
+
completedAtMs = event.t;
|
|
8347
8513
|
runCompleteJson = JSON.stringify(event);
|
|
8348
8514
|
model = event.model ?? model;
|
|
8349
8515
|
elapsedMs = Math.round(event.elapsed_s * 1000);
|
|
8516
|
+
phase = null;
|
|
8517
|
+
phaseStartedAtMs = null;
|
|
8350
8518
|
continue;
|
|
8351
8519
|
}
|
|
8352
8520
|
if (event.type === "stale_warning" && event.reason === "tool_duration") {
|
|
8353
8521
|
stallGaps.push({ t: event.t, tool: event.tool ?? null, silence_ms: event.silence_ms, threshold_ms: event.threshold_ms });
|
|
8354
8522
|
}
|
|
8355
8523
|
}
|
|
8524
|
+
if (startedAtMs !== null && completedAtMs === null) {
|
|
8525
|
+
completedAtMs = events.length > 0 ? events[events.length - 1].t : startedAtMs;
|
|
8526
|
+
}
|
|
8527
|
+
if (elapsedMs === null && startedAtMs !== null && completedAtMs !== null) {
|
|
8528
|
+
elapsedMs = Math.max(0, completedAtMs - startedAtMs);
|
|
8529
|
+
}
|
|
8356
8530
|
const record = {
|
|
8357
8531
|
job_id: jobRow.job_id,
|
|
8358
8532
|
specialist: jobRow.specialist,
|
|
@@ -8366,6 +8540,8 @@ class SqliteClient {
|
|
|
8366
8540
|
started_at_ms: startedAtMs,
|
|
8367
8541
|
completed_at_ms: completedAtMs,
|
|
8368
8542
|
elapsed_ms: elapsedMs,
|
|
8543
|
+
active_runtime_ms: activeRuntimeMs,
|
|
8544
|
+
waiting_ms: waitingMs,
|
|
8369
8545
|
total_turns: totalTurns,
|
|
8370
8546
|
total_tools: totalTools,
|
|
8371
8547
|
tool_call_counts_json: stringifyJson(toolCallCounts),
|
|
@@ -8373,15 +8549,16 @@ class SqliteClient {
|
|
|
8373
8549
|
context_trajectory_json: stringifyJson(contextTrajectory),
|
|
8374
8550
|
stall_gaps_json: stringifyJson(stallGaps),
|
|
8375
8551
|
run_complete_json: runCompleteJson,
|
|
8552
|
+
startup_payload_json: jobRow.startup_payload_json ?? null,
|
|
8376
8553
|
updated_at_ms: jobRow.updated_at_ms
|
|
8377
8554
|
};
|
|
8378
8555
|
this.db.run(`
|
|
8379
8556
|
INSERT INTO specialist_job_metrics (
|
|
8380
8557
|
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,
|
|
8558
|
+
started_at_ms, completed_at_ms, elapsed_ms, active_runtime_ms, waiting_ms, total_turns, total_tools,
|
|
8382
8559
|
tool_call_counts_json, token_trajectory_json, context_trajectory_json, stall_gaps_json,
|
|
8383
8560
|
run_complete_json, updated_at_ms
|
|
8384
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8561
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8385
8562
|
ON CONFLICT(job_id) DO UPDATE SET
|
|
8386
8563
|
specialist = excluded.specialist,
|
|
8387
8564
|
model = excluded.model,
|
|
@@ -8394,6 +8571,8 @@ class SqliteClient {
|
|
|
8394
8571
|
started_at_ms = excluded.started_at_ms,
|
|
8395
8572
|
completed_at_ms = excluded.completed_at_ms,
|
|
8396
8573
|
elapsed_ms = excluded.elapsed_ms,
|
|
8574
|
+
active_runtime_ms = excluded.active_runtime_ms,
|
|
8575
|
+
waiting_ms = excluded.waiting_ms,
|
|
8397
8576
|
total_turns = excluded.total_turns,
|
|
8398
8577
|
total_tools = excluded.total_tools,
|
|
8399
8578
|
tool_call_counts_json = excluded.tool_call_counts_json,
|
|
@@ -8415,6 +8594,8 @@ class SqliteClient {
|
|
|
8415
8594
|
record.started_at_ms,
|
|
8416
8595
|
record.completed_at_ms,
|
|
8417
8596
|
record.elapsed_ms,
|
|
8597
|
+
record.active_runtime_ms,
|
|
8598
|
+
record.waiting_ms,
|
|
8418
8599
|
record.total_turns,
|
|
8419
8600
|
record.total_tools,
|
|
8420
8601
|
record.tool_call_counts_json,
|
|
@@ -8447,6 +8628,29 @@ class SqliteClient {
|
|
|
8447
8628
|
return this.db.query(`SELECT * FROM specialist_job_metrics ${where} ORDER BY updated_at_ms DESC, job_id DESC`).all(...params);
|
|
8448
8629
|
}, "listJobMetrics");
|
|
8449
8630
|
}
|
|
8631
|
+
listElapsedMsBySpecialist(sinceMs, limitPerSpecialist = 200) {
|
|
8632
|
+
return withRetry(() => {
|
|
8633
|
+
const rows = this.db.query(`
|
|
8634
|
+
WITH ranked AS (
|
|
8635
|
+
SELECT specialist, elapsed_ms,
|
|
8636
|
+
ROW_NUMBER() OVER (PARTITION BY specialist ORDER BY updated_at_ms DESC) AS rn
|
|
8637
|
+
FROM specialist_job_metrics
|
|
8638
|
+
WHERE status = 'completed' AND updated_at_ms >= ? AND elapsed_ms IS NOT NULL
|
|
8639
|
+
)
|
|
8640
|
+
SELECT specialist, elapsed_ms
|
|
8641
|
+
FROM ranked
|
|
8642
|
+
WHERE rn <= ?
|
|
8643
|
+
ORDER BY specialist, rn
|
|
8644
|
+
`).all(sinceMs, limitPerSpecialist);
|
|
8645
|
+
const bySpecialist = {};
|
|
8646
|
+
for (const row of rows) {
|
|
8647
|
+
if (!row.specialist || typeof row.elapsed_ms !== "number" || !Number.isFinite(row.elapsed_ms))
|
|
8648
|
+
continue;
|
|
8649
|
+
(bySpecialist[row.specialist] ??= []).push(row.elapsed_ms);
|
|
8650
|
+
}
|
|
8651
|
+
return bySpecialist;
|
|
8652
|
+
}, "listElapsedMsBySpecialist");
|
|
8653
|
+
}
|
|
8450
8654
|
readResult(jobId) {
|
|
8451
8655
|
return withRetry(() => {
|
|
8452
8656
|
const row = this.db.query("SELECT output FROM specialist_results WHERE job_id = ? LIMIT 1").get(jobId);
|
|
@@ -8847,27 +9051,72 @@ function createObservabilitySqliteClient(cwd = process.cwd()) {
|
|
|
8847
9051
|
}
|
|
8848
9052
|
|
|
8849
9053
|
// src/specialist/script-runner.ts
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
9054
|
+
class CompatGuardError extends Error {
|
|
9055
|
+
field;
|
|
9056
|
+
constructor(field, message) {
|
|
9057
|
+
super(message);
|
|
9058
|
+
this.field = field;
|
|
9059
|
+
this.name = "CompatGuardError";
|
|
9060
|
+
}
|
|
8853
9061
|
}
|
|
8854
|
-
function
|
|
9062
|
+
function hasUnsubstitutedVariables(template, variables) {
|
|
9063
|
+
const matches = template.match(/\$([a-zA-Z_][a-zA-Z0-9_]*)/g) ?? [];
|
|
9064
|
+
for (const match of matches) {
|
|
9065
|
+
const key = match.slice(1);
|
|
9066
|
+
if (variables[key] === undefined)
|
|
9067
|
+
return key;
|
|
9068
|
+
}
|
|
9069
|
+
return null;
|
|
9070
|
+
}
|
|
9071
|
+
function compatGuard(spec, trust) {
|
|
8855
9072
|
const execution = spec.specialist.execution;
|
|
8856
9073
|
if (execution.interactive)
|
|
8857
|
-
throw new
|
|
9074
|
+
throw new CompatGuardError("execution.interactive", "interactive specialists are not allowed");
|
|
8858
9075
|
if (execution.requires_worktree)
|
|
8859
|
-
throw new
|
|
9076
|
+
throw new CompatGuardError("execution.requires_worktree", "worktree specialists are not allowed");
|
|
8860
9077
|
if (execution.permission_required !== "READ_ONLY")
|
|
8861
|
-
throw new
|
|
8862
|
-
|
|
8863
|
-
|
|
9078
|
+
throw new CompatGuardError("execution.permission_required", "permission_required must be READ_ONLY");
|
|
9079
|
+
const hasScripts = (spec.specialist.skills?.scripts?.length ?? 0) > 0;
|
|
9080
|
+
if (hasScripts && !trust?.allowLocalScripts) {
|
|
9081
|
+
throw new CompatGuardError("skills.scripts", "scripts not allowed (enable with --allow-local-scripts)");
|
|
9082
|
+
}
|
|
9083
|
+
const hasPaths = (spec.specialist.skills?.paths?.length ?? 0) > 0;
|
|
9084
|
+
const hasSkillInherit = Boolean(spec.specialist.prompt.skill_inherit);
|
|
9085
|
+
if (hasPaths && !trust?.allowSkills) {
|
|
9086
|
+
throw new CompatGuardError("skills.paths", "skills not allowed (enable with --allow-skills)");
|
|
9087
|
+
}
|
|
9088
|
+
if (hasSkillInherit && !trust?.allowSkills) {
|
|
9089
|
+
throw new CompatGuardError("prompt.skill_inherit", "skills not allowed (enable with --allow-skills)");
|
|
9090
|
+
}
|
|
9091
|
+
if (hasPaths && trust?.allowSkills && trust.allowSkillsRoots && trust.allowSkillsRoots.length > 0) {
|
|
9092
|
+
const paths = spec.specialist.skills?.paths ?? [];
|
|
9093
|
+
for (const path of paths) {
|
|
9094
|
+
const allowed = trust.allowSkillsRoots.some((root) => path.startsWith(root));
|
|
9095
|
+
if (!allowed) {
|
|
9096
|
+
throw new CompatGuardError("skills.paths", `skill path '${path}' not under any --allow-skills-roots entry`);
|
|
9097
|
+
}
|
|
9098
|
+
}
|
|
9099
|
+
}
|
|
9100
|
+
}
|
|
9101
|
+
function computeSkillSources(spec) {
|
|
9102
|
+
const paths = spec.specialist.skills?.paths ?? [];
|
|
9103
|
+
const sources = [];
|
|
9104
|
+
for (const path of paths) {
|
|
9105
|
+
try {
|
|
9106
|
+
const content = readFileSync2(path);
|
|
9107
|
+
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
9108
|
+
sources.push({ path, sha256 });
|
|
9109
|
+
} catch {
|
|
9110
|
+
sources.push({ path, sha256: "unreadable" });
|
|
9111
|
+
}
|
|
9112
|
+
}
|
|
9113
|
+
return sources;
|
|
8864
9114
|
}
|
|
8865
9115
|
function renderTaskTemplate(template, variables) {
|
|
8866
|
-
const
|
|
8867
|
-
const missing = hasUnsubstitutedVariables(output);
|
|
9116
|
+
const missing = hasUnsubstitutedVariables(template, variables);
|
|
8868
9117
|
if (missing)
|
|
8869
9118
|
throw new Error(`Missing template variable: ${missing}`);
|
|
8870
|
-
return
|
|
9119
|
+
return renderTemplate(template, variables);
|
|
8871
9120
|
}
|
|
8872
9121
|
function mapErrorType(message) {
|
|
8873
9122
|
if (message.includes("Specialist not found"))
|
|
@@ -8897,59 +9146,32 @@ function textFromMessage(message) {
|
|
|
8897
9146
|
return "";
|
|
8898
9147
|
return message.content.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("");
|
|
8899
9148
|
}
|
|
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);
|
|
9149
|
+
function extractAssistantTextFromEvent(event) {
|
|
9150
|
+
if (event.type === "message_end") {
|
|
9151
|
+
const text = textFromMessage(event.message);
|
|
9152
|
+
if (text)
|
|
9153
|
+
return text;
|
|
9154
|
+
}
|
|
9155
|
+
if (event.type === "agent_end" && Array.isArray(event.messages)) {
|
|
9156
|
+
for (let j = event.messages.length - 1;j >= 0; j--) {
|
|
9157
|
+
const text = textFromMessage(event.messages[j]);
|
|
8913
9158
|
if (text)
|
|
8914
9159
|
return text;
|
|
8915
9160
|
}
|
|
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
9161
|
}
|
|
8929
|
-
|
|
9162
|
+
if (event.type === "assistant" && typeof event.data?.text === "string")
|
|
9163
|
+
return event.data.text;
|
|
9164
|
+
const legacyContent = event.data?.content?.[0]?.text;
|
|
9165
|
+
if (typeof legacyContent === "string")
|
|
9166
|
+
return legacyContent;
|
|
9167
|
+
return;
|
|
8930
9168
|
}
|
|
8931
9169
|
function stripMarkdownFences(text) {
|
|
8932
9170
|
const trimmed = text.trim();
|
|
8933
9171
|
const fenced = trimmed.match(/^```(?:json|JSON)?\s*\n?([\s\S]*?)\n?```\s*$/);
|
|
8934
9172
|
return fenced ? fenced[1].trim() : trimmed;
|
|
8935
9173
|
}
|
|
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) {
|
|
9174
|
+
function writeTraceRow(client, specialist, model, traceId, output, durationMs, skillSources, onAuditFailure) {
|
|
8953
9175
|
if (!client)
|
|
8954
9176
|
return;
|
|
8955
9177
|
const status = {
|
|
@@ -8960,106 +9182,231 @@ function writeTraceRow(client, specialist, model, traceId, output, durationMs) {
|
|
|
8960
9182
|
started_at_ms: Date.now() - durationMs,
|
|
8961
9183
|
elapsed_s: durationMs / 1000,
|
|
8962
9184
|
last_event_at_ms: Date.now(),
|
|
8963
|
-
surface: "script_specialist"
|
|
9185
|
+
surface: "script_specialist",
|
|
9186
|
+
...skillSources && skillSources.length > 0 ? { skill_sources: skillSources } : {}
|
|
8964
9187
|
};
|
|
8965
|
-
|
|
8966
|
-
|
|
9188
|
+
try {
|
|
9189
|
+
client.upsertStatus(status);
|
|
9190
|
+
client.upsertResult(traceId, output);
|
|
9191
|
+
} catch (error) {
|
|
9192
|
+
onAuditFailure?.(error);
|
|
9193
|
+
}
|
|
9194
|
+
}
|
|
9195
|
+
var DEFAULT_PENDING_LINE_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
9196
|
+
var DEFAULT_ASSISTANT_TEXT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
9197
|
+
var DEFAULT_STDERR_LIMIT_BYTES = 1 * 1024 * 1024;
|
|
9198
|
+
function resolveAssistantTextLimitBytes(spec) {
|
|
9199
|
+
return spec.specialist.execution.stdout_limit_bytes ?? resolveEnvAssistantTextLimitBytes() ?? DEFAULT_ASSISTANT_TEXT_LIMIT_BYTES;
|
|
9200
|
+
}
|
|
9201
|
+
function resolveEnvAssistantTextLimitBytes() {
|
|
9202
|
+
const raw = process.env.SPECIALISTS_SCRIPT_STDOUT_LIMIT_BYTES;
|
|
9203
|
+
if (raw === undefined)
|
|
9204
|
+
return;
|
|
9205
|
+
const envLimit = Number(raw);
|
|
9206
|
+
if (!Number.isFinite(envLimit) || envLimit <= 0)
|
|
9207
|
+
return;
|
|
9208
|
+
process.stderr.write(`warning: SPECIALISTS_SCRIPT_STDOUT_LIMIT_BYTES is deprecated; applies to assistant text cap
|
|
9209
|
+
`);
|
|
9210
|
+
return Math.floor(envLimit);
|
|
8967
9211
|
}
|
|
8968
9212
|
function openObservabilityClient(options) {
|
|
8969
9213
|
const dbPath = options.observabilityDbPath ?? options.projectDir;
|
|
8970
9214
|
return createObservabilitySqliteClient(dbPath);
|
|
8971
9215
|
}
|
|
9216
|
+
function resolveScriptSpecialistName(name) {
|
|
9217
|
+
if (name === "changelog-keeper")
|
|
9218
|
+
return "changelog-drafter";
|
|
9219
|
+
return name;
|
|
9220
|
+
}
|
|
8972
9221
|
async function runScriptSpecialist(input, options) {
|
|
8973
9222
|
const traceId = randomUUID();
|
|
8974
9223
|
const startedAt = Date.now();
|
|
8975
9224
|
try {
|
|
8976
|
-
const
|
|
8977
|
-
|
|
9225
|
+
const resolvedSpecialist = resolveScriptSpecialistName(input.specialist);
|
|
9226
|
+
const spec = await options.loader.get(resolvedSpecialist);
|
|
9227
|
+
compatGuard(spec, options.trust);
|
|
9228
|
+
const skillSources = options.trust?.allowSkills ? computeSkillSources(spec) : undefined;
|
|
8978
9229
|
const template = input.template ?? spec.specialist.prompt.task_template;
|
|
8979
9230
|
const prompt = renderTaskTemplate(template, input.variables ?? {});
|
|
8980
|
-
|
|
9231
|
+
if (process.env.SPECIALISTS_SCRIPT_STUB_OUTPUT) {
|
|
9232
|
+
return {
|
|
9233
|
+
success: true,
|
|
9234
|
+
output: prompt,
|
|
9235
|
+
meta: {
|
|
9236
|
+
specialist: resolvedSpecialist,
|
|
9237
|
+
requested_specialist: input.requested_specialist ?? input.specialist,
|
|
9238
|
+
resolved_specialist: resolvedSpecialist,
|
|
9239
|
+
model: "stub",
|
|
9240
|
+
duration_ms: Date.now() - startedAt,
|
|
9241
|
+
trace_id: traceId
|
|
9242
|
+
}
|
|
9243
|
+
};
|
|
9244
|
+
}
|
|
8981
9245
|
const timeoutMs = input.timeout_ms ?? spec.specialist.execution.timeout_ms ?? 120000;
|
|
9246
|
+
const modelCandidates = collectModelCandidates(input, spec, options);
|
|
9247
|
+
const assistantTextLimitBytes = resolveAssistantTextLimitBytes(spec);
|
|
9248
|
+
const attempts = [];
|
|
9249
|
+
for (const model of modelCandidates) {
|
|
9250
|
+
const attempt = await runSingleAttempt(prompt, model, input.thinking_level ?? spec.specialist.execution.thinking_level, timeoutMs, assistantTextLimitBytes, options);
|
|
9251
|
+
attempts.push(attempt);
|
|
9252
|
+
const parsed = classifyAttempt(attempt);
|
|
9253
|
+
if (parsed.retryable)
|
|
9254
|
+
continue;
|
|
9255
|
+
const durationMs2 = Date.now() - startedAt;
|
|
9256
|
+
const observability2 = openObservabilityClient(options);
|
|
9257
|
+
if (input.trace !== false && observability2)
|
|
9258
|
+
writeTraceRow(observability2, resolvedSpecialist, model, traceId, parsed.text, durationMs2, skillSources, options.onAuditFailure);
|
|
9259
|
+
if (parsed.kind === "success") {
|
|
9260
|
+
let parsed_json;
|
|
9261
|
+
if (spec.specialist.execution.response_format === "json") {
|
|
9262
|
+
try {
|
|
9263
|
+
parsed_json = JSON.parse(stripMarkdownFences(parsed.text));
|
|
9264
|
+
const required = Array.isArray(spec.specialist.prompt.output_schema?.required) ? spec.specialist.prompt.output_schema.required.filter((value) => typeof value === "string") : [];
|
|
9265
|
+
for (const key of required) {
|
|
9266
|
+
if (parsed_json === null || typeof parsed_json !== "object" || !(key in parsed_json))
|
|
9267
|
+
throw new Error(`Missing required output field: ${key}`);
|
|
9268
|
+
}
|
|
9269
|
+
} catch (error) {
|
|
9270
|
+
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 } };
|
|
9271
|
+
}
|
|
9272
|
+
}
|
|
9273
|
+
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 } };
|
|
9274
|
+
}
|
|
9275
|
+
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 } };
|
|
9276
|
+
}
|
|
9277
|
+
const lastAttempt = attempts.at(-1);
|
|
9278
|
+
const durationMs = Date.now() - startedAt;
|
|
9279
|
+
const observability = openObservabilityClient(options);
|
|
9280
|
+
if (input.trace !== false && observability)
|
|
9281
|
+
writeTraceRow(observability, resolvedSpecialist, modelCandidates.at(-1) ?? "unknown", traceId, lastAttempt?.text ?? "", durationMs, skillSources, options.onAuditFailure);
|
|
9282
|
+
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 } };
|
|
9283
|
+
} catch (error) {
|
|
9284
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9285
|
+
const resolvedSpecialist = resolveScriptSpecialistName(input.specialist);
|
|
9286
|
+
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 } };
|
|
9287
|
+
}
|
|
9288
|
+
}
|
|
9289
|
+
function collectModelCandidates(input, spec, options) {
|
|
9290
|
+
const candidates = [input.model_override, spec.specialist.execution.model, spec.specialist.execution.fallback_model, options.fallbackModel].filter((value) => typeof value === "string" && value.length > 0);
|
|
9291
|
+
return [...new Set(candidates)];
|
|
9292
|
+
}
|
|
9293
|
+
function runSingleAttempt(prompt, model, thinkingLevel, timeoutMs, assistantTextLimitBytes, options) {
|
|
9294
|
+
return new Promise((resolve, reject) => {
|
|
8982
9295
|
const args = ["--mode", "json", "--no-session", "--no-extensions", "--no-tools", "--model", model];
|
|
8983
|
-
const thinkingLevel = input.thinking_level ?? spec.specialist.execution.thinking_level;
|
|
8984
9296
|
if (thinkingLevel)
|
|
8985
9297
|
args.push("--thinking", thinkingLevel);
|
|
8986
9298
|
args.push(prompt);
|
|
8987
9299
|
const pi = spawn("pi", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
8988
9300
|
options.onChild?.(pi);
|
|
8989
|
-
const chunks = [];
|
|
8990
9301
|
let stderr = "";
|
|
8991
9302
|
let timedOut = false;
|
|
8992
9303
|
let outputTooLarge = false;
|
|
8993
|
-
|
|
8994
|
-
let
|
|
9304
|
+
let outputTooLargeReason;
|
|
9305
|
+
let pending = "";
|
|
9306
|
+
let assistantText = "";
|
|
9307
|
+
let pendingBytes = 0;
|
|
9308
|
+
let stderrBytes = 0;
|
|
8995
9309
|
const timer = setTimeout(() => {
|
|
8996
9310
|
timedOut = true;
|
|
8997
9311
|
pi.kill("SIGTERM");
|
|
8998
9312
|
setTimeout(() => pi.kill("SIGKILL"), 2000);
|
|
8999
9313
|
}, timeoutMs);
|
|
9000
9314
|
pi.stdout.on("data", (chunk) => {
|
|
9315
|
+
if (outputTooLarge)
|
|
9316
|
+
return;
|
|
9001
9317
|
const buffer = Buffer.from(chunk);
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
if (
|
|
9318
|
+
pending += buffer.toString("utf-8");
|
|
9319
|
+
pendingBytes += buffer.length;
|
|
9320
|
+
if (pendingBytes > DEFAULT_PENDING_LINE_LIMIT_BYTES) {
|
|
9005
9321
|
outputTooLarge = true;
|
|
9322
|
+
outputTooLargeReason = "malformed_line_too_large";
|
|
9006
9323
|
pi.kill("SIGTERM");
|
|
9007
9324
|
setTimeout(() => pi.kill("SIGKILL"), 2000);
|
|
9325
|
+
return;
|
|
9008
9326
|
}
|
|
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}`);
|
|
9327
|
+
const lines = pending.split(/\r?\n/);
|
|
9328
|
+
pending = lines.pop() ?? "";
|
|
9329
|
+
pendingBytes = Buffer.byteLength(pending);
|
|
9330
|
+
for (const rawLine of lines) {
|
|
9331
|
+
const line = rawLine.trim();
|
|
9332
|
+
if (!line)
|
|
9333
|
+
continue;
|
|
9334
|
+
try {
|
|
9335
|
+
const event = JSON.parse(line);
|
|
9336
|
+
const nextAssistantText = extractAssistantTextFromEvent(event);
|
|
9337
|
+
if (nextAssistantText !== undefined) {
|
|
9338
|
+
if (Buffer.byteLength(nextAssistantText, "utf8") > assistantTextLimitBytes) {
|
|
9339
|
+
outputTooLarge = true;
|
|
9340
|
+
outputTooLargeReason = "assistant_text_too_large";
|
|
9341
|
+
pi.kill("SIGTERM");
|
|
9342
|
+
setTimeout(() => pi.kill("SIGKILL"), 2000);
|
|
9343
|
+
return;
|
|
9344
|
+
}
|
|
9345
|
+
assistantText = nextAssistantText;
|
|
9047
9346
|
}
|
|
9347
|
+
} catch {
|
|
9348
|
+
continue;
|
|
9048
9349
|
}
|
|
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
9350
|
}
|
|
9052
|
-
}
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9351
|
+
});
|
|
9352
|
+
pi.stderr.on("data", (chunk) => {
|
|
9353
|
+
if (outputTooLarge)
|
|
9354
|
+
return;
|
|
9355
|
+
const text = String(chunk);
|
|
9356
|
+
stderr += text;
|
|
9357
|
+
stderrBytes += Buffer.byteLength(text, "utf8");
|
|
9358
|
+
if (stderrBytes > DEFAULT_STDERR_LIMIT_BYTES) {
|
|
9359
|
+
outputTooLarge = true;
|
|
9360
|
+
outputTooLargeReason = "stderr_too_large";
|
|
9361
|
+
stderr = stderr.slice(0, DEFAULT_STDERR_LIMIT_BYTES);
|
|
9362
|
+
pi.kill("SIGTERM");
|
|
9363
|
+
setTimeout(() => pi.kill("SIGKILL"), 2000);
|
|
9364
|
+
}
|
|
9365
|
+
});
|
|
9366
|
+
pi.on("error", reject);
|
|
9367
|
+
pi.on("close", (code) => {
|
|
9368
|
+
clearTimeout(timer);
|
|
9369
|
+
resolve({
|
|
9370
|
+
model,
|
|
9371
|
+
text: assistantText,
|
|
9372
|
+
stderr,
|
|
9373
|
+
exitCode: code ?? 0,
|
|
9374
|
+
timedOut,
|
|
9375
|
+
outputTooLarge,
|
|
9376
|
+
outputTooLargeReason
|
|
9377
|
+
});
|
|
9378
|
+
});
|
|
9379
|
+
});
|
|
9380
|
+
}
|
|
9381
|
+
function classifyAttempt(attempt) {
|
|
9382
|
+
if (attempt.outputTooLarge) {
|
|
9383
|
+
if (attempt.outputTooLargeReason === "assistant_text_too_large")
|
|
9384
|
+
return { retryable: false, kind: "failure", error: "assistant message too large", errorType: "output_too_large", text: attempt.text };
|
|
9385
|
+
if (attempt.outputTooLargeReason === "stderr_too_large")
|
|
9386
|
+
return { retryable: false, kind: "failure", error: "stderr too large", errorType: "output_too_large", text: attempt.text };
|
|
9387
|
+
if (attempt.outputTooLargeReason === "malformed_line_too_large")
|
|
9388
|
+
return { retryable: false, kind: "failure", error: "malformed line too large", errorType: "output_too_large", text: attempt.text };
|
|
9389
|
+
return { retryable: false, kind: "failure", error: "output exceeded cap", errorType: "output_too_large", text: attempt.text };
|
|
9390
|
+
}
|
|
9391
|
+
if (attempt.timedOut)
|
|
9392
|
+
return { retryable: false, kind: "failure", error: attempt.stderr || "timed out", errorType: "timeout", text: attempt.text };
|
|
9393
|
+
const retryable = isRetryableModelFailure(attempt.stderr, attempt.text);
|
|
9394
|
+
if (attempt.exitCode !== 0) {
|
|
9395
|
+
const errorType = mapErrorType(attempt.stderr);
|
|
9396
|
+
return { retryable, kind: "failure", error: attempt.stderr || `pi exit ${attempt.exitCode}`, errorType, text: attempt.text };
|
|
9397
|
+
}
|
|
9398
|
+
if (!attempt.text) {
|
|
9399
|
+
return { retryable, kind: "failure", error: attempt.stderr || "pi produced no assistant text", errorType: mapErrorType(attempt.stderr), text: attempt.text };
|
|
9400
|
+
}
|
|
9401
|
+
return { retryable: false, kind: "success", error: "", errorType: "internal", text: attempt.text };
|
|
9402
|
+
}
|
|
9403
|
+
function isRetryableModelFailure(stderr, text) {
|
|
9404
|
+
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
9405
|
}
|
|
9059
9406
|
// src/specialist/loader.ts
|
|
9060
9407
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
9061
9408
|
import { basename, join as join2 } from "node:path";
|
|
9062
|
-
import { existsSync as
|
|
9409
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
9063
9410
|
|
|
9064
9411
|
// node_modules/yaml/dist/index.js
|
|
9065
9412
|
var composer = require_composer();
|
|
@@ -12937,6 +13284,7 @@ var ExecutionSchema = objectType({
|
|
|
12937
13284
|
stall_timeout_ms: numberType().optional(),
|
|
12938
13285
|
max_retries: numberType().int().min(0).default(0),
|
|
12939
13286
|
interactive: booleanType().default(false),
|
|
13287
|
+
stdout_limit_bytes: numberType().int().positive().optional(),
|
|
12940
13288
|
response_format: enumType(["text", "json", "markdown"]).default("text"),
|
|
12941
13289
|
output_type: enumType(["codegen", "analysis", "review", "synthesis", "orchestration", "workflow", "research", "custom"]).default("custom"),
|
|
12942
13290
|
permission_required: enumType(["READ_ONLY", "LOW", "MEDIUM", "HIGH"]).default("READ_ONLY"),
|
|
@@ -13087,6 +13435,20 @@ ${result.warnings.map((w) => ` ⚠ ${w}`).join(`
|
|
|
13087
13435
|
return SpecialistSchema.parseAsync(raw);
|
|
13088
13436
|
}
|
|
13089
13437
|
|
|
13438
|
+
// src/specialist/canonical-asset-resolver.ts
|
|
13439
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
13440
|
+
import { fileURLToPath } from "node:url";
|
|
13441
|
+
function resolveCanonicalAssetDir(relativePath) {
|
|
13442
|
+
const configPath = `config/${relativePath}`;
|
|
13443
|
+
let resolved = fileURLToPath(new URL(`../${configPath}`, import.meta.url));
|
|
13444
|
+
if (existsSync2(resolved))
|
|
13445
|
+
return resolved;
|
|
13446
|
+
resolved = fileURLToPath(new URL(`../../${configPath}`, import.meta.url));
|
|
13447
|
+
if (existsSync2(resolved))
|
|
13448
|
+
return resolved;
|
|
13449
|
+
return null;
|
|
13450
|
+
}
|
|
13451
|
+
|
|
13090
13452
|
// src/specialist/loader.ts
|
|
13091
13453
|
class SpecialistLoader {
|
|
13092
13454
|
cache = new Map;
|
|
@@ -13101,11 +13463,12 @@ class SpecialistLoader {
|
|
|
13101
13463
|
{ path: join2(this.projectDir, ".specialists", "default"), scope: "default", source: "default-mirror" },
|
|
13102
13464
|
{ path: join2(this.projectDir, ".specialists", "default", "specialists"), scope: "default", source: "legacy" },
|
|
13103
13465
|
{ path: join2(this.projectDir, "config", "specialists"), scope: "package", source: "package-fallback" },
|
|
13466
|
+
{ path: resolveCanonicalAssetDir("specialists") ?? "", scope: "package", source: "package-live" },
|
|
13104
13467
|
{ path: join2(this.projectDir, "specialists"), scope: "default", source: "legacy" },
|
|
13105
13468
|
{ path: join2(this.projectDir, ".claude", "specialists"), scope: "default", source: "legacy" },
|
|
13106
13469
|
{ path: join2(this.projectDir, ".agent-forge", "specialists"), scope: "default", source: "legacy" }
|
|
13107
13470
|
];
|
|
13108
|
-
return dirs.filter((d) =>
|
|
13471
|
+
return dirs.filter((d) => d.path && existsSync3(d.path));
|
|
13109
13472
|
}
|
|
13110
13473
|
toJson(content, isYaml) {
|
|
13111
13474
|
if (!isYaml)
|
|
@@ -13114,11 +13477,11 @@ class SpecialistLoader {
|
|
|
13114
13477
|
}
|
|
13115
13478
|
resolveSpecialistPath(dirPath, specialistName) {
|
|
13116
13479
|
const jsonPath = join2(dirPath, `${specialistName}.specialist.json`);
|
|
13117
|
-
if (
|
|
13480
|
+
if (existsSync3(jsonPath)) {
|
|
13118
13481
|
return { filePath: jsonPath, deprecatedYaml: false };
|
|
13119
13482
|
}
|
|
13120
13483
|
const yamlPath = join2(dirPath, `${specialistName}.specialist.yaml`);
|
|
13121
|
-
if (
|
|
13484
|
+
if (existsSync3(yamlPath)) {
|
|
13122
13485
|
return { filePath: yamlPath, deprecatedYaml: true };
|
|
13123
13486
|
}
|
|
13124
13487
|
return null;
|
|
@@ -13159,6 +13522,7 @@ class SpecialistLoader {
|
|
|
13159
13522
|
thinking_level: spec.specialist.execution.thinking_level,
|
|
13160
13523
|
skills: spec.specialist.skills?.paths ?? [],
|
|
13161
13524
|
scripts: spec.specialist.skills?.scripts ?? [],
|
|
13525
|
+
mandatoryRuleTemplateSets: spec.specialist.mandatory_rules?.template_sets ?? [],
|
|
13162
13526
|
scope: dir.scope,
|
|
13163
13527
|
source: dir.source,
|
|
13164
13528
|
filePath: resolved.filePath,
|