sortie-dogs 0.12.24 → 0.13.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.
@@ -3,5 +3,5 @@
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
5
  export declare const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
6
- export declare const V010_RUNTIME_ASSET_VERSION = "0.12.24-review-evidence-recovery-v1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.13.0-fast-first-v1";
7
7
  export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION;
@@ -3,4 +3,4 @@
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
5
  export const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
6
- export const V010_RUNTIME_ASSET_VERSION = "0.12.24-review-evidence-recovery-v1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.13.0-fast-first-v1";
@@ -133,10 +133,10 @@ function classifyVersionTransition(installedValue, currentValue, v010Restore = f
133
133
  // cross-minor migration; skipped lines still fail closed instead of bypassing migration steps.
134
134
  const adjacentPreOneLine = installed.major === 0 && current.major === 0 &&
135
135
  current.minor === installed.minor + 1;
136
- // v0.11 used a different runtime. This explicit v010 asset migration restores
137
- // the 0.10 line directly into 0.12 without selecting any v0.11 execution path.
136
+ // v0.11 used a different runtime. The v010 asset migration restores the 0.10
137
+ // preview into the 0.12/0.13 Mission line without selecting a v0.11 execution path.
138
138
  const restoredPreview = v010Restore && installed.major === 0 && installed.minor === 10 &&
139
- current.major === 0 && current.minor === 12;
139
+ current.major === 0 && (current.minor === 12 || current.minor === 13);
140
140
  return sameLine || adjacentPreOneLine || restoredPreview ? "compatible-update" : "incompatible";
141
141
  }
142
142
  async function metadata(path) {
@@ -174,7 +174,7 @@ export declare class OperatorMissionRuntime {
174
174
  constructor(projectRoot: string, profile: RuntimeProfile);
175
175
  private file;
176
176
  private load;
177
- /** Recover the predecessor link for missions started before this fix, using one exact archived mission. */
177
+ /** Recover non-replacement continuations only; an explicit replacement must link the current cancelled run. */
178
178
  private loadMission;
179
179
  /** Find exactly one archived cancelled mission that owned this run; ambiguity never grants recovery. */
180
180
  archivedRun(root: string, runID: string): Promise<OperatorMission | undefined>;
@@ -217,5 +217,5 @@ export declare function missionPlan(mission: OperatorMission, raw: unknown): Ope
217
217
  export declare function missionPacket(mission: OperatorMission, run?: OperatorState): Record<string, unknown>;
218
218
  /** Models forward a short capability, never recopy the host's evidence hashes and source packet. */
219
219
  export declare function missionReviewTask(mission: OperatorMission): OperatorTask;
220
- /** Project explicitly grouped R-ID traces into the legacy reviewer's ordered criterion mapping. */
220
+ /** Project grouped R-ID traces, including consecutive ranges, into the existing per-criterion mapping. */
221
221
  export declare function missionReviewTraces(mission: OperatorMission, raw: unknown): string[];
@@ -113,10 +113,10 @@ export class OperatorMissionRuntime {
113
113
  throw error;
114
114
  }
115
115
  }
116
- /** Recover the predecessor link for missions started before this fix, using one exact archived mission. */
116
+ /** Recover non-replacement continuations only; an explicit replacement must link the current cancelled run. */
117
117
  async loadMission(root) {
118
118
  const state = await this.load(this.file(root));
119
- if (!state || state.supersededRunID !== undefined || state.runID !== null ||
119
+ if (!state || state.supersededRunID !== undefined || state.runID !== null || state.requirementsReplaced ||
120
120
  !["open", "running", "submitted", "cancelled"].includes(state.phase) || state.requests.length === 0)
121
121
  return state;
122
122
  const directory = join(this.projectRoot, this.profile.stateDirectory, "missions");
@@ -241,10 +241,11 @@ export class OperatorMissionRuntime {
241
241
  }
242
242
  if (previous)
243
243
  await this.save(this.file(root, `.${previous.id}`), previous);
244
- // A cancelled standalone/legacy run has no mission-owned runID. Only an explicit
245
- // replacement may link it; normal continuation must not silently inherit its goal.
246
- const predecessor = previous?.runID ?? previous?.supersededRunID ??
247
- (replaceRequirements ? options.cancelledRunID : undefined);
244
+ // The Operator runtime's current cancelled run is authoritative for an explicit
245
+ // replacement. A no-run mission can still carry an older ancestor's run ID.
246
+ // Ordinary continuation retains that predecessor when no current run is supplied.
247
+ const predecessor = (replaceRequirements ? options.cancelledRunID : undefined) ??
248
+ previous?.runID ?? previous?.supersededRunID;
248
249
  const state = { version: "0.12", id: `mission-${randomUUID()}`, root, requests: [request],
249
250
  kind: options.kind ?? "implementation",
250
251
  context: (options.context ?? []).filter(item => item.id !== request.id),
@@ -252,7 +253,7 @@ export class OperatorMissionRuntime {
252
253
  ...(replaceRequirements ? { requirementsReplaced: true } : {}),
253
254
  coordinator: null, callID: null, dispatchOpen: false, runID: null, plans: 0, progress: [], submission: null,
254
255
  ...((previous === undefined || previous.phase === "cancelled") &&
255
- (previous?.runID === null || previous === undefined || request.id !== previous.requests[0]?.id) && predecessor
256
+ (replaceRequirements || previous?.runID === null || previous === undefined || request.id !== previous.requests[0]?.id) && predecessor
256
257
  ? { supersededRunID: predecessor } : {}) };
257
258
  await this.save(this.file(root), state);
258
259
  return state;
@@ -505,19 +506,35 @@ export function missionReviewTask(mission) {
505
506
  return { ...review.task, prompt: `${MISSION_REVIEW_REFERENCE}${JSON.stringify({ r: mission.root,
506
507
  m: mission.id, n: review.runID, h: digest(review.task.prompt) })}` };
507
508
  }
508
- /** Project explicitly grouped R-ID traces into the legacy reviewer's ordered criterion mapping. */
509
+ /** Project grouped R-ID traces, including consecutive ranges, into the existing per-criterion mapping. */
509
510
  export function missionReviewTraces(mission, raw) {
510
511
  if (!Array.isArray(raw) || raw.length === 0 || !raw.every(item => typeof item === "string" && item.trim())) {
511
512
  throw new Error("mission-review-traces: supply concise implementation/test traces");
512
513
  }
513
514
  const grouped = raw.map(text => ({ text,
514
- ids: [...text.matchAll(/(?:^|[.;]\s+|[\r\n])\s*((?:R\d+\s*[/,]?\s*)+):/gu)]
515
- .flatMap(match => match[1].match(/R\d+/gu) ?? []) }));
515
+ ids: [...text.matchAll(/(?:^|[.;]\s+|[\r\n])\s*((?:R\d+\s*[-/,]?\s*)+):/gu)]
516
+ .flatMap(match => [...match[1].matchAll(/R(\d+)(?:\s*-\s*R(\d+))?/gu)]
517
+ .flatMap(([, first, last]) => {
518
+ const firstID = `R${first}`;
519
+ if (last === undefined)
520
+ return [firstID];
521
+ const lastID = `R${last}`;
522
+ const start = mission.requirements.findIndex(item => item.id === firstID);
523
+ const end = mission.requirements.findIndex(item => item.id === lastID);
524
+ if (start < 0 || end < 0)
525
+ return [firstID, lastID];
526
+ if (start > end)
527
+ throw new Error(`mission-review-traces: descending range ${firstID}-${lastID}`);
528
+ return mission.requirements.slice(start, end + 1).map(item => item.id);
529
+ })) }));
516
530
  if (grouped.every(item => item.ids.length === 0) && raw.length === mission.requirements.length)
517
531
  return raw;
518
532
  const unknown = grouped.flatMap(item => item.ids).filter(id => !mission.requirements.some(requirement => requirement.id === id));
519
533
  const missing = mission.requirements.filter(requirement => !grouped.some(item => item.ids.includes(requirement.id)));
520
534
  if (unknown.length || missing.length)
521
- throw new Error(`mission-review-traces: name the existing R IDs; missing ${missing.map(item => item.id).join(", ")}; unknown ${unknown.join(", ")}`);
535
+ throw new Error(`mission-review-traces: name the existing R IDs; ${[
536
+ ...(missing.length ? [`missing ${missing.map(item => item.id).join(", ")}`] : []),
537
+ ...(unknown.length ? [`unknown ${unknown.join(", ")}`] : []),
538
+ ].join("; ")}`);
522
539
  return mission.requirements.map(requirement => grouped.filter(item => item.ids.includes(requirement.id)).map(item => item.text).join("\n"));
523
540
  }
@@ -920,11 +920,11 @@ export class OperatorRuntime {
920
920
  `goal_declaration_path: ${declarationPath}`, "acceptance:", ...plan.acceptance.map(value => ` - ${value}`),
921
921
  "validation:", ...unit.validation.map(value => ` - ${value}`),
922
922
  ...(mission ? ["Read handoff_path and other repository files using their project-relative paths as supplied. The native working directory is project_root; do not prepend or reconstruct its absolute path for read/search/shell. Copy project_root only when binding the write gate. Preserve explicitly declared external paths."] : []),
923
- mission ? "Read-only investigation commands are unrestricted. Use shell to reproduce and diagnose without asking for command registration. Keep all writes, including generated/transient outputs and cleanup, inside unit.write. Run formal validation exactly as listed, in order and in separate calls, so the host records its real result. If a write scope or formal check must change, return the precise change to your Coordinator; it can extend/redeclare immediately within the original requirements. Diagnostic success is not formal acceptance evidence."
923
+ mission ? "Read-only investigation commands are unrestricted. Use shell to reproduce and diagnose without asking for command registration. Keep all writes, including generated/transient outputs and cleanup, inside unit.write. Run formal validation exactly as listed, in order and in separate calls, so the host records its real result. If a write scope or formal check must change, return the precise change to your parent Operator or Coordinator; it can extend/redeclare immediately within the original requirements. Diagnostic success is not formal acceptance evidence."
924
924
  : "Execute validation in its declared order. Earlier entries may be approved generator, build, formatter, or exact cleanup commands required before canonical criterion tests. Every persistent or transient generator output must be declared in unit.write. Cleanup may remove only declared unit.write outputs and must be an explicit ordered command after generation and before post-commit or canonical validation; never add an ignore rule or remove an undeclared path. If any necessary command, input, output, or cleanup is missing, do not run an undeclared command or variant and do not use resume evidence tooling to invent permission; return a contract-repair decision.",
925
925
  ...(mission ? [] : ["Preserve existing public API success and error return semantics unless acceptance explicitly changes them, and cover those compatibility boundaries in the declared validation."]),
926
926
  mission
927
- ? "Complete the assigned work and listed validation within this Task. For a known operation, proceed through setup, execution and result collection; preparation alone is not execution. Preserve existing public behavior when changing source. Do not spawn nested subagents. The Coordinator handles any applicable independent review after your return; review is not a prerequisite to execution. Return actual results, including failed or not-started operations, and any exact contract correction needed."
927
+ ? "Complete the assigned work and listed validation within this Task. For a known operation, proceed through setup, execution and result collection; preparation alone is not execution. Preserve existing public behavior when changing source. Do not spawn nested subagents. The parent Operator or Coordinator handles any applicable independent review after your return; review is not a prerequisite to execution. Return actual results, including failed or not-started operations, and any exact contract correction needed."
928
928
  : "Do not spawn nested subagents for consultation. Required consultations belong to the root before dispatch; use the confirmed decisions and evidence declared in the unit objective and inputs. If required consultation results or user decisions are missing, return the exact contract gap to the parent instead of attempting a deeper Task, inventing consent, or asking the user to repeat an already recorded decision.",
929
929
  ...(plan.git_lifecycle !== undefined && index === plan.units.length - 1 ? [
930
930
  `git_post_commit_validation: ${JSON.stringify(plan.git_lifecycle.post_commit_validation)}`,
@@ -1564,6 +1564,11 @@ export class OperatorRuntime {
1564
1564
  state.units.some(candidate => candidate.terminalRescue))
1565
1565
  throw new Error("operator-mission-normal-remediation-unavailable");
1566
1566
  await this.verifyControls(unit);
1567
+ // The ordinary retry keeps its exact scope and validation, but the next Worker needs the
1568
+ // host-observed failed command/exit rather than reconstructing it from an earlier session.
1569
+ unit.task = { ...unit.task, prompt: `${unit.task.prompt}\nnormal_remediation_prior_validation: ${JSON.stringify({
1570
+ command: unit.failure.command, exit_code: unit.failure.exitCode
1571
+ })}` };
1567
1572
  unit.normalRemediationUsed = true;
1568
1573
  unit.status = "pending";
1569
1574
  unit.callID = null;
@@ -6060,8 +6060,12 @@ export const SortieDogsPlugin = async (input, options) => {
6060
6060
  appLogInfo("run-metrics.career-unavailable", textInput.sessionID, { outcome: runOutcome }, "warn");
6061
6061
  }
6062
6062
  }
6063
- if (sortieResult !== undefined)
6064
- textOutput.text = insertSortieResult(textOutput.text, sortieResult);
6063
+ if (sortieResult !== undefined) {
6064
+ const review = terminal?.receipt?.status === "succeeded"
6065
+ ? await input.runtimeBridge?.missionReviewPresentation?.(textInput.sessionID).catch(() => undefined)
6066
+ : undefined;
6067
+ textOutput.text = insertSortieResult(textOutput.text, sortieResult, review?.verdict, review?.evidenceGaps);
6068
+ }
6065
6069
  else if (metrics !== undefined && runOutcome === "DONE")
6066
6070
  textOutput.text = insertRunMetrics(textOutput.text, metrics);
6067
6071
  const { debrief: _debriefObservation, ...metricSummary } = metrics ?? {};
@@ -7445,7 +7449,7 @@ export const SortieDogsPlugin = async (input, options) => {
7445
7449
  },
7446
7450
  };
7447
7451
  input.runtimeBridge?.connected?.({
7448
- renderReturnReport: async (root, text, expectedReceipt) => {
7452
+ renderReturnReport: async (root, text, expectedReceipt, missionReview, reviewEvidenceGaps) => {
7449
7453
  let rendered;
7450
7454
  await serializeChatTransition(root, async () => {
7451
7455
  if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
@@ -7473,7 +7477,7 @@ export const SortieDogsPlugin = async (input, options) => {
7473
7477
  catch {
7474
7478
  appLogInfo("run-metrics.career-unavailable", root, { profile: runtimeProfile.id }, "warn");
7475
7479
  }
7476
- rendered = insertSortieResult(receiptBoundTerminalText(text, receipt), result);
7480
+ rendered = insertSortieResult(receiptBoundTerminalText(text, receipt), result, missionReview, reviewEvidenceGaps);
7477
7481
  });
7478
7482
  return rendered;
7479
7483
  },
@@ -1,6 +1,17 @@
1
1
  import type { OperatorState } from "../core/operator-runtime.js";
2
2
  import { type MissionEvidenceExcerpt, type MissionReviewScope, type OperatorMission } from "../core/operator-mission.js";
3
3
  import { type RuntimeProfile } from "../core/runtime-profile.js";
4
+ /** Show native command outcomes to the Reviewer without turning non-criterion checks into acceptance evidence. */
5
+ export declare function observedMissionValidation(validation: readonly string[], childSessionID: string | null, history: readonly Record<string, unknown>[]): {
6
+ attempts: readonly {
7
+ command: string;
8
+ exit_code: number | null;
9
+ started_ms: number | null;
10
+ completed_ms: number | null;
11
+ }[];
12
+ not_observed: readonly string[];
13
+ omitted_attempts: number;
14
+ };
4
15
  export declare function missionReviewBaseline(directory: string): Promise<string | undefined>;
5
16
  /** A child ID alone cannot establish the initial phase after a restart or failed dispatch. */
6
17
  export declare function initialMissionReviewPrompt(mission: OperatorMission, prompts: readonly string[]): string | undefined;
@@ -14,4 +25,5 @@ export declare function missionReviewSource(directory: string, run: OperatorStat
14
25
  fingerprint: string;
15
26
  excerpt: string;
16
27
  truncatedEvidence: string[];
28
+ truncatedSource: string[];
17
29
  }>;
@@ -11,8 +11,36 @@ import { canonicalAgent } from "../core/runtime-profile.js";
11
11
  import { taskChildSessionID } from "./task-result-repair.js";
12
12
  import { normalizeManifestScope } from "../core/path.js";
13
13
  import { declaredArtifacts } from "./declared-artifacts.js";
14
+ import { normalizeCommand } from "./gate.js";
14
15
  const exec = promisify(execFile);
15
16
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
17
+ /** Show native command outcomes to the Reviewer without turning non-criterion checks into acceptance evidence. */
18
+ export function observedMissionValidation(validation, childSessionID, history) {
19
+ const declared = validation.map(normalizeCommand), expected = new Set(declared);
20
+ const time = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
21
+ const attempts = childSessionID === null ? [] : history.flatMap(message => {
22
+ if (!record(message.info) || message.info.role !== "assistant" || message.info.sessionID !== childSessionID ||
23
+ !Array.isArray(message.parts))
24
+ return [];
25
+ return message.parts.flatMap(part => {
26
+ if (!record(part) || part.type !== "tool" || !["bash", "shell", "powershell", "pwsh"].includes(String(part.tool)) ||
27
+ !record(part.state) || part.state.status !== "completed" || !record(part.state.input) ||
28
+ typeof part.state.input.command !== "string")
29
+ return [];
30
+ const command = normalizeCommand(part.state.input.command);
31
+ if (!expected.has(command))
32
+ return [];
33
+ const exit = record(part.state.metadata) ? part.state.metadata.exit : undefined;
34
+ return [{ command, exit_code: typeof exit === "number" && Number.isSafeInteger(exit) ? exit : null,
35
+ started_ms: record(part.time) ? time(part.time.ran) : null,
36
+ completed_ms: record(part.time) ? time(part.time.completed) : null }];
37
+ });
38
+ });
39
+ // Retain early attempts and the latest checks if a Worker retried many times.
40
+ const shown = attempts.length > 32 ? [...attempts.slice(0, 8), ...attempts.slice(-24)] : attempts;
41
+ return { attempts: shown, not_observed: declared.filter(command => !attempts.some(item => item.command === command)),
42
+ omitted_attempts: attempts.length - shown.length };
43
+ }
16
44
  /** Use the space left by short references for longer requested branches, without starving later references. */
17
45
  function focusedAllowances(sizes, budget) {
18
46
  const allowances = sizes.map(() => 0);
@@ -65,19 +93,22 @@ export function initialMissionReviewPrompt(mission, prompts) {
65
93
  }
66
94
  /** Recover from native completion records, never from the pending review's inherited child ID. */
67
95
  export async function completedMissionReviewPrompts(mission, profile, root, requestedPrompt, host) {
68
- if (!mission || mission.root !== root || !mission.coordinator || ["completed", "cancelled"].includes(mission.phase) ||
96
+ if (!mission || mission.root !== root || ["completed", "cancelled"].includes(mission.phase) ||
69
97
  mission.review?.task?.prompt !== requestedPrompt)
70
98
  return [];
71
99
  // The completion hook has already bound this prompt to a real independent Reviewer child.
72
100
  // Avoid a second V2 history read when its page/list API is temporarily unavailable.
73
101
  if (initialMissionReviewPrompt(mission, []))
74
102
  return [mission.review.initialPrompt];
75
- const coordinator = await host.get(mission.coordinator);
76
- if (!record(coordinator) || coordinator.parentID !== root || canonicalAgent(profile, coordinator.agent) !== "dog-operator")
77
- return [];
103
+ const owner = mission.coordinator ?? root;
104
+ if (mission.coordinator !== null) {
105
+ const coordinator = await host.get(owner);
106
+ if (!record(coordinator) || coordinator.parentID !== root || canonicalAgent(profile, coordinator.agent) !== "dog-operator")
107
+ return [];
108
+ }
78
109
  const prompts = [];
79
- for (const message of (await host.messages(mission.coordinator)).slice(-1000)) {
80
- if (!record(message.info) || message.info.role !== "assistant" || message.info.sessionID !== mission.coordinator ||
110
+ for (const message of (await host.messages(owner)).slice(-1000)) {
111
+ if (!record(message.info) || message.info.role !== "assistant" || message.info.sessionID !== owner ||
81
112
  !Array.isArray(message.parts))
82
113
  continue;
83
114
  for (const part of message.parts) {
@@ -89,7 +120,7 @@ export async function completedMissionReviewPrompts(mission, profile, root, requ
89
120
  if (!childID)
90
121
  continue;
91
122
  const child = await host.get(childID);
92
- if (!record(child) || child.parentID !== mission.coordinator || canonicalAgent(profile, child.agent) !== "dog-reviewer")
123
+ if (!record(child) || child.parentID !== owner || canonicalAgent(profile, child.agent) !== "dog-reviewer")
93
124
  continue;
94
125
  let prompt = part.state.input.prompt;
95
126
  if (prompt.startsWith(MISSION_REVIEW_REFERENCE)) {
@@ -218,7 +249,7 @@ export async function missionReviewSource(directory, run, evidence = [], baselin
218
249
  if (writes.length === 0)
219
250
  return { fingerprint: `sha256:${hash.digest("hex")}`,
220
251
  excerpt: selected + "[Read-only units: no declared output files. Review the supplied observations, traces and validation evidence.]",
221
- truncatedEvidence };
252
+ truncatedEvidence, truncatedSource: [] };
222
253
  // The shared dependency environment is local tooling, never reviewed or pinned source.
223
254
  const external = [], local = [];
224
255
  for (const path of writes) {
@@ -280,7 +311,8 @@ export async function missionReviewSource(directory, run, evidence = [], baselin
280
311
  const absolute = resolve(directory, path), stat = await lstat(absolute);
281
312
  hash.update(String(stat.mode));
282
313
  const include = untracked.has(path) || unchanged;
283
- const room = include ? Math.max(0, 24_000 - Buffer.byteLength(excerpt)) : 0;
314
+ const heading = `\n--- ${untracked.has(path) ? "new file" : "current file"}: ${path} ---\n`;
315
+ const room = include ? Math.max(0, 24_000 - Buffer.byteLength(excerpt) - Buffer.byteLength(heading)) : 0;
284
316
  let preview = Buffer.alloc(0);
285
317
  if (stat.isSymbolicLink()) {
286
318
  const content = Buffer.from(await readlink(absolute));
@@ -297,9 +329,27 @@ export async function missionReviewSource(directory, run, evidence = [], baselin
297
329
  }
298
330
  }
299
331
  if (include) {
300
- if (room)
301
- excerpt += `\n--- ${untracked.has(path) ? "new file" : "current file"}: ${path} ---\n${preview.includes(0) ? "[binary artifact: bytes fingerprinted]" : preview.toString("utf8")}`;
302
- if (stat.size > room)
332
+ const headingFits = Buffer.byteLength(excerpt) + Buffer.byteLength(heading) <= 24_000;
333
+ if (headingFits) {
334
+ const rendered = preview.includes(0) ? "[binary artifact: bytes fingerprinted]" : preview.toString("utf8");
335
+ let shown = rendered;
336
+ if (Buffer.byteLength(rendered) > room) {
337
+ let used = 0;
338
+ const chars = [];
339
+ for (const char of rendered) {
340
+ const bytes = Buffer.byteLength(char);
341
+ if (used + bytes > room)
342
+ break;
343
+ chars.push(char);
344
+ used += bytes;
345
+ }
346
+ shown = chars.join("");
347
+ }
348
+ excerpt += heading + shown;
349
+ if (stat.size > room || Buffer.byteLength(rendered) > room)
350
+ omitted.push(path);
351
+ }
352
+ else
303
353
  omitted.push(path);
304
354
  }
305
355
  }
@@ -321,8 +371,11 @@ export async function missionReviewSource(directory, run, evidence = [], baselin
321
371
  unreadable.push(...artifacts.unreadable);
322
372
  }
323
373
  const bytes = Buffer.from(excerpt);
374
+ const truncatedSource = [...new Set(omitted)];
375
+ if (bytes.length > 24_000 && truncatedSource.length === 0)
376
+ truncatedSource.push("(source diff exceeds excerpt budget)");
324
377
  return { fingerprint: `sha256:${hash.digest("hex")}`, excerpt: (bytes.length > 24_000 ? bytes.subarray(0, 24_000).toString("utf8") : excerpt) +
325
- (bytes.length > 24_000 || omitted.length ? `\n[EXCERPT TRUNCATED: ${omitted.slice(0, 20).join(", ")}; supply focused traces for missing sections, not another implementation unit]` : "") +
378
+ (truncatedSource.length ? `\n[EXCERPT TRUNCATED: ${truncatedSource.slice(0, 20).join(", ")}; supply focused traces for missing sections, not another implementation unit]` : "") +
326
379
  (unreadable.length ? `\n[UNINSPECTED EXTERNAL DIRECTORIES: ${unreadable.slice(0, 20).join(", ")}; select specific result files as review evidence]` : ""),
327
- truncatedEvidence };
380
+ truncatedEvidence, truncatedSource };
328
381
  }