@sema-agent/core 7.15.0 → 7.17.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 (287) hide show
  1. package/CHANGELOG.md +202 -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 +4 -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/delegation-settlement.d.ts +37 -12
  49. package/dist/core/memory-engine/delegation-settlement.js +128 -39
  50. package/dist/core/memory-engine/engine.d.ts +9 -8
  51. package/dist/core/memory-engine/engine.js +115 -120
  52. package/dist/core/memory-engine/file-backend.d.ts +17 -5
  53. package/dist/core/memory-engine/file-backend.js +78 -34
  54. package/dist/core/memory-engine/index.d.ts +2 -2
  55. package/dist/core/memory-engine/index.js +2 -2
  56. package/dist/core/memory-engine/layout.d.ts +152 -54
  57. package/dist/core/memory-engine/layout.js +441 -104
  58. package/dist/core/memory-engine/types.d.ts +8 -3
  59. package/dist/core/memory-engine/types.js +5 -0
  60. package/dist/core/memory-recall.d.ts +0 -8
  61. package/dist/core/memory.d.ts +1 -1
  62. package/dist/core/oracle-isolation.js +2 -2
  63. package/dist/core/parent-spec-seat.d.ts +66 -0
  64. package/dist/core/parent-spec-seat.js +47 -0
  65. package/dist/core/permission-rule-model.d.ts +11 -6
  66. package/dist/core/permission-rule-model.js +53 -37
  67. package/dist/core/permission-rules.js +8 -3
  68. package/dist/core/protocol-naming.d.ts +2 -2
  69. package/dist/core/remote-env.d.ts +1 -1
  70. package/dist/core/retired-keys.d.ts +19 -0
  71. package/dist/core/retired-keys.js +15 -0
  72. package/dist/core/runner/abort-race.d.ts +3 -3
  73. package/dist/core/runner/active-skill-scope.d.ts +2 -2
  74. package/dist/core/runner/active-skill-scope.js +3 -3
  75. package/dist/core/runner/assemble-result.d.ts +42 -40
  76. package/dist/core/runner/assemble-result.js +5 -4
  77. package/dist/core/runner/checkpoint-scope.d.ts +4 -4
  78. package/dist/core/runner/clock-and-limits.d.ts +3 -3
  79. package/dist/core/runner/compaction-call-options.d.ts +35 -99
  80. package/dist/core/runner/compaction-knobs.d.ts +4 -4
  81. package/dist/core/runner/compaction-seams.d.ts +5 -5
  82. package/dist/core/runner/contracts.d.ts +232 -233
  83. package/dist/core/runner/decide-continuation.d.ts +6 -6
  84. package/dist/core/runner/denial-limit-arms.d.ts +8 -8
  85. package/dist/core/runner/derived-route-fallback.d.ts +2 -2
  86. package/dist/core/runner/gate-exit.d.ts +6 -6
  87. package/dist/core/runner/gate-exit.js +15 -1
  88. package/dist/core/runner/git-status-frame.d.ts +7 -7
  89. package/dist/core/runner/halt-attribution.d.ts +35 -0
  90. package/dist/core/runner/halt-attribution.js +16 -0
  91. package/dist/core/runner/inherited-ask-grants.d.ts +6 -6
  92. package/dist/core/runner/memory-capture-optout.d.ts +1 -1
  93. package/dist/core/runner/memory-consolidation-driver.d.ts +4 -4
  94. package/dist/core/runner/memory-consolidation.d.ts +6 -8
  95. package/dist/core/runner/memory-consolidation.js +0 -1
  96. package/dist/core/runner/park-commit.d.ts +6 -6
  97. package/dist/core/runner/permission-rule-lanes.d.ts +19 -19
  98. package/dist/core/runner/prepare-acquire-reconcile.d.ts +4 -4
  99. package/dist/core/runner/prepare-artifact.d.ts +53 -0
  100. package/dist/core/runner/prepare-artifact.js +77 -0
  101. package/dist/core/runner/prepare-ask-lane.d.ts +3 -3
  102. package/dist/core/runner/prepare-boundary-parks.d.ts +3 -3
  103. package/dist/core/runner/prepare-caps-and-workflow.d.ts +9 -7
  104. package/dist/core/runner/prepare-caps-and-workflow.js +2 -3
  105. package/dist/core/runner/prepare-config-doors.d.ts +24 -24
  106. package/dist/core/runner/prepare-config-doors.js +5 -2
  107. package/dist/core/runner/prepare-context-lane.d.ts +8 -8
  108. package/dist/core/runner/prepare-context-lane.js +2 -13
  109. package/dist/core/runner/prepare-defer-classify.d.ts +2 -2
  110. package/dist/core/runner/prepare-delegation-surface.d.ts +1 -1
  111. package/dist/core/runner/prepare-file-history.d.ts +3 -3
  112. package/dist/core/runner/prepare-file-history.js +3 -52
  113. package/dist/core/runner/prepare-gate-stations.d.ts +5 -5
  114. package/dist/core/runner/prepare-hands-readface.d.ts +35 -38
  115. package/dist/core/runner/prepare-hands-readface.js +3 -2
  116. package/dist/core/runner/prepare-inherited-gate.d.ts +9 -6
  117. package/dist/core/runner/prepare-inherited-gate.js +7 -8
  118. package/dist/core/runner/prepare-listings.d.ts +3 -3
  119. package/dist/core/runner/prepare-lsp.d.ts +3 -3
  120. package/dist/core/runner/prepare-memory-engine-session.d.ts +5 -5
  121. package/dist/core/runner/prepare-memory.d.ts +4 -6
  122. package/dist/core/runner/prepare-memory.js +10 -9
  123. package/dist/core/runner/prepare-offload-wrappers.d.ts +2 -2
  124. package/dist/core/runner/prepare-park-ask.d.ts +6 -6
  125. package/dist/core/runner/prepare-policy-chain.d.ts +7 -7
  126. package/dist/core/runner/prepare-policy-chain.js +15 -30
  127. package/dist/core/runner/prepare-prompt-assembly.js +1 -1
  128. package/dist/core/runner/prepare-prompt-inputs.d.ts +5 -5
  129. package/dist/core/runner/prepare-protocol-tools.d.ts +1 -1
  130. package/dist/core/runner/prepare-question-face.d.ts +4 -4
  131. package/dist/core/runner/prepare-run-refs.d.ts +11 -11
  132. package/dist/core/runner/prepare-safety-scan.d.ts +2 -2
  133. package/dist/core/runner/prepare-suspend-saga.d.ts +4 -4
  134. package/dist/core/runner/prepare-suspend-saga.js +0 -2
  135. package/dist/core/runner/prepare-task.d.ts +12 -12
  136. package/dist/core/runner/prepare-task.js +10 -9
  137. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +2 -3
  138. package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -4
  139. package/dist/core/runner/prepare-wiring-manifest.js +9 -7
  140. package/dist/core/runner/prepare-workspace-restore.d.ts +1 -1
  141. package/dist/core/runner/remote-env-retry.d.ts +3 -3
  142. package/dist/core/runner/resume-admission.d.ts +1 -1
  143. package/dist/core/runner/resume-admission.js +2 -1
  144. package/dist/core/runner/resume-claim.d.ts +1 -1
  145. package/dist/core/runner/resume-internals-and-config.d.ts +1 -1
  146. package/dist/core/runner/resume-internals-and-config.js +4 -0
  147. package/dist/core/runner/resume-policy-outcome.d.ts +1 -1
  148. package/dist/core/runner/resume-preflight.d.ts +1 -1
  149. package/dist/core/runner/resume-review-outcome.d.ts +1 -1
  150. package/dist/core/runner/retired-runner-deps.d.ts +3 -0
  151. package/dist/core/runner/retired-runner-deps.js +7 -0
  152. package/dist/core/runner/run-brain-sinks.d.ts +1 -1
  153. package/dist/core/runner/run-clock-and-content.d.ts +4 -4
  154. package/dist/core/runner/run-compaction-machinery.d.ts +3 -3
  155. package/dist/core/runner/run-git-lane.d.ts +1 -1
  156. package/dist/core/runner/run-harness-handlers.d.ts +2 -2
  157. package/dist/core/runner/run-harness-handlers.js +2 -3
  158. package/dist/core/runner/run-identity-wiring.d.ts +4 -4
  159. package/dist/core/runner/run-leg.d.ts +3 -3
  160. package/dist/core/runner/run-notification-lane.d.ts +2 -2
  161. package/dist/core/runner/run-reasoning-seat.d.ts +2 -2
  162. package/dist/core/runner/run-settle-and-teardown.d.ts +5 -5
  163. package/dist/core/runner/run-settle-and-teardown.js +3 -1
  164. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
  165. package/dist/core/runner/run-terminal-adoption.d.ts +5 -5
  166. package/dist/core/runner/runtask.d.ts +14 -14
  167. package/dist/core/runner/session-file-state-replay.d.ts +3 -3
  168. package/dist/core/runner/session-file-state-replay.js +3 -3
  169. package/dist/core/runner/session-rule-policy.d.ts +2 -2
  170. package/dist/core/runner/session-rule-policy.js +3 -4
  171. package/dist/core/runner/steer-admission.d.ts +2 -2
  172. package/dist/core/runner/stream-halt-verbs.d.ts +1 -1
  173. package/dist/core/runner/stream-halt-verbs.js +1 -1
  174. package/dist/core/runner/stream-reap.d.ts +1 -1
  175. package/dist/core/runner/stream-steer-verb.js +1 -4
  176. package/dist/core/runner/synthetic-tools.d.ts +8 -8
  177. package/dist/core/runner/teardown-bounded.d.ts +2 -2
  178. package/dist/core/runner/tool-defer-gate.d.ts +1 -1
  179. package/dist/core/runner/tool-disclosure.d.ts +33 -33
  180. package/dist/core/runner/tool-end-body.d.ts +3 -3
  181. package/dist/core/runner/tool-face-overlay.d.ts +2 -2
  182. package/dist/core/runner/tool-output-projection.d.ts +2 -2
  183. package/dist/core/runner/turn-attachments.d.ts +53 -53
  184. package/dist/core/runner/usage-accounting.d.ts +4 -4
  185. package/dist/core/runner/workspace-path.d.ts +1 -1
  186. package/dist/core/runner/workspace-path.js +3 -0
  187. package/dist/core/runner-deps.d.ts +30 -49
  188. package/dist/core/safe-notify.d.ts +1 -1
  189. package/dist/core/sensitive-path-policy.d.ts +1 -1
  190. package/dist/core/sensitive-path-policy.js +4 -4
  191. package/dist/core/session.d.ts +2 -2
  192. package/dist/core/skill-tool-specifier.d.ts +1 -1
  193. package/dist/core/skills-directory.d.ts +1 -1
  194. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  195. package/dist/core/strategy-store.d.ts +8 -11
  196. package/dist/core/task-event.d.ts +20 -17
  197. package/dist/core/task-limits.d.ts +1 -1
  198. package/dist/core/task-notification.d.ts +5 -5
  199. package/dist/core/task-registry-agent.d.ts +5 -5
  200. package/dist/core/task-registry-monitor.d.ts +2 -2
  201. package/dist/core/task-registry-shared.d.ts +27 -27
  202. package/dist/core/task-registry-shared.js +3 -13
  203. package/dist/core/task-registry.d.ts +8 -8
  204. package/dist/core/task-result.d.ts +37 -37
  205. package/dist/core/task-spec.d.ts +5 -5
  206. package/dist/core/task-stream.d.ts +16 -11
  207. package/dist/core/tool-catalog-entries.d.ts +1 -0
  208. package/dist/core/tool-catalog-entries.js +5 -0
  209. package/dist/core/tool-conformance.d.ts +0 -2
  210. package/dist/core/tool-conformance.js +1 -3
  211. package/dist/core/tool-errors.d.ts +5 -0
  212. package/dist/core/tool-errors.js +1 -0
  213. package/dist/core/tool-policy.d.ts +51 -22
  214. package/dist/core/tool-policy.js +28 -5
  215. package/dist/core/tool-result-budget.d.ts +1 -1
  216. package/dist/core/tool-result-store.d.ts +11 -11
  217. package/dist/core/tool-spec.d.ts +34 -38
  218. package/dist/core/tools.d.ts +2 -2
  219. package/dist/core/trace.d.ts +12 -15
  220. package/dist/core/untrusted-text.d.ts +1 -1
  221. package/dist/core/wiring-manifest.d.ts +4 -3
  222. package/dist/core/workflow-journal-store.d.ts +11 -11
  223. package/dist/engine/execution-env/node-execution-env.d.ts +3 -2
  224. package/dist/engine/execution-env/node-execution-env.js +4 -2
  225. package/dist/engine/harness/agent-harness.d.ts +47 -4
  226. package/dist/engine/harness/agent-harness.js +52 -9
  227. package/dist/engine/llm/types.d.ts +1 -1
  228. package/dist/engine/loop/agent-loop.js +15 -8
  229. package/dist/engine/loop/types.d.ts +5 -0
  230. package/dist/index.d.ts +21 -16
  231. package/dist/index.js +17 -12
  232. package/dist/internal/harness.d.ts +1 -0
  233. package/dist/orchestration/run-spec.d.ts +3 -0
  234. package/dist/orchestration/run-spec.js +3 -3
  235. package/dist/orchestration/run-workflow-tool.d.ts +12 -6
  236. package/dist/orchestration/run-workflow-tool.js +23 -5
  237. package/dist/orchestration/workflow-primitives.d.ts +4 -2
  238. package/dist/orchestration/workflow-primitives.js +1 -4
  239. package/dist/orchestration/workflow-types.d.ts +34 -4
  240. package/dist/orchestration/workflow-types.js +2 -0
  241. package/dist/orchestration/workflow.d.ts +39 -7
  242. package/dist/orchestration/workflow.js +234 -113
  243. package/dist/prompt-assembly/assemble.d.ts +2 -2
  244. package/dist/prompt-assembly/assemble.js +9 -9
  245. package/dist/prompt-assembly/composer.d.ts +6 -7
  246. package/dist/prompt-assembly/composer.js +10 -10
  247. package/dist/prompt-assembly/epoch.js +1 -1
  248. package/dist/prompt-assembly/event-registry.d.ts +0 -21
  249. package/dist/prompt-assembly/packs/sema-default.js +30 -30
  250. package/dist/prompt-assembly/types.d.ts +14 -14
  251. package/dist/prompts/coordinator.d.ts +0 -13
  252. package/dist/prompts/coordinator.js +0 -3
  253. package/dist/stores/file/checkpoint-store.js +1 -5
  254. package/dist/stores/file/file-history-store.js +2 -2
  255. package/dist/stores/file/permission-rule-store.d.ts +4 -35
  256. package/dist/stores/file/permission-rule-store.js +4 -148
  257. package/dist/tools/artifact/artifact-text.d.ts +62 -0
  258. package/dist/tools/artifact/artifact-text.js +68 -0
  259. package/dist/tools/artifact/artifact-tool.d.ts +52 -0
  260. package/dist/tools/artifact/artifact-tool.js +351 -0
  261. package/dist/tools/artifact/local-stub.d.ts +26 -0
  262. package/dist/tools/artifact/local-stub.js +296 -0
  263. package/dist/tools/fs/bash-readonly-classifier.d.ts +114 -48
  264. package/dist/tools/fs/bash-readonly-classifier.js +171 -60
  265. package/dist/tools/fs/encoding.d.ts +31 -40
  266. package/dist/tools/fs/fs-bash.d.ts +49 -45
  267. package/dist/tools/fs/fs-bash.js +19 -27
  268. package/dist/tools/fs/fs-pdf.d.ts +8 -8
  269. package/dist/tools/fs/fs-search-tools.d.ts +4 -4
  270. package/dist/tools/fs/fs-search-tools.js +5 -2
  271. package/dist/tools/fs/fs-shared.d.ts +146 -158
  272. package/dist/tools/fs/fs-shared.js +3 -11
  273. package/dist/tools/fs/fs-write.d.ts +8 -8
  274. package/dist/tools/fs/gh-rate-limit.d.ts +13 -14
  275. package/dist/tools/fs/index.d.ts +78 -78
  276. package/dist/tools/fs/index.js +4 -1
  277. package/dist/tools/fs/notebook.d.ts +24 -25
  278. package/dist/tools/fs/pdf.d.ts +3 -3
  279. package/dist/tools/fs/read-deny.d.ts +22 -22
  280. package/dist/tools/fs/read-face.d.ts +21 -24
  281. package/dist/tools/fs/repo-map.d.ts +8 -8
  282. package/dist/tools/fs/safety.d.ts +233 -130
  283. package/dist/tools/fs/safety.js +131 -31
  284. package/dist/tools/fs/search.d.ts +63 -71
  285. package/dist/tools/fs/search.js +8 -6
  286. package/package.json +6 -2
  287. package/test/export-surface.snapshot.json +119 -39
@@ -1,4 +1,4 @@
1
- import { isAbsolutePathForm, isBlockedDevicePath, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
1
+ import { isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, pathFamilyOf, withinAnyRoot } from "./safety.js";
2
2
  export const NOT_AUTO_ALLOWED = "— not auto-allowed";
3
3
  export const BASH_READONLY_DEFAULT_ALLOW = [
4
4
  "ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
@@ -213,7 +213,7 @@ const NO_PATH_OPERAND_COMMANDS = new Set([
213
213
  function isPathShapedToken(tok) {
214
214
  return tok.includes("/") || tok.startsWith("~") || tok === "." || tok === "..";
215
215
  }
216
- function resolveOperandLexically(base, operand, homeDir) {
216
+ export function resolveOperandLexically(base, operand, homeDir) {
217
217
  let raw = operand;
218
218
  if (raw === "~" || raw.startsWith("~/")) {
219
219
  if (homeDir === undefined || !isAbsolutePathForm(homeDir))
@@ -223,15 +223,33 @@ function resolveOperandLexically(base, operand, homeDir) {
223
223
  else if (raw.startsWith("~")) {
224
224
  return undefined;
225
225
  }
226
- if (isAbsolutePathForm(raw))
227
- return normalizeAbsPathLexically(raw);
228
- if (base === undefined || !isAbsolutePathForm(base))
226
+ const family = base === undefined ? undefined : pathFamilyOf({ root: base });
227
+ if (isAbsoluteForFamily(family, raw))
228
+ return normalizeAbsPathLexically(nativeUncSpellingOf(family, raw));
229
+ if (base === undefined || family === undefined || !isAbsoluteForFamily(family, base))
229
230
  return undefined;
230
- return normalizeAbsPathLexically(`${base.replace(/[/\\]+$/, "")}/${raw}`);
231
+ if (isShellRootedSpellingUnmapped(family, raw))
232
+ return undefined;
233
+ return normalizeAbsPathLexically(joinForFamily(family, base, raw));
231
234
  }
232
235
  function tokenizeSegment(segment) {
233
236
  const raw = splitWordsQuoteAware(segment);
234
- return { folded: raw.map(foldQuoteRemovalToken), raw };
237
+ return { folded: raw.map((r) => { const f = foldQuoteRemovalToken(r); return tildeIsLiteral(r) ? literalTildeSpelling(f) : f; }), raw };
238
+ }
239
+ function literalTildeSpelling(word) {
240
+ return word.startsWith("~") ? `./${word}` : word;
241
+ }
242
+ function tildeIsLiteral(raw) {
243
+ if (!raw.startsWith("~"))
244
+ return true;
245
+ for (let i = 1; i < raw.length; i++) {
246
+ const ch = raw[i];
247
+ if (ch === '"' || ch === "'")
248
+ return true;
249
+ if (ch === "/")
250
+ return false;
251
+ }
252
+ return false;
235
253
  }
236
254
  function splitWordsQuoteAware(segment) {
237
255
  const s = segment.trim();
@@ -575,11 +593,12 @@ function grepClusterValueOwner(tok) {
575
593
  return undefined;
576
594
  }
577
595
  function attachedOptionPayloads(tok) {
596
+ const literal = literalTildeSpelling;
578
597
  const out = [];
579
598
  if (tok.startsWith("--")) {
580
599
  const eq = tok.indexOf("=");
581
600
  if (eq > 0 && eq + 1 < tok.length)
582
- out.push(tok.slice(eq + 1));
601
+ out.push(literal(tok.slice(eq + 1)));
583
602
  return out;
584
603
  }
585
604
  for (let k = 1; k <= tok.length; k++) {
@@ -587,7 +606,7 @@ function attachedOptionPayloads(tok) {
587
606
  break;
588
607
  const payload = tok.slice(k);
589
608
  if (payload.length > 0)
590
- out.push(payload);
609
+ out.push(literal(payload));
591
610
  }
592
611
  return out;
593
612
  }
@@ -600,31 +619,35 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
600
619
  const candidates = [];
601
620
  const bareWordOperands = [];
602
621
  const positionalOperands = [];
622
+ let stdinOperand = false;
603
623
  const argGlobs = (k) => {
604
624
  const raw = tokens.raw[k + 1];
605
625
  return raw !== undefined && hasUnquotedGlobMetachar(raw);
606
626
  };
607
- for (const rawArg of tokens.raw.slice(1)) {
627
+ const expandingArgs = new Set();
628
+ tokens.raw.slice(1).forEach((rawArg, k) => {
608
629
  if (!hasUnquotedExpansionMetachar(rawArg))
609
- continue;
610
- return [
611
- {
612
- kind: "unresolvable",
613
- reason: `"${name}" is given the argument "${rawArg}", which the shell expands (brace/variable/command expansion) before the command runs — the path it would actually read cannot be resolved statically, so it is not auto-allowed`,
614
- },
615
- ];
616
- }
630
+ return;
631
+ expandingArgs.add(k);
632
+ findings.push({
633
+ kind: "unresolvable",
634
+ operand: rawArg,
635
+ reason: `"${name}" is given the argument "${rawArg}", which the shell expands (brace/variable/command expansion) before the command runs — the path it would actually read cannot be resolved statically, so it is not auto-allowed`,
636
+ });
637
+ });
638
+ if (name === "cd" && expandingArgs.size > 0)
639
+ return findings;
617
640
  if (name === "cd") {
618
641
  const targetIdx = args.findIndex((t) => !t.startsWith("-") || t === "-");
619
642
  const target = targetIdx === -1 ? undefined : args[targetIdx];
620
643
  if (target !== undefined && argGlobs(targetIdx)) {
621
- return [{ kind: "unresolvable", reason: `\`cd\` is given the pattern "${target}", which the shell expands to a directory this check cannot know ${NOT_AUTO_ALLOWED}` }];
644
+ return [{ kind: "unresolvable", operand: target, reason: `\`cd\` is given the pattern "${target}", which the shell expands to a directory this check cannot know ${NOT_AUTO_ALLOWED}` }];
622
645
  }
623
646
  if (target === undefined) {
624
- return [{ kind: "unresolvable", reason: '`cd` with no argument targets the home directory, which cannot be checked against the allowed directories ' + NOT_AUTO_ALLOWED }];
647
+ return [{ kind: "unresolvable", operand: "cd", reason: '`cd` with no argument targets the home directory, which cannot be checked against the allowed directories ' + NOT_AUTO_ALLOWED }];
625
648
  }
626
649
  if (target === "-") {
627
- return [{ kind: "unresolvable", reason: '`cd -` targets the previous working directory, which cannot be resolved statically ' + NOT_AUTO_ALLOWED }];
650
+ return [{ kind: "unresolvable", operand: "-", reason: '`cd -` targets the previous working directory, which cannot be resolved statically ' + NOT_AUTO_ALLOWED }];
628
651
  }
629
652
  candidates.push({ text: target, globbed: false });
630
653
  }
@@ -635,6 +658,8 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
635
658
  let endOfOptions = false;
636
659
  for (let k = 0; k < args.length; k++) {
637
660
  const t = args[k];
661
+ if (expandingArgs.has(k))
662
+ continue;
638
663
  if (!endOfOptions) {
639
664
  if (t === "--") {
640
665
  endOfOptions = true;
@@ -662,8 +687,10 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
662
687
  if (isGrepPatternSlot)
663
688
  continue;
664
689
  const m = RECURSIVE_READ_FORMS[name];
665
- if (m === undefined || m.dashIsStdin === true)
690
+ if (m === undefined || m.dashIsStdin === true) {
691
+ stdinOperand = true;
666
692
  continue;
693
+ }
667
694
  positionalOperands.push(t);
668
695
  continue;
669
696
  }
@@ -677,7 +704,7 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
677
704
  }
678
705
  }
679
706
  for (let k = 0; k < args.length; k++) {
680
- if (!argGlobs(k))
707
+ if (!argGlobs(k) || expandingArgs.has(k))
681
708
  continue;
682
709
  const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], args[k], boundary.homeDir);
683
710
  findings.push({ kind: "undecided", path: resolved ?? args[k] });
@@ -687,6 +714,7 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
687
714
  if (resolved === undefined) {
688
715
  findings.push({
689
716
  kind: "unresolvable",
717
+ operand: candidate,
690
718
  reason: `"${name}" names the path "${candidate}", which cannot be resolved statically ${NOT_AUTO_ALLOWED}`,
691
719
  });
692
720
  continue;
@@ -706,7 +734,7 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
706
734
  findings.push({ kind: "inside", path: resolved });
707
735
  }
708
736
  if (boundary.denyMatch !== undefined && segmentSelectsRecursiveRead(name, args)) {
709
- const roots = positionalOperands.length > 0 ? positionalOperands : ["."];
737
+ const roots = positionalOperands.length > 0 ? positionalOperands : stdinOperand ? [] : ["."];
710
738
  for (const operand of roots) {
711
739
  const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], operand, boundary.homeDir);
712
740
  findings.push({ kind: "recursive", path: resolved ?? operand });
@@ -714,8 +742,56 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
714
742
  }
715
743
  return findings;
716
744
  }
717
- function segmentCompoundForReadonly(command) {
718
- const trimmed = command.trim();
745
+ export function foldHomeVariableSpelling(command, homeDir) {
746
+ if (homeDir === undefined || !command.includes("$") || /[\\`]/.test(command))
747
+ return command;
748
+ const unquotedIsExact = /^[A-Za-z0-9_./+@:%=,-]+$/.test(homeDir);
749
+ const quotedIsExact = !/["$`\\\n\r]/.test(homeDir);
750
+ const UNQUOTED_HOME_WORD = /^\$(?:HOME|\{HOME\})(?=[/\s;|&"]|$)/;
751
+ const DOUBLE_QUOTED_HOME_WORD = /^\$(?:HOME|\{HOME\})(?=[/"]|$)/;
752
+ let open;
753
+ let wordStart = true;
754
+ let quoteHadContent = false;
755
+ let out = "";
756
+ for (let i = 0; i < command.length; i++) {
757
+ const ch = command[i];
758
+ if (open === undefined && (ch === '"' || ch === "'")) {
759
+ open = ch;
760
+ quoteHadContent = false;
761
+ out += ch;
762
+ continue;
763
+ }
764
+ if (open !== undefined && open === ch) {
765
+ open = undefined;
766
+ wordStart = wordStart && !quoteHadContent;
767
+ out += ch;
768
+ continue;
769
+ }
770
+ if (open !== undefined)
771
+ quoteHadContent = true;
772
+ if (open === undefined && /[\s;|&]/.test(ch)) {
773
+ wordStart = true;
774
+ out += ch;
775
+ continue;
776
+ }
777
+ if (ch === "$" && wordStart && ((open === '"' && quotedIsExact) || (open === undefined && unquotedIsExact))) {
778
+ const m = (open === '"' ? DOUBLE_QUOTED_HOME_WORD : UNQUOTED_HOME_WORD).exec(command.slice(i));
779
+ if (m !== null) {
780
+ wordStart = false;
781
+ if (open === '"')
782
+ quoteHadContent = true;
783
+ out += homeDir;
784
+ i += m[0].length - 1;
785
+ continue;
786
+ }
787
+ }
788
+ wordStart = false;
789
+ out += ch;
790
+ }
791
+ return open === undefined ? out : command;
792
+ }
793
+ function segmentCompoundForReadonly(command, homeDir) {
794
+ const trimmed = foldHomeVariableSpelling(command, homeDir).trim();
719
795
  if (!trimmed)
720
796
  return { reject: "empty command" };
721
797
  const redirectionStripped = stripExemptRedirections(trimmed);
@@ -725,7 +801,7 @@ function segmentCompoundForReadonly(command) {
725
801
  return splitShellCompoundSegments(terminatorStripped, { trailingTerminator: "keep" });
726
802
  }
727
803
  export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts) {
728
- const split = segmentCompoundForReadonly(command);
804
+ const split = segmentCompoundForReadonly(command, boundary?.homeDir);
729
805
  if ("reject" in split)
730
806
  return { reason: split.reject };
731
807
  const { segments, pipeFed } = split;
@@ -764,6 +840,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
764
840
  };
765
841
  const STDIN_FILE_FLOOR = { cat: 1, grep: 2, head: 1, tail: 1, wc: 1, cut: 1, tr: Infinity, diff: 2, cmp: 2, comm: 2, sed: 2 };
766
842
  const foldedSegments = [];
843
+ let shapeRefusal;
767
844
  for (let si = 0; si < segments.length; si++) {
768
845
  const segmentTokens = tokenizeSegment(segments[si]);
769
846
  const toks = segmentTokens.folded;
@@ -772,8 +849,10 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
772
849
  foldedSegments.push(segmentTokens);
773
850
  const name = toks[0];
774
851
  const guardReason = guardedVerbFormReason(name, toks, segmentTokens.raw);
775
- if (guardReason !== undefined)
776
- return { reason: guardReason };
852
+ if (guardReason !== undefined) {
853
+ shapeRefusal ??= guardReason;
854
+ continue;
855
+ }
777
856
  if (!(pipeFed[si] ?? false)) {
778
857
  const floor = STDIN_FILE_FLOOR[name];
779
858
  const restArgs = toks.slice(1);
@@ -815,14 +894,17 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
815
894
  }
816
895
  }
817
896
  if (floor !== undefined && hasStdinDash) {
818
- return { reason: `"${name}" reads stdin via an explicit "-" argument and would block until the tool timeout ${NOT_AUTO_ALLOWED}` };
897
+ shapeRefusal ??= `"${name}" reads stdin via an explicit "-" argument and would block until the tool timeout ${NOT_AUTO_ALLOWED}`;
898
+ continue;
819
899
  }
820
900
  if (floor !== undefined && operandCount < floor) {
821
- return { reason: `"${name}" with no file argument reads stdin and would block until the tool timeout ${NOT_AUTO_ALLOWED}` };
901
+ shapeRefusal ??= `"${name}" with no file argument reads stdin and would block until the tool timeout ${NOT_AUTO_ALLOWED}`;
902
+ continue;
822
903
  }
823
904
  }
824
905
  if (name === "tail" && toks.slice(1).some((t) => t === "--follow" || t.startsWith("--follow=") || /^[-+][^\s]*[fF]/.test(t))) {
825
- return { reason: "`tail` in follow mode never terminates " + NOT_AUTO_ALLOWED };
906
+ shapeRefusal ??= "`tail` in follow mode never terminates " + NOT_AUTO_ALLOWED;
907
+ continue;
826
908
  }
827
909
  const GENERATOR_DEVICES = new Set(["/dev/zero", "/dev/random", "/dev/urandom", "/dev/full"]);
828
910
  const deviceCandidates = [];
@@ -833,20 +915,26 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
833
915
  }
834
916
  deviceCandidates.push(t);
835
917
  }
836
- const deviceArgs = deviceCandidates.map(normalizeAbsPathLexically).filter(isBlockedDevicePath);
918
+ const deviceArgs = deviceCandidates.map((t) => resolveOperandLexically(undefined, t, boundary?.homeDir) ?? normalizeAbsPathLexically(t)).filter(isBlockedDevicePath);
837
919
  const rescuedByHead = headBoundIsSmall(name, toks) && deviceArgs.every((d) => GENERATOR_DEVICES.has(d));
838
920
  if (!rescuedByHead && deviceArgs.length > 0) {
839
- return { reason: "reads a device/special file that is either unbounded (/dev/zero, /dev/stdin, /proc/<pid>/fd/0, … — blocks the pipeline until the tool timeout) or process-private (/proc/<pid>/environ, /proc/<pid>/mem, …) " + NOT_AUTO_ALLOWED };
921
+ shapeRefusal ??= "reads a device/special file that is either unbounded (/dev/zero, /dev/stdin, /proc/<pid>/fd/0, … — blocks the pipeline until the tool timeout) or process-private (/proc/<pid>/environ, /proc/<pid>/mem, …) " + NOT_AUTO_ALLOWED;
840
922
  }
841
923
  }
842
924
  if (opts?.iterated !== true) {
843
925
  const cdSegments = foldedSegments.filter((st) => st.folded[0] === "cd");
844
- if (cdSegments.length > 1) {
845
- return { reason: `multiple directory changes in one command require approval for clarity ${NOT_AUTO_ALLOWED}` };
846
- }
926
+ if (cdSegments.length > 1)
927
+ shapeRefusal ??= `multiple directory changes in one command require approval for clarity ${NOT_AUTO_ALLOWED}`;
928
+ }
929
+ if (boundary !== undefined) {
930
+ const walk = evaluateReadBoundary(foldedSegments, boundary);
931
+ if (shapeRefusal === undefined)
932
+ return walk;
933
+ const { reason: _walkSentence, ...structure } = walk;
934
+ return { ...structure, reason: shapeRefusal };
847
935
  }
848
- if (boundary !== undefined)
849
- return evaluateReadBoundary(foldedSegments, boundary);
936
+ if (shapeRefusal !== undefined)
937
+ return { reason: shapeRefusal };
850
938
  if (foldedSegments.some((st) => st.folded[0] === "cd")) {
851
939
  return { reason: `\`cd\` cannot be judged without a read boundary (no base to resolve the directory change against) ${NOT_AUTO_ALLOWED}` };
852
940
  }
@@ -855,14 +943,25 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
855
943
  function evaluateReadBoundary(foldedSegments, boundary) {
856
944
  const outside = [];
857
945
  const inside = [];
946
+ let unresolvable;
947
+ const unresolved = [];
948
+ let baseLost = false;
858
949
  const undecided = [];
859
950
  const recursive = [];
860
951
  let currentBase = boundary.cwd ?? boundary.roots[0];
861
952
  for (const toks of foldedSegments) {
953
+ if (baseLost)
954
+ break;
862
955
  const segBoundary = currentBase === (boundary.cwd ?? boundary.roots[0]) ? boundary : { ...boundary, cwd: currentBase };
863
956
  for (const finding of collectSegmentBoundaryFindings(toks, segBoundary)) {
864
- if (finding.kind === "unresolvable")
865
- return { reason: finding.reason };
957
+ if (finding.kind === "unresolvable") {
958
+ unresolvable ??= finding.reason;
959
+ if (!unresolved.includes(finding.operand))
960
+ unresolved.push(finding.operand);
961
+ if (toks.folded[0] === "cd")
962
+ baseLost = true;
963
+ continue;
964
+ }
866
965
  const denied = boundary.denyMatch?.(finding.path);
867
966
  if (denied != null) {
868
967
  return {
@@ -893,7 +992,7 @@ function evaluateReadBoundary(foldedSegments, boundary) {
893
992
  if (!outside.some((o) => o.path === finding.path))
894
993
  outside.push(finding);
895
994
  }
896
- if (toks.folded[0] === "cd") {
995
+ if (!baseLost && toks.folded[0] === "cd") {
897
996
  const cdArgs = toks.folded.slice(1);
898
997
  const cdTargetIdx = cdArgs.findIndex((t) => !t.startsWith("-") || t === "-");
899
998
  const cdTarget = cdTargetIdx === -1 ? undefined : cdArgs[cdTargetIdx];
@@ -907,6 +1006,15 @@ function evaluateReadBoundary(foldedSegments, boundary) {
907
1006
  ...(undecidedAll.length > 0 ? { undecidedPaths: undecidedAll } : {}),
908
1007
  ...(recursive.length > 0 ? { recursiveReadPaths: recursive } : {}),
909
1008
  };
1009
+ if (unresolvable !== undefined) {
1010
+ return {
1011
+ reason: unresolvable,
1012
+ unresolvedOperands: unresolved,
1013
+ ...(outside.length > 0 ? { outOfRootRead: true, outOfRootPaths: outside.map((o) => o.path) } : {}),
1014
+ ...(inside.length > 0 ? { checkedPaths: inside } : {}),
1015
+ ...undecidedField,
1016
+ };
1017
+ }
910
1018
  if (outside.length === 0)
911
1019
  return inside.length > 0 ? { checkedPaths: inside, ...undecidedField } : { ...undecidedField };
912
1020
  const paths = outside.map((o) => `"${o.path}"`).join(", ");
@@ -948,7 +1056,7 @@ export function classifyOutOfRootReadGate(command, allow, boundary) {
948
1056
  ...(boundary.face !== undefined ? { face: boundary.face } : {}),
949
1057
  };
950
1058
  const judge = (text) => {
951
- const split = segmentCompoundForReadonly(text);
1059
+ const split = segmentCompoundForReadonly(text, containmentOnly.homeDir);
952
1060
  if ("reject" in split)
953
1061
  return NOT_GATED;
954
1062
  const readers = [];
@@ -1028,53 +1136,56 @@ function pollLoopSleepReason(segment) {
1028
1136
  return undefined;
1029
1137
  }
1030
1138
  export function classifyBoundedReadonlyPollLoop(command, allow, boundary) {
1139
+ return classifyBoundedReadonlyPollLoopDetailed(command, allow, boundary).reason;
1140
+ }
1141
+ export function classifyBoundedReadonlyPollLoopDetailed(command, allow, boundary) {
1031
1142
  if (NON_ASCII_WHITESPACE.test(command)) {
1032
- return "the command contains a non-ASCII or control whitespace character (only space and tab are allowed) — its word boundaries cannot be read the way bash would split them, so it is not auto-allowed";
1143
+ return { reason: "the command contains a non-ASCII or control whitespace character (only space and tab are allowed) — its word boundaries cannot be read the way bash would split them, so it is not auto-allowed" };
1033
1144
  }
1034
- const trimmed = command.trim();
1145
+ const trimmed = foldHomeVariableSpelling(command, boundary?.homeDir).trim();
1035
1146
  if (!trimmed)
1036
- return "empty command";
1147
+ return { reason: "empty command" };
1037
1148
  if (!pollLoopQuotesBalanced(trimmed)) {
1038
- return "unbalanced quote — the command's structure cannot be read reliably, so it is not auto-allowed";
1149
+ return { reason: "unbalanced quote — the command's structure cannot be read reliably, so it is not auto-allowed" };
1039
1150
  }
1040
1151
  const m = POLL_LOOP_SHAPE.exec(trimmed);
1041
1152
  if (m === null) {
1042
- return "not a bounded read-only poll loop (`for <v> in <literal bound>; do <read commands>; done` is the only accepted control structure)";
1153
+ return { reason: "not a bounded read-only poll loop (`for <v> in <literal bound>; do <read commands>; done` is the only accepted control structure)" };
1043
1154
  }
1044
1155
  const varName = m[1];
1045
1156
  const iterable = m[2];
1046
1157
  const body = m[3];
1047
1158
  if (!/^[a-z]$/.test(varName)) {
1048
- return `the loop variable "${varName}" must be a single lowercase letter — a name like PATH/IFS/LD_PRELOAD would change how the body's commands resolve`;
1159
+ return { reason: `the loop variable "${varName}" must be a single lowercase letter — a name like PATH/IFS/LD_PRELOAD would change how the body's commands resolve` };
1049
1160
  }
1050
1161
  const beats = pollLoopBeatsOf(iterable);
1051
1162
  if (beats === undefined) {
1052
- return `the loop bound "${iterable}" is not a recognized literal — only \`$(seq <int> <int>)\`, \`{<int>..<int>}\` (ascending) or a literal word list is accepted`;
1163
+ return { reason: `the loop bound "${iterable}" is not a recognized literal — only \`$(seq <int> <int>)\`, \`{<int>..<int>}\` (ascending) or a literal word list is accepted` };
1053
1164
  }
1054
1165
  if (beats > POLL_LOOP_MAX_BEATS) {
1055
- return `the loop runs ${beats} iterations, above the ${POLL_LOOP_MAX_BEATS}-iteration cap for auto-allow`;
1166
+ return { reason: `the loop runs ${beats} iterations, above the ${POLL_LOOP_MAX_BEATS}-iteration cap for auto-allow` };
1056
1167
  }
1057
1168
  if (POLL_BODY_HARD_REJECT.test(body)) {
1058
- return "the loop body may not contain redirection, pipes, backgrounding, substitution, subshells, braces, escapes, or line breaks";
1169
+ return { reason: "the loop body may not contain redirection, pipes, backgrounding, substitution, subshells, braces, escapes, or line breaks" };
1059
1170
  }
1060
1171
  const readSegments = [];
1061
1172
  for (const rawSegment of body.split(";")) {
1062
1173
  const segment = rawSegment.trim();
1063
1174
  if (segment.length === 0)
1064
- return "empty command in the loop body";
1175
+ return { reason: "empty command in the loop body" };
1065
1176
  const parsed = parseLeadingCommandName(segment);
1066
1177
  if ("reject" in parsed)
1067
- return parsed.reject;
1178
+ return { reason: parsed.reject };
1068
1179
  if (parsed.name === "sleep") {
1069
1180
  const sleepReason = pollLoopSleepReason(segment);
1070
1181
  if (sleepReason !== undefined)
1071
- return sleepReason;
1182
+ return { reason: sleepReason };
1072
1183
  continue;
1073
1184
  }
1074
1185
  readSegments.push(segment);
1075
1186
  }
1076
1187
  if (readSegments.length === 0) {
1077
- return "the loop body has no read command — a sleep-only loop observes nothing and is not auto-allowed";
1188
+ return { reason: "the loop body has no read command — a sleep-only loop observes nothing and is not auto-allowed" };
1078
1189
  }
1079
1190
  const bodyHasCd = readSegments.some((seg) => {
1080
1191
  const p = parseLeadingCommandName(seg);
@@ -1083,14 +1194,14 @@ export function classifyBoundedReadonlyPollLoop(command, allow, boundary) {
1083
1194
  const modelled = bodyHasCd ? Array.from({ length: beats }, () => readSegments.join("; ")).join("; ") : readSegments.join("; ");
1084
1195
  const verdict = classifyCompoundReadonlyDetailed(modelled, allow, boundary, { iterated: bodyHasCd });
1085
1196
  if (verdict.reason !== undefined)
1086
- return verdict.reason;
1197
+ return verdict;
1087
1198
  if (verdict.recursiveReadPaths !== undefined) {
1088
1199
  const roots = verdict.recursiveReadPaths;
1089
1200
  const shown = roots.slice(0, POLL_LOOP_CAUSE_MAX_PATHS);
1090
- return `this lexical check cannot bound what a recursive read form in the loop body traverses — ${roots.length} path(s) taken as traversal roots: ${shown.join(", ")}${roots.length > shown.length ? `, and ${roots.length - shown.length} more` : ""}`;
1201
+ return { ...verdict, reason: `this lexical check cannot bound what a recursive read form in the loop body traverses — ${roots.length} path(s) taken as traversal roots: ${shown.join(", ")}${roots.length > shown.length ? `, and ${roots.length - shown.length} more` : ""}` };
1091
1202
  }
1092
1203
  if (verdict.undecidedPaths !== undefined) {
1093
- return `the loop body carries an unexpanded glob (${verdict.undecidedPaths.join(", ")}) — what a REPEATED read touches is decided at run time, so it is not auto-allowed`;
1204
+ return { ...verdict, reason: `the loop body carries an unexpanded glob (${verdict.undecidedPaths.join(", ")}) — what a REPEATED read touches is decided at run time, so it is not auto-allowed` };
1094
1205
  }
1095
- return undefined;
1206
+ return verdict;
1096
1207
  }
@@ -1,25 +1,19 @@
1
1
  /**
2
- * 编码 parity 批(docs/CC-TOOL-PARITY-SWEEP-2026-07.md P0)— file encoding + line-ending detection
3
- * for the Read/Edit/Write band, ported from CC's `utils/fileRead.ts` mechanism and hardened where CC
4
- * is lossy:
2
+ * File encoding + line-ending detection for the Read/Edit/Write band, ported from CC's
3
+ * `utils/fileRead.ts` mechanism and hardened where CC is lossy:
5
4
  *
6
- * - BOM detection: `FF FE` → utf16le, `EF BB BF` → utf8-with-BOM, else plain utf8. (CC 88→2.1.187
7
- * dynamic-verified: detection is BOM-only; UTF-16BE / heuristic sniffing deliberately NOT done —
8
- * a wrong guess silently corrupts, and the BOM-less case stays a refusal at the Read layer.)
5
+ * - BOM detection: `FF FE` → utf16le, `EF BB BF` → utf8-with-BOM, else plain utf8. Detection is
6
+ * BOM-only; UTF-16BE / heuristic sniffing is deliberately NOT done — a wrong guess silently
7
+ * corrupts, and the BOM-less case stays a refusal at the Read layer.
9
8
  * - The MODEL-FACING text is BOM-stripped and CRLF-normalized (what the model quotes in `old_string`
10
- * must match what Read showed it).
11
- * RB-285 (encoding LOW, 归因收窄 — CC 220 亲核): the blanket "(CC parity)" that used to sit on this
12
- * line was half wrong, because CC has TWO read lanes and they disagree with each other.
13
- * · CRLF-normalize parity on both lanes (`Zr_` @276877 and `Mqe` @58197 both `replaceAll`).
14
- * · BOM-strip — parity on CC's READ lane only (`Zr_` @276877: `charCodeAt(0) === 65279 slice(1)`).
15
- * CC's EDIT/WRITE lane (`Mqe`) does NOT strip: it hands the model text with the leading U+FEFF
16
- * still in it, and only the staleness/hash coordinate (`X9` = `CTe` + CRLF, @365630-365635)
17
- * strips. sema strips in ONE shared decoder for every lane, so the model-facing view and the
18
- * hash coordinate cannot drift apart — an improvement over CC's split, recorded as such rather
19
- * than claimed as parity.
20
- * - Write-back re-encodes with the ORIGINAL encoding, PRESERVES the BOM (CC drops it on Write — its
21
- * lossy face, dynamic-verified W1; we keep it, recorded as a deliberate improvement), and restores
22
- * the file's dominant line endings for Edit (Write keeps the model's endings as-is, CC decision).
9
+ * must match what Read showed it). CRLF-normalize is parity on both of CC's read lanes; BOM-strip
10
+ * is parity on CC's READ lane only CC's EDIT/WRITE lane hands the model text with the leading
11
+ * U+FEFF still in it and strips only inside its staleness/hash coordinate. sema strips in ONE
12
+ * shared decoder for every lane, so the model-facing view and the hash coordinate cannot drift
13
+ * apart: a registered delta, not parity.
14
+ * - Write-back re-encodes with the ORIGINAL encoding, PRESERVES the BOM (CC drops it on Write — a
15
+ * registered deliberate improvement), and restores the file's dominant line endings for Edit
16
+ * (Write keeps the model's endings as-is, CC decision).
23
17
  */
24
18
  /** Detected byte-level encoding of a text file (BOM-driven; utf8 is the no-BOM default). */
25
19
  export interface DetectedFileEncoding {
@@ -31,19 +25,17 @@ export interface DecodedTextFile {
31
25
  /** Model-facing text: BOM stripped, `\r\n` normalized to `\n`. The staleness-hash coordinate. */
32
26
  text: string;
33
27
  encoding: DetectedFileEncoding;
34
- /** Dominant line ending of the ORIGINAL bytes (majority vote CC `CKt` @58184 同算法).
28
+ /** Dominant line ending of the ORIGINAL bytes (majority vote; CC's counting rule).
35
29
  * Deliberately NOT per-line lossless: a mixed-endings file is unified to the dominant ending on
36
- * the first edit (镜头1/2 LOW, accepted non-goal — CC behaves the same way).
37
- * RB-285 (encoding LOW): the COUNTING RULE is CC's; the SAMPLE is not. CC votes on the first 4096
38
- * characters only (`CKt(s.slice(0, 4096))` @58201) while {@link detectLineEndings} below votes on
39
- * the whole file so a file whose first 4KB is LF and whose bulk is CRLF gets opposite verdicts.
40
- * sema's whole-file vote is kept on purpose: the verdict decides what an Edit WRITES BACK across
41
- * the entire file, so sampling a prefix is exactly the case where the cheap answer flips the
42
- * remaining 99% of the lines. Registered as a deliberate delta, not parity. */
30
+ * the first edit (accepted non-goal — CC behaves the same way).
31
+ * The SAMPLE is a registered delta: CC votes on the first 4096 characters only, while
32
+ * {@link detectLineEndings} below votes on the whole file. The verdict decides what an Edit WRITES
33
+ * BACK across the entire file, so sampling a prefix is exactly the case where the cheap answer
34
+ * flips the remaining 99% of the lines. */
43
35
  endings: DetectedLineEndings;
44
- /** 双镜头 MED: a utf16le body with an ODD byte count is TRUNCATED/corrupt — decoding would
45
- * silently drop the dangling byte and a later write-back would destroy it permanently. Flagged so
46
- * the tool layer fails closed instead of "repairing" the file. */
36
+ /** A utf16le body with an ODD byte count is TRUNCATED/corrupt — decoding would silently drop the
37
+ * dangling byte and a later write-back would destroy it permanently. Flagged so the tool layer
38
+ * fails closed instead of "repairing" the file. */
47
39
  malformed?: true;
48
40
  }
49
41
  /** BOM-only encoding detection (CC `detectEncodingForResolvedPath` parity — no content heuristics). */
@@ -56,8 +48,8 @@ export declare function decodeTextBytes(bytes: Uint8Array): DecodedTextFile;
56
48
  * @param text normalized (`\n`) text when `endings` is CRLF/LF (Edit path — restore the file's
57
49
  * dominant endings); pass `endings:"preserve"` to write the text's OWN line endings untouched
58
50
  * (Write path — CC decision: the model sent explicit endings in `content` and meant them).
59
- * @returns a plain string for the no-BOM utf8 + LF-or-preserve fast path (byte-compatible with the
60
- * pre-batch writer), else the exact bytes (BOM + encoded body).
51
+ * @returns a plain string for the no-BOM utf8 + LF-or-preserve fast path, else the exact bytes
52
+ * (BOM + encoded body).
61
53
  */
62
54
  export declare function encodeTextForFile(text: string, encoding: DetectedFileEncoding, endings: DetectedLineEndings | "preserve"): string | Uint8Array;
63
55
  /** Normalize model-supplied match/replacement text the same way the file text was normalized
@@ -65,16 +57,15 @@ export declare function encodeTextForFile(text: string, encoding: DetectedFileEn
65
57
  * Deliberately does NOT strip a leading U+FEFF: an `old_string` starting with one may be quoting
66
58
  * genuine file content — use {@link normalizeFileText} only where the string stands for a whole file. */
67
59
  export declare function normalizeEditText(s: string): string;
68
- /** RB-222 — split a leading U+FEFF off MODEL-SUPPLIED whole-file text (CC `CTe` parity,
69
- * 220 @365630). The BOM is a file-level byte marker, not text: decodeTextBytes strips it on the way
70
- * in, so it can never live in the normalized coordinate. Only for strings that stand for a WHOLE
71
- * file (Write `content`, readFileState seeding) — a decoded file text's leading U+FEFF is genuine
72
- * content and must stay. */
60
+ /** Split a leading U+FEFF off MODEL-SUPPLIED whole-file text (CC parity). The BOM is a file-level
61
+ * byte marker, not text: decodeTextBytes strips it on the way in, so it can never live in the
62
+ * normalized coordinate. Only for strings that stand for a WHOLE file (Write `content`,
63
+ * readFileState seeding) — a decoded file text's leading U+FEFF is genuine content and must stay. */
73
64
  export declare function splitLeadingBom(text: string): {
74
65
  hadBom: boolean;
75
66
  text: string;
76
67
  };
77
- /** RB-222 — CC `X9` parity (220 @365635 = BOM-strip + CRLF-normalize): THE coordinate
78
- * every readFileState hash lives in (Read/Edit/Write staleness all hash `decodeTextBytes(...).text`).
79
- * A hash taken on raw model content with a leading U+FEFF can never agree with any later re-read. */
68
+ /** CC parity (BOM-strip + CRLF-normalize): THE coordinate every readFileState hash lives in
69
+ * (Read/Edit/Write staleness all hash `decodeTextBytes(...).text`). A hash taken on raw model content
70
+ * with a leading U+FEFF can never agree with any later re-read. */
80
71
  export declare function normalizeFileText(s: string): string;