@sema-agent/core 7.16.0 → 7.17.1

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 (207) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/dist/agents/builtin-agents.d.ts +3 -3
  3. package/dist/agents/cross-session-envelope.d.ts +0 -1
  4. package/dist/agents/cross-session-envelope.js +0 -1
  5. package/dist/agents/peer-notices.d.ts +0 -9
  6. package/dist/agents/peer-notices.js +0 -63
  7. package/dist/agents/subagent.js +7 -15
  8. package/dist/brain/request-params.d.ts +2 -2
  9. package/dist/config/defaults.d.ts +0 -7
  10. package/dist/config/defaults.js +0 -2
  11. package/dist/core/artifact-host.d.ts +218 -0
  12. package/dist/core/artifact-host.js +92 -0
  13. package/dist/core/ask-origin.d.ts +29 -3
  14. package/dist/core/ask-origin.js +17 -9
  15. package/dist/core/ask-question.d.ts +25 -0
  16. package/dist/core/ask-question.js +4 -0
  17. package/dist/core/ask-unresolvable-notice.d.ts +30 -21
  18. package/dist/core/ask-unresolvable-notice.js +16 -8
  19. package/dist/core/auto-compaction.d.ts +10 -10
  20. package/dist/core/auto-mode-arming.d.ts +2 -2
  21. package/dist/core/background-agent-store.d.ts +5 -5
  22. package/dist/core/background-shell.d.ts +1 -1
  23. package/dist/core/checkpoint-execution-record.d.ts +0 -3
  24. package/dist/core/checkpoint-execution-record.js +0 -4
  25. package/dist/core/checkpoint-store.d.ts +71 -99
  26. package/dist/core/checkpoint-store.js +9 -16
  27. package/dist/core/context-edit.d.ts +36 -104
  28. package/dist/core/context-edit.js +37 -60
  29. package/dist/core/effective-path-target.d.ts +68 -8
  30. package/dist/core/effective-path-target.js +42 -30
  31. package/dist/core/engine-notice.d.ts +46 -32
  32. package/dist/core/env-budget.d.ts +31 -0
  33. package/dist/core/env-budget.js +39 -0
  34. package/dist/core/exec-output-tail.d.ts +5 -5
  35. package/dist/core/file-history-store.js +3 -6
  36. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  37. package/dist/core/fs-write-gate-policy.js +3 -3
  38. package/dist/core/gate-lanes.js +9 -15
  39. package/dist/core/governance-codes.d.ts +1 -1
  40. package/dist/core/governance-codes.js +2 -0
  41. package/dist/core/hands-band.d.ts +1 -1
  42. package/dist/core/hooks.d.ts +18 -17
  43. package/dist/core/lsp-session.d.ts +1 -1
  44. package/dist/core/lsp.d.ts +1 -1
  45. package/dist/core/lsp.js +5 -4
  46. package/dist/core/mcp.d.ts +5 -5
  47. package/dist/core/mcp.js +4 -48
  48. package/dist/core/memory-engine/engine.js +2 -1
  49. package/dist/core/memory-engine/layout.d.ts +18 -6
  50. package/dist/core/memory-engine/layout.js +40 -21
  51. package/dist/core/memory-recall.d.ts +0 -8
  52. package/dist/core/memory.d.ts +1 -1
  53. package/dist/core/oracle-isolation.js +2 -2
  54. package/dist/core/parent-spec-seat.d.ts +66 -0
  55. package/dist/core/parent-spec-seat.js +47 -0
  56. package/dist/core/permission-rule-model.d.ts +11 -6
  57. package/dist/core/permission-rule-model.js +53 -37
  58. package/dist/core/permission-rules.js +8 -3
  59. package/dist/core/physical-path.d.ts +37 -0
  60. package/dist/core/physical-path.js +30 -0
  61. package/dist/core/protocol-naming.d.ts +2 -2
  62. package/dist/core/remote-env.d.ts +1 -1
  63. package/dist/core/retired-keys.d.ts +19 -0
  64. package/dist/core/retired-keys.js +15 -0
  65. package/dist/core/runner/active-skill-scope.js +3 -3
  66. package/dist/core/runner/assemble-result.d.ts +15 -13
  67. package/dist/core/runner/assemble-result.js +5 -4
  68. package/dist/core/runner/compaction-call-options.d.ts +25 -89
  69. package/dist/core/runner/contracts.d.ts +11 -10
  70. package/dist/core/runner/gate-exit.js +15 -1
  71. package/dist/core/runner/halt-attribution.d.ts +35 -0
  72. package/dist/core/runner/halt-attribution.js +16 -0
  73. package/dist/core/runner/memory-consolidation.d.ts +0 -2
  74. package/dist/core/runner/memory-consolidation.js +0 -1
  75. package/dist/core/runner/prepare-artifact.d.ts +52 -0
  76. package/dist/core/runner/prepare-artifact.js +63 -0
  77. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  78. package/dist/core/runner/prepare-caps-and-workflow.js +2 -3
  79. package/dist/core/runner/prepare-config-doors.d.ts +3 -3
  80. package/dist/core/runner/prepare-config-doors.js +5 -2
  81. package/dist/core/runner/prepare-context-lane.d.ts +2 -2
  82. package/dist/core/runner/prepare-context-lane.js +2 -13
  83. package/dist/core/runner/prepare-file-history.d.ts +1 -1
  84. package/dist/core/runner/prepare-file-history.js +3 -52
  85. package/dist/core/runner/prepare-hands-readface.d.ts +4 -7
  86. package/dist/core/runner/prepare-hands-readface.js +3 -2
  87. package/dist/core/runner/prepare-inherited-gate.d.ts +5 -2
  88. package/dist/core/runner/prepare-inherited-gate.js +7 -8
  89. package/dist/core/runner/prepare-memory.d.ts +4 -6
  90. package/dist/core/runner/prepare-memory.js +4 -2
  91. package/dist/core/runner/prepare-policy-chain.d.ts +3 -3
  92. package/dist/core/runner/prepare-policy-chain.js +15 -30
  93. package/dist/core/runner/prepare-prompt-assembly.js +1 -1
  94. package/dist/core/runner/prepare-question-face.js +2 -1
  95. package/dist/core/runner/prepare-safety-scan.d.ts +1 -1
  96. package/dist/core/runner/prepare-suspend-saga.js +0 -2
  97. package/dist/core/runner/prepare-task.js +12 -11
  98. package/dist/core/runner/prepare-wiring-manifest.js +9 -7
  99. package/dist/core/runner/resume-admission.js +2 -1
  100. package/dist/core/runner/resume-internals-and-config.js +4 -0
  101. package/dist/core/runner/retired-runner-deps.d.ts +3 -0
  102. package/dist/core/runner/retired-runner-deps.js +7 -0
  103. package/dist/core/runner/run-harness-handlers.js +5 -3
  104. package/dist/core/runner/run-settle-and-teardown.d.ts +1 -1
  105. package/dist/core/runner/run-settle-and-teardown.js +3 -1
  106. package/dist/core/runner/session-file-state-replay.js +3 -3
  107. package/dist/core/runner/session-rule-policy.js +3 -4
  108. package/dist/core/runner/stream-halt-verbs.js +1 -1
  109. package/dist/core/runner/stream-steer-verb.js +1 -4
  110. package/dist/core/runner/workspace-path.js +3 -0
  111. package/dist/core/runner-deps.d.ts +30 -49
  112. package/dist/core/safe-notify.d.ts +1 -1
  113. package/dist/core/sensitive-path-policy.d.ts +1 -1
  114. package/dist/core/sensitive-path-policy.js +11 -12
  115. package/dist/core/session.d.ts +2 -2
  116. package/dist/core/skill-tool-specifier.d.ts +1 -1
  117. package/dist/core/skills-directory.d.ts +1 -1
  118. package/dist/core/skills-directory.js +4 -3
  119. package/dist/core/spec-contract.js +5 -4
  120. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  121. package/dist/core/strategy-store.d.ts +8 -11
  122. package/dist/core/task-event.d.ts +20 -17
  123. package/dist/core/task-limits.d.ts +1 -1
  124. package/dist/core/task-notification.d.ts +5 -5
  125. package/dist/core/task-registry-agent.d.ts +5 -5
  126. package/dist/core/task-registry-monitor.d.ts +2 -2
  127. package/dist/core/task-registry-shared.d.ts +32 -28
  128. package/dist/core/task-registry-shared.js +4 -13
  129. package/dist/core/task-registry.d.ts +8 -8
  130. package/dist/core/task-result.d.ts +37 -37
  131. package/dist/core/task-spec.d.ts +5 -5
  132. package/dist/core/task-stream.d.ts +16 -11
  133. package/dist/core/tool-catalog-entries.d.ts +1 -0
  134. package/dist/core/tool-catalog-entries.js +6 -1
  135. package/dist/core/tool-conformance.d.ts +0 -2
  136. package/dist/core/tool-conformance.js +1 -3
  137. package/dist/core/tool-errors.d.ts +5 -0
  138. package/dist/core/tool-errors.js +1 -0
  139. package/dist/core/tool-policy.d.ts +66 -22
  140. package/dist/core/tool-policy.js +31 -5
  141. package/dist/core/tool-result-budget.d.ts +1 -1
  142. package/dist/core/tool-result-store.d.ts +11 -11
  143. package/dist/core/tool-spec.d.ts +34 -38
  144. package/dist/core/tools.d.ts +2 -2
  145. package/dist/core/trace.d.ts +12 -15
  146. package/dist/core/untrusted-text.d.ts +1 -1
  147. package/dist/core/wiring-manifest.d.ts +4 -3
  148. package/dist/core/workflow-journal-store.d.ts +11 -11
  149. package/dist/engine/execution-env/node-execution-env.d.ts +3 -2
  150. package/dist/engine/execution-env/node-execution-env.js +8 -5
  151. package/dist/engine/harness/agent-harness.d.ts +47 -4
  152. package/dist/engine/harness/agent-harness.js +52 -9
  153. package/dist/engine/loop/agent-loop.js +15 -8
  154. package/dist/engine/loop/types.d.ts +5 -0
  155. package/dist/index.d.ts +20 -14
  156. package/dist/index.js +16 -10
  157. package/dist/internal/harness.d.ts +1 -0
  158. package/dist/orchestration/run-spec.d.ts +3 -0
  159. package/dist/orchestration/run-spec.js +3 -3
  160. package/dist/orchestration/run-workflow-tool.d.ts +12 -6
  161. package/dist/orchestration/run-workflow-tool.js +23 -5
  162. package/dist/orchestration/workflow-primitives.d.ts +4 -2
  163. package/dist/orchestration/workflow-primitives.js +1 -4
  164. package/dist/orchestration/workflow-script-store.js +9 -25
  165. package/dist/orchestration/workflow-types.d.ts +34 -4
  166. package/dist/orchestration/workflow-types.js +2 -0
  167. package/dist/orchestration/workflow.d.ts +39 -7
  168. package/dist/orchestration/workflow.js +237 -113
  169. package/dist/prompt-assembly/assemble.d.ts +2 -2
  170. package/dist/prompt-assembly/assemble.js +9 -9
  171. package/dist/prompt-assembly/composer.d.ts +6 -7
  172. package/dist/prompt-assembly/composer.js +10 -10
  173. package/dist/prompt-assembly/epoch.js +1 -1
  174. package/dist/prompt-assembly/event-registry.d.ts +0 -21
  175. package/dist/prompt-assembly/packs/sema-default.js +30 -30
  176. package/dist/prompt-assembly/types.d.ts +14 -14
  177. package/dist/prompts/coordinator.d.ts +0 -13
  178. package/dist/prompts/coordinator.js +0 -3
  179. package/dist/stores/cc/task-list-store.js +2 -10
  180. package/dist/stores/file/checkpoint-store.js +1 -5
  181. package/dist/stores/file/file-history-store.js +2 -2
  182. package/dist/stores/file/fs-atomic.d.ts +15 -18
  183. package/dist/stores/file/fs-atomic.js +4 -14
  184. package/dist/stores/file/mailbox-store.d.ts +7 -11
  185. package/dist/stores/file/mailbox-store.js +4 -11
  186. package/dist/stores/file/permission-rule-store.d.ts +4 -35
  187. package/dist/stores/file/permission-rule-store.js +4 -148
  188. package/dist/tools/artifact/artifact-text.d.ts +62 -0
  189. package/dist/tools/artifact/artifact-text.js +68 -0
  190. package/dist/tools/artifact/artifact-tool.d.ts +52 -0
  191. package/dist/tools/artifact/artifact-tool.js +351 -0
  192. package/dist/tools/artifact/local-stub.d.ts +26 -0
  193. package/dist/tools/artifact/local-stub.js +297 -0
  194. package/dist/tools/fs/bash-readonly-classifier.d.ts +90 -8
  195. package/dist/tools/fs/bash-readonly-classifier.js +294 -70
  196. package/dist/tools/fs/fs-bash.d.ts +14 -10
  197. package/dist/tools/fs/fs-bash.js +50 -33
  198. package/dist/tools/fs/fs-search-tools.js +5 -2
  199. package/dist/tools/fs/fs-shared.d.ts +2 -0
  200. package/dist/tools/fs/fs-shared.js +3 -11
  201. package/dist/tools/fs/index.d.ts +4 -0
  202. package/dist/tools/fs/index.js +4 -1
  203. package/dist/tools/fs/safety.d.ts +117 -1
  204. package/dist/tools/fs/safety.js +131 -31
  205. package/dist/tools/fs/search.js +8 -6
  206. package/package.json +2 -1
  207. package/test/export-surface.snapshot.json +119 -39
@@ -4,6 +4,7 @@ import { compileReadDeny } from "../tools/fs/read-deny.js";
4
4
  import { carriesShellRedirection, parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
5
5
  import { inlineUntrusted } from "./untrusted-text.js";
6
6
  import { pathTargetOf } from "./tool-registry.js";
7
+ import { carriesDotSegment, isAbsoluteForFamily, isIdentityDir, isPathIdentity, isShellRootedSpellingUnmapped, isUncIdentity, pathFamilyOf, pathIdentityOf } from "../tools/fs/safety.js";
7
8
  import { readShellCommand } from "./shell-lexer.js";
8
9
  export const RULE_BEHAVIORS = ["deny", "ask", "allow"];
9
10
  const RULE_BEHAVIOR_SET = new Set(RULE_BEHAVIORS);
@@ -364,12 +365,18 @@ function baseValueOf(base, bases) {
364
365
  }
365
366
  }
366
367
  function resolvePathPattern(pattern, bases, behavior) {
367
- if (pattern.startsWith("//"))
368
- return { resolved: "/" + pattern.slice(2) };
368
+ const family = pathFamilyOf(bases);
369
+ const tree = bases.root ?? bases.cwd;
370
+ if (pattern.startsWith("//")) {
371
+ const abs = pathIdentityOf("/" + pattern.slice(2), family, tree);
372
+ return abs.ok ? { resolved: abs.identity } : { missingBase: "root" };
373
+ }
369
374
  const needed = ruleBasesNeeded({ match: "path", command: pattern })[0];
370
- const normal = lexicalNormalAbsolutePathOf(baseValueOf(needed, bases) ?? "");
371
- if (normal === undefined)
375
+ const baseValue = baseValueOf(needed, bases);
376
+ const base = baseValue !== undefined && isAbsoluteForFamily(family, baseValue) ? pathIdentityOf(baseValue, family, tree) : undefined;
377
+ if (base === undefined || !base.ok)
372
378
  return { missingBase: needed };
379
+ const normal = base.identity;
373
380
  const rest = pattern.startsWith("~/") ? pattern.slice(2) : pattern.startsWith("/") ? pattern.slice(1) : pattern.startsWith("./") ? pattern.slice(2) : pattern;
374
381
  const peeled = rest.endsWith("/**") ? rest.slice(0, -3) : rest;
375
382
  const bare = needed === "cwd" && peeled !== "" && !peeled.includes("/");
@@ -377,8 +384,8 @@ function resolvePathPattern(pattern, bases, behavior) {
377
384
  const body = anyDepth ? "**/" + peeled : rest;
378
385
  return { resolved: (normal === "/" ? "" : normal) + "/" + body };
379
386
  }
380
- export function isUsablePathBase(base) {
381
- return base !== undefined && lexicalNormalAbsolutePathOf(base) !== undefined;
387
+ export function isUsablePathBase(base, family) {
388
+ return base !== undefined && isAbsoluteForFamily(family, base) && pathIdentityOf(base, family, base).ok;
382
389
  }
383
390
  function segmentGlobMatches(glob, segment) {
384
391
  if (!glob.includes("*"))
@@ -422,15 +429,18 @@ function globSegmentsReach(globs, target) {
422
429
  }
423
430
  export function pathRuleReachOf(rule, target, bases) {
424
431
  if (rule.match === "subpath")
425
- return directoryRuleAdmits(rule, target) ? REACHED : NOT_REACHED;
432
+ return directoryRuleAdmits(rule, target, bases) ? REACHED : NOT_REACHED;
426
433
  if (rule.match !== "path")
427
434
  return NOT_REACHED;
428
- if (!isLexicalNormalAbsoluteDir(target) && target !== "/")
435
+ const family = pathFamilyOf(bases);
436
+ if (!isPathIdentity(target, family))
429
437
  return NOT_REACHED;
430
438
  const r = resolvePathPattern(rule.command, bases, rule.behavior);
431
439
  if ("missingBase" in r) {
432
- return { reach: "unreadable", reason: `the rule is relative to ${PATH_RULE_BASE_LABEL[r.missingBase]} and this call supplies no absolute \`${r.missingBase}\` base to resolve it against` };
440
+ return { reach: "unreadable", reason: `the rule is relative to ${PATH_RULE_BASE_LABEL[r.missingBase]} and this call supplies no absolute \`${r.missingBase}\` base of this tree's family to resolve it against` };
433
441
  }
442
+ if (isUncIdentity(r.resolved) !== isUncIdentity(target))
443
+ return NOT_REACHED;
434
444
  const globs = r.resolved.split("/").filter((sg) => sg !== "");
435
445
  const segments = target.split("/").filter((sg) => sg !== "");
436
446
  return globSegmentsReach(globs, segments) ? REACHED : NOT_REACHED;
@@ -587,21 +597,9 @@ export function pathWithinRoot(path, root) {
587
597
  const base = root.endsWith("/") ? root : root + "/";
588
598
  return path.startsWith(base);
589
599
  }
590
- export function lexicalNormalAbsolutePathOf(path) {
591
- if (typeof path !== "string" || !path.startsWith("/"))
592
- return undefined;
593
- const out = [];
594
- for (const seg of path.split("/")) {
595
- if (seg === "" || seg === ".")
596
- continue;
597
- if (seg === "..") {
598
- if (out.length > 0)
599
- out.pop();
600
- continue;
601
- }
602
- out.push(seg);
603
- }
604
- return out.length === 0 ? "/" : "/" + out.join("/");
600
+ export function lexicalNormalAbsolutePathOf(path, family, anchor) {
601
+ const r = pathIdentityOf(path, family, anchor);
602
+ return r.ok ? r.identity : undefined;
605
603
  }
606
604
  function isLexicalNormalAbsoluteDir(path) {
607
605
  if (typeof path !== "string" || !path.startsWith("/") || path.startsWith("//"))
@@ -609,14 +607,22 @@ function isLexicalNormalAbsoluteDir(path) {
609
607
  const segments = path.split("/").slice(1);
610
608
  return segments.length > 0 && segments.every((s) => s !== "" && s !== "." && s !== "..");
611
609
  }
612
- export function directoryRuleAdmits(rule, path) {
610
+ export function directoryRuleAdmits(rule, path, bases) {
613
611
  if (rule.match !== "subpath")
614
612
  return false;
615
- if (!isLexicalNormalAbsoluteDir(rule.command))
613
+ const family = pathFamilyOf(bases);
614
+ if (carriesDotSegment(rule.command, family))
615
+ return false;
616
+ const dir = pathIdentityOf(rule.command, family, bases.root ?? bases.cwd);
617
+ if (!dir.ok || !isIdentityDir(dir.identity, family))
616
618
  return false;
617
- if (!isLexicalNormalAbsoluteDir(path))
619
+ if (!isPathIdentity(path, family) || !isIdentityDir(path, family))
618
620
  return false;
619
- return pathWithinRoot(path, rule.command);
621
+ return pathWithinRoot(path, dir.identity);
622
+ }
623
+ export function directoryIdentityOf(spelling, bases) {
624
+ const r = pathIdentityOf(spelling, pathFamilyOf(bases), bases.root ?? bases.cwd);
625
+ return r.ok ? r.identity : undefined;
620
626
  }
621
627
  const CD_TARGET_REJECT_CHARS = /[*?[\]{}$`\\'"<>\n\r]/;
622
628
  function cdSegmentDirectoryOf(segment, opts) {
@@ -711,7 +717,8 @@ function resolveCdSegmentDirectories(segments, cwd) {
711
717
  const dir = cdSegmentDirectoryOf(segment, { cwd, priorCd });
712
718
  if (!priorCd && movesShellCursor(segment))
713
719
  priorCd = true;
714
- return dir;
720
+ const bases = basesOfCwd(cwd);
721
+ return dir === undefined || isShellRootedSpellingUnmapped(pathFamilyOf(bases), dir) ? undefined : directoryIdentityOf(dir, bases);
715
722
  });
716
723
  }
717
724
  export function scopeCoversCwd(scope, cwd, sessionId) {
@@ -767,11 +774,14 @@ function firstEligibleDirectoryRule(rules, directory, call) {
767
774
  for (const rule of rules) {
768
775
  if (!eligiblePersisted(rule, { tool: "Read", cwd: call.cwd, sessionId: call.sessionId }, "allow"))
769
776
  continue;
770
- if (directoryRuleAdmits(rule, directory))
777
+ if (directoryRuleAdmits(rule, directory, basesOfCwd(call.execCwd ?? call.cwd)))
771
778
  return rule;
772
779
  }
773
780
  return undefined;
774
781
  }
782
+ function basesOfCwd(cwd) {
783
+ return cwd === undefined ? {} : { root: cwd };
784
+ }
775
785
  export function adjudicatePersistedRules(rules, call) {
776
786
  for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
777
787
  if (behavior === "allow") {
@@ -798,23 +808,29 @@ export function adjudicatePersistedRules(rules, call) {
798
808
  export function adjudicatePersistedPathRules(rules, call, targets, bases) {
799
809
  for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
800
810
  if (behavior === "allow") {
801
- if (targets.allow === undefined || call.tool !== READ_RULE_TOOL)
811
+ if (targets.allow?.kind !== "path" || call.tool !== READ_RULE_TOOL)
802
812
  continue;
803
813
  for (const rule of rules) {
804
814
  if (!eligiblePersisted(rule, call, behavior))
805
815
  continue;
806
- if (directoryRuleAdmits(rule, targets.allow))
816
+ if (directoryRuleAdmits(rule, targets.allow.identity, bases))
807
817
  return { behavior, rules: [rule] };
808
818
  }
809
819
  continue;
810
820
  }
811
- if (targets.tighten === undefined)
821
+ const tighten = targets.tighten;
822
+ if (tighten === undefined || tighten.kind === "refused")
812
823
  continue;
813
824
  let unreadable;
814
825
  for (const rule of rules) {
815
826
  if (!eligiblePersisted(rule, call, behavior))
816
827
  continue;
817
- const outcome = pathRuleReachOf(rule, targets.tighten, bases);
828
+ if (tighten.kind === "unreadable") {
829
+ if (rule.match === "path" || rule.match === "subpath")
830
+ unreadable ??= { behavior: "ask", rules: [rule], unreadable: tighten.reason };
831
+ continue;
832
+ }
833
+ const outcome = pathRuleReachOf(rule, tighten.identity, bases);
818
834
  if (outcome.reach === "reached")
819
835
  return { behavior, rules: [rule] };
820
836
  if (outcome.reach === "unreadable")
@@ -843,7 +859,7 @@ export function segmentCoverageOf(command, rules, call) {
843
859
  proposed.some((p) => p.rule.tool === "Read"
844
860
  ? dir !== undefined &&
845
861
  eligibleContext({ tool: p.rule.tool, scope: p.scope }, { tool: "Read", cwd: call.cwd, sessionId: call.sessionId }) &&
846
- directoryRuleAdmits(p.rule, dir)
862
+ directoryRuleAdmits(p.rule, dir, basesOfCwd(call.execCwd ?? call.cwd))
847
863
  : eligibleContext({ tool: p.rule.tool, scope: p.scope }, call) && ruleAdmitsCommand(p.rule, segment)),
848
864
  };
849
865
  });
@@ -889,7 +905,7 @@ export function suggestRulesForCommand(command, ctx) {
889
905
  const parsed = parseRuleText(formatRuleText(directory, "subpath", READ_RULE_TOOL), "allow");
890
906
  if (!("rule" in parsed) || parsed.rule.match !== "subpath")
891
907
  return undefined;
892
- if (!directoryRuleAdmits(parsed.rule, directory))
908
+ if (!directoryRuleAdmits(parsed.rule, directory, basesOfCwd(ctx?.execCwd ?? ctx?.cwd)))
893
909
  return undefined;
894
910
  return { kind: "directoryRead", rule: parsed.rule.rule, directory: parsed.rule.command, segment };
895
911
  };
@@ -963,7 +979,7 @@ export function suggestRulesForCommand(command, ctx) {
963
979
  if (parsedBatch.some((p) => p.tool !== "Read" && ruleAdmitsCommand(p, segment)))
964
980
  continue;
965
981
  const dir = cdDirs[i];
966
- if (dir !== undefined && parsedBatch.some((p) => directoryRuleAdmits(p, dir)))
982
+ if (dir !== undefined && parsedBatch.some((p) => directoryRuleAdmits(p, dir, basesOfCwd(ctx?.execCwd ?? ctx?.cwd))))
967
983
  continue;
968
984
  const reason = carriesShellRedirection(segment) ? "redirection" : mintSegmentRule(segment) === undefined ? "no_rule_form" : "cap_overflow";
969
985
  uncoveredDetail.push({ segment, reason });
@@ -2,6 +2,7 @@ import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
2
2
  import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
3
3
  import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
4
4
  import { PATH_RULE_BASE_LABEL, isUsablePathBase, parseRuleText, pathRuleReachOf, programRunReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
5
+ import { pathFamilyOf } from "../tools/fs/safety.js";
5
6
  import { effectivePathTargetOf } from "./effective-path-target.js";
6
7
  import { protectivePathTargetOf } from "./tool-registry.js";
7
8
  export { parsePermissionRule };
@@ -188,7 +189,7 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
188
189
  }
189
190
  for (const base of ruleBasesNeeded(parsedPath.rule)) {
190
191
  const value = base === "cwd" ? (pathBases.cwd ?? pathBases.root) : pathBases[base];
191
- if (!isUsablePathBase(value)) {
192
+ if (!isUsablePathBase(value, pathFamilyOf(pathBases))) {
192
193
  bad(text, "unsupported.path_base", `"${text}" is relative to ${PATH_RULE_BASE_LABEL[base]}, and this policy was given no absolute \`pathBases.${base}\`${base === "cwd" ? " (or `root`)" : ""} to resolve it against — the rule could not be judged on any call`);
193
194
  }
194
195
  }
@@ -321,9 +322,13 @@ export function createPermissionRulePolicy(rules, opts) {
321
322
  let unreadable;
322
323
  for (const r of lane) {
323
324
  if (r.parsed.match === "subpath" || r.parsed.match === "path") {
324
- if (target === undefined)
325
+ if (target === undefined || target.kind === "refused")
325
326
  continue;
326
- const outcome = pathRuleReachOf(r.parsed, target, pathBases);
327
+ if (target.kind === "unreadable") {
328
+ unreadable ??= { rule: r, unreadable: target.reason };
329
+ continue;
330
+ }
331
+ const outcome = pathRuleReachOf(r.parsed, target.identity, pathBases);
327
332
  if (outcome.reach === "reached")
328
333
  return { rule: r };
329
334
  if (outcome.reach === "unreadable")
@@ -0,0 +1,37 @@
1
+ /**
2
+ * THE physical path of a path — one spelling for the whole tree.
3
+ *
4
+ * A physical path is what `realpath(3)` answers: every symlink resolved and, on a case-insensitive
5
+ * volume, the directory entry's ON-DISK spelling. That last half is the reason this module exists.
6
+ * Node has two realpaths and they disagree exactly there (measured on macOS): `realpathSync.native`
7
+ * and `fsPromises.realpath` are the libuv call and return the disk case, while the JS `fs.realpathSync`
8
+ * resolves the links but PRESERVES the caller's casing. A tree that used both minted two spellings for
9
+ * one directory, so a key minted on one face could not be found on the other — a memory write root
10
+ * spelled `…/Proj` by configuration was keyed `…/Proj` by the engine's gates and resolved to `…/proj`
11
+ * by the execution env's fence, and the advertised-writable-directory seat had to refuse the hole its
12
+ * own instruction taught.
13
+ *
14
+ * Case FOLDING is not part of a path's identity and is not done here: folding a filesystem PATH names a
15
+ * different directory on a case-sensitive volume. A store whose in-memory KEY must fold (see
16
+ * `canonicalStoreKey`) folds this answer at its own call site.
17
+ */
18
+ export declare function physicalPathOf(p: string): string;
19
+ /** The spelling the memory layout keyed by BEFORE this mint — links resolved, the caller's casing kept (the JS
20
+ * `fs.realpathSync`, which differs from the native call exactly there). Migration probes only: never an identity. */
21
+ export declare function priorCallerCasedPathOf(p: string): string;
22
+ /** The async door onto the same rule (`fsPromises.realpath` is the libuv call `realpathSync.native`
23
+ * makes). The `ExecutionEnv` faces take this one: a sync realpath against a hung network mount blocks
24
+ * the event loop for the mount timeout, and every pending abort with it. */
25
+ export declare function physicalPathOfAsync(p: string): Promise<string>;
26
+ /**
27
+ * {@link physicalPathOf} for a path that need not exist yet: the deepest ancestor that resolves,
28
+ * canonicalized, with the missing tail rejoined — so a not-yet-created target is still judged against
29
+ * roots that are physical paths, and a symlinked PARENT still cannot carry a write past them (the
30
+ * parent is exactly what gets resolved before the tail is rejoined).
31
+ *
32
+ * Total, by construction rather than by swallowing: a path with no resolvable ancestor at all has no
33
+ * physical form to answer with, and the absolute lexical form is that answer. Callers that must
34
+ * distinguish "unresolvable" from "does not exist yet" call {@link physicalPathOf} and handle the throw
35
+ * beside their own contract.
36
+ */
37
+ export declare function physicalPathOfExisting(p: string): string;
@@ -0,0 +1,30 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { realpath } from "node:fs/promises";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ export function physicalPathOf(p) {
5
+ return realpathSync.native(p);
6
+ }
7
+ export function priorCallerCasedPathOf(p) {
8
+ return realpathSync(p);
9
+ }
10
+ export async function physicalPathOfAsync(p) {
11
+ return await realpath(p);
12
+ }
13
+ export function physicalPathOfExisting(p) {
14
+ const abs = resolve(p);
15
+ const missingTail = [];
16
+ let cur = abs;
17
+ for (;;) {
18
+ try {
19
+ const real = physicalPathOf(cur);
20
+ return missingTail.length === 0 ? real : join(real, ...[...missingTail].reverse());
21
+ }
22
+ catch {
23
+ const parent = dirname(cur);
24
+ if (parent === cur)
25
+ return abs;
26
+ missingTail.push(basename(cur));
27
+ cur = parent;
28
+ }
29
+ }
30
+ }
@@ -18,13 +18,13 @@ export declare const MINTED_TOOL_SEGMENT_MIN_CHARS = 16;
18
18
  * live domain key here (the refresh splice). That case is decided by
19
19
  * {@link findNamespacePrefixCollision} and refused at the mount, loudly; the mint stays total.
20
20
  *
21
- * RB-83 (2026-07-25, red probe): the pattern this enforces is `{1,64}`, and only the charset half was
21
+ * The pattern this enforces is `{1,64}`, and only the charset half was
22
22
  * enforced. Both other halves matter for the same reason the charset does — an over-long or empty segment
23
23
  * produces a name the provider rejects, which 400s the WHOLE model request, the exact failure the intake
24
24
  * gate exists to contain ("a single misconfigured server never bricks the whole task").
25
25
  */
26
26
  export declare function normalizeNameSegment(name: string): string;
27
- /** RB-83: force a normalized segment into `{1,max}`, keeping long names distinct via a stable digest. */
27
+ /** Force a normalized segment into `{1,max}`, keeping long names distinct via a stable digest. */
28
28
  export declare function clampNameSegment(seg: string, max?: number): string;
29
29
  /**
30
30
  * The registered-name prefix for `peer` in `ns` — i.e. the set `name.startsWith(prefix)` that a consumer
@@ -508,7 +508,7 @@ export declare function missingRestoreSurface(env: ExecutionEnv): readonly ("res
508
508
  * A composite type guard (structural AND capability) so a `suspendVM` call site narrowed by THIS predicate
509
509
  * gets the {@link RemoteExecutionEnv} type without a cast — `true` always implies the structural check too.
510
510
  *
511
- * RB-439-b: it also requires the RESTORE surface ({@link missingRestoreSurface}). A suspend is only durable
511
+ * It also requires the RESTORE surface ({@link missingRestoreSurface}). A suspend is only durable
512
512
  * if something can undo it; an adapter that offers `suspendVM` but no `resumeVM` used to pass this guard,
513
513
  * take a real snapshot, commit a real checkpoint — and then blow up on the resume leg with an untyped
514
514
  * TypeError. "Suspendable" now means the whole round trip, so a half-adapter is rejected at the suspend site
@@ -0,0 +1,19 @@
1
+ /**
2
+ * ONE presence rule for a RETIRED key on a caller-supplied bag (a deps object, an options object): the key is
3
+ * PRESENT iff ordinary property lookup answers a defined value — prototype and non-enumerable members included,
4
+ * exactly what the former direct read of the seat honored; an explicit `undefined` reads as absence, as every
5
+ * optional seat reads. Every retired-key door reads through here so no door grows its own idea of "present";
6
+ * each composes its own refusal from the answer. A table row is `{ key: replacement }` — the sentence that says
7
+ * what replaced the seat, quoted in the refusal.
8
+ */
9
+ /** The first retired key present on `bag`, in table order, or undefined when none is. */
10
+ export declare function retiredKeyOf(bag: object, table: Readonly<Record<string, string>>): {
11
+ key: string;
12
+ replacement: string;
13
+ } | undefined;
14
+ /**
15
+ * The deps-bag door: a retired seat still wired by a deployment is refused BY NAME (`config.deps_retired_key`)
16
+ * instead of vanishing silently — a seat that is silently dropped reads to the deployment as wired. `owner` is
17
+ * the bag's type name for the sentence (`RunnerDeps`, `RunWorkflowToolDeps`).
18
+ */
19
+ export declare function assertNoRetiredKeys(bag: object, table: Readonly<Record<string, string>>, owner: string): void;
@@ -0,0 +1,15 @@
1
+ export function retiredKeyOf(bag, table) {
2
+ for (const [key, replacement] of Object.entries(table)) {
3
+ if (Reflect.get(bag, key) !== undefined)
4
+ return { key, replacement };
5
+ }
6
+ return undefined;
7
+ }
8
+ export function assertNoRetiredKeys(bag, table, owner) {
9
+ const hit = retiredKeyOf(bag, table);
10
+ if (hit === undefined)
11
+ return;
12
+ const e = new Error(`${owner}.${hit.key} is retired and is not read by this engine — ${hit.replacement}. Refused rather than ignored: a seat that is silently dropped reads to the deployment as wired.`);
13
+ e.code = "config.deps_retired_key";
14
+ throw e;
15
+ }
@@ -1,4 +1,4 @@
1
- import { canonicalizeTarget } from "../../tools/fs/safety.js";
1
+ import { canonicalizeTarget, pathFamilyOf } from "../../tools/fs/safety.js";
2
2
  import { pathTargetValue } from "../tool-registry.js";
3
3
  import { pathTargetBaseOf } from "../effective-path-target.js";
4
4
  import { PATH_WRITE_TOOLS, isWithin } from "./session-rule-policy.js";
@@ -100,7 +100,7 @@ export function createActiveSkillScopePolicy(opts) {
100
100
  decisionReason: "safety",
101
101
  };
102
102
  }
103
- const canon = await canonicalizeTarget(env, path, signal, pathTargetBaseOf(declaredTarget, { root: rootPath, cwd: req.cwd }));
103
+ const canon = await canonicalizeTarget(env, path, signal, pathFamilyOf({ root: rootPath, cwd: req.cwd ?? env.cwd }), pathTargetBaseOf(declaredTarget, { root: rootPath, cwd: req.cwd }));
104
104
  if (!canon.ok) {
105
105
  return {
106
106
  action: "deny",
@@ -111,7 +111,7 @@ export function createActiveSkillScopePolicy(opts) {
111
111
  for (const m of constraining) {
112
112
  const roots = [];
113
113
  for (const raw of m.allowPaths ?? []) {
114
- const r = await canonicalizeTarget(env, raw, signal, rootPath);
114
+ const r = await canonicalizeTarget(env, raw, signal, pathFamilyOf({ root: rootPath ?? env.cwd }), rootPath);
115
115
  if (r.ok)
116
116
  roots.push(r.key);
117
117
  }
@@ -1,5 +1,6 @@
1
1
  import type { AssistantMessage } from "../../internal/llm.js";
2
2
  import type { NestedUsage, PausedCause, TaskResult, TaskSpec, TerminalCause } from "../types.js";
3
+ import type { HaltConsumption } from "../../internal/harness.js";
3
4
  /** Per-task usage/cost accumulator (filled across the run loop, surfaced as `TaskResult.stats`). */
4
5
  export interface Stats {
5
6
  turns: number;
@@ -134,15 +135,14 @@ export interface ResultFlags {
134
135
  * path the terminal is `completed`, and this is what tells that completion apart from a natural
135
136
  * one — the model did not finish; the person stopped it and the run awaits their direction). */
136
137
  haltedOnUserRejection?: boolean;
137
- /** A `TaskStream.halt` (the bare user interrupt, CC Esc form) was accepted
138
- * while the run was live; echoed on `TaskResult.haltedByUser`. Pure pass-through on every
139
- * terminal (the sibling `haltedOnUserRejection` law: the fact is about the leg that ran) PLUS
140
- * one status decision it owns (slot 9.7 below): a run whose ONLY stop was this halt reads
141
- * `"completed"`, never the `!final` failure and never the aborted terminal whether the halt
142
- * landed before the model said anything (empty result) or cut a turn whose partial text the
143
- * brain kept (that text IS the result). The person stopped the run, which is the verb working,
144
- * not a defect. A REAL abort/limit that also fired still owns the terminal above it. */
145
- userHalted?: boolean;
138
+ /** the harness's STATEMENT of what the run's halt did (see `HaltConsumption`):
139
+ * the loop's report that the halt's cut settled the final turn, and/or the boundary consult that
140
+ * stopped the loop for the halt's request. This is the ONLY input to `TaskResult.haltedByUser`
141
+ * (`haltConsumed`, halt-attribution.ts) and to slot 9.7 the assembly never infers causation from the
142
+ * final's shape or from the verb's acceptance. A halt that was accepted but consumed nothing (the run
143
+ * ended for its own reason first) leaves this empty: the original terminal stands and the seat is
144
+ * NOT signed (the settle lane mints `task.halt_unconsumed` for it). */
145
+ haltConsumption?: HaltConsumption;
146
146
  /** Call ids of answered-but-never-collected questions, echoed on
147
147
  * `TaskResult.strandedHumanAnswers`. Pure pass-through; empty/absent ⇒ the field is omitted. The
148
148
  * optional `onError` alert is NOT the disclosure — this mandatory result face is. */
@@ -298,13 +298,15 @@ export declare function terminalCauseOf(final: AssistantMessage | undefined, sta
298
298
  * 9. `abortedLive` | stopReason aborted → `"limits.max_walltime_exceeded"` | `"limits.max_turns_exceeded"` | undefined
299
299
  * (audit A-1: hoisted ABOVE no-final — the loop exits cleanly on abort, so `final` may be a
300
300
  * normal message or absent; a plain user interrupt carries NO errorCode — switch on `status`),
301
- * EXCEPT when the aborted `final` is the bare halt's OWN cut artifact (`haltOwnsAbortedFinal`),
301
+ * EXCEPT when the aborted `final` is the bare halt's OWN cut artifact (`haltOwnsAbortedFinal`: the
302
+ * loop reported the halt's cut settled the final turn — never shape inference),
302
303
  * and EXCEPT when the answer had already settled (`answerSettled`: a clean `"stop"` final, not
303
304
  * partial) — a stop landing on the run's tail cuts no work, so the run falls through to 12
304
305
  * (`completed`, the answer as `result`, no `limits.*` code for the axis; three axes, one law)
305
- * 9.7 `userHalted` + (no `final` | the halt's own aborted `final`) → status `"completed"` (a
306
- * bare user halt — the verb working, not a defect; below the aborts on purpose, a real
307
- * abort/limit that also fired owns the terminal and the halt rides as the pass-through seat)
306
+ * 9.7 halt CONSUMED (`haltConsumed(flags.haltConsumption)`) + (no `final` | the halt's own aborted
307
+ * `final`) → status `"completed"` (a bare user halt — the verb working, not a defect; below the
308
+ * aborts on purpose, a real abort/limit that also fired owns the terminal and the halt rides as
309
+ * the pass-through seat; an accepted-but-unconsumed halt never reaches this slot)
308
310
  * 10. no `final` → status `"failed"`
309
311
  * 11. stopReason error → brain `[code]` prefix (or `"conflict"` from the storage-layer flag)
310
312
  * 12. else → `"completed"`
@@ -1,6 +1,7 @@
1
1
  import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
2
2
  import { extractErrorCode, stripErrorCodePrefix } from "../../brain/errors.js";
3
3
  import { terminalProjection } from "./terminal-projection.js";
4
+ import { haltConsumed } from "./halt-attribution.js";
4
5
  const SALVAGE_ELIGIBLE_TERMINALS = new Set([
5
6
  "output.degenerate",
6
7
  "limits.max_tokens_exceeded",
@@ -47,7 +48,7 @@ export function answerSettled(final, continuationCut = false) {
47
48
  export function terminalCauseOf(final, stats, flags) {
48
49
  let terminal;
49
50
  let apiFailure;
50
- const haltOwnsAbortedFinal = flags.userHalted === true &&
51
+ const haltOwnsAbortedFinal = flags.haltConsumption?.turnCut === "halt" &&
51
52
  final?.stopReason === "aborted" &&
52
53
  !flags.abortedLive &&
53
54
  flags.abortedForTimeout !== true &&
@@ -103,7 +104,7 @@ export function terminalCauseOf(final, stats, flags) {
103
104
  const code = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined;
104
105
  terminal = { kind: "failed", ...(code !== undefined ? { code } : {}), message: final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted") };
105
106
  }
106
- else if (flags.userHalted === true && (!final || haltOwnsAbortedFinal)) {
107
+ else if (haltConsumed(flags.haltConsumption) && (!final || haltOwnsAbortedFinal)) {
107
108
  terminal = { kind: "completed" };
108
109
  }
109
110
  else if (!final) {
@@ -155,7 +156,7 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
155
156
  const result = text;
156
157
  let salvagedOutput;
157
158
  const { terminal, apiFailure } = terminalCauseOf(final, stats, flags);
158
- const { status, errorCode } = terminalProjection(terminal);
159
+ const { errorCode } = terminalProjection(terminal);
159
160
  if (errorCode !== undefined && SALVAGE_ELIGIBLE_TERMINALS.has(errorCode)) {
160
161
  salvagedOutput = text.trim() || undefined;
161
162
  }
@@ -164,6 +165,6 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
164
165
  void _internalCompaction;
165
166
  if (flags.unpricedSpend)
166
167
  delete publicStats.costMicroUsd;
167
- const stampHaltedByUser = flags.userHalted === true && status !== "suspended" && status !== "needs_review";
168
+ const stampHaltedByUser = haltConsumed(flags.haltConsumption);
168
169
  return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, terminal, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
169
170
  }
@@ -30,96 +30,32 @@ import type { Prepared } from "./contracts.js";
30
30
  */
31
31
  export declare function buildWorkingFileAttachments(spec: TaskSpec, prepared: Prepared): MaybeCompactOptions["workingFileAttachments"];
32
32
  /**
33
- * `MaybeCompactOptions.contextInstructionFiles`: the seat that delivers the deployment's own
34
- * instruction-file content (the CLAUDE.md family) to the SUMMARIZER, assembled once here and spread
35
- * on all three lanes. Without it the compaction boundary is the one place a long task loses the
36
- * standing instructions every other turn has been reading — and it loses them exactly where the
37
- * summary REPLACES the transcript that carried them, so the loss is durable.
33
+ * `MaybeCompactOptions.contextInstructionFiles`: the deployment's instruction-file content (the CLAUDE.md
34
+ * family) delivered to the SUMMARIZER, assembled once here and spread on all three lanes — the compaction
35
+ * boundary is where the summary REPLACES the transcript that carried those instructions.
38
36
  *
39
- * WHICH BYTES, and why the neighbours are not candidates. `Prepared.projectInstructionContent` =
40
- * the RAW loader text (`ProjectMemoryLoad.content`, exactly the string prepare-task feeds
41
- * `composeMemoryBlock(projectMem, "project")`), captured beside the existing `instructionSources`
42
- * capture under the SAME non-blank predicate that composes the project layer:
43
- * · the assembled `memoryBlock` is NOT a candidate. It is the memory ENGINE's composed layers —
44
- * bytes the model itself authored in-band this session through the memory tools — with the
45
- * project layer appended at the tail. The seat's contract is host/systemPrompt tier and says in
46
- * terms: never from tool output, model text, or any other in-band channel. Handing
47
- * model-authored bytes an envelope whose preamble tells the summarizer they may instruct it is
48
- * the laundering shape, not a purity nit. (Mechanically moot too — `memoryBlock` is a
49
- * prepare-task local and never reaches `Prepared`.)
50
- * · the COMPOSED project block is not it either, and this one is checkable rather than a
51
- * judgement: `contextInstructionFilesSection` neutralizes the whole authority family
52
- * (ENGINE_AUTHORITY_ENVELOPE_TAGS) over its payload, and `user_memory`/`scope` ARE in that
53
- * family so feeding a composed block would have the engine rewrite the tags of its own
54
- * composition. Raw text in, one fence at the mint.
55
- * · `prepared.instructionSources` carries path + contentHash only, by design (core never learns a
56
- * file-name policy), so it cannot serve this seat at all.
57
- *
58
- * PROVENANCE, stated plainly because "the host supplies it" is the easy half of the truth. On this
59
- * engine the value is REPOSITORY-CONTROLLED text: the loading block one file over calls a CLAUDE.md
60
- * of unknown authorship untrusted in so many words, the MAIN lane's own framing tells the model it is
61
- * "repository-controlled DATA, not instructions to obey", and this seat hands the same bytes to the
62
- * summarizer with instruction standing for the summarization task. Delivery THROUGH a host callback
63
- * is not host AUTHORSHIP: a prior agent, or anyone who can land a commit, can write a
64
- * "## Compact Instructions" section, and the mint's neutralization stops tag breakout — not a
65
- * sentence asking for a fact to be left out of the summary that replaces the transcript.
66
- * Why it is nevertheless this value: the seat's own contract names this path ("hosts pass the same
67
- * instruction-file content their prompt assembly mounts... on the runner path"), the parity form this
68
- * channel exists to reach reads the user's and the project's instruction files at every boundary with
69
- * exactly this standing, and the two alternatives are worse for reasons above rather than better.
70
- * The standing is also narrower than the framing gap suggests — the section's preamble scopes it to
71
- * guidance about HOW to summarize and classifies the rest as background — and a deployment that does
72
- * not want it has two exits that need no code: do not wire the loader, or return content it vouches
73
- * for. Recorded as an open design candidate rather than built here, because it is a contract change
74
- * and not a wiring one: a SEPARATE deployment-vouched summarization-instructions seat, leaving
75
- * repository-derived context at data tier.
76
- *
77
- * RULING ① — BOTH summary forms, no per-form split. Fork eligibility is decided per pass INSIDE
78
- * maybeCompact (main-model-only, recorded-request-present, not lossy), so a call site cannot say
79
- * "independent form only"; the closest approximation — pass the seat only when `prepared.compModel`
80
- * is set, since an independent compaction model has no main prefix to fork — leaves the hole the
81
- * wrong way round: the configuration it does NOT cover (no compaction model) is precisely the one
82
- * where a fork degrades mid-pass to the independent form, which would then run WITHOUT the files.
83
- * Feeding both forms closes that case by construction, and the library face already handles the fork
84
- * half (the section rides the APPENDED instruction message, never the cached prefix, so the fork's
85
- * whole economic point is untouched).
86
- *
87
- * RULING ② — no new size knob. The bound is the library's own `fitContextInstructionFilesSection`
88
- * (independent form: cap against the compaction model's window, disclosed elision, and a complete
89
- * yield when not even the framing fits). Its three KNOWN LIMITS are accepted here KNOWINGLY, as the
90
- * pre-condition of this wiring rather than something it discovered: a token-dense payload (CJK,
91
- * emoji) can pass a chars-domain fit and still overflow the gateway's tokenizer; an unknown
92
- * `contextWindow` leaves the section unbounded (the conversation clamp declines to guess there too);
93
- * and the FORK form cannot fit at all — its bound is a prompt-too-long that degrades ONCE to the
94
- * independent form, which the engine's own note records as real but NOT total (an oversize rejection
95
- * the classifier cannot read as prompt-too-long returns terminally instead).
96
- *
97
- * HOW BIG THAT ACCEPTANCE ACTUALLY IS — measured, because the sentence above is easy to read more
98
- * comfortably than it deserves. The fitted form is NOT the default path: when a fork is eligible and
99
- * the model answers in its envelope, the fork IS the pass, and its section is delivered WHOLE at any
100
- * size. The independent form — the only one that fits — runs when there is no fork to take or the
101
- * fork's answer did not conform. Both facts are pinned in the REF-A2 suite (a 55 KB payload: whole on
102
- * the fork, elided on the independent fallback). And when a limit does bite, the failure is not a
103
- * quiet degradation: the section is a FIXED part of the request, the reactive retry loop may drop
104
- * only CONVERSATION groups, so a request the SECTION overflowed cannot converge and the boundary
105
- * fails — the task loses that compaction (or that prompt-too-long recovery, or its end-of-task
106
- * summary) rather than sending a shorter one. The exposure is what the wiring widened: this was
107
- * previously reachable only by hosts calling the library directly, and is now reachable by every
108
- * deployment that wires `loadProjectMemory` with a large instruction file and an unknown or small
109
- * window. Making the section participate in the retry loop is the structural answer; it is a change
110
- * to that loop's contract, tracked at the library face, and a runner-side knob would only move the
111
- * decision to a place with less information.
112
- *
113
- * WHAT IT COSTS, measured rather than rounded: every deployment that wires `loadProjectMemory` now
114
- * sends its instruction-file content plus EXACTLY 716 chars of framing (the preamble and the fence)
115
- * on EVERY summarization request, both forms, once per pass — where a real project instruction file
116
- * in this family runs to tens of KB, so the payload, not the framing, is the bill. The number is
117
- * pinned in the REF-A2 suite against the engine's own renderer, so a preamble edit shows up as a
118
- * changed assertion rather than as prose drifting away from the code it describes.
119
- * Deployments that never wired the loader pay nothing: the key is absent, not
120
- * `undefined`, so their requests are byte-identical to the pre-wiring shape on the independent and
121
- * UPDATE passes (the FORK form appends its ~860-char authority clause with or without the seat, and
122
- * always did).
37
+ * Contract:
38
+ * - Bytes = `Prepared.projectInstructionContent`, the RAW loader text captured under the same non-blank
39
+ * predicate that composes the project layer. Not the assembled `memoryBlock` (model-authored in-band
40
+ * bytes would gain an instructing envelope), not the COMPOSED project block (`contextInstructionFilesSection`
41
+ * neutralizes the whole authority family, `user_memory`/`scope` included), not `instructionSources`
42
+ * (path + hash only). Raw text in, one fence at the mint.
43
+ * - Provenance: REPOSITORY-CONTROLLED text delivered through a host callback delivery is not authorship;
44
+ * the mint's neutralization stops tag breakout, not a sentence asking for an omission. Two no-code exits
45
+ * for a deployment that does not want it: do not wire the loader, or return vouched content. A separate
46
+ * deployment-vouched summarization-instructions seat is an open design candidate, not built here.
47
+ * - Both summary forms, no per-form split: fork eligibility is decided per pass inside maybeCompact, and the
48
+ * form NOT covered by a `compModel`-gated split is exactly the fork that degrades mid-pass to the
49
+ * independent form; the section rides the APPENDED instruction message, never the cached prefix.
50
+ * - No new size knob: the bound is `fitContextInstructionFilesSection` (independent form only). Its three
51
+ * known limits are accepted knowingly: a token-dense payload can pass a chars-domain fit and still overflow;
52
+ * an unknown `contextWindow` leaves the section unbounded; the FORK form cannot fit and degrades ONCE. When
53
+ * a limit bites the boundary FAILS (the section is a fixed request part the retry loop cannot drop) — the
54
+ * task loses that compaction rather than sending a shorter one; making the section participate in the
55
+ * retry loop is a library-face contract change, tracked there.
56
+ * - Cost: instruction-file content + exactly 716 chars of framing on every summarization request, both
57
+ * forms, once per pass (pinned in the REF-A2 suite). Deployments that never wired the loader pay nothing:
58
+ * the key is ABSENT, so their requests are byte-identical to the pre-wiring shape.
123
59
  */
124
60
  export declare function contextInstructionFilesOption(prepared: Prepared): Partial<Pick<MaybeCompactOptions, "contextInstructionFiles">>;
125
61
  /**