@arbiterforge/ca-pi 0.17.7 → 0.17.8

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
@@ -10,7 +10,7 @@ and durable project context. You decide. codeArbiter enforces.
10
10
  <img alt="Claude Code plugin" src="https://img.shields.io/badge/Claude_Code-plugin-d97757">
11
11
  <img alt="Codex plugin" src="https://img.shields.io/badge/OpenAI_Codex-plugin-10a37f">
12
12
  <img alt="Pi Feature Forge preview" src="https://img.shields.io/badge/ca--pi-Feature_Forge_preview-d97757">
13
- <img alt="version 2.25.6" src="https://img.shields.io/badge/version-2.25.6-2b7489">
13
+ <img alt="version 2.25.7" src="https://img.shields.io/badge/version-2.25.7-2b7489">
14
14
  <img alt="core lanes" src="https://img.shields.io/badge/core_lanes-18-555">
15
15
  <img alt="skills" src="https://img.shields.io/badge/skills-22-555">
16
16
  <img alt="agents" src="https://img.shields.io/badge/agents-20-555">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.17.7",
3
+ "version": "0.17.8",
4
4
  "license": "AGPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,6 +4,18 @@ All notable changes to `ca-pi` are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.17.8] - 2026-10-12
8
+
9
+ ### Fixed
10
+
11
+ - Restore reviewed legacy code-map metadata updates and explicit per-block Git
12
+ conflict resolution with bounded helpers, preserving native ownership,
13
+ stale-input checks, unselected text and the Git index (#967).
14
+ - Explain debug packet shapes and link the trusted schema and a synthetic
15
+ unresolved example before one-shot validation (#951).
16
+ - Document brownfield host prerequisites before scaffolding, preserving
17
+ containment, native approval and partial-state recovery gates (#950).
18
+
7
19
  ## [0.17.7] - 2026-10-10
8
20
 
9
21
  ### Fixed
@@ -37,11 +37,11 @@
37
37
  },
38
38
  "context_qualification": {
39
39
  "darwin/amd64": {
40
- "source_commit": "91b2cbba46d29d69ea052019acfabf52dc8e3897",
40
+ "source_commit": "4c2529e2f3804d38855094e1c11d36178067c826",
41
41
  "workflow": ".github/workflows/ci.yml",
42
- "run_id": "38199172149",
42
+ "run_id": "38219547942",
43
43
  "binary_sha256": "5214e1062e49aa39cd73868cd9792e7e296a865bb6e86230b17c19dda1ba3b71",
44
- "observation_sha256": "1c6f01fc33af2fabbd15633dbd1f5313208dd6eb754d00a1a17d80287f656a37",
44
+ "observation_sha256": "fdd1d120d5ad0e4aed101efad7c32aeac561035faf075b70abf3ba5f82a4bc3b",
45
45
  "module_sha256": "1aea76bc4dc8b54b368dccc80c3a1133b403b5915018fdcb6018f1a3913fe66c",
46
46
  "native_cases": [
47
47
  "TestContextBoundedRender",
@@ -53,11 +53,11 @@
53
53
  ]
54
54
  },
55
55
  "darwin/arm64": {
56
- "source_commit": "91b2cbba46d29d69ea052019acfabf52dc8e3897",
56
+ "source_commit": "4c2529e2f3804d38855094e1c11d36178067c826",
57
57
  "workflow": ".github/workflows/ci.yml",
58
- "run_id": "38199172149",
58
+ "run_id": "38219547942",
59
59
  "binary_sha256": "dde6deb178a0e725f8a88b1b30fa6980622b8af3b13fb73261aaaea47eb75a38",
60
- "observation_sha256": "94f984c74dd5a906bd5ea8ad03c2efe2e561c1e19e3574bb2825e353531c138d",
60
+ "observation_sha256": "18294ead6b8a48804464f06f1bcc8254e0176c0ac0c48486a121aa1a018a309a",
61
61
  "module_sha256": "1aea76bc4dc8b54b368dccc80c3a1133b403b5915018fdcb6018f1a3913fe66c",
62
62
  "native_cases": [
63
63
  "TestContextBoundedRender",
@@ -69,11 +69,11 @@
69
69
  ]
70
70
  },
71
71
  "linux/amd64": {
72
- "source_commit": "91b2cbba46d29d69ea052019acfabf52dc8e3897",
72
+ "source_commit": "4c2529e2f3804d38855094e1c11d36178067c826",
73
73
  "workflow": ".github/workflows/ci.yml",
74
- "run_id": "38199172149",
74
+ "run_id": "38219547942",
75
75
  "binary_sha256": "ca2d5e0fecac0c99c45dc7576fff7d56af63dbd85decb4cfc54f81621ac90901",
76
- "observation_sha256": "ba2155d373bbfd6e4469aa4e240cb1f06a6ac93823d9b6958ae8c0053a4d4a61",
76
+ "observation_sha256": "e4717e28c2ca9a27fc278196310a00a3adff09766d307951ba93de57b262f6ab",
77
77
  "module_sha256": "1aea76bc4dc8b54b368dccc80c3a1133b403b5915018fdcb6018f1a3913fe66c",
78
78
  "native_cases": [
79
79
  "TestContextBoundedRender",
@@ -85,11 +85,11 @@
85
85
  ]
86
86
  },
87
87
  "linux/arm64": {
88
- "source_commit": "91b2cbba46d29d69ea052019acfabf52dc8e3897",
88
+ "source_commit": "4c2529e2f3804d38855094e1c11d36178067c826",
89
89
  "workflow": ".github/workflows/ci.yml",
90
- "run_id": "38199172149",
90
+ "run_id": "38219547942",
91
91
  "binary_sha256": "de18df4c499814f6ccfc69efa6bfb6f4bbcd37c2af5dd091727233f1aaf91a39",
92
- "observation_sha256": "fd52f870338809a6dca3a0f1367130af716fbe463c1f7715e775bed2307508f3",
92
+ "observation_sha256": "03064179c8e677c66c0c90a2d109b139e5b3bb057fa296d5a9da4946ae26c37c",
93
93
  "module_sha256": "1aea76bc4dc8b54b368dccc80c3a1133b403b5915018fdcb6018f1a3913fe66c",
94
94
  "native_cases": [
95
95
  "TestContextBoundedRender",
@@ -101,11 +101,11 @@
101
101
  ]
102
102
  },
103
103
  "windows/amd64": {
104
- "source_commit": "91b2cbba46d29d69ea052019acfabf52dc8e3897",
104
+ "source_commit": "4c2529e2f3804d38855094e1c11d36178067c826",
105
105
  "workflow": ".github/workflows/ci.yml",
106
- "run_id": "38199172149",
106
+ "run_id": "38219547942",
107
107
  "binary_sha256": "ddab696f02b946400feac527f747a020b76dbb9e5dd444b0b7d464acbcd3888e",
108
- "observation_sha256": "fcfedfe79d6b914347674538c432dad334da8d06794d138ec295c1568e3ef158",
108
+ "observation_sha256": "e31ae79bc15c37dbb157c11e5a3775dd0d314350bbf761a968999ba4ffde2d9c",
109
109
  "module_sha256": "1aea76bc4dc8b54b368dccc80c3a1133b403b5915018fdcb6018f1a3913fe66c",
110
110
  "native_cases": [
111
111
  "TestContextBoundedRender",
@@ -117,11 +117,11 @@
117
117
  ]
118
118
  },
119
119
  "windows/arm64": {
120
- "source_commit": "91b2cbba46d29d69ea052019acfabf52dc8e3897",
120
+ "source_commit": "4c2529e2f3804d38855094e1c11d36178067c826",
121
121
  "workflow": ".github/workflows/ci.yml",
122
- "run_id": "38199172149",
122
+ "run_id": "38219547942",
123
123
  "binary_sha256": "23689eaa4df71f95bde58b18bc32fab8d4ae709495250ddfd13192fc0427a813",
124
- "observation_sha256": "3ea32af39b84aaf1ccfbda2063c2c2c3031f205f838b52e3fc2b03ae00f3e392",
124
+ "observation_sha256": "e04cb8b45299950247c82557e7aa4c60c4c7b6b9718775b8c3ecaedc1d7bb6fe",
125
125
  "module_sha256": "1aea76bc4dc8b54b368dccc80c3a1133b403b5915018fdcb6018f1a3913fe66c",
126
126
  "native_cases": [
127
127
  "TestContextBoundedRender",
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- # codeArbiter — restore the committed current side of a legacy code-map conflict.
2
+ # codeArbiter — bounded legacy code-map recovery and metadata claim refresh.
3
3
  #
4
4
  # Fixed target and source only. Inspection binds the real merge, all index bytes,
5
5
  # conflict rendering and legacy provenance. Application rechecks that identity,
@@ -9,9 +9,14 @@
9
9
  # Public API:
10
10
  # inspect(root) -> dict read-only recovery binding
11
11
  # restore_head(root, expected) -> dict restore exact HEAD blob, never stage
12
+ # inspect_claims(root) -> dict bind existing metadata and sources
13
+ # refresh_claims(root, expected, claims) -> dict selected metadata only
14
+ # inspect_blocks(root) -> dict bind genuine Git conflict blocks
15
+ # resolve_blocks(root, expected, choices) -> dict finite ours/theirs choices
12
16
  # main(argv=None) -> int bounded JSON CLI
13
17
 
14
18
  import argparse
19
+ import datetime
15
20
  import hashlib
16
21
  import json
17
22
  import os
@@ -20,6 +25,7 @@ import re
20
25
  import stat
21
26
  import subprocess
22
27
  import sys
28
+ import tempfile
23
29
 
24
30
  from _appendonlyconflictlib import ResolutionError, _validate_conflicted_worktree, _write_bytes_atomic
25
31
  from _artifactlib import ArtifactError, _decode, _open_pinned_regular, _trusted_directory
@@ -32,6 +38,18 @@ TARGET = ".codearbiter/code-map.md"
32
38
  PROVENANCE = ".codearbiter/.provenance/code-map.json"
33
39
  MAX_BYTES = 8 << 20
34
40
  OID = r"[0-9a-f]{40}(?:[0-9a-f]{24})?"
41
+ VERSION = r"(?:0|[1-9][0-9]{0,8})\.(?:0|[1-9][0-9]{0,8})\.(?:0|[1-9][0-9]{0,8})"
42
+ MANIFESTS = {
43
+ "ca_version": "plugins/ca/.claude-plugin/plugin.json",
44
+ "codex_version": "plugins/ca-codex/.codex-plugin/plugin.json",
45
+ "pi_version": "plugins/ca-pi/package.json",
46
+ }
47
+ CLAIM_PATTERNS = {
48
+ "ca_version": rb"^- `plugins/ca/` \(v(" + VERSION.encode() + rb")(?=\))",
49
+ "codex_version": rb"^- `plugins/ca-codex/` \(v(" + VERSION.encode() + rb")(?=\))",
50
+ "pi_version": rb"^- `plugins/ca-pi/` \(v(" + VERSION.encode() + rb")(?=[,)])",
51
+ "metadata_date": rb"metadata refreshed on ([0-9]{4}-[0-9]{2}-[0-9]{2})(?=\.)",
52
+ }
35
53
 
36
54
 
37
55
  def _require(condition, reason):
@@ -54,13 +72,14 @@ def _git(root, *args):
54
72
  return result.stdout
55
73
 
56
74
 
57
- def _read(path):
75
+ def _read(path, *, single_link=False):
58
76
  _trusted_directory(path.parent, "UNSAFE_CONTEXT_PATH")
59
77
  descriptor = _open_pinned_regular(path)
60
78
  try:
61
79
  info = os.fstat(descriptor)
62
80
  _require(stat.S_ISREG(info.st_mode) and not getattr(info, "st_file_attributes", 0) & 0x400
63
81
  and info.st_size <= MAX_BYTES, "context evidence must be a bounded regular file")
82
+ _require(not single_link or info.st_nlink == 1, "linked claim evidence is not supported")
64
83
  with os.fdopen(descriptor, "rb", closefd=False) as handle:
65
84
  raw = handle.read(MAX_BYTES + 1)
66
85
  _require(len(raw) <= MAX_BYTES, "context evidence is oversized")
@@ -75,7 +94,7 @@ def _legacy(raw):
75
94
  and record["doc"] == "code-map", "legacy code-map provenance required; native ownership is not recoverable here")
76
95
 
77
96
 
78
- def _snapshot(requested_root):
97
+ def _snapshot(requested_root, *, blocks=False):
79
98
  root = _trusted_directory(requested_root, "UNSAFE_CONTEXT_ROOT")
80
99
  actual = Path(_git(root, "rev-parse", "--show-toplevel").decode().strip()).resolve(strict=True)
81
100
  _require(actual == root, "explicit repository root required")
@@ -103,7 +122,13 @@ def _snapshot(requested_root):
103
122
  theirs = _git(root, "cat-file", "blob", stages[2][1])
104
123
  # Reuse the existing single-conflict renderer check. Unsupported renderings
105
124
  # (including a non-empty diff3 base) refuse rather than discarding human text.
106
- _validate_conflicted_worktree(current, b"", ours, theirs)
125
+ payload = ours
126
+ rendered = None
127
+ if blocks:
128
+ payload, rendered = _git_conflict_blocks(root, current,
129
+ _git(root, "cat-file", "blob", stages[0][1]), ours, theirs)
130
+ else:
131
+ _validate_conflicted_worktree(current, b"", ours, theirs)
107
132
  provenance = _read(root / PROVENANCE)
108
133
  _legacy(provenance)
109
134
  for revision in (base, head, incoming):
@@ -116,9 +141,17 @@ def _snapshot(requested_root):
116
141
  binding = {"root": str(root), "git_dir": str(git_dir), "head": head, "merge_head": incoming,
117
142
  "stages": stages, "index_sha256": _hash(_read(index)),
118
143
  "worktree_sha256": _hash(current), "provenance_sha256": _hash(provenance)}
144
+ if blocks:
145
+ binding["resolution"] = {"operation": "resolve-blocks", "rendering": rendered,
146
+ "blocks": len(payload[0])}
119
147
  digest = _hash(json.dumps(binding, sort_keys=True, separators=(",", ":")).encode())
120
- return root, index, current, ours, {"target": TARGET, "head": head, "merge_head": incoming,
121
- "head_sha256": _hash(ours), "binding": digest}
148
+ result = {"target": TARGET, "head": head, "merge_head": incoming,
149
+ "head_sha256": _hash(ours), "binding": digest}
150
+ if blocks:
151
+ result.update(blocks=len(payload[0]), rendering=rendered,
152
+ sides=[{"ours_sha256": _hash(part["ours"]),
153
+ "theirs_sha256": _hash(part["theirs"])} for part in payload[0]])
154
+ return root, index, current, payload, result
122
155
 
123
156
 
124
157
  def inspect(root):
@@ -143,16 +176,270 @@ def restore_head(root, expected):
143
176
  return {**fresh, "status": "restored", "staged": False}
144
177
 
145
178
 
179
+ def _claim_matches(raw):
180
+ text = raw.decode("utf-8", "strict")
181
+ _require(re.match(r"^# Code map(?:[ \t][^\r\n]*)?\r?\n", text) is not None
182
+ and "\0" not in text and re.search(r"^(?:<{7,}|={7,}|>{7,}|\|{7,})", text, re.M) is None,
183
+ "a resolved UTF-8 code map is required")
184
+ fields = {}
185
+ for field, pattern in CLAIM_PATTERNS.items():
186
+ matches = list(re.finditer(pattern, raw, re.M))
187
+ _require(len(matches) <= 1, "ambiguous metadata claim")
188
+ if matches:
189
+ fields[field] = matches[0]
190
+ return fields
191
+
192
+
193
+ def _claim_snapshot(requested_root):
194
+ root = _trusted_directory(requested_root, "UNSAFE_CONTEXT_ROOT")
195
+ actual = Path(_git(root, "rev-parse", "--show-toplevel").decode().strip()).resolve(strict=True)
196
+ _require(actual == root, "explicit repository root required")
197
+ git_dir = _trusted_directory(
198
+ _git(root, "rev-parse", "--absolute-git-dir").decode().strip(), "UNSAFE_GIT_DIRECTORY")
199
+ index = Path(_git(root, "rev-parse", "--path-format=absolute", "--git-path", "index").decode().strip())
200
+ head = _git(root, "rev-parse", "--verify", "HEAD^{commit}").decode().strip()
201
+ _require(re.fullmatch(OID, head), "current Git head required")
202
+ _require(not _git(root, "ls-files", "--unmerged", "--", TARGET, PROVENANCE),
203
+ "resolve context and provenance conflicts before claim refresh")
204
+ tracked = _git(root, "ls-files", "--stage", "-z", "--", TARGET, PROVENANCE).split(b"\0")
205
+ indexed = {}
206
+ for raw in tracked:
207
+ if raw:
208
+ matched = re.fullmatch(r"100644 (" + OID + r") 0\t(.+)", raw.decode())
209
+ _require(matched is not None and matched[2] in {TARGET, PROVENANCE},
210
+ "regular tracked legacy context required")
211
+ indexed[matched[2]] = matched[1]
212
+ _require(set(indexed) == {TARGET, PROVENANCE}, "tracked document and provenance required")
213
+ current = _read(root / TARGET, single_link=True)
214
+ fields = _claim_matches(current)
215
+ provenance = _read(root / PROVENANCE, single_link=True)
216
+ _legacy(provenance)
217
+ _legacy(_git(root, "cat-file", "blob", head + ":" + PROVENANCE))
218
+ _legacy(_git(root, "cat-file", "blob", indexed[PROVENANCE]))
219
+ merge = None
220
+ if os.path.lexists(git_dir / "MERGE_HEAD"):
221
+ merge = _read(git_dir / "MERGE_HEAD").decode().strip()
222
+ _require(re.fullmatch(OID, merge), "one current merge head required")
223
+ _legacy(_git(root, "cat-file", "blob", merge + ":" + PROVENANCE))
224
+ for relative in (".codearbiter/.artifacts", ".codearbiter/.artifacts/transactions"):
225
+ if os.path.lexists(root / relative):
226
+ _trusted_directory(root / relative, "UNSAFE_CONTEXT_JOURNAL")
227
+ _require(_committed_context_journal(root, "code-map", TARGET) is False,
228
+ "native or uncertain context ownership requires its writer")
229
+ sources = {}
230
+ for field, relative in MANIFESTS.items():
231
+ raw = _read(root / relative, single_link=True)
232
+ manifest = _decode(raw)
233
+ version = manifest.get("version") if isinstance(manifest, dict) else None
234
+ _require(isinstance(version, str) and re.fullmatch(VERSION, version),
235
+ "literal current source manifest version required")
236
+ sources[field] = {"version": version, "sha256": _hash(raw)}
237
+ binding = {"root": str(root), "git_dir": str(git_dir), "head": head, "merge_head": merge,
238
+ "index_sha256": _hash(_read(index)), "target_sha256": _hash(current),
239
+ "provenance_sha256": _hash(provenance), "sources": sources,
240
+ "claims": {field: matched[1].decode("ascii") for field, matched in fields.items()},
241
+ "inspection_date": datetime.date.today().isoformat()}
242
+ digest = _hash(json.dumps(binding, sort_keys=True, separators=(",", ":")).encode())
243
+ return root, index, current, sources, {**binding, "target": TARGET, "binding": digest}
244
+
245
+
246
+ def inspect_claims(root):
247
+ return _claim_snapshot(root)[-1]
248
+
249
+
250
+ def _selected_claim_bytes(current, sources, inspection_date, claims):
251
+ _require(type(claims) is list and 1 <= len(claims) <= 4, "select one to four existing metadata claims")
252
+ matches = _claim_matches(current)
253
+ edits = []
254
+ seen = set()
255
+ for claim in claims:
256
+ _require(type(claim) is dict and set(claim) == {"field", "before", "after"}, "typed metadata claims required")
257
+ field, before, after = claim["field"], claim["before"], claim["after"]
258
+ _require(type(field) is str and field in matches and field not in seen
259
+ and type(before) is str and type(after) is str and before != after,
260
+ "select each existing metadata claim once")
261
+ seen.add(field)
262
+ matched = matches[field]
263
+ _require(matched[1].decode() == before, "exact claim preimage required")
264
+ if field == "metadata_date":
265
+ datetime.date.fromisoformat(before)
266
+ _require(after == inspection_date, "refresh date must be the actual inspection date")
267
+ else:
268
+ _require(re.fullmatch(VERSION, after) and after == sources[field]["version"],
269
+ "claim must match the current source manifest")
270
+ edits.append((*matched.span(1), after.encode("ascii")))
271
+ for start, end, replacement in sorted(edits, reverse=True):
272
+ current = current[:start] + replacement + current[end:]
273
+ return current
274
+
275
+
276
+ def refresh_claims(root, expected, claims):
277
+ _require(isinstance(expected, str) and re.fullmatch(r"[0-9a-f]{64}", expected), "exact inspection binding required")
278
+ root, index, current, sources, initial = _claim_snapshot(root)
279
+ _require(initial["binding"] == expected, "stale claim binding; inspect again")
280
+ _selected_claim_bytes(current, sources, initial["inspection_date"], claims)
281
+ lock_path = Path(str(index) + ".lock")
282
+ descriptor = os.open(lock_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
283
+ try:
284
+ root, _index, before, sources, fresh = _claim_snapshot(root)
285
+ _require(fresh["binding"] == expected, "stale claim binding; inspect again")
286
+ proposed = _selected_claim_bytes(before, sources, fresh["inspection_date"], claims)
287
+ _write_bytes_atomic(root / TARGET, proposed, expected=before)
288
+ finally:
289
+ os.close(descriptor)
290
+ lock_path.unlink()
291
+ return {**fresh, "status": "refreshed", "staged": False, "after_sha256": _hash(proposed)}
292
+
293
+
294
+ def _marker(line):
295
+ matched = re.fullmatch(rb"([<|=>]{7,32})(?: ([^\r\n]{1,256}))?\r?\n", line)
296
+ if matched is None or len(set(matched[1])) != 1:
297
+ return None
298
+ return matched[1][:1], len(matched[1]), matched[2] or b""
299
+
300
+
301
+ def _parse_conflict_blocks(raw):
302
+ text = raw.decode("utf-8", "strict")
303
+ _require("\0" not in text, "UTF-8 text conflict required")
304
+ lines = raw.splitlines(keepends=True)
305
+ pieces, labels, marker_size = [], None, None
306
+ cursor = 0
307
+ prefix = []
308
+ while cursor < len(lines):
309
+ marker = _marker(lines[cursor])
310
+ if marker is None:
311
+ prefix.append(lines[cursor])
312
+ cursor += 1
313
+ continue
314
+ _require(marker[0] == b"<" and marker[2] == b"HEAD", "recognized Git HEAD conflict required")
315
+ size = marker[1]
316
+ side_labels = [marker[2], b"base", None]
317
+ part = {"prefix": b"".join(prefix), "ours": b"", "base": None, "theirs": b""}
318
+ prefix = []
319
+ cursor += 1
320
+ side, content = "ours", []
321
+ while cursor < len(lines):
322
+ marker = _marker(lines[cursor])
323
+ if marker is None:
324
+ content.append(lines[cursor])
325
+ cursor += 1
326
+ continue
327
+ _require(marker[1] == size, "consistent conflict marker size required")
328
+ if marker[0] == b"|" and side == "ours":
329
+ _require(marker[2], "Git base label required")
330
+ part[side] = b"".join(content)
331
+ side_labels[1] = marker[2]
332
+ side, content = "base", []
333
+ elif marker[0] == b"=" and side in {"ours", "base"}:
334
+ _require(not marker[2], "plain Git separator required")
335
+ part[side] = b"".join(content)
336
+ side, content = "theirs", []
337
+ elif marker[0] == b">" and side == "theirs":
338
+ _require(marker[2], "Git incoming label required")
339
+ part[side] = b"".join(content)
340
+ side_labels[2] = marker[2]
341
+ cursor += 1
342
+ break
343
+ else:
344
+ raise ResolutionError("ambiguous or nested conflict markers")
345
+ cursor += 1
346
+ _require(side_labels[2] is not None, "complete conflict block required")
347
+ if labels is None:
348
+ labels, marker_size = side_labels, size
349
+ _require(side_labels == labels and size == marker_size, "one consistent Git rendering required")
350
+ pieces.append(part)
351
+ _require(len(pieces) <= 32, "at most 32 conflict blocks supported")
352
+ _require(pieces, "Git conflict blocks required")
353
+ return (pieces, b"".join(prefix)), labels, marker_size
354
+
355
+
356
+ def _git_conflict_blocks(root, current, base, ours, theirs):
357
+ payload, labels, size = _parse_conflict_blocks(current)
358
+ modes = (("diff3", "--diff3"), ("zdiff3", "--zdiff3")) if payload[0][0]["base"] is not None else (("merge", None),)
359
+ env = root_bound_git_env()
360
+ env.update(GIT_OPTIONAL_LOCKS="0", GIT_TERMINAL_PROMPT="0", GIT_NO_REPLACE_OBJECTS="1")
361
+ # Git itself must reproduce all hunk bodies and all unconflicted text from
362
+ # the real three stages. Labels are only the current bounded marker labels;
363
+ # no caller content, custom merge driver or arbitrary revision is executed.
364
+ with tempfile.TemporaryDirectory(prefix="codearbiter-context-merge-") as temporary:
365
+ paths = [Path(temporary) / name for name in ("ours", "base", "theirs")]
366
+ for path, raw in zip(paths, (ours, base, theirs)):
367
+ path.write_bytes(raw)
368
+ for name, flag in modes:
369
+ args = [git_executable(), "--no-pager", "--no-lazy-fetch", "-c", "core.fsmonitor=false",
370
+ "-c", "core.untrackedCache=false", "-c", "merge.conflictStyle=merge",
371
+ "merge-file", "--stdout", "--marker-size=" + str(size)]
372
+ if flag:
373
+ args.append(flag)
374
+ for label in labels:
375
+ args.extend(("-L", label.decode("utf-8", "strict")))
376
+ result = subprocess.run([*args, *(str(path) for path in paths)], cwd=root, env=env,
377
+ capture_output=True, shell=False, timeout=30)
378
+ _require(len(result.stdout) <= MAX_BYTES, "Git conflict rendering is oversized")
379
+ if 1 <= result.returncode <= 127 and result.stdout.replace(b"\r\n", b"\n") == current.replace(b"\r\n", b"\n"):
380
+ return payload, name
381
+ raise ResolutionError("worktree is not Git's current three-stage rendering")
382
+
383
+
384
+ def inspect_blocks(root):
385
+ return _snapshot(root, blocks=True)[-1]
386
+
387
+
388
+ def _choose_blocks(payload, choices):
389
+ parts, suffix = payload
390
+ _require(type(choices) is list and len(choices) == len(parts)
391
+ and all(type(choice) is str and choice in {"ours", "theirs"} for choice in choices),
392
+ "one explicit ours/theirs choice per conflict block required")
393
+ return b"".join(part["prefix"] + part[choice] for part, choice in zip(parts, choices)) + suffix
394
+
395
+
396
+ def resolve_blocks(root, expected, choices):
397
+ _require(isinstance(expected, str) and re.fullmatch(r"[0-9a-f]{64}", expected), "exact inspection binding required")
398
+ root, index, _before, payload, initial = _snapshot(root, blocks=True)
399
+ _require(initial["binding"] == expected, "stale conflict binding; inspect again")
400
+ _choose_blocks(payload, choices)
401
+ lock_path = Path(str(index) + ".lock")
402
+ descriptor = os.open(lock_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
403
+ try:
404
+ root, _index, before, payload, fresh = _snapshot(root, blocks=True)
405
+ _require(fresh["binding"] == expected, "stale conflict binding; inspect again")
406
+ proposed = _choose_blocks(payload, choices)
407
+ _write_bytes_atomic(root / TARGET, proposed, expected=before)
408
+ finally:
409
+ os.close(descriptor)
410
+ lock_path.unlink()
411
+ return {**fresh, "status": "resolved", "staged": False, "after_sha256": _hash(proposed)}
412
+
413
+
146
414
  def main(argv=None):
147
- parser = argparse.ArgumentParser(description="Recover only the exact HEAD-side legacy code-map in a current Git conflict.")
415
+ parser = argparse.ArgumentParser(description="Bounded fixed-target legacy code-map recovery and metadata refresh.")
148
416
  parser.add_argument("--root", required=True)
149
417
  operations = parser.add_subparsers(dest="operation", required=True)
150
418
  operations.add_parser("inspect")
151
419
  restore = operations.add_parser("restore-head")
152
420
  restore.add_argument("--expected", required=True)
421
+ operations.add_parser("inspect-claims")
422
+ refresh = operations.add_parser("refresh-claims")
423
+ refresh.add_argument("--expected", required=True)
424
+ refresh.add_argument("--claims", required=True)
425
+ operations.add_parser("inspect-blocks")
426
+ resolve = operations.add_parser("resolve-blocks")
427
+ resolve.add_argument("--expected", required=True)
428
+ resolve.add_argument("--choices", required=True)
153
429
  args = parser.parse_args(argv)
154
430
  try:
155
- result = inspect(args.root) if args.operation == "inspect" else restore_head(args.root, args.expected)
431
+ if args.operation == "inspect":
432
+ result = inspect(args.root)
433
+ elif args.operation == "restore-head":
434
+ result = restore_head(args.root, args.expected)
435
+ elif args.operation == "inspect-claims":
436
+ result = inspect_claims(args.root)
437
+ elif args.operation == "refresh-claims":
438
+ result = refresh_claims(args.root, args.expected, _decode(args.claims.encode("utf-8")))
439
+ elif args.operation == "inspect-blocks":
440
+ result = inspect_blocks(args.root)
441
+ else:
442
+ result = resolve_blocks(args.root, args.expected, _decode(args.choices.encode("utf-8")))
156
443
  except (OSError, ValueError, RuntimeError, ArtifactError, subprocess.SubprocessError):
157
444
  print("context conflict recovery refused: absent, changed, unsafe or unsupported conflict evidence", file=sys.stderr)
158
445
  return 1
@@ -11,7 +11,7 @@ import hostapi # noqa: E402
11
11
  class PiHost(hostapi.Host):
12
12
  name = "pi"
13
13
  adapter_name = "@arbiterforge/ca-pi"
14
- adapter_version = "0.17.7"
14
+ adapter_version = "0.17.8"
15
15
  update_target = "ca-pi"
16
16
  update_tag_prefix = "ca-pi-v"
17
17
  update_command = "pi update npm:@arbiterforge/ca-pi"
@@ -313,7 +313,7 @@ class Host:
313
313
 
314
314
  name = "claude"
315
315
  adapter_name = "ca"
316
- adapter_version = "2.25.6"
316
+ adapter_version = "2.25.7"
317
317
 
318
318
  # Update-notifier descriptor. Each independently versioned host overrides
319
319
  # these three values in its per-plugin _host.py. Keeping the target,
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- # codeArbiter — sanctioned fixed-target legacy context conflict recovery.
2
+ # codeArbiter — sanctioned fixed-target legacy recovery and metadata refresh.
3
3
 
4
4
  from _contextmergelib import main
5
5
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.17.7",
3
+ "version": "0.17.8",
4
4
  "private": true,
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -114,6 +114,31 @@ Run an **incremental re-scout for each affected document**, scoped to its select
114
114
  - **Claim still holds → silently re-baseline only that document's provenance record.** Call `_provenancelib.rebaseline(doc_provenance, selected_current_hashes)` where `selected_current_hashes` contains current hashes only for the selected staged paths whose claims in that document were inspected and still hold. Do not re-baseline a path whose claim changed or was not inspected. Stage only `.codearbiter/.provenance/<doc>.json` by explicit path so the re-baselined record rides THIS commit (ADR-0008 ride-along pattern — nothing is surfaced to the user). After staging, re-run the secrets scan from `tech-stack.md` over the newly-staged path(s) — any file staged after Phase 4 must still pass the automated secrets scan before the commit proceeds. A missing current hash (deletion or rename) cannot be silently re-baselined; surface it in Phase 6 for review.
115
115
  - **Claim changed → route to the existing Phase 6 diff-review.** Propose only the affected doc or code-map edit required to reflect its changed claim. The user reviews that edit in the normal diff, and Phase 7 stages approved paths explicitly; do not silently rewrite or add unrelated documentation.
116
116
 
117
+ For reviewed legacy code-map metadata, use the shipped fixed-target helper at
118
+ `<plugin-root>/hooks/resolve-context-conflict.py`. Run `--root "<project-root>"
119
+ inspect-claims`, then `--root "<project-root>" refresh-claims --expected <binding>
120
+ --claims <json-array>` with one to four reviewed records containing exactly
121
+ `field`, `before`, and `after`. The supported existing fields are `ca_version`,
122
+ `codex_version`, `pi_version`, and `metadata_date`. Use the inspection's exact
123
+ current claim as `before`; a version's `after` must match its fixed source
124
+ manifest, and the date must match `inspection_date`. Select the interpreter once
125
+ by presence and use it for both calls; never retry a failed operation under
126
+ another interpreter.
127
+
128
+ This operation requires resolved, tracked legacy schema-1 document/provenance
129
+ and no native or uncertain journal ownership. It binds document, provenance,
130
+ Git/index, source manifest bytes and date, preserves all unselected bytes, and
131
+ does not stage, rebaseline, adopt, enroll or create a native receipt. Inspect the
132
+ worktree diff for `.codearbiter/code-map.md`, selectively stage that explicit
133
+ path, and re-run the normal secrets scan over the newly staged Markdown before
134
+ Phase 6. Treat it as a heal-proposed doc/code-map edit in Phase 6 review, never as
135
+ a silent rebaseline, and retain its explicit inclusion in Phase 7. Provenance
136
+ rebaseline remains separately scoped by Phase 5.5. Unsupported
137
+ claims or refused preimages stop the edit; do not substitute a direct write or
138
+ native adoption. A codeArbiter contributor may exercise the tested and
139
+ independently reviewed canonical helper under `core/pysrc/` with live enforcement
140
+ intact, never a copied script, installed-cache edit or hook/trust change.
141
+
117
142
  ## Phase 6 — Diff review · gate: BLOCK
118
143
 
119
144
  Read the complete staged diff (`git diff --cached`). Flag as blocking:
@@ -241,6 +241,17 @@ resume condition when applicable. Distinguish an executed check from a proposed
241
241
  command and a recommended handoff from a completed operation. Keep the actual
242
242
  diagnosis-only or repair-requested caller scope visible throughout.
243
243
 
244
+ Before assembling a handoff or invoking its validator, read the installed
245
+ packet preparation guide at `<plugin-root>/routines/debug/references/preparation.md`,
246
+ field reference at `<plugin-root>/routines/debug/references/handoff.md`, and
247
+ JSON Schema at `<plugin-root>/routines/debug/references/debug-handoff.schema.json`.
248
+ Use the linked minimal unresolved example to learn the closed field shapes;
249
+ its synthetic observations are never evidence for the current case. In
250
+ particular, `case_id` must match `DBG-[A-Za-z0-9][A-Za-z0-9_-]{2,63}`.
251
+ Resolve these references from the loaded trusted package. If they are absent,
252
+ stop validated transfer and report the missing resource; do not guess a shape
253
+ or use the one-shot validator to discover fields by trial and error.
254
+
244
255
  For a handoff, assemble the transient `codearbiter.debug-handoff/1.0.0` packet
245
256
  from the observations actually in hand. Preserve symptom, expectation and its
246
257
  citations, reproduction state and limitations, evidence observations and
@@ -0,0 +1,58 @@
1
+ {
2
+ "protocol": "codearbiter.debug-handoff/1.0.0",
3
+ "case_id": "DBG-example-unresolved",
4
+ "request": {
5
+ "intent": "diagnosis_only",
6
+ "request_ref": "example:synthetic-request",
7
+ "scope": "Synthetic documentation example only.",
8
+ "restrictions": ["No repair or external operations."]
9
+ },
10
+ "snapshots": [],
11
+ "symptom": {
12
+ "observed": "An example operator reports a timeout without an accessible trace.",
13
+ "expected": null,
14
+ "expected_evidence_ids": []
15
+ },
16
+ "reproduction": {
17
+ "status": "not_yet_reproduced",
18
+ "recipe": null,
19
+ "evidence_ids": [],
20
+ "limitations": "No runtime was exercised; this is a synthetic example."
21
+ },
22
+ "hypotheses": [],
23
+ "checks": [],
24
+ "evidence": [
25
+ {
26
+ "id": "E-01",
27
+ "kind": "user_report",
28
+ "snapshot_id": null,
29
+ "locator": "example:synthetic-request",
30
+ "observation": "The example operator reports a timeout.",
31
+ "observed_at": null,
32
+ "check_id": null,
33
+ "coverage": "unknown",
34
+ "limitations": "Synthetic report; no independent execution evidence."
35
+ }
36
+ ],
37
+ "disposition": {
38
+ "kind": "unresolved",
39
+ "rationale": "The failure trace is unavailable; no cause is established.",
40
+ "evidence_ids": ["E-01"],
41
+ "cause_ids": [],
42
+ "blockers": [
43
+ {
44
+ "kind": "evidence",
45
+ "missing": "The trace from the reported failing attempt.",
46
+ "owner": null,
47
+ "resume_condition": "A sanitized trace bound to that attempt is available."
48
+ }
49
+ ]
50
+ },
51
+ "regression": null,
52
+ "next_step": {
53
+ "target": "evidence",
54
+ "owner": null,
55
+ "action": "Identify the trace owner and obtain the missing observation.",
56
+ "completion_evidence": "A sanitized trace bound to the reported failing attempt."
57
+ }
58
+ }
@@ -0,0 +1,41 @@
1
+ # Prepare a debug handoff before validation
2
+
3
+ Read the installed field reference at `<plugin-root>/routines/debug/references/handoff.md`
4
+ and JSON Schema at `<plugin-root>/routines/debug/references/debug-handoff.schema.json`
5
+ before assembling the packet. Both are generated from the validator's finite
6
+ model. Every declared field is required, including nullable fields; objects
7
+ reject extra fields. Use `null` for an unknown nullable value and the declared
8
+ empty array where allowed. Do not use an empty string in place of unknown text.
9
+
10
+ The minimal unresolved example at `<plugin-root>/routines/debug/references/minimal-unresolved.json`
11
+ is a synthetic shape example with no runtime proof or repair authority. Read it
12
+ as data. Build the current case from actual observations and their limitations;
13
+ do not submit the example as a real diagnosis or copy its evidence claims.
14
+
15
+ These scalar shapes often cause preventable rejection:
16
+
17
+ | Field | Required shape |
18
+ |---|---|
19
+ | `case_id` | `DBG-[A-Za-z0-9][A-Za-z0-9_-]{2,63}`, matched in full |
20
+ | Snapshot, evidence, hypothesis, check IDs | `S-`, `E-`, `H-`, `C-` followed by 2–4 ASCII digits |
21
+ | `snapshot.commit` | `null` or 40/64 lowercase hexadecimal digits |
22
+ | `snapshot.fingerprint` | `null` or `sha256:` followed by 64 lowercase hexadecimal digits |
23
+ | `snapshot.fingerprint_recipe` | `null` or `[a-z][a-z0-9-]{0,63}/[1-9][0-9]{0,3}` |
24
+ | `recipe.preconditions` | One nonblank string, at most 1,000 characters |
25
+ | `check.exit_code` | `null` or a JSON integer, never a boolean |
26
+
27
+ Choose enums from the reference exactly. For example, hypothesis status is
28
+ `supported`, `refuted`, or `inconclusive`; reproduction status is `reproduced`,
29
+ `intermittent`, `not_yet_reproduced`, `unsafe_to_replay`, or `unavailable`.
30
+ Cross-record IDs must resolve to records in this packet. A shape-valid packet
31
+ can still fail the validator's semantic rules. Schema inspection proves
32
+ neither the diagnosis nor the caller's authority.
33
+
34
+ Measure the finished candidate's UTF-8 byte count before the first invocation.
35
+ The limit is 65,536 bytes. Prepare and, if needed, compact the candidate before
36
+ validation, retaining all evidence required by its claims. Then invoke the
37
+ installed helper once as the debug owner directs. A rejected packet stays
38
+ rejected. Keep the bounded error code and field path; do not rewrite and
39
+ resubmit it, retry through another interpreter, or use an alternate validator
40
+ as an acceptance path. Existing logs and observations remain evidence inputs;
41
+ rejection does not authorize an unvalidated repair handoff.
@@ -168,6 +168,23 @@ cache edit or hook/trust change is permitted. Native context writer/adoption
168
168
  qualification remains unchanged. Re-run the current-head review and CI
169
169
  requirements after integration; previous green evidence does not bind new code.
170
170
 
171
+ For an explicitly selected per-block resolution, use the same shipped helper
172
+ with `--root "<project-root>" inspect-blocks`, then `--root "<project-root>"
173
+ resolve-blocks --expected <binding> --choices <json-array>`. Review each block
174
+ and supply exactly one `"ours"` or `"theirs"` choice in its displayed order. The
175
+ inspection returns the block count and side hashes; read the conflict document
176
+ to review the actual text. This is a separate operation: `restore-head` keeps
177
+ its original single-block exact-HEAD contract.
178
+
179
+ The explicit resolver binds the actual current merge and all three index
180
+ stages, and requires Git to reproduce the complete worktree rendering in merge,
181
+ diff3 or zdiff3 form. It supports at most 32 blocks and preserves every
182
+ unconflicted byte, including incoming rows. Changed or foreign worktree text,
183
+ native/uncertain ownership, malformed choices and unsupported renderings refuse
184
+ without staging or changing other files. It leaves every index entry untouched;
185
+ review the result and stage through the existing lane. The same interpreter,
186
+ contributor-source, native-qualification and no-bypass requirements above apply.
187
+
171
188
  ### Open-PR procedure
172
189
 
173
190
  Run this procedure only for the selected open-PR or merge-via-PR action, after the
@@ -114,6 +114,31 @@ Run an **incremental re-scout for each affected document**, scoped to its select
114
114
  - **Claim still holds → silently re-baseline only that document's provenance record.** Call `_provenancelib.rebaseline(doc_provenance, selected_current_hashes)` where `selected_current_hashes` contains current hashes only for the selected staged paths whose claims in that document were inspected and still hold. Do not re-baseline a path whose claim changed or was not inspected. Stage only `.codearbiter/.provenance/<doc>.json` by explicit path so the re-baselined record rides THIS commit (ADR-0008 ride-along pattern — nothing is surfaced to the user). After staging, re-run the secrets scan from `tech-stack.md` over the newly-staged path(s) — any file staged after Phase 4 must still pass the automated secrets scan before the commit proceeds. A missing current hash (deletion or rename) cannot be silently re-baselined; surface it in Phase 6 for review.
115
115
  - **Claim changed → route to the existing Phase 6 diff-review.** Propose only the affected doc or code-map edit required to reflect its changed claim. The user reviews that edit in the normal diff, and Phase 7 stages approved paths explicitly; do not silently rewrite or add unrelated documentation.
116
116
 
117
+ For reviewed legacy code-map metadata, use the shipped fixed-target helper at
118
+ `<plugin-root>/hooks/resolve-context-conflict.py`. Run `--root "<project-root>"
119
+ inspect-claims`, then `--root "<project-root>" refresh-claims --expected <binding>
120
+ --claims <json-array>` with one to four reviewed records containing exactly
121
+ `field`, `before`, and `after`. The supported existing fields are `ca_version`,
122
+ `codex_version`, `pi_version`, and `metadata_date`. Use the inspection's exact
123
+ current claim as `before`; a version's `after` must match its fixed source
124
+ manifest, and the date must match `inspection_date`. Select the interpreter once
125
+ by presence and use it for both calls; never retry a failed operation under
126
+ another interpreter.
127
+
128
+ This operation requires resolved, tracked legacy schema-1 document/provenance
129
+ and no native or uncertain journal ownership. It binds document, provenance,
130
+ Git/index, source manifest bytes and date, preserves all unselected bytes, and
131
+ does not stage, rebaseline, adopt, enroll or create a native receipt. Inspect the
132
+ worktree diff for `.codearbiter/code-map.md`, selectively stage that explicit
133
+ path, and re-run the normal secrets scan over the newly staged Markdown before
134
+ Phase 6. Treat it as a heal-proposed doc/code-map edit in Phase 6 review, never as
135
+ a silent rebaseline, and retain its explicit inclusion in Phase 7. Provenance
136
+ rebaseline remains separately scoped by Phase 5.5. Unsupported
137
+ claims or refused preimages stop the edit; do not substitute a direct write or
138
+ native adoption. A codeArbiter contributor may exercise the tested and
139
+ independently reviewed canonical helper under `core/pysrc/` with live enforcement
140
+ intact, never a copied script, installed-cache edit or hook/trust change.
141
+
117
142
  ## Phase 6 — Diff review · gate: BLOCK
118
143
 
119
144
  Read the complete staged diff (`git diff --cached`). Flag as blocking:
@@ -241,6 +241,17 @@ resume condition when applicable. Distinguish an executed check from a proposed
241
241
  command and a recommended handoff from a completed operation. Keep the actual
242
242
  diagnosis-only or repair-requested caller scope visible throughout.
243
243
 
244
+ Before assembling a handoff or invoking its validator, read the installed
245
+ packet preparation guide at `<plugin-root>/routines/debug/references/preparation.md`,
246
+ field reference at `<plugin-root>/routines/debug/references/handoff.md`, and
247
+ JSON Schema at `<plugin-root>/routines/debug/references/debug-handoff.schema.json`.
248
+ Use the linked minimal unresolved example to learn the closed field shapes;
249
+ its synthetic observations are never evidence for the current case. In
250
+ particular, `case_id` must match `DBG-[A-Za-z0-9][A-Za-z0-9_-]{2,63}`.
251
+ Resolve these references from the loaded trusted package. If they are absent,
252
+ stop validated transfer and report the missing resource; do not guess a shape
253
+ or use the one-shot validator to discover fields by trial and error.
254
+
244
255
  For a handoff, assemble the transient `codearbiter.debug-handoff/1.0.0` packet
245
256
  from the observations actually in hand. Preserve symptom, expectation and its
246
257
  citations, reproduction state and limitations, evidence observations and
@@ -27,12 +27,50 @@ An initialized refresh is routed through `/ca-status drift` and the existing
27
27
  `context-check` owner after an explicit scoped or full selection; `--brownfield`
28
28
  keeps its initial-population lock.
29
29
 
30
+ ### Brownfield host prerequisites
31
+
32
+ For repositories with existing source, every scaffold and populator step in
33
+ the Procedure below is conditional on observing these prerequisites first.
34
+ This applies to both `--brownfield` and the default auto-detection route.
35
+ When qualification is unavailable or unknown, stop here and issue the handoff
36
+ described below. Do not execute the later Procedure steps or enter brownfield
37
+ population through the When NOT to use section.
38
+
39
+ Scaffolding enables governance and may install Git hooks before population.
40
+ It does not qualify the host for context extraction. Before selecting
41
+ `--brownfield` or automatically routing existing source to brownfield
42
+ population, inspect the installed
43
+ context-creation containment profile in
44
+ `<plugin-root>/routines/context-creation/SKILL.md`, under that exact heading.
45
+ The active host needs an observed context-specific child profile with enforced
46
+ read-only containment and report delivery, plus the existing native context
47
+ writer and approval support. Ordinary unrestricted collaboration agents on
48
+ Codex desktop/Windows do not satisfy that requirement. The repository-only
49
+ Claude preparation fixture is neither a shipped consumer route nor proof of
50
+ a qualified host.
51
+
52
+ When these prerequisites are unavailable or unknown, report the missing
53
+ capability before invoking the scaffolder. Keep a fresh repository unactivated
54
+ and retain every byte of any existing partial state. Name the loaded package,
55
+ actual host limitation, selected population route and next missing prerequisite
56
+ in the handoff; leave unobserved qualification and prior operation identities
57
+ unknown. Resume on a host only after fresh containment and writer qualification,
58
+ using the existing interrupted-population recovery procedure. No supported
59
+ destination is implied by this guidance.
60
+
61
+ This is a workflow preflight. The low-level scaffolder has no native scout
62
+ capability attestation input and does not enforce or prove this assessment.
63
+ Its successful exit, `--check` inventory or enabled stub never establishes
64
+ population readiness. Do not fabricate a profile, receipt, context document
65
+ or initialization sentinel to pass the later gates.
66
+
30
67
  The two flags are mutually exclusive and neither may combine with `--check`. `--stage N` may
31
- accompany one only while `.codearbiter/CONTEXT.md` is absent: scaffold at that stage, then enter the
68
+ accompany one only while `.codearbiter/CONTEXT.md` is absent: after any required brownfield preflight,
69
+ scaffold at that stage, then enter the
32
70
  selected workflow. When an uninitialized stub already exists, skip the refusing scaffolder and enter
33
71
  the selected workflow directly. An initialized marker or source-shape mismatch retains the selected
34
72
  legacy workflow's BLOCK. Without an explicit strategy, continue with the unchanged auto-detection
35
- procedure below.
73
+ procedure below only after any required brownfield preflight.
36
74
  <!-- catalog-command-modes:end -->
37
75
 
38
76
  ## Procedure
@@ -168,6 +168,23 @@ cache edit or hook/trust change is permitted. Native context writer/adoption
168
168
  qualification remains unchanged. Re-run the current-head review and CI
169
169
  requirements after integration; previous green evidence does not bind new code.
170
170
 
171
+ For an explicitly selected per-block resolution, use the same shipped helper
172
+ with `--root "<project-root>" inspect-blocks`, then `--root "<project-root>"
173
+ resolve-blocks --expected <binding> --choices <json-array>`. Review each block
174
+ and supply exactly one `"ours"` or `"theirs"` choice in its displayed order. The
175
+ inspection returns the block count and side hashes; read the conflict document
176
+ to review the actual text. This is a separate operation: `restore-head` keeps
177
+ its original single-block exact-HEAD contract.
178
+
179
+ The explicit resolver binds the actual current merge and all three index
180
+ stages, and requires Git to reproduce the complete worktree rendering in merge,
181
+ diff3 or zdiff3 form. It supports at most 32 blocks and preserves every
182
+ unconflicted byte, including incoming rows. Changed or foreign worktree text,
183
+ native/uncertain ownership, malformed choices and unsupported renderings refuse
184
+ without staging or changing other files. It leaves every index entry untouched;
185
+ review the result and stage through the existing lane. The same interpreter,
186
+ contributor-source, native-qualification and no-bypass requirements above apply.
187
+
171
188
  ### Open-PR procedure
172
189
 
173
190
  Run this procedure only for the selected open-PR or merge-via-PR action, after the