@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.
- package/README.md +31 -12
- package/package.json +2 -1
- package/plugins/ca-pi/CHANGELOG.md +39 -0
- package/plugins/ca-pi/LICENSE +661 -0
- package/plugins/ca-pi/THIRD_PARTY_NOTICES.md +40 -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/_artifactauthoritylib.py +486 -37
- package/plugins/ca-pi/hooks/_artifactlib.py +150 -3
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_releaselib.py +345 -111
- package/plugins/ca-pi/hooks/artifact-authority-hook.py +57 -2
- package/plugins/ca-pi/hooks/artifact-authority.py +23 -1
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/releasehash.py +36 -23
- package/plugins/ca-pi/includes/artifacts.md +26 -6
- package/plugins/ca-pi/package.json +1 -1
- package/plugins/ca-pi/routines/release/SKILL.md +220 -70
- package/plugins/ca-pi/skills/ca-release/SKILL.md +6 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# codeArbiter — Codex hook bridge for
|
|
3
|
-
"""Correlate closed production review requests with
|
|
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.
|
|
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`
|
|
5
|
-
# shell that `/ca:release`
|
|
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
|
|
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
|
|
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
|
|
95
|
-
marker identity, so existing confirmations do not churn.
|
|
96
|
-
command is appended with a typed, length-prefixed
|
|
97
|
-
|
|
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
|
|
170
|
-
confirm, and reporting it as confirmed would claim an operator approved
|
|
171
|
-
something that does not exist. A
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
177
|
-
Pi, stop at this boundary rather than treating model-authored results as
|
|
178
|
-
authority;
|
|
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
|
|
208
|
-
child stop; a pasted or coordinator-authored decision is not review
|
|
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
|