@rulvar/core 1.53.0 → 1.55.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +339 -4
  2. package/dist/index.js +1041 -28
  3. package/package.json +1 -1
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,650 @@ 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
3753
+ //#region src/tools/progress.ts
3754
+ /** The stock progress tool name the engine scans terminals for. */
3755
+ const PROGRESS_REPORT_TOOL_NAME = "report_progress";
3756
+ const PROGRESS_SCHEMA = {
3757
+ type: "object",
3758
+ additionalProperties: false,
3759
+ required: ["facts"],
3760
+ properties: {
3761
+ facts: {
3762
+ type: "array",
3763
+ items: { type: "string" },
3764
+ description: "New facts established since the last report; may be empty early on."
3765
+ },
3766
+ evidence: {
3767
+ type: "array",
3768
+ items: { type: "string" },
3769
+ description: "Evidence references backing the facts (file:line or recorded evidence ids)."
3770
+ },
3771
+ questions: {
3772
+ type: "array",
3773
+ items: { type: "string" },
3774
+ description: "Remaining unresolved questions."
3775
+ },
3776
+ note: {
3777
+ type: "string",
3778
+ description: "Optional short status note."
3779
+ }
3780
+ }
3781
+ };
3782
+ /**
3783
+ * The stock progress-report tool. Stateless and deterministic: the
3784
+ * result echoes the counts, so a verbatim repeated report is a
3785
+ * duplicate result digest to the exploration guards. The value is the
3786
+ * side contract: the engine captures the LAST successful call of this
3787
+ * tool as the structured terminal partial of a 'limit' invocation, so
3788
+ * an agent that reports after every batch never loses its collected
3789
+ * work to a budget expiry.
3790
+ */
3791
+ function progressReportTool() {
3792
+ return tool({
3793
+ name: PROGRESS_REPORT_TOOL_NAME,
3794
+ description: "Report research progress after every batch of tool calls: the new facts you established, the evidence references backing them, and the questions still open. If the invocation ends at a limit, your LAST report is returned to the caller as the structured partial result, so report before the budget runs out.",
3795
+ parameters: PROGRESS_SCHEMA,
3796
+ risk: "read",
3797
+ execute: (input) => {
3798
+ const report = input;
3799
+ return Promise.resolve({
3800
+ recorded: true,
3801
+ facts: report.facts?.length ?? 0,
3802
+ evidence: report.evidence?.length ?? 0,
3803
+ questions: report.questions?.length ?? 0
3804
+ });
3805
+ }
3806
+ });
3807
+ }
3808
+ function stringArray(value) {
3809
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
3810
+ }
3811
+ /**
3812
+ * The deterministic terminal scan: pairs `report_progress` tool calls
3813
+ * with their SUCCESSFUL results by id (a denied or failed call never
3814
+ * counts, mirroring the exploration guard's restore) and normalizes the
3815
+ * last one into a {@link ProgressReport}. Pure over the message window
3816
+ * it is given: the live loop hands its own history, the replay path
3817
+ * hands the terminal checkpoint's messages, and a compaction naturally
3818
+ * narrows the window to what the model itself still sees.
3819
+ */
3820
+ function latestProgressReport(messages) {
3821
+ const callsById = /* @__PURE__ */ new Map();
3822
+ let latest;
3823
+ for (const msg of messages) for (const part of msg.parts) if (part.type === "tool-call" && part.name === "report_progress") callsById.set(part.id, part.args);
3824
+ else if (part.type === "tool-result" && part.name === "report_progress" && part.isError !== true && callsById.has(part.id)) {
3825
+ const args = callsById.get(part.id);
3826
+ if (typeof args === "object" && args !== null && !Array.isArray(args)) {
3827
+ const record = args;
3828
+ const report = {
3829
+ facts: stringArray(record.facts),
3830
+ evidence: stringArray(record.evidence),
3831
+ questions: stringArray(record.questions)
3832
+ };
3833
+ if (typeof record.note === "string") report.note = record.note;
3834
+ latest = report;
3835
+ }
3836
+ }
3837
+ return latest;
3838
+ }
3839
+ //#endregion
3840
+ //#region src/engine/profile-templates.ts
3841
+ /**
3842
+ * The research template's stop conditions: a weighted unit budget over
3843
+ * the research tools (bookkeeping tools are free), per-tool caps, both
3844
+ * repetition guards, and soft budget notices. Exported so hosts and
3845
+ * tests can read the exact defaults they are overriding.
3846
+ */
3847
+ const RESEARCH_PROFILE_LIMITS = {
3848
+ maxTurns: 24,
3849
+ maxToolCalls: 48,
3850
+ toolBudgetNotices: true,
3851
+ maxRepeatedToolSignature: 2,
3852
+ maxNoNewEvidenceCalls: 6,
3853
+ maxCallsPerTool: {
3854
+ list_files: 12,
3855
+ search_files: 20,
3856
+ read_file: 30
3857
+ },
3858
+ toolUnits: {
3859
+ max: 64,
3860
+ costs: {
3861
+ list_files: 1,
3862
+ search_files: 2,
3863
+ read_file: 2,
3864
+ record_evidence: 0,
3865
+ list_evidence: 0,
3866
+ report_progress: 0
3867
+ }
3868
+ }
3869
+ };
3870
+ /** The implementation template's stop conditions. */
3871
+ const IMPLEMENTATION_PROFILE_LIMITS = {
3872
+ maxTurns: 32,
3873
+ maxToolCalls: 64,
3874
+ toolBudgetNotices: true,
3875
+ maxRepeatedToolSignature: 3,
3876
+ noProgressTurns: 3
3877
+ };
3878
+ /** The review template's stop conditions. */
3879
+ const REVIEW_PROFILE_LIMITS = {
3880
+ maxTurns: 16,
3881
+ maxToolCalls: 32,
3882
+ toolBudgetNotices: true,
3883
+ maxRepeatedToolSignature: 2,
3884
+ maxNoNewEvidenceCalls: 8
3885
+ };
3886
+ function mergeLimits(template, overrides) {
3887
+ return {
3888
+ ...template,
3889
+ ...overrides ?? {}
3890
+ };
3891
+ }
3892
+ /**
3893
+ * The batteries-included research child: the confined
3894
+ * {@link repositoryResearchToolset} over `root`, the stock
3895
+ * report_progress tool, and {@link RESEARCH_PROFILE_LIMITS} as the stop
3896
+ * conditions. A child spawned from this profile that runs out of budget
3897
+ * settles 'limit' WITH its last progress report as the structured
3898
+ * partial, and the recorded evidence stays readable host-side through
3899
+ * `evidence()`.
3900
+ */
3901
+ function researchAgentProfile(options) {
3902
+ const { description, limits, extraTools, ...toolsetOptions } = options;
3903
+ const kit = repositoryResearchToolset(toolsetOptions);
3904
+ return {
3905
+ profile: {
3906
+ description: description ?? "Repository research over a confined root: paginated list_files/search_files/read_file with stable cursors, record_evidence verifying every citation, and report_progress after every batch. Stop conditions built in: weighted tool units, per-tool caps, repetition and no-new-evidence guards, budget notices. On limit the last progress report is the structured partial.",
3907
+ tools: [
3908
+ ...kit.tools,
3909
+ progressReportTool(),
3910
+ ...extraTools ?? []
3911
+ ],
3912
+ limits: mergeLimits(RESEARCH_PROFILE_LIMITS, limits)
3913
+ },
3914
+ evidence: () => kit.evidence()
3915
+ };
3916
+ }
3917
+ /**
3918
+ * The implementation child template: the caller's task tools plus the
3919
+ * progress contract, with {@link IMPLEMENTATION_PROFILE_LIMITS} as the
3920
+ * stop conditions (a no-progress detector instead of the research
3921
+ * no-new-evidence guard: implementation legitimately re-reads state).
3922
+ */
3923
+ function implementationAgentProfile(options = {}) {
3924
+ return {
3925
+ description: options.description ?? "Implementation work with built-in stop conditions: tool budget with notices, repeated-call guard, no-progress detector. Report progress with report_progress after every batch; on limit the last report is the structured partial.",
3926
+ tools: [progressReportTool(), ...options.tools ?? []],
3927
+ limits: mergeLimits(IMPLEMENTATION_PROFILE_LIMITS, options.limits)
3928
+ };
3929
+ }
3930
+ /**
3931
+ * The review child template: the caller's task tools plus the progress
3932
+ * contract, with {@link REVIEW_PROFILE_LIMITS} as the stop conditions
3933
+ * (a tighter turn budget and the no-new-evidence guard: a reviewer
3934
+ * circling over the same pages should stop, not spin).
3935
+ */
3936
+ function reviewAgentProfile(options = {}) {
3937
+ return {
3938
+ description: options.description ?? "Focused review with built-in stop conditions: tight turn and tool budgets with notices, repetition and no-new-evidence guards. Report findings with report_progress after every batch; on limit the last report is the structured partial.",
3939
+ tools: [progressReportTool(), ...options.tools ?? []],
3940
+ limits: mergeLimits(REVIEW_PROFILE_LIMITS, options.limits)
3941
+ };
3942
+ }
3943
+ //#endregion
3300
3944
  //#region src/journal/identity.ts
3301
3945
  /**
3302
3946
  * Content-addressed entry identity (M1-T04): IdentityInput records per
@@ -8008,6 +8652,10 @@ function mergeUsageLimits(call, profile, engine) {
8008
8652
  if (maxRepeatedToolSignature !== void 0) merged.maxRepeatedToolSignature = maxRepeatedToolSignature;
8009
8653
  const maxNoNewEvidenceCalls = pick("maxNoNewEvidenceCalls");
8010
8654
  if (maxNoNewEvidenceCalls !== void 0) merged.maxNoNewEvidenceCalls = maxNoNewEvidenceCalls;
8655
+ const maxCallsPerTool = pick("maxCallsPerTool");
8656
+ if (maxCallsPerTool !== void 0) merged.maxCallsPerTool = maxCallsPerTool;
8657
+ const toolUnits = pick("toolUnits");
8658
+ if (toolUnits !== void 0) merged.toolUnits = toolUnits;
8011
8659
  return merged;
8012
8660
  }
8013
8661
  /**
@@ -8032,6 +8680,21 @@ function validateUsageLimits(limits, site) {
8032
8680
  if (limits.toolBudgetNotices !== void 0 && typeof limits.toolBudgetNotices !== "boolean") throw new ConfigError(`${site}.toolBudgetNotices must be a boolean; got ${typeof limits.toolBudgetNotices}`);
8033
8681
  if (limits.maxRepeatedToolSignature !== void 0) requirePositiveInteger(limits.maxRepeatedToolSignature, `${site}.maxRepeatedToolSignature`);
8034
8682
  if (limits.maxNoNewEvidenceCalls !== void 0) requirePositiveInteger(limits.maxNoNewEvidenceCalls, `${site}.maxNoNewEvidenceCalls`);
8683
+ if (limits.maxCallsPerTool !== void 0) {
8684
+ const caps = limits.maxCallsPerTool;
8685
+ if (typeof caps !== "object" || caps === null || Array.isArray(caps)) throw new ConfigError(`${site}.maxCallsPerTool must be a record of per-tool caps`);
8686
+ for (const [name, cap] of Object.entries(caps)) requireNonNegativeInteger(cap, `${site}.maxCallsPerTool['${name}']`);
8687
+ }
8688
+ if (limits.toolUnits !== void 0) {
8689
+ const units = limits.toolUnits;
8690
+ if (typeof units !== "object" || units === null || Array.isArray(units)) throw new ConfigError(`${site}.toolUnits must be { max, costs? }`);
8691
+ const { max, costs } = units;
8692
+ requirePositiveInteger(max, `${site}.toolUnits.max`);
8693
+ if (costs !== void 0) {
8694
+ if (typeof costs !== "object" || costs === null || Array.isArray(costs)) throw new ConfigError(`${site}.toolUnits.costs must be a record of per-tool costs`);
8695
+ for (const [name, cost] of Object.entries(costs)) requireNonNegativeInteger(cost, `${site}.toolUnits.costs['${name}']`);
8696
+ }
8697
+ }
8035
8698
  }
8036
8699
  //#endregion
8037
8700
  //#region src/runtime/model-retry.ts
@@ -8566,7 +9229,7 @@ function formatRePrompt(issues, attempt, maxAttempts) {
8566
9229
  const GUARD_DOCS_URL = "https://docs.rulvar.com/guide/agents#exploration-guards";
8567
9230
  /** True when any exploration guard field asks for tracking. */
8568
9231
  function explorationTrackingEnabled(limits) {
8569
- return limits.maxRepeatedToolSignature !== void 0 || limits.maxNoNewEvidenceCalls !== void 0 || limits.toolBudgetNotices === true;
9232
+ return limits.maxRepeatedToolSignature !== void 0 || limits.maxNoNewEvidenceCalls !== void 0 || limits.toolBudgetNotices === true || limits.maxCallsPerTool !== void 0 || limits.toolUnits !== void 0;
8570
9233
  }
8571
9234
  function digestOf$1(value) {
8572
9235
  try {
@@ -8585,6 +9248,8 @@ var ExplorationGuard = class {
8585
9248
  repeated = 0;
8586
9249
  duplicateResults = 0;
8587
9250
  denied = 0;
9251
+ deniedToolCap = 0;
9252
+ unitsUsed = 0;
8588
9253
  unserializableSeq = 0;
8589
9254
  constructor(config) {
8590
9255
  this.config = config;
@@ -8622,10 +9287,25 @@ var ExplorationGuard = class {
8622
9287
  }
8623
9288
  }
8624
9289
  /**
8625
- * The pre-dispatch verdict: denies the call that would exceed
8626
- * maxRepeatedToolSignature executions of the same signature.
9290
+ * The pre-dispatch verdict: denies the call that would exceed its
9291
+ * tool's maxCallsPerTool cap, then the call that would exceed
9292
+ * maxRepeatedToolSignature executions of the same signature. A denial
9293
+ * never consumes maxToolCalls or tool units.
8627
9294
  */
8628
9295
  beforeExecute(name, args) {
9296
+ const cap = this.config.maxCallsPerTool?.[name];
9297
+ if (cap !== void 0) {
9298
+ const executions = this.byTool.get(name) ?? 0;
9299
+ if (executions >= cap) {
9300
+ this.deniedToolCap += 1;
9301
+ return {
9302
+ deny: true,
9303
+ guard: "per-tool-cap",
9304
+ executions,
9305
+ reason: `exploration guard: '${name}' already executed ${String(executions)} time(s) this invocation (maxCallsPerTool ${String(cap)}). Use what you have or a different tool (${GUARD_DOCS_URL}).`
9306
+ };
9307
+ }
9308
+ }
8629
9309
  const max = this.config.maxRepeatedToolSignature;
8630
9310
  if (max === void 0) return { deny: false };
8631
9311
  const executions = this.signatureExecutions.get(this.signatureOf(name, args)) ?? 0;
@@ -8633,6 +9313,7 @@ var ExplorationGuard = class {
8633
9313
  this.denied += 1;
8634
9314
  return {
8635
9315
  deny: true,
9316
+ guard: "repeated-signature",
8636
9317
  executions,
8637
9318
  reason: `exploration guard: this exact '${name}' call already executed ${String(executions)} time(s) this invocation (maxRepeatedToolSignature ${String(max)}). Reuse the earlier result or change the arguments (${GUARD_DOCS_URL}).`
8638
9319
  };
@@ -8650,6 +9331,7 @@ var ExplorationGuard = class {
8650
9331
  recordExecution(name, args, result, successful) {
8651
9332
  this.executed += 1;
8652
9333
  this.byTool.set(name, (this.byTool.get(name) ?? 0) + 1);
9334
+ if (this.config.toolUnits !== void 0) this.unitsUsed += this.config.toolUnits.costs?.[name] ?? 1;
8653
9335
  const signature = this.signatureOf(name, args);
8654
9336
  const prior = this.signatureExecutions.get(signature) ?? 0;
8655
9337
  if (prior > 0) this.repeated += 1;
@@ -8666,6 +9348,14 @@ var ExplorationGuard = class {
8666
9348
  const max = this.config.maxNoNewEvidenceCalls;
8667
9349
  return max !== void 0 && this.noNewEvidenceStreak >= max;
8668
9350
  }
9351
+ /**
9352
+ * True once the spent tool units reached the weighted budget: the
9353
+ * loop's pre-dispatch check, mirroring maxToolCalls (terminal 'limit',
9354
+ * paid partial work). Never true without toolUnits configured.
9355
+ */
9356
+ unitsExhausted() {
9357
+ return this.config.toolUnits !== void 0 && this.unitsUsed >= this.config.toolUnits.max;
9358
+ }
8669
9359
  /** The abort message for a tripped no-new-evidence guard. */
8670
9360
  describeTrip() {
8671
9361
  return `exploration guard: ${String(this.noNewEvidenceStreak)} consecutive tool calls returned no new evidence (maxNoNewEvidenceCalls ${String(this.config.maxNoNewEvidenceCalls ?? this.noNewEvidenceStreak)}; every result was already seen this invocation). The executed work is kept; narrow the scope, vary the queries, or raise the limit (${GUARD_DOCS_URL}).`;
@@ -8680,7 +9370,9 @@ var ExplorationGuard = class {
8680
9370
  repeatedCalls: this.repeated,
8681
9371
  duplicateResultCalls: this.duplicateResults,
8682
9372
  deniedRepeats: this.denied,
8683
- byTool
9373
+ byTool,
9374
+ ...this.config.maxCallsPerTool === void 0 ? {} : { deniedToolCap: this.deniedToolCap },
9375
+ ...this.config.toolUnits === void 0 ? {} : { toolUnitsUsed: this.unitsUsed }
8684
9376
  };
8685
9377
  }
8686
9378
  };
@@ -9290,6 +9982,10 @@ async function runAgent(options) {
9290
9982
  parts,
9291
9983
  limitHit: true
9292
9984
  };
9985
+ if (guard !== void 0 && guard.unitsExhausted()) return {
9986
+ parts,
9987
+ limitHit: true
9988
+ };
9293
9989
  const def = runtime.defs.find((candidate) => candidate.name === call.name);
9294
9990
  events?.emit({
9295
9991
  type: "tool:start",
@@ -9445,11 +10141,11 @@ async function runAgent(options) {
9445
10141
  toolName: gatedCall.name,
9446
10142
  outcome: "denied",
9447
10143
  durationMs: now() - gateStartedAt,
9448
- guard: "repeated-signature"
10144
+ guard: guardVerdict.guard
9449
10145
  });
9450
10146
  parts.push(errorPart(call, {
9451
10147
  error: guardVerdict.reason,
9452
- guard: "repeated-signature"
10148
+ guard: guardVerdict.guard
9453
10149
  }));
9454
10150
  continue;
9455
10151
  }
@@ -10298,6 +10994,8 @@ async function runAgent(options) {
10298
10994
  }
10299
10995
  endPhase(extractPhase, phaseOutcome(), extractServed);
10300
10996
  }
10997
+ const limitPartial = status === "limit" ? latestProgressReport(messages) : void 0;
10998
+ if (limitPartial !== void 0) await saveBoundary();
10301
10999
  let transcriptRef = "";
10302
11000
  if (options.transcript !== void 0) {
10303
11001
  transcriptRef = options.transcript.mintRef();
@@ -10320,6 +11018,7 @@ async function runAgent(options) {
10320
11018
  if (abortClass !== void 0) result.abortClass = abortClass;
10321
11019
  if (errorMessage !== void 0) result.errorMessage = errorMessage;
10322
11020
  if (guard !== void 0) result.exploration = guard.summary(toolCallsUsed);
11021
+ if (limitPartial !== void 0) result.partial = limitPartial;
10323
11022
  if (usageApprox) result.usageApprox = true;
10324
11023
  if (transportRetries > 0) result.transportRetries = transportRetries;
10325
11024
  return result;
@@ -11291,7 +11990,13 @@ const WAKE_SUMMARY_RENDER_BUDGET_CHARS = 400;
11291
11990
  * spawn ordinal; the LLM distillation upgrade is M7 territory).
11292
11991
  */
11293
11992
  function summarizeOutput(result) {
11294
- return truncateToBudget(result.status === "ok" ? typeof result.output === "string" ? result.output : JSON.stringify(result.output ?? null) : result.errorMessage ?? `terminal status ${result.status}`, 400);
11993
+ let raw;
11994
+ if (result.status === "ok") raw = typeof result.output === "string" ? result.output : JSON.stringify(result.output ?? null);
11995
+ else {
11996
+ raw = result.errorMessage ?? `terminal status ${result.status}`;
11997
+ if (result.partial !== void 0) raw = `${raw}; partial: ${JSON.stringify(result.partial)}`;
11998
+ }
11999
+ return truncateToBudget(raw, 400);
11295
12000
  }
11296
12001
  /** Folds one settled child into its digest (spawn-ordinal ordering is the caller's). */
11297
12002
  function digestOf(record, result) {
@@ -12164,6 +12869,10 @@ function createCtx(internals, rootWorkflow) {
12164
12869
  const checkpoint = blob === null ? void 0 : decodeCheckpoint(blob);
12165
12870
  if (checkpoint !== void 0) {
12166
12871
  result.turns = checkpoint.turns;
12872
+ if (result.status === "limit") {
12873
+ const partialReport = latestProgressReport(checkpoint.messages);
12874
+ if (partialReport !== void 0) result.partial = partialReport;
12875
+ }
12167
12876
  replayedToolResults = checkpoint.messages.filter((msg) => msg.role === "tool").flatMap((msg) => msg.parts).filter((part) => part.type === "tool-result").map((part) => ({
12168
12877
  name: part.name,
12169
12878
  isError: part.isError === true
@@ -13265,6 +13974,52 @@ async function executeWorkflow(internals, wf, args) {
13265
13974
  }
13266
13975
  }
13267
13976
  //#endregion
13977
+ //#region src/orchestrator/claims.ts
13978
+ /** The conservative matching key: trim plus inner-whitespace collapse. */
13979
+ function claimKey(line) {
13980
+ return line.trim().replace(/\s+/gu, " ");
13981
+ }
13982
+ /**
13983
+ * Removes later occurrences of repeated claim lines across the rows and
13984
+ * indexes each repeated claim with its reporters. Deterministic: output
13985
+ * depends only on the input order and bytes.
13986
+ */
13987
+ function dedupeRepeatedClaims(rows) {
13988
+ const seen = /* @__PURE__ */ new Map();
13989
+ const order = [];
13990
+ return {
13991
+ rows: rows.map((row) => {
13992
+ const kept = [];
13993
+ for (const line of row.text.split("\n")) {
13994
+ const key = claimKey(line);
13995
+ if (key === "") {
13996
+ kept.push(line);
13997
+ continue;
13998
+ }
13999
+ const prior = seen.get(key);
14000
+ if (prior === void 0) {
14001
+ const entry = {
14002
+ claim: line,
14003
+ nodeIds: [row.nodeId],
14004
+ count: 1
14005
+ };
14006
+ seen.set(key, entry);
14007
+ order.push(entry);
14008
+ kept.push(line);
14009
+ continue;
14010
+ }
14011
+ prior.count += 1;
14012
+ if (!prior.nodeIds.includes(row.nodeId)) prior.nodeIds.push(row.nodeId);
14013
+ }
14014
+ return {
14015
+ nodeId: row.nodeId,
14016
+ text: kept.join("\n")
14017
+ };
14018
+ }),
14019
+ repeated: order.filter((entry) => entry.count > 1)
14020
+ };
14021
+ }
14022
+ //#endregion
13268
14023
  //#region src/orchestrator/orchestrate.ts
13269
14024
  /**
13270
14025
  * The mode (c) dynamic orchestrator (M6-T07/T08).
@@ -13291,6 +14046,12 @@ const DEFAULT_FINISH_MAX_REPAIRS = 1;
13291
14046
  * call plus headroom for one validator repair exchange.
13292
14047
  */
13293
14048
  const DEFAULT_SYNTHESIS_MAX_TURNS = 4;
14049
+ /**
14050
+ * Default maxTurns of ONE incremental synthesis note (RV-211 remainder):
14051
+ * a note summarizes a single settled child into a bounded finish call,
14052
+ * so it needs less headroom than the full synthesis invocation.
14053
+ */
14054
+ const DEFAULT_SYNTHESIS_NOTE_MAX_TURNS = 2;
13294
14055
  const ORCHESTRATE_WORKFLOW_NAME = "rulvar-orchestrate";
13295
14056
  /**
13296
14057
  * One page of a string, for the child result evidence tools: maxChars is
@@ -13312,7 +14073,14 @@ function pageOf(content, rawOffset, rawMaxChars) {
13312
14073
  }
13313
14074
  /** The serialized full result of a settled child: the raw string, or JSON. */
13314
14075
  function serializeChildOutput(result) {
13315
- if (result.status !== "ok") return result.errorMessage ?? `terminal status ${result.status}`;
14076
+ if (result.status !== "ok") {
14077
+ const base = result.errorMessage ?? `terminal status ${result.status}`;
14078
+ if (result.partial !== void 0) return JSON.stringify({
14079
+ error: base,
14080
+ partial: result.partial
14081
+ });
14082
+ return base;
14083
+ }
13316
14084
  return typeof result.output === "string" ? result.output : JSON.stringify(result.output ?? null);
13317
14085
  }
13318
14086
  /**
@@ -13334,6 +14102,8 @@ function validateOrchestrateOptions(opts) {
13334
14102
  const minSuccessful = typeof policy === "object" && policy !== null && !Array.isArray(policy) ? policy.minSuccessful : void 0;
13335
14103
  if (policy !== "all-ok" && minSuccessful === void 0) throw new ConfigError(`orchestrate acceptance.childPolicy must be 'all-ok' or { minSuccessful: N }; got ${JSON.stringify(policy)}`);
13336
14104
  if (policy !== "all-ok") requirePositiveInteger(minSuccessful, "orchestrate acceptance.childPolicy.minSuccessful");
14105
+ const acceptPartial = opts.acceptance.acceptPartialChildren;
14106
+ if (acceptPartial !== void 0 && typeof acceptPartial !== "boolean") throw new ConfigError(`orchestrate acceptance.acceptPartialChildren must be a boolean; got ${typeof acceptPartial}`);
13337
14107
  }
13338
14108
  if (opts.finishValidation !== void 0) {
13339
14109
  const fv = opts.finishValidation;
@@ -13350,6 +14120,10 @@ function validateOrchestrateOptions(opts) {
13350
14120
  }
13351
14121
  if (opts.synthesis !== void 0) {
13352
14122
  const synthesis = opts.synthesis;
14123
+ 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));
14124
+ 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");
14125
+ if (synthesis.dedupeClaims !== void 0 && typeof synthesis.dedupeClaims !== "boolean") throw new ConfigError("orchestrate synthesis.dedupeClaims must be a boolean; got " + typeof synthesis.dedupeClaims);
14126
+ if (synthesis.noteLimits !== void 0) validateUsageLimits(synthesis.noteLimits, "orchestrate synthesis.noteLimits");
13353
14127
  if (synthesis.effort !== void 0 && ![
13354
14128
  "low",
13355
14129
  "medium",
@@ -13394,6 +14168,16 @@ function finishValidationPromptLines(spec) {
13394
14168
  return [`The host validates every finish({ result }) with deterministic validators: ${names}.`, "A rejected finish returns the failure reasons as the tool error result; repair the result and call finish again. " + (repairs === 0 ? "No repair attempt is granted: the first rejected finish fails the run." : repairs === 1 ? "At most one repair attempt is granted before the run fails." : `At most ${String(repairs)} repair attempts are granted before the run fails.`)];
13395
14169
  }
13396
14170
  /**
14171
+ * The partial-salvage contract rides the PROMPT exactly like finish
14172
+ * validation (RV-210 close-out): present only when
14173
+ * acceptance.acceptPartialChildren is set, so every other configuration
14174
+ * keeps byte-identical coordination prompts.
14175
+ */
14176
+ function acceptancePromptLines(acceptance) {
14177
+ if (acceptance?.acceptPartialChildren !== true) return [];
14178
+ return ["Partial salvage is on: a child that ends at its limit AFTER recording progress with report_progress counts as a partial success for acceptance; its digest carries the partial and get_child_result (when enabled) pages the full report. When the gap matters, respawn a NARROWED child carrying the partial instead of repeating the task."];
14179
+ }
14180
+ /**
13397
14181
  * Resolves per-spawn dispatch options against the engine registries
13398
14182
  * (registered SchemaSpec and tool profile names; M7-T05). An
13399
14183
  * unknown ref is a typed ConfigError, surfaced as a tool error to the
@@ -13551,6 +14335,22 @@ function makeOrchestratorWorkflow(goal, opts) {
13551
14335
  const recoveryDone = new Promise((resolve) => {
13552
14336
  releaseRecovery = resolve;
13553
14337
  });
14338
+ /**
14339
+ * Incremental synthesis notes (RV-211 remainder): one bounded
14340
+ * synthesize-role invocation per settled child, keyed by nodeId so a
14341
+ * note can never double-dispatch. The settle hook fires the note the
14342
+ * moment its child settles (overlapping the still-running fan-out);
14343
+ * the deterministic reconciliation is the completeness backstop and
14344
+ * dispatches any note the hook missed. The dispatcher installs right
14345
+ * before the coordination loop because it closes over runtime pieces
14346
+ * built below; these bindings are declared HERE, before
14347
+ * dispatchChild, so a recovered child's settle hook (which can fire
14348
+ * during the recovery scan) never touches a binding in its temporal
14349
+ * dead zone.
14350
+ */
14351
+ const synthesisNotes = /* @__PURE__ */ new Map();
14352
+ let synthesisNoteDispatcher;
14353
+ let synthesisSettleFrozen = false;
13554
14354
  let activityChain = Promise.resolve();
13555
14355
  const childScopeOf = () => {
13556
14356
  if (orchSeq === void 0) throw new ConfigError("orchestrator dispatch seq unknown before the loop started");
@@ -13636,6 +14436,7 @@ function makeOrchestratorWorkflow(goal, opts) {
13636
14436
  };
13637
14437
  settledResult.then(async (settled) => {
13638
14438
  record.settled = settled;
14439
+ if (!synthesisSettleFrozen) synthesisNoteDispatcher?.(record);
13639
14440
  await runExtensionActivity();
13640
14441
  for (const listener of [...settleListeners]) listener();
13641
14442
  });
@@ -14425,33 +15226,198 @@ function makeOrchestratorWorkflow(goal, opts) {
14425
15226
  };
14426
15227
  };
14427
15228
  /**
15229
+ * One incremental synthesis note (RV-211 remainder): a FRESH agent
15230
+ * entry with role 'synthesize' on the finish-only toolset whose
15231
+ * prompt derives deterministically from the goal and the ONE settled
15232
+ * child's digest, so a resume replays it by identity with zero paid
15233
+ * calls. The invocation itself never throws out of here: an infra
15234
+ * failure settles as a synthesized error result and the
15235
+ * reconciliation falls back to the raw digest summary.
15236
+ */
15237
+ const runSynthesisNote = async (record) => {
15238
+ const spec = opts?.synthesis;
15239
+ const finishOnly = buildOrchestratorTools(orchestratorRuntime, fullCardText).filter((tool) => tool.name === FINISH_TOOL_NAME);
15240
+ const digest = digestOf(record, record.settled);
15241
+ const prompt = [
15242
+ "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.",
15243
+ ...spec.instructions === void 0 ? [] : [spec.instructions],
15244
+ `GOAL: ${goal}`,
15245
+ `CHILD: ${JSON.stringify(digest)}`
15246
+ ].join("\n");
15247
+ const noteState = { ...callingState };
15248
+ if (orchestratorAccount !== void 0) noteState.budgetScope = orchestratorAccount;
15249
+ const noteOpts = {
15250
+ role: "synthesize",
15251
+ result: "full",
15252
+ tools: finishOnly,
15253
+ limits: spec.noteLimits ?? { maxTurns: 2 },
15254
+ ...spec.model === void 0 ? {} : { model: spec.model },
15255
+ ...spec.effort === void 0 ? {} : { effort: spec.effort },
15256
+ ...spec.estCost === void 0 ? {} : { estCost: spec.estCost },
15257
+ [kTerminalTool]: { name: FINISH_TOOL_NAME }
15258
+ };
15259
+ return runtime.runInScope(noteState, () => ctx.agent(prompt, noteOpts));
15260
+ };
15261
+ /**
15262
+ * Note dispatch is idempotent per child: the settle hook and the
15263
+ * reconciliation both come through here, and the map guarantees one
15264
+ * dispatch per nodeId (a concurrent identical dispatch would mint a
15265
+ * second occurrence and PAY twice: the v1.32.0 lesson).
15266
+ */
15267
+ const ensureSynthesisNote = (record) => {
15268
+ const existing = synthesisNotes.get(record.nodeId);
15269
+ if (existing !== void 0) return existing;
15270
+ const note = runSynthesisNote(record).catch((thrown) => ({
15271
+ status: "error",
15272
+ output: null,
15273
+ usage: {
15274
+ inputTokens: 0,
15275
+ outputTokens: 0,
15276
+ cacheReadTokens: 0,
15277
+ cacheWriteTokens: 0
15278
+ },
15279
+ costUsd: 0,
15280
+ turns: 0,
15281
+ servedBy: "unknown:unknown",
15282
+ transcriptRef: "",
15283
+ errorMessage: thrown instanceof Error ? thrown.message : String(thrown)
15284
+ }));
15285
+ synthesisNotes.set(record.nodeId, note);
15286
+ return note;
15287
+ };
15288
+ if (opts?.synthesis?.mode === "incremental" && capDecisionRef === void 0) synthesisNoteDispatcher = ensureSynthesisNote;
15289
+ /**
15290
+ * The deterministic reconciliation of 'incremental' synthesis: the
15291
+ * final result is a PURE fold of the journaled draft and the note
15292
+ * results in spawn order, never another model call. A note that died
15293
+ * falls back to the child's raw digest summary under a journaled
15294
+ * per-child decision and a warn log. With dedupeClaims, repeated
15295
+ * claim lines keep their first occurrence and the envelope carries
15296
+ * the repeatedClaims index.
15297
+ */
15298
+ const reconcileIncremental = async (draft, spec) => {
15299
+ synthesisSettleFrozen = true;
15300
+ const settledRecords = [...records.values()].filter((record) => record.settled !== void 0).sort((a, b) => a.spawnOrdinal - b.spawnOrdinal);
15301
+ const sections = [];
15302
+ for (const record of settledRecords) {
15303
+ const settled = record.settled;
15304
+ const note = await ensureSynthesisNote(record);
15305
+ let noteText;
15306
+ if (note.status === "ok") noteText = typeof note.output === "string" ? note.output : JSON.stringify(note.output ?? null);
15307
+ else {
15308
+ const fallbackKey = deriverV2.deriveKey({
15309
+ kind: "orchestrator-synthesis-note-fallback",
15310
+ nodeId: record.nodeId
15311
+ });
15312
+ if (!internals.replayer.snapshot().some((entry) => entry.kind === "decision" && entry.key === fallbackKey)) await internals.replayer.appendSinglePhase({
15313
+ scope: callingState.scope,
15314
+ key: fallbackKey,
15315
+ kind: "decision",
15316
+ status: "ok",
15317
+ spanId: internals.spans.mint(callingState.spanId),
15318
+ site: "orchestrator-synthesis",
15319
+ value: {
15320
+ decisionType: "orchestrator_synthesis_note_fallback",
15321
+ nodeId: record.nodeId,
15322
+ status: note.status,
15323
+ turnsUsed: note.turns
15324
+ }
15325
+ });
15326
+ internals.events.emit({
15327
+ type: "log",
15328
+ level: "warn",
15329
+ 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')`
15330
+ }, callingState.spanId);
15331
+ noteText = digestOf(record, settled).outputSummary;
15332
+ }
15333
+ sections.push({
15334
+ nodeId: record.nodeId,
15335
+ logicalTaskId: record.logicalTaskId,
15336
+ status: settled.status,
15337
+ noteStatus: note.status,
15338
+ note: noteText
15339
+ });
15340
+ }
15341
+ let repeatedClaims;
15342
+ if (spec.dedupeClaims === true) {
15343
+ const deduped = dedupeRepeatedClaims(sections.map((section) => ({
15344
+ nodeId: section.nodeId,
15345
+ text: section.note
15346
+ })));
15347
+ const textByNode = new Map(deduped.rows.map((row) => [row.nodeId, row.text]));
15348
+ for (const section of sections) section.note = textByNode.get(section.nodeId) ?? section.note;
15349
+ repeatedClaims = deduped.repeated;
15350
+ }
15351
+ const draftJson = JSON.stringify(draft ?? null);
15352
+ internals.events.emit({
15353
+ type: "log",
15354
+ level: "debug",
15355
+ msg: "orchestrator synthesis reconciliation",
15356
+ data: {
15357
+ children: sections.length,
15358
+ draftChars: draftJson.length,
15359
+ notesChars: sections.reduce((sum, section) => sum + section.note.length, 0),
15360
+ perChild: sections.map((section) => ({
15361
+ nodeId: section.nodeId,
15362
+ chars: section.note.length
15363
+ })),
15364
+ ...repeatedClaims === void 0 ? {} : { repeatedClaims: repeatedClaims.length }
15365
+ }
15366
+ }, callingState.spanId);
15367
+ return {
15368
+ synthesis: "incremental",
15369
+ draft,
15370
+ sections,
15371
+ ...repeatedClaims === void 0 ? {} : { repeatedClaims }
15372
+ };
15373
+ };
15374
+ /**
14428
15375
  * The post-fan-in synthesis invocation (RV-211): a FRESH agent entry
14429
15376
  * with role 'synthesize' on the finish-only toolset (a distinct
14430
15377
  * toolsetHash, the reserved-finalizer precedent), its prompt derived
14431
15378
  * deterministically from the goal, the journaled coordination draft,
14432
15379
  * and the settled child digest, so a resume replays it by identity
14433
15380
  * with zero paid calls. Runs strictly AFTER the acceptance verdict
14434
- * (a rejected run never pays for synthesis) and owns the finish
14435
- * validators when they are configured. Failure posture: with
14436
- * validators the run fails typed (the validated path is mandatory);
14437
- * without them the run falls back to the draft under a journaled
14438
- * decision and a warn log, never silently.
15381
+ * (a rejected run never pays for synthesis; in 'incremental' mode
15382
+ * the per-child notes are paid DURING the run, so only the
15383
+ * reconciliation itself is deferred) and owns the finish validators
15384
+ * when they are configured. Failure posture: with validators the run
15385
+ * fails typed (the validated path is mandatory); without them the
15386
+ * run falls back to the draft under a journaled decision and a warn
15387
+ * log, never silently.
14439
15388
  */
14440
15389
  const runSynthesis = async (draft) => {
14441
15390
  const spec = opts?.synthesis;
14442
15391
  if (spec === void 0) return draft;
14443
15392
  await recoveryDone;
15393
+ if (spec.mode === "incremental") return await reconcileIncremental(draft, spec);
14444
15394
  const finishOnly = buildOrchestratorTools(orchestratorRuntime, fullCardText).filter((tool) => tool.name === FINISH_TOOL_NAME);
14445
15395
  const settledDigests = [...records.values()].filter((record) => record.settled !== void 0).sort((a, b) => a.spawnOrdinal - b.spawnOrdinal).map((record) => digestOf(record, record.settled));
15396
+ let digestRows = settledDigests;
15397
+ let repeatedClaims;
15398
+ if (spec.dedupeClaims === true) {
15399
+ const deduped = dedupeRepeatedClaims(settledDigests.map((row) => ({
15400
+ nodeId: row.nodeId,
15401
+ text: row.outputSummary
15402
+ })));
15403
+ const textByNode = new Map(deduped.rows.map((row) => [row.nodeId, row.text]));
15404
+ digestRows = settledDigests.map((row) => ({
15405
+ ...row,
15406
+ outputSummary: textByNode.get(row.nodeId) ?? row.outputSummary
15407
+ }));
15408
+ repeatedClaims = deduped.repeated;
15409
+ }
14446
15410
  const draftJson = JSON.stringify(draft ?? null);
14447
- const digestJson = JSON.stringify(settledDigests);
15411
+ const digestJson = JSON.stringify(digestRows);
14448
15412
  const prompt = [
14449
15413
  "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.",
15414
+ ...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."],
14450
15415
  ...spec.instructions === void 0 ? [] : [spec.instructions],
14451
15416
  ...finishValidationPromptLines(validationSpec),
14452
15417
  `GOAL: ${goal}`,
14453
15418
  `DRAFT: ${draftJson}`,
14454
- `DIGEST: ${digestJson}`
15419
+ `DIGEST: ${digestJson}`,
15420
+ ...repeatedClaims === void 0 ? [] : [`REPEATED CLAIMS: ${JSON.stringify(repeatedClaims)}`]
14455
15421
  ].join("\n");
14456
15422
  internals.events.emit({
14457
15423
  type: "log",
@@ -14462,10 +15428,11 @@ function makeOrchestratorWorkflow(goal, opts) {
14462
15428
  draftChars: draftJson.length,
14463
15429
  digestChars: digestJson.length,
14464
15430
  promptChars: prompt.length,
14465
- perChild: settledDigests.map((entry) => ({
15431
+ perChild: digestRows.map((entry) => ({
14466
15432
  nodeId: entry.nodeId,
14467
15433
  chars: JSON.stringify(entry).length
14468
- }))
15434
+ })),
15435
+ ...repeatedClaims === void 0 ? {} : { repeatedClaims: repeatedClaims.length }
14469
15436
  }
14470
15437
  }, callingState.spanId);
14471
15438
  const synthesisState = { ...callingState };
@@ -14540,7 +15507,11 @@ function makeOrchestratorWorkflow(goal, opts) {
14540
15507
  const priorRejection = validationDecisions().find((decision) => decision.verdict === "rejected");
14541
15508
  if (priorRejection !== void 0) throw finishValidationError(priorRejection);
14542
15509
  }
14543
- const promptLines = [...extension?.promptLines?.() ?? [], ...finishValidationPromptLines(validationSpec)];
15510
+ const promptLines = [
15511
+ ...extension?.promptLines?.() ?? [],
15512
+ ...finishValidationPromptLines(validationSpec),
15513
+ ...acceptancePromptLines(opts?.acceptance)
15514
+ ];
14544
15515
  const result = await runtime.runInScope(orchestratorState, () => ctx.agent(orchestratorPrompt(goal, opts?.maxSpawns, promptLines.length === 0 ? void 0 : promptLines), agentOpts));
14545
15516
  const liveTermination = extensionTermination;
14546
15517
  if (liveTermination !== void 0) throw liveTermination;
@@ -14556,21 +15527,32 @@ function makeOrchestratorWorkflow(goal, opts) {
14556
15527
  else {
14557
15528
  const childStatusCounts = {};
14558
15529
  const degradedReasons = [];
15530
+ const salvaged = [];
15531
+ let hardDegraded = 0;
15532
+ const acceptPartial = opts.acceptance.acceptPartialChildren === true;
14559
15533
  const sortedRecords = [...records.values()].sort((a, b) => a.spawnOrdinal - b.spawnOrdinal);
14560
15534
  for (const record of sortedRecords) {
14561
15535
  const status = record.settled?.status ?? "running";
14562
15536
  childStatusCounts[status] = (childStatusCounts[status] ?? 0) + 1;
14563
- if (status !== "ok") degradedReasons.push(status === "running" ? `child ${record.nodeId} was still running when finish validated` : `child ${record.nodeId} settled '${status}'`);
15537
+ if (status === "ok") continue;
15538
+ if (acceptPartial && status === "limit" && record.settled?.partial !== void 0) {
15539
+ salvaged.push(record.nodeId);
15540
+ degradedReasons.push(`child ${record.nodeId} accepted as partial (settled 'limit' with a structured partial)`);
15541
+ continue;
15542
+ }
15543
+ hardDegraded += 1;
15544
+ degradedReasons.push(status === "running" ? `child ${record.nodeId} was still running when finish validated` : `child ${record.nodeId} settled '${status}'`);
14564
15545
  }
14565
15546
  const childPolicy = opts.acceptance.childPolicy;
14566
- const accepted = childPolicy === "all-ok" ? degradedReasons.length === 0 : (childStatusCounts.ok ?? 0) >= childPolicy.minSuccessful;
15547
+ const accepted = childPolicy === "all-ok" ? hardDegraded === 0 : (childStatusCounts.ok ?? 0) + salvaged.length >= childPolicy.minSuccessful;
14567
15548
  decision = {
14568
15549
  decisionType: "orchestrator_acceptance",
14569
15550
  verdict: accepted ? "accepted" : "rejected",
14570
15551
  completion: !accepted ? "rejected" : degradedReasons.length === 0 ? "complete" : "partial",
14571
15552
  childPolicy,
14572
15553
  childStatusCounts,
14573
- degradedReasons
15554
+ degradedReasons,
15555
+ ...salvaged.length === 0 ? {} : { salvagedPartialChildren: salvaged }
14574
15556
  };
14575
15557
  await internals.replayer.appendSinglePhase({
14576
15558
  scope: callingState.scope,
@@ -14586,16 +15568,19 @@ function makeOrchestratorWorkflow(goal, opts) {
14586
15568
  const required = decision.childPolicy === "all-ok" ? "every child ok" : `at least ${String(decision.childPolicy.minSuccessful)} children ok`;
14587
15569
  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: {
14588
15570
  source: "orchestrator_acceptance",
15571
+ completion: "rejected",
14589
15572
  childPolicy: decision.childPolicy,
14590
15573
  childStatusCounts: decision.childStatusCounts,
14591
- degradedReasons: decision.degradedReasons
15574
+ degradedReasons: decision.degradedReasons,
15575
+ ...decision.salvagedPartialChildren === void 0 ? {} : { salvagedPartialChildren: decision.salvagedPartialChildren }
14592
15576
  } });
14593
15577
  }
14594
15578
  return {
14595
15579
  result: await runSynthesis(result.output),
14596
15580
  completion: decision.completion,
14597
15581
  childStatusCounts: decision.childStatusCounts,
14598
- degradedReasons: decision.degradedReasons
15582
+ degradedReasons: decision.degradedReasons,
15583
+ ...decision.salvagedPartialChildren === void 0 ? {} : { salvagedPartialChildren: decision.salvagedPartialChildren }
14599
15584
  };
14600
15585
  });
14601
15586
  }
@@ -15243,6 +16228,32 @@ function workflowSourceRef(runId) {
15243
16228
  return `${runId}/workflow-source`;
15244
16229
  }
15245
16230
  /**
16231
+ * The completion envelope contract (RV-207 tail): a workflow reports
16232
+ * SEMANTIC completion by returning an object result carrying a
16233
+ * `completion` literal (and optionally `childStatusCounts`), or by
16234
+ * throwing a typed RulvarError whose `data` carries them; the engine
16235
+ * lifts the validated fields onto the `run:end` event so telemetry
16236
+ * consumers read completeness without parsing workflow-specific result
16237
+ * shapes. The orchestrator acceptance path emits this envelope. Pure
16238
+ * shape validation: anything malformed is silently absent (the event is
16239
+ * telemetry, never authority), and an invalid counts record drops the
16240
+ * counts while keeping a valid completion.
16241
+ */
16242
+ function liftRunCompletion(candidate) {
16243
+ if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return;
16244
+ const completion = candidate.completion;
16245
+ if (completion !== "complete" && completion !== "partial" && completion !== "rejected") return;
16246
+ const counts = candidate.childStatusCounts;
16247
+ if (typeof counts === "object" && counts !== null && !Array.isArray(counts)) {
16248
+ const entries = Object.entries(counts);
16249
+ if (entries.every(([, value]) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0)) return {
16250
+ completion,
16251
+ childStatusCounts: Object.fromEntries(entries)
16252
+ };
16253
+ }
16254
+ return { completion };
16255
+ }
16256
+ /**
15246
16257
  * sha256 hex over the JCS canonical serialization of a run's args: the
15247
16258
  * value the engine records as `RunMeta.argsHash` at genesis, exposed so
15248
16259
  * hosts can verify re-supplied resume args against the recorded hash
@@ -15613,11 +16624,13 @@ function createEngine(options) {
15613
16624
  }
15614
16625
  }
15615
16626
  await putMeta(status).catch(() => void 0);
16627
+ const lifted = liftRunCompletion(status === "ok" || status === "exhausted" ? outcome.value : status === "error" ? wireError?.data : void 0);
15616
16628
  bus.emit({
15617
16629
  type: "run:end",
15618
16630
  status,
15619
16631
  totalUsd: ledger.usd,
15620
- ...outcome.cost.usageApprox === true ? { usageApprox: true } : {}
16632
+ ...outcome.cost.usageApprox === true ? { usageApprox: true } : {},
16633
+ ...lifted === void 0 ? {} : lifted
15621
16634
  }, rootSpanId);
15622
16635
  bus.end();
15623
16636
  resumeCtx?.previewResolve({
@@ -16089,4 +17102,4 @@ function createSandboxBridge(ctx, options) {
16089
17102
  };
16090
17103
  }
16091
17104
  //#endregion
16092
- 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, 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, reduceCriticalPath, 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 };
17105
+ 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, IMPLEMENTATION_PROFILE_LIMITS, 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, PROGRESS_REPORT_TOOL_NAME, ParallelSiteCounter, PlanInvariantError, READ_CHILD_ARTIFACT_SCHEMA, READ_CHILD_ARTIFACT_TOOL_NAME, RESEARCH_PROFILE_LIMITS, REVIEW_PROFILE_LIMITS, 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, implementationAgentProfile, isEscalated, isSchemaPairSpec, isStandardSchemaSpec, isStrictCompatibleSchema, kMaxOf, knowledgeHash, ladderLengthOf, ladderRungChoice, lastRunSettle, latestProgressReport, 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, progressReportTool, projectHistory, projectIdentity, projectToJsonSchema, proposalStatement, providerOf, readRunMeta, readTerminationInit, reconcileRunMeta, reduceCriticalPath, reduceInvocationTable, registryKeyRing, remeasureQueue, replayDisposition, repositoryResearchToolset, requiredFieldsValidator, requiredSectionsValidator, researchAgentProfile, resolveModelInvocation, resolvePricing, resolveToolset, retryClassOf, retryDelayMs, reviewAgentProfile, 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 };