@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 +1 -1
- package/package.json +1 -1
- package/plugins/ca-pi/CHANGELOG.md +25 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/release.json +6 -6
- package/plugins/ca-pi/hooks/_approvallib.py +134 -9
- package/plugins/ca-pi/hooks/_artifactauthoritylib.py +286 -38
- package/plugins/ca-pi/hooks/_artifactlib.py +1 -1
- package/plugins/ca-pi/hooks/_artifactpromptlib.py +4 -0
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_prerequisitelib.py +15 -6
- package/plugins/ca-pi/hooks/_reconciliationlib.py +15 -4
- package/plugins/ca-pi/hooks/_replylib.py +470 -0
- package/plugins/ca-pi/hooks/_sprintapprovallib.py +12 -6
- package/plugins/ca-pi/hooks/artifact-authority-hook.py +39 -0
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/includes/artifacts.md +63 -7
- package/plugins/ca-pi/includes/sprint-authority.md +4 -2
- package/plugins/ca-pi/package.json +1 -1
- package/plugins/ca-pi/routines/writing-plans/SKILL.md +12 -1
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.
|
|
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
|
@@ -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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -6,32 +6,32 @@
|
|
|
6
6
|
"binaries": {
|
|
7
7
|
"darwin/amd64": {
|
|
8
8
|
"file": "ca-artifact-darwin-amd64",
|
|
9
|
-
"sha256": "
|
|
9
|
+
"sha256": "3c8d8deb48d85ba1c9a6cfaf1286543cc8193fc8db307fe35446f29e53d3ca7a",
|
|
10
10
|
"native_tested": true
|
|
11
11
|
},
|
|
12
12
|
"darwin/arm64": {
|
|
13
13
|
"file": "ca-artifact-darwin-arm64",
|
|
14
|
-
"sha256": "
|
|
14
|
+
"sha256": "af3a36a6926532c6e0b4da10397aae17a472e300f50d5c0dc7886aa722046d28",
|
|
15
15
|
"native_tested": true
|
|
16
16
|
},
|
|
17
17
|
"linux/amd64": {
|
|
18
18
|
"file": "ca-artifact-linux-amd64",
|
|
19
|
-
"sha256": "
|
|
19
|
+
"sha256": "83300e32dd6514b4716707091d900611f42454097b177443c9c47f464cad5065",
|
|
20
20
|
"native_tested": true
|
|
21
21
|
},
|
|
22
22
|
"linux/arm64": {
|
|
23
23
|
"file": "ca-artifact-linux-arm64",
|
|
24
|
-
"sha256": "
|
|
24
|
+
"sha256": "47f1e529140642c7574f29664bb9adacc2755458a1cd9a30c6fdc0f9df447f24",
|
|
25
25
|
"native_tested": true
|
|
26
26
|
},
|
|
27
27
|
"windows/amd64": {
|
|
28
28
|
"file": "ca-artifact-windows-amd64.exe",
|
|
29
|
-
"sha256": "
|
|
29
|
+
"sha256": "fa1a11489cc4521c945a59d2dce9f270ba9fcf6fcdc220011579326d5e97569a",
|
|
30
30
|
"native_tested": true
|
|
31
31
|
},
|
|
32
32
|
"windows/arm64": {
|
|
33
33
|
"file": "ca-artifact-windows-arm64.exe",
|
|
34
|
-
"sha256": "
|
|
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
|
-
|
|
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":
|
|
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}:
|
|
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
|
|
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']})."
|