@arbiterforge/ca-pi 0.15.2 → 0.15.3

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 (59) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/plugins/ca-pi/CHANGELOG.md +62 -0
  4. package/plugins/ca-pi/COMMANDS.md +3 -9
  5. package/plugins/ca-pi/SKILLS.md +16 -22
  6. package/plugins/ca-pi/agents/coverage-auditor.md +16 -2
  7. package/plugins/ca-pi/generated/command-catalog.json +13 -22
  8. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
  9. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
  10. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
  11. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
  12. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
  13. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
  14. package/plugins/ca-pi/helpers/artifacts/release.json +6 -6
  15. package/plugins/ca-pi/hooks/_artifactauthoritylib.py +383 -32
  16. package/plugins/ca-pi/hooks/_artifactlib.py +3 -1
  17. package/plugins/ca-pi/hooks/_host.py +1 -1
  18. package/plugins/ca-pi/hooks/artifact-authority-hook.py +4 -21
  19. package/plugins/ca-pi/hooks/artifact-authority.py +11 -0
  20. package/plugins/ca-pi/hooks/hostapi.py +1 -1
  21. package/plugins/ca-pi/includes/command-compatibility.md +31 -0
  22. package/plugins/ca-pi/includes/farm.md +187 -36
  23. package/plugins/ca-pi/includes/maturity-coverage.md +57 -17
  24. package/plugins/ca-pi/includes/routing-table.md +10 -11
  25. package/plugins/ca-pi/includes/safety-core.md +2 -2
  26. package/plugins/ca-pi/package.json +1 -1
  27. package/plugins/ca-pi/routines/INDEX.md +12 -11
  28. package/plugins/ca-pi/routines/context-check/SKILL.md +17 -1
  29. package/plugins/ca-pi/routines/context-creation/SKILL.md +8 -1
  30. package/plugins/ca-pi/routines/debug/SKILL.md +18 -2
  31. package/plugins/ca-pi/routines/decision-lifecycle/SKILL.md +72 -118
  32. package/plugins/ca-pi/routines/decision-lifecycle/references/authoring.md +152 -0
  33. package/plugins/ca-pi/routines/decision-variance/SKILL.md +107 -133
  34. package/plugins/ca-pi/routines/decision-variance/references/analysis.md +136 -0
  35. package/plugins/ca-pi/routines/decompose/SKILL.md +8 -1
  36. package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +85 -7
  37. package/plugins/ca-pi/routines/post-merge-cleanup/SKILL.md +19 -2
  38. package/plugins/ca-pi/routines/refactor/SKILL.md +27 -8
  39. package/plugins/ca-pi/routines/release/SKILL.md +2 -1
  40. package/plugins/ca-pi/routines/security-architecture/SKILL.md +14 -3
  41. package/plugins/ca-pi/routines/tdd/SKILL.md +9 -5
  42. package/plugins/ca-pi/routines/tribunal/SKILL.md +28 -14
  43. package/plugins/ca-pi/skills/ca-adr/SKILL.md +103 -18
  44. package/plugins/ca-pi/skills/ca-adr-status/SKILL.md +6 -24
  45. package/plugins/ca-pi/skills/ca-cleanup/SKILL.md +241 -52
  46. package/plugins/ca-pi/skills/ca-context-check/SKILL.md +87 -24
  47. package/plugins/ca-pi/skills/ca-create-context/SKILL.md +159 -19
  48. package/plugins/ca-pi/skills/ca-debug/SKILL.md +100 -27
  49. package/plugins/ca-pi/skills/ca-decompose/SKILL.md +156 -17
  50. package/plugins/ca-pi/skills/ca-pr/SKILL.md +161 -26
  51. package/plugins/ca-pi/skills/ca-reconcile/SKILL.md +108 -30
  52. package/plugins/ca-pi/skills/ca-refactor/SKILL.md +96 -30
  53. package/plugins/ca-pi/skills/ca-release/SKILL.md +614 -39
  54. package/plugins/ca-pi/skills/ca-status/SKILL.md +1 -1
  55. package/plugins/ca-pi/skills/ca-threat-model/SKILL.md +56 -27
  56. package/plugins/ca-pi/skills/ca-tribunal/SKILL.md +105 -26
  57. package/plugins/ca-pi/routines/skill-author/SKILL.md +0 -129
  58. package/plugins/ca-pi/routines/skill-author/references/skill-template.md +0 -58
  59. package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +0 -129
@@ -76,6 +76,14 @@ CLAUDE_REVIEWER = "ca:authority-reviewer"
76
76
  CLAUDE_REVIEWER_MODELS = frozenset({"opus", "sonnet", "haiku"})
77
77
  CLAUDE_REVIEW_PROFILE = "claude-review/0.1.0"
78
78
  CODEX_REVIEW_PROFILE = "codex-review/0.1.0"
79
+ CODEX_NATIVE_V1 = "codex-native-v1/0.145.0"
80
+ CODEX_UUID_RE = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
81
+ CODEX_STEERING_TOOLS = frozenset({
82
+ "multi_agent_v1send_input", "multi_agent_v1resume_agent", "multi_agent_v1close_agent",
83
+ })
84
+ CODEX_REVIEW_TOOLS = frozenset({"spawn_agent", "collaborationspawn_agent"}) | CODEX_STEERING_TOOLS
85
+ CODEX_CHILD_FORMAT = "codearbiter.codex-native-child/0.1.0"
86
+ MAX_CHILD_MARKER = 4096
79
87
  # None means the real ~/.claude/agents (plus CLAUDE_CONFIG_DIR/agents); tests
80
88
  # point it at a fixture directory.
81
89
  CLAUDE_USER_AGENT_DIR: Path | None = None
@@ -389,7 +397,7 @@ def _load(
389
397
  "observation_sha256", "receipt", "integrity_sha256", "payload",
390
398
  "authority_source", "dispatch_prompt", "review_contract_sha256",
391
399
  "required_coverage", "wrapper", "command_bindings", "recovery",
392
- "launch_envelope", "workspace_roots", "host",
400
+ "launch_envelope", "workspace_roots", "host", "codex_review_profile",
393
401
  }
394
402
  if (
395
403
  not isinstance(value, dict)
@@ -410,6 +418,12 @@ def _load(
410
418
  or SHA256_RE.fullmatch(value.get("context_sha256", "")) is None
411
419
  ):
412
420
  raise AuthorityError("INVALID_AUTHORITY_STATE", "request state failed validation")
421
+ if "codex_review_profile" in value and (
422
+ value["codex_review_profile"] != CODEX_NATIVE_V1
423
+ or value.get("host", "codex") != "codex"
424
+ or value["activity"] not in REVIEW_ACTIVITIES
425
+ ):
426
+ raise AuthorityError("INVALID_AUTHORITY_STATE", "Codex review profile is unsupported")
413
427
  if len(raw) > MAX_STATE:
414
428
  try:
415
429
  _validate_legacy_review_recovery(root, value)
@@ -571,6 +585,15 @@ def _validate_legacy_review_recovery(root: Path, request: dict[str, Any]) -> Non
571
585
  # Recovery validates the retained snapshot; it neither recreates absent
572
586
  # evidence files nor upgrades the legacy prompt into launchable authority.
573
587
  prompt = _dispatch_prompt(request, legacy_inline=True)
588
+ # Qualified Codex 0.13.11 predates both the host field and JSON-only suffix.
589
+ # Admit that one exact earlier form, without normalizing retained text.
590
+ original_prompt = request.get("dispatch_prompt")
591
+ if (
592
+ "host" not in request
593
+ and isinstance(original_prompt, str)
594
+ and original_prompt + " Reply with the JSON object only: no code fence and no other text." == prompt
595
+ ):
596
+ prompt = original_prompt
574
597
  if not isinstance(request.get("launch_envelope"), dict):
575
598
  raise AuthorityError("INVALID_AUTHORITY_STATE", "legacy launch envelope is malformed")
576
599
  if request.get("host", "codex") == "claude":
@@ -1097,11 +1120,17 @@ def _dispatch_prompt(request: dict[str, Any], *, legacy_inline: bool = False) ->
1097
1120
  "Do not pass if the evidence is missing, unreadable or inconsistent. The locator "
1098
1121
  "is a reference to the complete immutable evidence, not replacement authority.\n"
1099
1122
  )
1123
+ profile = (
1124
+ f"Native review profile: {CODEX_NATIVE_V1}. This review was dispatched through "
1125
+ "multi_agent_v1.spawn_agent with default role and fork_context=false. Read-only conduct is cooperative; "
1126
+ "these settings do not provide an OS sandbox.\n"
1127
+ if request.get("codex_review_profile") == CODEX_NATIVE_V1 else ""
1128
+ )
1100
1129
  return (
1101
1130
  f"[CODEARBITER_AUTHORITY_REQUEST:{request['request_id']}]\n"
1102
1131
  f"Target repository: {request['repository']['path']}\n"
1103
1132
  f"Frozen context: {request['context_ref']} sha256={request['context_sha256']}\n"
1104
- + target
1133
+ + target + profile
1105
1134
  + "Act as a fresh read-only independent codeArbiter reviewer. Do not edit files or "
1106
1135
  "delegate. Review only the frozen target and return exactly one JSON object using "
1107
1136
  f"format {DECISION_FORMAT}. Bind request_id={request['request_id']}, "
@@ -1126,6 +1155,7 @@ def arm_request(
1126
1155
  workspace_roots: dict[str, str | Path] | None = None,
1127
1156
  host: str = "codex",
1128
1157
  reviewer_model: str = "opus",
1158
+ codex_review_profile: str | None = None,
1129
1159
  **unexpected: Any,
1130
1160
  ) -> dict[str, Any]:
1131
1161
  if unexpected:
@@ -1139,6 +1169,10 @@ def arm_request(
1139
1169
  raise AuthorityError("INVALID_AUTHORITY_REQUEST", "host is unsupported")
1140
1170
  if host == "claude" and reviewer_model not in CLAUDE_REVIEWER_MODELS:
1141
1171
  raise AuthorityError("INVALID_AUTHORITY_REQUEST", "reviewer model is unsupported")
1172
+ if codex_review_profile is not None and (
1173
+ codex_review_profile != "native-v1" or host != "codex" or activity not in REVIEW_ACTIVITIES
1174
+ ):
1175
+ raise AuthorityError("INVALID_AUTHORITY_REQUEST", "Codex review profile is unsupported")
1142
1176
  root = _real_root(root)
1143
1177
  if host == "claude" and activity in REVIEW_ACTIVITIES:
1144
1178
  _refuse_shadowed_reviewer(root)
@@ -1164,6 +1198,8 @@ def arm_request(
1164
1198
  }
1165
1199
  if host != "codex":
1166
1200
  seed_value["host"] = host
1201
+ if codex_review_profile is not None:
1202
+ seed_value["codex_review_profile"] = CODEX_NATIVE_V1
1167
1203
  seed = _canonical(seed_value)
1168
1204
  request_id = _digest(seed)
1169
1205
  request = {
@@ -1187,6 +1223,8 @@ def arm_request(
1187
1223
  }
1188
1224
  if host != "codex":
1189
1225
  request["host"] = host
1226
+ if codex_review_profile is not None:
1227
+ request["codex_review_profile"] = CODEX_NATIVE_V1
1190
1228
  if activity in REVIEW_ACTIVITIES:
1191
1229
  request["review_contract_sha256"], request["required_coverage"] = _review_binding(context)
1192
1230
  request["dispatch_prompt"] = _dispatch_prompt(request)
@@ -1197,6 +1235,10 @@ def arm_request(
1197
1235
  "subagent_type": CLAUDE_REVIEWER,
1198
1236
  "model": reviewer_model,
1199
1237
  }
1238
+ elif codex_review_profile is not None:
1239
+ request["launch_envelope"] = {
1240
+ "message": request["dispatch_prompt"], "fork_context": False,
1241
+ }
1200
1242
  else:
1201
1243
  request["launch_envelope"] = {
1202
1244
  "message": request["dispatch_prompt"],
@@ -1215,6 +1257,8 @@ def arm_request(
1215
1257
  result["review_contract_sha256"] = request["review_contract_sha256"]
1216
1258
  result["required_coverage"] = request["required_coverage"]
1217
1259
  result["launch_envelope"] = request["launch_envelope"]
1260
+ if codex_review_profile is not None:
1261
+ result["codex_review_profile"] = CODEX_NATIVE_V1
1218
1262
  return result
1219
1263
 
1220
1264
 
@@ -2041,8 +2085,8 @@ def _parse_decision(raw: Any, request: dict[str, Any]) -> dict[str, Any]:
2041
2085
  if not isinstance(raw, str) or len(raw.encode("utf-8")) > MAX_DECISION:
2042
2086
  raise AuthorityError("INVALID_REVIEW_DECISION", "review decision is absent or oversized")
2043
2087
  try:
2044
- value = json.loads(raw)
2045
- except ValueError as exc:
2088
+ value = json.loads(raw, object_pairs_hook=_unique_json_pairs, parse_constant=_invalid_json_constant)
2089
+ except (ValueError, RecursionError) as exc:
2046
2090
  raise AuthorityError("INVALID_REVIEW_DECISION", "review decision is not JSON") from exc
2047
2091
  expected = {
2048
2092
  "format", "request_id", "target_sha256", "contract_sha256", "decision",
@@ -2060,6 +2104,7 @@ def _parse_decision(raw: Any, request: dict[str, Any]) -> dict[str, Any]:
2060
2104
  or not isinstance(value.get("assessment"), str)
2061
2105
  or not value["assessment"].strip()
2062
2106
  or not isinstance(value.get("coverage"), list)
2107
+ or any(not isinstance(item, str) or not item for item in value["coverage"])
2063
2108
  or len(set(value["coverage"])) != len(value["coverage"])
2064
2109
  or not isinstance(value.get("findings"), list)
2065
2110
  ):
@@ -2081,13 +2126,329 @@ def _parse_decision(raw: Any, request: dict[str, Any]) -> dict[str, Any]:
2081
2126
  return value
2082
2127
 
2083
2128
 
2129
+ def _codex_uuid(value: Any) -> str:
2130
+ if not isinstance(value, str) or CODEX_UUID_RE.fullmatch(value) is None:
2131
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native Codex child identity is not a UUID")
2132
+ return value
2133
+
2134
+
2135
+ def _codex_native_marker(agent_id: str) -> Path:
2136
+ # UUID, not arrival order or session, is the direct host-observed join key.
2137
+ # Retaining this marker also prevents reuse across requests and sessions.
2138
+ key = _digest(_canonical({"profile": CODEX_NATIVE_V1, "agent_id": _codex_uuid(agent_id)}))
2139
+ return _registry_root() / f"codex-native-{key}.marker"
2140
+
2141
+
2142
+ def _codex_claimed(agent_id: str, kind: str) -> bool:
2143
+ path = _codex_native_marker(agent_id).with_suffix("." + kind)
2144
+ try:
2145
+ info = path.lstat()
2146
+ except FileNotFoundError:
2147
+ return False
2148
+ if (stat.S_ISLNK(info.st_mode) or getattr(info, "st_file_attributes", 0) & 0x400
2149
+ or not stat.S_ISREG(info.st_mode) or info.st_size != 0):
2150
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native child event claim is unsafe")
2151
+ return True
2152
+
2153
+
2154
+ def _claim_codex_event(agent_id: str, kind: str) -> bool:
2155
+ """Atomic, single-use refusal evidence independent of the child mutex.
2156
+
2157
+ Zero-byte claims grant no authority. They retain the first lifecycle attempt
2158
+ even when its marker lock times out or its process dies before joining.
2159
+ """
2160
+ path = _codex_native_marker(agent_id).with_suffix("." + kind)
2161
+ try:
2162
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0), 0o600)
2163
+ except FileExistsError:
2164
+ _codex_claimed(agent_id, kind)
2165
+ return False
2166
+ except OSError as exc:
2167
+ raise AuthorityError("AUTHORITY_BUSY", "native child first event could not be retained") from exc
2168
+ try:
2169
+ os.fsync(fd)
2170
+ finally:
2171
+ os.close(fd)
2172
+ return True
2173
+
2174
+
2175
+ def _read_codex_child(path: Path, agent_id: str, session_id: str) -> dict[str, Any]:
2176
+ try:
2177
+ info = path.lstat()
2178
+ except FileNotFoundError:
2179
+ return {
2180
+ "format": CODEX_CHILD_FORMAT, "agent_id": agent_id, "session_id": session_id,
2181
+ "request_id": None, "start": None, "stop_seen": False, "rejected": False,
2182
+ }
2183
+ if (stat.S_ISLNK(info.st_mode) or getattr(info, "st_file_attributes", 0) & 0x400
2184
+ or not stat.S_ISREG(info.st_mode) or info.st_size > MAX_CHILD_MARKER):
2185
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native Codex child marker is unsafe")
2186
+ flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
2187
+ with os.fdopen(os.open(path, flags), "rb") as stream:
2188
+ opened = os.fstat(stream.fileno())
2189
+ if (opened.st_dev, opened.st_ino) != (info.st_dev, info.st_ino):
2190
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native Codex child marker changed")
2191
+ raw = stream.read(MAX_CHILD_MARKER + 1)
2192
+ try:
2193
+ value = json.loads(raw, object_pairs_hook=_unique_json_pairs, parse_constant=_invalid_json_constant)
2194
+ except (ValueError, UnicodeError) as exc:
2195
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native Codex child marker is unreadable") from exc
2196
+ start = value.get("start") if isinstance(value, dict) else None
2197
+ if (
2198
+ len(raw) > MAX_CHILD_MARKER or not isinstance(value, dict)
2199
+ or set(value) != {"format", "agent_id", "session_id", "request_id", "start", "stop_seen",
2200
+ "rejected", "integrity_sha256"}
2201
+ or value.get("format") != CODEX_CHILD_FORMAT or value.get("agent_id") != agent_id
2202
+ or not isinstance(value.get("session_id"), str) or not HOST_ID_RE.fullmatch(value["session_id"])
2203
+ or (value.get("request_id") is not None and (
2204
+ not isinstance(value["request_id"], str) or not REQUEST_RE.fullmatch(value["request_id"])))
2205
+ or type(value.get("stop_seen")) is not bool or type(value.get("rejected")) is not bool
2206
+ or value.get("integrity_sha256") != _integrity(value)
2207
+ or (start is not None and (
2208
+ not isinstance(start, dict) or set(start) != {"turn_id", "agent_type"}
2209
+ or start.get("agent_type") != "default" or not isinstance(start.get("turn_id"), str)
2210
+ or not CODEX_UUID_RE.fullmatch(start["turn_id"])))
2211
+ ):
2212
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native Codex child marker failed validation")
2213
+ return value
2214
+
2215
+
2216
+ def _write_codex_child(path: Path, child: dict[str, Any]) -> None:
2217
+ child["integrity_sha256"] = _integrity(child)
2218
+ raw = _canonical(child)
2219
+ if len(raw) > MAX_CHILD_MARKER:
2220
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native Codex child marker is oversized")
2221
+ _atomic_replace(path.parent, Path(path.name), raw)
2222
+
2223
+
2224
+ @contextmanager
2225
+ def _codex_child_lock(agent_id: str, session_id: str):
2226
+ path = _codex_native_marker(agent_id)
2227
+ try:
2228
+ with _request_lock(path.parent, Path(path.name)):
2229
+ yield path, _read_codex_child(path, agent_id, session_id)
2230
+ except OSError as exc:
2231
+ raise AuthorityError("AUTHORITY_BUSY", "native Codex child transition is unavailable") from exc
2232
+
2233
+
2234
+ def _codex_native_refuse(root: Path, request: dict[str, Any], reason: str) -> None:
2235
+ if request["state"] in {"LAUNCHING", "RUNNING"}:
2236
+ _reject(root, request, reason)
2237
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", reason)
2238
+
2239
+
2240
+ def _codex_native_post(event: dict[str, Any], root: Path, request: dict[str, Any]) -> dict[str, Any]:
2241
+ try:
2242
+ return _codex_native_post_join(event, root, request)
2243
+ except (AuthorityError, ValueError, TypeError, RecursionError) as exc:
2244
+ # S/P/C has already identified this exact call. Its first failed Post
2245
+ # cannot be discarded in favor of a later successful replacement.
2246
+ current = _load(root, request["request_id"])
2247
+ if current["state"] in {"LAUNCHING", "RUNNING"}:
2248
+ _reject(root, current, "invalid-first-native-review-post")
2249
+ if isinstance(exc, AuthorityError):
2250
+ raise
2251
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native-v1 spawn result is malformed") from exc
2252
+
2253
+
2254
+ def _codex_native_post_join(event: dict[str, Any], root: Path, request: dict[str, Any]) -> dict[str, Any]:
2255
+ if _canonical(event.get("tool_input")) != _canonical(request["launch_envelope"]):
2256
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native review PostToolUse envelope was changed")
2257
+ _require_frozen_context(root, request)
2258
+ raw = event.get("tool_response")
2259
+ if not isinstance(raw, str) or len(raw.encode("utf-8")) > MAX_DECISION:
2260
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native-v1 spawn result must be a bounded JSON string")
2261
+ try:
2262
+ response = json.loads(raw, object_pairs_hook=_unique_json_pairs, parse_constant=_invalid_json_constant)
2263
+ except ValueError as exc:
2264
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native-v1 spawn result is not strict JSON") from exc
2265
+ if (not isinstance(response, dict) or set(response) != {"agent_id", "nickname"}
2266
+ or (response["nickname"] is not None and (
2267
+ not isinstance(response["nickname"], str) or len(response["nickname"]) > 256))):
2268
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native-v1 spawn result fields are unsupported")
2269
+ agent_id = _codex_uuid(response.get("agent_id"))
2270
+ session_id = request["launch"]["parent_session_id"]
2271
+ with _codex_child_lock(agent_id, session_id) as (path, child):
2272
+ request = _load(root, request["request_id"])
2273
+ if request["state"] != "LAUNCHING" or request["launch"].get("post_confirmed"):
2274
+ _codex_native_refuse(root, request, "native reviewer launch was already associated")
2275
+ if child["request_id"] is not None or child["session_id"] != session_id:
2276
+ _codex_native_refuse(root, request, "native reviewer UUID was already used")
2277
+ child["request_id"] = request["request_id"]
2278
+ _write_codex_child(path, child)
2279
+ if (child["rejected"] or child["stop_seen"] or _codex_claimed(agent_id, "failed")
2280
+ or _codex_claimed(agent_id, "stop") or (child["start"] is not None
2281
+ and child["start"]["turn_id"] == request["launch"]["parent_turn_id"])):
2282
+ _codex_native_refuse(root, request, "native reviewer stopped or conflicted before association")
2283
+ launch = request["launch"]
2284
+ launch.update(agent_id=agent_id, post_confirmed=True)
2285
+ if child["start"] is not None:
2286
+ launch.update(child_turn_id=child["start"]["turn_id"], agent_type="default")
2287
+ request["state"] = "RUNNING"
2288
+ _save(root, request)
2289
+ return {"request_id": request["request_id"], "state": request["state"]}
2290
+
2291
+
2292
+ def _codex_review_complete(root: Path, request: dict[str, Any], agent_id: str, decision: dict[str, Any]) -> dict[str, Any]:
2293
+ payload = {
2294
+ "input_sha256": request["context"]["input_sha256"],
2295
+ "spec_sha256": request["context"]["spec_sha256"], "assessment": decision["assessment"],
2296
+ }
2297
+ if request["activity"] == "spec_review":
2298
+ payload["task_sha256"] = request["context"]["task_sha256"]
2299
+ else:
2300
+ payload["base_input_sha256"] = request["context"]["base_input_sha256"]
2301
+ payload["task_hashes"] = request["context"]["task_hashes"]
2302
+ observation = _closed_observation(
2303
+ request, payload, request.get("codex_review_profile", CODEX_REVIEW_PROFILE), agent_id,
2304
+ {"launch": request["launch"], "decision": decision},
2305
+ )
2306
+ _observation(root, request, observation)
2307
+ request["payload"] = payload
2308
+ request["state"] = "COMPLETED"
2309
+ _save(root, request)
2310
+ return {"request_id": request["request_id"], "state": "COMPLETED"}
2311
+
2312
+
2313
+ def _codex_native_lifecycle(event: dict[str, Any]) -> dict[str, Any] | None:
2314
+ agent_id = _codex_uuid(event.get("agent_id"))
2315
+ try:
2316
+ kind = "start" if event["hook_event_name"] == "SubagentStart" else "stop"
2317
+ if not _claim_codex_event(agent_id, kind):
2318
+ _claim_codex_event(agent_id, "failed")
2319
+ return _codex_native_lifecycle_join(event)
2320
+ except (AuthorityError, OSError) as exc:
2321
+ _claim_codex_event(agent_id, "failed")
2322
+ for root, request in _registered_requests():
2323
+ if (request.get("codex_review_profile") == CODEX_NATIVE_V1
2324
+ and request["state"] in {"LAUNCHING", "RUNNING"}
2325
+ and (request.get("launch") or {}).get("agent_id") == agent_id):
2326
+ _reject(root, request, "failed-native-review-lifecycle")
2327
+ if isinstance(exc, AuthorityError):
2328
+ raise
2329
+ raise AuthorityError("AUTHORITY_BUSY", "native child lifecycle could not be retained") from exc
2330
+
2331
+
2332
+ def _codex_native_lifecycle_join(event: dict[str, Any]) -> dict[str, Any] | None:
2333
+ agent_id = _codex_uuid(event.get("agent_id"))
2334
+ observed_session = event.get("session_id")
2335
+ session_valid = isinstance(observed_session, str) and HOST_ID_RE.fullmatch(observed_session) is not None
2336
+ # A malformed first event with a known child UUID still consumes that
2337
+ # child's evidence. Do not discard it and accept a later replacement Stop.
2338
+ session_id = observed_session if session_valid else "invalid-session"
2339
+ name = event["hook_event_name"]
2340
+ with _codex_child_lock(agent_id, session_id) as (path, child):
2341
+ start = child["start"]
2342
+ valid = (
2343
+ session_valid and child["session_id"] == session_id and not child["rejected"] and not child["stop_seen"]
2344
+ and not _codex_claimed(agent_id, "failed")
2345
+ and event.get("agent_type") == "default" and isinstance(event.get("turn_id"), str)
2346
+ and CODEX_UUID_RE.fullmatch(event["turn_id"]) is not None
2347
+ )
2348
+ if name == "SubagentStart":
2349
+ valid = valid and start is None
2350
+ if valid:
2351
+ child["start"] = {"turn_id": event["turn_id"], "agent_type": "default"}
2352
+ else:
2353
+ valid = (valid and start is not None and start["turn_id"] == event.get("turn_id")
2354
+ and event.get("stop_hook_active") is False and child["request_id"] is not None)
2355
+ # First stop is durable even before Post associates the UUID. A later
2356
+ # follow-up turn/result can never replace evidence we could not join.
2357
+ child["stop_seen"] = True
2358
+ child["rejected"] = child["rejected"] or not valid
2359
+ _write_codex_child(path, child)
2360
+ if child["request_id"] is None:
2361
+ return None
2362
+ root, request = _registered_request(child["request_id"])
2363
+ launch = request.get("launch") or {}
2364
+ if (not valid or request.get("codex_review_profile") != CODEX_NATIVE_V1
2365
+ or launch.get("parent_session_id") != child["session_id"]
2366
+ or launch.get("agent_id") != agent_id or launch.get("post_confirmed") is not True
2367
+ or event.get("turn_id") == launch.get("parent_turn_id")):
2368
+ _codex_native_refuse(root, request, "native reviewer lifecycle is conflicting or out of order")
2369
+ if name == "SubagentStart":
2370
+ if request["state"] != "LAUNCHING":
2371
+ _codex_native_refuse(root, request, "native reviewer start was repeated")
2372
+ launch.update(child_turn_id=event["turn_id"], agent_type="default")
2373
+ request["state"] = "RUNNING"
2374
+ _save(root, request)
2375
+ return {"request_id": request["request_id"], "state": "RUNNING"}
2376
+ if (request["state"] != "RUNNING" or launch.get("child_turn_id") != event["turn_id"]
2377
+ or launch.get("agent_type") != "default"):
2378
+ _codex_native_refuse(root, request, "native reviewer stop is out of order")
2379
+ try:
2380
+ _require_frozen_context(root, request)
2381
+ decision = _parse_decision(event.get("last_assistant_message"), request)
2382
+ except (AuthorityError, TypeError, ValueError, RecursionError) as exc:
2383
+ _reject(root, request, "invalid-first-native-review-stop")
2384
+ if isinstance(exc, AuthorityError):
2385
+ raise
2386
+ raise AuthorityError("INVALID_REVIEW_DECISION", "native first review decision is malformed") from exc
2387
+ launch["first_stop"] = True
2388
+ return _codex_review_complete(root, request, agent_id, decision)
2389
+
2390
+
2391
+ def _codex_steering_uuid(value: Any) -> str | None:
2392
+ # Pinned Codex uses uuid 1.20.0 parse_str: simple, hyphenated, braced,
2393
+ # and lowercase urn:uuid: prefix; hex digits may use either case.
2394
+ # This normalization is solely for denying steering, never lifecycle proof.
2395
+ if not isinstance(value, str):
2396
+ return None
2397
+ if len(value) == 38 and value.startswith("{") and value.endswith("}"):
2398
+ value = value[1:-1]
2399
+ elif len(value) == 45 and value.startswith("urn:uuid:"):
2400
+ value = value[9:]
2401
+ if len(value) == 32 and re.fullmatch(r"[0-9a-fA-F]{32}", value):
2402
+ value = "-".join((value[:8], value[8:12], value[12:16], value[16:20], value[20:]))
2403
+ return value.lower() if CODEX_UUID_RE.fullmatch(value.lower()) else None
2404
+
2405
+
2406
+ def _codex_native_steering(event: dict[str, Any]) -> None:
2407
+ tool_input = event.get("tool_input")
2408
+ field = "id" if event["tool_name"] == "multi_agent_v1resume_agent" else "target"
2409
+ if not isinstance(tool_input, dict):
2410
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native reviewer steering input is malformed")
2411
+ agent_id = _codex_steering_uuid(tool_input.get(field))
2412
+ if agent_id is None:
2413
+ return None
2414
+ _claim_codex_event(agent_id, "failed")
2415
+ with _codex_child_lock(agent_id, _host_id(event.get("session_id"), "session_id")) as (path, child):
2416
+ child["rejected"] = True
2417
+ _write_codex_child(path, child)
2418
+ if child["request_id"] is not None:
2419
+ root, request = _registered_request(child["request_id"])
2420
+ _codex_native_refuse(root, request, "native reviewer cannot be resumed, steered or closed")
2421
+
2422
+
2084
2423
  def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any]:
2085
2424
  _real_root(root) # Invocation repository is observed but cannot select authority.
2086
2425
  if not isinstance(event, dict):
2087
2426
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "hook event is malformed")
2088
2427
  name = event.get("hook_event_name")
2428
+ if (name in {"SubagentStart", "SubagentStop"} and isinstance(event.get("agent_id"), str)
2429
+ and CODEX_UUID_RE.fullmatch(event["agent_id"])):
2430
+ native = _codex_native_lifecycle(event)
2431
+ if native is not None:
2432
+ return native
2089
2433
  session_id = _host_id(event.get("session_id"), "session_id")
2090
2434
 
2435
+ if name in {"PreToolUse", "PostToolUse"} and event.get("tool_name") == "collaborationspawn_agent":
2436
+ tool_input = event.get("tool_input")
2437
+ message = tool_input.get("message") if isinstance(tool_input, dict) else None
2438
+ if isinstance(message, str) and message.startswith("[CODEARBITER_AUTHORITY_REQUEST:"):
2439
+ raise AuthorityError(
2440
+ "UNSUPPORTED_HOST_SEAM",
2441
+ "default Codex v2 has no direct child UUID binding; qualify the native-v1 interface and arm a fresh request",
2442
+ )
2443
+ if name == "PreToolUse":
2444
+ turn_id = _host_id(event.get("turn_id"), "turn_id")
2445
+ _record_ordinary_spawn(session_id, turn_id)
2446
+ _reject_overlapping_authority(session_id, turn_id)
2447
+ return None
2448
+
2449
+ if name in {"PreToolUse", "PostToolUse"} and event.get("tool_name") in CODEX_STEERING_TOOLS:
2450
+ return _codex_native_steering(event)
2451
+
2091
2452
  if name == "PreToolUse":
2092
2453
  if event.get("tool_name") != "spawn_agent":
2093
2454
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "only spawn_agent can launch a reviewer")
@@ -2109,7 +2470,7 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
2109
2470
  or request["state"] != "ARMED"
2110
2471
  ):
2111
2472
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review request is not launchable")
2112
- if tool_input != request.get("launch_envelope"):
2473
+ if _canonical(tool_input) != _canonical(request.get("launch_envelope")):
2113
2474
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch envelope was changed")
2114
2475
  _require_frozen_context(root, request)
2115
2476
  if _ordinary_spawn_marker(session_id, turn_id).exists():
@@ -2127,9 +2488,14 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
2127
2488
  "parent_session_id": session_id, "parent_turn_id": turn_id,
2128
2489
  "tool_use_id": tool_use_id, "post_confirmed": False,
2129
2490
  "agent_id": None, "agent_type": None,
2130
- "task_name": tool_input["task_name"],
2131
- "fork_turns": tool_input["fork_turns"],
2132
2491
  }
2492
+ if request.get("codex_review_profile") == CODEX_NATIVE_V1:
2493
+ request["launch"].update(
2494
+ codex_review_profile=CODEX_NATIVE_V1, child_turn_id=None,
2495
+ fork_context=False,
2496
+ )
2497
+ else:
2498
+ request["launch"].update(task_name=tool_input["task_name"], fork_turns=tool_input["fork_turns"])
2133
2499
  request["state"] = "LAUNCHING"
2134
2500
  _save(root, request)
2135
2501
  return {"request_id": request_id, "state": "LAUNCHING"}
@@ -2139,12 +2505,8 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
2139
2505
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "post-tool event is not reviewer launch")
2140
2506
  tool_use_id = _host_id(event.get("tool_use_id"), "tool_use_id")
2141
2507
  turn_id = _host_id(event.get("turn_id"), "turn_id")
2142
- response = event.get("tool_response")
2143
- if not isinstance(response, dict):
2144
- raise AuthorityError("UNSUPPORTED_HOST_SEAM", "spawn result is malformed")
2145
- task_name = _host_id(response.get("task_name"), "task_name")
2146
2508
  matches = []
2147
- for candidate_root, candidate in _registered_requests():
2509
+ for candidate_root, candidate in _registered_requests(strict=True):
2148
2510
  launch = candidate.get("launch") or {}
2149
2511
  if (
2150
2512
  candidate["state"] in {"LAUNCHING", "RUNNING"}
@@ -2158,6 +2520,12 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
2158
2520
  if len(matches) != 1:
2159
2521
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch correlation is ambiguous")
2160
2522
  root, request = matches[0]
2523
+ if request.get("codex_review_profile") == CODEX_NATIVE_V1:
2524
+ return _codex_native_post(event, root, request)
2525
+ response = event.get("tool_response")
2526
+ if not isinstance(response, dict):
2527
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "spawn result is malformed")
2528
+ task_name = _host_id(response.get("task_name"), "task_name")
2161
2529
  if task_name != request["launch"].get("task_name"):
2162
2530
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "spawn result task_name differs")
2163
2531
  request["launch"]["post_confirmed"] = True
@@ -2170,6 +2538,8 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
2170
2538
  matches = []
2171
2539
  same_turn = []
2172
2540
  for candidate_root, candidate in _registered_requests():
2541
+ if candidate.get("codex_review_profile") == CODEX_NATIVE_V1:
2542
+ continue
2173
2543
  launch = candidate.get("launch") or {}
2174
2544
  if (
2175
2545
  launch.get("parent_session_id") == session_id
@@ -2215,26 +2585,7 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
2215
2585
  request["state"] = "REJECTED"
2216
2586
  _save(root, request)
2217
2587
  raise
2218
- payload = {
2219
- "input_sha256": request["context"]["input_sha256"],
2220
- "spec_sha256": request["context"]["spec_sha256"],
2221
- "assessment": decision["assessment"],
2222
- }
2223
- if request["activity"] == "spec_review":
2224
- payload["task_sha256"] = request["context"]["task_sha256"]
2225
- else:
2226
- payload["base_input_sha256"] = request["context"]["base_input_sha256"]
2227
- payload["task_hashes"] = request["context"]["task_hashes"]
2228
- producer_result = {"launch": request["launch"], "decision": decision}
2229
- observation = _closed_observation(
2230
- request, payload, CODEX_REVIEW_PROFILE, agent_id,
2231
- producer_result,
2232
- )
2233
- _observation(root, request, observation)
2234
- request["payload"] = payload
2235
- request["state"] = "COMPLETED"
2236
- _save(root, request)
2237
- return {"request_id": request["request_id"], "state": "COMPLETED"}
2588
+ return _codex_review_complete(root, request, agent_id, decision)
2238
2589
 
2239
2590
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "hook event is not supported")
2240
2591
 
@@ -345,7 +345,9 @@ def _installed_workflow_preflight(installation: Path) -> dict[str, object]:
345
345
 
346
346
  required = [("UserPromptSubmit", None, "prompt-submit.py")]
347
347
  tools = ("Bash", "Agent") if host == "claude" else (
348
- "Bash", "shell_command", "exec_command", "unified_exec", "spawn_agent")
348
+ "Bash", "shell_command", "exec_command", "unified_exec", "spawn_agent",
349
+ "collaborationspawn_agent", "multi_agent_v1send_input", "multi_agent_v1resume_agent",
350
+ "multi_agent_v1close_agent")
349
351
  required += [(event, tool, "artifact-authority-hook.py")
350
352
  for event in ("PreToolUse", "PostToolUse") for tool in tools]
351
353
  required += [(event, None, "artifact-authority-hook.py")
@@ -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.15.2"
14
+ adapter_version = "0.15.3"
15
15
  update_target = "ca-pi"
16
16
  update_tag_prefix = "ca-pi-v"
17
17
  update_command = "pi update npm:@arbiterforge/ca-pi"
@@ -25,7 +25,7 @@ def main() -> int:
25
25
  tool = payload.get("tool_name")
26
26
  root = _hooklib.project_root(payload)
27
27
  try:
28
- if event in {"PreToolUse", "PostToolUse"} and tool != "spawn_agent":
28
+ if event in {"PreToolUse", "PostToolUse"} and tool not in _artifactauthoritylib.CODEX_REVIEW_TOOLS:
29
29
  result = _artifactauthoritylib.observe_verifier_hook(root, payload)
30
30
  if result is None:
31
31
  return 0
@@ -40,26 +40,9 @@ def main() -> int:
40
40
  else:
41
41
  sys.stderr.write(reason + "\n")
42
42
  return 0
43
- if event == "PreToolUse" and result.get("state") == "AUTHORIZED":
44
- print(json.dumps({
45
- "decision": "allow",
46
- "reason": (
47
- "codeArbiter authorized this wrapper for the exact resolved child set: "
48
- + json.dumps(result["command_bindings"], sort_keys=True)
49
- ),
50
- }))
51
- if event == "SubagentStop" and result.get("state") == "COMPLETED":
52
- print(json.dumps({
53
- "continue": True,
54
- "hookSpecificOutput": {
55
- "hookEventName": "SubagentStop",
56
- "additionalContext": (
57
- "codeArbiter: independent review observation recorded for request "
58
- + result["request_id"]
59
- + "; publish it through the production authority adapter."
60
- ),
61
- },
62
- }))
43
+ # Saved observations are the result. Success is silent: Codex's closed
44
+ # SubagentStop schema has no additionalContext, and an explicit approval
45
+ # would unnecessarily override the host's normal permission processing.
63
46
  return 0
64
47
 
65
48
 
@@ -46,6 +46,10 @@ def main(argv=None) -> int:
46
46
  "--reviewer-model", choices=sorted(_artifactauthoritylib.CLAUDE_REVIEWER_MODELS),
47
47
  default="opus", help="Claude reviewer model pinned into the launch envelope",
48
48
  )
49
+ arm.add_argument(
50
+ "--codex-review-profile", choices=("native-v1",), default=None,
51
+ help="select only after qualifying the actual registered native V1 UUID interface",
52
+ )
49
53
  verify = sub.add_parser("verify")
50
54
  verify.add_argument("--root", required=True)
51
55
  verify.add_argument("--request-id", required=True)
@@ -68,10 +72,17 @@ def main(argv=None) -> int:
68
72
  raise _artifactauthoritylib.AuthorityError(
69
73
  "UNSUPPORTED_HOST_SEAM", f"host {host} has no verification or review authority"
70
74
  )
75
+ if (host == "codex" and args.activity in _artifactauthoritylib.REVIEW_ACTIVITIES
76
+ and args.codex_review_profile is None):
77
+ raise _artifactauthoritylib.AuthorityError(
78
+ "UNSUPPORTED_HOST_SEAM",
79
+ "Codex review requires explicit --codex-review-profile native-v1 after live interface qualification",
80
+ )
71
81
  result = _artifactauthoritylib.arm_request(
72
82
  root, client, args.artifact_id, args.record_id, args.activity,
73
83
  workspace_roots=workspaces or None, host=host,
74
84
  reviewer_model=args.reviewer_model,
85
+ codex_review_profile=args.codex_review_profile,
75
86
  )
76
87
  if host == "claude" and args.activity == "verification":
77
88
  # The Claude verifier hook pins this exact shipped script and