@arbiterforge/ca-pi 0.14.17 → 0.15.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.21.17" src="https://img.shields.io/badge/version-2.21.17-2b7489">
13
+ <img alt="version 2.22.1" src="https://img.shields.io/badge/version-2.22.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-23-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.14.17",
3
+ "version": "0.15.1",
4
4
  "license": "AGPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,6 +4,31 @@ All notable changes to `ca-pi` are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.15.1] - 2026-09-26
8
+
9
+ ### Fixed
10
+
11
+ - Ship the shared whole-plan verification preflight, exact named-result collectors,
12
+ and pinned native Windows npm runner contract.
13
+ - Reject ineffective snapshot exclusions, prevent explicit roots from bypassing
14
+ exclusions, and retain actionable oversized-input diagnostics.
15
+
16
+ ## [0.15.0] - 2026-09-26
17
+
18
+ ### Added
19
+
20
+ - Approve a structured artifact by typing a four-character code, for example
21
+ `approve K7MQ`, instead of pasting a long token. The hook expands it to the
22
+ exact armed reply, so the engine and recorded evidence are unchanged.
23
+ - On Claude Code, approve by clicking an Approve option in the host's own
24
+ question dialog; the recorded origin names that seam.
25
+
26
+ ### Fixed
27
+
28
+ - Replies with extra spacing, invisible characters, backticks, quotes or a
29
+ closing period are no longer ignored; a reply that still cannot match now
30
+ says why instead of failing silently.
31
+
7
32
  ## [0.14.17] - 2026-09-25
8
33
 
9
34
  ### Fixed
@@ -6,32 +6,32 @@
6
6
  "binaries": {
7
7
  "darwin/amd64": {
8
8
  "file": "ca-artifact-darwin-amd64",
9
- "sha256": "8e2a1f3026d5d9a8de36f3f748a7140ecf1ef8e768022aaa8745884e09551f9a",
9
+ "sha256": "3c8d8deb48d85ba1c9a6cfaf1286543cc8193fc8db307fe35446f29e53d3ca7a",
10
10
  "native_tested": true
11
11
  },
12
12
  "darwin/arm64": {
13
13
  "file": "ca-artifact-darwin-arm64",
14
- "sha256": "cce4b1cd751ada483bffd2de53bd14cdfda50f5d7392b49ba3f020b58fc59da4",
14
+ "sha256": "af3a36a6926532c6e0b4da10397aae17a472e300f50d5c0dc7886aa722046d28",
15
15
  "native_tested": true
16
16
  },
17
17
  "linux/amd64": {
18
18
  "file": "ca-artifact-linux-amd64",
19
- "sha256": "7637fe233e8731f4f15ff36b2e80714edd7e6fd96117b41f911d0c8f46639615",
19
+ "sha256": "83300e32dd6514b4716707091d900611f42454097b177443c9c47f464cad5065",
20
20
  "native_tested": true
21
21
  },
22
22
  "linux/arm64": {
23
23
  "file": "ca-artifact-linux-arm64",
24
- "sha256": "daa4ed9f1f577c59afd0250701959718f501799c204df01a92f00876c088f619",
24
+ "sha256": "47f1e529140642c7574f29664bb9adacc2755458a1cd9a30c6fdc0f9df447f24",
25
25
  "native_tested": true
26
26
  },
27
27
  "windows/amd64": {
28
28
  "file": "ca-artifact-windows-amd64.exe",
29
- "sha256": "16a3a26c5c606fe77703cbc9ae69414cbbe93f2c50dda9301eb9784e2e842206",
29
+ "sha256": "fa1a11489cc4521c945a59d2dce9f270ba9fcf6fcdc220011579326d5e97569a",
30
30
  "native_tested": true
31
31
  },
32
32
  "windows/arm64": {
33
33
  "file": "ca-artifact-windows-arm64.exe",
34
- "sha256": "f64155cd951be94039ea85d4420b2d3bad75ebcb14e68aeb15be8ccf9ec1f690",
34
+ "sha256": "88e0e9c2cf7d5861ff1365d8aa4d9376f6fa1ff8cdf0eb708be78be88af45468",
35
35
  "native_tested": true
36
36
  }
37
37
  }
@@ -17,6 +17,7 @@ from typing import Any
17
17
  import _artifactlib
18
18
  import _artifactauthoritylib
19
19
  import _artifactpromptlib
20
+ import _replylib
20
21
 
21
22
 
22
23
  PENDING = Path(".codearbiter/.markers/pending-user-approval.json")
@@ -70,7 +71,102 @@ def _ready(client: Any, identity: dict[str, Any]) -> None:
70
71
  or result.get("model_sha256") != identity["model_sha256"]
71
72
  or result.get("normative_sha256") != identity["normative_sha256"]
72
73
  ):
73
- raise ApprovalError("NOT_READY", "only the exact current ready artifact can be armed")
74
+ details = "; ".join(
75
+ f"{item.get('code', 'NOT_READY')} {item.get('symbol', '')} {item.get('field', '')}: {item.get('message', '')}"
76
+ for item in result.get("diagnostics", []) if isinstance(item, dict)
77
+ )
78
+ raise ApprovalError("NOT_READY", "only the exact current ready artifact can be armed" + (": " + details if details else ""))
79
+
80
+
81
+ def _preflight_plan_verification(client: Any, identity: dict[str, Any]) -> dict[str, Any]:
82
+ """Inspect the whole engine-owned plan before requesting approval.
83
+
84
+ This private admission check runs no declared command or test discovery.
85
+ Proposed tests may be absent. Existing input roots must already be readable;
86
+ a containing directory records future file creation in the input snapshot.
87
+ Runtime binding, host authorization and fresh evidence remain separate.
88
+ """
89
+ if identity.get("kind") != "plan":
90
+ raise _artifactlib.ArtifactError("WRONG_KIND", "verification preflight requires a plan")
91
+ keys = ("artifact_id", "revision", "model_sha256", "normative_sha256")
92
+ artifact_id = identity["artifact_id"]
93
+
94
+ def same_identity(value: dict[str, Any]) -> None:
95
+ if not isinstance(value, dict) or any(value.get(key) != identity.get(key) for key in keys):
96
+ raise _artifactlib.ArtifactError("STALE_PLAN_PREFLIGHT", "plan identity changed during verification preflight; reread the complete current plan")
97
+
98
+ same_identity(client.call("identity", {"artifact_id": artifact_id}))
99
+ root = _artifactlib._trusted_directory(client.root, "UNSAFE_ROOT")
100
+ diagnostics: list[str] = []
101
+ seen: set[str] = set()
102
+ command_count = 0
103
+ offset, total = 0, None
104
+ for _ in range(8193):
105
+ request = {"artifact_id": artifact_id, "offset": offset, "budget": 16384}
106
+ if offset:
107
+ request["model_sha256"] = identity["model_sha256"]
108
+ page = client.call("outline", request)
109
+ same_identity(page)
110
+ _artifactlib.ArtifactClient._check_page(page, offset, "records")
111
+ if (page.get("offset") != offset or type(page.get("total")) is not int
112
+ or page["total"] < offset + len(page["records"])
113
+ or total is not None and page["total"] != total):
114
+ raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan outline count or offset changed")
115
+ total = page["total"]
116
+ for row in page["records"]:
117
+ if (not isinstance(row, dict) or not isinstance(row.get("id"), str)
118
+ or row["id"] in seen or type(row.get("retired")) is not bool):
119
+ raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan outline contains an invalid or duplicate record")
120
+ seen.add(row["id"])
121
+ if row.get("kind") != "tasks" or row["retired"]:
122
+ continue
123
+ exact = client.call("read", {"artifact_id": artifact_id, "symbol": row["id"], "mode": "exact", "budget": 16384})
124
+ same_identity(exact)
125
+ task = exact.get("record")
126
+ if (not isinstance(task, dict) or task.get("id") != row["id"]
127
+ or not isinstance(task.get("verification"), list) or not task["verification"]):
128
+ raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan task lacks complete verification definitions")
129
+ for number, definition in enumerate(task["verification"], 1):
130
+ command_count += 1
131
+ try:
132
+ if not isinstance(definition, dict):
133
+ raise _artifactlib.ArtifactError("INVALID_RESPONSE", "verification definition is not an object")
134
+ label = definition.get("cwd")
135
+ cwd = None
136
+ if label in {".", "current worktree", "candidate worktree"}:
137
+ cwd = root
138
+ elif isinstance(label, str):
139
+ relative = Path(label)
140
+ if relative.is_absolute() or ".." in relative.parts:
141
+ raise _artifactlib.ArtifactError("UNSUPPORTED_WORKSPACE", "declared cwd must be a contained repository path or mapped worktree label")
142
+ candidate = root / relative
143
+ if candidate.exists():
144
+ cwd = _artifactlib._trusted_directory(candidate, "UNSUPPORTED_WORKSPACE")
145
+ _artifactauthoritylib.validate_command_definition(definition, cwd=cwd)
146
+ except (_artifactlib.ArtifactError, _artifactauthoritylib.AuthorityError) as exc:
147
+ argv = definition.get("argv") if isinstance(definition, dict) else None
148
+ diagnostics.append(f"{row['id']} verification[{number}] {json.dumps(argv, ensure_ascii=True)}: {exc}")
149
+ next_offset = page["next_offset"]
150
+ expected = offset + len(page["records"])
151
+ if next_offset is None:
152
+ if expected != total:
153
+ raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan outline ended before every record was inspected")
154
+ break
155
+ if next_offset != expected:
156
+ raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan outline skipped records")
157
+ offset = next_offset
158
+ else:
159
+ raise _artifactlib.ArtifactError("INVALID_RESPONSE", "plan outline did not terminate")
160
+ try:
161
+ snapshot = client.call("snapshot", {"artifact_id": artifact_id})
162
+ except _artifactlib.ArtifactError as exc:
163
+ diagnostics.append(f"verification_inputs: {exc}")
164
+ snapshot = None
165
+ same_identity(client.call("identity", {"artifact_id": artifact_id}))
166
+ if diagnostics:
167
+ raise _artifactlib.ArtifactError("PLAN_VERIFICATION_UNAVAILABLE", "; ".join(diagnostics))
168
+ return {"artifact_id": artifact_id, "model_sha256": identity["model_sha256"],
169
+ "command_count": command_count, "snapshot": snapshot}
74
170
 
75
171
 
76
172
  def _safe_directory(root: Path, relative: Path, *, create: bool) -> Path:
@@ -134,6 +230,8 @@ def arm_user_approval(
134
230
  root = Path(root).resolve(strict=True)
135
231
  identity = _identity(client, artifact_id)
136
232
  _ready(client, identity)
233
+ if identity["kind"] == "plan":
234
+ _preflight_plan_verification(client, identity)
137
235
  token = token or secrets.token_urlsafe(24)
138
236
  if not TOKEN_RE.fullmatch(token):
139
237
  raise ApprovalError("INVALID_TOKEN", "approval token has an invalid shape")
@@ -159,9 +257,21 @@ def arm_user_approval(
159
257
  except _artifactpromptlib.PromptRouteError:
160
258
  (root / PENDING).unlink()
161
259
  raise
260
+ reply = f"approve {artifact_id} {token}"
261
+ code = _replylib.offer_code(root, "approval", artifact_id, reply,
262
+ names=[artifact_id], short_prefix=["approve"])
263
+ envelope = _replylib.ask_envelope(
264
+ root, "approval", artifact_id, code_fields=code,
265
+ question=(f"Approve {identity['kind']} {artifact_id} revision {identity['revision']} "
266
+ f"as you reviewed it? (normative {identity['normative_sha256'][:12]})"),
267
+ header=f"Approve {code.get('code', '')}",
268
+ description=f"Records your approval of this exact revision. Same as typing {code.get('short_reply')}.",
269
+ )
162
270
  return {
163
271
  "artifact_id": artifact_id,
164
- "reply": f"approve {artifact_id} {token}",
272
+ "reply": reply,
273
+ **code,
274
+ **({"ask_envelope": envelope} if envelope else {}),
165
275
  "model_sha256": identity["model_sha256"],
166
276
  "normative_sha256": identity["normative_sha256"],
167
277
  }
@@ -243,14 +353,17 @@ def consume_user_approval(
243
353
  *,
244
354
  host: str,
245
355
  session_id: str,
356
+ seam: str = "UserPromptSubmit",
246
357
  ) -> dict[str, Any]:
247
358
  root = Path(root).resolve(strict=True)
359
+ if seam not in {"UserPromptSubmit", "AskUserQuestion"}:
360
+ raise ApprovalError("INVALID_HOST_CONTEXT", "approval seam is unsupported")
248
361
  pending = _load_pending(root)
249
362
  if pending is None or not isinstance(prompt, str):
250
363
  return {"matched": False, "approved": False}
251
364
  if pending["format"] == "codearbiter.pending-user-approval/0.2.0":
252
365
  import _sprintapprovallib
253
- return _sprintapprovallib.consume(root, client, pending, prompt, host=host, session_id=session_id)
366
+ return _sprintapprovallib.consume(root, client, pending, prompt, host=host, session_id=session_id, seam=seam)
254
367
  parts = prompt.split(" ")
255
368
  if len(parts) != 3 or parts[0] != "approve" or parts[1] != pending["artifact_id"]:
256
369
  return {"matched": False, "approved": False}
@@ -277,7 +390,7 @@ def consume_user_approval(
277
390
  "record_id": current["artifact_id"],
278
391
  },
279
392
  "actor": "interactive repository user",
280
- "origin": f"{host}:UserPromptSubmit:{session_id}",
393
+ "origin": f"{host}:{seam}:{session_id}",
281
394
  "verdict": "approved",
282
395
  "payload": {},
283
396
  "source_text": prompt,
@@ -316,25 +429,37 @@ def consume_user_approval(
316
429
 
317
430
 
318
431
  def consume_from_hook(*, root: str | Path, plugin_root: str | Path, prompt: str,
319
- host: str, session_id: str) -> str:
432
+ host: str, session_id: str, seam: str = "UserPromptSubmit") -> str:
433
+ raw = prompt
434
+ prepared = _replylib.prepare(raw, "approval")
435
+ prompt = prepared["text"]
436
+ attempted = prepared["attempted"]
437
+
438
+ def missed(reason: str) -> str:
439
+ _replylib.record_near_miss(root, raw, reason)
440
+ return prepared["notice"] or _replylib.miss_notice("approve K7MQ")
441
+
442
+ if prepared["notice"]:
443
+ return missed("code")
320
444
  try:
321
445
  routed = _artifactpromptlib.resolve("approval", prompt)
322
446
  root = routed or Path(root)
323
447
  if not (Path(root) / PENDING).exists():
324
- return ""
448
+ return missed("no-pending") if attempted else ""
325
449
  client = _artifactlib.ArtifactClient(
326
450
  root, Path(plugin_root) / "helpers" / "artifacts"
327
451
  )
328
452
  result = consume_user_approval(
329
- root, client, prompt, host=host, session_id=session_id
453
+ root, client, prompt, host=host, session_id=session_id, seam=seam
330
454
  )
331
455
  except (ApprovalError, _artifactlib.ArtifactError,
332
456
  _artifactpromptlib.PromptRouteError, OSError) as exc:
333
- if isinstance(prompt, str) and prompt.startswith(("approve ", "approve-sprint ")):
457
+ if attempted:
458
+ _replylib.record_near_miss(root, raw, "capture-failed")
334
459
  return f"codeArbiter: approval capture failed: {exc}"
335
460
  return ""
336
461
  if not result.get("approved"):
337
- return ""
462
+ return missed("mismatch") if attempted else ""
338
463
  return (
339
464
  "codeArbiter: workflow approval recorded for "
340
465
  f"{result['artifact_id']} (receipt {result['receipt']})."