chainlesschain 0.166.138 → 0.166.139

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,22 +1,38 @@
1
1
  # chainlesschain CLI
2
2
 
3
- Publication authorization: the user explicitly permits this release irrespective of the five task results. Complete exact-commit CLI CI and Strict Sandbox remain required before Actions OIDC publication, followed by CLI registry verification and paired IDE releases. Latest model acceptance is 0/5; historical 41/45 is unchanged.
4
-
5
3
  Command-line interface for installing, configuring, and managing [ChainlessChain](https://www.chainlesschain.com), including the coding agent, governed tools, sessions, memory, and IDE integrations.
6
4
 
7
- The .137 OIDC run stopped before CLI upload because an Agent SDK test changed without a version bump. SDK **0.2.17** restores source/provenance alignment; its runtime output is unchanged. Release uses a new immutable .138 tag, with the existing tree/provenance checks retained.
5
+ <!-- release139-candidate:start -->
6
+
7
+ ## CLI 0.166.139 / IDE repair release
8
+
9
+ This package is CLI **0.166.139**, paired with Open VSX **0.37.175** and JetBrains **0.4.192**, with unchanged upstream SDK **0.2.17** and Kernel **0.1.11**. Production publication uses GitHub Actions OIDC after the exact-source CLI CI and Strict Sandbox matrices pass on every configured platform. Install `npm i -g chainlesschain@0.166.139 --registry https://registry.npmjs.org`, restart the CLI and IDE chat hosts, and check `cc --version` and `cc doctor`. IDE plugins publish after the public CLI package is verified; mobile and PC clients follow verified IDE releases on their separate product version track.
10
+
11
+ The repair reserves author, fresh independent review and final reporting work within the original total request budget. Partial JavaScript reads recover complete surrounding statements from the same file snapshot within the existing output limit; focused reads do not silently switch to unrelated ranges. This release includes the standalone WebSocket host-model-policy repair.
12
+
13
+ CLI .138 acceptance remains **2/4**: verify-13 and first-run-darwin-vscode passed; verify-08 and verify-36 failed on author-stage iteration exhaustion despite passing candidate checks. CLI .139 engineering checks do not establish new model-task acceptance or rescore those results. CLI .136 verify-19 remains a separate historical pass; historical **41/45** and .137 **0/5** are unchanged. Review stays enabled, with 20 model turns, 24 physical requests per diagnostic case and the 32768-token planning context. JetBrains uploads may await Marketplace approval; an official pending-review ZIP is distinct from a public listing. Microsoft Marketplace remains deferred.
14
+
15
+ <!-- release139-candidate:end -->
16
+
17
+ <!-- release138-current:start -->
18
+
19
+ ## CLI 0.166.138 — previous verified public release
20
+
21
+ CLI **0.166.138** and upstream Agent SDK **0.2.17** are published through GitHub Actions OIDC. Install `npm i -g chainlesschain@0.166.138 --registry https://registry.npmjs.org`, restart `cc ui` and IDE chat hosts, then run `cc --version` and `cc doctor`. Open VSX **0.37.174** publication succeeded. JetBrains **0.4.191** is approved and publicly listed in Marketplace (update `1209507`); public ZIP payload comparison with the official build remains pending. Both new IDE versions recommend CLI **0.166.138**.
22
+
23
+ This release preserves recovery when source content was omitted from a model request and retains the original user task across canonical compaction, tool tails and persistent sessions. Only exact source spans selected for a provider count as historical read coverage. Repeated delivery failures stop with an explicit limitation.
8
24
 
9
- ## CLI 0.166.138 — prepared candidate, not yet published
25
+ Exact release source `e42ea07b6da7ab2c3eb709002448b32d5fedf535` passed all 75 required CLI CI jobs (one optional PR dry-run skipped) and all 6 Strict Sandbox jobs across the configured Linux, Windows and macOS matrix. The official [OIDC workflow](https://github.com/chainlesschain/chainlesschain/actions/runs/41313099948) independently downloaded the public SDK/CLI, verified immutable package bytes and signed provenance, and installed/started the CLI before IDE release tags were created. SDK 0.2.17 aligns its full source tree with provenance; SDK runtime output is unchanged.
10
26
 
11
- After verified publication, install with `npm i -g chainlesschain@0.166.138 --registry https://registry.npmjs.org`, restart the chat host, and verify `cc --version` / `cc doctor`. The current public installation command remains `npm i -g chainlesschain@0.166.136 --registry https://registry.npmjs.org`. The candidate preserves focused recovery when source content is omitted from a model request. Same-batch reservations remain synchronous; only exact source spans actually selected for the provider become historical coverage. Repeated delivery failures stop with an explicit limitation, and confirmed navigation does not advance or move backward on an unsuccessful read.
27
+ As reviewed on **2026-11-02**, the public .138 four-sample retest accepted **2/4**: first-run VS Code and verify-13 passed. verify-08 and verify-36 passed independent candidate tests but exhausted **20/20** author turns before independent review, so both remain failures. All **80/80** physical requests have complete usage records. Previous .137 **0/5**, historical cumulative **41/45**, and the separate image follow-up **0/1** retain their own scopes; this is not a full 45-task success rate for .138. Accepted samples are skipped in later retests. Engineering checks and publication add no model task acceptance. Kernel stays **0.1.11**; Microsoft Marketplace and PC/mobile are outside this release.
12
28
 
13
- Canonical compaction also retains the original user task and owned resume sources across tool tails and persistent sessions, rejecting task loss, changes or new duplicates before commit. Its 69 focused local regressions pass; fresh same-source generation and independent acceptance have completed at 0/5; the user waived that result as a publication prerequisite.
29
+ [Publication evidence](../../docs/research/cli/evidence/release138-public/README.md) · [Open VSX release](https://github.com/chainlesschain/chainlesschain/actions/runs/41316653600) · [Current model results](../../docs/research/cli/verify01-openai-138/README.md) · [Upgrade guide](https://docs.chainlesschain.com/chainlesschain/cli-ide-update-2026-11-02-138.html).
14
30
 
15
- The recorded CLI .137 candidate round completed at **0/5**; CLI .138 has not started another model cohort. Source projection now preserves proven JavaScript source expressions while retaining strict standalone/decoded JSON and sensitive-field checks under ruleset v9. General coding guidance uses bounded assertions for large binary fixtures. Known remaining limits include late independent-review scheduling, omitted unproven partial source fragments, and incorrect or incomplete model-generated assertions. Two independently valid candidate suites still failed end-to-end task completion; their checks do not count as successful model tasks. [Raw results and publication authorization](../../docs/research/cli/verify01-openai-137/README.md).
31
+ Main also contains the later standalone WebSocket host-model-policy repair (`2e97ce3d79`), awaiting its own fully validated CLI release. The published CLI 0.166.138 remains bound to its verified release source.
16
32
 
17
- Local validation: 109 related regressions pass; restoring the released .136 guard makes seven new controls fail. The prepared pairing is Open VSX **0.37.174** / JetBrains **0.4.190**, both recommending CLI **0.166.138**, released only after the CLI is verified public. Until then the older public IDEs remain compatible. VS Code also preserves provider diagnostics and saved tool identity across reload; no child-package update is required. Full exact-source CLI CI and CLI Strict Sandbox must pass before Actions OIDC publication. Historical model acceptance remains **41/45**; the focused image follow-up remains **0/1** and is not rescored by engineering checks. [Evidence](../../docs/research/cli/verify01-openai-136/read-delivery-repair/README.md).
33
+ <!-- release138-current:end -->
18
34
 
19
- ## CLI 0.166.136 — verified public repair
35
+ ## CLI 0.166.136 — previous public repair
20
36
 
21
37
  Install with `npm i -g chainlesschain@0.166.136 --registry https://registry.npmjs.org`, restart the chat host, then run `cc --version` and `cc doctor`. This release repairs full-history session forks and preserves independent plugin/MCP restrictions through background restore. Malformed policy values fail before normalization or customization loading.
22
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chainlesschain",
3
- "version": "0.166.138",
3
+ "version": "0.166.139",
4
4
  "description": "CLI for ChainlessChain - install, configure, and manage your personal AI management system",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,5 +6,5 @@
6
6
  "imageDigest": "sha256:6395a47a5bd9b038303e94a5e93754258c8480dab64635460f973eb93dc6f4d8",
7
7
  "bytes": 778808,
8
8
  "linkage": "static",
9
- "commit": "e42ea07b6da7ab2c3eb709002448b32d5fedf535"
9
+ "commit": "eec8b97b6040d19004670bfead0bec1b8a27b71b"
10
10
  }
@@ -6,5 +6,5 @@
6
6
  "imageDigest": "sha256:f162e55ad3f3f72f8a52e9d7f12b5b8db44506c2ca66a14d7d62d5cd4f12498e",
7
7
  "bytes": 934160,
8
8
  "linkage": "static",
9
- "commit": "e42ea07b6da7ab2c3eb709002448b32d5fedf535"
9
+ "commit": "eec8b97b6040d19004670bfead0bec1b8a27b71b"
10
10
  }
@@ -1,6 +1,19 @@
1
1
  {
2
2
  "source": "CHANGELOG.md",
3
3
  "releases": [
4
+ {
5
+ "productVersion": null,
6
+ "cliVersion": "0.166.139",
7
+ "cliVersions": ["0.166.139"],
8
+ "date": "2026-11-02",
9
+ "title": "",
10
+ "sections": [
11
+ {
12
+ "heading": "Fixed - cc CLI 0.166.139: complete author work and independent review within one budget (candidate, 2026-11-02)",
13
+ "body": "- Reserve fresh independent inspection, repair, read-back and final reporting inside the original model-turn limit, including when the author continues calling tools. Unknown usage and insufficient remaining review capacity fail explicitly; last-turn tool calls cannot become a successful terminal result.\n- Recover complete surrounding JavaScript statements from the same file snapshot when a partial page cannot be projected. Preserve the original requested range and actual returned range, the existing JSON/source validation, provider delivery accounting and output metadata headroom.\n- Preserve focused explicit read ranges instead of silently substituting an unrelated unread section. Broad truncated scans retain bounded cursor progress.\n- Pair CLI 0.166.139 with Open VSX 0.37.175 and JetBrains 0.4.192 after complete exact-source release gates and verified upstream publication. SDK 0.2.17 and Kernel 0.1.11 remain unchanged and must pass upstream reuse checks.\n- Retest only verify-08 and verify-36 with the new package set, keeping independent review and the original budget. Prior CLI .138 result remains 2/4; previously accepted samples are not repeated. These engineering repairs do not claim new real-model success."
14
+ }
15
+ ]
16
+ },
4
17
  {
5
18
  "productVersion": null,
6
19
  "cliVersion": "0.166.138",
@@ -388,6 +388,22 @@ export class ChainlessChainWSServer extends EventEmitter {
388
388
  );
389
389
  this.resolveEvolutionCompositionFactory =
390
390
  options.resolveEvolutionCompositionFactory || null;
391
+ // Standalone embedders use the same host-owned optional Evolution policy
392
+ // as cc ui. Explicit policy hooks/factories (including null) still win.
393
+ if (
394
+ options.prepareUiModelTurn === undefined &&
395
+ options.evolutionCompositionFactory === undefined &&
396
+ options.resolveEvolutionCompositionFactory === undefined
397
+ ) {
398
+ this.prepareUiModelTurn = async (context) => {
399
+ const { prepareConfiguredUiModelTurn } = await import(
400
+ "../../lib/evolution/optional-ui-model-turn.js"
401
+ );
402
+ return prepareConfiguredUiModelTurn(context, {
403
+ cwd: this.projectRoot,
404
+ });
405
+ };
406
+ }
391
407
  this.runtimeAdmissionDependencies =
392
408
  captureSkillRuntimeDependencies(options);
393
409
  this.skillOutcomeIndex =
@@ -140,6 +140,57 @@ function pendingCalls(messages) {
140
140
  * unmarked system messages must already be trusted host instructions.
141
141
  * This journal neither replaces durable history nor grants any authority.
142
142
  */
143
+ export function changeReviewRequestReserve(limit) {
144
+ // Inspection, repair, read-back and a terminal account share the original
145
+ // budget. Tiny runs retain at least one author request, never extra credit.
146
+ return Number.isFinite(limit) ? Math.min(4, Math.max(0, limit - 1)) : 4;
147
+ }
148
+
149
+ export function changeReviewBudgetNotice(budget, reviewing) {
150
+ const available = budget.remaining() + 1;
151
+ if (reviewing)
152
+ return available === 1
153
+ ? " Independent change review: this is the final model request. Give a concise terminal account based on observations already obtained. Report unresolved requirements as incomplete. A tool call now cannot receive another model response and will not count as completion."
154
+ : " Independent change review is active. Inspect current files, repair concrete requirement gaps, then read back the repair. Reserve the final request for a concise terminal account of results and limitations; further exploration cannot extend this budget.";
155
+ const reserve = changeReviewRequestReserve(budget.limit);
156
+ const authorRequests = Math.max(0, available - reserve);
157
+ return (
158
+ ` Author allocation: ${authorRequests} requests remain before the ${reserve}-request independent change-review reserve (including this request when positive). Write the deliverable early; author completion is provisional. Review needs fresh inspection and a terminal response and does not receive extra requests. ` +
159
+ (authorRequests === 0
160
+ ? "The preferred author allocation is exhausted; further inspection without a delivered change may leave insufficient budget for review."
161
+ : "The runtime will hand changed files to independent review when the reserve is reached.")
162
+ );
163
+ }
164
+
165
+ export function changeReviewBudgetInsufficientError(remaining) {
166
+ const error = new Error(
167
+ `Independent change review requires at least two remaining model requests for fresh inspection and a terminal response; only ${remaining} remain. The task has not completed independent review.`,
168
+ );
169
+ error.code = "CC_CHANGE_REVIEW_BUDGET_INSUFFICIENT";
170
+ return error;
171
+ }
172
+
173
+ export function canReserveChangeReview({
174
+ enabled,
175
+ changed,
176
+ requested,
177
+ usageKnown,
178
+ pendingMutations,
179
+ pendingChildren,
180
+ remaining,
181
+ limit,
182
+ }) {
183
+ return (
184
+ enabled === true &&
185
+ changed === true &&
186
+ !requested &&
187
+ usageKnown !== false &&
188
+ pendingMutations === 0 &&
189
+ pendingChildren === 0 &&
190
+ remaining <= changeReviewRequestReserve(limit)
191
+ );
192
+ }
193
+
143
194
  export class CodingChangeReviewContext {
144
195
  #initial;
145
196
  #journal = [];
@@ -11,7 +11,7 @@ export const CODING_TASK_QUALITY_GUIDANCE = `Coding task quality:
11
11
  - Trace rejection before asserting its aftermath: it may precede registration or persistence. Inspect whether state stays unchanged, a failed record exists or an event is emitted. Compare independently recorded identities and provenance; equal text does not prove ownership.
12
12
  - Inspect persistence projections before comparing histories. Host notices, lineage and imports may accompany user/assistant messages; verify their provenance separately. Report defects that cannot be repaired within the permitted scope instead of inventing passing behavior.
13
13
  - Run focused checks with permitted execution tools. With file tools only, inspect imports, contracts and fixtures and say tests were not run. Read-back or static inspection is not a passing test.
14
- - Finish requested edits and read-back before the final reply; reserve the last available model request for a concise terminal account. For large files, write a small complete section, await its result, then extend it with a unique edit anchor rather than rewriting the growing file. Report delivery, actual checks and limitations once.`;
14
+ - Finish requested edits and read-back before the final reply; reserve the last available model request for a concise terminal account. If an independent review allocation is announced, finish the author deliverable within the author allocation: the review must inspect current files, repair gaps and report within the same total budget. Do not spend the review allocation on additional author discovery. For large files, write a small complete section, await its result, then extend it with a unique edit anchor rather than rewriting the growing file. Report delivery, actual checks and limitations once.`;
15
15
 
16
16
  export const HERMETIC_CODING_SYSTEM_PROMPT = `You are a hermetic coding assistant. The exposed file tools are the complete tool surface. Inspect and mutate only the exact paths authorized by the user request. Do not invoke hidden tools, commands, language servers or other execution paths.
17
17
 
@@ -267,11 +267,9 @@ export class ReadFileLoopGuard {
267
267
  const covered = entry.spans.some(
268
268
  ([start, end]) => start <= span.start && end >= span.end,
269
269
  );
270
- let targeted =
271
- Number(args.offset) > 0 &&
272
- Number(args.limit) > 0 &&
273
- Number(args.limit) <= 200 &&
274
- !page.truncated;
270
+ const explicitRange =
271
+ Number(args.offset) > 0 && Number(args.limit) > 0 && !page.truncated;
272
+ let targeted = explicitRange;
275
273
  const coveredPrefix = entry.spans.some(
276
274
  ([start, end]) => start <= span.start && end > span.start,
277
275
  );
@@ -327,7 +325,7 @@ export class ReadFileLoopGuard {
327
325
  if (restoring) page.readRecovery.fileVersion = fileVersion;
328
326
  } else {
329
327
  const unread = entry.spans[0]?.[0] === 0 ? entry.spans[0][1] : 0;
330
- if (unread < span.total) {
328
+ if (unread < span.total && !explicitRange) {
331
329
  const requestedRange = page.range;
332
330
  page = continuePage(unread);
333
331
  if (page.error || !page.readSpan) return page;
@@ -340,7 +338,12 @@ export class ReadFileLoopGuard {
340
338
  } else {
341
339
  const confirmedEnd =
342
340
  entry.confirmedSpans[0]?.[0] === 0 ? entry.confirmedSpans[0][1] : 0;
343
- if (this.visiblePages !== null && confirmedEnd < span.total) {
341
+ if (
342
+ this.visiblePages !== null &&
343
+ !entry.confirmedSpans.some(
344
+ ([start, end]) => start <= span.start && end >= span.end,
345
+ )
346
+ ) {
344
347
  return {
345
348
  path: filePath,
346
349
  fileVersion,
@@ -358,7 +361,7 @@ export class ReadFileLoopGuard {
358
361
  hashed: args.hashed === true,
359
362
  alreadyRead: true,
360
363
  contentOmitted: true,
361
- reachedEnd: true,
364
+ reachedEnd: confirmedEnd >= span.total,
362
365
  readProgress: { newContent: false },
363
366
  readRecovery: { action: "use-findings" },
364
367
  ...(entry.summary?.outline
@@ -366,12 +369,12 @@ export class ReadFileLoopGuard {
366
369
  : {}),
367
370
  recoveryRead: {
368
371
  path: filePath,
369
- offset: 1,
372
+ offset: explicitRange ? page.range.startLine : 1,
370
373
  limit: 80,
371
374
  ...(args.hashed === true ? { hashed: true } : {}),
372
375
  ...(args.raw === true ? { raw: true } : {}),
373
376
  },
374
- hint: "The unchanged file was read earlier, but historical coverage does not mean its source is visible now. Use any source excerpts actually present in this request. For missing details, use search_files or read_file with a specific offset and limit (at most 80 lines); recoveryRead is a bounded starting range, not permission to restart a full scan. Reading the file is not task completion.",
377
+ hint: "This requested range was read earlier, but historical coverage does not mean its source is visible now. The runtime has not substituted an unrelated file range. Use source actually present in this request; recover missing details with a focused search or the bounded recoveryRead. Reading the file is not task completion.",
375
378
  };
376
379
  }
377
380
  }
@@ -1,3 +1,5 @@
1
+ import { recoverCompleteSourceRead } from "./source-read-recovery.js";
2
+
1
3
  /** Build a bounded read result with a cursor describing what was actually read. */
2
4
  export function buildReadFilePage(
3
5
  rendered,
@@ -61,7 +63,7 @@ export function buildReadFilePage(
61
63
  const charEnd = truncated
62
64
  ? charStart + count
63
65
  : lines.slice(0, end).reduce((n, line) => n + line.length + 1, 0);
64
- return {
66
+ const page = {
65
67
  path: filePath,
66
68
  ...(fileVersion ? { fileVersion } : {}),
67
69
  ...(offset || limit || column > 1 || truncated
@@ -96,6 +98,7 @@ export function buildReadFilePage(
96
98
  hashed: args.hashed === true,
97
99
  content,
98
100
  };
101
+ return recoverCompleteSourceRead(page, rendered, args, maxChars);
99
102
  }
100
103
 
101
104
  /** A bounded navigation index, not a substitute for reading the cited lines. */
@@ -0,0 +1,89 @@
1
+ import { parse } from "acorn";
2
+ import { canProjectHostToolExcerpt } from "./tool-result-preview.js";
3
+
4
+ /**
5
+ * A cut source container is not safe evidence. Recover its complete top-level
6
+ * statements from the very same read snapshot, with explicit range metadata.
7
+ * This does not exempt source from either projection or provider sanitization.
8
+ */
9
+ export function recoverCompleteSourceRead(page, source, args, maxChars) {
10
+ if (
11
+ typeof page?.content !== "string" ||
12
+ typeof page.fileVersion !== "string" ||
13
+ !/\.(?:[cm]?js)$/iu.test(page.path || "") ||
14
+ args.hashed === true ||
15
+ !page.range ||
16
+ !page.readSpan ||
17
+ source.length > 512 * 1024 ||
18
+ canProjectHostToolExcerpt(page.content, "read_file")
19
+ )
20
+ return page;
21
+
22
+ let program;
23
+ let tokens = 0;
24
+ try {
25
+ program = parse(source, {
26
+ ecmaVersion: "latest",
27
+ sourceType: "module",
28
+ allowReturnOutsideFunction: true,
29
+ onToken() {
30
+ if (++tokens > 32768)
31
+ throw new RangeError("Source recovery token bound");
32
+ },
33
+ });
34
+ } catch {
35
+ // Malformed or oversized source retains the ordinary omission boundary.
36
+ return page;
37
+ }
38
+ const statements = program.body.filter(
39
+ (node) => node.start < page.readSpan.end && node.end > page.readSpan.start,
40
+ );
41
+ if (!statements.length) return page;
42
+ const start = Math.min(
43
+ page.readSpan.start,
44
+ source.lastIndexOf("\n", statements[0].start - 1) + 1,
45
+ );
46
+ const followingNewline = source.indexOf("\n", statements.at(-1).end);
47
+ const end = Math.max(
48
+ page.readSpan.end,
49
+ followingNewline < 0 ? source.length : followingNewline + 1,
50
+ );
51
+ const content = source.slice(start, end);
52
+ if (
53
+ content === page.content ||
54
+ !canProjectHostToolExcerpt(content, "read_file")
55
+ )
56
+ return page;
57
+ const startLine = 1 + (source.slice(0, start).match(/\n/gu) || []).length;
58
+ const endLine =
59
+ startLine +
60
+ (content.match(/\n/gu) || []).length -
61
+ (content.endsWith("\n") ? 1 : 0);
62
+ const recovered = {
63
+ ...page,
64
+ content,
65
+ range: { startLine, endLine, totalLines: page.range.totalLines },
66
+ readSpan: { start, end, total: source.length },
67
+ sourceRecovery: {
68
+ action: "complete-source-container",
69
+ fileVersion: page.fileVersion,
70
+ requestedRange: { ...page.range },
71
+ requestedSpan: { ...page.readSpan },
72
+ note: "The requested excerpt cut a source container. These complete surrounding statements come from the same file version; the range and readSpan describe the content actually returned.",
73
+ },
74
+ };
75
+ delete recovered.truncated;
76
+ delete recovered.size;
77
+ delete recovered.nextRead;
78
+ delete recovered.hint;
79
+ if (end < source.length) {
80
+ recovered.nextRead = { ...args, offset: endLine + 1, column: 1 };
81
+ recovered.hint = "Continue with nextRead only if more source is needed.";
82
+ }
83
+ // The read guard, host telemetry and delivery envelope still add metadata.
84
+ // Reuse the ordinary page's headroom: an almost-full recovery would be
85
+ // projected back to an omission by the final cap before reaching a provider.
86
+ return JSON.stringify(recovered).length <= Math.max(0, maxChars - 2048)
87
+ ? recovered
88
+ : page;
89
+ }
@@ -33,7 +33,7 @@ const PREVIEW_HINT =
33
33
  "Tool output truncated. Narrow the request: filter/count locally, select fields, search_files, or read_file with offset/limit. Omitted text is not full-output evidence.";
34
34
 
35
35
  /** Check only host-created excerpts; original uncut tool text still reaches ingress. */
36
- function completeExcerpt(text, tool) {
36
+ export function canProjectHostToolExcerpt(text, tool) {
37
37
  const scan = createEvidenceJsonTextBoundary((message) => new Error(message), {
38
38
  ...EVIDENCE_JSON_POLICY,
39
39
  sourceLiteralPolicy: "explicit-source-containers-v1",
@@ -239,7 +239,8 @@ export function safeHostToolResultForModel(tool, result) {
239
239
  let safe = result;
240
240
  for (const field of fields) {
241
241
  const text = result[field];
242
- if (typeof text !== "string" || completeExcerpt(text, tool)) continue;
242
+ if (typeof text !== "string" || canProjectHostToolExcerpt(text, tool))
243
+ continue;
243
244
  if (safe === result) safe = { ...result };
244
245
  safe[field] =
245
246
  tool === "read_file"
@@ -268,7 +269,10 @@ export function boundedCommandOutput(output, max, tool = "run_code") {
268
269
  output,
269
270
  Math.max(1, Math.floor(max / 2)),
270
271
  );
271
- if (JSON.stringify(preview).length <= max && completeExcerpt(preview, tool))
272
+ if (
273
+ JSON.stringify(preview).length <= max &&
274
+ canProjectHostToolExcerpt(preview, tool)
275
+ )
272
276
  return preview;
273
277
  } else if (!/[{}\[\]"']/u.test(output)) {
274
278
  let low = 0;
@@ -77,7 +77,12 @@ import {
77
77
  CODING_TASK_QUALITY_GUIDANCE,
78
78
  CODING_CHANGE_REVIEW_PROMPT,
79
79
  } from "../lib/coding-task-guidance.js";
80
- import { CodingChangeReviewContext } from "../lib/coding-change-review-context.js";
80
+ import {
81
+ CodingChangeReviewContext,
82
+ changeReviewBudgetNotice,
83
+ changeReviewBudgetInsufficientError,
84
+ canReserveChangeReview,
85
+ } from "../lib/coding-change-review-context.js";
81
86
  import { RemoteReadLoopGuard } from "../lib/remote-read-loop-guard.js";
82
87
  import {
83
88
  isPrActionRequest,
@@ -15608,6 +15613,37 @@ export async function* agentLoop(messages, options) {
15608
15613
  toolContext.readFileLoopGuard = readFileLoopGuard;
15609
15614
  let lastTaskProgressIntervention = null;
15610
15615
 
15616
+ // Both an author completion and the reserved review window enter the same
15617
+ // phase. Neither path manufactures an author response or adds request budget.
15618
+ const beginChangeReview = (draft = null) => {
15619
+ throwIfAborted(signal);
15620
+ if (budget.remaining() < 2)
15621
+ throw changeReviewBudgetInsufficientError(budget.remaining());
15622
+ if (independentReviewSetupError) throw independentReviewSetupError;
15623
+ changeReviewState.requested = true;
15624
+ const reviewPrompt = markRuntimeOnlySessionMessage({
15625
+ role: "user",
15626
+ content: CODING_CHANGE_REVIEW_PROMPT,
15627
+ });
15628
+ if (draft)
15629
+ appendMessages(
15630
+ markRuntimeOnlySessionMessage({
15631
+ role: "assistant",
15632
+ content: draft.content || "",
15633
+ }),
15634
+ );
15635
+ appendMessages(reviewPrompt);
15636
+ if (independentReviewContext) {
15637
+ independentReviewContext.activate(reviewPrompt);
15638
+ changeReviewState.independentReviewPending = true;
15639
+ readFileLoopGuard = new ReadFileLoopGuard();
15640
+ remoteReadLoopGuard = new RemoteReadLoopGuard();
15641
+ taskProgressTracker = new TaskProgressTracker();
15642
+ toolContext.readFileLoopGuard = readFileLoopGuard;
15643
+ lastTaskProgressIntervention = null;
15644
+ }
15645
+ };
15646
+
15611
15647
  while (budget.hasRemaining()) {
15612
15648
  if (loopMcpCatalog) {
15613
15649
  currentMcpSnapshot = readMcpToolCatalog(loopMcpCatalog);
@@ -15654,6 +15690,24 @@ export async function* agentLoop(messages, options) {
15654
15690
  return;
15655
15691
  }
15656
15692
  readFileLoopGuard.finishBatch();
15693
+ if (
15694
+ canReserveChangeReview({
15695
+ enabled: options.reviewChanges,
15696
+ changed: hasFileContentChanges,
15697
+ requested: changeReviewState.requested,
15698
+ usageKnown: changeReviewState.usageKnown,
15699
+ pendingMutations: pendingSubtreeMutations.size,
15700
+ pendingChildren: backgroundSubAgents.size,
15701
+ remaining: budget.remaining(),
15702
+ limit: budget.limit,
15703
+ })
15704
+ ) {
15705
+ beginChangeReview();
15706
+ yield {
15707
+ type: "iteration-warning",
15708
+ message: "Reviewing changed files against the task before finishing.",
15709
+ };
15710
+ }
15657
15711
  if (remoteReadLoopGuard.stalled && !remoteReadLoopGuard.synthesisRequired) {
15658
15712
  await _awaitBackgroundUsageSettlement(
15659
15713
  backgroundSubAgents,
@@ -16585,7 +16639,10 @@ export async function* agentLoop(messages, options) {
16585
16639
  `Execution budget: this request plus ${furtherRequests} further model requests. ` +
16586
16640
  "For requested changes, reserve requests for implementation, focused validation, and repair or an accurate unfinished-work report. " +
16587
16641
  "Reading or an existing passing baseline does not deliver requested changes. " +
16588
- "For research or monitoring, use the remaining requests for evidence and the requested answer; do not create files just to show activity.",
16642
+ "For research or monitoring, use the remaining requests for evidence and the requested answer; do not create files just to show activity." +
16643
+ (options.reviewChanges === true
16644
+ ? changeReviewBudgetNotice(budget, changeReviewState.requested)
16645
+ : ""),
16589
16646
  },
16590
16647
  ];
16591
16648
  contextMemoryTrustedSystemIndexes.push(callMessages.length - 1);
@@ -16988,7 +17045,7 @@ export async function* agentLoop(messages, options) {
16988
17045
  }
16989
17046
  throwIfAborted(signal);
16990
17047
  if (
16991
- independentReviewContext?.active &&
17048
+ options.reviewChanges === true &&
16992
17049
  changeReviewState.usageKnown === false
16993
17050
  ) {
16994
17051
  const error = new Error(
@@ -17247,42 +17304,14 @@ export async function* agentLoop(messages, options) {
17247
17304
  };
17248
17305
  return;
17249
17306
  }
17250
- // Opt-in review uses the same loop, tools, usage settlement and budget.
17251
- // Never emit a provisional completion or turn this self-check into an
17252
- // acceptance verdict. Failed/deferred writes do not arm the review.
17307
+ // Author completion is provisional until the independent review phase.
17253
17308
  if (
17254
17309
  options.reviewChanges === true &&
17255
17310
  hasFileContentChanges &&
17256
17311
  !changeReviewState.requested &&
17257
- changeReviewState.usageKnown !== false &&
17258
- budget.hasRemaining()
17312
+ changeReviewState.usageKnown !== false
17259
17313
  ) {
17260
- throwIfAborted(signal);
17261
- if (independentReviewSetupError) throw independentReviewSetupError;
17262
- changeReviewState.requested = true;
17263
- const reviewPrompt = markRuntimeOnlySessionMessage({
17264
- role: "user",
17265
- content: CODING_CHANGE_REVIEW_PROMPT,
17266
- });
17267
- appendMessages(
17268
- markRuntimeOnlySessionMessage({
17269
- role: "assistant",
17270
- content: msg.content || "",
17271
- }),
17272
- reviewPrompt,
17273
- );
17274
- if (independentReviewContext) {
17275
- independentReviewContext.activate(reviewPrompt);
17276
- changeReviewState.independentReviewPending = true;
17277
- // These are phase-local discovery hints, not permission or spend
17278
- // authorities. Fresh inspection must not inherit author assertions,
17279
- // old visible file pages, or a pause caused by author exploration.
17280
- readFileLoopGuard = new ReadFileLoopGuard();
17281
- remoteReadLoopGuard = new RemoteReadLoopGuard();
17282
- taskProgressTracker = new TaskProgressTracker();
17283
- toolContext.readFileLoopGuard = readFileLoopGuard;
17284
- lastTaskProgressIntervention = null;
17285
- }
17314
+ beginChangeReview(msg);
17286
17315
  yield {
17287
17316
  type: "iteration-warning",
17288
17317
  message: "Reviewing changed files against the task before finishing.",