@arbiterforge/ca-pi 0.14.9 → 0.14.12

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.
Files changed (40) hide show
  1. package/README.md +7 -2
  2. package/package.json +1 -1
  3. package/plugins/ca-pi/CHANGELOG.md +39 -0
  4. package/plugins/ca-pi/SKILLS.md +2 -2
  5. package/plugins/ca-pi/SPRINT.md +47 -0
  6. package/plugins/ca-pi/generated/command-catalog.json +2 -2
  7. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
  8. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
  9. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
  10. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
  11. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
  12. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
  13. package/plugins/ca-pi/helpers/artifacts/release.json +6 -6
  14. package/plugins/ca-pi/hooks/_approvallib.py +58 -22
  15. package/plugins/ca-pi/hooks/_artifactauthoritylib.py +1686 -0
  16. package/plugins/ca-pi/hooks/_artifactlib.py +14 -4
  17. package/plugins/ca-pi/hooks/_artifactpromptlib.py +177 -0
  18. package/plugins/ca-pi/hooks/_githooks.py +368 -39
  19. package/plugins/ca-pi/hooks/_host.py +1 -1
  20. package/plugins/ca-pi/hooks/_overrideappendlib.py +5 -1
  21. package/plugins/ca-pi/hooks/_prerequisitelib.py +44 -22
  22. package/plugins/ca-pi/hooks/_reconciliationlib.py +472 -0
  23. package/plugins/ca-pi/hooks/_sprintapprovallib.py +302 -0
  24. package/plugins/ca-pi/hooks/artifact-authority-hook.py +72 -0
  25. package/plugins/ca-pi/hooks/artifact-authority.py +80 -0
  26. package/plugins/ca-pi/hooks/artifact-reconcile.py +57 -0
  27. package/plugins/ca-pi/hooks/doctor.py +3 -1
  28. package/plugins/ca-pi/hooks/hostapi.py +1 -1
  29. package/plugins/ca-pi/hooks/prompt-submit.py +20 -3
  30. package/plugins/ca-pi/includes/artifacts.md +99 -12
  31. package/plugins/ca-pi/includes/sprint-authority.md +69 -0
  32. package/plugins/ca-pi/package.json +1 -1
  33. package/plugins/ca-pi/routines/INDEX.md +7 -4
  34. package/plugins/ca-pi/routines/brainstorming/SKILL.md +97 -29
  35. package/plugins/ca-pi/routines/commit-gate/SKILL.md +7 -1
  36. package/plugins/ca-pi/routines/executing-plans/SKILL.md +32 -15
  37. package/plugins/ca-pi/routines/skill-author/SKILL.md +26 -5
  38. package/plugins/ca-pi/routines/writing-plans/SKILL.md +22 -10
  39. package/plugins/ca-pi/skills/ca-commit/SKILL.md +170 -16
  40. package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +116 -21
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env python3
2
+ """Private one-reply sprint approval adapter; no public registration.
3
+
4
+ The existing host hook records an actual reply. The native engine validates
5
+ both frozen definitions and commits their approvals in one journal transaction.
6
+ A retained submission can resume after interruption without another user reply;
7
+ merely arming a request is never approval or delegation.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ from functools import wraps
13
+ import json
14
+ import os
15
+ from pathlib import Path
16
+ import secrets
17
+ import stat
18
+ from typing import Any
19
+
20
+ import _approvallib as approval
21
+ import _artifactlib
22
+ import _artifactauthoritylib
23
+ import _artifactpromptlib
24
+
25
+ FORMAT = "codearbiter.pending-user-approval/0.2.0"
26
+ SUBMISSION_FORMAT = "codearbiter.sprint-submission/0.1.0"
27
+ IDENTITY_KEYS = {"artifact_id", "kind", "revision", "model_sha256", "normative_sha256"}
28
+ PAIR_KEYS = {"format", "spec", "plan", "approved_plan_normative_sha256", "scope_sha256", "delegate_methods"}
29
+
30
+
31
+ def _hash(value: Any) -> str:
32
+ return hashlib.sha256(approval._canonical(value)).hexdigest()
33
+
34
+
35
+ def _digest(value: Any) -> bool:
36
+ return isinstance(value, str) and approval.SHA256_RE.fullmatch(value) is not None
37
+
38
+
39
+ def _serialized(function):
40
+ """Reuse the existing private OS lock; no lock file enters the input snapshot."""
41
+ @wraps(function)
42
+ def run(root, *args, **kwargs):
43
+ import _prerequisitelib
44
+ root = _artifactlib._trusted_directory(root, "UNSAFE_ROOT")
45
+ try:
46
+ # One pending approval marker is shared by all pairs in this root.
47
+ with _prerequisitelib._pending_transition_lock(root, "sprint-pair-approval"):
48
+ return function(root, *args, **kwargs)
49
+ except _prerequisitelib.PrerequisiteError as exc:
50
+ raise approval.ApprovalError("APPROVAL_BUSY", "another session is completing this repository's paired approval") from exc
51
+ return run
52
+
53
+
54
+ def validate_pending(value: dict[str, Any]) -> dict[str, Any]:
55
+ expected = {"format", "artifact_id", "kind", "pair", "spec_context", "plan_context", "prompt_sha256", "token_sha256", "request_key"}
56
+ if set(value) != expected or value.get("format") != FORMAT or value.get("kind") != "sprint-pair":
57
+ raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "paired pending shape is invalid")
58
+ pair = value.get("pair")
59
+ if not isinstance(pair, dict) or set(pair) != PAIR_KEYS or pair.get("format") != "codearbiter.sprint-pair/0.1.0" or type(pair.get("delegate_methods")) is not bool:
60
+ raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "paired scope is malformed")
61
+ for name in ("spec", "plan"):
62
+ identity = pair.get(name)
63
+ if (not isinstance(identity, dict) or set(identity) != IDENTITY_KEYS
64
+ or identity.get("kind") != name or not isinstance(identity.get("artifact_id"), str)
65
+ or not identity["artifact_id"] or type(identity.get("revision")) is not int
66
+ or identity["revision"] < 1
67
+ or not all(_digest(identity.get(k)) for k in ("model_sha256", "normative_sha256"))):
68
+ raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "paired identity is malformed")
69
+ context = value.get(name + "_context")
70
+ if (not isinstance(context, dict) or set(context) != {"context_ref", "context_sha256", "subject"}
71
+ or not _digest(context.get("context_sha256"))
72
+ or context.get("context_ref") != ".codearbiter/.artifacts/evidence-contexts/" + context["context_sha256"] + ".json"):
73
+ raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "context locator is malformed")
74
+ expected_norm = identity["normative_sha256"] if name == "spec" else pair["approved_plan_normative_sha256"]
75
+ if context.get("subject") != {"artifact_id": identity["artifact_id"], "record_id": identity["artifact_id"], "normative_sha256": expected_norm}:
76
+ raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "context belongs to a different pair")
77
+ if (value.get("artifact_id") != pair["plan"]["artifact_id"]
78
+ or not all(_digest(value.get(k)) for k in ("prompt_sha256", "token_sha256", "request_key"))
79
+ or not all(_digest(pair.get(k)) for k in ("approved_plan_normative_sha256", "scope_sha256"))
80
+ or value["request_key"] != _hash({"pair": pair, "prompt_sha256": value["prompt_sha256"]})):
81
+ raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "paired request identity is invalid")
82
+ return value
83
+
84
+
85
+ def _read(root: Path, relative: Path, limit: int = 65536) -> dict[str, Any]:
86
+ parent = approval._safe_directory(root, relative.parent, create=False)
87
+ fd = _artifactlib._open_pinned_regular(parent / relative.name)
88
+ try:
89
+ info = os.fstat(fd)
90
+ if not stat.S_ISREG(info.st_mode) or info.st_size > limit:
91
+ raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "record is not bounded regular data")
92
+ chunks = []
93
+ size = 0
94
+ while True:
95
+ chunk = os.read(fd, min(65536, limit + 1 - size))
96
+ if not chunk:
97
+ break
98
+ chunks.append(chunk)
99
+ size += len(chunk)
100
+ if size > limit:
101
+ raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "record exceeds its bound")
102
+ raw = b"".join(chunks)
103
+ finally:
104
+ os.close(fd)
105
+ value = _artifactlib._decode(raw)
106
+ if not isinstance(value, dict):
107
+ raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "record must be an object")
108
+ return value
109
+
110
+
111
+ @_serialized
112
+ def arm(root: str | Path, client: Any, spec_id: str, plan_id: str, *, delegate_methods: bool = False, token: str | None = None) -> dict[str, Any]:
113
+ root = _artifactlib._trusted_directory(root, "UNSAFE_ROOT")
114
+ if type(delegate_methods) is not bool:
115
+ raise approval.ApprovalError("INVALID_DELEGATION", "delegation must be an explicit boolean")
116
+ token = token or secrets.token_urlsafe(24)
117
+ if not approval.TOKEN_RE.fullmatch(token):
118
+ raise approval.ApprovalError("INVALID_TOKEN", "approval token has an invalid shape")
119
+ mode = "delegate-methods" if delegate_methods else "approve-only"
120
+ reply = f"approve-sprint {spec_id} {plan_id} {mode} {token}"
121
+ ph = hashlib.sha256(reply.encode("utf-8")).hexdigest()
122
+ context = client.call("sprint-approval-context", {"artifact_id": plan_id, "spec_id": spec_id, "delegate_methods": delegate_methods, "prompt_sha256": ph})
123
+ pending = {"format": FORMAT, "artifact_id": plan_id, "kind": "sprint-pair", "pair": context["pair"], "spec_context": context["spec_context"], "plan_context": context["plan_context"], "prompt_sha256": ph, "token_sha256": hashlib.sha256(token.encode("ascii")).hexdigest()}
124
+ pending["request_key"] = _hash({"pair": pending["pair"], "prompt_sha256": ph})
125
+ validate_pending(pending)
126
+ try:
127
+ approval._write_new(root, approval.PENDING, approval._canonical(pending))
128
+ except FileExistsError as exc:
129
+ raise approval.ApprovalError("PENDING_APPROVAL", "resolve the existing approval request first") from exc
130
+ try:
131
+ _artifactpromptlib.register(root, "approval", plan_id, reply)
132
+ except _artifactpromptlib.PromptRouteError:
133
+ (root / approval.PENDING).unlink()
134
+ raise
135
+ return {"spec_artifact_id": spec_id, "artifact_id": plan_id, "pair": pending["pair"], "reply": reply, "delegation": context.get("scope"), "approved": False}
136
+
137
+
138
+ def _submission_path(pending: dict[str, Any]) -> Path:
139
+ return approval.PENDING.parent / ("sprint-submission-" + pending["request_key"] + ".json")
140
+
141
+
142
+ def _base_request(pending: dict[str, Any]) -> dict[str, Any]:
143
+ pair = pending["pair"]
144
+ return {"artifact_id": pair["plan"]["artifact_id"], "spec_id": pair["spec"]["artifact_id"], "operation_id": "sprint-approval-" + pending["request_key"][:32], "expected": {k: pair["plan"][k] for k in ("revision", "model_sha256")}, "spec_expected": {k: pair["spec"][k] for k in ("revision", "model_sha256")}}
145
+
146
+
147
+ def _submission(root: Path, pending: dict[str, Any]) -> dict[str, Any] | None:
148
+ try:
149
+ value = _read(root, _submission_path(pending))
150
+ except FileNotFoundError:
151
+ return None
152
+ if set(value) != {"format", "request_key", "request"} or value.get("format") != SUBMISSION_FORMAT or value.get("request_key") != pending["request_key"]:
153
+ raise approval.ApprovalError("INVALID_SUBMISSION", "submission belongs to a different armed pair")
154
+ request = value.get("request")
155
+ base = _base_request(pending)
156
+ if not isinstance(request, dict) or set(request) != set(base) | {"spec_source", "plan_source"} or any(request.get(k) != v for k, v in base.items()):
157
+ raise approval.ApprovalError("INVALID_SUBMISSION", "submission changed its frozen transaction")
158
+ for key in ("spec_source", "plan_source"):
159
+ source = request[key]
160
+ if (not isinstance(source, dict) or set(source) != {"source_ref", "source_sha256"}
161
+ or not _digest(source.get("source_sha256"))
162
+ or source.get("source_ref") != ".codearbiter/.artifacts/authority-sources/" + source["source_sha256"] + ".json"):
163
+ raise approval.ApprovalError("INVALID_SUBMISSION", "submission source locator is invalid")
164
+ return request
165
+
166
+
167
+ def _source(root: Path, pending: dict[str, Any], name: str, prompt: str, host: str, session_id: str) -> dict[str, str]:
168
+ locator = pending[name + "_context"]
169
+ context = _read(root, Path(locator["context_ref"]))
170
+ if (_hash(context) != locator["context_sha256"] or context.get("subject") != locator["subject"]
171
+ or context.get("prompt_sha256") != pending["prompt_sha256"]
172
+ or context.get("record") != {"sprint_pair": pending["pair"]}):
173
+ raise approval.ApprovalError("INVALID_AUTHORITY_SOURCE", "the armed engine context changed")
174
+ payload = {"sprint_pair": pending["pair"]}
175
+ producer = {"host": host, "session_id": session_id, "prompt_sha256": pending["prompt_sha256"]}
176
+ observed = {"format": "codearbiter.observation/0.2.0", "kind": "approval", "subject": context["subject"], "context_ref": locator["context_ref"], "context_sha256": locator["context_sha256"], "payload_sha256": _hash(payload), "producer_profile": "host-user-pair/0.1.0", "producer_run_id": f"{host}:{session_id}", "producer_result": producer, "producer_result_sha256": _hash(producer)}
177
+ oh = _hash(observed)
178
+ observation_ref = Path(".codearbiter/.artifacts/observations") / f"{oh}.json"
179
+ _artifactauthoritylib._publish_immutable(root, observation_ref, approval._canonical(observed))
180
+ event = {"format": "codearbiter.workflow-event/0.2.0", "kind": "approval", "authority_kind": "user_workflow", "subject": context["subject"], "actor": "interactive repository user", "origin": f"{host}:UserPromptSubmit:{session_id}", "verdict": "approved", "payload": payload, "source_text": prompt, "observation_ref": observation_ref.as_posix(), "observation_sha256": oh}
181
+ eh = _hash(event)
182
+ source_ref = Path(".codearbiter/.artifacts/authority-sources") / f"{eh}.json"
183
+ _artifactauthoritylib._publish_immutable(root, source_ref, approval._canonical(event))
184
+ return {"source_ref": source_ref.as_posix(), "source_sha256": eh}
185
+
186
+
187
+ def _execute(root: Path, client: Any, pending: dict[str, Any], request: dict[str, Any]) -> dict[str, Any]:
188
+ try:
189
+ result = client.call("sprint-approve", request)
190
+ except _artifactlib.ArtifactError as exc:
191
+ if exc.code != "RECOVERY_REQUIRED":
192
+ raise
193
+ # Complete only the exact previously observed submission. A different
194
+ # pending transaction is not a reason to recover an unrelated operation.
195
+ client.call("recover", {"operation_id": request["operation_id"], "mode": "complete"})
196
+ result = client.call("sprint-approve", request)
197
+ if (result.get("authority_kind") != "user_workflow"
198
+ or result.get("spec", {}).get("artifact_id") != request["spec_id"]
199
+ or result.get("plan", {}).get("artifact_id") != request["artifact_id"]
200
+ or not isinstance(result.get("receipt"), str)):
201
+ raise approval.ApprovalError("INVALID_RECEIPT", "transaction returned an incomplete paired receipt")
202
+ # Remove the arm before the retained submission: a crash here must not
203
+ # leave an armed request whose only observed reply has already been erased.
204
+ if approval._load_pending(root) != pending:
205
+ raise approval.ApprovalError("PENDING_APPROVAL_MISMATCH", "pending pair changed during completion")
206
+ _artifactpromptlib.unregister(root, "approval", pending["artifact_id"])
207
+ (root / approval.PENDING).unlink()
208
+ (root / _submission_path(pending)).unlink(missing_ok=True)
209
+ return {"matched": True, "approved": True, "artifact_id": request["artifact_id"], "spec_artifact_id": request["spec_id"], "receipt": result["receipt"], "spec_receipt": result.get("spec_receipt"), "grant_receipt": result.get("grant_receipt"), "operation_id": request["operation_id"]}
210
+
211
+
212
+ @_serialized
213
+ def consume(root: Path, client: Any, pending: dict[str, Any], prompt: str, *, host: str, session_id: str) -> dict[str, Any]:
214
+ validate_pending(pending)
215
+ if approval._load_pending(root) != pending:
216
+ return {"matched": False, "approved": False}
217
+ parts = prompt.split(" ")
218
+ pair = pending["pair"]
219
+ mode = "delegate-methods" if pair["delegate_methods"] else "approve-only"
220
+ if len(parts) != 5 or parts[:4] != ["approve-sprint", pair["spec"]["artifact_id"], pair["plan"]["artifact_id"], mode]:
221
+ return {"matched": False, "approved": False}
222
+ if (not approval.TOKEN_RE.fullmatch(parts[4])
223
+ or not secrets.compare_digest(hashlib.sha256(parts[4].encode("ascii")).hexdigest(), pending["token_sha256"])
224
+ or not secrets.compare_digest(hashlib.sha256(prompt.encode("utf-8")).hexdigest(), pending["prompt_sha256"])):
225
+ return {"matched": False, "approved": False}
226
+ if not approval.HOST_RE.fullmatch(host) or not approval.SESSION_RE.fullmatch(session_id):
227
+ raise approval.ApprovalError("INVALID_HOST_CONTEXT", "host approval context is malformed")
228
+ request = _submission(root, pending)
229
+ if request is None:
230
+ for name in ("spec", "plan"):
231
+ current = approval._identity(client, pair[name]["artifact_id"])
232
+ if any(current.get(k) != v for k, v in pair[name].items()):
233
+ raise approval.ApprovalError("STALE_APPROVAL", "a paired artifact changed after review was requested")
234
+ request = _base_request(pending)
235
+ request["spec_source"] = _source(root, pending, "spec", prompt, host, session_id)
236
+ request["plan_source"] = _source(root, pending, "plan", prompt, host, session_id)
237
+ record = {"format": SUBMISSION_FORMAT, "request_key": pending["request_key"], "request": request}
238
+ try:
239
+ approval._write_new(root, _submission_path(pending), approval._canonical(record))
240
+ except FileExistsError:
241
+ # A concurrent genuine observer wins once. Replay its original
242
+ # sources rather than substituting a later session's metadata.
243
+ request = _submission(root, pending)
244
+ if request is None:
245
+ raise approval.ApprovalError("INVALID_SUBMISSION", "concurrent submission disappeared")
246
+ return _execute(root, client, pending, request)
247
+
248
+
249
+ @_serialized
250
+ def resume(root: str | Path, client: Any, plan_id: str) -> dict[str, Any]:
251
+ root = _artifactlib._trusted_directory(root, "UNSAFE_ROOT")
252
+ pending = approval._load_pending(root)
253
+ if pending is None or pending.get("format") != FORMAT or pending["artifact_id"] != plan_id:
254
+ raise approval.ApprovalError("NO_PENDING_APPROVAL", "no matching paired approval is pending")
255
+ request = _submission(root, pending)
256
+ if request is None:
257
+ raise approval.ApprovalError("NO_OBSERVED_APPROVAL", "arming alone is not a user reply; there is nothing to resume")
258
+ try:
259
+ return _execute(root, client, pending, request)
260
+ except _artifactlib.ArtifactError as exc:
261
+ if exc.code not in {"REVISION_CONFLICT", "OPERATION_ROLLED_BACK"}:
262
+ raise
263
+ # The engine checks journal replay before CAS. Only these closed-set
264
+ # outcomes establish that this exact old mutation did not commit.
265
+ if approval._load_pending(root) != pending:
266
+ raise approval.ApprovalError("PENDING_APPROVAL_MISMATCH", "pending request changed during failed-transaction recovery") from exc
267
+ _artifactpromptlib.unregister(root, "approval", pending["artifact_id"])
268
+ (root / approval.PENDING).unlink()
269
+ (root / _submission_path(pending)).unlink(missing_ok=True)
270
+ return {"matched": True, "approved": False, "artifact_id": plan_id,
271
+ "operation_id": request["operation_id"], "diagnostic": exc.code,
272
+ "reapproval_required": True}
273
+
274
+
275
+ @_serialized
276
+ def cancel(root: Path, pending: dict[str, Any]) -> dict[str, Any]:
277
+ if approval._load_pending(root) != pending:
278
+ raise approval.ApprovalError("PENDING_APPROVAL_MISMATCH", "pending pair changed before cancellation")
279
+ if _submission(root, pending) is not None:
280
+ raise approval.ApprovalError("SUBMITTED_APPROVAL", "recover the observed transaction before cancelling its pending state")
281
+ _artifactpromptlib.unregister(root, "approval", pending["artifact_id"])
282
+ (root / approval.PENDING).unlink()
283
+ return {"artifact_id": pending["artifact_id"], "cancelled": True}
284
+
285
+
286
+ def apply_decision(client: Any, request_file: str | Path) -> dict[str, Any]:
287
+ """Read a bounded inert request; only the native closed producer grants authority."""
288
+ path = Path(request_file)
289
+ fd = _artifactlib._open_pinned_regular(path)
290
+ try:
291
+ info = os.fstat(fd)
292
+ if not stat.S_ISREG(info.st_mode) or info.st_size > 65536:
293
+ raise approval.ApprovalError("INVALID_SMARTS", "decision request must be bounded regular data")
294
+ data = os.read(fd, 65537)
295
+ if len(data) > 65536:
296
+ raise approval.ApprovalError("INVALID_SMARTS", "decision request exceeds the context budget")
297
+ finally:
298
+ os.close(fd)
299
+ request = _artifactlib._decode(data)
300
+ if not isinstance(request, dict):
301
+ raise approval.ApprovalError("INVALID_SMARTS", "decision request must be a JSON object")
302
+ return client.call("smarts-apply", request)
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — Codex hook bridge for independent artifact reviewers.
3
+ """Correlate closed production review requests with documented Codex hooks."""
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+
9
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
10
+ import hostapi # noqa: E402
11
+ import _artifactauthoritylib # noqa: E402
12
+ import _hooklib # noqa: E402
13
+
14
+
15
+ def main() -> int:
16
+ _hooklib.utf8_stdio()
17
+ payload = _hooklib.read_input()
18
+ if not isinstance(payload, dict):
19
+ return 0
20
+ event = payload.get("hook_event_name")
21
+ if event not in {"PreToolUse", "PostToolUse", "SubagentStart", "SubagentStop"}:
22
+ return 0
23
+ tool = payload.get("tool_name")
24
+ root = _hooklib.project_root(payload)
25
+ try:
26
+ if event in {"PreToolUse", "PostToolUse"} and tool != "spawn_agent":
27
+ result = _artifactauthoritylib.observe_verifier_hook(root, payload)
28
+ if result is None:
29
+ return 0
30
+ else:
31
+ result = _artifactauthoritylib.observe_codex_hook(root, payload)
32
+ if result is None:
33
+ return 0
34
+ except _artifactauthoritylib.AuthorityError as exc:
35
+ reason = f"codeArbiter authority producer blocked: {exc}"
36
+ if event == "PreToolUse":
37
+ print(json.dumps({"decision": "block", "reason": reason}))
38
+ else:
39
+ sys.stderr.write(reason + "\n")
40
+ return 0
41
+ if event == "PreToolUse" and result.get("state") == "AUTHORIZED":
42
+ print(json.dumps({
43
+ "decision": "allow",
44
+ "reason": (
45
+ "codeArbiter authorized this wrapper for the exact resolved child set: "
46
+ + json.dumps(result["command_bindings"], sort_keys=True)
47
+ ),
48
+ }))
49
+ if event == "SubagentStop" and result.get("state") == "COMPLETED":
50
+ print(json.dumps({
51
+ "continue": True,
52
+ "hookSpecificOutput": {
53
+ "hookEventName": "SubagentStop",
54
+ "additionalContext": (
55
+ "codeArbiter: independent review observation recorded for request "
56
+ + result["request_id"]
57
+ + "; publish it through the production authority adapter."
58
+ ),
59
+ },
60
+ }))
61
+ return 0
62
+
63
+
64
+ def run(host) -> int:
65
+ _hooklib.set_host(host)
66
+ if host.name != "codex":
67
+ return 0
68
+ return main()
69
+
70
+
71
+ if __name__ == "__main__":
72
+ raise SystemExit(run(hostapi.load_host()))
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — thin internal CLI for production artifact authority.
3
+ """Arm, execute, and publish closed authority requests; not a public command."""
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import sys
10
+
11
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
12
+ import _artifactauthoritylib # noqa: E402
13
+ import _artifactlib # noqa: E402
14
+
15
+
16
+ def _workspace_map(values: list[str]) -> dict[str, Path]:
17
+ result = {}
18
+ for value in values:
19
+ if "=" not in value:
20
+ raise argparse.ArgumentTypeError("workspace must be LABEL=PATH")
21
+ label, raw = value.split("=", 1)
22
+ if not label or not raw or label in result:
23
+ raise argparse.ArgumentTypeError("workspace labels and paths must be unique")
24
+ result[label] = Path(raw)
25
+ return result
26
+
27
+
28
+ def main(argv=None) -> int:
29
+ parser = argparse.ArgumentParser()
30
+ sub = parser.add_subparsers(dest="command", required=True)
31
+ arm = sub.add_parser("arm")
32
+ arm.add_argument("--root", required=True)
33
+ arm.add_argument("--artifact-id", required=True)
34
+ arm.add_argument("--record-id", required=True)
35
+ arm.add_argument(
36
+ "--activity", required=True,
37
+ choices=("verification", "spec_review", "quality_review"),
38
+ )
39
+ arm.add_argument("--workspace", action="append", default=[], metavar="LABEL=PATH")
40
+ verify = sub.add_parser("verify")
41
+ verify.add_argument("--root", required=True)
42
+ verify.add_argument("--request-id", required=True)
43
+ publish = sub.add_parser("publish")
44
+ publish.add_argument("--root", required=True)
45
+ publish.add_argument("--request-id", required=True)
46
+ recover = sub.add_parser("recover")
47
+ recover.add_argument("--root", required=True)
48
+ recover.add_argument("--request-id", required=True)
49
+ recover.add_argument("--disposition", required=True, choices=("failed", "abandoned"))
50
+ args = parser.parse_args(argv)
51
+ root = Path(args.root).resolve(strict=True)
52
+ client = _artifactlib.ArtifactClient(
53
+ root, _artifactlib.helper_installation(__file__)
54
+ )
55
+ if args.command == "arm":
56
+ workspaces = _workspace_map(args.workspace)
57
+ result = _artifactauthoritylib.arm_request(
58
+ root, client, args.artifact_id, args.record_id, args.activity,
59
+ workspace_roots=workspaces or None,
60
+ )
61
+ elif args.command == "verify":
62
+ result = _artifactauthoritylib.run_verification(
63
+ root, client, args.request_id,
64
+ )
65
+ elif args.command == "publish":
66
+ result = _artifactauthoritylib.publish_request(root, client, args.request_id)
67
+ else:
68
+ result = _artifactauthoritylib.recover_request(
69
+ root, args.request_id, args.disposition
70
+ )
71
+ print(json.dumps(result, sort_keys=True))
72
+ return 0
73
+
74
+
75
+ if __name__ == "__main__":
76
+ try:
77
+ raise SystemExit(main())
78
+ except (_artifactauthoritylib.AuthorityError, _artifactlib.ArtifactError) as exc:
79
+ sys.stderr.write(str(exc) + "\n")
80
+ raise SystemExit(1)
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env python3
2
+ """Arm an exact user-workflow reconciliation request."""
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import sys
9
+
10
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
11
+ import _artifactlib
12
+ import _reconciliationlib
13
+
14
+
15
+ def main(argv=None):
16
+ parser = argparse.ArgumentParser()
17
+ parser.add_argument("--root", required=True)
18
+ parser.add_argument("--artifact-id", required=True)
19
+ parser.add_argument("--target-id")
20
+ parser.add_argument("--operation", choices=("task-reconcile", "scope-reconcile"))
21
+ parser.add_argument("--target-state", choices=("PENDING", "REVIEW", "BLOCKED"))
22
+ parser.add_argument("--reason", default="")
23
+ parser.add_argument("--assessment")
24
+ parser.add_argument("--cancel", action="store_true", help="cancel an armed request before its mutation attempt")
25
+ parser.add_argument("--cancel-orphan", action="store_true", help="cancel a pending request whose prompt route was never registered")
26
+ parser.add_argument("--recover", action="store_true", help="replay an in-flight mutation before clearing its marker")
27
+ parser.add_argument("--prompt", help="exact armed prompt required for --cancel")
28
+ args = parser.parse_args(argv)
29
+ root = Path(args.root).resolve(strict=True)
30
+ if args.cancel_orphan:
31
+ if args.cancel or args.recover or args.prompt or args.target_id or args.operation or args.target_state or args.assessment:
32
+ parser.error("--cancel-orphan excludes arming and exact-prompt cancellation fields")
33
+ print(json.dumps(_reconciliationlib.cancel_orphaned_reconciliation(root, args.artifact_id), sort_keys=True))
34
+ return 0
35
+ if args.cancel:
36
+ if args.recover or args.prompt is None or args.target_id or args.operation or args.target_state or args.assessment:
37
+ parser.error("--cancel requires --prompt and excludes arming fields")
38
+ print(json.dumps(_reconciliationlib.cancel_reconciliation(root, args.artifact_id, args.prompt), sort_keys=True))
39
+ return 0
40
+ if args.recover:
41
+ if args.prompt or args.target_id or args.operation or args.target_state or args.assessment:
42
+ parser.error("--recover excludes arming and cancellation fields")
43
+ client = _artifactlib.ArtifactClient(root, _artifactlib.helper_installation(__file__))
44
+ print(json.dumps(_reconciliationlib.recover_reconciliation(root, client, args.artifact_id), sort_keys=True))
45
+ return 0
46
+ if not args.target_id or not args.operation or not args.assessment or args.prompt is not None:
47
+ parser.error("arming requires --target-id, --operation, and --assessment, without --prompt")
48
+ client = _artifactlib.ArtifactClient(root, _artifactlib.helper_installation(__file__))
49
+ print(json.dumps(_reconciliationlib.arm_reconciliation(
50
+ root, client, args.artifact_id, args.target_id, args.operation,
51
+ target_state=args.target_state, reason=args.reason, assessment=args.assessment,
52
+ ), sort_keys=True))
53
+ return 0
54
+
55
+
56
+ if __name__ == "__main__":
57
+ raise SystemExit(main())
@@ -376,7 +376,9 @@ def check_git_hook_freshness(root):
376
376
  if "ignored because" in (probe.stderr or "").lower():
377
377
  fail(f"git-hook backstop live-fire was ignored by Git: {probe.stderr.strip()[:240]}")
378
378
  return
379
- ok("git-hook backstop live-fire: selected Git executed the managed pre-push shim")
379
+ ok("git-hook backstop live-fire: this host's selected Git executed the managed "
380
+ "pre-push shim (proves THIS host only — a sibling host sharing the registry "
381
+ "is not covered by this result, B3/#684)")
380
382
  stale = _githooks.stale_registered_plugins(dropin_dir)
381
383
  if stale:
382
384
  names = ", ".join(sorted(stale))
@@ -313,7 +313,7 @@ class Host:
313
313
 
314
314
  name = "claude"
315
315
  adapter_name = "ca"
316
- adapter_version = "2.21.9"
316
+ adapter_version = "2.21.12"
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,
@@ -68,6 +68,7 @@ import _approvallib # noqa: E402 — host-owned artifact approval capture
68
68
  import _hooklib # noqa: E402
69
69
  import _modelib # noqa: E402 — mode plane core (T-06..T-16, Lane A)
70
70
  import _prerequisitelib # noqa: E402 — host-owned prerequisite capture
71
+ import _reconciliationlib # noqa: E402 — host-owned reconciliation capture
71
72
  import _readinjectlib # noqa: E402 — marker_path(prefix=) (T-30)
72
73
 
73
74
 
@@ -449,9 +450,8 @@ def _handle_codex(payload, host, root, session_id, token, approval_context=""):
449
450
  def _handle_user_prompt_submit(payload, host):
450
451
  session_id = payload.get("session_id") or ""
451
452
  root = _hooklib.project_root(payload)
452
- if not _hooklib.arbiter_active(root):
453
- return 0 # dormant repo: no flip, no report, no injection
454
- state_root = _hooklib.marker_root(payload)
453
+ active = _hooklib.arbiter_active(root)
454
+ state_root = _hooklib.marker_root(payload) if active else root
455
455
  prompt = payload.get("prompt")
456
456
  prompt = prompt if isinstance(prompt, str) else ""
457
457
  token = _modelib.match_mode_token(prompt)
@@ -476,7 +476,24 @@ def _handle_user_prompt_submit(payload, host):
476
476
  )
477
477
  if prerequisite:
478
478
  contexts.append(prerequisite)
479
+ reconciliation = _reconciliationlib.consume_from_hook(
480
+ root=root,
481
+ plugin_root=host.plugin_root(),
482
+ prompt=prompt,
483
+ host=host.name,
484
+ session_id=session_id,
485
+ )
486
+ if reconciliation:
487
+ contexts.append(reconciliation)
479
488
  approval_context = "\n".join(contexts)
489
+ if not active:
490
+ if not approval_context:
491
+ return 0
492
+ if host.name == "codex":
493
+ print(json.dumps(_codex_allow_envelope(approval_context)))
494
+ else:
495
+ print(approval_context)
496
+ return 0
480
497
  if host.name == "codex":
481
498
  return _handle_codex(
482
499
  payload, host, state_root, session_id, token, approval_context