@arbiterforge/ca-pi 0.8.1 → 0.10.2
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 +29 -90
- package/package.json +1 -1
- package/plugins/ca-pi/CHANGELOG.md +89 -0
- package/plugins/ca-pi/COMMANDS.md +141 -64
- package/plugins/ca-pi/SKILLS.md +137 -28
- package/plugins/ca-pi/agents/INDEX.md +3 -2
- package/plugins/ca-pi/agents/checkpoint-aggregator.md +8 -7
- package/plugins/ca-pi/agents/finding-triage.md +31 -14
- package/plugins/ca-pi/agents/verdict-aggregator.md +64 -0
- package/plugins/ca-pi/arbiter.md +12 -3
- package/plugins/ca-pi/extensions/codearbiter.js +137 -15
- package/plugins/ca-pi/generated/command-catalog.json +386 -186
- package/plugins/ca-pi/generated/roles.json +9 -0
- package/plugins/ca-pi/hooks/_bashguardlib.py +33 -16
- package/plugins/ca-pi/hooks/_gitexec.py +23 -0
- package/plugins/ca-pi/hooks/_githooks.py +50 -23
- package/plugins/ca-pi/hooks/_hooklib.py +94 -7
- package/plugins/ca-pi/hooks/_host.py +9 -1
- package/plugins/ca-pi/hooks/_modelib.py +173 -55
- package/plugins/ca-pi/hooks/_protectedlib.py +13 -4
- package/plugins/ca-pi/hooks/_releaselib.py +278 -48
- package/plugins/ca-pi/hooks/_updatelib.py +230 -50
- package/plugins/ca-pi/hooks/doctor.py +56 -8
- package/plugins/ca-pi/hooks/git-enforce.py +10 -3
- package/plugins/ca-pi/hooks/hostapi.py +220 -22
- package/plugins/ca-pi/hooks/session-start.py +8 -6
- package/plugins/ca-pi/hooks/statusline.py +1 -1
- package/plugins/ca-pi/hooks/wire-statusline.py +13 -8
- package/plugins/ca-pi/includes/command-compatibility.md +16 -0
- package/plugins/ca-pi/includes/routing-table.md +13 -5
- package/plugins/ca-pi/routines/INDEX.md +1 -1
- package/plugins/ca-pi/routines/decision-lifecycle/SKILL.md +54 -2
- package/plugins/ca-pi/routines/decision-lifecycle/references/adr-template.md +9 -1
- package/plugins/ca-pi/routines/dispatching-parallel-agents/SKILL.md +4 -4
- package/plugins/ca-pi/routines/release/SKILL.md +1 -1
- package/plugins/ca-pi/skills/ca-checkpoint/SKILL.md +5 -4
- package/plugins/ca-pi/skills/ca-cleanup/SKILL.md +6 -0
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +6 -0
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +6 -0
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +6 -0
- package/plugins/ca-pi/skills/ca-doctor/SKILL.md +4 -0
- package/plugins/ca-pi/skills/ca-init/SKILL.md +18 -1
- package/plugins/ca-pi/skills/ca-pr/SKILL.md +17 -1
- package/plugins/ca-pi/skills/ca-review/SKILL.md +3 -4
- package/plugins/ca-pi/skills/ca-status/SKILL.md +13 -1
- package/plugins/ca-pi/skills/ca-watch/SKILL.md +6 -0
|
@@ -27,11 +27,114 @@
|
|
|
27
27
|
# Behavioral contract for M1: under Claude Code, everything routed through
|
|
28
28
|
# this seam resolves to byte-identical results as the pre-seam inline code.
|
|
29
29
|
|
|
30
|
+
import json
|
|
30
31
|
import os
|
|
32
|
+
import re
|
|
31
33
|
import subprocess
|
|
32
34
|
import sys
|
|
33
35
|
|
|
34
|
-
from _gitexec import git_executable
|
|
36
|
+
from _gitexec import git_executable, root_bound_git_env
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PluginRootError(RuntimeError):
|
|
40
|
+
"""A host adapter's installed package boundary cannot be authenticated."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_VERSION_RE = re.compile(
|
|
44
|
+
r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _safe_relative_path(value, label):
|
|
48
|
+
"""Reject an absolute or traversing adapter-relative path before joining."""
|
|
49
|
+
if not isinstance(value, str) or not value:
|
|
50
|
+
raise PluginRootError(f"{label} must be a non-empty relative path")
|
|
51
|
+
normalized = os.path.normpath(value)
|
|
52
|
+
if (os.path.isabs(value) or normalized == os.pardir or
|
|
53
|
+
normalized.startswith(os.pardir + os.sep)):
|
|
54
|
+
raise PluginRootError(f"{label} escapes the adapter: {value!r}")
|
|
55
|
+
return normalized
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _inside(root, candidate):
|
|
59
|
+
"""Whether normalized ``candidate`` is contained by normalized ``root``."""
|
|
60
|
+
try:
|
|
61
|
+
return os.path.commonpath((root, candidate)) == root
|
|
62
|
+
except ValueError: # distinct Windows volumes are never contained
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def resolve_plugin_root(authority_file, *, adapter_name, adapter_version, manifest_relpath,
|
|
67
|
+
anchor_relpath, environment=None, signal_names=(),
|
|
68
|
+
required_signal_names=(), legacy_signal_names=()):
|
|
69
|
+
"""Return the authenticated root of the executing adapter, or fail closed.
|
|
70
|
+
|
|
71
|
+
The authority is the file/module that is already executing. Environment
|
|
72
|
+
variables may only corroborate that exact real path; none can select a
|
|
73
|
+
different, merely plausible package. ``anchor_relpath`` and the manifest
|
|
74
|
+
are constrained inside that root so traversal and symlink escapes cannot
|
|
75
|
+
authenticate an external file.
|
|
76
|
+
"""
|
|
77
|
+
if not isinstance(adapter_name, str) or not adapter_name:
|
|
78
|
+
raise PluginRootError("adapter name must be a non-empty string")
|
|
79
|
+
if not isinstance(adapter_version, str) or not _VERSION_RE.fullmatch(adapter_version):
|
|
80
|
+
raise PluginRootError("adapter version must be an exact SemVer string")
|
|
81
|
+
manifest_relpath = _safe_relative_path(manifest_relpath, "manifest path")
|
|
82
|
+
anchor_relpath = _safe_relative_path(anchor_relpath, "anchor path")
|
|
83
|
+
env = os.environ if environment is None else environment
|
|
84
|
+
|
|
85
|
+
# Keep the package boundary lexical until after its parent directories are
|
|
86
|
+
# derived. If hooks/hostapi.py itself is a symlink, realpathing it first
|
|
87
|
+
# would re-home this adapter in a complete, matching foreign package.
|
|
88
|
+
lexical_authority = os.path.abspath(authority_file)
|
|
89
|
+
root = os.path.realpath(os.path.dirname(os.path.dirname(lexical_authority)))
|
|
90
|
+
source = os.path.realpath(lexical_authority)
|
|
91
|
+
anchor = os.path.realpath(os.path.join(root, anchor_relpath))
|
|
92
|
+
if (not _inside(root, source) or not _inside(root, anchor) or
|
|
93
|
+
anchor != source or not os.path.isfile(anchor)):
|
|
94
|
+
raise PluginRootError(
|
|
95
|
+
f"executing adapter anchor is outside or missing from its package: {source}")
|
|
96
|
+
|
|
97
|
+
manifest = os.path.realpath(os.path.join(root, manifest_relpath))
|
|
98
|
+
if not _inside(root, manifest) or not os.path.isfile(manifest):
|
|
99
|
+
raise PluginRootError(
|
|
100
|
+
f"{adapter_name}: required manifest is missing or outside the adapter: {manifest}")
|
|
101
|
+
try:
|
|
102
|
+
with open(manifest, encoding="utf-8") as handle:
|
|
103
|
+
data = json.load(handle)
|
|
104
|
+
except (OSError, ValueError) as error:
|
|
105
|
+
raise PluginRootError(
|
|
106
|
+
f"{adapter_name}: required manifest is unreadable: {manifest} ({error})") from error
|
|
107
|
+
name = data.get("name") if isinstance(data, dict) else None
|
|
108
|
+
version = data.get("version") if isinstance(data, dict) else None
|
|
109
|
+
if name != adapter_name:
|
|
110
|
+
raise PluginRootError(
|
|
111
|
+
f"adapter manifest mismatch at {manifest}: expected {adapter_name!r}, got {name!r}")
|
|
112
|
+
if not isinstance(version, str) or not _VERSION_RE.fullmatch(version):
|
|
113
|
+
raise PluginRootError(
|
|
114
|
+
f"adapter manifest has invalid version at {manifest}: {version!r}")
|
|
115
|
+
if version != adapter_version:
|
|
116
|
+
raise PluginRootError(
|
|
117
|
+
f"adapter manifest version mismatch at {manifest}: "
|
|
118
|
+
f"expected {adapter_version!r}, got {version!r}")
|
|
119
|
+
|
|
120
|
+
for signal in required_signal_names:
|
|
121
|
+
if not env.get(signal):
|
|
122
|
+
raise PluginRootError(
|
|
123
|
+
f"{adapter_name}: required {signal} is absent; executing root is {root}")
|
|
124
|
+
for signal in signal_names:
|
|
125
|
+
value = env.get(signal)
|
|
126
|
+
if not value:
|
|
127
|
+
continue
|
|
128
|
+
claimed = os.path.realpath(os.path.abspath(value))
|
|
129
|
+
if claimed != root:
|
|
130
|
+
raise PluginRootError(
|
|
131
|
+
f"{adapter_name}: {signal} root {claimed} disagrees with "
|
|
132
|
+
f"executing adapter root {root}")
|
|
133
|
+
if signal in legacy_signal_names:
|
|
134
|
+
sys.stderr.write(
|
|
135
|
+
f"codeArbiter: {signal} is deprecated for {adapter_name}; "
|
|
136
|
+
"accepted only as matching corroboration.\n")
|
|
137
|
+
return root
|
|
35
138
|
|
|
36
139
|
|
|
37
140
|
def git_toplevel(cwd=None):
|
|
@@ -52,7 +155,7 @@ def git_toplevel(cwd=None):
|
|
|
52
155
|
try:
|
|
53
156
|
out = subprocess.run(
|
|
54
157
|
args, capture_output=True, text=True, encoding="utf-8",
|
|
55
|
-
errors="replace", timeout=5,
|
|
158
|
+
errors="replace", timeout=5, env=root_bound_git_env(),
|
|
56
159
|
)
|
|
57
160
|
if out.returncode == 0:
|
|
58
161
|
top = out.stdout.strip()
|
|
@@ -63,6 +166,38 @@ def git_toplevel(cwd=None):
|
|
|
63
166
|
return None
|
|
64
167
|
|
|
65
168
|
|
|
169
|
+
def _root_bound_git_env():
|
|
170
|
+
"""Compatibility seam for callers of the former local helper."""
|
|
171
|
+
return root_bound_git_env()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _has_enabled_context(root):
|
|
175
|
+
"""Whether a real CONTEXT.md satisfies the canonical activation parser."""
|
|
176
|
+
canonical_root = os.path.realpath(root)
|
|
177
|
+
state_dir = os.path.join(canonical_root, ".codearbiter")
|
|
178
|
+
if os.path.islink(state_dir) or not os.path.isdir(state_dir):
|
|
179
|
+
return False
|
|
180
|
+
context = os.path.join(state_dir, "CONTEXT.md")
|
|
181
|
+
canonical_context = os.path.realpath(context)
|
|
182
|
+
try:
|
|
183
|
+
contained = os.path.normcase(os.path.commonpath(
|
|
184
|
+
[canonical_root, canonical_context])) == os.path.normcase(canonical_root)
|
|
185
|
+
except (TypeError, ValueError):
|
|
186
|
+
contained = False
|
|
187
|
+
if (not contained or os.path.islink(context)
|
|
188
|
+
or not os.path.isfile(canonical_context)):
|
|
189
|
+
return False
|
|
190
|
+
try:
|
|
191
|
+
# Deferred to avoid hostapi <-> _activationlib's import-time cycle.
|
|
192
|
+
# This must remain the single parser for the activation contract,
|
|
193
|
+
# including its accepted UTF-8 BOM spelling.
|
|
194
|
+
from _activationlib import frontmatter_enabled
|
|
195
|
+
enabled, malformed = frontmatter_enabled(canonical_context)
|
|
196
|
+
return enabled and not malformed
|
|
197
|
+
except Exception: # noqa: BLE001 - root selection must fail closed
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
|
|
66
201
|
def git_worktree_main_root(root):
|
|
67
202
|
"""When `root` (an already-resolved project root — a `project_root()`
|
|
68
203
|
answer, NOT necessarily a fresh `git_toplevel` call) is itself the
|
|
@@ -100,24 +235,71 @@ def git_worktree_main_root(root):
|
|
|
100
235
|
FILE, but only a worktree's `gitdir:` pointer names a path under
|
|
101
236
|
`.git/worktrees/<name>`; a submodule's names `.git/modules/<name>`, which
|
|
102
237
|
is not a "main root" to climb to and must fall through untouched (mirrors
|
|
103
|
-
`_durabilitylib._gitfile_points_at_worktree`'s same distinction).
|
|
238
|
+
`_durabilitylib._gitfile_points_at_worktree`'s same distinction).
|
|
239
|
+
|
|
240
|
+
The selected Git binary is the authority for accepting and resolving the
|
|
241
|
+
worktree metadata. That keeps native relative pointers aligned with Git,
|
|
242
|
+
rejects incomplete or foreign-dialect metadata Git itself cannot use, and
|
|
243
|
+
avoids translating a path grammar in Python. Only Git-confirmed linked
|
|
244
|
+
worktrees whose common directory is the main checkout's `.git` directory,
|
|
245
|
+
and whose linked and reported-main checkouts are both independently
|
|
246
|
+
arbiter-enabled, can escalate the marker root."""
|
|
104
247
|
git_meta = os.path.join(root, ".git")
|
|
105
248
|
if not os.path.isfile(git_meta):
|
|
106
249
|
return None
|
|
107
250
|
try:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
251
|
+
probe = subprocess.run(
|
|
252
|
+
[git_executable(), "-C", root, "rev-parse", "--path-format=absolute",
|
|
253
|
+
"--git-dir", "--git-common-dir"],
|
|
254
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
255
|
+
timeout=5, env=_root_bound_git_env(),
|
|
256
|
+
)
|
|
257
|
+
except Exception: # noqa: BLE001 - root selection must fail closed
|
|
258
|
+
return None
|
|
259
|
+
lines = [line.strip() for line in probe.stdout.splitlines() if line.strip()]
|
|
260
|
+
if probe.returncode != 0 or len(lines) != 2:
|
|
261
|
+
return None
|
|
262
|
+
git_dir = os.path.normpath(lines[0])
|
|
263
|
+
common_dir = os.path.normpath(lines[1])
|
|
264
|
+
if not os.path.isabs(git_dir) or not os.path.isabs(common_dir):
|
|
265
|
+
return None
|
|
266
|
+
real_git_dir = os.path.normcase(os.path.realpath(git_dir))
|
|
267
|
+
real_common_dir = os.path.normcase(os.path.realpath(common_dir))
|
|
268
|
+
if real_git_dir == real_common_dir:
|
|
269
|
+
return None # ordinary checkout or submodule, not a linked worktree
|
|
270
|
+
expected_admin_parent = os.path.normcase(
|
|
271
|
+
os.path.realpath(os.path.join(common_dir, "worktrees")))
|
|
272
|
+
if os.path.normcase(os.path.realpath(os.path.dirname(git_dir))) != expected_admin_parent:
|
|
273
|
+
return None
|
|
274
|
+
if os.path.basename(common_dir) != ".git" or not os.path.isdir(common_dir):
|
|
275
|
+
return None
|
|
276
|
+
main_root = os.path.dirname(common_dir)
|
|
277
|
+
if not os.path.isdir(main_root):
|
|
278
|
+
return None
|
|
279
|
+
try:
|
|
280
|
+
main_probe = subprocess.run(
|
|
281
|
+
[git_executable(), "-C", main_root, "rev-parse", "--path-format=absolute",
|
|
282
|
+
"--show-toplevel", "--git-common-dir"],
|
|
283
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
284
|
+
timeout=5, env=_root_bound_git_env(),
|
|
285
|
+
)
|
|
286
|
+
except Exception: # noqa: BLE001 - root selection must fail closed
|
|
287
|
+
return None
|
|
288
|
+
main_lines = [line.strip() for line in main_probe.stdout.splitlines() if line.strip()]
|
|
289
|
+
if main_probe.returncode != 0 or len(main_lines) != 2:
|
|
290
|
+
return None
|
|
291
|
+
main_toplevel = os.path.normpath(main_lines[0])
|
|
292
|
+
confirmed_common = os.path.normpath(main_lines[1])
|
|
293
|
+
if not os.path.isabs(main_toplevel) or not os.path.isabs(confirmed_common):
|
|
294
|
+
return None
|
|
295
|
+
if os.path.normcase(os.path.realpath(main_toplevel)) != os.path.normcase(
|
|
296
|
+
os.path.realpath(main_root)):
|
|
111
297
|
return None
|
|
112
|
-
if
|
|
298
|
+
if os.path.normcase(os.path.realpath(confirmed_common)) != real_common_dir:
|
|
113
299
|
return None
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if idx == -1:
|
|
118
|
-
return None # not a linked worktree (e.g. a submodule) — nothing to climb to
|
|
119
|
-
main_git_dir = gitdir[:idx + len("/.git")]
|
|
120
|
-
return os.path.dirname(main_git_dir) or None
|
|
300
|
+
if not _has_enabled_context(root) or not _has_enabled_context(main_root):
|
|
301
|
+
return None
|
|
302
|
+
return main_root.replace("\\", "/") if os.name == "nt" else main_root
|
|
121
303
|
|
|
122
304
|
|
|
123
305
|
class Host:
|
|
@@ -130,6 +312,17 @@ class Host:
|
|
|
130
312
|
"""
|
|
131
313
|
|
|
132
314
|
name = "claude"
|
|
315
|
+
adapter_name = "ca"
|
|
316
|
+
adapter_version = "2.17.1"
|
|
317
|
+
|
|
318
|
+
# Update-notifier descriptor. Each independently versioned host overrides
|
|
319
|
+
# these three values in its per-plugin _host.py. Keeping the target,
|
|
320
|
+
# release prefix, and remediation command together on the active Host
|
|
321
|
+
# prevents the shared notifier from comparing or instructing for a sibling
|
|
322
|
+
# product line (RA-02).
|
|
323
|
+
update_target = "ca"
|
|
324
|
+
update_tag_prefix = "v"
|
|
325
|
+
update_command = "/plugin marketplace update codearbiter"
|
|
133
326
|
|
|
134
327
|
# Capability flags — what surfaces this host actually has. A hook that
|
|
135
328
|
# heals/queries a statusline gates on has_statusline; a hook registered
|
|
@@ -220,14 +413,16 @@ class Host:
|
|
|
220
413
|
return git_worktree_main_root(root) or root
|
|
221
414
|
|
|
222
415
|
def plugin_root(self):
|
|
223
|
-
"""The
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
416
|
+
"""The authenticated payload root derived from this executing module.
|
|
417
|
+
|
|
418
|
+
CLAUDE_PLUGIN_ROOT may corroborate that derived root, but can never
|
|
419
|
+
select a different adapter package.
|
|
420
|
+
"""
|
|
421
|
+
return resolve_plugin_root(
|
|
422
|
+
__file__, adapter_name=self.adapter_name, adapter_version=self.adapter_version,
|
|
423
|
+
manifest_relpath=self.manifest_relpath(), anchor_relpath="hooks/hostapi.py",
|
|
424
|
+
signal_names=("CLAUDE_PLUGIN_ROOT",),
|
|
425
|
+
)
|
|
231
426
|
|
|
232
427
|
def manifest_relpath(self):
|
|
233
428
|
"""The plugin manifest's path, relative to plugin_root() (#263,
|
|
@@ -402,6 +597,9 @@ class FailClosedHost(Host):
|
|
|
402
597
|
silent (observability-002)."""
|
|
403
598
|
|
|
404
599
|
name = "unknown"
|
|
600
|
+
update_target = None
|
|
601
|
+
update_tag_prefix = None
|
|
602
|
+
update_command = None
|
|
405
603
|
has_statusline = False
|
|
406
604
|
has_read_tool = False
|
|
407
605
|
has_prunable_transcript = False
|
|
@@ -754,16 +754,18 @@ def clear_mode_marker(root, host_name=None, session_id=None, now=None):
|
|
|
754
754
|
# "No opinion yet" is the ABSENCE of an entry, not the arbiter VALUE:
|
|
755
755
|
# `current_mode` answers `arbiter` for both "never flipped" and
|
|
756
756
|
# "deliberately flipped back", and only the first may be migrated over.
|
|
757
|
-
#
|
|
758
|
-
#
|
|
759
|
-
#
|
|
757
|
+
# `session_has_entry` is the only reader that keeps that distinction
|
|
758
|
+
# (it also consults the pre-#681 shared map, so a session that chose
|
|
759
|
+
# before the per-session split still counts as having chosen).
|
|
760
|
+
# A corrupt or unreadable entry answers False WITH a diagnostic, which
|
|
761
|
+
# would otherwise read as "no entry" and migrate `dangerous` back on top
|
|
760
762
|
# of a user who had explicitly returned to `arbiter`. Absence is the
|
|
761
763
|
# only clean "nothing to convert over"; anything we could not read is
|
|
762
764
|
# not evidence of anything, and guessing gates-off from unreadable
|
|
763
765
|
# state is the one direction ADR-0030 forbids.
|
|
764
|
-
|
|
766
|
+
has_entry, state_diag = _modelib.session_has_entry(session_id, root=root)
|
|
765
767
|
readable = state_diag in (None, _modelib.MODE_DIAG_ABSENT)
|
|
766
|
-
unconverted = readable and
|
|
768
|
+
unconverted = readable and not has_entry
|
|
767
769
|
if marker_live and unconverted and _modelib.write_mode(session_id, "dangerous", root=root):
|
|
768
770
|
try:
|
|
769
771
|
os.remove(marker)
|
|
@@ -883,7 +885,7 @@ def update_notice_line(plugin):
|
|
|
883
885
|
no network call itself."""
|
|
884
886
|
try:
|
|
885
887
|
state = _updatelib.read_state(_updatelib.state_path())
|
|
886
|
-
latest = state.get("latest")
|
|
888
|
+
latest = _updatelib.target_state(state).get("latest")
|
|
887
889
|
installed = _updatelib.installed_version(plugin)
|
|
888
890
|
return _updatelib.notice_line(installed, latest) or ""
|
|
889
891
|
except Exception: # noqa: BLE001 — never crash session startup
|
|
@@ -431,7 +431,7 @@ def seg_update(plugin=None):
|
|
|
431
431
|
try:
|
|
432
432
|
plugin = plugin if plugin is not None else plugin_root_for_render()
|
|
433
433
|
state = _updatelib.read_state(_updatelib.state_path())
|
|
434
|
-
latest = state.get("latest")
|
|
434
|
+
latest = _updatelib.target_state(state).get("latest")
|
|
435
435
|
installed = _updatelib.installed_version(plugin)
|
|
436
436
|
if not _updatelib.update_available(installed, latest):
|
|
437
437
|
return None
|
|
@@ -68,14 +68,19 @@ ARBITER_SPINNER_VERBS = {
|
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
def plugin_root(opt):
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
# The host seam authenticates the package currently executing this hook.
|
|
72
|
+
# --plugin-root remains test/operator corroboration, never an authority
|
|
73
|
+
# able to select a different executable for settings.json.
|
|
74
|
+
authenticated = os.path.realpath(
|
|
75
|
+
os.path.abspath(_hooklib.get_host().plugin_root()))
|
|
76
|
+
if not opt:
|
|
77
|
+
return authenticated
|
|
78
|
+
requested = os.path.realpath(os.path.abspath(opt))
|
|
79
|
+
if requested != authenticated:
|
|
80
|
+
raise SystemExit(
|
|
81
|
+
"ERROR: --plugin-root must match the authenticated executing "
|
|
82
|
+
f"adapter root ({authenticated}); got {requested}.")
|
|
83
|
+
return authenticated
|
|
79
84
|
|
|
80
85
|
|
|
81
86
|
def settings_path(opt):
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Command-route compatibility
|
|
2
|
+
|
|
3
|
+
A compatibility alias is an installed legacy route with a preferred canonical form. The legacy
|
|
4
|
+
route keeps its argument grammar, confirmation gates, side effects, durable outputs, and host
|
|
5
|
+
availability; its migration notice does not invoke or forward to another host command.
|
|
6
|
+
|
|
7
|
+
The registry permanently declares ca 2.17.0, ca-codex 0.9.0, and ca-pi 0.10.0 as the first-containing
|
|
8
|
+
candidates. Each payload's deprecation clock becomes effective only when GitHub's Release API confirms
|
|
9
|
+
an exact, non-draft Release for that candidate tag and the tag's commit contains both the matching
|
|
10
|
+
registry declaration and matching payload version.
|
|
11
|
+
A tag alone, a draft, unavailable API evidence, or any tag/Release/payload mismatch does not start a
|
|
12
|
+
clock. Published releases ca 2.16.0, ca-codex 0.8.0, and ca-pi 0.9.0 predate this registry and do not
|
|
13
|
+
contain the compatibility metadata. ca retains these routes through every 2.x release, with no removal
|
|
14
|
+
before a separately approved 3.0.0. ca-codex and ca-pi retain them through every later 0.x release,
|
|
15
|
+
with no removal before a separately approved 1.0.0. Passing a version floor never authorizes removal:
|
|
16
|
+
removal needs a new governed decision and fresh compatibility evidence.
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
# Routing table
|
|
2
2
|
|
|
3
3
|
Loaded on a scope-touch or `/command`, not every turn. This table is the authoritative trigger→route
|
|
4
|
-
surface: it answers *what to invoke or route given a trigger
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
surface and destructive-operation registry: it answers *what to invoke or route given a trigger* and
|
|
5
|
+
*which operations always require tier-2 confirmation*; for *what doc to read before touching a scope*,
|
|
6
|
+
use `reference-map.md`. Follow the primary route; the gate is a hard stop, not a suggestion. A command
|
|
7
|
+
is **invoked**; the orchestrator **routes** to a skill; a skill **dispatches** an agent.
|
|
7
8
|
Routing to a skill means loading its body from `<plugin-root>/routines/<name>/SKILL.md` — a route
|
|
8
9
|
cell names the skill; this path convention locates it. That resolution never depends on the host's
|
|
9
10
|
skill registry: a chain-internal skill hidden from the registry (`disable-model-invocation`) is
|
|
10
11
|
reached the same way.
|
|
11
12
|
|
|
13
|
+
## Destructive operations (tier-2 regardless of cue)
|
|
14
|
+
|
|
15
|
+
- Logged bypass (`/override`)
|
|
16
|
+
- Merge to the default branch
|
|
17
|
+
- Branch or worktree deletion
|
|
18
|
+
- Release and tag publication
|
|
19
|
+
|
|
12
20
|
| Invocation cue | Primary route | Also dispatch | Hard gate |
|
|
13
21
|
|---|---|---|---|
|
|
14
22
|
| New feature | `/feature` Step 0 triage → full lane `brainstorming` → `writing-plans` → `executing-plans` → `tdd`, or logged small lane straight to `tdd` | `backend-`/`frontend-`/`infra-author` | No spec, no code; no code before `tdd` Phase 1; small lane only on all triage criteria, logged to `triage.log` |
|
|
@@ -21,8 +29,8 @@ reached the same way.
|
|
|
21
29
|
| Commit | `/commit` → `commit-gate` | — | No commit without all nine gates green |
|
|
22
30
|
| Open a PR / finish a branch | `/pr` → `finishing-a-development-branch` | reviewer fleet per path; PR-body prose applies `anti-slop-design` (`core` + `medium-documents` §7.A.1) | PR only; no direct-to-default, no force-push |
|
|
23
31
|
| Watch a PR's CI / babysit checks | `/watch` → detached `gh pr checks --watch` | on-red diagnose (propose\|branch) | Never auto-merges; green → notify + offer; merge-to-default routes through the hard gate; no poll loop |
|
|
24
|
-
| Code review of the diff | `/review` → `dispatching-parallel-agents` | reviewer fleet → `finding-triage` → `
|
|
25
|
-
| Periodic sweep | `/checkpoint` → `dispatching-parallel-agents` | reviewer fleet → triage → aggregator | Surfaces a triaged report; not a promotion gate |
|
|
32
|
+
| Code review of the diff | `/review` → `dispatching-parallel-agents` | reviewer fleet → `finding-triage` → `verdict-aggregator` | BLOCK on any CRITICAL/HIGH |
|
|
33
|
+
| Periodic sweep | `/checkpoint` → `dispatching-parallel-agents` | reviewer fleet → finding-triage → read-only verdict; then explicit `checkpoint-aggregator` persistence | Surfaces and persists a triaged report; not a promotion gate |
|
|
26
34
|
| Governance record for a window | `/audit` | — | Read-only; never overwrites a packet; audit lines quoted verbatim |
|
|
27
35
|
| Release / version tag | `/release` → `release` skill | `commit-gate` (release commit); CHANGELOG prose applies `anti-slop-design` (`core` §3.A/§3.B) | No tag on a red suite; tag not pushed unbidden |
|
|
28
36
|
| Code uses crypto / hashing / signing / TLS / random | `crypto-compliance` skill | `auth-crypto-reviewer` | BLOCK on any banned primitive |
|
|
@@ -18,7 +18,7 @@ Skill bodies load on routing only. This index is the surface scan; never bulk-re
|
|
|
18
18
|
| [writing-plans](writing-plans/SKILL.md) | `/feature`, `/sprint` (after the spec) | Decomposes an approved spec into small tasks, each with a path + a verification that maps to a `tdd` obligation; writes `plans/<slug>.md` with bijective criterion↔task coverage. |
|
|
19
19
|
| [executing-plans](executing-plans/SKILL.md) | `/feature` | Checkpoint coordinator — groups tasks into batches, delegates each to `subagent-driven-development` (fresh author agent per task, full review chain), stops for user acknowledgement between batches. |
|
|
20
20
|
| [subagent-driven-development](subagent-driven-development/SKILL.md) | `/sprint` (engine), `executing-plans` (batch scope) | Fresh subagent per task → spec-compliance then quality review → fresh-run verification; accepts only on proof. Hard-stops on `tdd` BLOCK, security CRITICAL, `[CONFIRM-NN]`. |
|
|
21
|
-
| [dispatching-parallel-agents](dispatching-parallel-agents/SKILL.md) | `subagent-driven-development`, `/sprint`, parallel `/review` | Reusable fan-out primitive: bound concurrency, collect, dedupe, funnel through `finding-triage`→`
|
|
21
|
+
| [dispatching-parallel-agents](dispatching-parallel-agents/SKILL.md) | `subagent-driven-development`, `/sprint`, parallel `/review` | Reusable fan-out primitive: bound concurrency, collect, dedupe, funnel through `finding-triage`→`verdict-aggregator`. Results unused until the read-only funnel runs. |
|
|
22
22
|
| [finishing-a-development-branch](finishing-a-development-branch/SKILL.md) | `/feature`, `/sprint` (terminal) | The terminal step after `commit-gate`: open-PR / merge-via-PR / discard. No direct-to-main, no force-push; `/sprint` auto-selects open-PR and never merges. |
|
|
23
23
|
| [using-git-worktrees](using-git-worktrees/SKILL.md) | `subagent-driven-development`, `dispatching-parallel-agents` (opt-in) | OPTIONAL per-unit filesystem isolation for parallel work; integrates accepted units back onto the caller's working branch for its single `commit-gate` + finish. Never the default path. |
|
|
24
24
|
| [secret-handling](secret-handling/SKILL.md) | changed code reads/writes/passes a secret | The secret-source gate: identify → source → sinks/persistence. Secrets only from the approved store in `security-controls.md`; never in source, log, error, telemetry, image, or LLM prompt. Dispatches `auth-crypto-reviewer`. |
|
|
@@ -57,9 +57,59 @@ rm -f "$(git rev-parse --show-toplevel)/.codearbiter/.markers/adr-authoring-acti
|
|
|
57
57
|
|
|
58
58
|
Gate: the ADR file is written with a real `decided-by` user attribution, numbered without a gap, and its log entry is appended. An ADR with no user attribution, or authored as the disposition of a finding, does not pass — STOP.
|
|
59
59
|
|
|
60
|
+
### Accepted/Planned binding
|
|
61
|
+
|
|
62
|
+
`accepted` means **Accepted/Planned**. It records the user's governance decision; it does not claim
|
|
63
|
+
that any obligation is Implemented or Verified. When the user explicitly authorizes acceptance:
|
|
64
|
+
|
|
65
|
+
1. Change only the ADR's status fields to accepted. Derive stable, stem-scoped obligations from every
|
|
66
|
+
normative clause in its immutable record, bind each obligation to exact ADR
|
|
67
|
+
text, and obtain independent review that the sealed obligation set is complete.
|
|
68
|
+
2. Route through `commit-gate` to commit the accepted ADR and decision-log append. Do not add the
|
|
69
|
+
acceptance binding to that commit: its `source_commit` cannot truthfully name a commit that does
|
|
70
|
+
not exist yet.
|
|
71
|
+
3. From that exact commit, hash the committed Git blob bytes and the separately canonicalized
|
|
72
|
+
immutable record: strict UTF-8 with LF-normalized line endings, containing the complete ADR while
|
|
73
|
+
replacing only the recognized status value in the strictly parsed frontmatter `status:` field and
|
|
74
|
+
`## Status` section with fixed sentinels. The two values must agree. All remaining Status prose,
|
|
75
|
+
including approval attribution, stays bound alongside title, date, `decided-by`, supersession,
|
|
76
|
+
governed paths, H1, and every other section. Malformed or duplicate frontmatter, status, or
|
|
77
|
+
headings fail closed. Append one
|
|
78
|
+
`acceptance` event to
|
|
79
|
+
`<project-root>/.codearbiter/decisions/adr-lifecycle.jsonl`, then persist that acceptance binding
|
|
80
|
+
in a subsequent commit. The event uses schema `adr-lifecycle/v1` and records `adr` (full stem),
|
|
81
|
+
`recorded_at`, `source_commit`, `blob_sha256`, `body_sha256`, `obligations`,
|
|
82
|
+
`obligations_sha256`, and `obligations_sealed: true`. A second acceptance or baseline binding for
|
|
83
|
+
the same stem is invalid.
|
|
84
|
+
|
|
85
|
+
The lifecycle ledger is append-only. A legacy accepted ADR receives a `baseline` with no fabricated
|
|
86
|
+
acceptance commit, an `observed_commit` whose Git blob is rechecked as the migration snapshot, an
|
|
87
|
+
empty or incrementally mapped obligation list, and
|
|
88
|
+
`obligations_sealed: false`; it remains Accepted/Planned. Later delivery evidence appends records:
|
|
89
|
+
`implemented` binds one declared obligation to a source commit and relevant input digests;
|
|
90
|
+
`verified` additionally binds a unique event ID, explicit proof contract, repository-scoped claim,
|
|
91
|
+
producer, command/workflow identity, timezone-aware observation and expiry times, and the same current
|
|
92
|
+
inputs. Evidence paths and digests are recomputed from the named Git commit, never trusted from the
|
|
93
|
+
caller. A later uniquely identified event may renew expired or changed-input evidence; an append-only
|
|
94
|
+
invalidation event may withdraw a prior evidence event. Only a complete,
|
|
95
|
+
sealed obligation set with current implementation inputs and fresh verification inputs derives
|
|
96
|
+
Implemented or Verified. Changed inputs invalidate the derived state; history is never rewritten.
|
|
97
|
+
|
|
98
|
+
After acceptance, do not edit any bound ADR content. A later user-authorized stored status transition
|
|
99
|
+
may change only the recognized status value in the strictly parsed frontmatter `status:` and
|
|
100
|
+
`## Status`; approval prose remains immutable. Supersession remains a forward reference in the new
|
|
101
|
+
ADR. The acceptance commit retains the exact original blob while the immutable-record digest proves
|
|
102
|
+
every other byte-equivalent field did not change.
|
|
103
|
+
|
|
60
104
|
## Phase 3 — Status (/adr-status) · gate: BLOCK
|
|
61
105
|
|
|
62
|
-
Read-only. For each ADR (or the `--adr N` target), report: stem, title, status,
|
|
106
|
+
Read-only. For each ADR (or the `--adr N` target), report: stem, title, stored governance status,
|
|
107
|
+
derived delivery state, date, and supersession state. Read `adr-lifecycle.jsonl` when present. Display
|
|
108
|
+
stored `accepted` as **Accepted/Planned**. Display Implemented or Verified only when every obligation
|
|
109
|
+
in a sealed binding has the required current, input-bound evidence; otherwise name the narrow reason
|
|
110
|
+
(unsealed, incomplete, stale, expired, or mismatched) and do not promote the ADR. Repository evidence
|
|
111
|
+
never implies live-host, publication, support, legal, or other external truth. Find supersession by
|
|
112
|
+
scanning forward for any later ADR whose `supersedes:` **resolves to** it.
|
|
63
113
|
|
|
64
114
|
Resolve a `supersedes:` value like this, and never guess:
|
|
65
115
|
|
|
@@ -75,7 +125,7 @@ If a supersession candidate contradicts an `accepted` ADR with no clear directio
|
|
|
75
125
|
## ADR Status — YYYY-MM-DD
|
|
76
126
|
|
|
77
127
|
### Active
|
|
78
|
-
- ADR-NNNN-<slug> — <title> — <status> (<date>)
|
|
128
|
+
- ADR-NNNN-<slug> — <title> — governance: <status>; delivery: <Accepted/Planned | Implemented | Verified> (<date>)
|
|
79
129
|
|
|
80
130
|
### Superseded
|
|
81
131
|
- ADR-NNNN-<slug> — <title> — superseded by ADR-MMMM-<slug>
|
|
@@ -97,6 +147,8 @@ Gate: every indexed ADR appears with its current status and supersession state;
|
|
|
97
147
|
- MUST NOT record a decision the user did not explicitly make. "Use your best judgment," "I trust you" are declined.
|
|
98
148
|
- MUST NOT resolve a `[CONFIRM-NN]` placeholder by guessing. Surface it and stop.
|
|
99
149
|
- MUST NOT advance an ADR's status without explicit user instruction.
|
|
150
|
+
- MUST NOT report accepted as Implemented or Verified without complete, sealed, current lifecycle evidence.
|
|
151
|
+
- MUST NOT rewrite or truncate a committed `adr-lifecycle.jsonl`, create a second binding, or fabricate legacy acceptance evidence.
|
|
100
152
|
- MUST NOT edit a prior ADR or a prior decision-log entry to add a back-reference — supersession is a forward-only chain; append a new record whose `supersedes:` names the prior one.
|
|
101
153
|
- **The never-edit rule protects decision CONTENT, not identifiers.** Rewriting what was decided corrupts the record; disambiguating *which document a pointer names* repairs it. Maintainer ruling, 2026-07-25: *"the never edit rule is meant to prevent this situation, not prevent this situation from being fixed."* So a correction that is provably identifier-only — a `supersedes:` value changed from a number to the stem it already meant — is permissible, and nothing else about the file is. Any such correction MUST be a single-line diff that alters not one word of any decision, MUST be visible in its own commit, and still requires the maintainer-armed `adr-authoring-active` marker. MUST NOT touch Context, Decision, Alternatives, Consequences, Risks, `status:`, `date:`, `decided-by:`, or `title:` under this allowance.
|
|
102
154
|
- MUST NOT number an ADR with a gap, and MUST NOT reuse a number an existing stem already holds — a shared number makes every bare reference to it ambiguous.
|
|
@@ -55,8 +55,16 @@ governs: <optional, comma-separated path globs this decision constrains — e.g.
|
|
|
55
55
|
mirrors it for human readers. Keep the two in agreement.
|
|
56
56
|
- **Status lifecycle:** `proposed → accepted → superseded | rejected`. `decompose` authors Layer 4
|
|
57
57
|
ADRs as **`status: draft`** during the interview and promotes each to `status: accepted` at its
|
|
58
|
-
Phase 5
|
|
58
|
+
Phase 5 in one sanctioned status edit that changes both the frontmatter `status:` field and the `## Status` value,
|
|
59
|
+
without changing any other body content. Status transitions otherwise
|
|
59
60
|
require explicit user instruction; never advance status on the skill's own judgment.
|
|
61
|
+
- **`accepted` means Accepted/Planned.** It records an approved governance decision and does not imply
|
|
62
|
+
implementation. Implemented and Verified are derived delivery states from the separate append-only
|
|
63
|
+
`adr-lifecycle.jsonl`; they are never written into ADR frontmatter. After acceptance, the decision
|
|
64
|
+
record is immutable except for the recognized, agreeing status value in the strictly parsed
|
|
65
|
+
frontmatter `status:` field and `## Status` section. Approval attribution and all other Status prose
|
|
66
|
+
remain bound with title, date, `decided-by`, supersession, governed paths, H1, and every other
|
|
67
|
+
section. A later explicit status transition changes only those recognized status values.
|
|
60
68
|
- **`decided-by:`** names the user who made the decision — real attribution, never inferred.
|
|
61
69
|
- **`supersedes:`** names the prior ADR's full filename stem — `supersedes:
|
|
62
70
|
0014-githook-shim-dropin-fail-closed`, not `supersedes: 0014` — or `none`. A bare number is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dispatching-parallel-agents
|
|
3
|
-
description: "The parallel fan-out primitive. Routed to by any skill or command that splits work across independent units and dispatches an agent per unit — subagent-driven-development, /sprint, parallel /review. It owns the dispatch/collect/funnel discipline: bound concurrency, isolate units, collect every result, dedupe overlap, and funnel through finding-triage then
|
|
3
|
+
description: "The parallel fan-out primitive. Routed to by any skill or command that splits work across independent units and dispatches an agent per unit — subagent-driven-development, /sprint, parallel /review. It owns the dispatch/collect/funnel discipline: bound concurrency, isolate units, collect every result, dedupe overlap, and funnel through finding-triage then the read-only verdict-aggregator. Raw agent output is never consumed before the funnel runs; an agent that errors records its unit as incomplete without corrupting the batch."
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -59,8 +59,8 @@ Gate: the result set is deduped, contradictions surfaced, and completion claims
|
|
|
59
59
|
|
|
60
60
|
The batch is consumed only here, through the fixed funnel — never directly by the caller.
|
|
61
61
|
|
|
62
|
-
1. Dispatch `finding-triage` (`<plugin-root>/agents/finding-triage.md`) over the deduped result set: it classifies severity
|
|
63
|
-
2. Hand the triaged set to `
|
|
62
|
+
1. Dispatch `finding-triage` (`<plugin-root>/agents/finding-triage.md`) over the deduped result set and batch completion contract: it classifies severity and marks out-of-scope items with an inline `[NEEDS-TRIAGE]` marker. Every reviewer finding reaches the verdict; only non-finding transport metadata may be omitted.
|
|
63
|
+
2. Hand the triaged set to `verdict-aggregator` (`<plugin-root>/agents/verdict-aggregator.md`): it composes the single read-only batch verdict the caller consumes — `PASS`, `BLOCKING_FINDINGS`, or `INCOMPLETE`.
|
|
64
64
|
|
|
65
65
|
The errored and deferred units from Phase 3 ride through the funnel as findings — an `ERRORED` unit is a finding the caller must see, not a silent gap.
|
|
66
66
|
|
|
@@ -71,6 +71,6 @@ Gate: the caller receives only the aggregated verdict. Bypassing the funnel —
|
|
|
71
71
|
- MUST NOT dispatch two units that mutate the same path in one batch; isolate via `using-git-worktrees` or serialize.
|
|
72
72
|
- MUST NOT fan out unbounded; dispatch within the concurrency bound.
|
|
73
73
|
- MUST NOT let one `ERRORED` unit discard or corrupt the rest of the batch.
|
|
74
|
-
- MUST NOT consume agent output before the `finding-triage` → `
|
|
74
|
+
- MUST NOT consume agent output before the `finding-triage` → `verdict-aggregator` funnel runs.
|
|
75
75
|
- MUST NOT trust a subagent's self-reported completion; verify with a fresh proving command.
|
|
76
76
|
- MUST NOT silently drop a unit — every unit terminates with a recorded state that rides through the funnel.
|
|
@@ -269,7 +269,7 @@ The tag and the GitHub Release publish together, and only after the user explici
|
|
|
269
269
|
On authorization:
|
|
270
270
|
|
|
271
271
|
1. Push the tag: `git push origin ${TAG_PREFIX}${VERSION}`.
|
|
272
|
-
2. **Resolve `<Phase-1 section file>` fresh in every Phase-3 invocation — never assume Phase 1's scratch file survived** (HIGH, blind exercise run 19). That file was created with `mktemp` outside the working tree and discarded once Phase 3 no longer needed it; on the `resume_publish` path (tag composed in a prior invocation, published now) it is normally already gone, and there was previously no stated way to get it back that did not read as the "re-derive or hand-write" the hard rules forbid. There is one sanctioned, mechanical way: `"$PY" "<plugin-root>/hooks/_releaselib.py" changelog-section $CHANGELOG $
|
|
272
|
+
2. **Resolve `<Phase-1 section file>` fresh in every Phase-3 invocation — never assume Phase 1's scratch file survived** (HIGH, blind exercise run 19). That file was created with `mktemp` outside the working tree and discarded once Phase 3 no longer needed it; on the `resume_publish` path (tag composed in a prior invocation, published now) it is normally already gone, and there was previously no stated way to get it back that did not read as the "re-derive or hand-write" the hard rules forbid. There is one sanctioned, mechanical way: `"$PY" "<plugin-root>/hooks/_releaselib.py" changelog-section "<project-root>" "${TAG_PREFIX}${VERSION}" "$CHANGELOG" "$VERSION"` prints the `## [${VERSION}] …` section back out of the exact regular-file blob committed under the already-composed tag — guaranteed present, because Phase 1 step 7 committed it before the tag was created. The project root and every value are separate quoted arguments; the helper rejects a nested or unrelated root, an absolute or escaping changelog path, a non-regular Git tree entry, a malformed heading, `Unreleased` in a released position, and duplicate target sections. It resolves the tag to a commit hash before reading the blob, so a dirty/deleted working file, a changed current `HEAD`, or an unrelated current directory cannot substitute Release notes after the tag is composed. Redirect stdout to a fresh local file and use that as `<Phase-1 section file>` for every step below; this is reading the exact text back from its one permanent home, not composing new notes. Exit 1 (no heading for `${VERSION}`) means the committed changelog and tag version disagree; exit 3 means the root/revision/path/blob binding could not be proven; exit 4 means the changelog is malformed or ambiguous. Every one STOPs for investigation; never compose a substitute section by hand. On a same-session fresh publish the Phase 1 scratch file is still there and this reconstruction is redundant but harmless — run it anyway, so Phase 3 does not need to know which case it is in. **Guard the notes-file first:** assert its first heading matches the tag — `"$PY" "<plugin-root>/hooks/_releaselib.py" notes-match "${TAG_PREFIX}${VERSION}" <Phase-1 section file>` (exit 0). A stale notes-file (`notes_heading_matches` False) would publish the wrong changelog section under the right tag — STOP on mismatch. Then create the GitHub Release from the **same changelog section composed in Phase 1** — reuse it as the notes, never re-derive or hand-write them. **`--latest` follows the declared row:** assert it only when `$TARGET`'s row declares `latest-eligible: true`, and only when this tag is also the newest release across every declared series (compare against `gh release list`; vacuously satisfied when only one series is declared); every other target passes `--latest=false`. GitHub has one repo-wide "Latest"; a declared file may name several series, so a target claiming it wrongly hides another's current release from every visitor. `gh release create "${TAG_PREFIX}${VERSION}" --title "<title>" --notes-file <Phase-1 section file> --latest --verify-tag` when the row qualifies per the rule above, otherwise `gh release create "${TAG_PREFIX}${VERSION}" --title "<title>" --notes-file <Phase-1 section file> --latest=false --verify-tag` — two distinct, individually runnable commands, never the bracket notation `--latest[=false]`, which is prose shorthand and not shell `gh` accepts. The title convention is `<$DISPLAY_NAME> ${VERSION}: <summary>` — `$DISPLAY_NAME` is the row's declared `display-name`, or `$TARGET` itself when the row declares none — with no em-dash separator. **`<summary>` is derived, not invented** (MEDIUM, adversarial review 2026-07-31, run 3: it appeared exactly once in this file and was never defined, so it was whatever the agent made up): take the single highest-precedence entry from the Phase 1 section — the first bullet under `### Added` if the window bumped minor, otherwise the first bullet under `### Fixed`, else the first bullet of the first non-empty group — and compress it to a noun phrase under ten words, in the entry's own words. If that yields nothing usable because the section has one group with one terse bullet, use that bullet verbatim. Never write a summary that names a change absent from the section.
|
|
273
273
|
3. Handle edge cases explicitly, never silently: if a Release for the tag already exists, report it and skip creation (the tag push may already have landed); if `gh` is missing, unauthenticated, or the call fails, STOP and print the exact `gh release create` command so publication can be finished by hand rather than left half-done.
|
|
274
274
|
4. **Verify publication — never assume it.** Read the Release back: `gh release view ${TAG_PREFIX}${VERSION} --json url,isDraft,tagName`. STOP unless it returns a **non-draft** Release on the correct tag; `gh release create` can partially succeed (tag pushed, Release rejected for an empty notes-file or a permissions/`--verify-tag` race), and an unverified publish is not a published release. Report the Release URL only once the read-back confirms it.
|
|
275
275
|
5. **Record the tag's provenance, when the row declares one.** A git tag is a mutable ref, and the commit a tag was *originally* published at is not recoverable from the API once it moves — a moved tag looks exactly like a tag that was always there. So, when `$TARGET`'s row declares `$PROVENANCE_MANIFEST`, write it down: add the new tag to that file under `tags`, as `{"object_sha": <the ref's sha>, "object_type": "tag", "commit_sha": <the commit it dereferences to>}`. Read both mechanically from the remote you just pushed to, never from local state: `git ls-remote --tags origin ${TAG_PREFIX}${VERSION}` for the ref sha and `git rev-parse ${TAG_PREFIX}${VERSION}^{commit}` for the commit. If this project runs an automated tag-drift check in CI against that file, an unrecorded tag is an unguarded tag there. **A row declaring no `$PROVENANCE_MANIFEST` skips this step — say so explicitly in the report** rather than silently doing nothing (a skipped step and a forgotten one must never look the same to the person reading the report). The entry rides in a normal commit through `commit-gate` on the release branch.
|
|
@@ -23,8 +23,8 @@ Periodic sweep of the entire codebase with the reviewer fleet, funneled to a sin
|
|
|
23
23
|
| `architecture-drift-reviewer` | `decisions/`; drift between code and accepted ADRs |
|
|
24
24
|
|
|
25
25
|
2. Route to `dispatching-parallel-agents` (`<plugin-root>/routines/dispatching-parallel-agents/SKILL.md`) with that unit list (read-only batch). It dedupes, then
|
|
26
|
-
funnels through `finding-triage` → `
|
|
27
|
-
3. `checkpoint-aggregator` writes the dated report to
|
|
26
|
+
funnels through `finding-triage` → `verdict-aggregator` and returns the single read-only verdict.
|
|
27
|
+
3. After the verdict returns, separately dispatch `checkpoint-aggregator` with that verdict. It writes the dated report to
|
|
28
28
|
`<project-root>/.codearbiter/checkpoints/YYYY-MM-DD.md`: findings by severity with
|
|
29
29
|
file:line, and out-of-scope items marked inline `[NEEDS-TRIAGE]`.
|
|
30
30
|
4. Write the current override **count** to `<project-root>/.codearbiter/last-checkpoint` — the
|
|
@@ -38,8 +38,9 @@ Periodic sweep of the entire codebase with the reviewer fleet, funneled to a sin
|
|
|
38
38
|
## Hard gate
|
|
39
39
|
|
|
40
40
|
Read-only except writing the checkpoint doc and `last-checkpoint` — MUST NOT modify code. MUST NOT
|
|
41
|
-
consume raw reviewer output — only the `finding-triage` → `
|
|
42
|
-
|
|
41
|
+
consume raw reviewer output — only the `finding-triage` → `verdict-aggregator` verdict. Checkpoint
|
|
42
|
+
persistence MUST remain the separate `checkpoint-aggregator` step and MUST NOT run for `/ca-review`
|
|
43
|
+
or another generic parallel batch. MUST NOT resolve a `[CONFIRM-NN]` surfaced during the sweep by guessing. The report surfaces findings; it does
|
|
43
44
|
not block or sign off anything.
|
|
44
45
|
|
|
45
46
|
## When NOT to use
|