pi-advisor-flow 0.5.6 → 0.6.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 (75) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/index.js +1371 -1128
  3. package/extensions/index.ts +9 -9
  4. package/package.json +3 -1
  5. package/src/{untracked.ts → attachments.ts} +2 -5
  6. package/src/commands/activation-preparation.ts +6 -6
  7. package/src/commands/activation.ts +7 -7
  8. package/src/commands/lifecycle.ts +5 -5
  9. package/src/commands/manual-command.ts +13 -9
  10. package/src/commands/manual-consultation.ts +8 -8
  11. package/src/commands/manual-progress.ts +3 -3
  12. package/src/commands/model-commands.ts +6 -6
  13. package/src/commands/model-options.ts +4 -4
  14. package/src/commands/model-picker.ts +2 -2
  15. package/src/commands/registration.ts +8 -8
  16. package/src/commands/renderers.ts +5 -5
  17. package/src/commands/runtime.ts +8 -8
  18. package/src/commands/settings-commands.ts +8 -8
  19. package/src/commands/settings-persistence.ts +4 -4
  20. package/src/commands/types.ts +5 -13
  21. package/src/commands.ts +2 -2
  22. package/src/config/args.ts +3 -3
  23. package/src/config/defaults.ts +3 -3
  24. package/src/config/schema.ts +322 -0
  25. package/src/config/state.ts +2 -2
  26. package/src/config/storage.ts +20 -117
  27. package/src/config/types.ts +1 -1
  28. package/src/config/validation.ts +61 -181
  29. package/src/config.ts +7 -7
  30. package/src/content-utils.ts +30 -0
  31. package/src/conversation.ts +13 -168
  32. package/src/git.ts +4 -18
  33. package/src/herdr.ts +6 -8
  34. package/src/model-stream.ts +5 -8
  35. package/src/outcomes.ts +2 -5
  36. package/src/pi-settings.ts +59 -0
  37. package/src/preferences.ts +2 -2
  38. package/src/redaction.ts +50 -0
  39. package/src/scout-context.ts +121 -485
  40. package/src/scout-curation.ts +84 -0
  41. package/src/scout-groups.ts +485 -0
  42. package/src/scout-protocol.ts +122 -0
  43. package/src/scout-reconstruct.ts +44 -0
  44. package/src/scout-types.ts +66 -0
  45. package/src/scout.ts +53 -63
  46. package/src/session-state.ts +119 -76
  47. package/src/tool-result-cap.ts +88 -0
  48. package/src/tools/consult-context.ts +155 -0
  49. package/src/tools/consultation.ts +71 -220
  50. package/src/tools/gate-policy.ts +5 -5
  51. package/src/tools/gate-protocol.ts +2 -2
  52. package/src/tools/loop-gate.ts +70 -61
  53. package/src/tools/prompts.ts +4 -7
  54. package/src/tools/register-ask-advisor.ts +9 -14
  55. package/src/tools/register-lifecycle.ts +7 -7
  56. package/src/tools/register-outcome.ts +5 -5
  57. package/src/tools/register-renderers.ts +5 -5
  58. package/src/tools/registration.ts +10 -10
  59. package/src/tools/render-advisor-result.ts +11 -8
  60. package/src/tools/render-common.ts +25 -13
  61. package/src/tools/scout-status.ts +5 -5
  62. package/src/tools/session.ts +1 -1
  63. package/src/tools/types.ts +7 -7
  64. package/src/tools.ts +9 -9
  65. package/src/ui/manual-dialog-render.ts +2 -2
  66. package/src/ui/manual-dialog.ts +4 -9
  67. package/src/ui/model-selector.ts +1 -1
  68. package/src/ui/settings-formatting.ts +2 -2
  69. package/src/ui/settings-items.ts +4 -4
  70. package/src/ui/settings-mutations.ts +2 -2
  71. package/src/ui/settings-selector.ts +5 -5
  72. package/src/ui/text-setting-submenu.ts +1 -1
  73. package/src/ui/types.ts +2 -2
  74. package/src/ui.ts +4 -4
  75. package/src/usage.ts +1 -6
package/dist/index.js CHANGED
@@ -297,66 +297,226 @@ ${patch}` : "Patch: (no tracked-file content changes)");
297
297
  }
298
298
  };
299
299
 
300
+ // src/config/schema.ts
301
+ var configuredModelRef = (value) => value?.trim() || undefined;
302
+ var isValidAdvisorToolPolicies = (value) => {
303
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
304
+ return false;
305
+ }
306
+ return Object.entries(value).every(([toolName, policy]) => toolName.trim().length > 0 && typeof policy === "string" && ADVISOR_TOOL_POLICIES.includes(policy));
307
+ };
308
+ var nonNegativeSafeInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
309
+ var isValidContextMaxChars = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= MAX_CONTEXT_MAX_CHARS;
310
+ var isValidLoopThreshold = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 2;
311
+ var isValidMaxCallsPerSession = (value) => nonNegativeSafeInteger(value);
312
+ var isValidGateFailureMode = (value) => typeof value === "string" && GATE_FAILURE_MODES.includes(value);
313
+ var isValidToolResultMaxLines = (value) => nonNegativeSafeInteger(value);
314
+ var isValidToolResultMaxBytes = (value) => nonNegativeSafeInteger(value);
315
+ var CONFIG_SCHEMA = {
316
+ advisor: {
317
+ accepted: "a provider/model string",
318
+ current: () => configuredModelRef(advisorRef),
319
+ persisted: true,
320
+ type: "string"
321
+ },
322
+ advisorAutoLoopGate: {
323
+ accepted: "true or false",
324
+ current: () => advisorAutoLoopGateRef,
325
+ persisted: true,
326
+ type: "boolean"
327
+ },
328
+ advisorBlockOnBlocked: {
329
+ accepted: "true or false",
330
+ current: () => advisorBlockOnBlockedRef,
331
+ persisted: true,
332
+ type: "boolean"
333
+ },
334
+ advisorCollapseResponses: {
335
+ accepted: "true or false",
336
+ current: () => advisorCollapseResponsesRef,
337
+ persisted: true,
338
+ type: "boolean"
339
+ },
340
+ advisorCompletionGate: {
341
+ accepted: "true or false",
342
+ current: () => advisorCompletionGateRef,
343
+ persisted: true,
344
+ type: "boolean"
345
+ },
346
+ advisorCustomInvocation: {
347
+ accepted: "a string",
348
+ current: () => advisorCustomInvocationRef,
349
+ persisted: true,
350
+ type: "string"
351
+ },
352
+ advisorEffort: {
353
+ accepted: "a string",
354
+ current: () => advisorEffortRef,
355
+ persisted: true,
356
+ type: "string"
357
+ },
358
+ advisorFailureGate: {
359
+ accepted: "true or false",
360
+ current: () => advisorFailureGateRef,
361
+ persisted: true,
362
+ type: "boolean"
363
+ },
364
+ advisorGitContext: {
365
+ accepted: GIT_CONTEXT_LEVELS.join(", "),
366
+ current: () => advisorGitContextRef,
367
+ persisted: true,
368
+ type: "enum",
369
+ validate: isValidGitContextLevel
370
+ },
371
+ advisorGitContextMaxChars: {
372
+ accepted: "a non-negative safe integer",
373
+ current: () => advisorGitContextMaxCharsRef,
374
+ persisted: true,
375
+ type: "number",
376
+ validate: nonNegativeSafeInteger
377
+ },
378
+ advisorHerdrIntegration: {
379
+ accepted: "true or false",
380
+ current: () => advisorHerdrIntegrationRef,
381
+ persisted: true,
382
+ type: "boolean"
383
+ },
384
+ advisorLoopThreshold: {
385
+ accepted: "a safe integer of at least 2",
386
+ current: () => advisorLoopThresholdRef,
387
+ persisted: true,
388
+ type: "number",
389
+ validate: isValidLoopThreshold
390
+ },
391
+ advisorMaxCallsPerSession: {
392
+ accepted: "a non-negative safe integer",
393
+ current: () => advisorMaxCallsPerSessionRef,
394
+ persisted: true,
395
+ type: "number",
396
+ validate: isValidMaxCallsPerSession
397
+ },
398
+ advisorOutcomeLogging: {
399
+ accepted: "true or false",
400
+ current: () => advisorOutcomeLoggingRef,
401
+ persisted: false,
402
+ type: "boolean"
403
+ },
404
+ advisorPlanGate: {
405
+ accepted: "true or false",
406
+ current: () => advisorPlanGateRef,
407
+ persisted: true,
408
+ type: "boolean"
409
+ },
410
+ advisorRedactSecrets: {
411
+ accepted: "true or false",
412
+ current: () => advisorRedactSecretsRef,
413
+ persisted: true,
414
+ type: "boolean"
415
+ },
416
+ advisorScoutEnabled: {
417
+ accepted: "true or false",
418
+ current: () => advisorScoutEnabledRef,
419
+ persisted: true,
420
+ type: "boolean"
421
+ },
422
+ advisorSessionSummary: {
423
+ accepted: "true or false",
424
+ current: () => advisorSessionSummaryRef,
425
+ persisted: true,
426
+ type: "boolean"
427
+ },
428
+ advisorToolPolicies: {
429
+ accepted: "a JSON object with non-empty tool names and full, summary, or exclude values",
430
+ current: () => ({ ...advisorToolPoliciesRef }),
431
+ persisted: true,
432
+ type: "object",
433
+ validate: isValidAdvisorToolPolicies
434
+ },
435
+ advisorToolResultMaxBytes: {
436
+ accepted: "a non-negative safe integer",
437
+ current: () => advisorToolResultMaxBytesRef,
438
+ persisted: true,
439
+ type: "number",
440
+ validate: isValidToolResultMaxBytes
441
+ },
442
+ advisorToolResultMaxLines: {
443
+ accepted: "a non-negative safe integer",
444
+ current: () => advisorToolResultMaxLinesRef,
445
+ persisted: true,
446
+ type: "number",
447
+ validate: isValidToolResultMaxLines
448
+ },
449
+ advisorTrackedFileContent: {
450
+ accepted: "true or false",
451
+ current: () => advisorTrackedFileContentRef,
452
+ persisted: true,
453
+ type: "boolean"
454
+ },
455
+ advisorUntrackedContent: {
456
+ accepted: "true or false",
457
+ current: () => advisorUntrackedContentRef,
458
+ persisted: true,
459
+ type: "boolean"
460
+ },
461
+ alwaysOn: {
462
+ accepted: "true or false",
463
+ current: () => alwaysOnRef,
464
+ persisted: true,
465
+ type: "boolean"
466
+ },
467
+ contextMaxChars: {
468
+ accepted: `a safe integer from 0 through ${MAX_CONTEXT_MAX_CHARS}`,
469
+ current: () => contextMaxCharsRef,
470
+ persisted: true,
471
+ type: "number",
472
+ validate: isValidContextMaxChars
473
+ },
474
+ executor: {
475
+ accepted: "a provider/model string",
476
+ current: () => configuredModelRef(executorRef),
477
+ persisted: true,
478
+ type: "string"
479
+ },
480
+ executorEffort: {
481
+ accepted: "a string",
482
+ current: () => executorEffortRef,
483
+ persisted: true,
484
+ type: "string"
485
+ },
486
+ gateFailureMode: {
487
+ accepted: GATE_FAILURE_MODES.join(", "),
488
+ current: () => advisorFailureModeRef,
489
+ persisted: true,
490
+ type: "enum",
491
+ validate: isValidGateFailureMode
492
+ },
493
+ showUsageDetails: {
494
+ accepted: "true or false",
495
+ current: () => showUsageDetailsRef,
496
+ persisted: true,
497
+ type: "boolean"
498
+ },
499
+ showUsageFooter: {
500
+ accepted: "true or false",
501
+ current: () => showUsageFooterRef,
502
+ persisted: true,
503
+ type: "boolean"
504
+ },
505
+ simpleMode: {
506
+ accepted: "true or false",
507
+ current: () => simpleModeRef,
508
+ persisted: true,
509
+ type: "boolean"
510
+ }
511
+ };
512
+ var SAVED_CONFIG_KEYS = Object.keys(CONFIG_SCHEMA).filter((key) => CONFIG_SCHEMA[key].persisted);
513
+ var SCHEMA_BY_KEY = CONFIG_SCHEMA;
514
+
300
515
  // src/config/validation.ts
301
- var CONFIG_KEYS = new Set([
302
- "advisor",
303
- "advisorAutoLoopGate",
304
- "advisorBlockOnBlocked",
305
- "advisorCollapseResponses",
306
- "advisorCompletionGate",
307
- "advisorCustomInvocation",
308
- "advisorEffort",
309
- "advisorFailureGate",
310
- "advisorGitContext",
311
- "advisorGitContextMaxChars",
312
- "advisorHerdrIntegration",
313
- "advisorLoopThreshold",
314
- "advisorMaxCallsPerSession",
315
- "advisorPlanGate",
316
- "advisorSessionSummary",
317
- "advisorScoutEnabled",
318
- "showUsageDetails",
319
- "showUsageFooter",
320
- "simpleMode",
321
- "alwaysOn",
322
- "advisorToolResultMaxBytes",
323
- "advisorToolResultMaxLines",
324
- "advisorTrackedFileContent",
325
- "advisorRedactSecrets",
326
- "advisorUntrackedContent",
327
- "advisorOutcomeLogging",
328
- "advisorToolPolicies",
329
- "contextMaxChars",
330
- "executor",
331
- "executorEffort",
332
- "gateFailureMode"
333
- ]);
334
- var BOOLEAN_CONFIG_KEYS = [
335
- "advisorPlanGate",
336
- "advisorFailureGate",
337
- "advisorCompletionGate",
338
- "advisorCollapseResponses",
339
- "advisorBlockOnBlocked",
340
- "advisorAutoLoopGate",
341
- "advisorSessionSummary",
342
- "advisorScoutEnabled",
343
- "showUsageDetails",
344
- "showUsageFooter",
345
- "simpleMode",
346
- "alwaysOn",
347
- "advisorHerdrIntegration",
348
- "advisorTrackedFileContent",
349
- "advisorRedactSecrets",
350
- "advisorUntrackedContent",
351
- "advisorOutcomeLogging"
352
- ];
353
- var STRING_CONFIG_KEYS = [
354
- "executor",
355
- "advisor",
356
- "executorEffort",
357
- "advisorEffort",
358
- "advisorCustomInvocation"
359
- ];
516
+ var CONFIG_KEYS = new Set(Object.keys(CONFIG_SCHEMA));
517
+ var keysOfType = (type) => Object.keys(CONFIG_SCHEMA).filter((key) => SCHEMA_BY_KEY[key].type === type);
518
+ var BOOLEAN_CONFIG_KEYS = keysOfType("boolean");
519
+ var STRING_CONFIG_KEYS = keysOfType("string");
360
520
  var invalidConfigValue = (path, key, accepted) => {
361
521
  throw new TypeError(`Invalid advisor configuration at ${path}, key ${JSON.stringify(key)}: expected ${accepted}.`);
362
522
  };
@@ -364,65 +524,38 @@ var unknownConfigKeys = (config) => Object.keys(config).filter((key) => !CONFIG_
364
524
  var validateStringValues = (config, path) => {
365
525
  for (const key of STRING_CONFIG_KEYS) {
366
526
  if (config[key] !== undefined && typeof config[key] !== "string") {
367
- invalidConfigValue(path, key, key === "executor" || key === "advisor" ? "a provider/model string" : "a string");
527
+ invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
368
528
  }
369
529
  }
370
530
  };
371
531
  var validateBooleanValues = (config, path) => {
372
532
  for (const key of BOOLEAN_CONFIG_KEYS) {
373
533
  if (config[key] !== undefined && typeof config[key] !== "boolean") {
374
- invalidConfigValue(path, key, "true or false");
534
+ invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
375
535
  }
376
536
  }
377
537
  };
378
- var isValidAdvisorToolPolicies = (value) => {
379
- if (!value || typeof value !== "object" || Array.isArray(value)) {
380
- return false;
538
+ var validateNumericValues = (config, path) => {
539
+ for (const key of keysOfType("number")) {
540
+ const isValid = SCHEMA_BY_KEY[key].validate;
541
+ if (config[key] !== undefined && isValid !== undefined && !isValid(config[key])) {
542
+ invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
543
+ }
381
544
  }
382
- return Object.entries(value).every(([toolName, policy]) => toolName.trim().length > 0 && typeof policy === "string" && ADVISOR_TOOL_POLICIES.includes(policy));
383
545
  };
384
- var isValidContextMaxChars = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= MAX_CONTEXT_MAX_CHARS;
385
- var isValidLoopThreshold = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 2;
386
- var isValidMaxCallsPerSession = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
387
- var isValidGateFailureMode = (value) => typeof value === "string" && GATE_FAILURE_MODES.includes(value);
388
- var isValidToolResultMaxLines = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
389
- var isValidToolResultMaxBytes = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
390
- var validateNumericValues = (config, path) => {
391
- const numericRules = [
392
- [
393
- "contextMaxChars",
394
- isValidContextMaxChars,
395
- `a safe integer from 0 through ${MAX_CONTEXT_MAX_CHARS}`
396
- ],
397
- [
398
- "advisorLoopThreshold",
399
- isValidLoopThreshold,
400
- "a safe integer of at least 2"
401
- ],
402
- [
403
- "advisorMaxCallsPerSession",
404
- isValidMaxCallsPerSession,
405
- "a non-negative safe integer"
406
- ],
407
- [
408
- "advisorToolResultMaxLines",
409
- isValidToolResultMaxLines,
410
- "a non-negative safe integer"
411
- ],
412
- [
413
- "advisorToolResultMaxBytes",
414
- isValidToolResultMaxBytes,
415
- "a non-negative safe integer"
416
- ],
417
- [
418
- "advisorGitContextMaxChars",
419
- isValidToolResultMaxBytes,
420
- "a non-negative safe integer"
421
- ]
422
- ];
423
- for (const [key, isValid, description] of numericRules) {
424
- if (config[key] !== undefined && !isValid(config[key])) {
425
- invalidConfigValue(path, key, description);
546
+ var validateEnumValues = (config, path) => {
547
+ for (const key of keysOfType("enum")) {
548
+ const isValid = SCHEMA_BY_KEY[key].validate;
549
+ if (config[key] !== undefined && isValid !== undefined && !isValid(config[key])) {
550
+ invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
551
+ }
552
+ }
553
+ };
554
+ var validateObjectValues = (config, path) => {
555
+ for (const key of keysOfType("object")) {
556
+ const isValid = SCHEMA_BY_KEY[key].validate;
557
+ if (config[key] !== undefined && isValid !== undefined && !isValid(config[key])) {
558
+ invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
426
559
  }
427
560
  }
428
561
  };
@@ -434,15 +567,8 @@ var validateConfig = (value, path = "advisor.json") => {
434
567
  validateStringValues(config, path);
435
568
  validateBooleanValues(config, path);
436
569
  validateNumericValues(config, path);
437
- if (config.advisorToolPolicies !== undefined && !isValidAdvisorToolPolicies(config.advisorToolPolicies)) {
438
- invalidConfigValue(path, "advisorToolPolicies", "a JSON object with non-empty tool names and full, summary, or exclude values");
439
- }
440
- if (config.gateFailureMode !== undefined && !isValidGateFailureMode(config.gateFailureMode)) {
441
- invalidConfigValue(path, "gateFailureMode", GATE_FAILURE_MODES.join(", "));
442
- }
443
- if (config.advisorGitContext !== undefined && !isValidGitContextLevel(config.advisorGitContext)) {
444
- invalidConfigValue(path, "advisorGitContext", GIT_CONTEXT_LEVELS.join(", "));
445
- }
570
+ validateObjectValues(config, path);
571
+ validateEnumValues(config, path);
446
572
  return true;
447
573
  };
448
574
 
@@ -562,71 +688,7 @@ var applyConfig = (config) => {
562
688
  };
563
689
 
564
690
  // src/config/storage.ts
565
- var configuredModelRef = (value) => value?.trim() || undefined;
566
- var SAVED_CONFIG_KEYS = [
567
- "advisor",
568
- "advisorAutoLoopGate",
569
- "advisorBlockOnBlocked",
570
- "advisorCollapseResponses",
571
- "advisorCompletionGate",
572
- "advisorCustomInvocation",
573
- "advisorEffort",
574
- "advisorFailureGate",
575
- "advisorGitContext",
576
- "advisorGitContextMaxChars",
577
- "advisorHerdrIntegration",
578
- "advisorLoopThreshold",
579
- "advisorMaxCallsPerSession",
580
- "advisorPlanGate",
581
- "advisorRedactSecrets",
582
- "advisorScoutEnabled",
583
- "advisorSessionSummary",
584
- "advisorToolPolicies",
585
- "advisorToolResultMaxBytes",
586
- "advisorToolResultMaxLines",
587
- "advisorTrackedFileContent",
588
- "advisorUntrackedContent",
589
- "alwaysOn",
590
- "contextMaxChars",
591
- "executor",
592
- "executorEffort",
593
- "gateFailureMode",
594
- "showUsageDetails",
595
- "showUsageFooter",
596
- "simpleMode"
597
- ];
598
- var currentConfigState = () => ({
599
- advisor: configuredModelRef(advisorRef),
600
- advisorAutoLoopGate: advisorAutoLoopGateRef,
601
- advisorBlockOnBlocked: advisorBlockOnBlockedRef,
602
- advisorCollapseResponses: advisorCollapseResponsesRef,
603
- advisorCompletionGate: advisorCompletionGateRef,
604
- advisorCustomInvocation: advisorCustomInvocationRef,
605
- advisorEffort: advisorEffortRef,
606
- advisorFailureGate: advisorFailureGateRef,
607
- advisorGitContext: advisorGitContextRef,
608
- advisorGitContextMaxChars: advisorGitContextMaxCharsRef,
609
- advisorHerdrIntegration: advisorHerdrIntegrationRef,
610
- advisorLoopThreshold: advisorLoopThresholdRef,
611
- advisorMaxCallsPerSession: advisorMaxCallsPerSessionRef,
612
- advisorPlanGate: advisorPlanGateRef,
613
- advisorRedactSecrets: advisorRedactSecretsRef,
614
- advisorScoutEnabled: advisorScoutEnabledRef,
615
- advisorSessionSummary: advisorSessionSummaryRef,
616
- advisorToolPolicies: { ...advisorToolPoliciesRef },
617
- advisorToolResultMaxBytes: advisorToolResultMaxBytesRef,
618
- advisorToolResultMaxLines: advisorToolResultMaxLinesRef,
619
- advisorTrackedFileContent: advisorTrackedFileContentRef,
620
- advisorUntrackedContent: advisorUntrackedContentRef,
621
- alwaysOn: alwaysOnRef,
622
- contextMaxChars: contextMaxCharsRef,
623
- executor: configuredModelRef(executorRef),
624
- executorEffort: executorEffortRef,
625
- gateFailureMode: advisorFailureModeRef,
626
- showUsageDetails: showUsageDetailsRef,
627
- showUsageFooter: showUsageFooterRef,
628
- simpleMode: simpleModeRef
629
- });
691
+ var currentConfigState = () => Object.fromEntries(SAVED_CONFIG_KEYS.map((key) => [key, CONFIG_SCHEMA[key].current()]));
630
692
  var sameConfigValue = (left, right) => JSON.stringify(left) === JSON.stringify(right);
631
693
  var readExistingConfig = (path) => {
632
694
  try {
@@ -654,13 +716,9 @@ var applyChangedConfigValues = (data, current, changedKeys, persistAdvisor, pers
654
716
  var loadedConfigState;
655
717
  var loadedConfigPath;
656
718
  var readConfig = (path) => {
657
- try {
658
- const config = JSON.parse(readFileSync(path, "utf8"));
659
- validateConfig(config, path);
660
- return config;
661
- } catch (error) {
662
- throw error instanceof Error ? error : new Error(String(error));
663
- }
719
+ const config = JSON.parse(readFileSync(path, "utf8"));
720
+ validateConfig(config, path);
721
+ return config;
664
722
  };
665
723
  var configCache = new Map;
666
724
  var warnedUnknownConfigIdentities = new Set;
@@ -1039,26 +1097,30 @@ var selectAdvisorModels = async (ctx, options) => {
1039
1097
  // src/herdr.ts
1040
1098
  import net from "node:net";
1041
1099
 
1042
- // src/conversation.ts
1043
- var isRecord = (value) => Boolean(value) && typeof value === "object";
1100
+ // src/content-utils.ts
1101
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
1102
+ var byteLength = (value) => Buffer.byteLength(value, "utf8");
1044
1103
  var contentParts = (content) => {
1045
1104
  if (typeof content === "string") {
1046
1105
  return [content];
1047
1106
  }
1048
1107
  return Array.isArray(content) ? content : [];
1049
1108
  };
1050
- var textFromPart = (part) => {
1051
- if (typeof part === "string") {
1052
- return part;
1053
- }
1054
- if (!isRecord(part) || part.type !== "text") {
1055
- return "";
1109
+ var capUtf8Bytes = (value, maxBytes) => {
1110
+ let result = "";
1111
+ let used = 0;
1112
+ for (const character of value) {
1113
+ const characterBytes = byteLength(character);
1114
+ if (used + characterBytes > maxBytes) {
1115
+ break;
1116
+ }
1117
+ result += character;
1118
+ used += characterBytes;
1056
1119
  }
1057
- return typeof part.text === "string" ? part.text : "";
1120
+ return result;
1058
1121
  };
1059
- var textFrom = (content) => contentParts(content).map(textFromPart).join(`
1060
- `).trim();
1061
- var byteLength = (value) => Buffer.byteLength(value, "utf8");
1122
+
1123
+ // src/redaction.ts
1062
1124
  var REDACTION_MARKER = "[REDACTED SECRET]";
1063
1125
  var PEM_BEGIN_PATTERN = /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----/gi;
1064
1126
  var PEM_END_PATTERN = /-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/i;
@@ -1088,263 +1150,85 @@ var redactSecrets = (value) => {
1088
1150
  };
1089
1151
  var redactAndCapText = (value, maxBytes, redact = true) => {
1090
1152
  const source = redact ? redactSecrets(value) : value;
1091
- let result = "";
1092
- for (const character of source) {
1093
- if (byteLength(result + character) > maxBytes) {
1094
- break;
1095
- }
1096
- result += character;
1097
- }
1098
- return result;
1153
+ return capUtf8Bytes(source, maxBytes);
1099
1154
  };
1100
- var capToolResult = (value, maxLines = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES, maxBytes = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES) => {
1101
- const lines = value.split(`
1102
- `);
1103
- const totalLines = lines.length;
1104
- const totalBytes = byteLength(value);
1105
- if ((maxLines === 0 || maxBytes === 0) && value.length > 0) {
1106
- return {
1107
- content: "[Tool result omitted: configured limit is zero]",
1108
- omittedLines: totalLines,
1109
- totalBytes,
1110
- totalLines,
1111
- truncated: true
1112
- };
1155
+
1156
+ // src/herdr.ts
1157
+ var HERDR_NOTIFICATION_METHOD = "notification.show";
1158
+ var SOURCE = "pi-advisor:advisor-activity";
1159
+ var BLOCK_SOURCE = "pi-advisor:advisor-block";
1160
+ var NOTIFICATION_SOURCE = "pi-advisor:advisor-notification";
1161
+ var HERDR_PI_SOURCE = "herdr:pi";
1162
+ var sequence = Date.now() * 1000;
1163
+ var nextSequence = () => {
1164
+ sequence += 1;
1165
+ return sequence;
1166
+ };
1167
+ var emitBlocked;
1168
+ var setHerdrBlockedEmitter = (emitter) => {
1169
+ emitBlocked = emitter;
1170
+ };
1171
+ var safeEmitBlocked = (active, label = "Advisor blocked") => {
1172
+ try {
1173
+ emitBlocked?.(active, label);
1174
+ } catch {}
1175
+ };
1176
+ var isControlCharacter = (character) => character <= "\x1F" || character === "";
1177
+ var cleanNotification = (value, max) => [...redactSecrets(value)].map((character) => isControlCharacter(character) ? " " : character).join("").replace(/\s+/g, " ").trim().slice(0, max);
1178
+ var createHerdrNotificationRequest = (title, body) => ({
1179
+ id: `${NOTIFICATION_SOURCE}:${nextSequence()}`,
1180
+ method: HERDR_NOTIFICATION_METHOD,
1181
+ params: {
1182
+ body: cleanNotification(body, 240),
1183
+ position: "top-left",
1184
+ sound: "request",
1185
+ title: cleanNotification(title, 80)
1113
1186
  }
1114
- if (totalLines <= maxLines && totalBytes <= maxBytes) {
1115
- return {
1116
- content: value,
1117
- omittedLines: 0,
1118
- totalBytes,
1119
- totalLines,
1120
- truncated: false
1121
- };
1187
+ });
1188
+ var sendToHerdr = (request) => {
1189
+ if (process.env.HERDR_ENV !== "1") {
1190
+ return;
1122
1191
  }
1123
- const marker = "[... omitted tool-result section ...]";
1124
- const markerBytes = byteLength(marker);
1125
- if (maxBytes < markerBytes || maxLines === 1) {
1126
- const content = [...marker].reduce((result, character) => byteLength(result + character) <= maxBytes ? result + character : result, "");
1127
- return {
1128
- content,
1129
- omittedLines: totalLines,
1130
- totalBytes,
1131
- totalLines,
1132
- truncated: true
1133
- };
1192
+ const paneId = process.env.HERDR_PANE_ID;
1193
+ const socketPath = process.env.HERDR_SOCKET_PATH;
1194
+ if (!(paneId && socketPath)) {
1195
+ return;
1134
1196
  }
1135
- const headCount = Math.floor((maxLines - 1) / 2);
1136
- const tailCount = maxLines - 1 - headCount;
1137
- const collect = (candidates, maxEntries, maxContentBytes) => {
1138
- const selected = [];
1139
- let used = 0;
1140
- for (const line of candidates.slice(0, maxEntries)) {
1141
- const next = used + byteLength(line) + (selected.length ? 1 : 0);
1142
- if (next > maxContentBytes) {
1143
- break;
1144
- }
1145
- selected.push(line);
1146
- used = next;
1147
- }
1148
- return selected;
1149
- };
1150
- const availableBytes = maxBytes - markerBytes - 2;
1151
- const head = collect(lines, headCount, Math.floor(availableBytes / 2));
1152
- const tail = collect(lines.slice(Math.max(head.length, lines.length - tailCount)), tailCount, availableBytes - byteLength(head.join(`
1153
- `)));
1154
- const content = [...head, marker, ...tail].join(`
1155
- `);
1156
- return {
1157
- content,
1158
- omittedLines: Math.max(0, totalLines - head.length - tail.length),
1159
- totalBytes,
1160
- totalLines,
1161
- truncated: true
1162
- };
1197
+ const endpoint = process.platform === "win32" ? `\\\\.\\pipe\\${socketPath}` : socketPath;
1198
+ const socket = net.createConnection(endpoint);
1199
+ const timeout = setTimeout(() => socket.destroy(), 500);
1200
+ timeout.unref?.();
1201
+ socket.once("connect", () => socket.write(`${JSON.stringify(request)}
1202
+ `));
1203
+ socket.once("data", () => socket.destroy());
1204
+ socket.once("error", () => socket.destroy());
1205
+ socket.once("close", () => clearTimeout(timeout));
1163
1206
  };
1164
- var assistantEntry = (message, policies, redact) => {
1165
- const parts = [];
1166
- const text = textFrom(message.content);
1167
- if (text) {
1168
- parts.push(redact ? redactSecrets(text) : text);
1207
+
1208
+ class HerdrAdvisorActivity {
1209
+ #activeConsultations = 0;
1210
+ report;
1211
+ enabled;
1212
+ constructor(report = sendToHerdr, enabled = () => true) {
1213
+ this.report = report;
1214
+ this.enabled = enabled;
1169
1215
  }
1170
- for (const part of contentParts(message.content)) {
1171
- if (!isRecord(part) || part.type !== "toolCall") {
1172
- continue;
1216
+ start() {
1217
+ if (!this.enabled()) {
1218
+ return;
1173
1219
  }
1174
- const toolName = typeof part.name === "string" ? part.name : "unknown";
1175
- const policy = policies[toolName] ?? "full";
1176
- if (policy === "exclude") {
1177
- parts.push(`[Tool Call: ${toolName}] (excluded by Advisor tool policy)`);
1178
- continue;
1220
+ this.#activeConsultations += 1;
1221
+ if (this.#activeConsultations === 1) {
1222
+ this.safeReport(false);
1179
1223
  }
1180
- if (policy === "summary") {
1181
- parts.push(`[Tool Call: ${toolName}] (arguments omitted by Advisor tool policy: summary)`);
1182
- continue;
1183
- }
1184
- const argumentsText = JSON.stringify(part.arguments) ?? "undefined";
1185
- parts.push(`[Tool Call: ${toolName}(${redact ? redactSecrets(argumentsText) : argumentsText})]`);
1186
- }
1187
- return parts.length > 0 ? `Executor: ${parts.join(`
1188
- `)}` : undefined;
1189
- };
1190
- var toolResultEntry = (message, toolResultMaxLines, toolResultMaxBytes, policies, redact) => {
1191
- const status = message.isError ? "error" : "success";
1192
- const toolName = typeof message.toolName === "string" ? message.toolName : "unknown";
1193
- const policy = policies[toolName] ?? "full";
1194
- const source = textFrom(message.content);
1195
- if (policy === "exclude") {
1196
- return `[Tool Result for ${toolName}] (excluded by Advisor tool policy)`;
1197
- }
1198
- if (policy === "summary") {
1199
- const capped = capToolResult(source, toolResultMaxLines, toolResultMaxBytes);
1200
- return `[Tool Result for ${toolName}] (output omitted by Advisor tool policy: summary; status: ${status}; ${capped.totalLines} lines, ${capped.totalBytes} bytes; source output was${capped.truncated ? "" : " not"} truncated)`;
1201
- }
1202
- const disclosed = redact ? redactSecrets(source) : source;
1203
- const capped = capToolResult(disclosed, toolResultMaxLines, toolResultMaxBytes);
1204
- return `[Tool Result for ${toolName}] (${message.isError ? "Error " : ""}output):
1205
- ${capped.content}`;
1206
- };
1207
- var conversationEntry = (entry, toolResultMaxLines, toolResultMaxBytes, policies, redact) => {
1208
- if (!isRecord(entry)) {
1209
- return;
1210
- }
1211
- if (entry.type === "compaction" && typeof entry.summary === "string") {
1212
- return `[System Compaction Summary]: ${redact ? redactSecrets(entry.summary) : entry.summary}`;
1213
- }
1214
- if (entry.type !== "message" || !isRecord(entry.message)) {
1215
- return;
1216
- }
1217
- const { message } = entry;
1218
- if (message.role === "user") {
1219
- const text = textFrom(message.content);
1220
- return text ? `User: ${redact ? redactSecrets(text) : text}` : undefined;
1221
- }
1222
- if (message.role === "assistant") {
1223
- return assistantEntry(message, policies, redact);
1224
- }
1225
- if (message.role === "toolResult" || message.role === "tool") {
1226
- return toolResultEntry(message, toolResultMaxLines, toolResultMaxBytes, policies, redact);
1227
- }
1228
- };
1229
- var selectRecentEntries = (entries, maxChars) => {
1230
- const separator = `
1231
-
1232
- `;
1233
- const joined = entries.join(separator);
1234
- if (joined.length <= maxChars || maxChars === Number.MAX_SAFE_INTEGER) {
1235
- return joined;
1236
- }
1237
- const newestTruncated = "[Newest entry truncated]";
1238
- if (entries.length === 1) {
1239
- const prefix = `${newestTruncated}${separator}`;
1240
- return `${prefix}${entries[0].slice(0, Math.max(0, maxChars - prefix.length))}`.slice(0, maxChars);
1241
- }
1242
- const selected = [];
1243
- let selectedLength = 0;
1244
- for (let index = entries.length - 1;index >= 0; index -= 1) {
1245
- const entry = entries[index];
1246
- const candidateCount = selected.length + 1;
1247
- const omitted = entries.length - candidateCount;
1248
- const marker = `[Older context omitted: ${omitted} complete entr${omitted === 1 ? "y" : "ies"}]`;
1249
- const candidateLength = selectedLength + entry.length + (selected.length > 0 ? separator.length : 0);
1250
- if (marker.length + separator.length + candidateLength > maxChars) {
1251
- break;
1252
- }
1253
- selected.unshift(entry);
1254
- selectedLength = candidateLength;
1255
- }
1256
- const omitted = entries.length - Math.max(1, selected.length);
1257
- const marker = `[Older context omitted: ${omitted} complete entr${omitted === 1 ? "y" : "ies"}]`;
1258
- if (selected.length > 0) {
1259
- return `${marker}${separator}${selected.join(separator)}`;
1260
- }
1261
- const prefix = `${marker}${separator}${newestTruncated}${separator}`;
1262
- return `${prefix}${entries.at(-1)?.slice(0, Math.max(0, maxChars - prefix.length)) ?? ""}`.slice(0, maxChars);
1263
- };
1264
- var recentConversation = (ctx, maxChars = 15000, toolResultMaxLines = advisorToolResultMaxLinesRef, toolResultMaxBytes = advisorToolResultMaxBytesRef, policies = advisorToolPoliciesRef, redact = advisorRedactSecretsRef) => {
1265
- if (maxChars === 0) {
1266
- return "";
1267
- }
1268
- const entries = ctx.sessionManager.getBranch().map((entry) => conversationEntry(entry, toolResultMaxLines, toolResultMaxBytes, policies, redact)).filter((entry) => entry !== undefined);
1269
- return selectRecentEntries(entries, maxChars);
1270
- };
1271
-
1272
- // src/herdr.ts
1273
- var HERDR_NOTIFICATION_METHOD = `${"notification"}.${"show"}`;
1274
- var SOURCE = "pi-advisor:advisor-activity";
1275
- var BLOCK_SOURCE = "pi-advisor:advisor-block";
1276
- var NOTIFICATION_SOURCE = "pi-advisor:advisor-notification";
1277
- var HERDR_PI_SOURCE = "herdr:pi";
1278
- var sequence = Date.now() * 1000;
1279
- var nextSequence = () => {
1280
- sequence += 1;
1281
- return sequence;
1282
- };
1283
- var emitBlocked;
1284
- var setHerdrBlockedEmitter = (emitter) => {
1285
- emitBlocked = emitter;
1286
- };
1287
- var safeEmitBlocked = (active, label = "Advisor blocked") => {
1288
- try {
1289
- emitBlocked?.(active, label);
1290
- } catch {}
1291
- };
1292
- var isControlCharacter = (character) => character <= "\x1F" || character === "";
1293
- var cleanNotification = (value, max) => [...redactSecrets(value)].map((character) => isControlCharacter(character) ? " " : character).join("").replace(/\s+/g, " ").trim().slice(0, max);
1294
- var createHerdrNotificationRequest = (title, body) => ({
1295
- id: `${NOTIFICATION_SOURCE}:${nextSequence()}`,
1296
- method: HERDR_NOTIFICATION_METHOD,
1297
- params: {
1298
- body: cleanNotification(body, 240),
1299
- position: "top-left",
1300
- sound: "request",
1301
- title: cleanNotification(title, 80)
1302
- }
1303
- });
1304
- var sendToHerdr = (request) => {
1305
- if (process.env.HERDR_ENV !== "1") {
1306
- return;
1307
- }
1308
- const paneId = process.env.HERDR_PANE_ID;
1309
- const socketPath = process.env.HERDR_SOCKET_PATH;
1310
- if (!(paneId && socketPath)) {
1311
- return;
1312
- }
1313
- const endpoint = process.platform === "win32" ? `\\\\.\\pipe\\${socketPath}` : socketPath;
1314
- const socket = net.createConnection(endpoint);
1315
- const timeout = setTimeout(() => socket.destroy(), 500);
1316
- timeout.unref?.();
1317
- socket.once("connect", () => socket.write(`${JSON.stringify(request)}
1318
- `));
1319
- socket.once("data", () => socket.destroy());
1320
- socket.once("error", () => socket.destroy());
1321
- socket.once("close", () => clearTimeout(timeout));
1322
- };
1323
-
1324
- class HerdrAdvisorActivity {
1325
- #activeConsultations = 0;
1326
- report;
1327
- enabled;
1328
- constructor(report = sendToHerdr, enabled = () => true) {
1329
- this.report = report;
1330
- this.enabled = enabled;
1331
- }
1332
- start() {
1333
- if (!this.enabled()) {
1334
- return;
1335
- }
1336
- this.#activeConsultations += 1;
1337
- if (this.#activeConsultations === 1) {
1338
- this.safeReport(false);
1339
- }
1340
- }
1341
- finish() {
1342
- if (this.#activeConsultations === 0) {
1343
- return;
1344
- }
1345
- this.#activeConsultations -= 1;
1346
- if (this.#activeConsultations === 0) {
1347
- this.safeReport(true);
1224
+ }
1225
+ finish() {
1226
+ if (this.#activeConsultations === 0) {
1227
+ return;
1228
+ }
1229
+ this.#activeConsultations -= 1;
1230
+ if (this.#activeConsultations === 0) {
1231
+ this.safeReport(true);
1348
1232
  }
1349
1233
  }
1350
1234
  clear() {
@@ -1596,57 +1480,444 @@ var collectTextStream = async (resolved, options, streamModel = stream) => {
1596
1480
  };
1597
1481
  };
1598
1482
 
1599
- // src/preferences.ts
1483
+ // src/attachments.ts
1484
+ import { execFileSync as execFileSync2 } from "node:child_process";
1485
+ import { constants } from "node:fs";
1600
1486
  import { lstat, open, realpath } from "node:fs/promises";
1601
- import { join as join2, relative } from "node:path";
1602
- var PREFERENCES_MAX_BYTES = 8 * 1024;
1603
- var PREFERENCES_FILENAME = ["advisor-preferences", "md"].join(".");
1604
- var inside = (root, candidate) => {
1487
+ import { isAbsolute, relative, resolve } from "node:path";
1488
+ var ADVISOR_FILE_MAX_BYTES = 8 * 1024;
1489
+ var ADVISOR_FILES_TOTAL_MAX_BYTES = 24 * 1024;
1490
+ var PATH_SEGMENTS = /[\\/]/;
1491
+ var within = (root, candidate) => {
1605
1492
  const path = relative(root, candidate);
1606
- return path === "" || !(path.startsWith("..") || path.includes("../"));
1493
+ return path !== "" && !path.startsWith("..") && !path.includes("../");
1607
1494
  };
1608
- var readProjectPreferences = async (ctx, maxBytes = PREFERENCES_MAX_BYTES, redact = true) => {
1609
- if (!ctx.isProjectTrusted()) {
1495
+ var normalizeRelativePath = (root, path) => relative(root, resolve(root, path));
1496
+ var normalizeRequestedPath = (root, value) => {
1497
+ if (typeof value !== "string" || !value || isAbsolute(value) || value.split(PATH_SEGMENTS).includes("..")) {
1610
1498
  return;
1611
1499
  }
1500
+ return normalizeRelativePath(root, value);
1501
+ };
1502
+ var git = (cwd, args) => execFileSync2("git", args, {
1503
+ cwd,
1504
+ encoding: "utf8",
1505
+ maxBuffer: 16 * 1024 * 1024,
1506
+ shell: false,
1507
+ stdio: ["ignore", "pipe", "pipe"],
1508
+ timeout: 5000,
1509
+ windowsHide: true
1510
+ });
1511
+ var repositoryRoot = (cwd) => {
1612
1512
  try {
1613
- const root = await realpath(ctx.cwd);
1614
- const candidate = join2(ctx.cwd, ".pi", PREFERENCES_FILENAME);
1615
- const stats = await lstat(candidate);
1616
- if (stats.isSymbolicLink() || !stats.isFile()) {
1617
- return;
1513
+ return realpath(git(cwd, ["rev-parse", "--show-toplevel"]).trim());
1514
+ } catch {
1515
+ return Promise.resolve(undefined);
1516
+ }
1517
+ };
1518
+ var untracked = (cwd, path) => {
1519
+ const output = git(cwd, [
1520
+ "ls-files",
1521
+ "--others",
1522
+ "--exclude-standard",
1523
+ "-z",
1524
+ "--",
1525
+ path
1526
+ ]);
1527
+ const expected = normalizeRelativePath(cwd, path);
1528
+ return output.split("\x00").filter(Boolean).some((entry) => normalizeRelativePath(cwd, entry) === expected);
1529
+ };
1530
+ var tracked = (cwd, path) => {
1531
+ const output = git(cwd, ["ls-files", "--stage", "-z", "--", path]);
1532
+ const expected = normalizeRelativePath(cwd, path);
1533
+ return output.split("\x00").some((entry) => {
1534
+ if (!entry) {
1535
+ return false;
1618
1536
  }
1619
- const resolved = await realpath(candidate);
1620
- if (!inside(root, resolved)) {
1537
+ const [metadata, name] = entry.split("\t");
1538
+ return name && normalizeRelativePath(cwd, name) === expected && !metadata.startsWith("160000 ");
1539
+ });
1540
+ };
1541
+ var isPermitted = (root, path, kind) => kind === "tracked" ? tracked(root, path) : untracked(root, path);
1542
+ var readAttachment = async (root, normalizedName, redact, available) => {
1543
+ const absolute = resolve(root, normalizedName);
1544
+ if (!(within(root, absolute) && available > 0)) {
1545
+ return;
1546
+ }
1547
+ const stats = await lstat(absolute);
1548
+ if (stats.isSymbolicLink() || !stats.isFile()) {
1549
+ return;
1550
+ }
1551
+ const resolved = await realpath(absolute);
1552
+ if (!within(root, resolved)) {
1553
+ return;
1554
+ }
1555
+ const flags = constants.O_NOFOLLOW ? constants.O_RDONLY | constants.O_NOFOLLOW : constants.O_RDONLY;
1556
+ const file = await open(resolved, flags);
1557
+ try {
1558
+ const openedStats = await file.stat();
1559
+ if (!openedStats.isFile()) {
1621
1560
  return;
1622
1561
  }
1623
- const file = await open(resolved, "r");
1624
- try {
1625
- const buffer = Buffer.alloc(maxBytes + 1);
1626
- const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
1627
- const source = buffer.subarray(0, bytesRead).toString("utf8");
1628
- const capped = redactAndCapText(source, maxBytes, redact);
1629
- return { bytes: Buffer.byteLength(capped, "utf8"), text: capped };
1630
- } finally {
1631
- await file.close();
1562
+ const buffer = Buffer.alloc(available + 1);
1563
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
1564
+ const raw = buffer.subarray(0, bytesRead).toString("utf8");
1565
+ if (raw.includes("\x00")) {
1566
+ return;
1632
1567
  }
1633
- } catch {}
1568
+ const text = redactAndCapText(raw, available, redact);
1569
+ return {
1570
+ bytes: Buffer.byteLength(text, "utf8"),
1571
+ path: normalizedName,
1572
+ text
1573
+ };
1574
+ } finally {
1575
+ await file.close();
1576
+ }
1634
1577
  };
1635
-
1636
- // src/scout-context.ts
1637
- import { createHash } from "node:crypto";
1638
- var SCOUT_MANIFEST_MAX_BYTES = 64 * 1024;
1578
+ var readFiles = async (cwd, requested, enabled, redact, kind, totalLimit = ADVISOR_FILES_TOTAL_MAX_BYTES) => {
1579
+ if (!(enabled && Array.isArray(requested))) {
1580
+ return [];
1581
+ }
1582
+ const root = await repositoryRoot(cwd);
1583
+ if (!root) {
1584
+ return [];
1585
+ }
1586
+ const unique = new Set;
1587
+ const attachments = [];
1588
+ let total = 0;
1589
+ for (const name of requested) {
1590
+ const normalizedName = normalizeRequestedPath(root, name);
1591
+ if (!normalizedName || unique.has(normalizedName)) {
1592
+ continue;
1593
+ }
1594
+ unique.add(normalizedName);
1595
+ try {
1596
+ if (!isPermitted(root, normalizedName, kind)) {
1597
+ continue;
1598
+ }
1599
+ const available = Math.min(ADVISOR_FILE_MAX_BYTES, totalLimit - total);
1600
+ if (available <= 0) {
1601
+ break;
1602
+ }
1603
+ const attachment = await readAttachment(root, normalizedName, redact, available);
1604
+ if (attachment) {
1605
+ attachments.push(attachment);
1606
+ total += attachment.bytes;
1607
+ }
1608
+ } catch {}
1609
+ }
1610
+ return attachments;
1611
+ };
1612
+ var readTrackedFiles = (cwd, requested, enabled, redact, totalLimit = ADVISOR_FILES_TOTAL_MAX_BYTES) => readFiles(cwd, requested, enabled, redact, "tracked", totalLimit);
1613
+ var readUntrackedFiles = (cwd, requested, enabled, redact, totalLimit = ADVISOR_FILES_TOTAL_MAX_BYTES) => readFiles(cwd, requested, enabled, redact, "untracked", totalLimit);
1614
+
1615
+ // src/preferences.ts
1616
+ import { lstat as lstat2, open as open2, realpath as realpath2 } from "node:fs/promises";
1617
+ import { join as join2, relative as relative2 } from "node:path";
1618
+ var PREFERENCES_MAX_BYTES = 8 * 1024;
1619
+ var PREFERENCES_FILENAME = ["advisor-preferences", "md"].join(".");
1620
+ var inside = (root, candidate) => {
1621
+ const path = relative2(root, candidate);
1622
+ return path === "" || !(path.startsWith("..") || path.includes("../"));
1623
+ };
1624
+ var readProjectPreferences = async (ctx, maxBytes = PREFERENCES_MAX_BYTES, redact = true) => {
1625
+ if (!ctx.isProjectTrusted()) {
1626
+ return;
1627
+ }
1628
+ try {
1629
+ const root = await realpath2(ctx.cwd);
1630
+ const candidate = join2(ctx.cwd, ".pi", PREFERENCES_FILENAME);
1631
+ const stats = await lstat2(candidate);
1632
+ if (stats.isSymbolicLink() || !stats.isFile()) {
1633
+ return;
1634
+ }
1635
+ const resolved = await realpath2(candidate);
1636
+ if (!inside(root, resolved)) {
1637
+ return;
1638
+ }
1639
+ const file = await open2(resolved, "r");
1640
+ try {
1641
+ const buffer = Buffer.alloc(maxBytes + 1);
1642
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
1643
+ const source = buffer.subarray(0, bytesRead).toString("utf8");
1644
+ const capped = redactAndCapText(source, maxBytes, redact);
1645
+ return { bytes: Buffer.byteLength(capped, "utf8"), text: capped };
1646
+ } finally {
1647
+ await file.close();
1648
+ }
1649
+ } catch {}
1650
+ };
1651
+
1652
+ // src/scout-groups.ts
1653
+ import { createHash } from "node:crypto";
1654
+
1655
+ // src/tool-result-cap.ts
1656
+ var OMITTED_MARKER = "[... omitted tool-result section ...]";
1657
+ var capToolResult = (value, maxLines = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES, maxBytes = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES) => {
1658
+ const lines = value.split(`
1659
+ `);
1660
+ const totalLines = lines.length;
1661
+ const totalBytes = byteLength(value);
1662
+ if ((maxLines === 0 || maxBytes === 0) && value.length > 0) {
1663
+ return {
1664
+ content: "[Tool result omitted: configured limit is zero]",
1665
+ omittedLines: totalLines,
1666
+ totalBytes,
1667
+ totalLines,
1668
+ truncated: true
1669
+ };
1670
+ }
1671
+ if (totalLines <= maxLines && totalBytes <= maxBytes) {
1672
+ return {
1673
+ content: value,
1674
+ omittedLines: 0,
1675
+ totalBytes,
1676
+ totalLines,
1677
+ truncated: false
1678
+ };
1679
+ }
1680
+ const markerBytes = byteLength(OMITTED_MARKER);
1681
+ if (maxBytes < markerBytes || maxLines === 1) {
1682
+ return {
1683
+ content: capUtf8Bytes(OMITTED_MARKER, maxBytes),
1684
+ omittedLines: totalLines,
1685
+ totalBytes,
1686
+ totalLines,
1687
+ truncated: true
1688
+ };
1689
+ }
1690
+ const headCount = Math.floor((maxLines - 1) / 2);
1691
+ const tailCount = maxLines - 1 - headCount;
1692
+ const collect = (candidates, maxEntries, maxContentBytes) => {
1693
+ const selected = [];
1694
+ let used = 0;
1695
+ for (const line of candidates.slice(0, maxEntries)) {
1696
+ const next = used + byteLength(line) + (selected.length ? 1 : 0);
1697
+ if (next > maxContentBytes) {
1698
+ break;
1699
+ }
1700
+ selected.push(line);
1701
+ used = next;
1702
+ }
1703
+ return selected;
1704
+ };
1705
+ const availableBytes = maxBytes - markerBytes - 2;
1706
+ const head = collect(lines, headCount, Math.floor(availableBytes / 2));
1707
+ const tail = collect(lines.slice(Math.max(head.length, lines.length - tailCount)), tailCount, availableBytes - byteLength(head.join(`
1708
+ `)));
1709
+ const content = [...head, OMITTED_MARKER, ...tail].join(`
1710
+ `);
1711
+ return {
1712
+ content,
1713
+ omittedLines: Math.max(0, totalLines - head.length - tail.length),
1714
+ totalBytes,
1715
+ totalLines,
1716
+ truncated: true
1717
+ };
1718
+ };
1719
+
1720
+ // src/conversation.ts
1721
+ var textFromPart = (part) => {
1722
+ if (typeof part === "string") {
1723
+ return part;
1724
+ }
1725
+ if (!isRecord(part) || part.type !== "text") {
1726
+ return "";
1727
+ }
1728
+ return typeof part.text === "string" ? part.text : "";
1729
+ };
1730
+ var textFrom = (content) => contentParts(content).map(textFromPart).join(`
1731
+ `).trim();
1732
+ var assistantEntry = (message, policies, redact) => {
1733
+ const parts = [];
1734
+ const text = textFrom(message.content);
1735
+ if (text) {
1736
+ parts.push(redact ? redactSecrets(text) : text);
1737
+ }
1738
+ for (const part of contentParts(message.content)) {
1739
+ if (!isRecord(part) || part.type !== "toolCall") {
1740
+ continue;
1741
+ }
1742
+ const toolName = typeof part.name === "string" ? part.name : "unknown";
1743
+ const policy = policies[toolName] ?? "full";
1744
+ if (policy === "exclude") {
1745
+ parts.push(`[Tool Call: ${toolName}] (excluded by Advisor tool policy)`);
1746
+ continue;
1747
+ }
1748
+ if (policy === "summary") {
1749
+ parts.push(`[Tool Call: ${toolName}] (arguments omitted by Advisor tool policy: summary)`);
1750
+ continue;
1751
+ }
1752
+ const argumentsText = JSON.stringify(part.arguments) ?? "undefined";
1753
+ parts.push(`[Tool Call: ${toolName}(${redact ? redactSecrets(argumentsText) : argumentsText})]`);
1754
+ }
1755
+ return parts.length > 0 ? `Executor: ${parts.join(`
1756
+ `)}` : undefined;
1757
+ };
1758
+ var toolResultEntry = (message, toolResultMaxLines, toolResultMaxBytes, policies, redact) => {
1759
+ const status = message.isError ? "error" : "success";
1760
+ const toolName = typeof message.toolName === "string" ? message.toolName : "unknown";
1761
+ const policy = policies[toolName] ?? "full";
1762
+ const source = textFrom(message.content);
1763
+ if (policy === "exclude") {
1764
+ return `[Tool Result for ${toolName}] (excluded by Advisor tool policy)`;
1765
+ }
1766
+ if (policy === "summary") {
1767
+ const capped = capToolResult(source, toolResultMaxLines, toolResultMaxBytes);
1768
+ return `[Tool Result for ${toolName}] (output omitted by Advisor tool policy: summary; status: ${status}; ${capped.totalLines} lines, ${capped.totalBytes} bytes; source output was${capped.truncated ? "" : " not"} truncated)`;
1769
+ }
1770
+ const disclosed = redact ? redactSecrets(source) : source;
1771
+ const capped = capToolResult(disclosed, toolResultMaxLines, toolResultMaxBytes);
1772
+ return `[Tool Result for ${toolName}] (${message.isError ? "Error " : ""}output):
1773
+ ${capped.content}`;
1774
+ };
1775
+ var conversationEntry = (entry, toolResultMaxLines, toolResultMaxBytes, policies, redact) => {
1776
+ if (!isRecord(entry)) {
1777
+ return;
1778
+ }
1779
+ if (entry.type === "compaction" && typeof entry.summary === "string") {
1780
+ return `[System Compaction Summary]: ${redact ? redactSecrets(entry.summary) : entry.summary}`;
1781
+ }
1782
+ if (entry.type !== "message" || !isRecord(entry.message)) {
1783
+ return;
1784
+ }
1785
+ const { message } = entry;
1786
+ if (message.role === "user") {
1787
+ const text = textFrom(message.content);
1788
+ return text ? `User: ${redact ? redactSecrets(text) : text}` : undefined;
1789
+ }
1790
+ if (message.role === "assistant") {
1791
+ return assistantEntry(message, policies, redact);
1792
+ }
1793
+ if (message.role === "toolResult" || message.role === "tool") {
1794
+ return toolResultEntry(message, toolResultMaxLines, toolResultMaxBytes, policies, redact);
1795
+ }
1796
+ };
1797
+ var omissionMarker = (omitted) => `[Older context omitted: ${omitted} complete entr${omitted === 1 ? "y" : "ies"}]`;
1798
+ var selectRecentEntries = (entries, maxChars) => {
1799
+ const separator = `
1800
+
1801
+ `;
1802
+ const joined = entries.join(separator);
1803
+ if (joined.length <= maxChars || maxChars === Number.MAX_SAFE_INTEGER) {
1804
+ return joined;
1805
+ }
1806
+ const newestTruncated = "[Newest entry truncated]";
1807
+ if (entries.length === 1) {
1808
+ const prefix = `${newestTruncated}${separator}`;
1809
+ return `${prefix}${entries[0].slice(0, Math.max(0, maxChars - prefix.length))}`.slice(0, maxChars);
1810
+ }
1811
+ const selected = [];
1812
+ let selectedLength = 0;
1813
+ for (let index = entries.length - 1;index >= 0; index -= 1) {
1814
+ const entry = entries[index];
1815
+ const candidateCount = selected.length + 1;
1816
+ const omitted = entries.length - candidateCount;
1817
+ const candidateLength = selectedLength + entry.length + (selected.length > 0 ? separator.length : 0);
1818
+ if (omissionMarker(omitted).length + separator.length + candidateLength > maxChars) {
1819
+ break;
1820
+ }
1821
+ selected.unshift(entry);
1822
+ selectedLength = candidateLength;
1823
+ }
1824
+ const omitted = entries.length - Math.max(1, selected.length);
1825
+ const marker = omissionMarker(omitted);
1826
+ if (selected.length > 0) {
1827
+ return `${marker}${separator}${selected.join(separator)}`;
1828
+ }
1829
+ const prefix = `${marker}${separator}${newestTruncated}${separator}`;
1830
+ return `${prefix}${entries.at(-1)?.slice(0, Math.max(0, maxChars - prefix.length)) ?? ""}`.slice(0, maxChars);
1831
+ };
1832
+ var recentConversation = (ctx, maxChars = 15000, toolResultMaxLines = advisorToolResultMaxLinesRef, toolResultMaxBytes = advisorToolResultMaxBytesRef, policies = advisorToolPoliciesRef, redact = advisorRedactSecretsRef) => {
1833
+ if (maxChars === 0) {
1834
+ return "";
1835
+ }
1836
+ const entries = ctx.sessionManager.getBranch().map((entry) => conversationEntry(entry, toolResultMaxLines, toolResultMaxBytes, policies, redact)).filter((entry) => entry !== undefined);
1837
+ return selectRecentEntries(entries, maxChars);
1838
+ };
1839
+
1840
+ // src/scout-protocol.ts
1841
+ var invalid = (message) => ({
1842
+ message,
1843
+ ok: false,
1844
+ reason: "invalid-protocol"
1845
+ });
1846
+ var toolCalls = (message) => contentParts(message.content).filter((part) => isRecord(part) && part.type === "toolCall");
1847
+ var toolCallId = (part) => typeof part.id === "string" ? part.id : undefined;
1848
+ var indexToolCalls = (entries) => {
1849
+ const callOwners = new Map;
1850
+ const resultsByCall = new Map;
1851
+ const state = { latestUserIndex: -1 };
1852
+ for (let index = 0;index < entries.length; index += 1) {
1853
+ const failure = indexEntry(entries[index], index, callOwners, resultsByCall, state);
1854
+ if (failure) {
1855
+ return failure;
1856
+ }
1857
+ }
1858
+ for (const [id, results] of resultsByCall) {
1859
+ if (results.length > 1) {
1860
+ return invalid(`Tool call ${id} has duplicate result messages.`);
1861
+ }
1862
+ }
1863
+ return {
1864
+ index: {
1865
+ callOwners,
1866
+ latestUserIndex: state.latestUserIndex,
1867
+ resultsByCall
1868
+ },
1869
+ ok: true
1870
+ };
1871
+ };
1872
+ var indexEntry = (entry, index, callOwners, resultsByCall, state) => {
1873
+ if (entry.type !== "message" || !isRecord(entry.message)) {
1874
+ return;
1875
+ }
1876
+ if (entry.message.role === "user" && textFrom(entry.message.content)) {
1877
+ state.latestUserIndex = index;
1878
+ }
1879
+ if (entry.message.role === "assistant") {
1880
+ return indexAssistantCalls(entry.message, index, callOwners);
1881
+ }
1882
+ if (entry.message.role === "toolResult") {
1883
+ return indexToolResult(entry, entry.message, index, resultsByCall);
1884
+ }
1885
+ return;
1886
+ };
1887
+ var indexToolResult = (entry, message, index, resultsByCall) => {
1888
+ const id = message.toolCallId;
1889
+ if (typeof id !== "string") {
1890
+ return invalid(`Tool result at context entry ${index} has no tool-call ID.`);
1891
+ }
1892
+ const results = resultsByCall.get(id) ?? [];
1893
+ results.push({ entry, index });
1894
+ resultsByCall.set(id, results);
1895
+ return;
1896
+ };
1897
+ var indexAssistantCalls = (message, index, callOwners) => {
1898
+ for (const call of toolCalls(message)) {
1899
+ const id = toolCallId(call);
1900
+ if (!id || callOwners.has(id)) {
1901
+ return invalid(`Assistant tool calls at context entry ${index} have missing or duplicate IDs.`);
1902
+ }
1903
+ callOwners.set(id, {
1904
+ index,
1905
+ name: typeof call.name === "string" ? call.name : "unknown"
1906
+ });
1907
+ }
1908
+ return;
1909
+ };
1910
+
1911
+ // src/scout-types.ts
1912
+ var SCOUT_MANIFEST_MAX_BYTES = 64 * 1024;
1639
1913
  var SCOUT_MANIFEST_MAX_GROUPS = 64;
1640
1914
  var SCOUT_GROUP_MAX_BYTES = 24 * 1024;
1641
1915
  var SCOUT_LABEL_MAX_CHARS = 160;
1642
1916
  var SCOUT_SELECTION_MAX_IDS = 32;
1643
1917
  var SCOUT_SYNTHESIS_MAX_BYTES = 4 * 1024;
1644
- var isRecord2 = (value) => Boolean(value) && typeof value === "object";
1645
- var byteLength2 = (value) => Buffer.byteLength(value, "utf8");
1918
+
1919
+ // src/scout-groups.ts
1646
1920
  var SPEAKER_PREFIX = /^(User|Executor):\s*/;
1647
- var contentParts2 = (content) => Array.isArray(content) ? content : [];
1648
- var toolCalls = (message) => contentParts2(message.content).filter((part) => isRecord2(part) && part.type === "toolCall");
1649
- var toolCallId = (part) => typeof part.id === "string" ? part.id : undefined;
1650
1921
  var boundedLabel = (value) => [...value.replace(/\s+/g, " ").trim()].slice(0, SCOUT_LABEL_MAX_CHARS).join("");
1651
1922
  var labelFor = (kind, content) => {
1652
1923
  const preview = boundedLabel(content.replace(SPEAKER_PREFIX, ""));
@@ -1660,16 +1931,17 @@ var labelFor = (kind, content) => {
1660
1931
  return boundedLabel(`${prefix[kind]}: ${preview || "(no text)"}`);
1661
1932
  };
1662
1933
  var stableId = (index, entryIds, kind, content) => `g_${createHash("sha256").update(JSON.stringify([index, entryIds, kind, content])).digest("hex").slice(0, 16)}`;
1663
- var groupWireBytes = (group) => byteLength2(JSON.stringify({
1934
+ var groupWire = (group) => ({
1664
1935
  bytes: group.bytes,
1665
1936
  content: group.content,
1666
1937
  id: group.id,
1667
1938
  kind: group.kind,
1668
1939
  label: group.label,
1669
1940
  required: group.required
1670
- }));
1941
+ });
1942
+ var groupWireBytes = (group) => byteLength(JSON.stringify(groupWire(group)));
1671
1943
  var createGroup = (originalIndex, entryIds, kind, content, required) => ({
1672
- bytes: byteLength2(content),
1944
+ bytes: byteLength(content),
1673
1945
  content,
1674
1946
  id: stableId(originalIndex, entryIds, kind, content),
1675
1947
  kind,
@@ -1678,7 +1950,7 @@ var createGroup = (originalIndex, entryIds, kind, content, required) => ({
1678
1950
  required
1679
1951
  });
1680
1952
  var pendingAdvisorArguments = (value) => {
1681
- if (!isRecord2(value)) {
1953
+ if (!isRecord(value)) {
1682
1954
  return {};
1683
1955
  }
1684
1956
  const allowed = {};
@@ -1690,241 +1962,166 @@ var pendingAdvisorArguments = (value) => {
1690
1962
  return allowed;
1691
1963
  };
1692
1964
  var pendingInvocationDisclosure = (entry, invocationId, toolResultMaxLines, toolResultMaxBytes, policies, redact, disclosed) => {
1693
- if (!isRecord2(entry.message)) {
1965
+ if (!isRecord(entry.message)) {
1694
1966
  return disclosed;
1695
1967
  }
1696
- const content = contentParts2(entry.message.content).map((part) => {
1697
- if (!isRecord2(part) || part.type !== "toolCall" || toolCallId(part) !== invocationId || part.name !== "ask_advisor") {
1968
+ const content = contentParts(entry.message.content).map((part) => {
1969
+ if (!isRecord(part) || part.type !== "toolCall" || toolCallId(part) !== invocationId || part.name !== "ask_advisor") {
1698
1970
  return part;
1699
1971
  }
1700
1972
  return { ...part, arguments: pendingAdvisorArguments(part.arguments) };
1701
1973
  });
1702
1974
  return conversationEntry({ ...entry, message: { ...entry.message, content } }, toolResultMaxLines, toolResultMaxBytes, policies, redact);
1703
1975
  };
1704
- var buildScoutManifest = (ctx, options = {}) => {
1705
- const entries = ctx.sessionManager.buildContextEntries();
1706
- const policies = options.policies ?? advisorToolPoliciesRef;
1707
- const redact = options.redact ?? advisorRedactSecretsRef;
1708
- const toolResultMaxLines = options.toolResultMaxLines ?? advisorToolResultMaxLinesRef;
1709
- const toolResultMaxBytes = options.toolResultMaxBytes ?? advisorToolResultMaxBytesRef;
1710
- const {
1711
- maxBytes,
1712
- maxConversationChars,
1713
- maxGroupBytes = SCOUT_GROUP_MAX_BYTES,
1714
- maxGroups = SCOUT_MANIFEST_MAX_GROUPS,
1715
- maxManifestBytes = maxBytes ?? SCOUT_MANIFEST_MAX_BYTES
1716
- } = options;
1717
- let latestUserIndex = -1;
1718
- const callOwners = new Map;
1719
- const resultsByCall = new Map;
1720
- for (let index = 0;index < entries.length; index += 1) {
1721
- const entry = entries[index];
1722
- if (entry.type !== "message" || !isRecord2(entry.message)) {
1976
+ var toolExchangeGroup = (entry, index, entryId, disclosed, immediate, indexed, consumedResultIndexes, caps) => {
1977
+ const calls = toolCalls(entry.message);
1978
+ const callIds = calls.map(toolCallId);
1979
+ const adjacentFailure = adjacentResultMismatch(immediate, index, callIds, indexed.callOwners);
1980
+ if (adjacentFailure) {
1981
+ return { kind: "invalid", message: adjacentFailure };
1982
+ }
1983
+ const missing = new Set;
1984
+ const resultParts = [];
1985
+ const resultEntryIds = [];
1986
+ const matchFailure = collectResults(calls, callIds, index, indexed.resultsByCall, consumedResultIndexes, caps, missing, resultParts, resultEntryIds);
1987
+ if (matchFailure) {
1988
+ return { kind: "invalid", message: matchFailure };
1989
+ }
1990
+ if (missing.size > 0) {
1991
+ return missingOutcome(entry, index, entryId, disclosed, callIds, missing, resultParts, resultEntryIds, caps);
1992
+ }
1993
+ return {
1994
+ group: createGroup(index, [entryId, ...resultEntryIds], "tool-exchange", [disclosed, ...resultParts].join(`
1995
+
1996
+ `), false),
1997
+ kind: "group"
1998
+ };
1999
+ };
2000
+ var adjacentResultMismatch = (immediate, index, callIds, callOwners) => {
2001
+ const next = immediate;
2002
+ if (next?.type === "message" && isRecord(next.message) && next.message.role === "toolResult" && typeof next.message.toolCallId === "string" && !callIds.includes(next.message.toolCallId) && !callOwners.has(next.message.toolCallId)) {
2003
+ return `Tool result at context entry ${index + 1} does not match its adjacent assistant group.`;
2004
+ }
2005
+ return;
2006
+ };
2007
+ var collectResults = (calls, callIds, index, resultsByCall, consumedResultIndexes, caps, missing, resultParts, resultEntryIds) => {
2008
+ for (const [callIndex, callId] of callIds.entries()) {
2009
+ const resultMatch = resultsByCall.get(callId)?.[0];
2010
+ if (!resultMatch) {
2011
+ missing.add(callId);
1723
2012
  continue;
1724
2013
  }
1725
- if (entry.message.role === "user" && textFrom(entry.message.content)) {
1726
- latestUserIndex = index;
2014
+ if (resultMatch.index <= index) {
2015
+ return `Tool result at context entry ${resultMatch.index} precedes its assistant call.`;
1727
2016
  }
1728
- if (entry.message.role === "assistant") {
1729
- for (const call of toolCalls(entry.message)) {
1730
- const id = toolCallId(call);
1731
- if (!id || callOwners.has(id)) {
1732
- return {
1733
- message: `Assistant tool calls at context entry ${index} have missing or duplicate IDs.`,
1734
- ok: false,
1735
- reason: "invalid-protocol"
1736
- };
1737
- }
1738
- callOwners.set(id, {
1739
- index,
1740
- name: typeof call.name === "string" ? call.name : "unknown"
1741
- });
1742
- }
1743
- } else if (entry.message.role === "toolResult") {
1744
- const id = entry.message.toolCallId;
1745
- if (typeof id !== "string") {
1746
- return {
1747
- message: `Tool result at context entry ${index} has no tool-call ID.`,
1748
- ok: false,
1749
- reason: "invalid-protocol"
1750
- };
1751
- }
1752
- const results = resultsByCall.get(id) ?? [];
1753
- results.push({ entry, index });
1754
- resultsByCall.set(id, results);
2017
+ const resultMessage = resultMatch.entry.message;
2018
+ const expectedName = typeof calls[callIndex].name === "string" ? calls[callIndex].name : "unknown";
2019
+ if (!isRecord(resultMessage) || resultMessage.toolName !== expectedName) {
2020
+ return `Tool result at context entry ${resultMatch.index} conflicts with call ${callId}.`;
1755
2021
  }
1756
- }
1757
- for (const [id, results] of resultsByCall) {
1758
- if (results.length > 1) {
1759
- return {
1760
- message: `Tool call ${id} has duplicate result messages.`,
1761
- ok: false,
1762
- reason: "invalid-protocol"
1763
- };
2022
+ consumedResultIndexes.add(resultMatch.index);
2023
+ const resultText = conversationEntry(resultMatch.entry, caps.toolResultMaxLines, caps.toolResultMaxBytes, caps.policies, caps.redact);
2024
+ if (resultText) {
2025
+ resultParts.push(resultText);
1764
2026
  }
2027
+ resultEntryIds.push(typeof resultMatch.entry.id === "string" ? resultMatch.entry.id : String(resultMatch.index));
1765
2028
  }
2029
+ return;
2030
+ };
2031
+ var missingOutcome = (entry, index, entryId, disclosed, callIds, missing, resultParts, resultEntryIds, caps) => {
2032
+ const { currentInvocationId } = caps;
2033
+ const pendingCurrentInvocation = currentInvocationId !== undefined && missing.size === 1 && missing.has(currentInvocationId) && callIds.includes(currentInvocationId);
2034
+ if (!pendingCurrentInvocation) {
2035
+ return {
2036
+ bytes: byteLength([disclosed, ...resultParts].join(`
2037
+
2038
+ `)),
2039
+ kind: "omitted"
2040
+ };
2041
+ }
2042
+ const pendingDisclosed = pendingInvocationDisclosure(entry, currentInvocationId, caps.toolResultMaxLines, caps.toolResultMaxBytes, caps.policies, caps.redact, disclosed);
2043
+ return {
2044
+ group: createGroup(index, [entryId, ...resultEntryIds], "pending-invocation", [pendingDisclosed ?? disclosed, ...resultParts].join(`
2045
+
2046
+ `), true),
2047
+ kind: "group"
2048
+ };
2049
+ };
2050
+ var buildGroups = (entries, indexed, caps) => {
1766
2051
  const groups = [];
1767
2052
  const consumedResultIndexes = new Set;
1768
2053
  let protocolOmittedBytes = 0;
1769
2054
  let protocolOmittedCount = 0;
1770
2055
  for (let index = 0;index < entries.length; index += 1) {
1771
2056
  const entry = entries[index];
1772
- const disclosed = conversationEntry(entry, toolResultMaxLines, toolResultMaxBytes, policies, redact);
2057
+ const disclosed = conversationEntry(entry, caps.toolResultMaxLines, caps.toolResultMaxBytes, caps.policies, caps.redact);
1773
2058
  if (!disclosed) {
1774
2059
  continue;
1775
2060
  }
1776
- const entryId = typeof entry.id === "string" ? entry.id : String(index);
1777
- if (entry.type !== "message" || !isRecord2(entry.message)) {
1778
- groups.push(createGroup(index, [entryId], "compaction", disclosed, false));
1779
- continue;
2061
+ const outcome = groupForEntry(entry, index, disclosed, entries[index + 1], indexed, consumedResultIndexes, caps);
2062
+ if (outcome.kind === "invalid") {
2063
+ return invalid(outcome.message);
1780
2064
  }
1781
- const { message } = entry;
1782
- if (message.role === "user") {
1783
- groups.push(createGroup(index, [entryId], "user", disclosed, index === latestUserIndex));
1784
- continue;
1785
- }
1786
- if (message.role === "toolResult") {
1787
- if (consumedResultIndexes.has(index)) {
1788
- continue;
1789
- }
1790
- const resultId = message.toolCallId;
1791
- const owner = typeof resultId === "string" ? callOwners.get(resultId) : undefined;
1792
- if (owner) {
1793
- return {
1794
- message: `Tool result at context entry ${index} precedes or conflicts with its retained call.`,
1795
- ok: false,
1796
- reason: "invalid-protocol"
1797
- };
1798
- }
2065
+ if (outcome.kind === "omitted") {
1799
2066
  protocolOmittedCount += 1;
1800
- protocolOmittedBytes += byteLength2(disclosed);
1801
- continue;
1802
- }
1803
- if (message.role !== "assistant") {
1804
- continue;
1805
- }
1806
- const calls = toolCalls(message);
1807
- if (calls.length === 0) {
1808
- groups.push(createGroup(index, [entryId], "assistant", disclosed, false));
1809
- continue;
1810
- }
1811
- const callIds = calls.map(toolCallId);
1812
- const immediate = entries[index + 1];
1813
- if (immediate?.type === "message" && isRecord2(immediate.message) && immediate.message.role === "toolResult" && typeof immediate.message.toolCallId === "string" && !callIds.includes(immediate.message.toolCallId) && !callOwners.has(immediate.message.toolCallId)) {
1814
- return {
1815
- message: `Tool result at context entry ${index + 1} does not match its adjacent assistant group.`,
1816
- ok: false,
1817
- reason: "invalid-protocol"
1818
- };
1819
- }
1820
- const missing = new Set;
1821
- const resultParts = [];
1822
- const resultEntryIds = [];
1823
- for (const [callIndex, callId] of callIds.entries()) {
1824
- const resultMatch = resultsByCall.get(callId)?.[0];
1825
- if (!resultMatch) {
1826
- missing.add(callId);
1827
- continue;
1828
- }
1829
- if (resultMatch.index <= index) {
1830
- return {
1831
- message: `Tool result at context entry ${resultMatch.index} precedes its assistant call.`,
1832
- ok: false,
1833
- reason: "invalid-protocol"
1834
- };
1835
- }
1836
- const resultMessage = resultMatch.entry.message;
1837
- const expectedName = typeof calls[callIndex].name === "string" ? calls[callIndex].name : "unknown";
1838
- if (!isRecord2(resultMessage) || resultMessage.toolName !== expectedName) {
1839
- return {
1840
- message: `Tool result at context entry ${resultMatch.index} conflicts with call ${callId}.`,
1841
- ok: false,
1842
- reason: "invalid-protocol"
1843
- };
1844
- }
1845
- consumedResultIndexes.add(resultMatch.index);
1846
- const resultText = conversationEntry(resultMatch.entry, toolResultMaxLines, toolResultMaxBytes, policies, redact);
1847
- if (resultText) {
1848
- resultParts.push(resultText);
1849
- }
1850
- resultEntryIds.push(typeof resultMatch.entry.id === "string" ? resultMatch.entry.id : String(resultMatch.index));
1851
- }
1852
- if (missing.size > 0) {
1853
- const { currentInvocationId } = options;
1854
- const pendingCurrentInvocation = currentInvocationId !== undefined && missing.size === 1 && missing.has(currentInvocationId) && callIds.includes(currentInvocationId);
1855
- if (!pendingCurrentInvocation) {
1856
- protocolOmittedCount += 1;
1857
- protocolOmittedBytes += byteLength2([disclosed, ...resultParts].join(`
1858
-
1859
- `));
1860
- continue;
1861
- }
1862
- const pendingDisclosed = pendingInvocationDisclosure(entry, currentInvocationId, toolResultMaxLines, toolResultMaxBytes, policies, redact, disclosed);
1863
- groups.push(createGroup(index, [entryId, ...resultEntryIds], "pending-invocation", [pendingDisclosed ?? disclosed, ...resultParts].join(`
1864
-
1865
- `), true));
1866
- continue;
2067
+ protocolOmittedBytes += outcome.bytes;
2068
+ } else if (outcome.kind === "group") {
2069
+ groups.push(outcome.group);
1867
2070
  }
1868
- groups.push(createGroup(index, [entryId, ...resultEntryIds], "tool-exchange", [disclosed, ...resultParts].join(`
1869
-
1870
- `), false));
1871
2071
  }
1872
- const availableCount = groups.length + protocolOmittedCount;
1873
- const availableBytes = groups.reduce((sum, group) => sum + groupWireBytes(group), 0) + protocolOmittedBytes;
1874
- if (maxManifestBytes <= 0) {
2072
+ return {
2073
+ groups,
2074
+ ok: true,
2075
+ protocolOmittedBytes,
2076
+ protocolOmittedCount
2077
+ };
2078
+ };
2079
+ var groupForEntry = (entry, index, disclosed, immediate, indexed, consumedResultIndexes, caps) => {
2080
+ const entryId = typeof entry.id === "string" ? entry.id : String(index);
2081
+ if (entry.type !== "message" || !isRecord(entry.message)) {
1875
2082
  return {
1876
- manifest: {
1877
- availableBytes: 0,
1878
- availableCount: 0,
1879
- groups: [],
1880
- omittedBytes: 0,
1881
- omittedCount: 0
1882
- },
1883
- ok: true
2083
+ group: createGroup(index, [entryId], "compaction", disclosed, false),
2084
+ kind: "group"
1884
2085
  };
1885
2086
  }
1886
- const required = groups.filter((group) => group.required);
1887
- if (required.some((group) => group.bytes > maxGroupBytes) || required.length > maxGroups || required.reduce((sum, group) => sum + groupWireBytes(group), 0) > maxManifestBytes) {
2087
+ const { message } = entry;
2088
+ if (message.role === "user") {
1888
2089
  return {
1889
- message: "Required Scout context exceeds the Scout manifest transport limit.",
1890
- ok: false,
1891
- reason: "required-group-overflow"
2090
+ group: createGroup(index, [entryId], "user", disclosed, index === indexed.latestUserIndex),
2091
+ kind: "group"
1892
2092
  };
1893
2093
  }
1894
- const contentChars = (items) => items.reduce((sum, group) => sum + group.content.length, 0) + Math.max(0, items.length - 1) * 2;
1895
- if (maxConversationChars !== undefined && contentChars(required) > maxConversationChars) {
2094
+ if (message.role === "toolResult") {
2095
+ return toolResultOutcome(message, index, disclosed, indexed.callOwners, consumedResultIndexes);
2096
+ }
2097
+ if (message.role !== "assistant") {
2098
+ return { kind: "skipped" };
2099
+ }
2100
+ if (!hasCalls(message)) {
1896
2101
  return {
1897
- message: "Required Scout context exceeds the Advisor conversation budget.",
1898
- ok: false,
1899
- reason: "required-group-overflow"
2102
+ group: createGroup(index, [entryId], "assistant", disclosed, false),
2103
+ kind: "group"
1900
2104
  };
1901
2105
  }
1902
- const selected = groups.filter((group) => group.required || group.bytes <= maxGroupBytes);
1903
- const fits = () => selected.length <= maxGroups && selected.reduce((sum, group) => sum + groupWireBytes(group), 0) <= maxManifestBytes && (maxConversationChars === undefined || contentChars(selected) <= maxConversationChars);
1904
- while (!fits()) {
1905
- const optionalIndex = selected.findIndex((group) => !group.required);
1906
- if (optionalIndex < 0) {
1907
- return {
1908
- message: "Required Scout context exceeds fixed manifest limits.",
1909
- ok: false,
1910
- reason: "required-group-overflow"
1911
- };
1912
- }
1913
- selected.splice(optionalIndex, 1);
2106
+ return toolExchangeGroup(entry, index, entryId, disclosed, immediate, indexed, consumedResultIndexes, caps);
2107
+ };
2108
+ var toolResultOutcome = (message, index, disclosed, callOwners, consumedResultIndexes) => {
2109
+ if (consumedResultIndexes.has(index)) {
2110
+ return { kind: "skipped" };
1914
2111
  }
1915
- const selectedIds = new Set(selected.map((group) => group.id));
1916
- const omitted = groups.filter((group) => !selectedIds.has(group.id));
1917
- return {
1918
- manifest: {
1919
- availableBytes,
1920
- availableCount,
1921
- groups: selected,
1922
- omittedBytes: protocolOmittedBytes + omitted.reduce((sum, group) => sum + group.bytes, 0),
1923
- omittedCount: protocolOmittedCount + omitted.length
1924
- },
1925
- ok: true
1926
- };
2112
+ if (ownerOf(message, callOwners)) {
2113
+ return {
2114
+ kind: "invalid",
2115
+ message: `Tool result at context entry ${index} precedes or conflicts with its retained call.`
2116
+ };
2117
+ }
2118
+ return { bytes: byteLength(disclosed), kind: "omitted" };
1927
2119
  };
2120
+ var ownerOf = (message, callOwners) => typeof message.toolCallId === "string" ? callOwners.get(message.toolCallId) : undefined;
2121
+ var hasCalls = (message) => contentPartsOf(message).some((part) => isRecord(part) && part.type === "toolCall");
2122
+ var contentPartsOf = (message) => Array.isArray(message.content) ? message.content : [];
2123
+
2124
+ // src/scout-reconstruct.ts
1928
2125
  var prefixWithinCharBudget = (value, maxChars) => {
1929
2126
  let result = "";
1930
2127
  for (const character of value) {
@@ -1965,13 +2162,12 @@ ${synthesis.trim()}` : undefined;
1965
2162
  // src/usage.ts
1966
2163
  var finite = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
1967
2164
  var add = (left, right) => left === undefined || right === undefined ? left ?? right : left + right;
1968
- var isRecord3 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
1969
2165
  var costFields = ["input", "output", "cacheRead", "cacheWrite", "total"];
1970
2166
  var snapshotAdvisorUsage = (usage) => {
1971
- if (!isRecord3(usage)) {
2167
+ if (!isRecord(usage)) {
1972
2168
  return;
1973
2169
  }
1974
- const cost = isRecord3(usage.cost) ? usage.cost : undefined;
2170
+ const cost = isRecord(usage.cost) ? usage.cost : undefined;
1975
2171
  const snapshot = {
1976
2172
  cacheRead: finite(usage.cacheRead),
1977
2173
  cacheWrite: finite(usage.cacheWrite),
@@ -1986,10 +2182,10 @@ var snapshotAdvisorUsage = (usage) => {
1986
2182
  var advisorUsageCost = (usage) => snapshotAdvisorUsage(usage)?.cost;
1987
2183
  var advisorUsageForPi = (usage) => {
1988
2184
  const snapshot = snapshotAdvisorUsage(usage);
1989
- if (!(snapshot && isRecord3(usage))) {
2185
+ if (!(snapshot && isRecord(usage))) {
1990
2186
  return;
1991
2187
  }
1992
- const cost = isRecord3(usage.cost) ? usage.cost : undefined;
2188
+ const cost = isRecord(usage.cost) ? usage.cost : undefined;
1993
2189
  const input = snapshot.input ?? 0;
1994
2190
  const output = snapshot.output ?? 0;
1995
2191
  const cacheRead = snapshot.cacheRead ?? 0;
@@ -2105,20 +2301,13 @@ var defaultDependencies = {
2105
2301
  collect: collectTextStream,
2106
2302
  resolve: resolveConfiguredModel
2107
2303
  };
2108
- var byteLength3 = (value) => Buffer.byteLength(value, "utf8");
2304
+ var byteLength2 = (value) => Buffer.byteLength(value, "utf8");
2109
2305
  var AUTH_ERROR_PATTERN = /api key|auth|login|credential/i;
2110
2306
  var manifestMessage = (manifest) => ({
2111
2307
  content: [
2112
2308
  {
2113
2309
  text: JSON.stringify({
2114
- groups: manifest.groups.map((group) => ({
2115
- bytes: group.bytes,
2116
- content: group.content,
2117
- id: group.id,
2118
- kind: group.kind,
2119
- label: group.label,
2120
- required: group.required
2121
- })),
2310
+ groups: manifest.groups.map(groupWire),
2122
2311
  omittedBeforeScout: {
2123
2312
  bytes: manifest.omittedBytes,
2124
2313
  groups: manifest.omittedCount
@@ -2168,7 +2357,7 @@ var parseScoutSelection = (text, manifest) => {
2168
2357
  if (typeof record.synthesis !== "string") {
2169
2358
  throw new Error("Scout synthesis must be a string.");
2170
2359
  }
2171
- if (byteLength3(record.synthesis) > SCOUT_SYNTHESIS_MAX_BYTES) {
2360
+ if (byteLength2(record.synthesis) > SCOUT_SYNTHESIS_MAX_BYTES) {
2172
2361
  throw new Error(`Scout synthesis exceeds ${SCOUT_SYNTHESIS_MAX_BYTES} UTF-8 bytes.`);
2173
2362
  }
2174
2363
  return {
@@ -2197,32 +2386,36 @@ var runAdvisorScout = async (ctx, manifest, parentSignal, onEvent, timeoutMs = S
2197
2386
  const publish = (event) => {
2198
2387
  onEvent?.(event);
2199
2388
  };
2200
- if (parentSignal?.aborted) {
2389
+ const cancelled = () => {
2201
2390
  publish({ type: "cancelled" });
2202
2391
  return { cancelled: true, ok: false };
2392
+ };
2393
+ const fallback = (category, message, usage) => {
2394
+ const outcome = {
2395
+ category,
2396
+ message,
2397
+ metrics: usage === undefined ? baseMetrics(manifest, startedAt) : { ...baseMetrics(manifest, startedAt), usage },
2398
+ model: executorRef,
2399
+ ok: false
2400
+ };
2401
+ publish({ outcome, type: "fallback" });
2402
+ return outcome;
2403
+ };
2404
+ if (parentSignal?.aborted) {
2405
+ return cancelled();
2203
2406
  }
2204
2407
  let resolved;
2205
2408
  try {
2206
2409
  resolved = await dependencies.resolve(ctx, executorRef, "Scout");
2207
2410
  } catch (error) {
2208
2411
  if (parentSignal?.aborted) {
2209
- publish({ type: "cancelled" });
2210
- return { cancelled: true, ok: false };
2412
+ return cancelled();
2211
2413
  }
2212
2414
  const message = error instanceof Error ? error.message : String(error);
2213
- const outcome = {
2214
- category: classifyResolutionError(message),
2215
- message,
2216
- metrics: baseMetrics(manifest, startedAt),
2217
- model: executorRef,
2218
- ok: false
2219
- };
2220
- publish({ outcome, type: "fallback" });
2221
- return outcome;
2415
+ return fallback(classifyResolutionError(message), message);
2222
2416
  }
2223
2417
  if (parentSignal?.aborted) {
2224
- publish({ type: "cancelled" });
2225
- return { cancelled: true, ok: false };
2418
+ return cancelled();
2226
2419
  }
2227
2420
  publish({ model: executorRef, type: "call" });
2228
2421
  const controller = new AbortController;
@@ -2242,6 +2435,11 @@ var runAdvisorScout = async (ctx, manifest, parentSignal, onEvent, timeoutMs = S
2242
2435
  once: true
2243
2436
  });
2244
2437
  });
2438
+ const teardown = () => {
2439
+ clearTimeout(timer);
2440
+ parentSignal?.removeEventListener("abort", abortFromParent);
2441
+ controller.signal.removeEventListener("abort", onControllerAbort);
2442
+ };
2245
2443
  let streamed;
2246
2444
  try {
2247
2445
  const collection = dependencies.collect(resolved, {
@@ -2255,50 +2453,25 @@ var runAdvisorScout = async (ctx, manifest, parentSignal, onEvent, timeoutMs = S
2255
2453
  signal: controller.signal,
2256
2454
  systemPrompt: SCOUT_SYSTEM
2257
2455
  });
2258
- streamed = await Promise.race([collection, abortPromise]);
2259
- } catch (error) {
2260
- clearTimeout(timer);
2261
- parentSignal?.removeEventListener("abort", abortFromParent);
2262
- controller.signal.removeEventListener("abort", onControllerAbort);
2456
+ streamed = await Promise.race([collection, abortPromise]);
2457
+ } catch (error) {
2458
+ teardown();
2263
2459
  if (parentSignal?.aborted) {
2264
- publish({ type: "cancelled" });
2265
- return { cancelled: true, ok: false };
2460
+ return cancelled();
2266
2461
  }
2267
2462
  const message = error instanceof Error ? error.message : String(error);
2268
- const outcome = {
2269
- category: timedOut ? "timeout" : "provider-error",
2270
- message: timedOut ? `Scout timed out after ${timeoutMs} ms.` : message,
2271
- metrics: baseMetrics(manifest, startedAt),
2272
- model: executorRef,
2273
- ok: false
2274
- };
2275
- publish({ outcome, type: "fallback" });
2276
- return outcome;
2463
+ return timedOut ? fallback("timeout", `Scout timed out after ${timeoutMs} ms.`) : fallback("provider-error", message);
2277
2464
  }
2278
- clearTimeout(timer);
2279
- parentSignal?.removeEventListener("abort", abortFromParent);
2280
- controller.signal.removeEventListener("abort", onControllerAbort);
2465
+ teardown();
2281
2466
  if (parentSignal?.aborted) {
2282
- publish({ type: "cancelled" });
2283
- return { cancelled: true, ok: false };
2467
+ return cancelled();
2284
2468
  }
2285
2469
  let selection;
2286
2470
  try {
2287
2471
  selection = parseScoutSelection(streamed.text, manifest);
2288
2472
  } catch (error) {
2289
2473
  const message = error instanceof Error ? error.message : String(error);
2290
- const outcome = {
2291
- category: streamed.text.trim() ? "invalid-selection" : "empty-response",
2292
- message,
2293
- metrics: {
2294
- ...baseMetrics(manifest, startedAt),
2295
- usage: snapshotAdvisorUsage(streamed.usage)
2296
- },
2297
- model: executorRef,
2298
- ok: false
2299
- };
2300
- publish({ outcome, type: "fallback" });
2301
- return outcome;
2474
+ return fallback(streamed.text.trim() ? "invalid-selection" : "empty-response", message, snapshotAdvisorUsage(streamed.usage));
2302
2475
  }
2303
2476
  const outcome = {
2304
2477
  conversation: reconstructScoutConversation(manifest, selection.selectedIds, selection.synthesis),
@@ -2319,234 +2492,137 @@ var runAdvisorScout = async (ctx, manifest, parentSignal, onEvent, timeoutMs = S
2319
2492
  return outcome;
2320
2493
  };
2321
2494
 
2322
- // src/untracked.ts
2323
- import { execFileSync as execFileSync2 } from "node:child_process";
2324
- import { constants } from "node:fs";
2325
- import { lstat as lstat2, open as open2, realpath as realpath2 } from "node:fs/promises";
2326
- import { isAbsolute, relative as relative2, resolve } from "node:path";
2327
- var ADVISOR_FILE_MAX_BYTES = 8 * 1024;
2328
- var ADVISOR_FILES_TOTAL_MAX_BYTES = 24 * 1024;
2329
- var PATH_SEGMENTS = /[\\/]/;
2330
- var within = (root, candidate) => {
2331
- const path = relative2(root, candidate);
2332
- return path !== "" && !path.startsWith("..") && !path.includes("../");
2333
- };
2334
- var normalizeRelativePath = (root, path) => relative2(root, resolve(root, path));
2335
- var normalizeRequestedPath = (root, value) => {
2336
- if (typeof value !== "string" || !value || isAbsolute(value) || value.split(PATH_SEGMENTS).includes("..")) {
2337
- return;
2338
- }
2339
- return normalizeRelativePath(root, value);
2340
- };
2341
- var git = (cwd, args) => execFileSync2("git", args, {
2342
- cwd,
2343
- encoding: "utf8",
2344
- maxBuffer: 16 * 1024 * 1024,
2345
- shell: false,
2346
- stdio: ["ignore", "pipe", "pipe"],
2347
- timeout: 5000,
2348
- windowsHide: true
2495
+ // src/scout-context.ts
2496
+ var resolveCaps = (options) => ({
2497
+ currentInvocationId: options.currentInvocationId,
2498
+ maxConversationChars: options.maxConversationChars,
2499
+ maxGroupBytes: options.maxGroupBytes ?? SCOUT_GROUP_MAX_BYTES,
2500
+ maxGroups: options.maxGroups ?? SCOUT_MANIFEST_MAX_GROUPS,
2501
+ maxManifestBytes: options.maxManifestBytes ?? options.maxBytes ?? SCOUT_MANIFEST_MAX_BYTES,
2502
+ policies: options.policies ?? advisorToolPoliciesRef,
2503
+ redact: options.redact ?? advisorRedactSecretsRef,
2504
+ toolResultMaxBytes: options.toolResultMaxBytes ?? advisorToolResultMaxBytesRef,
2505
+ toolResultMaxLines: options.toolResultMaxLines ?? advisorToolResultMaxLinesRef
2349
2506
  });
2350
- var repositoryRoot = (cwd) => {
2351
- try {
2352
- return realpath2(git(cwd, ["rev-parse", "--show-toplevel"]).trim());
2353
- } catch {
2354
- return Promise.resolve(undefined);
2355
- }
2356
- };
2357
- var untracked = (cwd, path) => {
2358
- const output = git(cwd, [
2359
- "ls-files",
2360
- "--others",
2361
- "--exclude-standard",
2362
- "-z",
2363
- "--",
2364
- path
2365
- ]);
2366
- const expected = normalizeRelativePath(cwd, path);
2367
- return output.split("\x00").filter(Boolean).some((entry) => normalizeRelativePath(cwd, entry) === expected);
2368
- };
2369
- var tracked = (cwd, path) => {
2370
- const output = git(cwd, ["ls-files", "--stage", "-z", "--", path]);
2371
- const expected = normalizeRelativePath(cwd, path);
2372
- return output.split("\x00").some((entry) => {
2373
- if (!entry) {
2374
- return false;
2375
- }
2376
- const [metadata, name] = entry.split("\t");
2377
- return name && normalizeRelativePath(cwd, name) === expected && !metadata.startsWith("160000 ");
2378
- });
2379
- };
2380
- var isPermitted = (root, path, kind) => kind === "tracked" ? tracked(root, path) : untracked(root, path);
2381
- var readAttachment = async (root, normalizedName, redact, available) => {
2382
- const absolute = resolve(root, normalizedName);
2383
- if (!(within(root, absolute) && available > 0)) {
2384
- return;
2385
- }
2386
- const stats = await lstat2(absolute);
2387
- if (stats.isSymbolicLink() || !stats.isFile()) {
2388
- return;
2507
+ var buildScoutManifest = (ctx, options = {}) => {
2508
+ const entries = ctx.sessionManager.buildContextEntries();
2509
+ const caps = resolveCaps(options);
2510
+ const indexed = indexToolCalls(entries);
2511
+ if (!indexed.ok) {
2512
+ return indexed;
2389
2513
  }
2390
- const resolved = await realpath2(absolute);
2391
- if (!within(root, resolved)) {
2392
- return;
2514
+ const built = buildGroups(entries, indexed.index, caps);
2515
+ if (!built.ok) {
2516
+ return built;
2393
2517
  }
2394
- const flags = constants.O_NOFOLLOW ? constants.O_RDONLY | constants.O_NOFOLLOW : constants.O_RDONLY;
2395
- const file = await open2(resolved, flags);
2396
- try {
2397
- const openedStats = await file.stat();
2398
- if (!openedStats.isFile()) {
2399
- return;
2400
- }
2401
- const buffer = Buffer.alloc(available + 1);
2402
- const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
2403
- const raw = buffer.subarray(0, bytesRead).toString("utf8");
2404
- if (raw.includes("\x00")) {
2405
- return;
2406
- }
2407
- const text = redactAndCapText(raw, available, redact);
2518
+ return fitToBudget(built, caps);
2519
+ };
2520
+ var fits = (selected, caps) => selected.length <= caps.maxGroups && selected.reduce((sum, group) => sum + groupWireBytes(group), 0) <= caps.maxManifestBytes && (caps.maxConversationChars === undefined || contentChars(selected) <= caps.maxConversationChars);
2521
+ var contentChars = (items) => items.reduce((sum, group) => sum + group.content.length, 0) + Math.max(0, items.length - 1) * 2;
2522
+ var fitToBudget = (built, caps) => {
2523
+ const { groups, protocolOmittedBytes, protocolOmittedCount } = built;
2524
+ const availableCount = groups.length + protocolOmittedCount;
2525
+ const availableBytes = groups.reduce((sum, group) => sum + groupWireBytes(group), 0) + protocolOmittedBytes;
2526
+ if (caps.maxManifestBytes <= 0) {
2408
2527
  return {
2409
- bytes: Buffer.byteLength(text, "utf8"),
2410
- path: normalizedName,
2411
- text
2528
+ manifest: {
2529
+ availableBytes: 0,
2530
+ availableCount: 0,
2531
+ groups: [],
2532
+ omittedBytes: 0,
2533
+ omittedCount: 0
2534
+ },
2535
+ ok: true
2412
2536
  };
2413
- } finally {
2414
- await file.close();
2415
- }
2416
- };
2417
- var readFiles = async (cwd, requested, enabled, redact, kind, totalLimit = ADVISOR_FILES_TOTAL_MAX_BYTES) => {
2418
- if (!(enabled && Array.isArray(requested))) {
2419
- return [];
2420
2537
  }
2421
- const root = await repositoryRoot(cwd);
2422
- if (!root) {
2423
- return [];
2424
- }
2425
- const unique = new Set;
2426
- const attachments = [];
2427
- let total = 0;
2428
- for (const name of requested) {
2429
- const normalizedName = normalizeRequestedPath(root, name);
2430
- if (!normalizedName || unique.has(normalizedName)) {
2431
- continue;
2432
- }
2433
- unique.add(normalizedName);
2434
- try {
2435
- if (!isPermitted(root, normalizedName, kind)) {
2436
- continue;
2437
- }
2438
- const available = Math.min(ADVISOR_FILE_MAX_BYTES, totalLimit - total);
2439
- if (available <= 0) {
2440
- break;
2441
- }
2442
- const attachment = await readAttachment(root, normalizedName, redact, available);
2443
- if (attachment) {
2444
- attachments.push(attachment);
2445
- total += attachment.bytes;
2446
- }
2447
- } catch {}
2538
+ const required = groups.filter((group) => group.required);
2539
+ const overflow = requiredOverflow(required, caps);
2540
+ if (overflow) {
2541
+ return overflow;
2448
2542
  }
2449
- return attachments;
2450
- };
2451
- var readTrackedFiles = (cwd, requested, enabled, redact, totalLimit = ADVISOR_FILES_TOTAL_MAX_BYTES) => readFiles(cwd, requested, enabled, redact, "tracked", totalLimit);
2452
- var readUntrackedFiles = (cwd, requested, enabled, redact, totalLimit = ADVISOR_FILES_TOTAL_MAX_BYTES) => readFiles(cwd, requested, enabled, redact, "untracked", totalLimit);
2453
-
2454
- // src/tools/gate-protocol.ts
2455
- var DECISION_LINE = /^Decision\s*:\s*(proceed|revise|blocked)\s*$/i;
2456
- var CODE_FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
2457
- var LINE_BREAK = /\r?\n/;
2458
- var advanceFence = (openingFence, marker, suffix) => {
2459
- if (!openingFence) {
2460
- if (marker[0] === "`" && suffix.includes("`")) {
2461
- return { closed: false, openingFence: undefined };
2543
+ const selected = groups.filter((group) => group.required || group.bytes <= caps.maxGroupBytes);
2544
+ while (!fits(selected, caps)) {
2545
+ const optionalIndex = selected.findIndex((group) => !group.required);
2546
+ if (optionalIndex < 0) {
2547
+ return {
2548
+ message: "Required Scout context exceeds fixed manifest limits.",
2549
+ ok: false,
2550
+ reason: "required-group-overflow"
2551
+ };
2462
2552
  }
2463
- return {
2464
- closed: false,
2465
- openingFence: { character: marker[0], length: marker.length }
2466
- };
2467
- }
2468
- if (suffix.trim().length > 0 || marker[0] !== openingFence.character || marker.length < openingFence.length) {
2469
- return { closed: false, openingFence };
2553
+ selected.splice(optionalIndex, 1);
2470
2554
  }
2471
- return { closed: true, openingFence: undefined };
2555
+ const selectedIds = new Set(selected.map((group) => group.id));
2556
+ const omitted = groups.filter((group) => !selectedIds.has(group.id));
2557
+ return {
2558
+ manifest: {
2559
+ availableBytes,
2560
+ availableCount,
2561
+ groups: selected,
2562
+ omittedBytes: protocolOmittedBytes + omitted.reduce((sum, group) => sum + group.bytes, 0),
2563
+ omittedCount: protocolOmittedCount + omitted.length
2564
+ },
2565
+ ok: true
2566
+ };
2472
2567
  };
2473
- var parseAutomaticDecision = (text) => {
2474
- const lines = text.split(LINE_BREAK);
2475
- const nonEmpty = lines.findIndex((line) => line.trim().length > 0);
2476
- if (nonEmpty === -1) {
2568
+ var requiredOverflow = (required, caps) => {
2569
+ if (required.some((group) => group.bytes > caps.maxGroupBytes) || required.length > caps.maxGroups || required.reduce((sum, group) => sum + groupWireBytes(group), 0) > caps.maxManifestBytes) {
2477
2570
  return {
2478
- category: "empty-response",
2479
- message: "Advisor returned an empty gate response.",
2480
- ok: false
2571
+ message: "Required Scout context exceeds the Scout manifest transport limit.",
2572
+ ok: false,
2573
+ reason: "required-group-overflow"
2481
2574
  };
2482
2575
  }
2483
- const first = lines[nonEmpty].trim();
2484
- const match = DECISION_LINE.exec(first);
2485
- if (!match) {
2576
+ if (caps.maxConversationChars !== undefined && contentChars(required) > caps.maxConversationChars) {
2486
2577
  return {
2487
- category: first.toLowerCase().startsWith("decision:") ? "malformed-decision" : "missing-decision",
2488
- markdown: text,
2489
- message: "Advisor gate response must begin with Decision: proceed, Decision: revise, or Decision: blocked.",
2490
- ok: false
2578
+ message: "Required Scout context exceeds the Advisor conversation budget.",
2579
+ ok: false,
2580
+ reason: "required-group-overflow"
2491
2581
  };
2492
2582
  }
2493
- const decision = match[1].toLowerCase();
2494
- let openingFence;
2495
- const decisions = [];
2496
- let pendingFencedDecisions = [];
2497
- for (const line of lines.slice(nonEmpty + 1)) {
2498
- const trimmed = line.trim();
2499
- const fence = CODE_FENCE.exec(line);
2500
- if (fence) {
2501
- const { closed, openingFence: nextOpeningFence } = advanceFence(openingFence, fence[1], fence[2]);
2502
- openingFence = nextOpeningFence;
2503
- if (closed) {
2504
- pendingFencedDecisions = [];
2505
- }
2506
- continue;
2507
- }
2508
- const subsequent = DECISION_LINE.exec(trimmed);
2509
- if (!subsequent) {
2510
- continue;
2511
- }
2512
- const repeated = subsequent[1].trim().toLowerCase();
2513
- if (openingFence) {
2514
- pendingFencedDecisions.push(repeated);
2515
- } else {
2516
- decisions.push(repeated);
2517
- }
2518
- }
2519
- if (openingFence) {
2520
- decisions.push(...pendingFencedDecisions);
2521
- }
2522
- for (const repeated of decisions) {
2523
- if (repeated === decision) {
2524
- return {
2525
- category: "duplicate-decision",
2526
- markdown: text,
2527
- message: "Advisor gate response contains duplicate decision lines.",
2528
- ok: false
2529
- };
2530
- }
2531
- return {
2532
- category: "contradictory-decision",
2533
- markdown: text,
2534
- message: "Advisor gate response contains contradictory decision lines.",
2583
+ return;
2584
+ };
2585
+
2586
+ // src/scout-curation.ts
2587
+ var curateAdvisorConversation = async (ctx, legacyConversation, signal, onScout, enabled = advisorScoutEnabledRef, runScout = runAdvisorScout, currentInvocationId, maxChars) => {
2588
+ if (!enabled) {
2589
+ return { conversation: legacyConversation };
2590
+ }
2591
+ if (maxChars !== undefined && maxChars <= 0) {
2592
+ return { conversation: "" };
2593
+ }
2594
+ const built = buildScoutManifest(ctx, {
2595
+ currentInvocationId,
2596
+ maxConversationChars: maxChars,
2597
+ maxManifestBytes: SCOUT_MANIFEST_MAX_BYTES
2598
+ });
2599
+ if (!built.ok) {
2600
+ const scout = {
2601
+ category: built.reason,
2602
+ message: built.message,
2603
+ metrics: {
2604
+ availableCount: 0,
2605
+ inputBytes: 0,
2606
+ latencyMs: 0,
2607
+ omittedBeforeScout: 0,
2608
+ selectedCount: 0
2609
+ },
2610
+ model: executorRef,
2535
2611
  ok: false
2536
2612
  };
2613
+ onScout?.({ outcome: scout, type: "fallback" });
2614
+ return { conversation: legacyConversation, scout };
2537
2615
  }
2538
- return {
2539
- decision,
2540
- markdown: text,
2541
- model: "",
2542
- ok: true,
2543
- thinkingText: "",
2544
- trigger: "repeated-tool-call"
2545
- };
2616
+ const outcome = await runScout(ctx, built.manifest, signal, onScout, undefined, undefined);
2617
+ if (!outcome.ok && outcome.cancelled) {
2618
+ throw signal?.reason instanceof Error ? signal.reason : new Error("Advisor operation cancelled during Scout.");
2619
+ }
2620
+ let conversation = legacyConversation;
2621
+ if (outcome.ok) {
2622
+ conversation = maxChars === undefined ? outcome.conversation : reconstructScoutConversation(built.manifest, outcome.selection.selectedIds, outcome.selection.synthesis, maxChars);
2623
+ }
2624
+ return { conversation, scout: outcome };
2546
2625
  };
2547
- var adviceForGateText = (result) => `**Decision: ${result.decision}**
2548
-
2549
- ${result.markdown}`;
2550
2626
 
2551
2627
  // src/tools/prompts.ts
2552
2628
  var advisorMessageText = (conversation, question, changes, draft, preferences, untracked, tracked) => {
@@ -2657,72 +2733,152 @@ var ADVISOR_DECISION_SYSTEM = [
2657
2733
  "Use blocked only for a critical issue requiring the user. Never claim verification that the supplied evidence does not show."
2658
2734
  ].join(" ");
2659
2735
 
2660
- // src/tools/consultation.ts
2661
- var curateAdvisorConversation = async (ctx, legacyConversation, signal, onScout, enabled = advisorScoutEnabledRef, runScout = runAdvisorScout, currentInvocationId, maxChars) => {
2662
- if (!enabled) {
2663
- return { conversation: legacyConversation };
2736
+ // src/tools/consult-context.ts
2737
+ var ATTACHMENT_TEXT_MAX_BYTES = 8 * 1024;
2738
+ var ATTACHMENTS_TOTAL_MAX_BYTES = 24 * 1024;
2739
+ var assembleConsultationContext = async (options) => {
2740
+ const { ctx } = options;
2741
+ const allowed = advisorGitContextRef;
2742
+ const level = clampGitContextLevel(options.gitContext ?? allowed, allowed);
2743
+ const gitBudget = advisorGitContextBudget(contextMaxCharsRef, advisorGitContextMaxCharsRef);
2744
+ const changes = collectGitContext(ctx.cwd, level, gitBudget, advisorRedactSecretsRef ? redactSecrets : undefined);
2745
+ const changeText = advisorRepositoryContext(changes, options.gitContext ?? allowed, level, gitBudget);
2746
+ const conversationBudget = Math.max(0, contextMaxCharsRef - changeText.length);
2747
+ const legacyConversation = advisorRequestConversation(ctx, conversationBudget);
2748
+ const curated = await curateAdvisorConversation(ctx, legacyConversation, options.signal, options.onScout, advisorScoutEnabledRef, runAdvisorScout, options.currentInvocationId, conversationBudget);
2749
+ const preferences = await readProjectPreferences(ctx, ATTACHMENT_TEXT_MAX_BYTES, advisorRedactSecretsRef);
2750
+ const draftText = options.draft ? redactAndCapText(options.draft, ATTACHMENT_TEXT_MAX_BYTES, advisorRedactSecretsRef) : undefined;
2751
+ const untracked = await readUntrackedFiles(ctx.cwd, options.includeUntracked ?? [], advisorUntrackedContentRef, advisorRedactSecretsRef);
2752
+ const tracked = await readTrackedFiles(ctx.cwd, options.includeTracked ?? [], advisorTrackedFileContentRef, advisorRedactSecretsRef, Math.max(0, ATTACHMENTS_TOTAL_MAX_BYTES - untracked.reduce((sum, item) => sum + item.bytes, 0)));
2753
+ return {
2754
+ changeText,
2755
+ conversation: curated.conversation,
2756
+ draftText,
2757
+ preferences,
2758
+ scout: curated.scout,
2759
+ tracked,
2760
+ untracked
2761
+ };
2762
+ };
2763
+
2764
+ // src/tools/gate-protocol.ts
2765
+ var DECISION_LINE = /^Decision\s*:\s*(proceed|revise|blocked)\s*$/i;
2766
+ var CODE_FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
2767
+ var LINE_BREAK = /\r?\n/;
2768
+ var advanceFence = (openingFence, marker, suffix) => {
2769
+ if (!openingFence) {
2770
+ if (marker[0] === "`" && suffix.includes("`")) {
2771
+ return { closed: false, openingFence: undefined };
2772
+ }
2773
+ return {
2774
+ closed: false,
2775
+ openingFence: { character: marker[0], length: marker.length }
2776
+ };
2664
2777
  }
2665
- if (maxChars !== undefined && maxChars <= 0) {
2666
- return { conversation: "" };
2778
+ if (suffix.trim().length > 0 || marker[0] !== openingFence.character || marker.length < openingFence.length) {
2779
+ return { closed: false, openingFence };
2667
2780
  }
2668
- const built = buildScoutManifest(ctx, {
2669
- currentInvocationId,
2670
- maxConversationChars: maxChars,
2671
- maxManifestBytes: SCOUT_MANIFEST_MAX_BYTES
2672
- });
2673
- if (!built.ok) {
2674
- const scout = {
2675
- category: built.reason,
2676
- message: built.message,
2677
- metrics: {
2678
- availableCount: 0,
2679
- inputBytes: 0,
2680
- latencyMs: 0,
2681
- omittedBeforeScout: 0,
2682
- selectedCount: 0
2683
- },
2684
- model: executorRef,
2781
+ return { closed: true, openingFence: undefined };
2782
+ };
2783
+ var parseAutomaticDecision = (text) => {
2784
+ const lines = text.split(LINE_BREAK);
2785
+ const nonEmpty = lines.findIndex((line) => line.trim().length > 0);
2786
+ if (nonEmpty === -1) {
2787
+ return {
2788
+ category: "empty-response",
2789
+ message: "Advisor returned an empty gate response.",
2685
2790
  ok: false
2686
2791
  };
2687
- onScout?.({ outcome: scout, type: "fallback" });
2688
- return { conversation: legacyConversation, scout };
2689
2792
  }
2690
- const outcome = await runScout(ctx, built.manifest, signal, onScout, undefined, undefined);
2691
- if (!outcome.ok && outcome.cancelled) {
2692
- throw signal?.reason instanceof Error ? signal.reason : new Error("Advisor operation cancelled during Scout.");
2793
+ const first = lines[nonEmpty].trim();
2794
+ const match = DECISION_LINE.exec(first);
2795
+ if (!match) {
2796
+ return {
2797
+ category: first.toLowerCase().startsWith("decision:") ? "malformed-decision" : "missing-decision",
2798
+ markdown: text,
2799
+ message: "Advisor gate response must begin with Decision: proceed, Decision: revise, or Decision: blocked.",
2800
+ ok: false
2801
+ };
2693
2802
  }
2694
- let conversation = legacyConversation;
2695
- if (outcome.ok) {
2696
- conversation = maxChars === undefined ? outcome.conversation : reconstructScoutConversation(built.manifest, outcome.selection.selectedIds, outcome.selection.synthesis, maxChars);
2803
+ const decision = match[1].toLowerCase();
2804
+ let openingFence;
2805
+ const decisions = [];
2806
+ let pendingFencedDecisions = [];
2807
+ for (const line of lines.slice(nonEmpty + 1)) {
2808
+ const trimmed = line.trim();
2809
+ const fence = CODE_FENCE.exec(line);
2810
+ if (fence) {
2811
+ const { closed, openingFence: nextOpeningFence } = advanceFence(openingFence, fence[1], fence[2]);
2812
+ openingFence = nextOpeningFence;
2813
+ if (closed) {
2814
+ pendingFencedDecisions = [];
2815
+ }
2816
+ continue;
2817
+ }
2818
+ const subsequent = DECISION_LINE.exec(trimmed);
2819
+ if (!subsequent) {
2820
+ continue;
2821
+ }
2822
+ const repeated = subsequent[1].trim().toLowerCase();
2823
+ if (openingFence) {
2824
+ pendingFencedDecisions.push(repeated);
2825
+ } else {
2826
+ decisions.push(repeated);
2827
+ }
2697
2828
  }
2698
- return { conversation, scout: outcome };
2829
+ if (openingFence) {
2830
+ decisions.push(...pendingFencedDecisions);
2831
+ }
2832
+ for (const repeated of decisions) {
2833
+ if (repeated === decision) {
2834
+ return {
2835
+ category: "duplicate-decision",
2836
+ markdown: text,
2837
+ message: "Advisor gate response contains duplicate decision lines.",
2838
+ ok: false
2839
+ };
2840
+ }
2841
+ return {
2842
+ category: "contradictory-decision",
2843
+ markdown: text,
2844
+ message: "Advisor gate response contains contradictory decision lines.",
2845
+ ok: false
2846
+ };
2847
+ }
2848
+ return {
2849
+ decision,
2850
+ markdown: text,
2851
+ model: "",
2852
+ ok: true,
2853
+ thinkingText: "",
2854
+ trigger: "repeated-tool-call"
2855
+ };
2699
2856
  };
2700
- var collectAdvisorResponse = async (ctx, systemPrompt, question, signal, onChunk, gitContext, draft, includeUntracked, includeTracked, onScout, currentInvocationId) => {
2857
+ var adviceForGateText = (result) => `**Decision: ${result.decision}**
2858
+
2859
+ ${result.markdown}`;
2860
+
2861
+ // src/tools/consultation.ts
2862
+ class AdvisorNoAdviceError extends Error {
2863
+ constructor() {
2864
+ super("Advisor returned no advice.");
2865
+ this.name = "AdvisorNoAdviceError";
2866
+ }
2867
+ }
2868
+ var fileTag = (item) => `<file path=${JSON.stringify(item.path)}>
2869
+ ${item.text}
2870
+ </file>`;
2871
+ var collectAdvisorResponse = async (options) => {
2872
+ const { ctx, question, signal, systemPrompt } = options;
2701
2873
  loadConfig(ctx);
2702
2874
  const resolved = await resolveConfiguredModel(ctx, advisorRef, "Advisor");
2703
- const allowed = advisorGitContextRef;
2704
- const level = clampGitContextLevel(gitContext ?? allowed, allowed);
2705
- const gitBudget = advisorGitContextBudget(contextMaxCharsRef, advisorGitContextMaxCharsRef);
2706
- const changes = collectGitContext(ctx.cwd, level, gitBudget, advisorRedactSecretsRef ? redactSecrets : undefined);
2707
- const changeText = advisorRepositoryContext(changes, gitContext ?? allowed, level, gitBudget);
2708
- const conversationBudget = Math.max(0, contextMaxCharsRef - changeText.length);
2709
- const legacyConversation = advisorRequestConversation(ctx, conversationBudget);
2710
- const curated = await curateAdvisorConversation(ctx, legacyConversation, signal, onScout, advisorScoutEnabledRef, runAdvisorScout, currentInvocationId, conversationBudget);
2711
- const { conversation, scout } = curated;
2712
- const preferences = await readProjectPreferences(ctx, 8 * 1024, advisorRedactSecretsRef);
2713
- const draftText = draft ? redactAndCapText(draft, 8 * 1024, advisorRedactSecretsRef) : undefined;
2714
- const untracked = await readUntrackedFiles(ctx.cwd, includeUntracked ?? [], advisorUntrackedContentRef, advisorRedactSecretsRef);
2715
- const tracked = await readTrackedFiles(ctx.cwd, includeTracked ?? [], advisorTrackedFileContentRef, advisorRedactSecretsRef, Math.max(0, 24 * 1024 - untracked.reduce((sum, item) => sum + item.bytes, 0)));
2875
+ const context = await assembleConsultationContext(options);
2716
2876
  const outboundQuestion = advisorRedactSecretsRef && question !== undefined ? redactSecrets(question) : question;
2717
2877
  const messages = [
2718
2878
  {
2719
2879
  content: [
2720
2880
  {
2721
- text: advisorMessageText(conversation, outboundQuestion, changeText, draftText, preferences?.text, untracked.map((item) => `<file path=${JSON.stringify(item.path)}>
2722
- ${item.text}
2723
- </file>`), tracked.map((item) => `<file path=${JSON.stringify(item.path)}>
2724
- ${item.text}
2725
- </file>`)),
2881
+ text: advisorMessageText(context.conversation, outboundQuestion, context.changeText, context.draftText, context.preferences?.text, context.untracked.map(fileTag), context.tracked.map(fileTag)),
2726
2882
  type: "text"
2727
2883
  }
2728
2884
  ],
@@ -2732,34 +2888,54 @@ ${item.text}
2732
2888
  ];
2733
2889
  const streamed = await collectTextStream(resolved, {
2734
2890
  messages,
2735
- onChunk,
2891
+ onChunk: options.onChunk,
2736
2892
  reasoning: advisorEffortRef,
2737
2893
  signal,
2738
2894
  systemPrompt
2739
2895
  });
2740
2896
  const markdown = streamed.text;
2741
2897
  if (!markdown.trim()) {
2742
- throw new Error("Advisor returned no advice.");
2898
+ throw new AdvisorNoAdviceError;
2743
2899
  }
2744
2900
  return {
2745
- draftBytes: draftText ? Buffer.byteLength(draftText, "utf8") : undefined,
2901
+ draftBytes: context.draftText ? Buffer.byteLength(context.draftText, "utf8") : undefined,
2746
2902
  markdown,
2747
2903
  model: advisorRef,
2748
- preferenceBytes: preferences?.bytes,
2904
+ preferenceBytes: context.preferences?.bytes,
2749
2905
  thinkingText: streamed.thinking,
2750
- trackedBytes: tracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
2751
- untrackedBytes: untracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
2906
+ trackedBytes: context.tracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
2907
+ untrackedBytes: context.untracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
2752
2908
  usage: streamed.usage,
2753
- ...scout ? { scout } : {}
2909
+ ...context.scout ? { scout: context.scout } : {}
2754
2910
  };
2755
2911
  };
2756
2912
  var consultAdvisor = async (ctx, question, signal, onChunk, trigger = "executor-requested", gitContext, draft, includeUntracked, includeTracked, onScout, currentInvocationId) => {
2757
- const result = await collectAdvisorResponse(ctx, ADVISOR_SYSTEM, question, signal, onChunk, gitContext, draft, includeUntracked, includeTracked, onScout, currentInvocationId);
2913
+ const result = await collectAdvisorResponse({
2914
+ ctx,
2915
+ currentInvocationId,
2916
+ draft,
2917
+ gitContext,
2918
+ includeTracked,
2919
+ includeUntracked,
2920
+ onChunk,
2921
+ onScout,
2922
+ question,
2923
+ signal,
2924
+ systemPrompt: ADVISOR_SYSTEM
2925
+ });
2758
2926
  return { ...result, adviceId: randomUUID(), trigger };
2759
2927
  };
2760
2928
  var runAdvisorGate = async (ctx, question, trigger = "repeated-tool-call", signal, onChunk, onScout, currentInvocationId) => {
2761
2929
  try {
2762
- const result = await collectAdvisorResponse(ctx, ADVISOR_DECISION_SYSTEM, question, signal, onChunk, undefined, undefined, undefined, undefined, onScout, currentInvocationId);
2930
+ const result = await collectAdvisorResponse({
2931
+ ctx,
2932
+ currentInvocationId,
2933
+ onChunk,
2934
+ onScout,
2935
+ question,
2936
+ signal,
2937
+ systemPrompt: ADVISOR_DECISION_SYSTEM
2938
+ });
2763
2939
  const parsed = parseAutomaticDecision(result.markdown);
2764
2940
  if (!parsed.ok) {
2765
2941
  return { ...parsed, usage: result.usage };
@@ -2777,7 +2953,7 @@ var runAdvisorGate = async (ctx, question, trigger = "repeated-tool-call", signa
2777
2953
  }
2778
2954
  const message = error instanceof Error ? error.message : String(error);
2779
2955
  return {
2780
- category: message === "Advisor returned no advice." ? "empty-response" : "provider-error",
2956
+ category: error instanceof AdvisorNoAdviceError ? "empty-response" : "provider-error",
2781
2957
  message,
2782
2958
  ok: false
2783
2959
  };
@@ -2796,6 +2972,46 @@ import {
2796
2972
  truncateToWidth as truncateToWidth2,
2797
2973
  visibleWidth
2798
2974
  } from "@earendil-works/pi-tui";
2975
+
2976
+ // src/pi-settings.ts
2977
+ import { existsSync as existsSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
2978
+ import { join as join3 } from "node:path";
2979
+ import { getAgentDir as getAgentDir2 } from "@earendil-works/pi-coding-agent";
2980
+ var SETTING_RECHECK_INTERVAL_MS = 2000;
2981
+ var cache = new Map;
2982
+ var settingsIdentity = (path) => {
2983
+ try {
2984
+ const stats = statSync2(path, { bigint: true });
2985
+ return `${stats.mtimeNs}:${stats.ctimeNs}:${stats.size}:${stats.ino}`;
2986
+ } catch {
2987
+ return `unstattable:${process.hrtime.bigint()}`;
2988
+ }
2989
+ };
2990
+ var readHideThinking = (path) => {
2991
+ try {
2992
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
2993
+ return typeof parsed === "object" && parsed !== null && parsed.hideThinkingBlock === true;
2994
+ } catch {
2995
+ return false;
2996
+ }
2997
+ };
2998
+ var piHideThinkingEnabled = () => {
2999
+ const path = join3(getAgentDir2(), "settings.json");
3000
+ if (!existsSync2(path)) {
3001
+ return false;
3002
+ }
3003
+ const now = Date.now();
3004
+ const cached = cache.get(path);
3005
+ if (cached && now - cached.readAt < SETTING_RECHECK_INTERVAL_MS) {
3006
+ return cached.hidden;
3007
+ }
3008
+ const identity = settingsIdentity(path);
3009
+ const hidden = cached && cached.identity === identity ? cached.hidden : readHideThinking(path);
3010
+ cache.set(path, { hidden, identity, readAt: now });
3011
+ return hidden;
3012
+ };
3013
+
3014
+ // src/tools/render-common.ts
2799
3015
  var SPINNER_FRAMES = [
2800
3016
  "⠋",
2801
3017
  "⠙",
@@ -2808,7 +3024,7 @@ var SPINNER_FRAMES = [
2808
3024
  "⠇",
2809
3025
  "⠏"
2810
3026
  ];
2811
- var THINKING_PREFIX = " \uD83D\uDCAD ";
3027
+ var THINKING_PREFIX = " ";
2812
3028
  var THINKING_PREFIX_WIDTH = visibleWidth(THINKING_PREFIX);
2813
3029
 
2814
3030
  class ThinkingMarkdown {
@@ -2831,7 +3047,18 @@ class ThinkingMarkdown {
2831
3047
  this.markdown.invalidate();
2832
3048
  }
2833
3049
  }
2834
- var renderThinkingMarkdown = (thinking, theme) => new ThinkingMarkdown(thinking, theme);
3050
+
3051
+ class HiddenThinkingLabel {
3052
+ label;
3053
+ constructor(theme) {
3054
+ this.label = theme.fg("thinkingText", `${THINKING_PREFIX}Thinking…`);
3055
+ }
3056
+ render() {
3057
+ return [this.label];
3058
+ }
3059
+ invalidate() {}
3060
+ }
3061
+ var renderThinkingMarkdown = (thinking, theme) => piHideThinkingEnabled() ? new HiddenThinkingLabel(theme) : new ThinkingMarkdown(thinking, theme);
2835
3062
  var resolveAdvisorRequest = (question) => question?.trim() || undefined;
2836
3063
  var renderAdvisorCallBox = (question, theme) => {
2837
3064
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
@@ -3078,42 +3305,41 @@ var normalizeToolInput = (toolName, input) => {
3078
3305
  return visit(input);
3079
3306
  };
3080
3307
  var normalizedToolSignature = (toolName, input) => `${toolName}:${JSON.stringify(normalizeToolInput(toolName, input))}`;
3308
+ var freshRepetition = () => ({
3309
+ count: 0,
3310
+ interventions: 0
3311
+ });
3312
+ var freshAdviceLedger = () => ({
3313
+ draftConsultations: 0,
3314
+ issued: new Map,
3315
+ outcomes: 0,
3316
+ pending: new Set,
3317
+ reported: new Set
3318
+ });
3319
+ var freshUsage = () => ({
3320
+ invocations: [],
3321
+ totals: emptyAdvisorUsageTotals()
3322
+ });
3081
3323
 
3082
3324
  class AdvisorSessionState {
3083
- #previousSignature;
3084
- #repetitions = 0;
3325
+ #repetition = freshRepetition();
3085
3326
  #blockedReason;
3086
- #invocations = [];
3087
- #loopInterventions = 0;
3327
+ #ledger = freshAdviceLedger();
3328
+ #usage = freshUsage();
3088
3329
  #consumedCalls = 0;
3089
- #issuedAdvice = new Map;
3090
- #pendingAdvice = new Set;
3091
- #reportedAdvice = new Set;
3092
- #draftConsultations = 0;
3093
- #outcomes = 0;
3094
- #lastAdvice;
3095
- #usage = emptyAdvisorUsageTotals();
3096
3330
  resetTask() {
3097
- this.#previousSignature = undefined;
3098
- this.#repetitions = 0;
3331
+ this.#repetition = freshRepetition();
3099
3332
  this.#blockedReason = undefined;
3100
- this.#invocations = [];
3101
- this.#loopInterventions = 0;
3333
+ this.#ledger = freshAdviceLedger();
3334
+ this.#usage = freshUsage();
3102
3335
  this.#consumedCalls = 0;
3103
- this.#issuedAdvice.clear();
3104
- this.#pendingAdvice.clear();
3105
- this.#reportedAdvice.clear();
3106
- this.#draftConsultations = 0;
3107
- this.#outcomes = 0;
3108
- this.#lastAdvice = undefined;
3109
- this.#usage = emptyAdvisorUsageTotals();
3110
3336
  }
3111
3337
  clearBlocked() {
3112
3338
  this.#blockedReason = undefined;
3113
3339
  }
3114
3340
  resetRepetition() {
3115
- this.#previousSignature = undefined;
3116
- this.#repetitions = 0;
3341
+ this.#repetition.count = 0;
3342
+ this.#repetition.previousSignature = undefined;
3117
3343
  }
3118
3344
  get blocked() {
3119
3345
  return this.#blockedReason !== undefined;
@@ -3129,12 +3355,12 @@ class AdvisorSessionState {
3129
3355
  return false;
3130
3356
  }
3131
3357
  const signature = normalizedToolSignature(toolName, input);
3132
- this.#repetitions = signature === this.#previousSignature ? this.#repetitions + 1 : 1;
3133
- this.#previousSignature = signature;
3134
- if (this.#repetitions < threshold) {
3358
+ this.#repetition.count = signature === this.#repetition.previousSignature ? this.#repetition.count + 1 : 1;
3359
+ this.#repetition.previousSignature = signature;
3360
+ if (this.#repetition.count < threshold) {
3135
3361
  return false;
3136
3362
  }
3137
- this.#loopInterventions += 1;
3363
+ this.#repetition.interventions += 1;
3138
3364
  return true;
3139
3365
  }
3140
3366
  canConsult(limit) {
@@ -3150,58 +3376,59 @@ class AdvisorSessionState {
3150
3376
  return this.#consumedCalls;
3151
3377
  }
3152
3378
  get usageTotals() {
3153
- return { ...this.#usage };
3379
+ return { ...this.#usage.totals };
3154
3380
  }
3155
3381
  usageStatus() {
3156
- return formatAdvisorUsageStatus(this.#usage);
3382
+ return formatAdvisorUsageStatus(this.#usage.totals);
3157
3383
  }
3158
3384
  recordInvocation(record) {
3159
- this.#invocations.push(record);
3160
- addAdvisorUsage(this.#usage, record.usage);
3385
+ this.#usage.invocations.push(record);
3386
+ addAdvisorUsage(this.#usage.totals, record.usage);
3161
3387
  }
3162
3388
  issueAdvice(id, advice, trigger, draft = false) {
3163
- this.#issuedAdvice.set(id, { advice, trigger });
3164
- this.#lastAdvice = advice;
3389
+ this.#ledger.issued.set(id, { advice, trigger });
3390
+ this.#ledger.lastAdvice = advice;
3165
3391
  if (draft) {
3166
- this.#draftConsultations += 1;
3392
+ this.#ledger.draftConsultations += 1;
3167
3393
  }
3168
3394
  }
3169
3395
  claimTrackedFiles(paths) {
3170
- if (!this.#lastAdvice || paths.length === 0) {
3396
+ const advice = this.#ledger.lastAdvice;
3397
+ if (!advice || paths.length === 0) {
3171
3398
  return false;
3172
3399
  }
3173
3400
  const mentioned = paths.every((path) => {
3174
3401
  const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3175
3402
  const boundary = "(^|[\\s\\\"'`()\\[])" + escaped + "(?=$|[\\s\\\"'`),;:!?\\]]|\\.(?=\\s|$))";
3176
- return new RegExp(boundary).test(this.#lastAdvice);
3403
+ return new RegExp(boundary).test(advice);
3177
3404
  });
3178
3405
  if (!mentioned) {
3179
3406
  return false;
3180
3407
  }
3181
- this.#lastAdvice = undefined;
3408
+ this.#ledger.lastAdvice = undefined;
3182
3409
  return true;
3183
3410
  }
3184
3411
  reserveAdvice(id) {
3185
- if (this.#reportedAdvice.has(id) || this.#pendingAdvice.has(id)) {
3412
+ if (this.#ledger.reported.has(id) || this.#ledger.pending.has(id)) {
3186
3413
  return;
3187
3414
  }
3188
- const advice = this.#issuedAdvice.get(id);
3415
+ const advice = this.#ledger.issued.get(id);
3189
3416
  if (!advice) {
3190
3417
  return;
3191
3418
  }
3192
- this.#pendingAdvice.add(id);
3419
+ this.#ledger.pending.add(id);
3193
3420
  return advice;
3194
3421
  }
3195
3422
  commitAdvice(id) {
3196
- if (!this.#pendingAdvice.delete(id)) {
3423
+ if (!this.#ledger.pending.delete(id)) {
3197
3424
  return false;
3198
3425
  }
3199
- this.#reportedAdvice.add(id);
3200
- this.#outcomes += 1;
3426
+ this.#ledger.reported.add(id);
3427
+ this.#ledger.outcomes += 1;
3201
3428
  return true;
3202
3429
  }
3203
3430
  releaseAdvice(id) {
3204
- this.#pendingAdvice.delete(id);
3431
+ this.#ledger.pending.delete(id);
3205
3432
  }
3206
3433
  claimAdvice(id) {
3207
3434
  const advice = this.reserveAdvice(id);
@@ -3211,34 +3438,47 @@ class AdvisorSessionState {
3211
3438
  this.commitAdvice(id);
3212
3439
  return advice;
3213
3440
  }
3214
- summary(limit) {
3215
- if (this.#invocations.length === 0 && this.#loopInterventions === 0) {
3216
- return;
3217
- }
3218
- const markdown = this.#invocations.filter((item) => item.kind === "markdown");
3219
- const gates = this.#invocations.filter((item) => item.kind === "gate");
3220
- const countTrigger = (trigger) => this.#invocations.filter((item) => item.trigger === trigger).length;
3221
- const decisions = ["proceed", "revise", "blocked"].map((decision) => [
3441
+ #decisionsLine() {
3442
+ const gates = this.#usage.invocations.filter((item) => item.kind === "gate");
3443
+ return ["proceed", "revise", "blocked"].map((decision) => [
3222
3444
  decision,
3223
3445
  gates.filter((item) => item.decision === decision).length
3224
3446
  ]).filter(([, count]) => count > 0).map(([decision, count]) => `${count} ${decision}`).join(", ") || "none";
3225
- const effects = (effect) => this.#invocations.filter((item) => item.executionEffect === effect).length;
3226
- const failures = this.#invocations.filter((item) => item.failure).map((item) => item.failure);
3227
- const models = [
3228
- ...new Set(this.#invocations.map((item) => item.model).filter(Boolean))
3229
- ].join(", ") || "unknown";
3447
+ }
3448
+ #countTrigger(trigger) {
3449
+ return this.#usage.invocations.filter((item) => item.trigger === trigger).length;
3450
+ }
3451
+ #triggersLine() {
3452
+ return [
3453
+ "manual",
3454
+ "executor-requested",
3455
+ "repeated-tool-call",
3456
+ "completion-review",
3457
+ "custom-rule"
3458
+ ].filter((trigger) => this.#countTrigger(trigger) > 0).join(", ") || "none";
3459
+ }
3460
+ summary(limit) {
3461
+ const { invocations, totals } = this.#usage;
3462
+ if (invocations.length === 0 && this.#repetition.interventions === 0) {
3463
+ return;
3464
+ }
3465
+ const markdown = invocations.filter((item) => item.kind === "markdown");
3466
+ const gates = invocations.filter((item) => item.kind === "gate");
3467
+ const effects = (effect) => invocations.filter((item) => item.executionEffect === effect).length;
3468
+ const failures = invocations.filter((item) => item.failure).map((item) => item.failure);
3469
+ const models = [...new Set(invocations.map((item) => item.model).filter(Boolean))].join(", ") || "unknown";
3230
3470
  const budget = limit === undefined ? `${this.#consumedCalls} used; unlimited remaining` : `${this.#consumedCalls} / ${limit} used; ${Math.max(0, limit - this.#consumedCalls)} remaining`;
3231
3471
  return [
3232
3472
  "[Session Advisor Summary]",
3233
- `Consultations: ${markdown.length} Markdown (${countTrigger("manual")} manual, ${countTrigger("executor-requested")} executor-requested), automatic gates: ${gates.length}`,
3234
- `Triggers: ${["manual", "executor-requested", "repeated-tool-call", "completion-review", "custom-rule"].filter((trigger) => countTrigger(trigger) > 0).join(", ") || "none"}`,
3473
+ `Consultations: ${markdown.length} Markdown (${this.#countTrigger("manual")} manual, ${this.#countTrigger("executor-requested")} executor-requested), automatic gates: ${gates.length}`,
3474
+ `Triggers: ${this.#triggersLine()}`,
3235
3475
  `Models: ${models}`,
3236
3476
  `Budget: ${budget}`,
3237
- `Usage: ${formatAdvisorUsageTotals(this.#usage)}`,
3238
- `Markdown advice: ${markdown.length} responses (${this.#draftConsultations} with drafts)`,
3239
- `Outcome reports: ${this.#outcomes}`,
3240
- `Gate decisions: ${decisions}`,
3241
- `Loop matching: normalized tool signatures; ${this.#loopInterventions} gate intervention${this.#loopInterventions === 1 ? "" : "s"}`,
3477
+ `Usage: ${formatAdvisorUsageTotals(totals)}`,
3478
+ `Markdown advice: ${markdown.length} responses (${this.#ledger.draftConsultations} with drafts)`,
3479
+ `Outcome reports: ${this.#ledger.outcomes}`,
3480
+ `Gate decisions: ${this.#decisionsLine()}`,
3481
+ `Loop matching: normalized tool signatures; ${this.#repetition.interventions} gate intervention${this.#repetition.interventions === 1 ? "" : "s"}`,
3242
3482
  `Execution effects: ${effects("tool-blocked")} tool blocked, ${effects("session-blocked")} sessions blocked, ${effects("continued")} continued`,
3243
3483
  `Failures: ${failures.length ? failures.join(", ") : "none"}`
3244
3484
  ].join(`
@@ -4099,7 +4339,6 @@ import {
4099
4339
 
4100
4340
  // src/ui/settings-formatting.ts
4101
4341
  import { visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
4102
- var DEFAULT_EFFORT_LEVEL2 = "Default (Model Default)";
4103
4342
  var TOGGLE_VALUES = ["On", "Off"];
4104
4343
  var SIMPLE_MODE_GRADIENT_INTERVAL_MS = 100;
4105
4344
  var SIMPLE_MODE_GRADIENT_COLORS = [
@@ -4156,7 +4395,7 @@ var contextDescription = (presets, contextMaxChars) => {
4156
4395
  ${meterPrefix}${meter} full
4157
4396
  ${markerLabel}`;
4158
4397
  };
4159
- var currentEffort = (effort) => effort || DEFAULT_EFFORT_LEVEL2;
4398
+ var currentEffort = (effort) => effort || DEFAULT_EFFORT_LEVEL;
4160
4399
  var rainbowGradient = (text, startedAt) => {
4161
4400
  const frame = Math.floor((Date.now() - startedAt) / SIMPLE_MODE_GRADIENT_INTERVAL_MS);
4162
4401
  const shinePosition = frame % (text.length * 2);
@@ -4746,8 +4985,8 @@ import {
4746
4985
  unlink,
4747
4986
  writeFile
4748
4987
  } from "node:fs/promises";
4749
- import { join as join3 } from "node:path";
4750
- import { getAgentDir as getAgentDir2 } from "@earendil-works/pi-coding-agent";
4988
+ import { join as join4 } from "node:path";
4989
+ import { getAgentDir as getAgentDir3 } from "@earendil-works/pi-coding-agent";
4751
4990
  var ADOPTIONS = [
4752
4991
  "followed",
4753
4992
  "not-followed",
@@ -4760,11 +4999,11 @@ var VALIDATIONS = [
4760
4999
  "unknown"
4761
5000
  ];
4762
5001
  var MAX_LOG_BYTES = 1024 * 1024;
4763
- var statePath = () => join3(getAgentDir2(), "advisor-outcomes-salt");
4764
- var outcomeLogPath = () => join3(getAgentDir2(), "advisor-outcomes.jsonl");
5002
+ var statePath = () => join4(getAgentDir3(), "advisor-outcomes-salt");
5003
+ var outcomeLogPath = () => join4(getAgentDir3(), "advisor-outcomes.jsonl");
4765
5004
  var salt = async () => {
4766
5005
  const path = statePath();
4767
- await mkdir(getAgentDir2(), { mode: 448, recursive: true });
5006
+ await mkdir(getAgentDir3(), { mode: 448, recursive: true });
4768
5007
  for (let attempt = 0;attempt < 20; attempt += 1) {
4769
5008
  try {
4770
5009
  const existing = await readFile(path);
@@ -4841,7 +5080,7 @@ var withOutcomeLock = async (run) => {
4841
5080
  var adviceDigest = (advice, key) => createHmac("sha256", key).update(advice).digest("hex").slice(0, 16);
4842
5081
  var appendOutcome = async (record) => {
4843
5082
  const path = outcomeLogPath();
4844
- await mkdir(getAgentDir2(), { mode: 448, recursive: true });
5083
+ await mkdir(getAgentDir3(), { mode: 448, recursive: true });
4845
5084
  return withOutcomeLock(async () => {
4846
5085
  const next = {
4847
5086
  adoption: record.adoption,
@@ -4949,7 +5188,7 @@ var reserveAdvisorCall = (event, ctx, session, reservedCalls) => {
4949
5188
  import {
4950
5189
  getMarkdownTheme as getMarkdownTheme4
4951
5190
  } from "@earendil-works/pi-coding-agent";
4952
- import { Box as Box3, Markdown as Markdown4, Text as Text5 } from "@earendil-works/pi-tui";
5191
+ import { Box as Box3, Markdown as Markdown4, Spacer, Text as Text5 } from "@earendil-works/pi-tui";
4953
5192
  var advisorResultDetails = (result) => result.details;
4954
5193
  var syncRenderPhase = (context, phase) => {
4955
5194
  if (context.state.phase !== phase && context.state.timerId) {
@@ -5036,7 +5275,7 @@ var renderFinalAdvisorResult = (box, result, expanded, theme, context) => {
5036
5275
  box.addChild(new Markdown4(adviceForDisplay(displayAdvice, expanded), 0, 0, getMarkdownTheme4()));
5037
5276
  };
5038
5277
  var renderAdvisorResult = (result, { isPartial, expanded }, theme, context) => {
5039
- const box = context.lastComponent instanceof Box3 ? context.lastComponent : new Box3(1, 1, (text) => theme.bg("customMessageBg", text));
5278
+ const box = context.lastComponent instanceof Box3 ? context.lastComponent : new Box3(1, 0, (text) => theme.bg("customMessageBg", text));
5040
5279
  box.setBgFn((text) => theme.bg("customMessageBg", text));
5041
5280
  box.clear();
5042
5281
  if (isPartial) {
@@ -5044,6 +5283,7 @@ var renderAdvisorResult = (result, { isPartial, expanded }, theme, context) => {
5044
5283
  } else {
5045
5284
  renderFinalAdvisorResult(box, result, expanded, theme, context);
5046
5285
  }
5286
+ box.addChild(new Spacer(1));
5047
5287
  return box;
5048
5288
  };
5049
5289
 
@@ -5231,6 +5471,44 @@ var sendAutomaticGateResult = (pi, result) => {
5231
5471
  display: true
5232
5472
  }, { deliverAs: "steer" });
5233
5473
  };
5474
+ var applyGateDecision = (pi, ctx, session, result, reason, failureMode) => {
5475
+ if (!result.ok) {
5476
+ session.recordInvocation({
5477
+ executionEffect: gateFailureEffectForMode(failureMode),
5478
+ failure: result.category,
5479
+ kind: "gate",
5480
+ model: advisorRef,
5481
+ trigger: "repeated-tool-call",
5482
+ usage: result.usage
5483
+ });
5484
+ updateAdvisorUsageStatus(ctx, session);
5485
+ const failure = failureEffect(result.category, result.message, ctx, session, failureMode);
5486
+ sendAutomaticGateFailure(pi, `**Advisor gate failure (${result.category}):** ${result.message}`, result.usage);
5487
+ return failure.block ? { block: true, reason: `${reason}
5488
+ ${failure.reason}` } : undefined;
5489
+ }
5490
+ session.recordInvocation({
5491
+ cost: advisorUsageCost(result.usage),
5492
+ decision: result.decision,
5493
+ executionEffect: gateDecisionEffect(result.decision, failureMode),
5494
+ kind: "gate",
5495
+ model: result.model,
5496
+ trigger: result.trigger,
5497
+ usage: result.usage
5498
+ });
5499
+ updateAdvisorUsageStatus(ctx, session);
5500
+ sendAutomaticGateResult(pi, result);
5501
+ if (result.decision === "proceed") {
5502
+ session.resetRepetition();
5503
+ return;
5504
+ }
5505
+ const gateReason = `Advisor loop review: ${result.markdown}`;
5506
+ if (result.decision === "blocked") {
5507
+ const effect = blockedDecisionEffect(gateReason, ctx, session, failureMode);
5508
+ return effect.block ? { block: true, reason: effect.reason } : undefined;
5509
+ }
5510
+ return { block: true, reason: gateReason };
5511
+ };
5234
5512
  var handleAutomaticGate = async (pi, event, ctx, session, runGate, scoutStatus) => {
5235
5513
  if (isSimpleMode() || event.toolName === "ask_advisor" || !advisorAutoLoopGateRef || !session.recordToolCall(event.toolName, event.input, advisorLoopThresholdRef)) {
5236
5514
  return;
@@ -5265,42 +5543,7 @@ var handleAutomaticGate = async (pi, event, ctx, session, runGate, scoutStatus)
5265
5543
  }
5266
5544
  }, event.toolCallId);
5267
5545
  ensureGateCall();
5268
- if (!result.ok) {
5269
- session.recordInvocation({
5270
- executionEffect: gateFailureEffectForMode(failureMode),
5271
- failure: result.category,
5272
- kind: "gate",
5273
- model: advisorRef,
5274
- trigger: "repeated-tool-call",
5275
- usage: result.usage
5276
- });
5277
- updateAdvisorUsageStatus(ctx, session);
5278
- const failure = failureEffect(result.category, result.message, ctx, session, failureMode);
5279
- sendAutomaticGateFailure(pi, `**Advisor gate failure (${result.category}):** ${result.message}`, result.usage);
5280
- return failure.block ? { block: true, reason: `${reason}
5281
- ${failure.reason}` } : undefined;
5282
- }
5283
- session.recordInvocation({
5284
- cost: advisorUsageCost(result.usage),
5285
- decision: result.decision,
5286
- executionEffect: gateDecisionEffect(result.decision, failureMode),
5287
- kind: "gate",
5288
- model: result.model,
5289
- trigger: result.trigger,
5290
- usage: result.usage
5291
- });
5292
- updateAdvisorUsageStatus(ctx, session);
5293
- sendAutomaticGateResult(pi, result);
5294
- if (result.decision === "proceed") {
5295
- session.resetRepetition();
5296
- return;
5297
- }
5298
- const gateReason = `Advisor loop review: ${result.markdown}`;
5299
- if (result.decision === "blocked") {
5300
- const effect = blockedDecisionEffect(gateReason, ctx, session, failureMode);
5301
- return effect.block ? { block: true, reason: effect.reason } : undefined;
5302
- }
5303
- return { block: true, reason: gateReason };
5546
+ return applyGateDecision(pi, ctx, session, result, reason, failureMode);
5304
5547
  } finally {
5305
5548
  scoutStatus.release(ctx, scoutStatusToken);
5306
5549
  herdrAdvisorActivity.finish();