@arbiterforge/ca-pi 0.14.12 → 0.14.16

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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
- # codeArbiter — Codex hook bridge for independent artifact reviewers.
3
- """Correlate closed production review requests with documented Codex hooks."""
2
+ # codeArbiter — Codex and Claude Code hook bridge for artifact authority.
3
+ """Correlate closed production verification and review requests with host hooks."""
4
4
 
5
5
  import json
6
6
  import os
@@ -61,8 +61,63 @@ def main() -> int:
61
61
  return 0
62
62
 
63
63
 
64
+ def claude_main() -> int:
65
+ """Claude Code seams. Success is silent: an explicit allow would bypass the
66
+ user's permission rules, and a SubagentStop block would make the reviewer
67
+ keep going. Only a PreToolUse refusal prints (a Claude deny decision)."""
68
+ _hooklib.utf8_stdio()
69
+ payload = _hooklib.read_input()
70
+ if not isinstance(payload, dict):
71
+ return 0
72
+ event = payload.get("hook_event_name")
73
+ tool = payload.get("tool_name")
74
+ root = _hooklib.project_root(payload)
75
+ try:
76
+ if event in {"PreToolUse", "PostToolUse", "PostToolUseFailure"} and tool == "Bash":
77
+ _artifactauthoritylib.observe_verifier_hook(root, payload, host="claude")
78
+ elif (
79
+ (event in {"PreToolUse", "PostToolUse"} and tool == "Agent")
80
+ or (event == "PreToolUse" and tool == "SendMessage")
81
+ or event in {"SubagentStart", "SubagentStop"}
82
+ ):
83
+ _artifactauthoritylib.observe_claude_hook(root, payload)
84
+ except Exception as exc: # noqa: BLE001 - an authority seam must fail closed
85
+ reason = f"codeArbiter authority producer blocked: {exc}"
86
+ if event == "PreToolUse" and (
87
+ isinstance(exc, _artifactauthoritylib.AuthorityError) or _authority_call(payload)
88
+ ):
89
+ print(json.dumps({"hookSpecificOutput": {
90
+ "hookEventName": "PreToolUse",
91
+ "permissionDecision": "deny",
92
+ "permissionDecisionReason": reason,
93
+ }}))
94
+ else:
95
+ sys.stderr.write(reason + "\n")
96
+ return 0
97
+
98
+
99
+ def _authority_call(payload: dict) -> bool:
100
+ """Whether an unexpected failure must deny: an ordinary tool call is never
101
+ refused, but a SendMessage could steer a running review and an authority
102
+ launch or wrapper must not proceed unobserved."""
103
+ tool = payload.get("tool_name")
104
+ tool_input = payload.get("tool_input")
105
+ tool_input = tool_input if isinstance(tool_input, dict) else {}
106
+ if tool == "SendMessage":
107
+ return True
108
+ if tool == "Agent":
109
+ prompt = tool_input.get("prompt")
110
+ return isinstance(prompt, str) and prompt.startswith("[CODEARBITER_AUTHORITY_REQUEST:")
111
+ if tool == "Bash":
112
+ command = tool_input.get("command")
113
+ return isinstance(command, str) and "artifact-authority" in command.casefold()
114
+ return False
115
+
116
+
64
117
  def run(host) -> int:
65
118
  _hooklib.set_host(host)
119
+ if host.name == "claude":
120
+ return claude_main()
66
121
  if host.name != "codex":
67
122
  return 0
68
123
  return main()
@@ -11,6 +11,7 @@ import sys
11
11
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
12
12
  import _artifactauthoritylib # noqa: E402
13
13
  import _artifactlib # noqa: E402
14
+ import hostapi # noqa: E402
14
15
 
15
16
 
16
17
  def _workspace_map(values: list[str]) -> dict[str, Path]:
@@ -37,6 +38,14 @@ def main(argv=None) -> int:
37
38
  choices=("verification", "spec_review", "quality_review"),
38
39
  )
39
40
  arm.add_argument("--workspace", action="append", default=[], metavar="LABEL=PATH")
41
+ arm.add_argument(
42
+ "--host", choices=sorted(_artifactauthoritylib.HOSTS), default=None,
43
+ help="observing host; defaults to the host this package was built for",
44
+ )
45
+ arm.add_argument(
46
+ "--reviewer-model", choices=sorted(_artifactauthoritylib.CLAUDE_REVIEWER_MODELS),
47
+ default="opus", help="Claude reviewer model pinned into the launch envelope",
48
+ )
40
49
  verify = sub.add_parser("verify")
41
50
  verify.add_argument("--root", required=True)
42
51
  verify.add_argument("--request-id", required=True)
@@ -54,10 +63,23 @@ def main(argv=None) -> int:
54
63
  )
55
64
  if args.command == "arm":
56
65
  workspaces = _workspace_map(args.workspace)
66
+ host = args.host or hostapi.load_host().name
67
+ if host not in _artifactauthoritylib.HOSTS:
68
+ raise _artifactauthoritylib.AuthorityError(
69
+ "UNSUPPORTED_HOST_SEAM", f"host {host} has no verification or review authority"
70
+ )
57
71
  result = _artifactauthoritylib.arm_request(
58
72
  root, client, args.artifact_id, args.record_id, args.activity,
59
- workspace_roots=workspaces or None,
73
+ workspace_roots=workspaces or None, host=host,
74
+ reviewer_model=args.reviewer_model,
60
75
  )
76
+ if host == "claude" and args.activity == "verification":
77
+ # The Claude verifier hook pins this exact shipped script and
78
+ # refuses shell expansion, so hand back literal paths.
79
+ script = Path(__file__).resolve().parent / "artifact-authority.py"
80
+ result["verify_command"] = (
81
+ f'python "{script}" verify --root "{root}" --request-id {result["request_id"]}'
82
+ )
61
83
  elif args.command == "verify":
62
84
  result = _artifactauthoritylib.run_verification(
63
85
  root, client, args.request_id,
@@ -313,7 +313,7 @@ class Host:
313
313
 
314
314
  name = "claude"
315
315
  adapter_name = "ca"
316
- adapter_version = "2.21.12"
316
+ adapter_version = "2.21.16"
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,8 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  # codeArbiter — executable release-command confirmation (A-2.10).
3
3
  #
4
- # A row's `pre-tag` commands and optional `release-build` are operator-authored
5
- # shell that `/ca:release` EXECUTES before composing a tag.
4
+ # A row's `pre-tag`, `release-build`, `rebuild`, and `generate` commands are
5
+ # operator-authored shell that `/ca:release` may execute before publication.
6
6
  # `.codearbiter/release-targets.md` is
7
7
  # marker-gated under H-22, so changing those commands already costs a fresh
8
8
  # authoring marker — but that gate is about WRITING the file. Nothing made the
@@ -31,11 +31,13 @@
31
31
  # file.
32
32
  #
33
33
  # Public API:
34
- # pre_tag_digest(commands, release_build=None) -> str
34
+ # pre_tag_digest(commands, release_build=None, rebuild=None, generate=None)
35
+ # -> str
35
36
  # confirmation_path(root, target) -> str
36
37
  # read_confirmation(root, target) -> str | None
37
38
  # record_confirmation(root, target, digest) -> str
38
- # confirmation_state(root, target, commands, release_build=None) -> str
39
+ # confirmation_state(root, target, commands, release_build=None,
40
+ # rebuild=None, generate=None) -> str
39
41
  # main(argv) -> int
40
42
  #
41
43
  # CLI:
@@ -78,7 +80,7 @@ NEVER = "never-confirmed"
78
80
  NO_COMMANDS = "no-commands"
79
81
 
80
82
 
81
- def pre_tag_digest(commands, release_build=None):
83
+ def pre_tag_digest(commands, release_build=None, rebuild=None, generate=None):
82
84
  """A stable digest of a row's executable release command inputs.
83
85
 
84
86
  ORDER-SENSITIVE on purpose. The commands run in declared order and a
@@ -91,10 +93,10 @@ def pre_tag_digest(commands, release_build=None):
91
93
  change to this construction cannot be mistaken for a content change
92
94
  by an old recorded value.
93
95
 
94
- Rows without `release-build` deliberately retain the exact v1 digest and
95
- marker identity, so existing confirmations do not churn. A present build
96
- command is appended with a typed, length-prefixed record, so changing only
97
- that command invalidates the same per-target confirmation marker.
96
+ Rows without any newly bound scalar command deliberately retain the exact
97
+ v1 digest and marker identity, so existing confirmations do not churn.
98
+ Each present scalar command is appended with a typed, length-prefixed
99
+ record, so changing only one invalidates the same per-target marker.
98
100
 
99
101
  Pure and non-raising: malformed values degrade conservatively to their
100
102
  absent forms rather than raising inside a release lane.
@@ -108,6 +110,10 @@ def pre_tag_digest(commands, release_build=None):
108
110
  parts.append(f"{len(command)}:{command}")
109
111
  if isinstance(release_build, str) and release_build:
110
112
  parts.append(f"release-build:{len(release_build)}:{release_build}")
113
+ if isinstance(rebuild, str) and rebuild:
114
+ parts.append(f"rebuild:{len(rebuild)}:{rebuild}")
115
+ if isinstance(generate, str) and generate:
116
+ parts.append(f"generate:{len(generate)}:{generate}")
111
117
  joined = "\n".join(parts)
112
118
  return hashlib.sha256(joined.encode("utf-8")).hexdigest()
113
119
 
@@ -162,22 +168,24 @@ def record_confirmation(root, target, digest):
162
168
  return path
163
169
 
164
170
 
165
- def confirmation_state(root, target, commands, release_build=None):
171
+ def confirmation_state(root, target, commands, release_build=None,
172
+ rebuild=None, generate=None):
166
173
  """One of `confirmed` / `changed` / `never-confirmed` / `no-commands`.
167
174
 
168
175
  `no-commands` is deliberately distinct from `confirmed`: a row that
169
- declares neither pre-tag commands nor release-build has nothing to
170
- confirm, and reporting it as confirmed would claim an operator approved
171
- something that does not exist. A release-build-only row still requires
172
- confirmation. The lane treats `no-commands` and `confirmed` as
176
+ declares none of pre-tag, release-build, rebuild, or generate has nothing
177
+ to confirm, and reporting it as confirmed would claim an operator approved
178
+ something that does not exist. A row with any one scalar command still
179
+ requires confirmation. The lane treats `no-commands` and `confirmed` as
173
180
  "proceed", but only one of them is a statement about human review.
174
181
  """
175
- if not commands and not release_build:
182
+ if not commands and not release_build and not rebuild and not generate:
176
183
  return NO_COMMANDS
177
184
  recorded = read_confirmation(root, target)
178
185
  if recorded is None:
179
186
  return NEVER
180
- return (CONFIRMED if recorded == pre_tag_digest(commands, release_build)
187
+ return (CONFIRMED if recorded == pre_tag_digest(
188
+ commands, release_build, rebuild, generate)
181
189
  else CHANGED)
182
190
 
183
191
 
@@ -187,7 +195,8 @@ def _row_executable_inputs(target, targets_path=None):
187
195
  row = next((r for r in rows if r["target"] == target), None)
188
196
  if row is None:
189
197
  return None
190
- return row.get("pre_tag") or [], row.get("release_build")
198
+ return (row.get("pre_tag") or [], row.get("release_build"),
199
+ row.get("rebuild"), row.get("generate"))
191
200
 
192
201
 
193
202
  def main(argv=None):
@@ -207,26 +216,29 @@ def main(argv=None):
207
216
  if executable_inputs is None:
208
217
  sys.stderr.write(f"unknown release target: {target}\n")
209
218
  return 65
210
- commands, release_build = executable_inputs
219
+ commands, release_build, rebuild, generate = executable_inputs
211
220
 
212
221
  if command == "digest":
213
- print(pre_tag_digest(commands, release_build))
222
+ print(pre_tag_digest(commands, release_build, rebuild, generate))
214
223
  return 0
215
224
 
216
225
  if command == "record":
217
226
  path = record_confirmation(
218
- root, target, pre_tag_digest(commands, release_build))
227
+ root, target, pre_tag_digest(
228
+ commands, release_build, rebuild, generate))
219
229
  print(f"recorded executable-command confirmation for {target}: {path}")
220
230
  return 0
221
231
 
222
- state = confirmation_state(root, target, commands, release_build)
232
+ state = confirmation_state(
233
+ root, target, commands, release_build, rebuild, generate)
223
234
  print(state)
224
235
  if state in (CONFIRMED, NO_COMMANDS):
225
236
  return 0
226
237
  if state == CHANGED:
227
238
  sys.stderr.write(
228
239
  f"releasehash: {target}'s executable release commands have CHANGED "
229
- "since they were last confirmed. Read every pre-tag and release-build "
240
+ "since they were last confirmed. Read every pre-tag, release-build, "
241
+ "rebuild, and generate "
230
242
  "command, and re-confirm with "
231
243
  f"`releasehash.py record {target}` before releasing. These commands "
232
244
  "are executed by the release lane, so a change nobody has read is "
@@ -234,7 +246,8 @@ def main(argv=None):
234
246
  return 1
235
247
  sys.stderr.write(
236
248
  f"releasehash: {target}'s executable release commands have never been "
237
- f"confirmed. Read every pre-tag and release-build command, then run "
249
+ f"confirmed. Read every pre-tag, release-build, rebuild, and generate "
250
+ f"command, then run "
238
251
  f"`releasehash.py record {target}`.\n")
239
252
  return 2
240
253
 
@@ -10,11 +10,30 @@ default to canonical `.html`. Check the installed capability before writing; a
10
10
  missing, invalid, mismatched or unqualified capability is a STOP and
11
11
  the workflow must not fall back to Markdown or create any fallback artifact.
12
12
  Report `CAPABILITY_MISSING` and direct the user to repair or reinstall the pinned
13
- artifact payload before retrying. An existing `.md` exact pair remains Markdown
13
+ artifact payload before retrying. The payload ships only through the release
14
+ channels: Claude Code `/plugin marketplace update codearbiter` then
15
+ `/plugin update ca@codearbiter`; Codex the `ca-codex-marketplace` ref; Pi
16
+ `pi install npm:@arbiterforge/ca-pi`. A main-branch checkout never carries it. An existing `.md` exact pair remains Markdown
14
17
  and continues on the legacy path without automatic conversion. The feature
15
18
  small lane remains an inline mini-spec and creates no artifact. HTML `--farm`
16
19
  dispatch remains disabled and outside this rollout.
17
20
 
21
+ ## Installed host admission
22
+
23
+ Before new full-lane or plan authoring, the bridge checks the exact installed
24
+ host's required producers, helpers, reviewer charter where applicable, and
25
+ platform-specific hook registrations. `_select_authoring_route` and
26
+ `_preflight_plan_authoring` require this read-only preflight in addition to the
27
+ native engine capability. `HOST_WORKFLOW_UNAVAILABLE` stops before authoring;
28
+ repair the installed package or use a supported host. Never create a fallback
29
+ artifact, substitute another package, or mint an authority event to proceed.
30
+
31
+ `ArtifactClient.workflow_preflight()` reports only installed resources. It does
32
+ not prove host trust, enabled hooks, supported host-version behavior, reviewer
33
+ independence, or a completed live workflow. Actual authority producers and their
34
+ existing validation remain required. Pi currently fails this admission check;
35
+ existing Markdown pairs and the inline small feature lane are unchanged.
36
+
18
37
  ## Internal execution contract
19
38
 
20
39
  The normal qualified package's installation-owned `release.json` pins the native
@@ -173,9 +192,9 @@ scope evidence. Provisional progress never counts as completion.
173
192
  The orchestrator executes the declared verification through its existing governed
174
193
  execution tool through the installed production authority adapter; it never
175
194
  constructs a successful workflow event itself. Arm an exact task request first.
176
- Verification and review authority below is currently Codex-only. On Claude or
177
- Pi, stop at this boundary rather than treating model-authored results as
178
- authority; their installed prompt approval seams do not imply verification or
195
+ Verification and review authority below is supported on Codex and Claude Code.
196
+ On Pi, stop at this boundary rather than treating model-authored results as
197
+ authority; its installed prompt approval seam does not imply verification or
179
198
  review support.
180
199
  Every distinct `cwd` label in the engine context must be mapped once to an exact
181
200
  linked Git worktree root. Mapped worktrees must share the artifact repository's
@@ -204,8 +223,9 @@ attempt under the same request.
204
223
 
205
224
  For `spec_review` or `quality_review`, arm the corresponding activity without
206
225
  workspace mappings and dispatch the returned `launch_envelope` unchanged to one
207
- fresh host subagent. The Codex hooks bind the exact spawn call, child start and
208
- child stop; a pasted or coordinator-authored decision is not review authority.
226
+ fresh host subagent. The host hooks bind the exact launch call, child start and
227
+ the child's first stop; a pasted or coordinator-authored decision is not review
228
+ authority.
209
229
  Publish the completed request through the same `publish` command. `task-review`
210
230
  records the separate verification and spec-review receipts. After every task in
211
231
  the scope reaches `REVIEW`, reverify against one current source snapshot, run the
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.14.12",
3
+ "version": "0.14.16",
4
4
  "private": true,
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",