@arbiterforge/ca-pi 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ project context. You decide. codeArbiter enforces.
10
10
  <img alt="Claude Code plugin" src="https://img.shields.io/badge/Claude_Code-plugin-d97757">
11
11
  <img alt="Codex plugin" src="https://img.shields.io/badge/OpenAI_Codex-plugin-10a37f">
12
12
  <img alt="Pi Feature Forge preview" src="https://img.shields.io/badge/ca--pi-Feature_Forge_preview-d97757">
13
- <img alt="version 2.24.0" src="https://img.shields.io/badge/version-2.24.0-2b7489">
13
+ <img alt="version 2.24.1" src="https://img.shields.io/badge/version-2.24.1-2b7489">
14
14
  <img alt="core lanes" src="https://img.shields.io/badge/core_lanes-18-555">
15
15
  <img alt="skills" src="https://img.shields.io/badge/skills-22-555">
16
16
  <img alt="agents" src="https://img.shields.io/badge/agents-20-555">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "license": "AGPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,6 +4,13 @@ All notable changes to `ca-pi` are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.16.1] - 2026-10-04
8
+
9
+ ### Fixed
10
+
11
+ - Carry the shared Claude review-join fix in the vendored authority library. Pi
12
+ behavior is unchanged.
13
+
7
14
  ## [0.16.0] - 2026-10-03
8
15
 
9
16
  ### Added
@@ -37,11 +37,11 @@
37
37
  },
38
38
  "context_qualification": {
39
39
  "darwin/amd64": {
40
- "source_commit": "4a578c5a31f768d4e8437adba31e876687c6fe4e",
40
+ "source_commit": "fa94c56afa59aaa3e429bf15613989bd444090f6",
41
41
  "workflow": ".github/workflows/ci.yml",
42
- "run_id": "37242446770",
42
+ "run_id": "37253197914",
43
43
  "binary_sha256": "27fc51465a79c1525b1300b347cee4d09afde3c7b0d845414659302f5bbca2f7",
44
- "observation_sha256": "a95ce59522b1f4c1552a906f8d50989ca021f6b3017aadead4ca6f337a1cbc0c",
44
+ "observation_sha256": "f6a8c71afa0e06521de263ae49934ce33f4272ed40d0fec6d9a17f7a68357c87",
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": "fa94c56afa59aaa3e429bf15613989bd444090f6",
57
57
  "workflow": ".github/workflows/ci.yml",
58
- "run_id": "37242446770",
58
+ "run_id": "37253197914",
59
59
  "binary_sha256": "3a3d61be18eba426db4f260b9cbae0d3d3c89f47efe184395d7f011cc3b4f59a",
60
- "observation_sha256": "165d4cc2688867896d88ce0973b4fd0155e4214658a0af14024f8cd011868d15",
60
+ "observation_sha256": "2ca37cd149e0efdf73929f0398334089cd3bbb1bba03c61407a485eb8edaecad",
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": "fa94c56afa59aaa3e429bf15613989bd444090f6",
73
73
  "workflow": ".github/workflows/ci.yml",
74
- "run_id": "37242446770",
74
+ "run_id": "37253197914",
75
75
  "binary_sha256": "3b6178bb54d6a19fc56d99f8fced862ae151605a0373297a69e130d3857a8406",
76
- "observation_sha256": "c561f67be64e618d90b42365c83daebc563e185bc428ce483c8199bde1dee6e1",
76
+ "observation_sha256": "85f789dc87517f96042a8e88da689100bcd561ddda863e0986ace221f18dbbab",
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": "fa94c56afa59aaa3e429bf15613989bd444090f6",
89
89
  "workflow": ".github/workflows/ci.yml",
90
- "run_id": "37242446770",
90
+ "run_id": "37253197914",
91
91
  "binary_sha256": "a5259074098932f815f33d3e1dea98e6d9a04277d1ada958469905da67d29437",
92
- "observation_sha256": "79cae85b4701dd7cf924c71e6fac603bf3a7f8422674cee2adf206629321a1cf",
92
+ "observation_sha256": "50afdbe9ce2e9893cc3557fc230010baf5c093da6f0839f17ed15a3c45c69905",
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": "fa94c56afa59aaa3e429bf15613989bd444090f6",
105
105
  "workflow": ".github/workflows/ci.yml",
106
- "run_id": "37242446770",
106
+ "run_id": "37253197914",
107
107
  "binary_sha256": "8339c077262f861631631de49bca7cdafec5e0730b9e3676fd2db36f3c119036",
108
- "observation_sha256": "83937f62110387bb614e0a0b4ad21d84179ffdad24b3de6b78cec21d80d65721",
108
+ "observation_sha256": "56e2f848ba306bbf4acbb2febdfd298e08988761a53b87d9dded7bbf23c9a0ea",
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": "fa94c56afa59aaa3e429bf15613989bd444090f6",
121
121
  "workflow": ".github/workflows/ci.yml",
122
- "run_id": "37242446770",
122
+ "run_id": "37253197914",
123
123
  "binary_sha256": "f7b193cf0d1eb9ecd233bf1588f6290c2fb8bc41ad327918a740beb2099f719c",
124
- "observation_sha256": "7d46e9b33eb4ddd2062b6c1405d5d2679b5be796596e4f3883c530df084cb085",
124
+ "observation_sha256": "5db05023f4fe8441c0e2f664fc0704f42967285a54566f801f9e37f9ad4819a5",
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:
@@ -3216,6 +3219,91 @@ def _claude_bind_child(root: Path, request: dict[str, Any]) -> dict[str, Any]:
3216
3219
  return {"request_id": request["request_id"], "state": "RUNNING"}
3217
3220
 
3218
3221
 
3222
+ @contextmanager
3223
+ def _claude_join_lock():
3224
+ """Serialize the Claude launch/child join across hook processes.
3225
+
3226
+ SubagentStart and an async Agent launch result fire back to back as
3227
+ separate hook processes. Each reads the request and then writes its half
3228
+ of the join; unserialized, either can read before the other writes and
3229
+ both miss, leaving the review LAUNCHING with no start marker."""
3230
+ with ExitStack() as stack:
3231
+ try:
3232
+ stack.enter_context(
3233
+ _request_lock(_registry_root(), Path("claude-join"), wait=CLAUDE_JOIN_LOCK_WAIT_SECONDS)
3234
+ )
3235
+ except OSError as exc:
3236
+ # Only acquisition maps here; the joined body reports its own errors.
3237
+ raise AuthorityError("AUTHORITY_BUSY", "Claude review join is unavailable") from exc
3238
+ yield
3239
+
3240
+
3241
+ def _claude_launch_result(event: dict[str, Any]) -> dict[str, Any] | None:
3242
+ session_id = event.get("session_id")
3243
+ tool_use_id = event.get("tool_use_id")
3244
+ matches = [
3245
+ (candidate_root, candidate)
3246
+ for candidate_root, candidate in _claude_requests({"LAUNCHING"})
3247
+ if (candidate.get("launch") or {}).get("tool_use_id") == tool_use_id
3248
+ and candidate["launch"].get("parent_session_id") == session_id
3249
+ ]
3250
+ if not matches:
3251
+ return None
3252
+ if len(matches) != 1:
3253
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch correlation is ambiguous")
3254
+ root, request = matches[0]
3255
+ response = event.get("tool_response")
3256
+ try:
3257
+ prompt_id = _host_id(event.get("prompt_id"), "prompt_id")
3258
+ if request["launch"].get("parent_prompt_id") != prompt_id or request["launch"]["post_confirmed"]:
3259
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch result is out of order")
3260
+ if not isinstance(response, dict):
3261
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "agent result is malformed")
3262
+ agent_id = _host_id(response.get("agentId"), "agentId")
3263
+ resolved = _model_id(response.get("resolvedModel"), "resolvedModel")
3264
+ except AuthorityError:
3265
+ _reject(root, request, "uncorrelated-launch-result")
3266
+ raise
3267
+ request["launch"].update(post_confirmed=True, agent_id=agent_id, resolved_model=resolved)
3268
+ return _claude_bind_child(root, request)
3269
+
3270
+
3271
+ def _claude_subagent_start(event: dict[str, Any]) -> dict[str, Any] | None:
3272
+ agent_id = _host_id(event.get("agent_id"), "agent_id")
3273
+ agent_type = _host_id(event.get("agent_type"), "agent_type")
3274
+ pending = _claude_requests({"LAUNCHING"})
3275
+ for candidate_root, candidate in pending:
3276
+ launch = candidate.get("launch") or {}
3277
+ if launch.get("agent_id") == agent_id:
3278
+ if agent_type != CLAUDE_REVIEWER:
3279
+ _reject(candidate_root, candidate, "unpinned-reviewer-agent")
3280
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "subagent is not the pinned reviewer")
3281
+ launch["agent_type"] = agent_type
3282
+ candidate["state"] = "RUNNING"
3283
+ _save(candidate_root, candidate)
3284
+ return {"request_id": candidate["request_id"], "state": "RUNNING"}
3285
+ if any((candidate.get("launch") or {}).get("post_confirmed") is False for _root, candidate in pending):
3286
+ # The launch result may not have arrived yet; remember this start.
3287
+ marker = _claude_start_marker(agent_id)
3288
+ data = _canonical({"agent_id": agent_id, "agent_type": agent_type})
3289
+ # Written aside and linked into place: a reader must never see an
3290
+ # empty or partial marker.
3291
+ temporary = marker.with_name(f".{marker.name}.{secrets.token_hex(8)}.tmp")
3292
+ fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
3293
+ try:
3294
+ os.write(fd, data)
3295
+ os.fsync(fd)
3296
+ finally:
3297
+ os.close(fd)
3298
+ try:
3299
+ os.link(temporary, marker)
3300
+ except FileExistsError:
3301
+ return None
3302
+ finally:
3303
+ temporary.unlink(missing_ok=True)
3304
+ return None
3305
+
3306
+
3219
3307
  def observe_claude_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any] | None:
3220
3308
  """Correlate a Claude Code reviewer launch, child lifecycle, and first result.
3221
3309
 
@@ -3275,77 +3363,14 @@ def observe_claude_hook(root: str | Path, event: dict[str, Any]) -> dict[str, An
3275
3363
  return {"request_id": request["request_id"], "state": "LAUNCHING"}
3276
3364
 
3277
3365
  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)
3366
+ with _claude_join_lock():
3367
+ return _claude_launch_result(event)
3305
3368
 
3306
3369
  if name == "SubagentStart":
3307
3370
  if not _claude_requests({"LAUNCHING"}):
3308
3371
  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
3372
+ with _claude_join_lock():
3373
+ return _claude_subagent_start(event)
3349
3374
 
3350
3375
  if name == "PostToolUse" and tool == CLAUDE_HANDBACK_TOOL:
3351
3376
  # Every child's handback lands here; only the bound reviewer's counts.
@@ -3376,8 +3401,12 @@ def observe_claude_hook(root: str | Path, event: dict[str, Any]) -> dict[str, An
3376
3401
  _reject(root, request, "repeated-handback")
3377
3402
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "reviewer reported more than once")
3378
3403
  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)
3404
+ # Defensive: the join lock leaves no late-marker race in this
3405
+ # adapter, but bind any marker still waiting under the same lock.
3406
+ with _claude_join_lock():
3407
+ current = _load(root, request["request_id"])
3408
+ if current["state"] == "LAUNCHING":
3409
+ _claude_bind_child(root, current)
3381
3410
  request = _load(root, request["request_id"])
3382
3411
  candidate = {**request, "handback": record}
3383
3412
  if (
@@ -3414,8 +3443,12 @@ def observe_claude_hook(root: str | Path, event: dict[str, Any]) -> dict[str, An
3414
3443
  root, request = matches[0]
3415
3444
  launch = request["launch"]
3416
3445
  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)
3446
+ # Defensive: the join lock leaves no late-marker race in this
3447
+ # adapter, but bind any marker still waiting under the same lock.
3448
+ with _claude_join_lock():
3449
+ current = _load(root, request["request_id"])
3450
+ if current["state"] == "LAUNCHING":
3451
+ _claude_bind_child(root, current)
3419
3452
  request = _load(root, request["request_id"])
3420
3453
  launch = request["launch"]
3421
3454
  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.1"
15
15
  update_target = "ca-pi"
16
16
  update_tag_prefix = "ca-pi-v"
17
17
  update_command = "pi update npm:@arbiterforge/ca-pi"
@@ -313,7 +313,7 @@ class Host:
313
313
 
314
314
  name = "claude"
315
315
  adapter_name = "ca"
316
- adapter_version = "2.24.0"
316
+ adapter_version = "2.24.1"
317
317
 
318
318
  # Update-notifier descriptor. Each independently versioned host overrides
319
319
  # these three values in its per-plugin _host.py. Keeping the target,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "private": true,
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",