@arbiterforge/ca-pi 0.16.0 → 0.16.2

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
@@ -2,25 +2,25 @@
2
2
 
3
3
  <img src="docs/readme-hero.webp" alt="codeArbiter. Hard gates for agentic coding." width="100%">
4
4
 
5
- **Shared enforcement and project-context parity across Claude Code, Codex CLI, and Pi.**
5
+ **Open-source governance and hard gates for AI coding agents across Claude Code, Codex CLI, and Pi.**
6
6
 
7
- One repository-owned governance layer for tests, reviews, security checks, decisions, and durable
8
- project context. You decide. codeArbiter enforces.
7
+ codeArbiter is a repository-owned governance layer for tests, reviews, security checks, decisions,
8
+ and durable project context. You decide. codeArbiter enforces.
9
9
 
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.24.0" src="https://img.shields.io/badge/version-2.24.0-2b7489">
13
+ <img alt="version 2.24.2" src="https://img.shields.io/badge/version-2.24.2-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">
17
17
  <img alt="license AGPL v3" src="https://img.shields.io/badge/license-AGPL_v3-3da639">
18
18
 
19
- [Start learning](https://arbiterforge.github.io/codeArbiter/learn/)
19
+ [Start learning](https://codearbiter.dev/learn/)
20
20
  &nbsp;&nbsp;·&nbsp;&nbsp;
21
- [Install](https://arbiterforge.github.io/codeArbiter/getting-started/install/)
21
+ [Install](https://codearbiter.dev/getting-started/install/)
22
22
  &nbsp;&nbsp;·&nbsp;&nbsp;
23
- [Browse the reference](https://arbiterforge.github.io/codeArbiter/reference/)
23
+ [Browse the reference](https://codearbiter.dev/reference/)
24
24
 
25
25
  <sub>Install it globally. It stays dormant until a repository explicitly opts in.</sub>
26
26
 
@@ -54,13 +54,13 @@ takes the full spec, test-first, review, and PR path.
54
54
 
55
55
  The Codex path was live-verified on **Codex CLI 0.144.1** with trusted SessionStart injection and a
56
56
  real `[H-03]` PreToolUse block. The dated
57
- [Claude Code + Codex evidence](https://arbiterforge.github.io/codeArbiter/getting-started/claude-code-and-codex/)
57
+ [Claude Code + Codex evidence](https://codearbiter.dev/getting-started/claude-code-and-codex/)
58
58
  separates continuously verified packaging from the live checks that require a model-backed turn.
59
59
 
60
60
  ## See it catch something
61
61
 
62
62
  The landing page includes a
63
- [faithful replay of a shipped hook invocation](https://arbiterforge.github.io/codeArbiter/#proof).
63
+ [faithful replay of a shipped hook invocation](https://codearbiter.dev/#proof).
64
64
  It invokes the real `pre-bash.py` guard, records the source digest, proves the blocked command never
65
65
  ran, and preserves the exact stderr and audit effect. It does not pretend that a direct hook replay
66
66
  proves host discovery or trust.
@@ -87,7 +87,7 @@ The user owns the decision. The repository keeps the evidence.
87
87
  ## Get running
88
88
 
89
89
  Choose the adapter for the coding host you already use. The
90
- [host chooser](https://arbiterforge.github.io/codeArbiter/getting-started/choose-your-host/) covers
90
+ [host chooser](https://codearbiter.dev/getting-started/choose-your-host/) covers
91
91
  stability, command syntax, trust, and platform differences.
92
92
 
93
93
  | Host | Adapter | Command form | Status |
@@ -101,7 +101,7 @@ Git 2.45.0+ with `--no-lazy-fetch` on all three governance hosts; unavailable fl
101
101
  verification with an upgrade prerequisite, never an implicit fetch fallback. Pi also requires Node.js
102
102
  22.19+. If Python is missing, Pi installs its final wrappers but blocks mutating calls and points to
103
103
  `/ca-doctor`; Claude Code and Codex surface an interpreter breadcrumb instead of silently claiming
104
- governance is active. The [compatibility matrix](https://arbiterforge.github.io/codeArbiter/getting-started/compatibility/)
104
+ governance is active. The [compatibility matrix](https://codearbiter.dev/getting-started/compatibility/)
105
105
  defines the supported native Git/runtime and linked-worktree boundary; mixed Windows Git and WSL Git
106
106
  over one shared repository are not supported.
107
107
 
@@ -205,7 +205,7 @@ project to `decompose`. `doctor` must report a healthy interpreter, current payl
205
205
  live-fire probe before you treat the repository as governed.
206
206
 
207
207
  Follow the complete
208
- [first-repository walkthrough](https://arbiterforge.github.io/codeArbiter/getting-started/quickstart/)
208
+ [first-repository walkthrough](https://codearbiter.dev/getting-started/quickstart/)
209
209
  for expected output, proof, and recovery.
210
210
 
211
211
  ## Practice before using your own repository
@@ -225,14 +225,14 @@ The site is designed as a continuous path from first contact to source-backed op
225
225
 
226
226
  | Need | Start here | You finish able to... |
227
227
  |---|---|---|
228
- | Understand the promise | [What is codeArbiter](https://arbiterforge.github.io/codeArbiter/overview/) | explain command, route, dispatch, gate, and ship |
229
- | Choose and install a host | [Start](https://arbiterforge.github.io/codeArbiter/getting-started/choose-your-host/) | install, trust, activate, and verify one adapter |
230
- | Learn in order | [Learning path](https://arbiterforge.github.io/codeArbiter/learn/) | progress from foundation through the power-user capstone |
231
- | Run daily work | [Workflows](https://arbiterforge.github.io/codeArbiter/guides/feature-lane/) | select and complete the smallest correct lane |
232
- | Understand the model | [Concept map](https://arbiterforge.github.io/codeArbiter/concepts/) | trace routing, decisions, context, review, and audit evidence |
233
- | Diagnose a problem | [Troubleshooting](https://arbiterforge.github.io/codeArbiter/guides/troubleshooting/) | follow a symptom to a deterministic recovery |
234
- | Look up exact behavior | [Reference](https://arbiterforge.github.io/codeArbiter/reference/) | inspect generated commands, skills, agents, hooks, and configuration |
235
- | Evaluate preview work | [Feature Forge](https://arbiterforge.github.io/codeArbiter/feature-forge/overview/) | opt in, verify the off switch, and understand the promotion bar |
228
+ | Understand the promise | [What is codeArbiter](https://codearbiter.dev/overview/) | explain command, route, dispatch, gate, and ship |
229
+ | Choose and install a host | [Start](https://codearbiter.dev/getting-started/choose-your-host/) | install, trust, activate, and verify one adapter |
230
+ | Learn in order | [Learning path](https://codearbiter.dev/learn/) | progress from foundation through the power-user capstone |
231
+ | Run daily work | [Workflows](https://codearbiter.dev/guides/feature-lane/) | select and complete the smallest correct lane |
232
+ | Understand the model | [Concept map](https://codearbiter.dev/concepts/) | trace routing, decisions, context, review, and audit evidence |
233
+ | Diagnose a problem | [Troubleshooting](https://codearbiter.dev/guides/troubleshooting/) | follow a symptom to a deterministic recovery |
234
+ | Look up exact behavior | [Reference](https://codearbiter.dev/reference/) | inspect generated commands, skills, agents, hooks, and configuration |
235
+ | Evaluate preview work | [Feature Forge](https://codearbiter.dev/feature-forge/overview/) | opt in, verify the off switch, and understand the promotion bar |
236
236
 
237
237
  Command, skill, agent, and hook-gate reference pages are generated from shipped source on every site
238
238
  build. Hand-authored guides teach the workflow; generated pages define the exact current surface.
@@ -274,7 +274,7 @@ The project record lives beside the code:
274
274
  └── overrides.log append-only sanctioned bypass record
275
275
  ```
276
276
 
277
- The [.codearbiter directory reference](https://arbiterforge.github.io/codeArbiter/codearbiter-directory/)
277
+ The [.codearbiter directory reference](https://codearbiter.dev/codearbiter-directory/)
278
278
  explains who creates, reads, and updates every artifact, plus its recovery rules.
279
279
 
280
280
  ### The hard gates
@@ -302,8 +302,8 @@ recommendation. The user still decides.
302
302
  `/ca:sprint` uses the same lenses for bounded non-hard decisions and writes every call to
303
303
  `.codearbiter/sprint-log.md` with a confidence flag. Security boundaries, irreversible operations,
304
304
  gate bypasses, merges, and unresolved questions remain true stops. See
305
- [SMARTS](https://arbiterforge.github.io/codeArbiter/concepts/smarts/) and
306
- [autonomous sprints](https://arbiterforge.github.io/codeArbiter/guides/autonomous-sprints/).
305
+ [SMARTS](https://codearbiter.dev/concepts/smarts/) and
306
+ [autonomous sprints](https://codearbiter.dev/guides/autonomous-sprints/).
307
307
 
308
308
  ## Core lanes
309
309
 
@@ -322,7 +322,7 @@ compatibility routes remain available in the complete reference.
322
322
  | Operate | `/ca:status`, `/ca:task`, `/ca:doctor`, `/ca:override` | Inspect state, manage the task board, verify enforcement, or log an allowed bypass |
323
323
  <!-- core-lane-chooser:end -->
324
324
 
325
- [Complete command reference](https://arbiterforge.github.io/codeArbiter/reference/commands/) includes
325
+ [Complete command reference](https://codearbiter.dev/reference/commands/) includes
326
326
  advanced operations, compatibility routes, internal protocols, deprecation guidance, and host
327
327
  availability. The generated host catalogs remain inspectable in
328
328
  [`plugins/ca/COMMANDS.md`](./plugins/ca/COMMANDS.md),
@@ -348,8 +348,8 @@ The optional `/ca:sprint --farm` preview is a distinct opt-in network feature. I
348
348
  secret-redacted task context to the OpenAI-compatible endpoint you configure and is inert without
349
349
  the flag and provider key.
350
350
 
351
- See [Enforcement and Security](https://arbiterforge.github.io/codeArbiter/enforcement/) and the
352
- [Hooks Reference](https://arbiterforge.github.io/codeArbiter/hooks/) for the exact event, read,
351
+ See [Enforcement and Security](https://codearbiter.dev/enforcement/) and the
352
+ [Hooks Reference](https://codearbiter.dev/hooks/) for the exact event, read,
353
353
  write, block, and network contract.
354
354
 
355
355
  ### Pi operational contract
@@ -387,7 +387,7 @@ spending or trust requirements.
387
387
  | `ca-sandbox` | install the sibling plugin | real untrusted repositories and interactive `--with-claude` use |
388
388
 
389
389
  The source-backed
390
- [live Forge catalog](https://arbiterforge.github.io/codeArbiter/feature-forge/whats-in-the-forge/)
390
+ [live Forge catalog](https://codearbiter.dev/feature-forge/whats-in-the-forge/)
391
391
  names every opt-in, off switch, dependency, trust boundary, and promotion signal.
392
392
 
393
393
  The farm preview uses one checked-in `farm.js` backend across supported hosts. A Pi-native embedded
@@ -424,8 +424,8 @@ Update the Claude marketplace explicitly:
424
424
 
425
425
  The operator-facing environment-variable catalog, defaults, accepted values, safety boundaries,
426
426
  and verification steps live in the
427
- [Configuration Reference](https://arbiterforge.github.io/codeArbiter/reference/configuration/).
428
- Release notes are in the [Changelog](https://arbiterforge.github.io/codeArbiter/changelog/).
427
+ [Configuration Reference](https://codearbiter.dev/reference/configuration/).
428
+ Release notes are in the [Changelog](https://codearbiter.dev/changelog/).
429
429
 
430
430
  </details>
431
431
 
@@ -437,7 +437,7 @@ with Claude Code's plugin manager, run `codex plugin remove ca-codex@codearbiter
437
437
  `pi remove` with the pinned Git source for Pi. The repository-owned `.codearbiter/` record survives,
438
438
  so another supported host can keep using it.
439
439
 
440
- Follow [Uninstall and Disable](https://arbiterforge.github.io/codeArbiter/guides/uninstalling/) for
440
+ Follow [Uninstall and Disable](https://codearbiter.dev/guides/uninstalling/) for
441
441
  host-specific removal, pinning, and verification.
442
442
 
443
443
  </details>
@@ -499,7 +499,7 @@ Inspect the repository, grant project trust, start a fresh Pi session, then run
499
499
 
500
500
  codeArbiter v2 is a ground-up rebuild of the earlier vendored `.agents/` framework as a native,
501
501
  multi-host plugin system. The complete record is in [`CHANGELOG.md`](./CHANGELOG.md) and the
502
- [site changelog](https://arbiterforge.github.io/codeArbiter/changelog/). The v1 implementation
502
+ [site changelog](https://codearbiter.dev/changelog/). The v1 implementation
503
503
  remains available in early repository history.
504
504
 
505
505
  ## License and contributions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "license": "AGPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,6 +4,21 @@ All notable changes to `ca-pi` are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.16.2] - 2026-10-04
8
+
9
+ ### Fixed
10
+
11
+ - Refresh the shared prerequisite deadline and reviewer finding contract.
12
+ - Correct approval guidance: Pi has an input event, but still needs a native
13
+ authority adapter before these workflows can run on Pi.
14
+
15
+ ## [0.16.1] - 2026-10-04
16
+
17
+ ### Fixed
18
+
19
+ - Carry the shared Claude review-join fix in the vendored authority library. Pi
20
+ behavior is unchanged.
21
+
7
22
  ## [0.16.0] - 2026-10-03
8
23
 
9
24
  ### Added
@@ -37,11 +37,11 @@
37
37
  },
38
38
  "context_qualification": {
39
39
  "darwin/amd64": {
40
- "source_commit": "4a578c5a31f768d4e8437adba31e876687c6fe4e",
40
+ "source_commit": "ed51f5030af5520d5ae67486c3c7776b76bcfae9",
41
41
  "workflow": ".github/workflows/ci.yml",
42
- "run_id": "37242446770",
42
+ "run_id": "37265481193",
43
43
  "binary_sha256": "27fc51465a79c1525b1300b347cee4d09afde3c7b0d845414659302f5bbca2f7",
44
- "observation_sha256": "a95ce59522b1f4c1552a906f8d50989ca021f6b3017aadead4ca6f337a1cbc0c",
44
+ "observation_sha256": "d0fb266f27d266e247c38e806336d94f60581a32e6f84602bdcab2ca4caa3e20",
45
45
  "module_sha256": "d5cccd13647e2c3a2a752b94b34dec4d4fc6aec5ced8fb0abce6ec9346ba1a7e",
46
46
  "native_cases": [
47
47
  "TestContextBoundedRender",
@@ -53,11 +53,11 @@
53
53
  ]
54
54
  },
55
55
  "darwin/arm64": {
56
- "source_commit": "4a578c5a31f768d4e8437adba31e876687c6fe4e",
56
+ "source_commit": "ed51f5030af5520d5ae67486c3c7776b76bcfae9",
57
57
  "workflow": ".github/workflows/ci.yml",
58
- "run_id": "37242446770",
58
+ "run_id": "37265481193",
59
59
  "binary_sha256": "3a3d61be18eba426db4f260b9cbae0d3d3c89f47efe184395d7f011cc3b4f59a",
60
- "observation_sha256": "165d4cc2688867896d88ce0973b4fd0155e4214658a0af14024f8cd011868d15",
60
+ "observation_sha256": "f90633dfe08215876ef5dbfd9ebddb2b20c48eb334dc52efb4e10af65216d9fa",
61
61
  "module_sha256": "d5cccd13647e2c3a2a752b94b34dec4d4fc6aec5ced8fb0abce6ec9346ba1a7e",
62
62
  "native_cases": [
63
63
  "TestContextBoundedRender",
@@ -69,11 +69,11 @@
69
69
  ]
70
70
  },
71
71
  "linux/amd64": {
72
- "source_commit": "4a578c5a31f768d4e8437adba31e876687c6fe4e",
72
+ "source_commit": "ed51f5030af5520d5ae67486c3c7776b76bcfae9",
73
73
  "workflow": ".github/workflows/ci.yml",
74
- "run_id": "37242446770",
74
+ "run_id": "37265481193",
75
75
  "binary_sha256": "3b6178bb54d6a19fc56d99f8fced862ae151605a0373297a69e130d3857a8406",
76
- "observation_sha256": "c561f67be64e618d90b42365c83daebc563e185bc428ce483c8199bde1dee6e1",
76
+ "observation_sha256": "0a58c6c7a8e5673f602398c3a5ac477b00d0477e981e43aa0b36f1061683e412",
77
77
  "module_sha256": "d5cccd13647e2c3a2a752b94b34dec4d4fc6aec5ced8fb0abce6ec9346ba1a7e",
78
78
  "native_cases": [
79
79
  "TestContextBoundedRender",
@@ -85,11 +85,11 @@
85
85
  ]
86
86
  },
87
87
  "linux/arm64": {
88
- "source_commit": "4a578c5a31f768d4e8437adba31e876687c6fe4e",
88
+ "source_commit": "ed51f5030af5520d5ae67486c3c7776b76bcfae9",
89
89
  "workflow": ".github/workflows/ci.yml",
90
- "run_id": "37242446770",
90
+ "run_id": "37265481193",
91
91
  "binary_sha256": "a5259074098932f815f33d3e1dea98e6d9a04277d1ada958469905da67d29437",
92
- "observation_sha256": "79cae85b4701dd7cf924c71e6fac603bf3a7f8422674cee2adf206629321a1cf",
92
+ "observation_sha256": "9cccf649f682ee7c0d62ed6a0fb4336479eb7f24a9977027811954d77f3c2efe",
93
93
  "module_sha256": "d5cccd13647e2c3a2a752b94b34dec4d4fc6aec5ced8fb0abce6ec9346ba1a7e",
94
94
  "native_cases": [
95
95
  "TestContextBoundedRender",
@@ -101,11 +101,11 @@
101
101
  ]
102
102
  },
103
103
  "windows/amd64": {
104
- "source_commit": "4a578c5a31f768d4e8437adba31e876687c6fe4e",
104
+ "source_commit": "ed51f5030af5520d5ae67486c3c7776b76bcfae9",
105
105
  "workflow": ".github/workflows/ci.yml",
106
- "run_id": "37242446770",
106
+ "run_id": "37265481193",
107
107
  "binary_sha256": "8339c077262f861631631de49bca7cdafec5e0730b9e3676fd2db36f3c119036",
108
- "observation_sha256": "83937f62110387bb614e0a0b4ad21d84179ffdad24b3de6b78cec21d80d65721",
108
+ "observation_sha256": "843fdcf7c1f8b171abbb3a6ed0d636a755ccd3898e24a33e7d651daaa1b233fc",
109
109
  "module_sha256": "d5cccd13647e2c3a2a752b94b34dec4d4fc6aec5ced8fb0abce6ec9346ba1a7e",
110
110
  "native_cases": [
111
111
  "TestContextBoundedRender",
@@ -117,11 +117,11 @@
117
117
  ]
118
118
  },
119
119
  "windows/arm64": {
120
- "source_commit": "4a578c5a31f768d4e8437adba31e876687c6fe4e",
120
+ "source_commit": "ed51f5030af5520d5ae67486c3c7776b76bcfae9",
121
121
  "workflow": ".github/workflows/ci.yml",
122
- "run_id": "37242446770",
122
+ "run_id": "37265481193",
123
123
  "binary_sha256": "f7b193cf0d1eb9ecd233bf1588f6290c2fb8bc41ad327918a740beb2099f719c",
124
- "observation_sha256": "7d46e9b33eb4ddd2062b6c1405d5d2679b5be796596e4f3883c530df084cb085",
124
+ "observation_sha256": "72aa77d09842772a7918b1ebd3ba6d2d7ecfefc24256cd557fb66949fef7b0ec",
125
125
  "module_sha256": "d5cccd13647e2c3a2a752b94b34dec4d4fc6aec5ced8fb0abce6ec9346ba1a7e",
126
126
  "native_cases": [
127
127
  "TestContextBoundedRender",
@@ -29,7 +29,7 @@ than one SubagentStop; only the first is a review result.
29
29
 
30
30
  from __future__ import annotations
31
31
 
32
- from contextlib import contextmanager
32
+ from contextlib import ExitStack, contextmanager
33
33
  import base64
34
34
  import errno
35
35
  import hashlib
@@ -77,6 +77,9 @@ MAX_STATE = 1 << 20
77
77
  # Only terminal abandonment can read a legacy inline-context review this large.
78
78
  MAX_RECOVERY_STATE = 2 << 20
79
79
  REQUEST_LOCK_WAIT_SECONDS = 5.0
80
+ # The Claude join lock is per user, not per request: every SubagentStart waits
81
+ # on it while any review is LAUNCHING, and the holder scans the registry.
82
+ CLAUDE_JOIN_LOCK_WAIT_SECONDS = 30.0
80
83
  MAX_OUTPUT = 8 << 20
81
84
  MAX_DECISION = 65536
82
85
  MAX_COMPLETION_FILES = 16
@@ -325,7 +328,7 @@ def _spool_root(root: Path) -> Path:
325
328
 
326
329
 
327
330
  @contextmanager
328
- def _request_lock(spool: Path, relative: Path):
331
+ def _request_lock(spool: Path, relative: Path, wait: float | None = None):
329
332
  """Keep one OS-owned lock inode for all writers of this request."""
330
333
  path = spool / f"{relative.name}.lock"
331
334
  fd = None
@@ -355,7 +358,7 @@ def _request_lock(spool: Path, relative: Path):
355
358
  locked = False
356
359
  try:
357
360
  check_path()
358
- deadline = time.monotonic() + REQUEST_LOCK_WAIT_SECONDS
361
+ deadline = time.monotonic() + (REQUEST_LOCK_WAIT_SECONDS if wait is None else wait)
359
362
  if os.name == "nt":
360
363
  import msvcrt
361
364
  else:
@@ -1322,6 +1325,8 @@ def _dispatch_prompt(request: dict[str, Any], *, legacy_inline: bool = False) ->
1322
1325
  "HTML to reconstruct artifact authority. "
1323
1326
  "Do not pass if the evidence is missing, unreadable or inconsistent. The locator "
1324
1327
  "is a reference to the complete immutable evidence, not replacement authority.\n"
1328
+ "Each finding has exactly severity, code, message; severity must be BLOCK, WARN, or INFO. "
1329
+ "code and message must be non-empty strings. Use an empty findings list when there are none.\n"
1325
1330
  )
1326
1331
  profile = (
1327
1332
  f"Native review profile: {CODEX_NATIVE_V1}. This review was dispatched through "
@@ -3216,6 +3221,91 @@ def _claude_bind_child(root: Path, request: dict[str, Any]) -> dict[str, Any]:
3216
3221
  return {"request_id": request["request_id"], "state": "RUNNING"}
3217
3222
 
3218
3223
 
3224
+ @contextmanager
3225
+ def _claude_join_lock():
3226
+ """Serialize the Claude launch/child join across hook processes.
3227
+
3228
+ SubagentStart and an async Agent launch result fire back to back as
3229
+ separate hook processes. Each reads the request and then writes its half
3230
+ of the join; unserialized, either can read before the other writes and
3231
+ both miss, leaving the review LAUNCHING with no start marker."""
3232
+ with ExitStack() as stack:
3233
+ try:
3234
+ stack.enter_context(
3235
+ _request_lock(_registry_root(), Path("claude-join"), wait=CLAUDE_JOIN_LOCK_WAIT_SECONDS)
3236
+ )
3237
+ except OSError as exc:
3238
+ # Only acquisition maps here; the joined body reports its own errors.
3239
+ raise AuthorityError("AUTHORITY_BUSY", "Claude review join is unavailable") from exc
3240
+ yield
3241
+
3242
+
3243
+ def _claude_launch_result(event: dict[str, Any]) -> dict[str, Any] | None:
3244
+ session_id = event.get("session_id")
3245
+ tool_use_id = event.get("tool_use_id")
3246
+ matches = [
3247
+ (candidate_root, candidate)
3248
+ for candidate_root, candidate in _claude_requests({"LAUNCHING"})
3249
+ if (candidate.get("launch") or {}).get("tool_use_id") == tool_use_id
3250
+ and candidate["launch"].get("parent_session_id") == session_id
3251
+ ]
3252
+ if not matches:
3253
+ return None
3254
+ if len(matches) != 1:
3255
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch correlation is ambiguous")
3256
+ root, request = matches[0]
3257
+ response = event.get("tool_response")
3258
+ try:
3259
+ prompt_id = _host_id(event.get("prompt_id"), "prompt_id")
3260
+ if request["launch"].get("parent_prompt_id") != prompt_id or request["launch"]["post_confirmed"]:
3261
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch result is out of order")
3262
+ if not isinstance(response, dict):
3263
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "agent result is malformed")
3264
+ agent_id = _host_id(response.get("agentId"), "agentId")
3265
+ resolved = _model_id(response.get("resolvedModel"), "resolvedModel")
3266
+ except AuthorityError:
3267
+ _reject(root, request, "uncorrelated-launch-result")
3268
+ raise
3269
+ request["launch"].update(post_confirmed=True, agent_id=agent_id, resolved_model=resolved)
3270
+ return _claude_bind_child(root, request)
3271
+
3272
+
3273
+ def _claude_subagent_start(event: dict[str, Any]) -> dict[str, Any] | None:
3274
+ agent_id = _host_id(event.get("agent_id"), "agent_id")
3275
+ agent_type = _host_id(event.get("agent_type"), "agent_type")
3276
+ pending = _claude_requests({"LAUNCHING"})
3277
+ for candidate_root, candidate in pending:
3278
+ launch = candidate.get("launch") or {}
3279
+ if launch.get("agent_id") == agent_id:
3280
+ if agent_type != CLAUDE_REVIEWER:
3281
+ _reject(candidate_root, candidate, "unpinned-reviewer-agent")
3282
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "subagent is not the pinned reviewer")
3283
+ launch["agent_type"] = agent_type
3284
+ candidate["state"] = "RUNNING"
3285
+ _save(candidate_root, candidate)
3286
+ return {"request_id": candidate["request_id"], "state": "RUNNING"}
3287
+ if any((candidate.get("launch") or {}).get("post_confirmed") is False for _root, candidate in pending):
3288
+ # The launch result may not have arrived yet; remember this start.
3289
+ marker = _claude_start_marker(agent_id)
3290
+ data = _canonical({"agent_id": agent_id, "agent_type": agent_type})
3291
+ # Written aside and linked into place: a reader must never see an
3292
+ # empty or partial marker.
3293
+ temporary = marker.with_name(f".{marker.name}.{secrets.token_hex(8)}.tmp")
3294
+ fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
3295
+ try:
3296
+ os.write(fd, data)
3297
+ os.fsync(fd)
3298
+ finally:
3299
+ os.close(fd)
3300
+ try:
3301
+ os.link(temporary, marker)
3302
+ except FileExistsError:
3303
+ return None
3304
+ finally:
3305
+ temporary.unlink(missing_ok=True)
3306
+ return None
3307
+
3308
+
3219
3309
  def observe_claude_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any] | None:
3220
3310
  """Correlate a Claude Code reviewer launch, child lifecycle, and first result.
3221
3311
 
@@ -3275,77 +3365,14 @@ def observe_claude_hook(root: str | Path, event: dict[str, Any]) -> dict[str, An
3275
3365
  return {"request_id": request["request_id"], "state": "LAUNCHING"}
3276
3366
 
3277
3367
  if name == "PostToolUse" and tool == "Agent":
3278
- session_id = event.get("session_id")
3279
- tool_use_id = event.get("tool_use_id")
3280
- matches = [
3281
- (candidate_root, candidate)
3282
- for candidate_root, candidate in _claude_requests({"LAUNCHING"})
3283
- if (candidate.get("launch") or {}).get("tool_use_id") == tool_use_id
3284
- and candidate["launch"].get("parent_session_id") == session_id
3285
- ]
3286
- if not matches:
3287
- return None
3288
- if len(matches) != 1:
3289
- raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch correlation is ambiguous")
3290
- root, request = matches[0]
3291
- response = event.get("tool_response")
3292
- try:
3293
- prompt_id = _host_id(event.get("prompt_id"), "prompt_id")
3294
- if request["launch"].get("parent_prompt_id") != prompt_id or request["launch"]["post_confirmed"]:
3295
- raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch result is out of order")
3296
- if not isinstance(response, dict):
3297
- raise AuthorityError("UNSUPPORTED_HOST_SEAM", "agent result is malformed")
3298
- agent_id = _host_id(response.get("agentId"), "agentId")
3299
- resolved = _model_id(response.get("resolvedModel"), "resolvedModel")
3300
- except AuthorityError:
3301
- _reject(root, request, "uncorrelated-launch-result")
3302
- raise
3303
- request["launch"].update(post_confirmed=True, agent_id=agent_id, resolved_model=resolved)
3304
- return _claude_bind_child(root, request)
3368
+ with _claude_join_lock():
3369
+ return _claude_launch_result(event)
3305
3370
 
3306
3371
  if name == "SubagentStart":
3307
3372
  if not _claude_requests({"LAUNCHING"}):
3308
3373
  return None
3309
- agent_id = _host_id(event.get("agent_id"), "agent_id")
3310
- agent_type = _host_id(event.get("agent_type"), "agent_type")
3311
- for candidate_root, candidate in _claude_requests({"LAUNCHING"}):
3312
- launch = candidate.get("launch") or {}
3313
- if launch.get("agent_id") == agent_id:
3314
- if agent_type != CLAUDE_REVIEWER:
3315
- _reject(candidate_root, candidate, "unpinned-reviewer-agent")
3316
- raise AuthorityError("UNSUPPORTED_HOST_SEAM", "subagent is not the pinned reviewer")
3317
- launch["agent_type"] = agent_type
3318
- candidate["state"] = "RUNNING"
3319
- _save(candidate_root, candidate)
3320
- return {"request_id": candidate["request_id"], "state": "RUNNING"}
3321
- if any(
3322
- (candidate.get("launch") or {}).get("post_confirmed") is False
3323
- for _root, candidate in _claude_requests({"LAUNCHING"})
3324
- ):
3325
- # The launch result may not have arrived yet; remember this start.
3326
- marker = _claude_start_marker(agent_id)
3327
- data = _canonical({"agent_id": agent_id, "agent_type": agent_type})
3328
- # Written aside and linked into place: a concurrent launch result
3329
- # joining this agent must never read an empty or partial marker.
3330
- temporary = marker.with_name(f".{marker.name}.{secrets.token_hex(8)}.tmp")
3331
- fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
3332
- try:
3333
- os.write(fd, data)
3334
- os.fsync(fd)
3335
- finally:
3336
- os.close(fd)
3337
- try:
3338
- os.link(temporary, marker)
3339
- except FileExistsError:
3340
- return None
3341
- finally:
3342
- temporary.unlink(missing_ok=True)
3343
- # The launch result may have bound this agent while the marker was
3344
- # being written; finish the join now rather than strand it.
3345
- for candidate_root, candidate in _claude_requests({"LAUNCHING"}):
3346
- if (candidate.get("launch") or {}).get("agent_id") == agent_id:
3347
- return _claude_bind_child(candidate_root, candidate)
3348
- return None
3374
+ with _claude_join_lock():
3375
+ return _claude_subagent_start(event)
3349
3376
 
3350
3377
  if name == "PostToolUse" and tool == CLAUDE_HANDBACK_TOOL:
3351
3378
  # Every child's handback lands here; only the bound reviewer's counts.
@@ -3376,8 +3403,12 @@ def observe_claude_hook(root: str | Path, event: dict[str, Any]) -> dict[str, An
3376
3403
  _reject(root, request, "repeated-handback")
3377
3404
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "reviewer reported more than once")
3378
3405
  if request["state"] == "LAUNCHING" and request["launch"].get("post_confirmed") is True:
3379
- # A start marker written late in the launch/start race.
3380
- _claude_bind_child(root, request)
3406
+ # Defensive: the join lock leaves no late-marker race in this
3407
+ # adapter, but bind any marker still waiting under the same lock.
3408
+ with _claude_join_lock():
3409
+ current = _load(root, request["request_id"])
3410
+ if current["state"] == "LAUNCHING":
3411
+ _claude_bind_child(root, current)
3381
3412
  request = _load(root, request["request_id"])
3382
3413
  candidate = {**request, "handback": record}
3383
3414
  if (
@@ -3414,8 +3445,12 @@ def observe_claude_hook(root: str | Path, event: dict[str, Any]) -> dict[str, An
3414
3445
  root, request = matches[0]
3415
3446
  launch = request["launch"]
3416
3447
  if request["state"] == "LAUNCHING" and launch.get("post_confirmed") is True:
3417
- # A start marker written late in the launch/start race.
3418
- _claude_bind_child(root, request)
3448
+ # Defensive: the join lock leaves no late-marker race in this
3449
+ # adapter, but bind any marker still waiting under the same lock.
3450
+ with _claude_join_lock():
3451
+ current = _load(root, request["request_id"])
3452
+ if current["state"] == "LAUNCHING":
3453
+ _claude_bind_child(root, current)
3419
3454
  request = _load(root, request["request_id"])
3420
3455
  launch = request["launch"]
3421
3456
  try:
@@ -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.16.0"
14
+ adapter_version = "0.16.2"
15
15
  update_target = "ca-pi"
16
16
  update_tag_prefix = "ca-pi-v"
17
17
  update_command = "pi update npm:@arbiterforge/ca-pi"
@@ -48,7 +48,7 @@ PENDING_DIR = Path(".codearbiter/.markers/pending-prerequisites")
48
48
  SOURCE_DIR = Path(".codearbiter/.artifacts/authority-sources")
49
49
  FORMAT = "codearbiter.pending-user-prerequisite/0.1.0"
50
50
  POLICY = "explicit-user-prerequisite-confirmation/0.1.0"
51
- CONFIRMATION_LIFETIME_SECONDS = 15 * 60
51
+ CONFIRMATION_LIFETIME_SECONDS = _replylib.CODE_TTL_SECONDS
52
52
  CONFIRMATION_RE = re.compile(r"[A-Za-z0-9_-]{12,128}")
53
53
  ID_RE = re.compile(r"[A-Z][A-Z0-9_-]{0,127}")
54
54
  HOST_RE = re.compile(r"[A-Za-z0-9_-]{1,32}")
@@ -620,9 +620,12 @@ def arm_user_prerequisite(
620
620
  "requirement": record["requirement"],
621
621
  "review_packet_sha256": packet_sha256,
622
622
  "reply": reply,
623
+ "expires_at": pending["expires_at"],
624
+ "expires_at_utc": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime(pending["expires_at"])),
623
625
  **_replylib.offer_code(root, "prerequisite", artifact_id, reply,
624
626
  names=[artifact_id, prerequisite_id],
625
- short_prefix=["satisfy-prerequisite"]),
627
+ short_prefix=["satisfy-prerequisite"],
628
+ expires_at=pending["expires_at"]),
626
629
  "pending": relative.as_posix(),
627
630
  }
628
631
 
@@ -652,7 +655,7 @@ def _confirm(
652
655
  session_id: str,
653
656
  now: int,
654
657
  ) -> dict[str, Any]:
655
- if now > pending["expires_at"]:
658
+ if now >= pending["expires_at"]:
656
659
  raise PrerequisiteError(
657
660
  "EXPIRED_PREREQUISITE", "the prerequisite confirmation request expired"
658
661
  )
@@ -207,7 +207,8 @@ def _write(path: Path, value: dict) -> None:
207
207
 
208
208
 
209
209
  def issue_code(root, route: str, artifact_id: str, full_reply: str, *,
210
- names: list[str], short_prefix: list[str]) -> dict[str, str]:
210
+ names: list[str], short_prefix: list[str],
211
+ expires_at: int | None = None) -> dict[str, str]:
211
212
  """Issue a unique four-character code expanding to ``full_reply``."""
212
213
  if route not in ROUTES or not isinstance(full_reply, str) or canonicalize(full_reply) != full_reply:
213
214
  raise ReplyCodeError("reply code request is invalid")
@@ -223,20 +224,22 @@ def issue_code(root, route: str, artifact_id: str, full_reply: str, *,
223
224
  raise ReplyCodeError("no free reply code")
224
225
  value = {"format": FORMAT, "route": route, "artifact_id": artifact_id, "repository": repository,
225
226
  "short_reply": short, "full_reply": full_reply, "names": list(names),
226
- "expires_at": int(time.time()) + CODE_TTL_SECONDS}
227
+ "expires_at": int(time.time()) + CODE_TTL_SECONDS if expires_at is None else expires_at}
227
228
  value["integrity_sha256"] = _digest(_canonical(value))
228
229
  _write(path, value)
229
230
  return {"code": code, "short_reply": short}
230
231
 
231
232
 
232
233
  def offer_code(root, route: str, artifact_id: str, full_reply: str, *,
233
- names: list[str], short_prefix: list[str]) -> dict[str, str]:
234
+ names: list[str], short_prefix: list[str],
235
+ expires_at: int | None = None) -> dict[str, str]:
234
236
  """Arm-result fields for a short code; empty when one cannot be issued.
235
237
 
236
238
  The code is a convenience: failing to issue one never blocks arming, and
237
239
  the full reply always remains accepted."""
238
240
  try:
239
- issued = issue_code(root, route, artifact_id, full_reply, names=names, short_prefix=short_prefix)
241
+ issued = issue_code(root, route, artifact_id, full_reply, names=names,
242
+ short_prefix=short_prefix, expires_at=expires_at)
240
243
  except (ReplyCodeError, OSError):
241
244
  return {}
242
245
  return {"code": issued["code"], "short_reply": issued["short_reply"]}
@@ -313,7 +313,7 @@ class Host:
313
313
 
314
314
  name = "claude"
315
315
  adapter_name = "ca"
316
- adapter_version = "2.24.0"
316
+ adapter_version = "2.24.2"
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,
@@ -99,9 +99,9 @@ limit or excluding source/test/governance inputs needed for the task.
99
99
  The existing user, SMARTS, reviewer, or verification boundary must first persist
100
100
  its actual policy-owned workflow event as canonical JSON in the reserved
101
101
  content-addressed authority-source store. Interactive prompt approval currently
102
- has production host seams in Claude Code and Codex. Pi 0.84.1 exposes no
103
- pre-model event carrying the user's exact prompt, so under Pi do not arm this
104
- adapter; the workflow remains blocked until a Pi-native authority adapter is
102
+ has production host seams in Claude Code and Codex. Pi 1.0.0 exposes an `input`
103
+ event, but codeArbiter has no Pi-native authority adapter. Under Pi do not arm this
104
+ adapter; the workflow remains blocked until that adapter is
105
105
  implemented. On Claude Code or Codex, arm the exact current artifact immediately
106
106
  before asking the approval question:
107
107
 
@@ -180,8 +180,9 @@ current approved plan and prerequisite:
180
180
  python "<plugin-root>/hooks/_prerequisitelib.py" arm --root "<project-root>" --artifact-id <artifact-id> --prerequisite-id <prerequisite-id>
181
181
  ```
182
182
 
183
- Show the returned title, requirement, and review-packet digest, then require the
184
- returned `short_reply` (`satisfy-prerequisite <code>`) or the full `reply`, with
183
+ Show the returned title, requirement, review-packet digest, and `expires_at_utc`
184
+ deadline. The request and its reply code expire together 24 hours after arming.
185
+ Require the returned `short_reply` (`satisfy-prerequisite <code>`) or the full `reply`, with
185
186
  the same tolerance and mismatch reporting as approval. The `UserPromptSubmit` hook
186
187
  durably records that observed decision before publishing its authority source,
187
188
  captures a prerequisite receipt, and applies the existing prerequisite CAS
@@ -211,7 +212,7 @@ python "<plugin-root>/hooks/_prerequisitelib.py" supersede --root "<project-root
211
212
  The adapter fixes `authority_kind: user_workflow` and `verdict: satisfied`;
212
213
  there is no caller-selected authority or bulk-satisfaction path. SMARTS prerequisite satisfaction remains unsupported; the plan-method producer below does not
213
214
  grant prerequisite, security, backend or publication authority. Pi has no
214
- qualified pre-model prompt seam and must remain blocked. Do not infer eligibility from one satisfied prerequisite:
215
+ prerequisite-capture adapter and must remain blocked. Do not infer eligibility from one satisfied prerequisite:
215
216
  rerun `eligible`, and resolve each remaining record independently against the
216
217
  new current plan identity.
217
218
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "private": true,
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",