@arbiterforge/ca-pi 0.16.5 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/plugins/ca-pi/CHANGELOG.md +46 -0
  4. package/plugins/ca-pi/agents/map-deps.md +26 -26
  5. package/plugins/ca-pi/agents/map-structure.md +27 -28
  6. package/plugins/ca-pi/agents/tribunal-lens-reviewer.md +70 -39
  7. package/plugins/ca-pi/generated/roles.json +2 -5
  8. package/plugins/ca-pi/helpers/artifacts/release.json +18 -18
  9. package/plugins/ca-pi/hooks/_artifactauthoritylib.py +16 -10
  10. package/plugins/ca-pi/hooks/_artifactlib.py +29 -2
  11. package/plugins/ca-pi/hooks/_contextmergelib.py +160 -0
  12. package/plugins/ca-pi/hooks/_githooks.py +2 -0
  13. package/plugins/ca-pi/hooks/_host.py +1 -1
  14. package/plugins/ca-pi/hooks/_tribunalinventorylib.py +653 -0
  15. package/plugins/ca-pi/hooks/_tribunalrunlib.py +652 -0
  16. package/plugins/ca-pi/hooks/hostapi.py +1 -1
  17. package/plugins/ca-pi/hooks/resolve-context-conflict.py +8 -0
  18. package/plugins/ca-pi/hooks/tribunal.py +113 -0
  19. package/plugins/ca-pi/package.json +1 -1
  20. package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +29 -0
  21. package/plugins/ca-pi/routines/tribunal/SKILL.md +192 -99
  22. package/plugins/ca-pi/routines/tribunal/references/ai-markers.md +6 -27
  23. package/plugins/ca-pi/routines/tribunal/references/cost-and-models.md +73 -52
  24. package/plugins/ca-pi/routines/tribunal/references/finding-record.md +69 -13
  25. package/plugins/ca-pi/routines/tribunal/references/issue-filing.md +63 -45
  26. package/plugins/ca-pi/routines/tribunal/references/lenses/appsec.md +55 -12
  27. package/plugins/ca-pi/routines/tribunal/references/lenses/architecture.md +51 -13
  28. package/plugins/ca-pi/routines/tribunal/references/lenses/change-closure.md +65 -0
  29. package/plugins/ca-pi/routines/tribunal/references/lenses/coverage.md +51 -10
  30. package/plugins/ca-pi/routines/tribunal/references/lenses/infra.md +53 -13
  31. package/plugins/ca-pi/routines/tribunal/references/lenses/migration.md +52 -12
  32. package/plugins/ca-pi/routines/tribunal/references/lenses/observability.md +52 -11
  33. package/plugins/ca-pi/routines/tribunal/references/lenses/performance.md +52 -12
  34. package/plugins/ca-pi/routines/tribunal/references/lenses/reliability.md +53 -13
  35. package/plugins/ca-pi/routines/tribunal/references/lenses/secrets-supply.md +55 -12
  36. package/plugins/ca-pi/routines/tribunal/references/lenses/semantic-contract.md +62 -0
  37. package/plugins/ca-pi/routines/tribunal/references/lenses/test-fidelity.md +48 -13
  38. package/plugins/ca-pi/routines/tribunal/references/lenses/typesafety.md +52 -11
  39. package/plugins/ca-pi/routines/tribunal/references/report.md +34 -12
  40. package/plugins/ca-pi/routines/tribunal/references/review-risk.md +78 -0
  41. package/plugins/ca-pi/routines/tribunal/references/schemas.md +171 -38
  42. package/plugins/ca-pi/routines/tribunal/references/telemetry.md +52 -17
  43. package/plugins/ca-pi/routines/tribunal/references/triage.md +95 -47
  44. package/plugins/ca-pi/routines/tribunal/references/verification-quality.md +61 -0
  45. package/plugins/ca-pi/skills/ca-pr/SKILL.md +29 -0
  46. package/plugins/ca-pi/skills/ca-tribunal/SKILL.md +192 -99
@@ -0,0 +1,652 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — source-bound Tribunal run records.
3
+ #
4
+ # Stdlib only; no import-time I/O. Git plumbing is read-only, with no shell,
5
+ # external diff, filters, fsmonitor, replacements or lazy fetch. Worktree bytes
6
+ # are read directly, never through Git filters or an escaping link. Fingerprints
7
+ # intentionally compare literal reviewed bytes, including line endings.
8
+ # Explicit evidence_paths name represented file/gitlink inputs, not directories.
9
+ # Ignored or otherwise unrepresented declarations are refused, never omitted.
10
+ # The coordinator owns log writes. Exclusive per-run locks reject concurrent
11
+ # writers; a stranded lock requires explicit operator recovery, never takeover.
12
+ # Existing records are never rewritten. These are cooperative audit records,
13
+ # not cryptographically authenticated attestations (ADR-0010).
14
+ #
15
+ # Public API (all return dictionaries with ok; errors are fixed reason codes):
16
+ # source_fingerprint(root, scope='.', reviewed_untracked=None,
17
+ # target_digest=None, active_run=None, evidence_paths=None) -> dict
18
+ # start_run(root, scope='.', reviewed_untracked=None,
19
+ # target_digest=None, detail=None, evidence_paths=None) -> dict
20
+ # resume_status(root, run_dir, scope=None, target_digest=None, evidence_paths=None) -> dict
21
+ # read_run(root, run_dir) -> dict
22
+ # append_event(root, run_dir, event, **fields) -> dict
23
+ # write_lead(root, run_dir, record) -> dict
24
+ # dispose_lead(root, run_dir, lead_id, disposition, rationale) -> dict
25
+ # read_leads(root, run_dir) -> dict
26
+ # validate_finding(record) -> dict
27
+ # triage_eligibility(finding, triage, verification=None) -> dict
28
+ # append_triage(root, run_dir, finding, triage, verification=None) -> dict
29
+
30
+ from contextlib import contextmanager
31
+ from datetime import datetime, timezone
32
+ from functools import wraps
33
+ import hashlib
34
+ import json
35
+ import math
36
+ import os
37
+ from pathlib import Path, PurePosixPath
38
+ import re
39
+ import secrets
40
+ import stat
41
+ import subprocess
42
+
43
+ from _gitexec import git_executable, root_bound_git_env
44
+
45
+
46
+ REPORTS = ".codearbiter/reports"
47
+ MAX_RECORD_BYTES = 8 * 1024 * 1024
48
+ MAX_GIT_BYTES = 128 * 1024 * 1024
49
+ AUDIT_EVENTS = {"lens-launched", "lens-skipped", "lens-completed", "wave-flushed", "wave-triaged", "report-written"}
50
+ FOLLOWUP_EVENTS = {"issues-filed", "filing-skipped", "telemetry-sent", "telemetry-skipped", "run-completed"}
51
+ TERMINAL_EVENTS = {"run-completed", "run-aborted"}
52
+ DECISIONS = {"keep", "combine", "duplicate", "false-positive", "defer", "accept-risk", "decision-required", "verify-required", "investigate"}
53
+ SEVERITIES = {"critical", "high", "medium", "low"}
54
+ DISPOSITIONS = {"routed", "promoted", "dismissed", "deferred"}
55
+ _NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}\Z")
56
+ _DIGEST = re.compile(r"[0-9a-f]{64}\Z")
57
+ _OBJECT = re.compile(r"[0-9a-f]{40}(?:[0-9a-f]{24})?\Z")
58
+
59
+
60
+ class _Invalid(ValueError):
61
+ pass
62
+
63
+
64
+ def _public(function):
65
+ @wraps(function)
66
+ def safe(*args, **kwargs):
67
+ try:
68
+ return function(*args, **kwargs)
69
+ except _Invalid as exc:
70
+ return {"ok": False, "error": str(exc)}
71
+ except (OSError, ValueError, TypeError, KeyError, OverflowError, RecursionError, subprocess.SubprocessError):
72
+ return {"ok": False, "error": "unreadable-or-malformed-input"}
73
+ return safe
74
+
75
+
76
+ def _require(condition, reason):
77
+ if not condition:
78
+ raise _Invalid(reason)
79
+
80
+
81
+ def _json(value):
82
+ return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")
83
+
84
+
85
+ def _digest(value):
86
+ return hashlib.sha256(_json(value)).hexdigest()
87
+
88
+
89
+ def _now():
90
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
91
+
92
+
93
+ def _text(value):
94
+ return isinstance(value, str) and bool(value.strip())
95
+
96
+
97
+ def _name(value):
98
+ return isinstance(value, str) and bool(_NAME.fullmatch(value))
99
+
100
+
101
+ def _relative(value):
102
+ _require(isinstance(value, str) and "\x00" not in value
103
+ and (os.name != "nt" or ("\\" not in value and ":" not in value)), "invalid-relative-path")
104
+ path = PurePosixPath(value)
105
+ _require(not path.is_absolute() and ".." not in path.parts and ".git" not in path.parts, "invalid-relative-path")
106
+ return path.as_posix()
107
+
108
+
109
+ def _linked(info):
110
+ return stat.S_ISLNK(info.st_mode) or bool(getattr(info, "st_file_attributes", 0) & 0x400)
111
+
112
+
113
+ def _contained(root, relative, leaf_link=False):
114
+ """Reject links/reparse points in ancestors before accessing any leaf."""
115
+ relative = _relative(relative)
116
+ current = root
117
+ parts = PurePosixPath(relative).parts
118
+ for index, part in enumerate(parts):
119
+ current = current / part
120
+ try:
121
+ info = current.lstat()
122
+ except FileNotFoundError:
123
+ continue
124
+ if _linked(info):
125
+ _require(leaf_link and index == len(parts) - 1 and stat.S_ISLNK(info.st_mode), "linked-path-refused")
126
+ return current
127
+
128
+
129
+ def _root(root):
130
+ path = Path(root).resolve(strict=True)
131
+ _require(path.is_dir(), "invalid-repository-root")
132
+ return path
133
+
134
+
135
+ def _git(root, *args, data=None):
136
+ env = {key: value for key, value in root_bound_git_env().items() if not key.startswith("GIT_")}
137
+ env.update(GIT_OPTIONAL_LOCKS="0", GIT_TERMINAL_PROMPT="0", GIT_NO_REPLACE_OBJECTS="1")
138
+ try:
139
+ executable = git_executable()
140
+ except RuntimeError:
141
+ raise _Invalid("git-executable-unavailable") from None
142
+ result = subprocess.run(
143
+ [executable, "--no-pager", "--no-lazy-fetch", "-c", "core.fsmonitor=false",
144
+ "-c", "core.hooksPath=" + os.devnull, "-c", "core.untrackedCache=false", *args],
145
+ cwd=root, env=env, input=data, capture_output=True, shell=False, timeout=30)
146
+ _require(result.returncode == 0, "git-plumbing-unavailable")
147
+ _require(len(result.stdout) <= MAX_GIT_BYTES, "git-output-over-limit")
148
+ return result.stdout
149
+
150
+
151
+ def _git_text(root, *args):
152
+ return _git(root, *args).decode("utf-8", "surrogateescape").strip()
153
+
154
+
155
+ def _identity(root):
156
+ actual = Path(_git_text(root, "rev-parse", "--show-toplevel")).resolve(strict=True)
157
+ _require(actual == root, "repository-root-mismatch")
158
+ common = _git_text(root, "rev-parse", "--path-format=absolute", "--git-common-dir")
159
+ return {"root": os.path.normcase(str(root)), "common_dir": os.path.normcase(str(Path(common).resolve(strict=True)))}
160
+
161
+
162
+ def _entries(raw, staged):
163
+ entries = []
164
+ for item in raw.split(b"\x00"):
165
+ if not item:
166
+ continue
167
+ metadata, path = item.split(b"\t", 1)
168
+ fields = metadata.decode("ascii").split()
169
+ mode, oid = (fields[0], fields[1]) if staged else (fields[0], fields[2])
170
+ _require(mode in {"100644", "100755", "120000", "160000"} and _OBJECT.fullmatch(oid), "invalid-git-entry")
171
+ stage = int(fields[2]) if staged else 0
172
+ _require(stage in {0, 1, 2, 3}, "invalid-index-stage")
173
+ entries.append({"path": _relative(path.decode("utf-8", "surrogateescape")), "mode": mode, "object": oid, "stage": stage})
174
+ return sorted(entries, key=lambda entry: (entry["path"], entry["stage"]))
175
+
176
+
177
+ def _blobs(root, entries):
178
+ objects = sorted({entry["object"] for entry in entries if entry["mode"] != "160000"})
179
+ if not objects:
180
+ return {}
181
+ raw = _git(root, "cat-file", "--batch", data=("\n".join(objects) + "\n").encode("ascii"))
182
+ values, cursor = {}, 0
183
+ for oid in objects:
184
+ end = raw.index(b"\n", cursor)
185
+ returned, kind, length = raw[cursor:end].decode("ascii").split()
186
+ _require(returned == oid and kind == "blob", "invalid-git-blob")
187
+ size = int(length)
188
+ _require(0 <= size <= MAX_GIT_BYTES and end + size + 1 < len(raw), "invalid-git-blob")
189
+ content = raw[end + 1:end + size + 1]
190
+ _require(raw[end + size + 1:end + size + 2] == b"\n", "invalid-git-blob")
191
+ values[oid] = hashlib.sha256(content).hexdigest()
192
+ cursor = end + size + 2
193
+ _require(cursor == len(raw), "invalid-git-blob")
194
+ return values
195
+
196
+
197
+ def _worktree_entry(root, name, indexed, allow_submodules=True):
198
+ path = _contained(root, name, leaf_link=True)
199
+ try:
200
+ info = path.lstat()
201
+ except FileNotFoundError:
202
+ return {"path": name, "mode": "missing", "content": None}
203
+ if indexed and indexed["mode"] == "160000":
204
+ _require(stat.S_ISDIR(info.st_mode), "invalid-submodule-directory")
205
+ if not any(path.iterdir()):
206
+ return {"path": name, "mode": "160000", "content": indexed["object"]}
207
+ # One declared gitlink level is supported, including child dirty and
208
+ # untracked bytes. Nested populated gitlinks require their own audit.
209
+ _require(allow_submodules, "nested-submodule-requires-separate-audit")
210
+ child = _fingerprint(path, ".", None, None, allow_submodules=False)
211
+ return {"path": name, "mode": "160000", "content": child["head"],
212
+ "submodule_digest": child["digest"], "submodule_clean": child["clean"]}
213
+ if stat.S_ISLNK(info.st_mode):
214
+ data, mode = os.fsencode(os.readlink(path)), "120000"
215
+ else:
216
+ _require(stat.S_ISREG(info.st_mode) and not _linked(info), "non-regular-input")
217
+ _require(info.st_size <= MAX_GIT_BYTES, "source-file-over-limit")
218
+ with path.open("rb") as handle:
219
+ data = handle.read(MAX_GIT_BYTES + 1)
220
+ _require(len(data) <= MAX_GIT_BYTES, "source-file-over-limit")
221
+ mode = "100755" if info.st_mode & 0o111 else "100644"
222
+ if os.name == "nt":
223
+ mode = indexed["mode"] if indexed and indexed["mode"] in {"100644", "100755"} else "100644"
224
+ return {"path": name, "mode": mode, "content": hashlib.sha256(data).hexdigest()}
225
+
226
+
227
+ def _fingerprint(root, scope, reviewed_untracked, target_digest, active_run=None, evidence_paths=None, allow_submodules=True):
228
+ scope = _relative(scope)
229
+ _contained(root, scope, leaf_link=True)
230
+ _require(evidence_paths is None or isinstance(evidence_paths, list), "invalid-evidence-paths")
231
+ evidence_paths = sorted({_relative(path) for path in evidence_paths or []})
232
+ for path in evidence_paths:
233
+ _contained(root, path, leaf_link=True)
234
+ _require(target_digest is None or isinstance(target_digest, str) and _DIGEST.fullmatch(target_digest), "invalid-target-digest")
235
+ identity = _identity(root)
236
+ head = _git_text(root, "rev-parse", "--verify", "HEAD")
237
+ _require(_OBJECT.fullmatch(head), "invalid-head")
238
+ excluded = active_run.relative_to(root).as_posix() if active_run else None
239
+ all_heads = _entries(_git(root, "ls-tree", "-rz", "--full-tree", "HEAD"), False)
240
+ all_indexes = _entries(_git(root, "ls-files", "--stage", "-z"), True)
241
+ gitlinks = {entry["path"] for entry in all_heads + all_indexes if entry["mode"] == "160000"}
242
+
243
+ def selected(name):
244
+ # A scope within a gitlink conservatively binds that whole child.
245
+ included = any(path == "." or name == path or name.startswith(path + "/") or
246
+ name in gitlinks and path.startswith(name + "/") for path in [scope, *evidence_paths])
247
+ return included and not (excluded and (name == excluded or name.startswith(excluded + "/")))
248
+
249
+ head_entries = [entry for entry in all_heads if selected(entry["path"])]
250
+ index_entries = [entry for entry in all_indexes if selected(entry["path"])]
251
+ available = {_relative(path.decode("utf-8", "surrogateescape")) for path in _git(root, "ls-files", "--others", "--exclude-standard", "-z").split(b"\x00") if path}
252
+ available = {name for name in available if selected(name)}
253
+ _require(reviewed_untracked is None or isinstance(reviewed_untracked, list), "invalid-untracked-selection")
254
+ untracked = sorted(available if reviewed_untracked is None else {_relative(name) for name in reviewed_untracked})
255
+ _require(all(name in available for name in untracked), "untracked-input-unavailable")
256
+ represented = {entry["path"] for entry in head_entries + index_entries} | set(untracked)
257
+ _require(all(path in represented for path in evidence_paths), "evidence-input-unavailable")
258
+ blobs = _blobs(root, head_entries + index_entries)
259
+
260
+ def content_entries(entries):
261
+ return [{"path": entry["path"], "mode": entry["mode"], "stage": entry["stage"],
262
+ "content": entry["object"] if entry["mode"] == "160000" else blobs[entry["object"]]} for entry in entries]
263
+
264
+ indexed = {entry["path"]: entry for entry in index_entries}
265
+ tracked = sorted({entry["path"] for entry in head_entries + index_entries})
266
+ worktree = [_worktree_entry(root, name, indexed.get(name), allow_submodules) for name in tracked]
267
+ untracked_content = [_worktree_entry(root, name, None) for name in untracked]
268
+ heads, indexes = content_entries(head_entries), content_entries(index_entries)
269
+ clean_index = heads == indexes
270
+ plain_worktree = [{key: entry[key] for key in ("path", "mode", "content")} for entry in worktree]
271
+ clean_worktree = [dict(entry, stage=0) for entry in plain_worktree] == indexes and all(entry.get("submodule_clean", True) for entry in worktree)
272
+ result = {"schema": "tribunal-source/v1", "repository": identity, "head": head, "scope": scope, "evidence_paths": evidence_paths,
273
+ "clean": clean_index and clean_worktree and not untracked,
274
+ "head_tree_digest": _digest(heads), "index_digest": _digest(indexes), "worktree_digest": _digest(worktree),
275
+ "untracked_digest": _digest(untracked_content), "reviewed_untracked": untracked,
276
+ "untracked_policy": "all" if reviewed_untracked is None else "selected", "target_digest": target_digest}
277
+ result["digest"] = _digest(result)
278
+ return result
279
+
280
+
281
+ def _run_path(root, run_dir):
282
+ path = Path(run_dir)
283
+ if not path.is_absolute():
284
+ path = root / path
285
+ parts = path.parts[-3:]
286
+ _require(len(parts) == 3 and "/".join(parts[:2]) == REPORTS and _name(parts[2]), "invalid-run-directory")
287
+ # Normalize only the repository prefix; leave the fixed report suffix
288
+ # unresolved so _contained still rejects its links and reparse points.
289
+ _require(_root(path.parents[2]) == root, "invalid-run-directory")
290
+ path = _contained(root, "/".join(parts))
291
+ _require(path.is_dir(), "run-directory-unavailable")
292
+ return path
293
+
294
+
295
+ def _pairs(pairs):
296
+ result = {}
297
+ for key, value in pairs:
298
+ _require(key not in result, "duplicate-json-key")
299
+ result[key] = value
300
+ return result
301
+
302
+
303
+ def _parse(data):
304
+ def invalid_number(value):
305
+ raise _Invalid("invalid-json-number")
306
+ value = json.loads(data, object_pairs_hook=_pairs, parse_constant=invalid_number)
307
+ _require(isinstance(value, dict), "invalid-record")
308
+ return value
309
+
310
+
311
+ def _read_bytes(root, path, optional=False):
312
+ _contained(root, path.relative_to(root).as_posix())
313
+ try:
314
+ with path.open("rb") as handle:
315
+ value = handle.read(MAX_RECORD_BYTES + 1)
316
+ except FileNotFoundError:
317
+ if optional:
318
+ return None
319
+ raise
320
+ _require(len(value) <= MAX_RECORD_BYTES, "record-over-limit")
321
+ return value
322
+
323
+
324
+ def _records(root, path):
325
+ value = _read_bytes(root, path, optional=True)
326
+ if not value:
327
+ return []
328
+ _require(value.endswith(b"\n"), "incomplete-log-record")
329
+ return [_parse(line) for line in value.splitlines()]
330
+
331
+
332
+ def _binding(root, folder):
333
+ raw = _read_bytes(root, folder / "source.json", optional=True)
334
+ if raw is None:
335
+ return None
336
+ value = _parse(raw)
337
+ _require(value.get("schema") == "tribunal-run/v1" and value.get("run_id") == folder.name, "invalid-run-binding")
338
+ fp = value.get("fingerprint")
339
+ _require(isinstance(fp, dict) and fp.get("schema") == "tribunal-source/v1", "invalid-source-binding")
340
+ _require(fp.get("digest") == _digest({key: entry for key, entry in fp.items() if key != "digest"}), "invalid-source-binding")
341
+ _require(isinstance(fp.get("repository"), dict) and fp["repository"].get("root") == os.path.normcase(str(root)), "repository-root-mismatch")
342
+ _require(isinstance(fp.get("evidence_paths"), list), "invalid-source-binding")
343
+ _require(fp.get("untracked_policy") in {"all", "selected"} and isinstance(fp.get("reviewed_untracked"), list), "invalid-source-binding")
344
+ return value
345
+
346
+
347
+ def _cursor(events, fingerprint):
348
+ _require(events and events[0].get("event") == "run-started" and events[0].get("source_digest") == fingerprint["digest"], "unbound-run-log")
349
+ state, filing, telemetry, last_wave = "audit", None, None, 0
350
+ for index, entry in enumerate(events):
351
+ event = entry.get("event")
352
+ _require(entry.get("schema") == "run/v1", "invalid-run-event")
353
+ if index == 0:
354
+ continue
355
+ _require(state != "terminal", "event-after-terminal")
356
+ _require(event in AUDIT_EVENTS | FOLLOWUP_EVENTS | {"run-aborted"}, "invalid-run-event")
357
+ if event in AUDIT_EVENTS:
358
+ _require(state == "audit", "audit-already-reported")
359
+ if event in FOLLOWUP_EVENTS:
360
+ _require(state == "follow-up", "report-required")
361
+ if event == "report-written":
362
+ state = "follow-up"
363
+ elif event in {"issues-filed", "filing-skipped"}:
364
+ _require(filing is None, "filing-already-disposed")
365
+ filing = event
366
+ elif event in {"telemetry-sent", "telemetry-skipped"}:
367
+ _require(telemetry is None, "telemetry-already-disposed")
368
+ telemetry = event
369
+ elif event == "wave-triaged":
370
+ wave = entry.get("wave")
371
+ _require(type(wave) is int and wave > last_wave, "invalid-wave")
372
+ last_wave = wave
373
+ elif event == "run-completed":
374
+ _require(filing is not None and telemetry is not None, "followup-dispositions-required")
375
+ if event in TERMINAL_EVENTS:
376
+ state = "terminal"
377
+ return {"state": state, "filing": filing, "telemetry": telemetry, "last_triaged_wave": last_wave,
378
+ "detail": events[0].get("detail")}
379
+
380
+
381
+ def _status(root, folder, scope=None, target_digest=None, evidence_paths=None):
382
+ try:
383
+ binding = _binding(root, folder)
384
+ if binding is None:
385
+ return {"ok": True, "state": "legacy-unbound", "can_resume_audit": False, "can_resume_follow_up": False}
386
+ fp = binding["fingerprint"]
387
+ cursor = _cursor(_records(root, folder / "run.jsonl"), fp)
388
+ except (ValueError, KeyError, TypeError, OSError):
389
+ return {"ok": False, "state": "invalid", "error": "invalid-run-records", "can_resume_audit": False, "can_resume_follow_up": False}
390
+ if cursor["state"] != "terminal":
391
+ try:
392
+ current = _fingerprint(root, fp["scope"] if scope is None else scope,
393
+ None if fp["untracked_policy"] == "all" else fp["reviewed_untracked"],
394
+ fp["target_digest"] if target_digest is None else target_digest, folder,
395
+ fp["evidence_paths"] if evidence_paths is None else evidence_paths)
396
+ unchanged = current == fp
397
+ except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError):
398
+ unchanged = False
399
+ if not unchanged:
400
+ cursor["state"] = "source-drift"
401
+ return {"ok": True, **cursor, "can_resume_audit": cursor["state"] == "audit",
402
+ "can_resume_follow_up": cursor["state"] == "follow-up"}
403
+
404
+
405
+ def _write(root, path, record, append=False):
406
+ _contained(root, path.relative_to(root).as_posix())
407
+ data = _json(record) + b"\n"
408
+ _require(len(data) <= MAX_RECORD_BYTES, "record-over-limit")
409
+ flags = os.O_WRONLY | os.O_CREAT | (os.O_APPEND if append else os.O_EXCL)
410
+ flags |= getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
411
+ fd = os.open(path, flags, 0o600)
412
+ with os.fdopen(fd, "ab" if append else "wb") as handle:
413
+ handle.write(data)
414
+ handle.flush()
415
+ os.fsync(handle.fileno())
416
+
417
+
418
+ @contextmanager
419
+ def _lock(root, folder):
420
+ path = folder / ".write-lock"
421
+ try:
422
+ _write(root, path, {"at": _now()})
423
+ except FileExistsError:
424
+ raise _Invalid("run-busy-recovery-required") from None
425
+ try:
426
+ yield
427
+ finally:
428
+ path.unlink()
429
+
430
+
431
+ def _writable(root, folder, followup=False):
432
+ status = _status(root, folder)
433
+ allowed = {"audit", "follow-up"} if followup else {"audit"}
434
+ _require(status.get("ok") and status["state"] in allowed, "run-not-writable")
435
+
436
+
437
+ @_public
438
+ def source_fingerprint(root, scope=".", reviewed_untracked=None, target_digest=None, active_run=None, evidence_paths=None):
439
+ root = _root(root)
440
+ if active_run is not None:
441
+ active_run = _run_path(root, active_run)
442
+ _require(_binding(root, active_run) is not None, "unowned-run-exclusion")
443
+ return {"ok": True, "fingerprint": _fingerprint(root, scope, reviewed_untracked, target_digest, active_run, evidence_paths)}
444
+
445
+
446
+ def _new_run_id(scope):
447
+ slug = re.sub(r"[^a-z0-9]+", "-", scope.lower()).strip("-")[:40] or "all"
448
+ return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ") + "-" + slug + "-" + secrets.token_hex(4)
449
+
450
+
451
+ @_public
452
+ def start_run(root, scope=".", reviewed_untracked=None, target_digest=None, detail=None, evidence_paths=None):
453
+ root, scope = _root(root), _relative(scope)
454
+ initial = _fingerprint(root, scope, reviewed_untracked, target_digest, evidence_paths=evidence_paths)
455
+ _json(detail)
456
+ reports = _contained(root, REPORTS)
457
+ reports.mkdir(parents=True, exist_ok=True)
458
+ for _ in range(32):
459
+ run_id = _new_run_id(scope)
460
+ _require(_name(run_id), "invalid-run-id")
461
+ folder = reports / run_id
462
+ try:
463
+ folder.mkdir()
464
+ break
465
+ except FileExistsError:
466
+ continue
467
+ else:
468
+ raise _Invalid("run-allocation-exhausted")
469
+ current = _fingerprint(root, scope, reviewed_untracked, target_digest, folder, evidence_paths)
470
+ _require(current == initial, "source-changed-during-start")
471
+ binding = {"schema": "tribunal-run/v1", "run_id": run_id, "created_at": _now(), "fingerprint": current}
472
+ _write(root, folder / "source.json", binding)
473
+ _write(root, folder / "run.jsonl", {"schema": "run/v1", "event": "run-started", "source_digest": current["digest"], "detail": detail, "at": _now()})
474
+ return {"ok": True, "run_id": run_id, "run_dir": str(folder), "fingerprint": current}
475
+
476
+
477
+ @_public
478
+ def resume_status(root, run_dir, scope=None, target_digest=None, evidence_paths=None):
479
+ root = _root(root)
480
+ return _status(root, _run_path(root, run_dir), scope, target_digest, evidence_paths)
481
+
482
+
483
+ @_public
484
+ def read_run(root, run_dir):
485
+ """Read historical records without migrating, rebinding or rewriting them."""
486
+ root = _root(root)
487
+ folder = _run_path(root, run_dir)
488
+ return {"ok": True, "events": _records(root, folder / "run.jsonl"), "triage": _records(root, folder / "triage.jsonl")}
489
+
490
+
491
+ @_public
492
+ def append_event(root, run_dir, event, **fields):
493
+ root = _root(root)
494
+ folder = _run_path(root, run_dir)
495
+ _require(not ({"schema", "event", "at", "source_digest"} & fields.keys()), "reserved-event-field")
496
+ _require(isinstance(event, str) and event in AUDIT_EVENTS | FOLLOWUP_EVENTS | {"run-aborted"}, "invalid-run-event")
497
+ with _lock(root, folder):
498
+ binding = _binding(root, folder)
499
+ _require(binding is not None, "legacy-unbound")
500
+ if event != "run-aborted":
501
+ _writable(root, folder, followup=True)
502
+ record = {"schema": "run/v1", "event": event, **fields, "at": _now()}
503
+ _cursor(_records(root, folder / "run.jsonl") + [record], binding["fingerprint"])
504
+ _write(root, folder / "run.jsonl", record, append=True)
505
+ return {"ok": True, "record": record}
506
+
507
+
508
+ def _locations(value):
509
+ _require(isinstance(value, list) and value, "locations-required")
510
+ for entry in value:
511
+ _require(isinstance(entry, dict) and _text(entry.get("path")) and _text(entry.get("lines")), "invalid-location")
512
+ _relative(entry["path"])
513
+
514
+
515
+ def _lead(record):
516
+ _require(isinstance(record, dict) and record.get("schema") == "lead/v1", "invalid-lead")
517
+ for field in ("id", "source_lens", "target_lens"):
518
+ _require(_name(record.get(field)), "invalid-lead-identity")
519
+ _require(record["id"].startswith(record["source_lens"] + "-"), "invalid-lead-identity")
520
+ _locations(record.get("locations"))
521
+ for field in ("observation", "reason", "created_at"):
522
+ _require(_text(record.get(field)), "invalid-lead-field")
523
+ _require(record.get("disposition") == "open", "invalid-initial-lead-disposition")
524
+ _json(record)
525
+
526
+
527
+ @_public
528
+ def write_lead(root, run_dir, record):
529
+ _lead(record)
530
+ root = _root(root)
531
+ folder = _run_path(root, run_dir)
532
+ with _lock(root, folder):
533
+ _writable(root, folder)
534
+ directory = _contained(root, (folder.relative_to(root) / "leads" / record["source_lens"]).as_posix())
535
+ directory.mkdir(parents=True, exist_ok=True)
536
+ _write(root, directory / (record["id"] + ".json"), record)
537
+ return {"ok": True, "record": record}
538
+
539
+
540
+ def _read_leads(root, folder):
541
+ directory = _contained(root, (folder.relative_to(root) / "leads").as_posix())
542
+ records = {}
543
+ if directory.exists():
544
+ for lens in sorted(directory.iterdir()):
545
+ _contained(root, lens.relative_to(root).as_posix())
546
+ _require(lens.is_dir() and _name(lens.name), "invalid-lead-directory")
547
+ for path in sorted(lens.iterdir()):
548
+ _require(path.suffix == ".json", "invalid-lead-file")
549
+ record = _parse(_read_bytes(root, path))
550
+ _lead(record)
551
+ _require(record["source_lens"] == lens.name and path.stem == record["id"] and record["id"] not in records, "invalid-lead-identity")
552
+ records[record["id"]] = {**record, "disposition_history": []}
553
+ for entry in _records(root, folder / "lead-dispositions.jsonl"):
554
+ _require(entry.get("schema") == "lead-disposition/v1" and entry.get("id") in records and entry.get("disposition") in DISPOSITIONS and _text(entry.get("rationale")), "invalid-lead-disposition")
555
+ record = records[entry["id"]]
556
+ record["disposition"] = entry["disposition"]
557
+ record["disposition_history"].append(entry)
558
+ return list(records.values())
559
+
560
+
561
+ @_public
562
+ def read_leads(root, run_dir):
563
+ root = _root(root)
564
+ return {"ok": True, "leads": _read_leads(root, _run_path(root, run_dir))}
565
+
566
+
567
+ @_public
568
+ def dispose_lead(root, run_dir, lead_id, disposition, rationale):
569
+ _require(_name(lead_id) and disposition in DISPOSITIONS and _text(rationale), "invalid-lead-disposition")
570
+ root = _root(root)
571
+ folder = _run_path(root, run_dir)
572
+ with _lock(root, folder):
573
+ _writable(root, folder, followup=True)
574
+ _require(any(record["id"] == lead_id for record in _read_leads(root, folder)), "unknown-lead")
575
+ record = {"schema": "lead-disposition/v1", "id": lead_id, "disposition": disposition, "rationale": rationale, "at": _now()}
576
+ _write(root, folder / "lead-dispositions.jsonl", record, append=True)
577
+ return {"ok": True, "record": record}
578
+
579
+
580
+ def _confidence(value):
581
+ return type(value) in {int, float} and math.isfinite(value) and 0 <= value <= 1
582
+
583
+
584
+ def _finding(record):
585
+ _require(isinstance(record, dict) and record.get("schema") == "finding/v1", "invalid-finding")
586
+ _require(_name(record.get("id")) and _name(record.get("lens")), "invalid-finding-identity")
587
+ _require(record.get("severity") in SEVERITIES and _confidence(record.get("confidence")), "invalid-finding-score")
588
+ _locations(record.get("locations"))
589
+ _require(_text(record.get("evidence")) and _text(record.get("recommendation")), "finding-evidence-required")
590
+ _require(record.get("claim_type", "fact") in {"fact", "design-choice"}, "invalid-claim-type")
591
+ if "root_cause_key" in record:
592
+ _require(_text(record["root_cause_key"]), "invalid-root-cause")
593
+ for field in ("corroborates", "related_lenses"):
594
+ if field in record:
595
+ _require(isinstance(record[field], list) and all(_name(value) for value in record[field]), "invalid-corroboration")
596
+ _json(record)
597
+
598
+
599
+ @_public
600
+ def validate_finding(record):
601
+ _finding(record)
602
+ return {"ok": True, "finding": record}
603
+
604
+
605
+ def _eligibility(finding, triage, verification):
606
+ _finding(finding)
607
+ _require(isinstance(triage, dict) and triage.get("schema") == "triage/v1" and triage.get("id") == finding["id"], "invalid-triage")
608
+ decision = triage.get("decision")
609
+ _require(decision in DECISIONS and triage.get("final_severity") in SEVERITIES and _confidence(triage.get("final_confidence")), "invalid-triage-decision")
610
+ _require(_text(triage.get("rationale")), "triage-rationale-required")
611
+ _require(decision != "decision-required" or finding.get("claim_type") == "design-choice", "factual-uncertainty-is-not-design-choice")
612
+ serious = finding["severity"] in {"critical", "high"} or triage["final_severity"] in {"critical", "high"}
613
+ required = serious or finding.get("requires_verification") is True
614
+ if serious:
615
+ _require(_text(triage.get("counter_argument")), "serious-counter-argument-required")
616
+ outcome, independent = None, False
617
+ if verification is not None:
618
+ _require(isinstance(verification, dict) and verification.get("schema") == "verification/v1" and verification.get("id") == finding["id"], "invalid-verification")
619
+ outcome = verification.get("outcome")
620
+ _require(outcome in {"confirmed", "narrowed", "refuted", "inconclusive"} and _text(verification.get("evidence")), "invalid-verification-outcome")
621
+ _require(verification.get("verification_independence") in {"independent", "limited"}, "invalid-verification-independence")
622
+ independent = verification["verification_independence"] == "independent"
623
+ verified = independent and outcome in {"confirmed", "narrowed"}
624
+ eligible = decision in {"keep", "combine"} and outcome not in {"refuted", "inconclusive"} and (not required or verified)
625
+ recommended = "false-positive" if outcome == "refuted" else "verify-required" if required and not verified else decision
626
+ root_cause = triage.get("root_cause_key", finding.get("root_cause_key", finding.get("dedup_key", finding["id"])))
627
+ _require(_text(root_cause), "invalid-root-cause")
628
+ _json(triage)
629
+ return {"ok": True, "filing_eligible": eligible, "plan_eligible": eligible,
630
+ "requires_verification": required and not verified, "recommended_decision": recommended,
631
+ "root_cause_key": root_cause, "related_lenses": sorted(set([finding["lens"], *finding.get("related_lenses", [])]))}
632
+
633
+
634
+ @_public
635
+ def triage_eligibility(finding, triage, verification=None):
636
+ return _eligibility(finding, triage, verification)
637
+
638
+
639
+ @_public
640
+ def append_triage(root, run_dir, finding, triage, verification=None):
641
+ eligibility = _eligibility(finding, triage, verification)
642
+ _require(eligibility["plan_eligible"] or triage["decision"] not in {"keep", "combine"}, "triage-requires-verification")
643
+ _require(not triage.get("issue_ref") or eligibility["filing_eligible"], "finding-not-filing-eligible")
644
+ root = _root(root)
645
+ folder = _run_path(root, run_dir)
646
+ record = {**triage, "root_cause_key": eligibility["root_cause_key"], "related_lenses": eligibility["related_lenses"],
647
+ "corroborates": finding.get("corroborates", []), "verification": verification}
648
+ with _lock(root, folder):
649
+ _writable(root, folder, followup=True)
650
+ _records(root, folder / "triage.jsonl")
651
+ _write(root, folder / "triage.jsonl", record, append=True)
652
+ return {"ok": True, "record": record, "eligibility": eligibility}
@@ -313,7 +313,7 @@ class Host:
313
313
 
314
314
  name = "claude"
315
315
  adapter_name = "ca"
316
- adapter_version = "2.24.4"
316
+ adapter_version = "2.25.0"
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,
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — sanctioned fixed-target legacy context conflict recovery.
3
+
4
+ from _contextmergelib import main
5
+
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(main())