@rulvar/core 1.52.0 → 1.54.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/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { createHash, getRandomValues, randomUUID } from "node:crypto";
2
- import { appendFileSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
- import { dirname, join, resolve, sep } from "node:path";
2
+ import { appendFileSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import path, { dirname, join, resolve, sep } from "node:path";
4
4
  import { Client } from "@modelcontextprotocol/sdk/client";
5
5
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
6
6
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
7
7
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
8
8
  import { ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
9
9
  import { execFile } from "node:child_process";
10
- import { mkdtemp, rm } from "node:fs/promises";
10
+ import { mkdtemp, readFile, readdir, realpath, rm, stat } from "node:fs/promises";
11
11
  import { tmpdir } from "node:os";
12
12
  import { promisify } from "node:util";
13
13
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -3297,6 +3297,459 @@ var GitWorktreeProvider = class {
3297
3297
  }
3298
3298
  };
3299
3299
  //#endregion
3300
+ //#region src/tools/research.ts
3301
+ /**
3302
+ * The standard repository research toolset (RV-210 remainder): paginated
3303
+ * list/search/read tools over a confined directory root with STABLE
3304
+ * keyset cursors, plus an evidence collector that verifies citations at
3305
+ * collection time. Design contract:
3306
+ *
3307
+ * - Responses are CANONICAL: a page is a pure function of (root
3308
+ * filesystem state, logical window), never of how the window was
3309
+ * addressed, so reading the same page through a cursor and through
3310
+ * fresh arguments returns byte-identical results. That is what makes
3311
+ * the RV-210 `maxNoNewEvidenceCalls` guard measure duplicate-page
3312
+ * reads correctly, and the `maxRepeatedToolSignature` guard already
3313
+ * denies byte-identical repeat calls: deduplication is composition,
3314
+ * not a marker field.
3315
+ * - Cursors are keyset cursors (the last path / line, not an offset), so
3316
+ * a page boundary never shifts when unrelated entries appear or
3317
+ * disappear, and every cursor embeds the query identity: replaying a
3318
+ * cursor against different arguments is a typed error result.
3319
+ * - Ordering is deterministic byte order (UTF-16 code unit sort), never
3320
+ * locale collation.
3321
+ * - The root confines everything: relative paths only, `..` escapes and
3322
+ * symlink escapes are typed error results, symlinked directories are
3323
+ * never walked.
3324
+ * - User-level failures (bad path, binary file, oversized file, invalid
3325
+ * cursor, an unverifiable citation) are RETURNED `{ error }` values,
3326
+ * deterministic and visible to the model; only host misconfiguration
3327
+ * throws (ConfigError at construction).
3328
+ * - Tool results are journaled at execution time, so replay never
3329
+ * touches the filesystem; live pages read the live tree.
3330
+ *
3331
+ * Public docs: https://docs.rulvar.com/guide/tools
3332
+ */
3333
+ const DEFAULT_PAGE_SIZE = 50;
3334
+ const DEFAULT_READ_PAGE_CHARS = 4e3;
3335
+ const DEFAULT_MAX_FILE_BYTES = 262144;
3336
+ const DEFAULT_MAX_SCANNED_FILES = 2e4;
3337
+ const ALWAYS_IGNORED = [".git", "node_modules"];
3338
+ const SEARCH_SNIPPET_CHARS = 200;
3339
+ const BINARY_SNIFF_BYTES = 8192;
3340
+ const LIST_SCHEMA = {
3341
+ type: "object",
3342
+ additionalProperties: false,
3343
+ properties: {
3344
+ dir: {
3345
+ type: "string",
3346
+ description: "Root-relative directory to list; default the root."
3347
+ },
3348
+ cursor: {
3349
+ type: "string",
3350
+ description: "Opaque cursor from a previous page."
3351
+ }
3352
+ }
3353
+ };
3354
+ const SEARCH_SCHEMA = {
3355
+ type: "object",
3356
+ additionalProperties: false,
3357
+ required: ["query"],
3358
+ properties: {
3359
+ query: {
3360
+ type: "string",
3361
+ minLength: 1,
3362
+ description: "Literal substring to find."
3363
+ },
3364
+ dir: {
3365
+ type: "string",
3366
+ description: "Root-relative directory to search; default the root."
3367
+ },
3368
+ cursor: {
3369
+ type: "string",
3370
+ description: "Opaque cursor from a previous page."
3371
+ }
3372
+ }
3373
+ };
3374
+ const READ_SCHEMA = {
3375
+ type: "object",
3376
+ additionalProperties: false,
3377
+ required: ["path"],
3378
+ properties: {
3379
+ path: {
3380
+ type: "string",
3381
+ minLength: 1,
3382
+ description: "Root-relative file path."
3383
+ },
3384
+ cursor: {
3385
+ type: "string",
3386
+ description: "Opaque cursor from a previous page."
3387
+ }
3388
+ }
3389
+ };
3390
+ const RECORD_EVIDENCE_SCHEMA = {
3391
+ type: "object",
3392
+ additionalProperties: false,
3393
+ required: ["claim", "file"],
3394
+ properties: {
3395
+ claim: {
3396
+ type: "string",
3397
+ minLength: 1,
3398
+ description: "The claim this evidence supports."
3399
+ },
3400
+ file: {
3401
+ type: "string",
3402
+ minLength: 1,
3403
+ description: "Root-relative file the claim cites."
3404
+ },
3405
+ lines: {
3406
+ type: "string",
3407
+ description: "Cited line or range, 1-based: '12' or '12-40'."
3408
+ },
3409
+ quote: {
3410
+ type: "string",
3411
+ minLength: 1,
3412
+ description: "Verbatim quote; verified to appear in the file."
3413
+ }
3414
+ }
3415
+ };
3416
+ const LIST_EVIDENCE_SCHEMA = {
3417
+ type: "object",
3418
+ additionalProperties: false,
3419
+ properties: { cursor: {
3420
+ type: "string",
3421
+ description: "Opaque cursor from a previous page."
3422
+ } }
3423
+ };
3424
+ function encodeCursor(payload) {
3425
+ return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
3426
+ }
3427
+ function decodeCursor(raw) {
3428
+ try {
3429
+ return JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
3430
+ } catch {
3431
+ return;
3432
+ }
3433
+ }
3434
+ function isBinary(buffer) {
3435
+ return buffer.subarray(0, BINARY_SNIFF_BYTES).includes(0);
3436
+ }
3437
+ /** Split into lines on LF; a trailing CR per line is presentation, not content. */
3438
+ function splitLines(text) {
3439
+ return text.split("\n").map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
3440
+ }
3441
+ function repositoryResearchToolset(options) {
3442
+ if (typeof options.root !== "string" || options.root.length === 0) throw new ConfigError("repositoryResearchToolset root must be a non-empty string");
3443
+ let realRoot;
3444
+ try {
3445
+ realRoot = realpathSync(path.resolve(options.root));
3446
+ } catch {
3447
+ throw new ConfigError(`repositoryResearchToolset root '${options.root}' does not exist`);
3448
+ }
3449
+ if (!statSync(realRoot).isDirectory()) throw new ConfigError(`repositoryResearchToolset root '${options.root}' is not a directory`);
3450
+ for (const [name, value] of [
3451
+ ["pageSize", options.pageSize],
3452
+ ["readPageChars", options.readPageChars],
3453
+ ["maxFileBytes", options.maxFileBytes],
3454
+ ["maxScannedFiles", options.maxScannedFiles]
3455
+ ]) if (value !== void 0 && (!Number.isSafeInteger(value) || value < 1)) throw new ConfigError(`repositoryResearchToolset ${name} must be a positive integer; got ${String(value)}`);
3456
+ const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
3457
+ const readPageChars = options.readPageChars ?? DEFAULT_READ_PAGE_CHARS;
3458
+ const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
3459
+ const maxScannedFiles = options.maxScannedFiles ?? DEFAULT_MAX_SCANNED_FILES;
3460
+ const includeHidden = options.includeHidden ?? false;
3461
+ const ignored = /* @__PURE__ */ new Set([...ALWAYS_IGNORED, ...options.ignore ?? []]);
3462
+ const evidence = [];
3463
+ /**
3464
+ * Resolves a root-relative POSIX path and confines it: absolute paths,
3465
+ * `..` escapes, and symlink escapes are error strings, never throws.
3466
+ * `mustExist` additionally resolves symlinks and re-checks containment
3467
+ * of the REAL path (the FileTranscriptStore traversal lesson).
3468
+ */
3469
+ const resolveWithin = async (rel, kind) => {
3470
+ const normalizedInput = rel.replaceAll("\\", "/");
3471
+ if (path.posix.isAbsolute(normalizedInput) || path.isAbsolute(rel)) return { error: `path must be relative to the research root; got '${rel}'` };
3472
+ const normalized = path.posix.normalize(normalizedInput);
3473
+ if (normalized === ".." || normalized.startsWith("../")) return { error: `path escapes the research root: '${rel}'` };
3474
+ const cleaned = normalized === "." ? "" : normalized;
3475
+ if (kind === "file" && cleaned === "") return { error: "path must name a file inside the research root" };
3476
+ const abs = path.resolve(realRoot, cleaned);
3477
+ let real;
3478
+ try {
3479
+ real = await realpath(abs);
3480
+ } catch {
3481
+ return { error: `no such ${kind} under the research root: '${cleaned === "" ? "." : cleaned}'` };
3482
+ }
3483
+ if (real !== realRoot && !real.startsWith(realRoot + path.sep)) return { error: `path escapes the research root: '${rel}'` };
3484
+ try {
3485
+ const info = await stat(real);
3486
+ if (kind === "file" && !info.isFile()) return { error: `not a regular file: '${cleaned}'` };
3487
+ if (kind === "dir" && !info.isDirectory()) return { error: `not a directory: '${cleaned === "" ? "." : cleaned}'` };
3488
+ } catch {
3489
+ return { error: `no such ${kind} under the research root: '${cleaned === "" ? "." : cleaned}'` };
3490
+ }
3491
+ return {
3492
+ abs: real,
3493
+ rel: cleaned
3494
+ };
3495
+ };
3496
+ /**
3497
+ * Deterministic recursive walk: sorted entries, ignored and hidden
3498
+ * names skipped, symlinks never followed, regular files only. Returns
3499
+ * root-relative POSIX paths in byte order, or an error when the walk
3500
+ * exceeds maxScannedFiles.
3501
+ */
3502
+ const walkFiles = async (absDir, relDir) => {
3503
+ const files = [];
3504
+ let visited = 0;
3505
+ const recurse = async (dirAbs, dirRel) => {
3506
+ let entries;
3507
+ try {
3508
+ entries = await readdir(dirAbs, { withFileTypes: true });
3509
+ } catch {
3510
+ return `directory disappeared during the walk: '${dirRel === "" ? "." : dirRel}'`;
3511
+ }
3512
+ const sorted = [...entries].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
3513
+ for (const entry of sorted) {
3514
+ const name = entry.name;
3515
+ if (ignored.has(name)) continue;
3516
+ if (!includeHidden && name.startsWith(".")) continue;
3517
+ const childRel = dirRel === "" ? name : `${dirRel}/${name}`;
3518
+ if (entry.isDirectory()) {
3519
+ const failure = await recurse(path.join(dirAbs, name), childRel);
3520
+ if (failure !== void 0) return failure;
3521
+ continue;
3522
+ }
3523
+ if (!entry.isFile()) continue;
3524
+ visited += 1;
3525
+ if (visited > maxScannedFiles) return `the walk exceeded maxScannedFiles (${String(maxScannedFiles)}); narrow dir or raise the limit`;
3526
+ files.push(childRel);
3527
+ }
3528
+ };
3529
+ const failure = await recurse(absDir, relDir);
3530
+ if (failure !== void 0) return { error: failure };
3531
+ return { files };
3532
+ };
3533
+ const loadTextFile = async (abs, rel) => {
3534
+ let info;
3535
+ try {
3536
+ info = await stat(abs);
3537
+ } catch {
3538
+ return { error: `no such file under the research root: '${rel}'` };
3539
+ }
3540
+ if (info.size > maxFileBytes) return { error: `file '${rel}' is ${String(info.size)} bytes, over the maxFileBytes limit (${String(maxFileBytes)})` };
3541
+ const buffer = await readFile(abs);
3542
+ if (isBinary(buffer)) return { error: `file '${rel}' is binary` };
3543
+ return { text: buffer.toString("utf8") };
3544
+ };
3545
+ return {
3546
+ tools: [
3547
+ tool({
3548
+ name: "list_files",
3549
+ description: "List files under a directory of the research root, recursively, in deterministic byte order, one page at a time. Returns { files, totalFiles, nextCursor? }; pass cursor to continue where the last page ended (the cursor is stable: unrelated changes never shift the boundary).",
3550
+ parameters: LIST_SCHEMA,
3551
+ risk: "read",
3552
+ execute: async (input) => {
3553
+ const params = input;
3554
+ let dir = params.dir ?? "";
3555
+ let after;
3556
+ if (params.cursor !== void 0) {
3557
+ const payload = decodeCursor(params.cursor);
3558
+ if (payload === void 0 || payload.t !== "list" || typeof payload.dir !== "string" || typeof payload.after !== "string" || params.dir !== void 0 && params.dir !== payload.dir) return { error: "invalid cursor: pass the nextCursor of a previous list_files page" };
3559
+ dir = payload.dir;
3560
+ after = payload.after;
3561
+ }
3562
+ const resolved = await resolveWithin(dir, "dir");
3563
+ if ("error" in resolved) return resolved;
3564
+ const walked = await walkFiles(resolved.abs, resolved.rel);
3565
+ if ("error" in walked) return walked;
3566
+ const remaining = after === void 0 ? walked.files : walked.files.filter((file) => file > after);
3567
+ const page = remaining.slice(0, pageSize);
3568
+ const more = remaining.length > pageSize;
3569
+ return {
3570
+ files: page,
3571
+ totalFiles: walked.files.length,
3572
+ ...more ? { nextCursor: encodeCursor({
3573
+ t: "list",
3574
+ dir: resolved.rel,
3575
+ after: page[page.length - 1]
3576
+ }) } : {}
3577
+ };
3578
+ }
3579
+ }),
3580
+ tool({
3581
+ name: "search_files",
3582
+ description: "Search files under the research root for a literal substring (case-sensitive, never a regex), one page of matches at a time in deterministic (path, line) order. Returns { matches: [{ file, line, text }], filesScanned, filesSkipped, nextCursor? }; binary and oversized files are skipped and counted.",
3583
+ parameters: SEARCH_SCHEMA,
3584
+ risk: "read",
3585
+ execute: async (input) => {
3586
+ const params = input;
3587
+ let dir = params.dir ?? "";
3588
+ let query = params.query;
3589
+ let after;
3590
+ if (params.cursor !== void 0) {
3591
+ const payload = decodeCursor(params.cursor);
3592
+ if (payload === void 0 || payload.t !== "search" || typeof payload.query !== "string" || typeof payload.dir !== "string" || typeof payload.file !== "string" || typeof payload.line !== "number" || payload.query !== params.query || params.dir !== void 0 && params.dir !== payload.dir) return { error: "invalid cursor: pass the nextCursor of a previous search_files page with the same query and dir" };
3593
+ dir = payload.dir;
3594
+ query = payload.query;
3595
+ after = {
3596
+ file: payload.file,
3597
+ line: payload.line
3598
+ };
3599
+ }
3600
+ if (query.length === 0) return { error: "query must be a non-empty literal substring" };
3601
+ const resolved = await resolveWithin(dir, "dir");
3602
+ if ("error" in resolved) return resolved;
3603
+ const walked = await walkFiles(resolved.abs, resolved.rel);
3604
+ if ("error" in walked) return walked;
3605
+ const matches = [];
3606
+ let filesScanned = 0;
3607
+ let filesSkipped = 0;
3608
+ for (const file of walked.files) {
3609
+ const loaded = await loadTextFile(path.join(realRoot, file), file);
3610
+ if ("error" in loaded) {
3611
+ filesSkipped += 1;
3612
+ continue;
3613
+ }
3614
+ filesScanned += 1;
3615
+ const lines = splitLines(loaded.text);
3616
+ for (let index = 0; index < lines.length; index += 1) if (lines[index].includes(query)) matches.push({
3617
+ file,
3618
+ line: index + 1,
3619
+ text: lines[index].trim().slice(0, SEARCH_SNIPPET_CHARS)
3620
+ });
3621
+ }
3622
+ const remaining = after === void 0 ? matches : matches.filter((match) => match.file > after.file || match.file === after.file && match.line > after.line);
3623
+ const page = remaining.slice(0, pageSize);
3624
+ const more = remaining.length > pageSize;
3625
+ const last = page[page.length - 1];
3626
+ return {
3627
+ matches: page,
3628
+ filesScanned,
3629
+ filesSkipped,
3630
+ ...more && last !== void 0 ? { nextCursor: encodeCursor({
3631
+ t: "search",
3632
+ query,
3633
+ dir: resolved.rel,
3634
+ file: last.file,
3635
+ line: last.line
3636
+ }) } : {}
3637
+ };
3638
+ }
3639
+ }),
3640
+ tool({
3641
+ name: "read_file",
3642
+ description: "Read a file of the research root as numbered lines, one page at a time (whole lines up to the page character budget). Returns { path, totalLines, fromLine, toLine, content, nextCursor? }; the same page reads byte-identically however it is addressed, so duplicate reads are visible to the exploration guards.",
3643
+ parameters: READ_SCHEMA,
3644
+ risk: "read",
3645
+ execute: async (input) => {
3646
+ const params = input;
3647
+ let rel = params.path;
3648
+ let fromLine = 1;
3649
+ if (params.cursor !== void 0) {
3650
+ const payload = decodeCursor(params.cursor);
3651
+ if (payload === void 0 || payload.t !== "read" || typeof payload.path !== "string" || typeof payload.after !== "number" || payload.path !== params.path) return { error: "invalid cursor: pass the nextCursor of a previous read_file page for the same path" };
3652
+ rel = payload.path;
3653
+ fromLine = payload.after + 1;
3654
+ }
3655
+ const resolved = await resolveWithin(rel, "file");
3656
+ if ("error" in resolved) return resolved;
3657
+ const loaded = await loadTextFile(resolved.abs, resolved.rel);
3658
+ if ("error" in loaded) return loaded;
3659
+ const lines = splitLines(loaded.text);
3660
+ const totalLines = lines.length;
3661
+ if (fromLine > totalLines) return { error: `fromLine ${String(fromLine)} is past the end of '${resolved.rel}' (${String(totalLines)} lines)` };
3662
+ const rendered = [];
3663
+ let used = 0;
3664
+ let toLine = fromLine - 1;
3665
+ for (let index = fromLine - 1; index < totalLines; index += 1) {
3666
+ const row = `${String(index + 1)}: ${lines[index]}`;
3667
+ if (rendered.length > 0 && used + 1 + row.length > readPageChars) break;
3668
+ rendered.push(row);
3669
+ used += (rendered.length > 1 ? 1 : 0) + row.length;
3670
+ toLine = index + 1;
3671
+ }
3672
+ const more = toLine < totalLines;
3673
+ return {
3674
+ path: resolved.rel,
3675
+ totalLines,
3676
+ fromLine,
3677
+ toLine,
3678
+ content: rendered.join("\n"),
3679
+ ...more ? { nextCursor: encodeCursor({
3680
+ t: "read",
3681
+ path: resolved.rel,
3682
+ after: toLine
3683
+ }) } : {}
3684
+ };
3685
+ }
3686
+ }),
3687
+ tool({
3688
+ name: "record_evidence",
3689
+ description: "Record one evidence entry supporting a claim. The citation is VERIFIED at record time: the file must exist under the research root, lines must be a valid 1-based line or range inside it ('12' or '12-40'), and quote (when given) must appear verbatim in the file. Returns { recorded, duplicate, totalEvidence }.",
3690
+ parameters: RECORD_EVIDENCE_SCHEMA,
3691
+ risk: "read",
3692
+ execute: async (input) => {
3693
+ const params = input;
3694
+ if (params.claim.trim().length === 0) return { error: "claim must be a non-empty string" };
3695
+ const resolved = await resolveWithin(params.file, "file");
3696
+ if ("error" in resolved) return resolved;
3697
+ const loaded = await loadTextFile(resolved.abs, resolved.rel);
3698
+ if ("error" in loaded) return loaded;
3699
+ const lines = splitLines(loaded.text);
3700
+ if (params.lines !== void 0) {
3701
+ const match = /^(\d+)(?:-(\d+))?$/u.exec(params.lines);
3702
+ if (match === null) return { error: "lines must be '12' or '12-40' (1-based)" };
3703
+ const from = Number(match[1]);
3704
+ const to = match[2] === void 0 ? from : Number(match[2]);
3705
+ if (from < 1 || to < from || to > lines.length) return { error: `lines '${params.lines}' is outside '${resolved.rel}' (${String(lines.length)} lines)` };
3706
+ }
3707
+ if (params.quote !== void 0 && !loaded.text.includes(params.quote)) return { error: `quote not found verbatim in '${resolved.rel}'; cite what the file actually says` };
3708
+ const entry = {
3709
+ claim: params.claim,
3710
+ file: resolved.rel,
3711
+ ...params.lines === void 0 ? {} : { lines: params.lines },
3712
+ ...params.quote === void 0 ? {} : { quote: params.quote }
3713
+ };
3714
+ const duplicate = evidence.some((existing) => existing.claim === entry.claim && existing.file === entry.file && existing.lines === entry.lines && existing.quote === entry.quote);
3715
+ if (!duplicate) evidence.push(entry);
3716
+ return {
3717
+ recorded: !duplicate,
3718
+ duplicate,
3719
+ totalEvidence: evidence.length
3720
+ };
3721
+ }
3722
+ }),
3723
+ tool({
3724
+ name: "list_evidence",
3725
+ description: "List the evidence recorded so far, one page at a time in record order. Returns { evidence, totalEvidence, nextCursor? }.",
3726
+ parameters: LIST_EVIDENCE_SCHEMA,
3727
+ risk: "read",
3728
+ execute: (input) => {
3729
+ const params = input;
3730
+ let from = 0;
3731
+ if (params.cursor !== void 0) {
3732
+ const payload = decodeCursor(params.cursor);
3733
+ if (payload === void 0 || payload.t !== "evidence" || typeof payload.after !== "number") return Promise.resolve({ error: "invalid cursor: pass the nextCursor of a previous list_evidence page" });
3734
+ from = payload.after;
3735
+ }
3736
+ const page = evidence.slice(from, from + pageSize);
3737
+ const more = from + pageSize < evidence.length;
3738
+ return Promise.resolve({
3739
+ evidence: page,
3740
+ totalEvidence: evidence.length,
3741
+ ...more ? { nextCursor: encodeCursor({
3742
+ t: "evidence",
3743
+ after: from + pageSize
3744
+ }) } : {}
3745
+ });
3746
+ }
3747
+ })
3748
+ ],
3749
+ evidence: () => evidence.map((entry) => ({ ...entry }))
3750
+ };
3751
+ }
3752
+ //#endregion
3300
3753
  //#region src/journal/identity.ts
3301
3754
  /**
3302
3755
  * Content-addressed entry identity (M1-T04): IdentityInput records per
@@ -13265,6 +13718,52 @@ async function executeWorkflow(internals, wf, args) {
13265
13718
  }
13266
13719
  }
13267
13720
  //#endregion
13721
+ //#region src/orchestrator/claims.ts
13722
+ /** The conservative matching key: trim plus inner-whitespace collapse. */
13723
+ function claimKey(line) {
13724
+ return line.trim().replace(/\s+/gu, " ");
13725
+ }
13726
+ /**
13727
+ * Removes later occurrences of repeated claim lines across the rows and
13728
+ * indexes each repeated claim with its reporters. Deterministic: output
13729
+ * depends only on the input order and bytes.
13730
+ */
13731
+ function dedupeRepeatedClaims(rows) {
13732
+ const seen = /* @__PURE__ */ new Map();
13733
+ const order = [];
13734
+ return {
13735
+ rows: rows.map((row) => {
13736
+ const kept = [];
13737
+ for (const line of row.text.split("\n")) {
13738
+ const key = claimKey(line);
13739
+ if (key === "") {
13740
+ kept.push(line);
13741
+ continue;
13742
+ }
13743
+ const prior = seen.get(key);
13744
+ if (prior === void 0) {
13745
+ const entry = {
13746
+ claim: line,
13747
+ nodeIds: [row.nodeId],
13748
+ count: 1
13749
+ };
13750
+ seen.set(key, entry);
13751
+ order.push(entry);
13752
+ kept.push(line);
13753
+ continue;
13754
+ }
13755
+ prior.count += 1;
13756
+ if (!prior.nodeIds.includes(row.nodeId)) prior.nodeIds.push(row.nodeId);
13757
+ }
13758
+ return {
13759
+ nodeId: row.nodeId,
13760
+ text: kept.join("\n")
13761
+ };
13762
+ }),
13763
+ repeated: order.filter((entry) => entry.count > 1)
13764
+ };
13765
+ }
13766
+ //#endregion
13268
13767
  //#region src/orchestrator/orchestrate.ts
13269
13768
  /**
13270
13769
  * The mode (c) dynamic orchestrator (M6-T07/T08).
@@ -13286,6 +13785,17 @@ async function executeWorkflow(internals, wf, args) {
13286
13785
  */
13287
13786
  /** How many rejected finishes are repaired by default: the plan's repair once. */
13288
13787
  const DEFAULT_FINISH_MAX_REPAIRS = 1;
13788
+ /**
13789
+ * Default maxTurns of the synthesize invocation (RV-211): the finish
13790
+ * call plus headroom for one validator repair exchange.
13791
+ */
13792
+ const DEFAULT_SYNTHESIS_MAX_TURNS = 4;
13793
+ /**
13794
+ * Default maxTurns of ONE incremental synthesis note (RV-211 remainder):
13795
+ * a note summarizes a single settled child into a bounded finish call,
13796
+ * so it needs less headroom than the full synthesis invocation.
13797
+ */
13798
+ const DEFAULT_SYNTHESIS_NOTE_MAX_TURNS = 2;
13289
13799
  const ORCHESTRATE_WORKFLOW_NAME = "rulvar-orchestrate";
13290
13800
  /**
13291
13801
  * One page of a string, for the child result evidence tools: maxChars is
@@ -13343,6 +13853,23 @@ function validateOrchestrateOptions(opts) {
13343
13853
  }
13344
13854
  if (fv.maxRepairs !== void 0) requireNonNegativeInteger(fv.maxRepairs, "orchestrate finishValidation.maxRepairs");
13345
13855
  }
13856
+ if (opts.synthesis !== void 0) {
13857
+ const synthesis = opts.synthesis;
13858
+ if (synthesis.mode !== void 0 && synthesis.mode !== "single" && synthesis.mode !== "incremental") throw new ConfigError("orchestrate synthesis.mode must be 'single' or 'incremental'; got " + JSON.stringify(synthesis.mode));
13859
+ if (synthesis.mode === "incremental" && opts.finishValidation !== void 0) throw new ConfigError("orchestrate synthesis.mode 'incremental' reconciles deterministically and has no model-composed final finish for finishValidation to bind; configure validators with mode 'single', or drop them");
13860
+ if (synthesis.dedupeClaims !== void 0 && typeof synthesis.dedupeClaims !== "boolean") throw new ConfigError("orchestrate synthesis.dedupeClaims must be a boolean; got " + typeof synthesis.dedupeClaims);
13861
+ if (synthesis.noteLimits !== void 0) validateUsageLimits(synthesis.noteLimits, "orchestrate synthesis.noteLimits");
13862
+ if (synthesis.effort !== void 0 && ![
13863
+ "low",
13864
+ "medium",
13865
+ "high",
13866
+ "xhigh",
13867
+ "max"
13868
+ ].includes(synthesis.effort)) throw new ConfigError(`orchestrate synthesis.effort must be one of 'low' | 'medium' | 'high' | 'xhigh' | 'max'; got ${JSON.stringify(synthesis.effort)}`);
13869
+ if (synthesis.limits !== void 0) validateUsageLimits(synthesis.limits, "orchestrate synthesis.limits");
13870
+ if (synthesis.instructions !== void 0 && typeof synthesis.instructions !== "string") throw new ConfigError(`orchestrate synthesis.instructions must be a string; got ${typeof synthesis.instructions}`);
13871
+ if (synthesis.estCost !== void 0) requireNonNegativeNumber(synthesis.estCost, "orchestrate synthesis.estCost");
13872
+ }
13346
13873
  const spec = opts.budget;
13347
13874
  if (spec === void 0) return;
13348
13875
  if (spec.capUsd !== void 0) requireNonNegativeNumber(spec.capUsd, "orchestrate budget.capUsd");
@@ -13533,6 +14060,22 @@ function makeOrchestratorWorkflow(goal, opts) {
13533
14060
  const recoveryDone = new Promise((resolve) => {
13534
14061
  releaseRecovery = resolve;
13535
14062
  });
14063
+ /**
14064
+ * Incremental synthesis notes (RV-211 remainder): one bounded
14065
+ * synthesize-role invocation per settled child, keyed by nodeId so a
14066
+ * note can never double-dispatch. The settle hook fires the note the
14067
+ * moment its child settles (overlapping the still-running fan-out);
14068
+ * the deterministic reconciliation is the completeness backstop and
14069
+ * dispatches any note the hook missed. The dispatcher installs right
14070
+ * before the coordination loop because it closes over runtime pieces
14071
+ * built below; these bindings are declared HERE, before
14072
+ * dispatchChild, so a recovered child's settle hook (which can fire
14073
+ * during the recovery scan) never touches a binding in its temporal
14074
+ * dead zone.
14075
+ */
14076
+ const synthesisNotes = /* @__PURE__ */ new Map();
14077
+ let synthesisNoteDispatcher;
14078
+ let synthesisSettleFrozen = false;
13536
14079
  let activityChain = Promise.resolve();
13537
14080
  const childScopeOf = () => {
13538
14081
  if (orchSeq === void 0) throw new ConfigError("orchestrator dispatch seq unknown before the loop started");
@@ -13618,6 +14161,7 @@ function makeOrchestratorWorkflow(goal, opts) {
13618
14161
  };
13619
14162
  settledResult.then(async (settled) => {
13620
14163
  record.settled = settled;
14164
+ if (!synthesisSettleFrozen) synthesisNoteDispatcher?.(record);
13621
14165
  await runExtensionActivity();
13622
14166
  for (const listener of [...settleListeners]) listener();
13623
14167
  });
@@ -14330,7 +14874,7 @@ function makeOrchestratorWorkflow(goal, opts) {
14330
14874
  },
14331
14875
  [kTerminalTool]: {
14332
14876
  name: FINISH_TOOL_NAME,
14333
- ...validationSpec === void 0 ? {} : { validate: validateFinish }
14877
+ ...validationSpec === void 0 || opts?.synthesis !== void 0 ? {} : { validate: validateFinish }
14334
14878
  },
14335
14879
  ...(() => {
14336
14880
  const priorCancelledRoot = internals.replayer.snapshot().filter((entry) => entry.kind === "agent" && entry.scope === callingState.scope && entry.status === "cancelled" && entry.checkpointRef !== void 0).at(-1);
@@ -14407,6 +14951,262 @@ function makeOrchestratorWorkflow(goal, opts) {
14407
14951
  };
14408
14952
  };
14409
14953
  /**
14954
+ * One incremental synthesis note (RV-211 remainder): a FRESH agent
14955
+ * entry with role 'synthesize' on the finish-only toolset whose
14956
+ * prompt derives deterministically from the goal and the ONE settled
14957
+ * child's digest, so a resume replays it by identity with zero paid
14958
+ * calls. The invocation itself never throws out of here: an infra
14959
+ * failure settles as a synthesized error result and the
14960
+ * reconciliation falls back to the raw digest summary.
14961
+ */
14962
+ const runSynthesisNote = async (record) => {
14963
+ const spec = opts?.synthesis;
14964
+ const finishOnly = buildOrchestratorTools(orchestratorRuntime, fullCardText).filter((tool) => tool.name === FINISH_TOOL_NAME);
14965
+ const digest = digestOf(record, record.settled);
14966
+ const prompt = [
14967
+ "You are an incremental synthesis note of an orchestrated run. Digest the SINGLE settled child below into a self-contained note for the final deterministic reconciliation by calling finish({ result }) EXACTLY once, where result is a STRING. Preserve concrete evidence and citations; do not invent findings. No other tool exists.",
14968
+ ...spec.instructions === void 0 ? [] : [spec.instructions],
14969
+ `GOAL: ${goal}`,
14970
+ `CHILD: ${JSON.stringify(digest)}`
14971
+ ].join("\n");
14972
+ const noteState = { ...callingState };
14973
+ if (orchestratorAccount !== void 0) noteState.budgetScope = orchestratorAccount;
14974
+ const noteOpts = {
14975
+ role: "synthesize",
14976
+ result: "full",
14977
+ tools: finishOnly,
14978
+ limits: spec.noteLimits ?? { maxTurns: 2 },
14979
+ ...spec.model === void 0 ? {} : { model: spec.model },
14980
+ ...spec.effort === void 0 ? {} : { effort: spec.effort },
14981
+ ...spec.estCost === void 0 ? {} : { estCost: spec.estCost },
14982
+ [kTerminalTool]: { name: FINISH_TOOL_NAME }
14983
+ };
14984
+ return runtime.runInScope(noteState, () => ctx.agent(prompt, noteOpts));
14985
+ };
14986
+ /**
14987
+ * Note dispatch is idempotent per child: the settle hook and the
14988
+ * reconciliation both come through here, and the map guarantees one
14989
+ * dispatch per nodeId (a concurrent identical dispatch would mint a
14990
+ * second occurrence and PAY twice: the v1.32.0 lesson).
14991
+ */
14992
+ const ensureSynthesisNote = (record) => {
14993
+ const existing = synthesisNotes.get(record.nodeId);
14994
+ if (existing !== void 0) return existing;
14995
+ const note = runSynthesisNote(record).catch((thrown) => ({
14996
+ status: "error",
14997
+ output: null,
14998
+ usage: {
14999
+ inputTokens: 0,
15000
+ outputTokens: 0,
15001
+ cacheReadTokens: 0,
15002
+ cacheWriteTokens: 0
15003
+ },
15004
+ costUsd: 0,
15005
+ turns: 0,
15006
+ servedBy: "unknown:unknown",
15007
+ transcriptRef: "",
15008
+ errorMessage: thrown instanceof Error ? thrown.message : String(thrown)
15009
+ }));
15010
+ synthesisNotes.set(record.nodeId, note);
15011
+ return note;
15012
+ };
15013
+ if (opts?.synthesis?.mode === "incremental" && capDecisionRef === void 0) synthesisNoteDispatcher = ensureSynthesisNote;
15014
+ /**
15015
+ * The deterministic reconciliation of 'incremental' synthesis: the
15016
+ * final result is a PURE fold of the journaled draft and the note
15017
+ * results in spawn order, never another model call. A note that died
15018
+ * falls back to the child's raw digest summary under a journaled
15019
+ * per-child decision and a warn log. With dedupeClaims, repeated
15020
+ * claim lines keep their first occurrence and the envelope carries
15021
+ * the repeatedClaims index.
15022
+ */
15023
+ const reconcileIncremental = async (draft, spec) => {
15024
+ synthesisSettleFrozen = true;
15025
+ const settledRecords = [...records.values()].filter((record) => record.settled !== void 0).sort((a, b) => a.spawnOrdinal - b.spawnOrdinal);
15026
+ const sections = [];
15027
+ for (const record of settledRecords) {
15028
+ const settled = record.settled;
15029
+ const note = await ensureSynthesisNote(record);
15030
+ let noteText;
15031
+ if (note.status === "ok") noteText = typeof note.output === "string" ? note.output : JSON.stringify(note.output ?? null);
15032
+ else {
15033
+ const fallbackKey = deriverV2.deriveKey({
15034
+ kind: "orchestrator-synthesis-note-fallback",
15035
+ nodeId: record.nodeId
15036
+ });
15037
+ if (!internals.replayer.snapshot().some((entry) => entry.kind === "decision" && entry.key === fallbackKey)) await internals.replayer.appendSinglePhase({
15038
+ scope: callingState.scope,
15039
+ key: fallbackKey,
15040
+ kind: "decision",
15041
+ status: "ok",
15042
+ spanId: internals.spans.mint(callingState.spanId),
15043
+ site: "orchestrator-synthesis",
15044
+ value: {
15045
+ decisionType: "orchestrator_synthesis_note_fallback",
15046
+ nodeId: record.nodeId,
15047
+ status: note.status,
15048
+ turnsUsed: note.turns
15049
+ }
15050
+ });
15051
+ internals.events.emit({
15052
+ type: "log",
15053
+ level: "warn",
15054
+ msg: `the synthesis note for child '${record.nodeId}' terminated with status '${note.status}'; falling back to the raw digest summary (journaled decision 'orchestrator_synthesis_note_fallback')`
15055
+ }, callingState.spanId);
15056
+ noteText = digestOf(record, settled).outputSummary;
15057
+ }
15058
+ sections.push({
15059
+ nodeId: record.nodeId,
15060
+ logicalTaskId: record.logicalTaskId,
15061
+ status: settled.status,
15062
+ noteStatus: note.status,
15063
+ note: noteText
15064
+ });
15065
+ }
15066
+ let repeatedClaims;
15067
+ if (spec.dedupeClaims === true) {
15068
+ const deduped = dedupeRepeatedClaims(sections.map((section) => ({
15069
+ nodeId: section.nodeId,
15070
+ text: section.note
15071
+ })));
15072
+ const textByNode = new Map(deduped.rows.map((row) => [row.nodeId, row.text]));
15073
+ for (const section of sections) section.note = textByNode.get(section.nodeId) ?? section.note;
15074
+ repeatedClaims = deduped.repeated;
15075
+ }
15076
+ const draftJson = JSON.stringify(draft ?? null);
15077
+ internals.events.emit({
15078
+ type: "log",
15079
+ level: "debug",
15080
+ msg: "orchestrator synthesis reconciliation",
15081
+ data: {
15082
+ children: sections.length,
15083
+ draftChars: draftJson.length,
15084
+ notesChars: sections.reduce((sum, section) => sum + section.note.length, 0),
15085
+ perChild: sections.map((section) => ({
15086
+ nodeId: section.nodeId,
15087
+ chars: section.note.length
15088
+ })),
15089
+ ...repeatedClaims === void 0 ? {} : { repeatedClaims: repeatedClaims.length }
15090
+ }
15091
+ }, callingState.spanId);
15092
+ return {
15093
+ synthesis: "incremental",
15094
+ draft,
15095
+ sections,
15096
+ ...repeatedClaims === void 0 ? {} : { repeatedClaims }
15097
+ };
15098
+ };
15099
+ /**
15100
+ * The post-fan-in synthesis invocation (RV-211): a FRESH agent entry
15101
+ * with role 'synthesize' on the finish-only toolset (a distinct
15102
+ * toolsetHash, the reserved-finalizer precedent), its prompt derived
15103
+ * deterministically from the goal, the journaled coordination draft,
15104
+ * and the settled child digest, so a resume replays it by identity
15105
+ * with zero paid calls. Runs strictly AFTER the acceptance verdict
15106
+ * (a rejected run never pays for synthesis; in 'incremental' mode
15107
+ * the per-child notes are paid DURING the run, so only the
15108
+ * reconciliation itself is deferred) and owns the finish validators
15109
+ * when they are configured. Failure posture: with validators the run
15110
+ * fails typed (the validated path is mandatory); without them the
15111
+ * run falls back to the draft under a journaled decision and a warn
15112
+ * log, never silently.
15113
+ */
15114
+ const runSynthesis = async (draft) => {
15115
+ const spec = opts?.synthesis;
15116
+ if (spec === void 0) return draft;
15117
+ await recoveryDone;
15118
+ if (spec.mode === "incremental") return await reconcileIncremental(draft, spec);
15119
+ const finishOnly = buildOrchestratorTools(orchestratorRuntime, fullCardText).filter((tool) => tool.name === FINISH_TOOL_NAME);
15120
+ const settledDigests = [...records.values()].filter((record) => record.settled !== void 0).sort((a, b) => a.spawnOrdinal - b.spawnOrdinal).map((record) => digestOf(record, record.settled));
15121
+ let digestRows = settledDigests;
15122
+ let repeatedClaims;
15123
+ if (spec.dedupeClaims === true) {
15124
+ const deduped = dedupeRepeatedClaims(settledDigests.map((row) => ({
15125
+ nodeId: row.nodeId,
15126
+ text: row.outputSummary
15127
+ })));
15128
+ const textByNode = new Map(deduped.rows.map((row) => [row.nodeId, row.text]));
15129
+ digestRows = settledDigests.map((row) => ({
15130
+ ...row,
15131
+ outputSummary: textByNode.get(row.nodeId) ?? row.outputSummary
15132
+ }));
15133
+ repeatedClaims = deduped.repeated;
15134
+ }
15135
+ const draftJson = JSON.stringify(draft ?? null);
15136
+ const digestJson = JSON.stringify(digestRows);
15137
+ const prompt = [
15138
+ "You are the synthesis invocation of an orchestrated run. Compose the FINAL result of the run from the goal, the coordination draft, and the settled child evidence below by calling finish({ result }) EXACTLY once. Preserve the evidence and citations the draft relies on; do not invent findings. No other tool exists.",
15139
+ ...repeatedClaims === void 0 ? [] : ["Repeated claims across children were deduplicated before this prompt: only the first occurrence of each repeated line remains in the digest, and the REPEATED CLAIMS index below lists each one with its reporters."],
15140
+ ...spec.instructions === void 0 ? [] : [spec.instructions],
15141
+ ...finishValidationPromptLines(validationSpec),
15142
+ `GOAL: ${goal}`,
15143
+ `DRAFT: ${draftJson}`,
15144
+ `DIGEST: ${digestJson}`,
15145
+ ...repeatedClaims === void 0 ? [] : [`REPEATED CLAIMS: ${JSON.stringify(repeatedClaims)}`]
15146
+ ].join("\n");
15147
+ internals.events.emit({
15148
+ type: "log",
15149
+ level: "debug",
15150
+ msg: "orchestrator synthesis context",
15151
+ data: {
15152
+ children: settledDigests.length,
15153
+ draftChars: draftJson.length,
15154
+ digestChars: digestJson.length,
15155
+ promptChars: prompt.length,
15156
+ perChild: digestRows.map((entry) => ({
15157
+ nodeId: entry.nodeId,
15158
+ chars: JSON.stringify(entry).length
15159
+ })),
15160
+ ...repeatedClaims === void 0 ? {} : { repeatedClaims: repeatedClaims.length }
15161
+ }
15162
+ }, callingState.spanId);
15163
+ const synthesisState = { ...callingState };
15164
+ if (orchestratorAccount !== void 0) synthesisState.budgetScope = orchestratorAccount;
15165
+ const synthesisBreak = validationSpec === void 0 ? void 0 : validationAbort.signal;
15166
+ if (synthesisBreak !== void 0) synthesisState.signal = callingState.signal === void 0 ? synthesisBreak : AbortSignal.any([callingState.signal, synthesisBreak]);
15167
+ const synthesisOpts = {
15168
+ role: "synthesize",
15169
+ result: "full",
15170
+ tools: finishOnly,
15171
+ limits: spec.limits ?? { maxTurns: 4 },
15172
+ ...spec.model === void 0 ? {} : { model: spec.model },
15173
+ ...spec.effort === void 0 ? {} : { effort: spec.effort },
15174
+ ...spec.estCost === void 0 ? {} : { estCost: spec.estCost },
15175
+ [kTerminalTool]: {
15176
+ name: FINISH_TOOL_NAME,
15177
+ ...validationSpec === void 0 ? {} : { validate: validateFinish }
15178
+ }
15179
+ };
15180
+ const synthesized = await runtime.runInScope(synthesisState, () => ctx.agent(prompt, synthesisOpts));
15181
+ if (validationTermination !== void 0) throw validationTermination;
15182
+ if (synthesized.status === "ok") return synthesized.output;
15183
+ if (validationSpec !== void 0) throw new FailRunError(`the synthesis invocation terminated with status '${synthesized.status}'` + (synthesized.errorMessage === void 0 ? "" : `: ${synthesized.errorMessage}`) + "; finish validators are configured, so the unvalidated draft cannot stand", { data: {
15184
+ source: "orchestrator_synthesis",
15185
+ status: synthesized.status,
15186
+ turnsUsed: synthesized.turns
15187
+ } });
15188
+ const fallbackKey = deriverV2.deriveKey({ kind: "orchestrator-synthesis-fallback" });
15189
+ if (!internals.replayer.snapshot().some((entry) => entry.kind === "decision" && entry.key === fallbackKey)) await internals.replayer.appendSinglePhase({
15190
+ scope: callingState.scope,
15191
+ key: fallbackKey,
15192
+ kind: "decision",
15193
+ status: "ok",
15194
+ spanId: internals.spans.mint(callingState.spanId),
15195
+ site: "orchestrator-synthesis",
15196
+ value: {
15197
+ decisionType: "orchestrator_synthesis_fallback",
15198
+ status: synthesized.status,
15199
+ turnsUsed: synthesized.turns
15200
+ }
15201
+ });
15202
+ internals.events.emit({
15203
+ type: "log",
15204
+ level: "warn",
15205
+ msg: `the synthesis invocation terminated with status '${synthesized.status}'; falling back to the coordination draft (journaled decision 'orchestrator_synthesis_fallback')`
15206
+ }, callingState.spanId);
15207
+ return draft;
15208
+ };
15209
+ /**
14410
15210
  * The settle at the cap: the JOURNALED cap decision drives the policy
14411
15211
  * branch (its `fallback` field froze budget.atCap when the cap
14412
15212
  * tripped), so a crash between the decision and its effect rolls the
@@ -14440,7 +15240,7 @@ function makeOrchestratorWorkflow(goal, opts) {
14440
15240
  if (validationTermination !== void 0) throw validationTermination;
14441
15241
  if (orchestratorAccount !== void 0) internals.cost.orchestrator.spentUsd = internals.budget.accountView(orchestratorAccount)?.spentUsd ?? 0;
14442
15242
  if (result.status !== "ok") throw new ConfigError(`the orchestrator agent terminated with status '${result.status}'` + (result.errorMessage === void 0 ? "" : `: ${result.errorMessage}`));
14443
- if (opts?.acceptance === void 0) return result.output;
15243
+ if (opts?.acceptance === void 0) return await runSynthesis(result.output);
14444
15244
  const acceptanceKey = "acceptance";
14445
15245
  const priorAcceptance = internals.replayer.snapshot().find((entry) => entry.kind === "decision" && entry.scope === callingState.scope && entry.key === acceptanceKey);
14446
15246
  let decision;
@@ -14478,13 +15278,14 @@ function makeOrchestratorWorkflow(goal, opts) {
14478
15278
  const required = decision.childPolicy === "all-ok" ? "every child ok" : `at least ${String(decision.childPolicy.minSuccessful)} children ok`;
14479
15279
  throw new FailRunError(`the orchestrator acceptance policy rejected the finish: ${String(decision.childStatusCounts.ok ?? 0)} children settled 'ok' but the policy requires ${required}; degraded: ${decision.degradedReasons.join("; ")}`, { data: {
14480
15280
  source: "orchestrator_acceptance",
15281
+ completion: "rejected",
14481
15282
  childPolicy: decision.childPolicy,
14482
15283
  childStatusCounts: decision.childStatusCounts,
14483
15284
  degradedReasons: decision.degradedReasons
14484
15285
  } });
14485
15286
  }
14486
15287
  return {
14487
- result: result.output,
15288
+ result: await runSynthesis(result.output),
14488
15289
  completion: decision.completion,
14489
15290
  childStatusCounts: decision.childStatusCounts,
14490
15291
  degradedReasons: decision.degradedReasons
@@ -14806,6 +15607,54 @@ function reduceInvocationTable(events) {
14806
15607
  totalCostUsd
14807
15608
  };
14808
15609
  }
15610
+ function reduceCriticalPath(events) {
15611
+ let runStart;
15612
+ let runEnd;
15613
+ const startBySpan = /* @__PURE__ */ new Map();
15614
+ let lastWorkerEnd;
15615
+ let workerSpans = 0;
15616
+ let synthesisMs = 0;
15617
+ for (const event of events) {
15618
+ const at = Date.parse(event.ts);
15619
+ if (!Number.isFinite(at)) continue;
15620
+ switch (event.type) {
15621
+ case "run:start":
15622
+ runStart ??= at;
15623
+ break;
15624
+ case "run:end":
15625
+ runEnd = at;
15626
+ break;
15627
+ case "agent:start":
15628
+ startBySpan.set(event.spanId, {
15629
+ role: event.role,
15630
+ at
15631
+ });
15632
+ break;
15633
+ case "agent:end": {
15634
+ const started = startBySpan.get(event.spanId);
15635
+ if (started === void 0) break;
15636
+ if (started.role === "synthesize") synthesisMs += Math.max(0, at - started.at);
15637
+ else if (started.role !== "orchestrate") {
15638
+ workerSpans += 1;
15639
+ lastWorkerEnd = lastWorkerEnd === void 0 ? at : Math.max(lastWorkerEnd, at);
15640
+ }
15641
+ break;
15642
+ }
15643
+ default: break;
15644
+ }
15645
+ }
15646
+ const path = {
15647
+ synthesisMs,
15648
+ workerSpans
15649
+ };
15650
+ if (runStart !== void 0 && runEnd !== void 0) path.runWallMs = Math.max(0, runEnd - runStart);
15651
+ if (runEnd !== void 0 && lastWorkerEnd !== void 0) path.postFanInMs = Math.max(0, runEnd - lastWorkerEnd);
15652
+ if (path.runWallMs !== void 0 && path.runWallMs > 0) {
15653
+ if (path.postFanInMs !== void 0) path.postFanInShare = path.postFanInMs / path.runWallMs;
15654
+ path.synthesisShare = synthesisMs / path.runWallMs;
15655
+ }
15656
+ return path;
15657
+ }
14809
15658
  //#endregion
14810
15659
  //#region src/l0/run-id.ts
14811
15660
  /**
@@ -15087,6 +15936,32 @@ function workflowSourceRef(runId) {
15087
15936
  return `${runId}/workflow-source`;
15088
15937
  }
15089
15938
  /**
15939
+ * The completion envelope contract (RV-207 tail): a workflow reports
15940
+ * SEMANTIC completion by returning an object result carrying a
15941
+ * `completion` literal (and optionally `childStatusCounts`), or by
15942
+ * throwing a typed RulvarError whose `data` carries them; the engine
15943
+ * lifts the validated fields onto the `run:end` event so telemetry
15944
+ * consumers read completeness without parsing workflow-specific result
15945
+ * shapes. The orchestrator acceptance path emits this envelope. Pure
15946
+ * shape validation: anything malformed is silently absent (the event is
15947
+ * telemetry, never authority), and an invalid counts record drops the
15948
+ * counts while keeping a valid completion.
15949
+ */
15950
+ function liftRunCompletion(candidate) {
15951
+ if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return;
15952
+ const completion = candidate.completion;
15953
+ if (completion !== "complete" && completion !== "partial" && completion !== "rejected") return;
15954
+ const counts = candidate.childStatusCounts;
15955
+ if (typeof counts === "object" && counts !== null && !Array.isArray(counts)) {
15956
+ const entries = Object.entries(counts);
15957
+ if (entries.every(([, value]) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0)) return {
15958
+ completion,
15959
+ childStatusCounts: Object.fromEntries(entries)
15960
+ };
15961
+ }
15962
+ return { completion };
15963
+ }
15964
+ /**
15090
15965
  * sha256 hex over the JCS canonical serialization of a run's args: the
15091
15966
  * value the engine records as `RunMeta.argsHash` at genesis, exposed so
15092
15967
  * hosts can verify re-supplied resume args against the recorded hash
@@ -15457,11 +16332,13 @@ function createEngine(options) {
15457
16332
  }
15458
16333
  }
15459
16334
  await putMeta(status).catch(() => void 0);
16335
+ const lifted = liftRunCompletion(status === "ok" || status === "exhausted" ? outcome.value : status === "error" ? wireError?.data : void 0);
15460
16336
  bus.emit({
15461
16337
  type: "run:end",
15462
16338
  status,
15463
16339
  totalUsd: ledger.usd,
15464
- ...outcome.cost.usageApprox === true ? { usageApprox: true } : {}
16340
+ ...outcome.cost.usageApprox === true ? { usageApprox: true } : {},
16341
+ ...lifted === void 0 ? {} : lifted
15465
16342
  }, rootSpanId);
15466
16343
  bus.end();
15467
16344
  resumeCtx?.previewResolve({
@@ -15933,4 +16810,4 @@ function createSandboxBridge(ctx, options) {
15933
16810
  };
15934
16811
  }
15935
16812
  //#endregion
15936
- export { AWAIT_SCHEMA, AdmissionController, AdmissionRejectedError, AgentCallError, BUDGET_ABORT_REASON, BudgetExhaustedError, CANCEL_AGENT_SCHEMA, CHECKPOINT_FORMAT_V1, CLAIM_STATEMENT_MAX_CHARS, CLAIM_TTL_DAYS, COMPACTION_SUMMARY_PREFIX, CURRENT_HASH_VERSION, ConfigError, DEFAULT_CHILD_BUDGET_FRACTION, DEFAULT_CHILD_RESULT_PAGE_CHARS, DEFAULT_CITATION_PATTERN, DEFAULT_COMPACTION_THRESHOLD, DEFAULT_ESCALATION_LIMITS, DEFAULT_EVIDENCE_MIN_SHARE, DEFAULT_FINISH_MAX_REPAIRS, DEFAULT_FLAT_RESERVE_USD, DEFAULT_MAX_CHILDREN_PER_NODE, DEFAULT_MAX_DEPTH, DEFAULT_MAX_OSCILLATIONS_PER_KEY, DEFAULT_MAX_PINNED_WORKTREES, DEFAULT_MAX_REVISIONS_PER_RUN, DEFAULT_MAX_TOTAL_SPAWNS, DEFAULT_MAX_TURNS, DEFAULT_MODEL_RETRY_ATTEMPTS, DEFAULT_NO_PROGRESS_TURNS, DEFAULT_PER_RUN_CONCURRENCY, DEFAULT_RETRY_POLICY, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DedupIndex, DeterminismError, EMIT_RESULT_TOOL, EMPTY_SCHEMA_HASH, EMPTY_TOOLSET_HASH, ESCALATE_TOOL_NAME, ESCALATION_REPORT_SCHEMA, ESCALATION_REQUEST_SCHEMA, EVENT_SEGMENT_STRIDE, EscalationDecisionAbortedError, EventBus, ExternalRegistry, FINALIZE_SYNTHESIS_INSTRUCTION, FINISH_SCHEMA, FINISH_TOOL_NAME, FailRunError, FileModelKnowledgeStore, FileTranscriptStore, GET_CHILD_RESULT_SCHEMA, GET_CHILD_RESULT_TOOL_NAME, GitWorktreeProvider, INBOX_PROPOSAL_TTL_DAYS, InMemoryStore, InMemoryTranscriptStore, InProcessRunner, InvalidResolutionError, JournalCompatibilityError, JournalMatcher, JournalMissError, JournalOrderViolation, JsonlFileStore, KB_ACTIVE_CLAIMS_CAP, KB_CARD_RENDER_BUDGET_CHARS, KeyedLimiter, KnowledgeCasError, LARGE_VALUE_WARN_BYTES, LEGACY_LTID_PREFIX, LEGACY_SIGNATURE_INPUTS, LINEAGE_SIG_VERSION, LeaseHeldError, LineageIndex, MASKED_SECRET, MAX_CHILD_RESULT_PAGE_CHARS, MAX_DEPTH_CEILING, ModelRetry, NoProgressDetector, NonSerializableValueError, ORCHESTRATE_WORKFLOW_NAME, OrchestratorCapConfigError, PARALLEL_AGENTS_SCHEMA, ParallelSiteCounter, PlanInvariantError, READ_CHILD_ARTIFACT_SCHEMA, READ_CHILD_ARTIFACT_TOOL_NAME, ROLE_EFFORT_DEFAULTS, ROOT_ACCOUNT, ROOT_SCOPE, RUN_PROFILES, RUN_SETTLE_DECISION_TYPE, ReplayPlanHashMismatch, Replayer, ResolutionArbiter, ResolutionFold, RulvarError, RunBudget, SANDBOX_AGENT_OPT_KEYS, SPAWN_AGENT_SCHEMA, SandboxError, ScriptRejected, Semaphore, SpanRegistry, TOOL_NAME_PATTERN, TerminationAccount, WAIT_FOR_EVENTS_SCHEMA, WAIT_FOR_EVENTS_TOOL_NAME, WAKE_SUMMARY_RENDER_BUDGET_CHARS, admissionReserveUsd, affordableOutputTokens, agentErrorFromWire, agentErrorToWire, agentResultWire, agentScope, applyClaimOps, applyStructuredOutputTier, approachSigCoarse, approachSigOf, archiveDeprecatedModelOps, assertFencedWrites, atCompactionThreshold, auditRun, auditRuns, buildAbandonFold, buildAdapterRegistry, buildCostReport, buildDeriverRegistry, buildOrchestratorTools, buildTerminationInitValue, buildToolContext, canRideLoopTurn, canonicalIsolationTag, canonicalizeLadder, canonicalizeSchema, capIssues, capsHashOf, checkFloors, checkpointRefFor, childCoveragePrefix, claimExpired, claimExpiry, claimIssues, claimOpIssues, classifyAgentError, classifyAttemptOutcome, collectDeclaredLadders, compactMessages, compilePermissionChain, compilePermissionPreset, compileVerifiedLayer, costReportFromJournal, countsAgainstLimit, createCanonicalIdMinter, createCtx, createEngine, createSandboxBridge, currentOnlyKeyRing, decodeCheckpoint, defineWorkflow, deriveContentKey, deriverV1, deriverV2, digestOf, dispositionHook, emptyDigestBlocks, emptyToolset, encodeCheckpoint, entryUsageSlices, escalateTool, evaluatePermission, evaluateReuse, evidencePreservedValidator, executeWorkflow, exhaustionCodeOf, extractCandidate, failoverTriggerOf, fallbackTriggerOf, filterClaimsForRun, finalizeFires, foldTermination, formatRePrompt, formatScopePath, hasFencedWrites, hasMetaLookup, hashRunArgs, hashRunOutput, hashWorkflowBody, hashWorkflowSource, identityJcs, isEscalated, isSchemaPairSpec, isStandardSchemaSpec, isStrictCompatibleSchema, kMaxOf, knowledgeHash, ladderLengthOf, ladderRungChoice, lastRunSettle, lexShellCommand, liftRetainedParts, lineageWeightOf, makeOrchestratorWorkflow, maskSecrets, maskSecretsDeep, maskSecretsJson, matchArgvPattern, matchShellCommand, mcp, mergeUsageLimits, metaMatchesFilter, minMatchesValidator, modelEpochOf, modelKnowledgeCard, modelSpecIdentity, needsSeparateExtract, nextFailover, nodeLinkKey, normalizeApproachTag, normalizeEntry, normalizeFallbacks, orchestrate, parallelScope, parseModelRef, parseScopePath, phiInitialOf, pipelineScope, planNodeScope, priceEntryUsage, priceUsdOf, profileCard, profileRegistrySnapshotHash, projectHistory, projectIdentity, projectToJsonSchema, proposalStatement, providerOf, readRunMeta, readTerminationInit, reconcileRunMeta, reduceInvocationTable, registryKeyRing, remeasureQueue, replayDisposition, requiredFieldsValidator, requiredSectionsValidator, resolveModelInvocation, resolvePricing, resolveToolset, retryClassOf, retryDelayMs, roleConfiguredInRouting, roundOneDisposition, runAgent, runProfile, sanitizeTerminalText, sanitizeTokenCount, sanitizeUsage, sanitizeUsageDelta, scanJournalCompatibility, schemaHash, schemaHashOfSpec, selectStructuredOutputTier, shouldCompact, snapshotUsage, spawnDepthOf, summarizeInstruction, summarizeOutput, terminationConfigDrift, tierWithinCaps, toApprovalDecision, toJournalValue, tool, toolContract, toolsetHash, ttlState, usageViolations, validateEditorialCommit, validateEntryShape, validateEscalationLimits, validateEscalationReport, validateRetryPolicy, validateSchemaSpec, validateTerminationLimits, validateUsageLimits, workflowScope, workflowSourceRef, wrapJournalStore, wrapTranscriptStore };
16813
+ export { AWAIT_SCHEMA, AdmissionController, AdmissionRejectedError, AgentCallError, BUDGET_ABORT_REASON, BudgetExhaustedError, CANCEL_AGENT_SCHEMA, CHECKPOINT_FORMAT_V1, CLAIM_STATEMENT_MAX_CHARS, CLAIM_TTL_DAYS, COMPACTION_SUMMARY_PREFIX, CURRENT_HASH_VERSION, ConfigError, DEFAULT_CHILD_BUDGET_FRACTION, DEFAULT_CHILD_RESULT_PAGE_CHARS, DEFAULT_CITATION_PATTERN, DEFAULT_COMPACTION_THRESHOLD, DEFAULT_ESCALATION_LIMITS, DEFAULT_EVIDENCE_MIN_SHARE, DEFAULT_FINISH_MAX_REPAIRS, DEFAULT_FLAT_RESERVE_USD, DEFAULT_MAX_CHILDREN_PER_NODE, DEFAULT_MAX_DEPTH, DEFAULT_MAX_OSCILLATIONS_PER_KEY, DEFAULT_MAX_PINNED_WORKTREES, DEFAULT_MAX_REVISIONS_PER_RUN, DEFAULT_MAX_TOTAL_SPAWNS, DEFAULT_MAX_TURNS, DEFAULT_MODEL_RETRY_ATTEMPTS, DEFAULT_NO_PROGRESS_TURNS, DEFAULT_PER_RUN_CONCURRENCY, DEFAULT_RETRY_POLICY, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_SYNTHESIS_MAX_TURNS, DEFAULT_SYNTHESIS_NOTE_MAX_TURNS, DedupIndex, DeterminismError, EMIT_RESULT_TOOL, EMPTY_SCHEMA_HASH, EMPTY_TOOLSET_HASH, ESCALATE_TOOL_NAME, ESCALATION_REPORT_SCHEMA, ESCALATION_REQUEST_SCHEMA, EVENT_SEGMENT_STRIDE, EscalationDecisionAbortedError, EventBus, ExternalRegistry, FINALIZE_SYNTHESIS_INSTRUCTION, FINISH_SCHEMA, FINISH_TOOL_NAME, FailRunError, FileModelKnowledgeStore, FileTranscriptStore, GET_CHILD_RESULT_SCHEMA, GET_CHILD_RESULT_TOOL_NAME, GitWorktreeProvider, INBOX_PROPOSAL_TTL_DAYS, InMemoryStore, InMemoryTranscriptStore, InProcessRunner, InvalidResolutionError, JournalCompatibilityError, JournalMatcher, JournalMissError, JournalOrderViolation, JsonlFileStore, KB_ACTIVE_CLAIMS_CAP, KB_CARD_RENDER_BUDGET_CHARS, KeyedLimiter, KnowledgeCasError, LARGE_VALUE_WARN_BYTES, LEGACY_LTID_PREFIX, LEGACY_SIGNATURE_INPUTS, LINEAGE_SIG_VERSION, LeaseHeldError, LineageIndex, MASKED_SECRET, MAX_CHILD_RESULT_PAGE_CHARS, MAX_DEPTH_CEILING, ModelRetry, NoProgressDetector, NonSerializableValueError, ORCHESTRATE_WORKFLOW_NAME, OrchestratorCapConfigError, PARALLEL_AGENTS_SCHEMA, ParallelSiteCounter, PlanInvariantError, READ_CHILD_ARTIFACT_SCHEMA, READ_CHILD_ARTIFACT_TOOL_NAME, ROLE_EFFORT_DEFAULTS, ROOT_ACCOUNT, ROOT_SCOPE, RUN_PROFILES, RUN_SETTLE_DECISION_TYPE, ReplayPlanHashMismatch, Replayer, ResolutionArbiter, ResolutionFold, RulvarError, RunBudget, SANDBOX_AGENT_OPT_KEYS, SPAWN_AGENT_SCHEMA, SandboxError, ScriptRejected, Semaphore, SpanRegistry, TOOL_NAME_PATTERN, TerminationAccount, WAIT_FOR_EVENTS_SCHEMA, WAIT_FOR_EVENTS_TOOL_NAME, WAKE_SUMMARY_RENDER_BUDGET_CHARS, admissionReserveUsd, affordableOutputTokens, agentErrorFromWire, agentErrorToWire, agentResultWire, agentScope, applyClaimOps, applyStructuredOutputTier, approachSigCoarse, approachSigOf, archiveDeprecatedModelOps, assertFencedWrites, atCompactionThreshold, auditRun, auditRuns, buildAbandonFold, buildAdapterRegistry, buildCostReport, buildDeriverRegistry, buildOrchestratorTools, buildTerminationInitValue, buildToolContext, canRideLoopTurn, canonicalIsolationTag, canonicalizeLadder, canonicalizeSchema, capIssues, capsHashOf, checkFloors, checkpointRefFor, childCoveragePrefix, claimExpired, claimExpiry, claimIssues, claimOpIssues, classifyAgentError, classifyAttemptOutcome, collectDeclaredLadders, compactMessages, compilePermissionChain, compilePermissionPreset, compileVerifiedLayer, costReportFromJournal, countsAgainstLimit, createCanonicalIdMinter, createCtx, createEngine, createSandboxBridge, currentOnlyKeyRing, decodeCheckpoint, dedupeRepeatedClaims, defineWorkflow, deriveContentKey, deriverV1, deriverV2, digestOf, dispositionHook, emptyDigestBlocks, emptyToolset, encodeCheckpoint, entryUsageSlices, escalateTool, evaluatePermission, evaluateReuse, evidencePreservedValidator, executeWorkflow, exhaustionCodeOf, extractCandidate, failoverTriggerOf, fallbackTriggerOf, filterClaimsForRun, finalizeFires, foldTermination, formatRePrompt, formatScopePath, hasFencedWrites, hasMetaLookup, hashRunArgs, hashRunOutput, hashWorkflowBody, hashWorkflowSource, identityJcs, isEscalated, isSchemaPairSpec, isStandardSchemaSpec, isStrictCompatibleSchema, kMaxOf, knowledgeHash, ladderLengthOf, ladderRungChoice, lastRunSettle, lexShellCommand, liftRetainedParts, lineageWeightOf, makeOrchestratorWorkflow, maskSecrets, maskSecretsDeep, maskSecretsJson, matchArgvPattern, matchShellCommand, mcp, mergeUsageLimits, metaMatchesFilter, minMatchesValidator, modelEpochOf, modelKnowledgeCard, modelSpecIdentity, needsSeparateExtract, nextFailover, nodeLinkKey, normalizeApproachTag, normalizeEntry, normalizeFallbacks, orchestrate, parallelScope, parseModelRef, parseScopePath, phiInitialOf, pipelineScope, planNodeScope, priceEntryUsage, priceUsdOf, profileCard, profileRegistrySnapshotHash, projectHistory, projectIdentity, projectToJsonSchema, proposalStatement, providerOf, readRunMeta, readTerminationInit, reconcileRunMeta, reduceCriticalPath, reduceInvocationTable, registryKeyRing, remeasureQueue, replayDisposition, repositoryResearchToolset, requiredFieldsValidator, requiredSectionsValidator, resolveModelInvocation, resolvePricing, resolveToolset, retryClassOf, retryDelayMs, roleConfiguredInRouting, roundOneDisposition, runAgent, runProfile, sanitizeTerminalText, sanitizeTokenCount, sanitizeUsage, sanitizeUsageDelta, scanJournalCompatibility, schemaHash, schemaHashOfSpec, selectStructuredOutputTier, shouldCompact, snapshotUsage, spawnDepthOf, summarizeInstruction, summarizeOutput, terminationConfigDrift, tierWithinCaps, toApprovalDecision, toJournalValue, tool, toolContract, toolsetHash, ttlState, usageViolations, validateEditorialCommit, validateEntryShape, validateEscalationLimits, validateEscalationReport, validateRetryPolicy, validateSchemaSpec, validateTerminationLimits, validateUsageLimits, workflowScope, workflowSourceRef, wrapJournalStore, wrapTranscriptStore };