@arbiterforge/ca-pi 0.15.4 → 0.15.6

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.22.4" src="https://img.shields.io/badge/version-2.22.4-2b7489">
13
+ <img alt="version 2.22.6" src="https://img.shields.io/badge/version-2.22.6-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.15.4",
3
+ "version": "0.15.6",
4
4
  "license": "AGPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,6 +4,20 @@ All notable changes to `ca-pi` are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.15.6] - 2026-09-27
8
+
9
+ ### Fixed
10
+
11
+ - Keep reconciliation bookkeeping outside verification inputs so an unchanged
12
+ workspace can be reconciled without locked-file failures or stale approvals.
13
+
14
+ ## [0.15.5] - 2026-09-26
15
+
16
+ ### Fixed
17
+
18
+ - Explain input-manifest entry kinds to independent reviewers so normative artifact
19
+ identities are not compared with raw HTML hashes.
20
+
7
21
  ## [0.15.4] - 2026-09-26
8
22
 
9
23
  ### Fixed
@@ -1117,6 +1117,16 @@ def _dispatch_prompt(request: dict[str, Any], *, legacy_inline: bool = False) ->
1117
1117
  "The adapter verifies its exact digest before launch and publication. Inspect its "
1118
1118
  "activity, subject, input_sha256, task definitions and criterion coverage against "
1119
1119
  "these bindings, then inspect the input manifest and referenced target files. "
1120
+ "Use the installed structured-artifact engine's snapshot operation for the frozen "
1121
+ "plan and compare its returned sha256 with input_sha256; that binds the complete "
1122
+ "frozen input_manifest. "
1123
+ "For kind=bytes, sha256 hashes raw file bytes; also check executable. For "
1124
+ "kind=directory, members lists names included under the frozen roots, "
1125
+ "exclude_directories and validated engine output policy, not an unfiltered "
1126
+ "filesystem listing. For kind=artifact_normative, "
1127
+ "sha256 is the normative_sha256 returned by the installed identity operation "
1128
+ "for that artifact, not a digest of raw rendered HTML. Do not parse rendered "
1129
+ "HTML to reconstruct artifact authority. "
1120
1130
  "Do not pass if the evidence is missing, unreadable or inconsistent. The locator "
1121
1131
  "is a reference to the complete immutable evidence, not replacement authority.\n"
1122
1132
  )
@@ -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.4"
14
+ adapter_version = "0.15.6"
15
15
  update_target = "ca-pi"
16
16
  update_tag_prefix = "ca-pi-v"
17
17
  update_command = "pi update npm:@arbiterforge/ca-pi"
@@ -23,8 +23,10 @@ import _replylib
23
23
 
24
24
  ID_RE = re.compile(r"[A-Z][A-Z0-9_-]{0,127}")
25
25
  TOKEN_RE = re.compile(r"[A-Za-z0-9_-]{12,128}")
26
- PENDING_DIR = Path(".codearbiter/.markers/reconciliations")
27
- ATTEMPT_DIR = Path(".codearbiter/.markers/reconciliation-attempts")
26
+ PENDING_DIR = Path("pending")
27
+ ATTEMPT_DIR = Path("attempts")
28
+ LEGACY_PENDING_DIR = Path(".codearbiter/.markers/reconciliations")
29
+ LEGACY_ATTEMPT_DIR = Path(".codearbiter/.markers/reconciliation-attempts")
28
30
  SOURCE_DIR = Path(".codearbiter/.artifacts/authority-sources")
29
31
  PROVEN_UNCOMMITTED = frozenset({"REVISION_CONFLICT", "OPERATION_ROLLED_BACK"})
30
32
 
@@ -53,6 +55,65 @@ def _attempt(pending: dict[str, Any]) -> Path:
53
55
  return ATTEMPT_DIR / f"{_operation_id(pending)}.json"
54
56
 
55
57
 
58
+ def _state_root(root: Path) -> Path:
59
+ """Keep operational state outside every repository input root."""
60
+ try:
61
+ spool = _artifactauthoritylib._spool_root(root)
62
+ return _artifactauthoritylib._safe_directory(spool, Path("reconciliations"), create=True)
63
+ except _artifactauthoritylib.AuthorityError as exc:
64
+ raise ReconciliationError("Git admin reconciliation storage is unavailable or unsafe") from exc
65
+
66
+
67
+ def _legacy_pending(root: Path, artifact_id: str) -> Path:
68
+ return _legacy_file(root, LEGACY_PENDING_DIR, f"{artifact_id}.json")
69
+
70
+
71
+ def _legacy_file(root: Path, directory: Path, filename: str) -> Path:
72
+ try:
73
+ parent = _approvallib._safe_directory(root, directory, create=False)
74
+ except FileNotFoundError:
75
+ return root / directory / filename
76
+ except _approvallib.ApprovalError as exc:
77
+ raise ReconciliationError("legacy reconciliation directory is unsafe") from exc
78
+ return parent / filename
79
+
80
+
81
+ def _state_file(root: Path, directory: Path, filename: str) -> Path:
82
+ try:
83
+ parent = _artifactauthoritylib._safe_directory(_state_root(root), directory, create=True)
84
+ except _artifactauthoritylib.AuthorityError as exc:
85
+ raise ReconciliationError("Git admin reconciliation directory is unsafe") from exc
86
+ return parent / filename
87
+
88
+
89
+ def _present(path: Path) -> bool:
90
+ try:
91
+ path.lstat()
92
+ except FileNotFoundError:
93
+ return False
94
+ return True
95
+
96
+
97
+ def _pending_path(root: Path, artifact_id: str) -> Path:
98
+ current = _state_file(root, PENDING_DIR, f"{artifact_id}.json")
99
+ legacy = _legacy_pending(root, artifact_id)
100
+ if _present(current) and _present(legacy):
101
+ raise ReconciliationError("ambiguous reconciliation state in repository and Git admin storage")
102
+ return legacy if _present(legacy) else current
103
+
104
+
105
+ def _attempt_path(root: Path, pending: dict[str, Any]) -> Path:
106
+ filename = f"{_operation_id(pending)}.json"
107
+ legacy = _legacy_file(root, LEGACY_ATTEMPT_DIR, filename)
108
+ current = _state_file(root, ATTEMPT_DIR, filename)
109
+ legacy_pending = _present(_legacy_pending(root, pending["artifact_id"]))
110
+ if (_present(legacy) and _present(current)) or (
111
+ legacy_pending and _present(current)
112
+ ) or (not legacy_pending and _present(legacy)):
113
+ raise ReconciliationError("mixed reconciliation state requires explicit repair")
114
+ return legacy if legacy_pending else current
115
+
116
+
56
117
  def _mutation_request(pending: dict[str, Any], receipt: str) -> dict[str, Any]:
57
118
  identity = pending["identity"]
58
119
  request = {
@@ -72,7 +133,7 @@ def _reconciliation_lock(root: Path, artifact_id: str):
72
133
  """Serialize all transitions for one artifact, including authority revocation."""
73
134
  if ID_RE.fullmatch(artifact_id) is None:
74
135
  raise ReconciliationError("reconciliation ID is invalid")
75
- parent = _approvallib._safe_directory(root, PENDING_DIR, create=True)
136
+ parent = _state_file(root, PENDING_DIR, artifact_id).parent
76
137
  path = parent / f"{artifact_id}.lock"
77
138
  flags = os.O_RDWR | os.O_CREAT | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
78
139
  fd = os.open(path, flags, 0o600)
@@ -147,11 +208,11 @@ def _write_attempt(root: Path, pending: dict[str, Any], request: dict[str, Any])
147
208
  "format": "codearbiter.reconciliation-attempt/0.1.0",
148
209
  "pending_sha256": _digest(_canonical(pending)), "request": request,
149
210
  }
150
- _write_atomic_new(root, _attempt(pending), _canonical(record))
211
+ _write_atomic_new(_state_root(root), _attempt(pending), _canonical(record))
151
212
 
152
213
 
153
214
  def _load_attempt(root: Path, pending: dict[str, Any]) -> dict[str, Any] | None:
154
- path = root / _attempt(pending)
215
+ path = _attempt_path(root, pending)
155
216
  try:
156
217
  info = path.lstat()
157
218
  except FileNotFoundError:
@@ -225,6 +286,8 @@ def _arm_reconciliation_unlocked(
225
286
  token = token or secrets.token_urlsafe(24)
226
287
  if TOKEN_RE.fullmatch(token) is None:
227
288
  raise ReconciliationError("reconciliation token is invalid")
289
+ if _present(_legacy_pending(root, artifact_id)):
290
+ raise ReconciliationError("PENDING_RECONCILIATION: legacy request must be cancelled or recovered")
228
291
  identity, snapshot, record = _current(client, artifact_id, target_id)
229
292
  reply = (
230
293
  f"reconcile-task {artifact_id} {target_id} {target_state} {token}"
@@ -240,10 +303,11 @@ def _arm_reconciliation_unlocked(
240
303
  "prompt_sha256": _digest(reply.encode()),
241
304
  }
242
305
  relative = _pending(artifact_id)
306
+ state = _state_root(root)
243
307
  created = False
244
308
  try:
245
309
  pending_bytes = _canonical(pending)
246
- _write_atomic_new(root, relative, pending_bytes)
310
+ _write_atomic_new(state, relative, pending_bytes)
247
311
  created = True
248
312
  _artifactpromptlib.register(
249
313
  root, "reconciliation", artifact_id, reply,
@@ -254,7 +318,7 @@ def _arm_reconciliation_unlocked(
254
318
  except Exception:
255
319
  if created:
256
320
  try:
257
- (root / relative).unlink()
321
+ (state / relative).unlink()
258
322
  except OSError:
259
323
  pass
260
324
  raise
@@ -267,7 +331,7 @@ def _arm_reconciliation_unlocked(
267
331
 
268
332
  def _load(root: Path, artifact_id: str) -> dict[str, Any]:
269
333
  try:
270
- path = root / _pending(artifact_id)
334
+ path = _pending_path(root, artifact_id)
271
335
  info = path.lstat()
272
336
  reparse = getattr(info, "st_file_attributes", 0) & 0x400
273
337
  if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISREG(info.st_mode) or info.st_size > 131072:
@@ -328,7 +392,7 @@ def _cancel_reconciliation_unlocked(root: Path, artifact_id: str, prompt: str) -
328
392
  raise ReconciliationError("reconciliation authority binding is unavailable") from exc
329
393
  if binding != _digest(_canonical(pending)):
330
394
  raise ReconciliationError("pending reconciliation changed after arming")
331
- (root / _pending(artifact_id)).unlink()
395
+ _pending_path(root, artifact_id).unlink()
332
396
  _artifactpromptlib.unregister(root, "reconciliation", artifact_id)
333
397
  return {"artifact_id": artifact_id, "cancelled": True}
334
398
 
@@ -349,7 +413,7 @@ def cancel_orphaned_reconciliation(root: str | Path, artifact_id: str) -> dict[s
349
413
  raise ReconciliationError("reconciliation authority binding is unreadable") from exc
350
414
  if binding == _digest(_canonical(pending)):
351
415
  raise ReconciliationError("reconciliation route is active; cancel with its exact prompt")
352
- (root / _pending(artifact_id)).unlink()
416
+ _pending_path(root, artifact_id).unlink()
353
417
  if binding is not None:
354
418
  _artifactpromptlib.unregister(root, "reconciliation", artifact_id)
355
419
  return {"artifact_id": artifact_id, "cancelled": True, "orphaned": True}
@@ -379,9 +443,10 @@ def recover_reconciliation(root: str | Path, client: Any, artifact_id: str) -> d
379
443
  raise
380
444
  result = None
381
445
  committed = False
382
- (root / _pending(artifact_id)).unlink()
446
+ attempt_path = _attempt_path(root, pending)
447
+ _pending_path(root, artifact_id).unlink()
383
448
  _artifactpromptlib.unregister(root, "reconciliation", artifact_id)
384
- (root / _attempt(pending)).unlink()
449
+ attempt_path.unlink()
385
450
  return {
386
451
  "artifact_id": artifact_id, "committed": committed,
387
452
  "revision": result.get("revision") if committed else None,
@@ -407,6 +472,8 @@ def _consume_reconciliation_unlocked(
407
472
  parts = prompt.split(" ")
408
473
  artifact_id, target_id = parts[1], parts[2]
409
474
  pending = _load(root, artifact_id)
475
+ if _present(_legacy_pending(root, artifact_id)):
476
+ raise ReconciliationError("legacy reconciliation must be cancelled or recovered before consumption")
410
477
  if not isinstance(host, str) or not host or not isinstance(session_id, str) or not session_id:
411
478
  raise ReconciliationError("reconciliation host context is invalid")
412
479
  try:
@@ -456,9 +523,10 @@ def _consume_reconciliation_unlocked(
456
523
  if request is None:
457
524
  raise ReconciliationError("in-flight reconciliation record disappeared")
458
525
  result = client.call(pending["operation"], request)
459
- (root / _pending(artifact_id)).unlink()
526
+ attempt_path = _attempt_path(root, pending)
527
+ _pending_path(root, artifact_id).unlink()
460
528
  _artifactpromptlib.unregister(root, "reconciliation", artifact_id)
461
- (root / _attempt(pending)).unlink()
529
+ attempt_path.unlink()
462
530
  return {"matched": True, "reconciled": True, "artifact_id": artifact_id, "target_id": target_id, "receipt": request["receipt"], "revision": result.get("revision")}
463
531
 
464
532
 
@@ -54,4 +54,8 @@ def main(argv=None):
54
54
 
55
55
 
56
56
  if __name__ == "__main__":
57
- raise SystemExit(main())
57
+ try:
58
+ raise SystemExit(main())
59
+ except (_reconciliationlib.ReconciliationError, _artifactlib.ArtifactError, OSError) as exc:
60
+ print(f"codeArbiter: reconciliation failed: {exc}", file=sys.stderr)
61
+ raise SystemExit(1) from exc
@@ -313,7 +313,7 @@ class Host:
313
313
 
314
314
  name = "claude"
315
315
  adapter_name = "ca"
316
- adapter_version = "2.22.4"
316
+ adapter_version = "2.22.6"
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.15.4",
3
+ "version": "0.15.6",
4
4
  "private": true,
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",