@fro.bot/systematic 3.18.2 → 3.18.4

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.
@@ -47,6 +47,25 @@ export interface ReviewReturnValidatorOptions {
47
47
  readonly outputSink?: (message: string) => void;
48
48
  readonly errorSink?: (message: string) => void;
49
49
  }
50
+ export type StdinRead = {
51
+ readonly status: 'ok';
52
+ readonly buffer: Buffer;
53
+ } | {
54
+ readonly status: 'oversized';
55
+ } | {
56
+ readonly status: 'read-error';
57
+ };
58
+ export declare function defaultReadChunk(fd: number, buffer: Buffer, offset: number, length: number, position: number | null): number;
59
+ /**
60
+ * Read stdin in bounded chunks, stopping at the cap plus one byte so an
61
+ * oversized payload is rejected without buffering the whole document.
62
+ *
63
+ * `maxBytes` defaults to {@link MAX_REVIEW_RETURN_BYTES} (the `return`/`screen`
64
+ * 1 MiB cap); callers with a different bound -- such as `prepare`'s larger
65
+ * aggregate envelope cap -- pass it explicitly. The default preserves
66
+ * `return` and `screen`'s existing behavior byte-for-byte.
67
+ */
68
+ export declare function readBoundedStdin(fd: number, readChunk: ReadChunk, maxBytes?: number): StdinRead;
50
69
  /**
51
70
  * Validate exactly one raw persona return from stdin.
52
71
  *
package/dist/pi.js CHANGED
@@ -3549,7 +3549,7 @@ var BUNDLED_SKILL_NAMES = [
3549
3549
  "writing-skills"
3550
3550
  ];
3551
3551
 
3552
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/core/util.js
3552
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/util.js
3553
3553
  function getEnumValues(entries) {
3554
3554
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
3555
3555
  const values = Object.entries(entries).filter(([k, _]) => numericValues.indexOf(+k) === -1).map(([_, v]) => v);
@@ -4095,7 +4095,7 @@ function constantCatch(value) {
4095
4095
  return fn;
4096
4096
  }
4097
4097
 
4098
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/core/core.js
4098
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/core.js
4099
4099
  var _a;
4100
4100
  var _zodDesc = { value: undefined, enumerable: false };
4101
4101
  var _E = "captureStackTrace" in Error ? Error : null;
@@ -4214,7 +4214,7 @@ function config(newConfig) {
4214
4214
  Object.assign(globalConfig, newConfig);
4215
4215
  return globalConfig;
4216
4216
  }
4217
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/core/errors.js
4217
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/errors.js
4218
4218
  function _getMessage() {
4219
4219
  const internals = this._zod;
4220
4220
  internals.message ?? (internals.message = JSON.stringify(internals.def, jsonStringifyReplacer, 2));
@@ -4329,7 +4329,7 @@ function formatError(error, mapper = (issue) => issue.message) {
4329
4329
  return fieldErrors;
4330
4330
  }
4331
4331
 
4332
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/core/parse.js
4332
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/parse.js
4333
4333
  function finalizeParams(callee, params) {
4334
4334
  return { callee: params?.callee ?? callee, Err: params?.Err };
4335
4335
  }
@@ -4476,7 +4476,7 @@ var _safeEncodeAsync = (_Err) => async (schema, value, _ctx) => {
4476
4476
  var _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => {
4477
4477
  return _safeParseAsync(_Err)(schema, value, _ctx);
4478
4478
  };
4479
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/core/regexes.js
4479
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/regexes.js
4480
4480
  var cuid = /^[cC][0-9a-z]{6,}$/;
4481
4481
  var cuid2 = /^[0-9a-z]+$/;
4482
4482
  var ulid = /^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$/;
@@ -4534,7 +4534,7 @@ var boolean = /^(?:true|false)$/i;
4534
4534
  var lowercase = /^[^A-Z]*$/;
4535
4535
  var uppercase = /^[^a-z]*$/;
4536
4536
 
4537
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/core/checks.js
4537
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/checks.js
4538
4538
  var $ZodCheck = /* @__PURE__ */ $constructor("$ZodCheck", (inst, def) => {
4539
4539
  var _a;
4540
4540
  inst._zod ?? (inst._zod = {});
@@ -4850,7 +4850,7 @@ var $ZodCheckOverwrite = /* @__PURE__ */ $constructor("$ZodCheckOverwrite", (ins
4850
4850
  };
4851
4851
  });
4852
4852
 
4853
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/core/doc.js
4853
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/doc.js
4854
4854
  class Doc {
4855
4855
  constructor(args = [], closed = {}) {
4856
4856
  this.content = [];
@@ -4892,14 +4892,14 @@ ${content.join(`
4892
4892
  }
4893
4893
  }
4894
4894
 
4895
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/core/versions.js
4895
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/versions.js
4896
4896
  var version = {
4897
4897
  major: 4,
4898
4898
  minor: 6,
4899
- patch: 1
4899
+ patch: 2
4900
4900
  };
4901
4901
 
4902
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/core/schemas.js
4902
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/schemas.js
4903
4903
  var $ZodType = /* @__PURE__ */ $constructor("$ZodType", (inst, def) => {
4904
4904
  var _a;
4905
4905
  inst ?? (inst = {});
@@ -5496,7 +5496,7 @@ function handlePropertyResult(result, final, key, input, optin, optout) {
5496
5496
  return;
5497
5497
  }
5498
5498
  if (result.value === undefined) {
5499
- if (isPresent) {
5499
+ if (isPresent || optin === "defaulted" && !isOptionalOut) {
5500
5500
  final.value[key] = undefined;
5501
5501
  }
5502
5502
  } else {
@@ -5730,16 +5730,16 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
5730
5730
  doc.write(`
5731
5731
  if (${id}.issues.length) {${prefixStr(id, k)}
5732
5732
  }
5733
-
5734
- if (${id}.value === undefined) {
5735
- if (${isPresent}) {
5736
- newResult[${k}] = undefined;
5737
- }
5733
+ `);
5734
+ if (optin === "defaulted") {
5735
+ doc.write(`newResult[${k}] = ${id}.value;`);
5738
5736
  } else {
5737
+ doc.write(`
5738
+ if (${id}.value !== undefined || ${isPresent}) {
5739
5739
  newResult[${k}] = ${id}.value;
5740
5740
  }
5741
-
5742
5741
  `);
5742
+ }
5743
5743
  }
5744
5744
  }
5745
5745
  doc.write(`payload.value = newResult;`);
@@ -6463,7 +6463,7 @@ var $ZodProperties = /* @__PURE__ */ $constructor("$ZodProperties", (inst, def)
6463
6463
  yield this;
6464
6464
  }
6465
6465
  });
6466
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/core/memoizer.js
6466
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/memoizer.js
6467
6467
  class $ZodCyclicError extends Error {
6468
6468
  constructor() {
6469
6469
  super(`Cannot parse a reference cycle that closes through a transform`);
@@ -6735,7 +6735,7 @@ function isBackEdge(ctx, value) {
6735
6735
  const backEdges = ctx[STATE]?.backEdges;
6736
6736
  return backEdges !== undefined && isRef(value) && backEdges.has(value);
6737
6737
  }
6738
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/locales/en.js
6738
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/locales/en.js
6739
6739
  var error = () => {
6740
6740
  const Sizable = {
6741
6741
  string: { unit: "characters", verb: "to have" },
@@ -6856,7 +6856,7 @@ function en_default() {
6856
6856
  localeError: error()
6857
6857
  };
6858
6858
  }
6859
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/core/registries.js
6859
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/registries.js
6860
6860
  var _a2;
6861
6861
  class $ZodRegistry {
6862
6862
  constructor() {
@@ -6903,7 +6903,7 @@ function registry() {
6903
6903
  }
6904
6904
  (_a2 = globalThis).__zod_globalRegistry ?? (_a2.__zod_globalRegistry = registry());
6905
6905
  var globalRegistry = globalThis.__zod_globalRegistry;
6906
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/core/api.js
6906
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/api.js
6907
6907
  function _string(Class, params) {
6908
6908
  return new Class({
6909
6909
  type: "string",
@@ -7353,7 +7353,7 @@ function _check(fn, params) {
7353
7353
  ch._zod.check = fn;
7354
7354
  return ch;
7355
7355
  }
7356
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/core/to-json-schema.js
7356
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/to-json-schema.js
7357
7357
  function assignProps(target, ...sources) {
7358
7358
  for (const source of sources) {
7359
7359
  for (const key of Reflect.ownKeys(source)) {
@@ -7876,7 +7876,7 @@ var createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) =
7876
7876
  extractDefs(ctx, schema);
7877
7877
  return finalize(ctx, schema);
7878
7878
  };
7879
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/core/json-schema-processors.js
7879
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/json-schema-processors.js
7880
7880
  var narrowMin = (agg, key, value) => {
7881
7881
  if (agg[key] === undefined || value > agg[key])
7882
7882
  agg[key] = value;
@@ -8403,7 +8403,7 @@ var optionalProcessor = (schema, ctx, _json, params) => {
8403
8403
  const seen = ctx.seen.get(schema);
8404
8404
  seen.ref = def.innerType;
8405
8405
  };
8406
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/classic/errors.js
8406
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/classic/errors.js
8407
8407
  var _installedErrorProtos = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]);
8408
8408
  function _lazyMethod(proto, key, make) {
8409
8409
  Object.defineProperty(proto, key, {
@@ -8448,7 +8448,7 @@ var ZodRealError = /* @__PURE__ */ $constructor("ZodError", initializer2, undefi
8448
8448
  Parent: Error
8449
8449
  });
8450
8450
 
8451
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/classic/parse.js
8451
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/classic/parse.js
8452
8452
  var parse2 = /* @__PURE__ */ _parse(ZodRealError);
8453
8453
  var parseAsync = /* @__PURE__ */ _parseAsync(ZodRealError);
8454
8454
  var safeParse = /* @__PURE__ */ _safeParse(ZodRealError);
@@ -8462,7 +8462,7 @@ var safeDecode = /* @__PURE__ */ _safeDecode(ZodRealError);
8462
8462
  var safeEncodeAsync = /* @__PURE__ */ _safeEncodeAsync(ZodRealError);
8463
8463
  var safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync(ZodRealError);
8464
8464
 
8465
- // node_modules/.bun/zod@4.6.1/node_modules/zod/v4/classic/schemas.js
8465
+ // node_modules/.bun/zod@4.6.2/node_modules/zod/v4/classic/schemas.js
8466
8466
  function _ensureDefaultLocale() {
8467
8467
  if (!globalConfig.localeError)
8468
8468
  config(en_default());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fro.bot/systematic",
3
- "version": "3.18.2",
3
+ "version": "3.18.4",
4
4
  "description": "Compound-engineering loops for OpenCode, Pi, and Claude Code",
5
5
  "type": "module",
6
6
  "homepage": "https://fro.bot/systematic",
@@ -97,7 +97,7 @@
97
97
  }
98
98
  },
99
99
  "devDependencies": {
100
- "@biomejs/biome": "2.5.12",
100
+ "@biomejs/biome": "2.5.13",
101
101
  "@earendil-works/pi-coding-agent": "0.83.0",
102
102
  "@opencode-ai/plugin": "1.18.30",
103
103
  "@opencode-ai/sdk": "1.18.30",
@@ -122,7 +122,7 @@
122
122
  "jsonc-parser": "^3.3.0",
123
123
  "tree-sitter-bash": "0.25.1",
124
124
  "web-tree-sitter": "0.27.0",
125
- "zod": "4.6.1"
125
+ "zod": "4.6.2"
126
126
  },
127
127
  "overrides": {
128
128
  "@earendil-works/pi-ai": "0.83.0",
@@ -55,7 +55,7 @@ All tokens are optional. Each one present means one less thing to infer. When ab
55
55
 
56
56
  - **Skip all user questions.** Infer intent conservatively if the diff metadata is thin.
57
57
  - **Never edit files or externalize work.** Do not write `.context/systematic/ce-review/<run-id>/`, do not create todo files, and do not commit, push, or create a PR.
58
- - **Report-only runs in memory.** Run raw-return structural validation, environment screening, synthesis, and reporting without writing a run directory, artifact, or ignore file.
58
+ - **Report-only runs in memory.** Run raw-return structural validation, synthesis, and reporting without writing a run directory, artifact, or ignore file.
59
59
  - **Safe for parallel read-only verification.** `mode:report-only` is the only mode that is safe to run concurrently with browser testing on the same checkout.
60
60
  - **Do not switch the shared checkout.** If the caller passes an explicit PR or branch target, `mode:report-only` must run in an isolated checkout/worktree or stop instead of running `gh pr checkout` / `git checkout`.
61
61
  - **Do not overlap mutating review with browser testing on the same checkout.** If a future orchestrator wants fixes, run the mutating review phase after browser testing or in an isolated checkout/worktree.
@@ -484,104 +484,75 @@ Returning the detail tier inline increases parent context per persona. The previ
484
484
 
485
485
  **CE conditional agents** (deployment-verification-agent) are also dispatched as standard Agent calls when applicable. Pass the same review context bundle plus the selection reason and triggering surface (for example, which migration files triggered the agent). Their output is unstructured and must be preserved for Stage 6 synthesis just like the other CE conditional agents.
486
486
 
487
- #### Raw return admission (all modes)
487
+ #### Screen each return (all modes)
488
488
 
489
- Before parsing a persona return into fields, screening it for environment values, assessing evidence, synthesizing, or persisting anything, admit it with the packaged structural validator. Invoke the validator through this skill's own installed directory so every harness resolves the same committed bytes:
490
-
491
- ```bash
492
- # Resolve the validator relative to this skill's directory.
493
- SKILL_DIR="<skill directory stated when this skill loads>";
494
- node "$SKILL_DIR/scripts/validate-review.mjs" return <<'REVIEW_RETURN_A1B2C3D4'
495
- <the persona's returned JSON payload, copied verbatim>
496
- REVIEW_RETURN_A1B2C3D4
497
- ```
498
-
499
- Before each invocation, choose a fresh delimiter for that exact raw payload over a safe token alphabet (`A-Z`, `0-9`, `_`), for example a random hex token. Verify the delimiter is absent as a complete line in that exact raw payload before running. The `REVIEW_RETURN_A1B2C3D4` token above is only an illustration; never reuse a fixed delimiter, and choose a new token for every payload. Open the heredoc with a single-quoted heredoc opener (`<<'DELIM'`) so the payload is never interpolated, and close it with a line containing exactly that delimiter. Feed the payload on stdin (never as a command argument) so it cannot appear in argv or a process listing; never use unquoted interpolation or command substitution to pass the payload, and never write it to a temp file. This block is self-contained for one-block execution: each fenced block re-assigns `SKILL_DIR` and terminates the assignment with `;`.
489
+ Before parsing a persona return into fields, assessing evidence, synthesizing, or persisting anything, admit it with the packaged structural validator's `screen` phase. `screen` replaces the former separate raw-return-admission and dispatch-identity-binding steps with one call: it structurally admits the return and binds it to the dispatched persona in the same pass. Invoke it through this skill's own installed directory (`node "$SKILL_DIR/scripts/validate-review.mjs" screen --reviewer <persona> --harness <opencode|pi|claude-code>`, the raw return on stdin through a fresh single-quoted heredoc delimiter) so every harness resolves the same committed bytes. The full envelope shape, the fresh-delimiter rules, and the invocation block are in [pipeline invocation: screen](./references/pipeline-invocation.md#screen).
500
490
 
501
491
  Read the exit status:
502
492
 
503
- - **exit 0** — structurally admitted. Parse the already structurally validated JSON without logging the raw text, then run the existing environment-value screen unchanged over that parsed object before persistence; only after parsing and a clean screen may the parent add `harness`, `dispatch_outcome`, and finding `disposition`. `exit 0` with zero findings is `dispatch_outcome: "empty"`; `exit 0` with findings is `dispatch_outcome: "findings"`.
504
- - **exit 1** — the whole return is `dispatch_outcome: "malformed"`. Retain only the bounded validator diagnostics in Coverage; never parse, screen, or persist its payload fields or values.
493
+ - **exit 0** — structurally admitted. Parse the already structurally validated JSON without logging the raw text; the parent adds `harness` and `dispatch_outcome` from the result to the persisted per-agent dispatch record. `exit 0` with zero findings is `dispatch_outcome: "empty"`; `exit 0` with findings is `dispatch_outcome: "findings"`, each finding already carrying a stable `input_id` and `disposition: "surviving"`.
494
+ - **exit 1** — the whole return is `dispatch_outcome: "malformed"`, and the parent must never parse or persist its payload fields or values; retain only the bounded validator diagnostic in Coverage. This covers malformed JSON, a schema violation, and a dispatch identity mismatch alike.
505
495
  - **exit 2**, a missing or unreadable helper, or a command launch failure — validation unavailable. Withhold the return and report the exact unavailability and what was withheld. Update that selected persona's preinitialized dispatch entry from `never_returned` to `dispatch_outcome: "validation_unavailable"` with `input_finding_count: 0` and, optionally, a safe `rejection_reason` naming the exit status, missing helper, or launch failure without payload values; set `run_status` to `degraded`. A run that contains `validation_unavailable` evidence can never finalize as `completed`, and that persona must not have an admitted input finding. Never omit the dispatch entry, never leave it as `never_returned`, never label it `malformed`, never admit the payload, and never fabricate a reviewer record or a rejected-summary ledger row. The word `unavailable` also names the artifact-level self-validation status, a different object and phase; never repurpose the artifact-level `validation` fields.
506
496
 
507
- **Dispatch identity binding.** Structural admission does not prove who produced a return. Immediately after `exit 0` and before the environment-value screen, persistence, or synthesis, parse the admitted return's `reviewer` field and confirm it equals the dispatched persona. A return whose `reviewer` does not match the dispatched persona is an identity mismatch: reject the whole return as `dispatch_outcome: "malformed"`, record only a bounded rejection reason naming the expected persona, set `run_status` to `degraded`, and do not admit, screen, persist, or synthesize its payload. The stdin-only, argument-free validator cannot see the dispatch identity, so this comparison stays the parent's responsibility.
497
+ **Dispatch identity binding.** `screen` confirms the returned `reviewer` field matches the dispatched persona before it admits anything; an identity mismatch is rejected as `dispatch_outcome: "malformed"` and degrades the run. This comparison happens inside the same exit 0/exit 1 decision above, before the parent parses a single finding field -- structural admission never proves identity on its own.
508
498
 
509
499
  A task that did not return is `never_returned`: a task-lifecycle fact recorded without invoking the validator. Validation unavailable is not malformed and is not never_returned; they are distinct coverage states. The public `systematic validate-review-return` command is an operator/development fallback selected before invocation, never a fallback chosen because a validator run exited 1 or 2.
510
500
 
511
- Structural validity never implies evidence validity. A return that passes the validator is admitted structurally only; its claims still require evidence assessment, and a wrong-checkout or unsupported citation remains unverified until current-target evidence resolves it.
512
-
513
- ### Stage 5: Merge findings
501
+ Structural validity never implies evidence validity. A return that passes `screen` is admitted structurally only. The parent must still assess evidence for its claims, and only then add parent annotations, persist, or synthesize -- a wrong-checkout or unsupported citation remains unverified until current-target evidence resolves it.
514
502
 
515
- The parent-owned artifact and its reconciliation rules are defined in the [synthesis artifact contract](./references/synthesis-artifact-contract.md). The stages below describe when synthesis decisions are made.
503
+ A helper failure is never permission to hand-synthesize an admitted finding, a dispatch outcome, or a rejected-summary row; see [Never-bypass](./references/pipeline-invocation.md#never-bypass) for the retry and correction protocol every phase shares.
516
504
 
517
- Convert multiple reviewer JSON returns into one deduplicated, confidence-gated finding set. Each persona return already contains both tiers. The parent must retain the validated payload in memory for merge and synthesis, then persist only the same validated data.
505
+ ### Stage 5: Merge findings
518
506
 
519
- Before applying the confidence gate, keep the parent-owned ledger through every later stage. See the [synthesis artifact contract](./references/synthesis-artifact-contract.md) for the input-ID and reconciliation rules.
507
+ The parent-owned artifact and its reconciliation rules are defined in the [synthesis artifact contract](./references/synthesis-artifact-contract.md). Stage 5 no longer computes the confidence gate, candidate grouping, cross-reviewer agreement, route narrowing, partitioning, sorting, or coverage union by hand -- the `prepare` and `merge` phases of the packaged validator compute all of it deterministically. The model's remaining job is adjudication: deciding, for each file's candidate group, which findings describe the same underlying defect, and proposing each merged finding's narrative and route.
520
508
 
521
- 1. **Validate before any write.** Treat every persona return as untrusted input. The order is fixed: the packaged raw validator (Stage 4's Raw return admission) must exit 0 before the parent parses anything. Then parse the already structurally validated JSON without logging the raw text, run the unchanged environment-value screen over that parsed object, assess evidence, and only then add parent annotations, persist, or synthesize. The executable validator already enforces `references/findings-schema.json` (including `why_it_matters` and `evidence`), so confirming the parsed object is a cross-check, not the admission gate. On exit 1 the return is `malformed`: do not parse, screen, or persist it.
522
- - **Top-level required:** reviewer (string), findings (array), residual_risks (array), testing_gaps (array). Reject the entire persona return if any are missing or wrong type.
523
- - **Per-finding required:** title, severity, file, line, why_it_matters, confidence, evidence, autofix_class, owner, requires_verification, pre_existing.
524
- - **Schema constraints:** enforce every enum, type, confidence, line, path, evidence count, evidence length, and explicit overflow-marker bound from the schema. Empty evidence, absolute paths, and over-bound evidence are rejection cases, not truncation cases.
525
- - **Environment-value detection:** JSON Schema cannot determine where a string came from, so recursively inspect every string leaf in the parsed payload before writing. Apply the environment-value matching, structural-detector, and finding-granularity rules in the [synthesis artifact contract](./references/synthesis-artifact-contract.md). This detector is an additional parent-side check, not a schema claim.
526
- - **Safe rejection message:** Use the safe rejection message rule in the [synthesis artifact contract](./references/synthesis-artifact-contract.md); never include the offending value, raw return, or validator parameters.
527
- - **No partial writes:** Do not write or merge a finding until it passes the parent-side validation rules. Apply the admitted-finding persistence and rejected-payload ledger rules in the [synthesis artifact contract](./references/synthesis-artifact-contract.md). A valid admitted finding is then annotated by the parent with `harness` and `dispatch_outcome` and written by the parent only. Revalidate the enriched record before persistence.
528
- - **Dispatch outcome:** Record the parent-owned dispatch outcomes and ledger dispositions according to the [synthesis artifact contract](./references/synthesis-artifact-contract.md); keep dispatch outcomes separate from finding dispositions.
529
- - **Rejection policy: degrade, do not fail the whole review.** Continue merging conforming returns when a persona or finding is rejected; record the rejection and apply the risk-aware verdict according to the [synthesis artifact contract](./references/synthesis-artifact-contract.md). If every persona fails or times out, use the existing degraded-review behavior.
530
- 2. **Confidence gate.** Suppress findings below 0.60 confidence. Exception: P0 findings at 0.50+ confidence survive the gate -- critical-but-uncertain issues must not be silently dropped. Record the suppressed finding's original confidence and an explicit reason in the input ledger. A retained P0 at 0.50+ is recorded as `surviving` unless it later participates in a deduplication merge. This matches the persona instructions and the schema's confidence thresholds.
531
- 3. **Deduplicate.** Group all gated findings by `normalize(file)`. A file with two or more findings from different personas forms a candidate group. Do not use line number to form groups. Sort findings within each group by line. Adjudicate each candidate group: merge findings judged to describe the same underlying defect; keep genuinely different defects separate. Adjacency creates a candidate, not a conclusion -- findings on the same line that describe different defects must not merge. For each resulting finding, derive the fingerprint from its file and line as `normalize(file) + "|" + line`. Keep highest severity, keep highest confidence, and retain the input IDs that produced the merged entry. Record each declined merge in the artifact's optional `declined_merges` field with the normalized file, input finding IDs considered but not merged, and a brief reason. A singleton that passes the gate is `surviving`; each input in a multi-input merge is provisionally `merged`.
509
+ 1. **Assemble every screen result.** Collect the `screen` output for every persona that returned (including `malformed`, `never_returned`, and `validation_unavailable` entries), plus the Stage 3 selection record (`selected_dispatches`), into the `prepare` input envelope. See [pipeline invocation: prepare](./references/pipeline-invocation.md#prepare) for the exact shape and invocation.
510
+ 2. **Run `prepare`.** It applies the confidence gate (suppress below 0.60, except P0 at 0.50+ survives), groups admitted findings into candidate groups by `normalize(file)` (never by line), sorts each group's members by line, and unions selection-surface coverage. Its output's `singletons` need no adjudication; its `candidate_groups` do.
511
+ 3. **Adjudicate every candidate group.** For each group, decide whether its members describe the same underlying defect (merge) or genuinely different defects (decline). Adjacency creates a candidate, not a conclusion -- findings on the same line describing different defects must stay declined. For a merge decision, write the merged finding's `title`, `why_it_matters`, `evidence`, and `line`, plus (when the route should narrow) a `proposed_route` with a `route_narrowing_reason`; optionally note `disagreement_facts` when reviewers disagreed on severity/autofix_class/owner, and `eligible_agreement_credit` for personas that agree without their own input finding in the group. For a decline decision, write a `declined_reason`. Every candidate-group member must be cited by exactly one decision -- no omissions, no double-citations.
532
512
 
533
513
  Worked example: at `src/lib/model-availability.ts:139`, reliability's `Config hook awaits providers API without a timeout` and adversarial's `Config startup can hang forever behind a stalled /config` describe the same underlying defect in different words, so they merge.
534
- 4. **Cross-reviewer agreement.** When an adjudicated merge contains findings from 2+ independent reviewers, boost the merged confidence by 0.10 (capped at 1.0). Cross-reviewer agreement is strong signal -- independent reviewers converging on the same issue is more reliable than any single reviewer's confidence. Preserve the distinction in the merged finding's artifact provenance according to the [synthesis artifact contract](./references/synthesis-artifact-contract.md).
535
- 5. **Separate pre-existing.** Pull out findings with `pre_existing: true` into a separate list.
536
- 6. **Resolve disagreements.** When reviewers flag the same code region but disagree on severity, autofix_class, or owner, annotate the Reviewer column with the disagreement (e.g., "security (P0), correctness (P1) -- kept P0"). This transparency helps the user understand why a finding was routed the way it was.
537
- 7. **Normalize routing.** For each merged finding, set the final `autofix_class`, `owner`, and `requires_verification`. If reviewers disagree, keep the most conservative route. Synthesis may narrow a finding from `safe_auto` to `gated_auto` or `manual`, but must not widen it without new evidence.
538
- 8. **Partition the work.** Build three sets:
539
- - in-skill fixer queue: only `safe_auto -> review-fixer`
540
- - residual actionable queue: unresolved `gated_auto` or `manual` findings whose owner is `downstream-resolver`
541
- - report-only queue: `advisory` findings plus anything owned by `human` or `release`
542
- 9. **Sort.** Order by severity (P0 first) -> confidence (descending) -> file path -> line number.
543
- 10. **Collect coverage data.** Union residual_risks and testing_gaps across reviewers.
544
- 11. **Preserve CE agent artifacts.** Keep the outputs of the selected learnings, agent-native, schema-drift, and deployment-verification agents alongside the merged finding set. Do not drop unstructured agent output just because it does not match the persona JSON schema.
545
- 12. **Keep the input ledger complete.** Reconcile admitted findings and rejected-payload summaries according to the [synthesis artifact contract](./references/synthesis-artifact-contract.md).
514
+ 4. **Run `merge`.** It applies the adjudication envelope to `prepare`'s output, deriving each merged finding's severity, the cross-reviewer-agreement-boosted confidence (+0.10, capped at 1.0, for a merge with 2+ independent submitters), the fingerprint (`normalize(file) + "|" + line`), and the conservatively narrowed `autofix_class`/`owner`/`requires_verification` -- synthesis may narrow a route, never widen it without new evidence. It also returns `validator_requests`: the merged findings that need Stage 5b validation. See [pipeline invocation: merge](./references/pipeline-invocation.md#merge).
515
+ 5. **Preserve CE agent artifacts.** Keep the outputs of the selected learnings, agent-native, schema-drift, and deployment-verification agents alongside the merged finding set for Stage 6 rendering. Do not drop unstructured agent output just because it does not match the persona JSON schema.
516
+
517
+ A helper failure (`prepare` or `merge` exiting 1 or 2) is never permission to hand-assemble a merged finding, a route, or a confidence value; see [Never-bypass](./references/pipeline-invocation.md#never-bypass).
546
518
 
547
519
  ### Stage 5b: Validation pass
548
520
 
549
- Run an independent validation pass over the merged finding set before synthesis. This pass annotates each gated finding `validated: true` or `validated: false` with a one-sentence reason. A `validated: false` finding is dropped from the surviving/actioned set and receives disposition `filtered`, but is retained in the synthesis artifact and surfaced in the "Filtered (not validated)" group in Stage 6. It is not erased from the record.
521
+ Dispatch validators for exactly the findings `merge`'s `validator_requests` names -- the P0/P1-or-`requires_verification` gating band is already computed; Stage 5b no longer identifies the gated set by hand.
550
522
 
551
- **Gating band (default):** Validate only findings that are **P0 or P1 severity**, or that have `requires_verification: true`. Findings outside this band pass through to Stage 6 unvalidated and unfiltered no validator is dispatched for them. This bounds cost: one validator subagent per gated finding.
523
+ 1. For each entry in `validator_requests`, spawn one validator subagent in parallel using the validator template at `references/validator-template.md`. Look up that finding's full fields (title, why_it_matters, evidence, file, line, severity, autofix_class, owner, suggested_fix) from `merge`'s `merged_findings` by `finding_id`, and pass them along with the intent summary, file list, and full diff.
524
+ 2. Collect `{outcome: 'true'|'false'|'failed'|'unavailable', reason?}` from each validator, keyed by `finding_id`. `outcome: 'true'` needs no reason; the other three outcomes each require one.
525
+ 3. Carry every result forward as `validator_lifecycle_results` into `finalize` (Stage 6). Do not reconcile filtered findings, update ledger dispositions, or recompute the "Filtered (not validated)" group by hand -- `finalize` derives all of it from these results plus `merge`'s output.
552
526
 
553
- **Dispatch:**
527
+ **Outcome semantics** (enforced by `finalize`, not the model): `true` validates the finding -- it flows to Stage 6 unchanged. `false` filters the finding -- it drops out of the surviving/actioned set, receives disposition `filtered` with the validator's exact one-sentence reason, and appears in the "Filtered (not validated)" group. `failed` and `unavailable` leave the finding actionable and unvalidated (no `validated` annotation; it appears in the normal severity tables) but each records a lifecycle failure that marks the run `degraded` and blocks a clean verdict -- report the failed/unavailable validator in Coverage rather than silently treating it as validated.
554
528
 
555
- 1. Identify all gated findings from the Stage 5 merged set.
556
- 2. For each gated finding, spawn one validator subagent in parallel using the validator template at `references/validator-template.md`. Pass the finding fields, the intent summary, the file list, and the full diff.
557
- 3. Collect `{validated, reason}` from each validator. Attach both fields to the finding.
558
- 4. **Reconcile filtered inputs.** A finding with `validated: false` moves to the "Filtered (not validated)" presentation group in Stage 6, and every input ID contributing to that merged finding is updated to disposition `filtered` with the validator's exact one-sentence reason. Apply the remaining ledger rules from the [synthesis artifact contract](./references/synthesis-artifact-contract.md).
559
- 5. Findings with `validated: true` flow through to Stage 6 unchanged — they appear in the normal severity tables.
560
- 6. Findings outside the gating band carry no `validated` annotation and appear in Stage 6 severity tables unchanged.
529
+ Findings outside the gating band (no `validator_requests` entry) carry no `validated` annotation and appear in Stage 6 severity tables unchanged.
561
530
 
562
- **Failure handling:** If a validator subagent fails or times out, treat the finding as `validated: true` (conservative fallback — keep it in the actioned set) and note the validator failure in the Coverage section.
531
+ ### Stage 6: Synthesize and present
563
532
 
564
- **Output-contract compatibility:** This pass is additive. Existing severity tables in Stage 6 keep their structure, headings, and order. The "Filtered (not validated)" group is appended after the existing severity tables. Consumers relying on existing section order are unaffected new content only ever appears after existing sections.
533
+ Call `finalize` with `applied_fixes: []` to synthesize the run's report projection -- verdict, findings, coverage, disposition counts, and every queue (`fixer`, `residual`, `report_only`) -- from `merge`'s output, `prepare`'s output, the screen results, the dispatch records, the Stage 5b validator lifecycle results, and the Stage 2b plan assessment below. See [pipeline invocation: finalize](./references/pipeline-invocation.md#finalize) for the exact envelope. This first call's `report.queues` feeds the post-review action sets directly (see Step 1 under [After Review](#after-review)); a fix-applying mode calls `finalize` again after fixes land with the real `applied_fixes`, and only that second call is persisted.
565
534
 
566
- ### Stage 6: Synthesize and present
535
+ **Plan assessment.** Read the plan's Requirements Trace and Implementation Units located in Stage 2b, and check each one against the diff: met / not addressed / partially addressed. For every requirement or unit not clearly met, produce one plan-assessment result:
567
536
 
568
- Assemble the final report using **pipe-delimited markdown tables for findings** from the review output template included below. The table format is mandatory for finding rows in interactive mode do not render findings as freeform text blocks or horizontal-rule-separated prose. Other report sections (Applied Fixes, Learnings, Coverage, etc.) use bullet lists and the `---` separator before the verdict, as shown in the template.
537
+ - **`explicit_unmet_requirement`** -- the plan is `plan_source: explicit` (caller-provided or an unambiguous PR body match) and a stated requirement is unaddressed. Routes to residual actionable work and blocks a clean verdict.
538
+ - **`inferred_gap`** -- the plan is `plan_source: inferred` (auto-discovered), or the gap is a suspicion rather than a stated requirement. Routes to advisory-only output and never blocks the verdict by itself.
539
+
540
+ Neither kind becomes a finding -- `finalize` routes `results` directly into `residual_actionable_work` or `advisory_outputs` strings, never into the severity tables. Omit plan assessment entirely when no plan was found in Stage 2b -- do not mention the absence of a plan, and pass an empty `results` array so `finalize` neither fabricates a gap nor silently relaxes the verdict gate.
541
+
542
+ Assemble the final report using **pipe-delimited markdown tables for findings** from the review output template included below, rendering `finalize`'s report projection directly -- do not recompute any of the fields it already derived. The table format is mandatory for finding rows in interactive mode — do not render findings as freeform text blocks or horizontal-rule-separated prose. Other report sections (Applied Fixes, Learnings, Coverage, etc.) use bullet lists and the `---` separator before the verdict, as shown in the template.
569
543
 
570
544
  1. **Header.** Scope, intent, mode, harness, reviewer team with per-conditional justifications.
571
- 2. **Findings.** Rendered as pipe-delimited tables grouped by severity (`### P0 -- Critical`, `### P1 -- High`, `### P2 -- Moderate`, `### P3 -- Low`). Each finding row shows `#`, file, issue, reviewer(s), confidence, and synthesized route. Omit empty severity levels. Never render findings as freeform text blocks or numbered lists. Only findings with `validated: true` (or no `validated` annotation) appear in these tables.
572
- 3. **Requirements Completeness.** Include only when a plan was found in Stage 2b. For each requirement (R1, R2, etc.) and implementation unit in the plan, report whether corresponding work appears in the diff. Use a simple checklist: met / not addressed / partially addressed. Routing depends on `plan_source`:
573
- - **`explicit`** (caller-provided or PR body): Flag unaddressed requirements as P1 findings with `autofix_class: manual`, `owner: downstream-resolver`. These enter the residual actionable queue and can become todos.
574
- - **`inferred`** (auto-discovered): Flag unaddressed requirements as P3 findings with `autofix_class: advisory`, `owner: human`. These stay in the report only — no todos, no autonomous follow-up. An inferred plan match is a hint, not a contract.
575
- Omit this section entirely when no plan was found — do not mention the absence of a plan.
545
+ 2. **Findings.** Rendered as pipe-delimited tables grouped by severity (`### P0 -- Critical`, `### P1 -- High`, `### P2 -- Moderate`, `### P3 -- Low`) from `report.findings`. Each finding row shows `#`, file, issue, reviewer(s), confidence, and synthesized route. Omit empty severity levels. Never render findings as freeform text blocks or numbered lists. Only findings with `validated: true` (or no `validated` annotation) appear in these tables.
546
+ 3. **Requirements Completeness.** Include only when a plan was found in Stage 2b. Render the met/not-addressed/partially-addressed checklist from the plan-assessment step above, then list `report.residual_actionable_work` and `report.advisory_outputs` as their own bullet lists -- these are plain descriptions, not findings, and never gain a file/line/route. Omit this section entirely when no plan was found.
576
547
  4. **Applied Fixes.** Include only if a fix phase ran in this invocation.
577
- 5. **Residual Actionable Work.** Include when unresolved actionable findings were handed off or should be handed off.
578
- 6. **Pre-existing.** Separate section, does not count toward verdict.
579
- 7. **Filtered (not validated).** Include when Stage 5b produced any findings with `validated: false`. Render as a pipe-delimited table with columns `#`, `File`, `Issue`, `Reviewer`, `Confidence`, `Validator reason`. These findings are surfaced for human review — they are not removed from the report. The validator found evidence that the issue may not be real in the code as written, was not introduced by this diff, or is already handled elsewhere; the human reviewer makes the final call. Omit this section when no findings were filtered.
548
+ 5. **Residual Actionable Work.** Render `report.queues.residual` as a table of findings whose owner is `downstream-resolver`, using `report.input_dispositions` to resolve each entry's fields.
549
+ 6. **Pre-existing.** Render `report.pre_existing_findings`. Separate section, does not count toward verdict.
550
+ 7. **Filtered (not validated).** Include when `report.findings` contains any entry with `validated: false`. Render as a pipe-delimited table with columns `#`, `File`, `Issue`, `Reviewer`, `Confidence`, `Validator reason`. These findings are surfaced for human review — they are not removed from the report. The validator found evidence that the issue may not be real in the code as written, was not introduced by this diff, or is already handled elsewhere; the human reviewer makes the final call. Omit this section when no findings were filtered.
580
551
  8. **Learnings & Past Solutions.** Render only when CE `learnings-researcher` was selected and returned relevant output: if past solutions are relevant, flag them as "Known Pattern" with links to docs/solutions/ files. Omit the section otherwise.
581
552
  9. **Agent-Native Gaps.** Render only when CE `agent-native-reviewer` was selected and returned relevant output. Omit the section otherwise.
582
553
  10. **Deployment Notes.** If deployment-verification-agent ran, surface the key Go/No-Go items: blocking pre-deploy checks, the most important verification queries, rollback caveats, and monitoring focus areas. Keep the checklist actionable rather than dropping it into Coverage.
583
- 11. **Coverage.** Suppressed count, residual risks, testing gaps, failed/timed-out reviewers, validator failures, risk-coverage entries with citing input finding IDs and exit conditions for blocked entries, and any intent uncertainty carried by non-interactive modes. For raw returns, state each selected persona's admission state — `findings`, `empty`, `malformed`, `never_returned`, `validation_unavailable` (the persisted raw dispatch outcome; distinct from the artifact-level `validation.status: "unavailable"`), `environment-screen` rejection — and what was admitted or withheld. Report admission states here only; do not add fields to `review-summary.v1`. Distinguish core reviewers, each selected conditional with its one-line rationale and triggering repository-relative paths, an explicit "no conditional selected" case, and any selected-but-failed/malformed/validation-unavailable reviewer; never label an unselected reviewer as failed.
584
- 12. **Verdict.** Ready to merge / Ready with fixes / Not ready. Fix order if applicable. When an `explicit` plan has unaddressed requirements, the verdict must reflect it — a PR that's code-clean but missing planned requirements is "Not ready" unless the omission is intentional. When an `inferred` plan has unaddressed requirements, note it in the verdict reasoning but do not block on it alone. Apply the risk-aware degraded verdict rule from the [synthesis artifact contract](./references/synthesis-artifact-contract.md), including the recorded exit condition for a blocked risk-critical verdict.
554
+ 11. **Coverage.** Render `report.coverage` directly: suppressed count, residual risks, testing gaps, failed/timed-out reviewers, validator lifecycle failures (`failed`/`unavailable` outcomes from Stage 5b), risk-coverage entries with citing input finding IDs and exit conditions for blocked entries, and any intent uncertainty carried by non-interactive modes. For raw returns, state each selected persona's admission state — `findings`, `empty`, `malformed`, `never_returned`, `validation_unavailable` (the persisted raw dispatch outcome; distinct from the artifact-level `validation.status: "unavailable"`) — and what was admitted or withheld. Report admission states here only; do not add fields to `review-summary.v1`. Distinguish core reviewers, each selected conditional with its one-line rationale and triggering repository-relative paths, an explicit "no conditional selected" case, and any selected-but-failed/malformed/validation-unavailable reviewer; never label an unselected reviewer as failed.
555
+ 12. **Verdict.** Render `report.verdict` directly: Ready to merge / Ready with fixes / Not ready, with fix order if applicable. `finalize` already applies the risk-aware degraded verdict rule and the plan-assessment gate from the [synthesis artifact contract](./references/synthesis-artifact-contract.md) -- do not recompute or override it.
585
556
 
586
557
  Do not include time estimates.
587
558
 
@@ -656,11 +627,7 @@ Coverage:
656
627
  Review complete
657
628
  ```
658
629
 
659
- **Detail enrichment (headless only):** The headless envelope includes `Why:`, `Evidence:`, and `Suggested fix:` lines. After merge (Stage 5), use the validated full persona returns retained in parent memory for only the findings that survived dedup and confidence gating.
660
- - **Field tiers:** `Why:` and `Evidence:` are detail-tier and are already present in the validated inline return. `Suggested fix:` is also available directly from that return and survives merge as optional fix context.
661
- - **In-memory matching:** For each surviving finding, look up its detail-tier fields in the validated returns of the contributing reviewers. Use the merged finding's `input_finding_ids` (`<reviewer>#<1-based finding index>`) to identify the contributing return and source finding. When an input ID cannot be resolved or multiple candidates remain, match on normalized `file`, then use line and `normalize(title)` only as tie-breakers.
662
- - **Reviewer order:** Try contributing reviewers in the order they appear in the merged finding's reviewer list; use the first validated match.
663
- - **No-match fallback:** If no validated in-memory return contains a match, omit the `Why:` and `Evidence:` lines for that finding and note the gap in Coverage. This should indicate a synthesis/matching gap, not a failed artifact-file write. Never re-read per-agent files to recover detail.
630
+ **Detail enrichment (headless only):** The headless envelope includes `Why:`, `Evidence:`, and `Suggested fix:` lines. `finalize`'s report projection already carries `why_it_matters`, `evidence`, and `suggested_fix` inline on every finding in `report.findings` -- render them directly. No in-memory matching against persona returns is needed or performed; that matching step is obsolete now that `merge` derives every detail-tier field as part of the merged finding itself.
664
631
 
665
632
  **Formatting rules:**
666
633
  - The `[needs-verification]` marker appears only on findings where `requires_verification: true`.
@@ -698,11 +665,13 @@ After presenting findings and verdict (Stage 6), route the next steps by mode. R
698
665
 
699
666
  #### Step 1: Build the action sets
700
667
 
701
- - **Clean review** means zero findings after suppression and pre-existing separation. Skip the fix/handoff phase when the review is clean.
702
- - **Fixer queue:** final findings routed to `safe_auto -> review-fixer`.
703
- - **Residual actionable queue:** unresolved `gated_auto` or `manual` findings whose final owner is `downstream-resolver`.
704
- - **Report-only queue:** `advisory` findings and any outputs owned by `human` or `release`.
705
- - **Never convert advisory-only outputs into fix work or todos.** Deployment notes, residual risks, and release-owned items stay in the report.
668
+ Call `finalize` with `applied_fixes: []` (see [Stage 6](#stage-6-synthesize-and-present)) and read the action sets directly from `report.queues` -- do not recompute them by hand.
669
+
670
+ - **Clean review** means `report.queues.fixer`, `report.queues.residual`, and `report.queues.report_only` are all empty. Skip the fix/handoff phase when the review is clean.
671
+ - **Fixer queue:** `report.queues.fixer` -- findings routed to `safe_auto -> review-fixer`.
672
+ - **Residual actionable queue:** `report.queues.residual` -- unresolved `gated_auto` or `manual` findings whose final owner is `downstream-resolver`.
673
+ - **Report-only queue:** `report.queues.report_only` -- `advisory` findings and any outputs owned by `human` or `release`.
674
+ - **Never convert advisory-only outputs into fix work or todos.** Deployment notes, residual risks, `report.advisory_outputs`, and release-owned items stay in the report.
706
675
 
707
676
  #### Step 2: Choose policy by mode
708
677
 
@@ -765,14 +734,16 @@ After presenting findings and verdict (Stage 6), route the next steps by mode. R
765
734
  #### Step 4: Emit artifacts and downstream handoff
766
735
 
767
736
  - In interactive, autofix, and headless modes, write **`review-summary.json` unconditionally** under `.context/systematic/ce-review/<run-id>`; `mode:report-only` remains the deliberate no-write exception.
768
- - `review-summary.json` is the parent-owned synthesis artifact. Its lifecycle, dispatch outcomes, complete input ledger, synthesized and filtered findings with provenance, disposition counts, and downstream work are defined in the [canonical synthesis artifact contract](./references/synthesis-artifact-contract.md), whose vocabulary and bounds are executable in [`findings-schema.json`](./references/findings-schema.json).
769
- - Before finalizing, follow the [artifact validation and failure path](./references/synthesis-artifact-contract.md).
770
- - Initialize the artifact before dispatch and persist only validated parent-owned records. Finalize lifecycle and reconciliation after synthesis; preserve the existing degraded and abnormal-run behavior described in the canonical contract.
771
- - Capture `branch` and `head_sha` at dispatch time, before any autofixes land, and write them into `review-summary.json` with `completed_at` when the verdict is finalized; see the [canonical synthesis artifact contract](./references/synthesis-artifact-contract.md) for the provenance semantics.
772
- - In autofix mode, create durable todo files only for unresolved actionable findings whose final owner is `downstream-resolver`. Load the `todos` skill (Create section) for the canonical directory path, naming convention, YAML frontmatter structure, and template. Each todo should map the finding's severity to the todo priority (`P0`/`P1` -> `p1`, `P2` -> `p2`, `P3` -> `p3`) and set `status: ready` since these findings have already been triaged by synthesis.
737
+ - If a fix phase ran (Step 3), call `finalize` again with the exact applied-fix outcomes in `parent_run_metadata.applied_fixes` and the same `validator_lifecycle_results` used in Step 1; only this second call's output is persisted. If no fix phase ran, Step 1's call already is the output to persist -- do not call `finalize` a third time.
738
+ - `review-summary.json` is the persisted call's `artifact` value, written verbatim. `finalize`'s writing-mode stdout is the wrapper `{ kind: 'writing', artifact, report }`, so extract only its `artifact` member -- never the whole wrapper -- and write that JSON to a same-directory temp file created exclusively with owner-only permissions inside `.context/systematic/ce-review/<run-id>`, then atomically rename it over `review-summary.json`; remove the temp file on any non-success. See [Persisting the artifact](./references/pipeline-invocation.md#persisting-the-artifact).
739
+ - After the rename, run the existing `artifact` subcommand on the persisted file exactly as before; see the [artifact validation and failure path](./references/synthesis-artifact-contract.md#artifact-validation).
740
+ - `review-summary.json`'s lifecycle, dispatch outcomes, complete input ledger, synthesized and filtered findings with provenance, disposition counts, and downstream work are defined in the [canonical synthesis artifact contract](./references/synthesis-artifact-contract.md), whose vocabulary and bounds are executable in [`findings-schema.json`](./references/findings-schema.json).
741
+ - Capture `branch` and `head_sha` at dispatch time, before any autofixes land, and pass them in `parent_run_metadata` with `completed_at` when the verdict is finalized; see the [canonical synthesis artifact contract](./references/synthesis-artifact-contract.md) for the provenance semantics.
742
+ - In autofix mode, create durable todo files only for unresolved actionable findings whose final owner is `downstream-resolver` (`report.queues.residual`). Load the `todos` skill (Create section) for the canonical directory path, naming convention, YAML frontmatter structure, and template. Each todo should map the finding's severity to the todo priority (`P0`/`P1` -> `p1`, `P2` -> `p2`, `P3` -> `p3`) and set `status: ready` since these findings have already been triaged by synthesis.
773
743
  - Do not create todos for `advisory` findings, `owner: human`, `owner: release`, or protected-artifact cleanup suggestions.
774
744
  - If only advisory outputs remain, create no todos.
775
745
  - Interactive mode may offer to externalize residual actionable work after fixes, but it is not required to finish the review.
746
+ - Report-only mode never runs this step (no ignore preparation, no run directory, no temp file, no artifact write, no `artifact` subcommand validation): it calls `finalize` once (Step 1) and renders `report` directly, in memory.
776
747
 
777
748
  #### Step 5: Final next steps
778
749