@arbiterforge/ca-pi 0.14.7 → 0.14.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugins/ca-pi/CHANGELOG.md +38 -0
- package/plugins/ca-pi/SPRINT.md +45 -5
- package/plugins/ca-pi/arbiter.md +39 -51
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_releaselib.py +274 -12
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/releasehash.py +17 -4
- package/plugins/ca-pi/includes/redirect.md +32 -63
- package/plugins/ca-pi/includes/routing-table.md +7 -5
- package/plugins/ca-pi/includes/safety-core.md +8 -4
- package/plugins/ca-pi/package.json +1 -1
- package/plugins/ca-pi/routines/INDEX.md +1 -1
- package/plugins/ca-pi/routines/release/SKILL.md +89 -26
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +19 -7
|
@@ -736,6 +736,110 @@ def last_tag_select_for_policy(tags, prefix, policy="semver", initial_version=No
|
|
|
736
736
|
return best[1] if best else NONE_SENTINEL
|
|
737
737
|
|
|
738
738
|
|
|
739
|
+
_ANCESTRY_ZERO_TAG = "zero-tag"
|
|
740
|
+
_ANCESTRY_ANCESTOR = "ancestor"
|
|
741
|
+
_ANCESTRY_NOT_ANCESTOR = "not-ancestor"
|
|
742
|
+
_ANCESTRY_UNRESOLVABLE = "unresolvable"
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def verify_tag_ancestor(tag, project_root, candidate="HEAD"):
|
|
746
|
+
"""Classify whether `tag` resolves to a commit reachable from `candidate`
|
|
747
|
+
(issue #570, finding BODY-03).
|
|
748
|
+
|
|
749
|
+
`last_tag_select`/`last_tag_select_for_policy` choose a series' highest
|
|
750
|
+
SemVer (or `numeric-sequence`) tag by VALUE alone -- pure functions with
|
|
751
|
+
no git access, per this module's design invariants -- so neither can
|
|
752
|
+
know whether that tag's commit is even in `candidate`'s own commit-graph
|
|
753
|
+
history. A tag pushed once from a sibling branch that diverged from
|
|
754
|
+
`candidate` before the newest tag, an abandoned branch's stray push, or
|
|
755
|
+
an unrelated orphan history that happens to carry a same-prefixed tag
|
|
756
|
+
can still win the highest-SemVer scan even though `candidate` never
|
|
757
|
+
descends from it -- silently anchoring `$BASE_VERSION` (and everything
|
|
758
|
+
derived from it) to history this release does not actually contain.
|
|
759
|
+
|
|
760
|
+
Returns one of four labels, never raises:
|
|
761
|
+
`"zero-tag"` -- `tag` is `NONE_SENTINEL`. There is nothing to
|
|
762
|
+
verify; the zero-tag first-release path is a
|
|
763
|
+
DISTINCT code path from tag selection and stays
|
|
764
|
+
completely unaffected by this check (P5).
|
|
765
|
+
`"ancestor"` -- `git merge-base --is-ancestor <tag> <candidate>`
|
|
766
|
+
resolved both revisions and confirmed reachability
|
|
767
|
+
-- today's behavior for every genuine ancestor,
|
|
768
|
+
unchanged, including one reached only through a
|
|
769
|
+
merge commit and both annotated and lightweight
|
|
770
|
+
tag forms (git peels either transparently).
|
|
771
|
+
`"not-ancestor"` -- both revisions resolved, but `tag` is NOT an
|
|
772
|
+
ancestor of `candidate` -- the exact gap this
|
|
773
|
+
function exists to catch (a sibling/decoy/orphan
|
|
774
|
+
tag that numerically outranks every real release).
|
|
775
|
+
`"unresolvable"` -- `tag` or `candidate` does not resolve to a commit
|
|
776
|
+
at all, the probe could not run (`OSError`), or it
|
|
777
|
+
exceeded this module's standard `timeout=30`
|
|
778
|
+
(#627 precedent) -- distinct from `"not-ancestor"`
|
|
779
|
+
because no verdict about reachability was actually
|
|
780
|
+
reached.
|
|
781
|
+
|
|
782
|
+
Bounded with `timeout=30`, matching this module's other internal
|
|
783
|
+
Git-probe call sites. This function only classifies; it never chooses a
|
|
784
|
+
different (older) tag, never implements maintenance-branch allocation,
|
|
785
|
+
and never mutates anything -- refusal is the caller's job (P5).
|
|
786
|
+
|
|
787
|
+
Both `tag` and `candidate` are validated with the same
|
|
788
|
+
`startswith("-")` / `"\\0" in` predicate `_committed_changelog_text`
|
|
789
|
+
already applies to a revision reaching this module's own argv, before
|
|
790
|
+
either reaches `git` as a bare argv element -- a name beginning with
|
|
791
|
+
`-` could otherwise be parsed as an option by the downstream `git
|
|
792
|
+
merge-base` subcommand. An explicit end-of-options `--` is also placed
|
|
793
|
+
ahead of the two revisions as defense-in-depth, matching this module's
|
|
794
|
+
existing `--` precedent elsewhere.
|
|
795
|
+
|
|
796
|
+
**Caller contract -- repo-root binding (R-02, Scope-C security review
|
|
797
|
+
finding 2, MEDIUM).** `project_root` is trusted verbatim; this function
|
|
798
|
+
does not independently discover a project directory. Whatever OTHER
|
|
799
|
+
command a caller used to SELECT `tag` (this module's own
|
|
800
|
+
`last_tag_select`/`last_tag_select_for_policy`, or SKILL.md's `git tag
|
|
801
|
+
-l`) MUST resolve against the IDENTICAL root passed here -- never
|
|
802
|
+
merely "whatever the invoking shell's cwd happens to be" -- or a tag
|
|
803
|
+
selected from one checkout's history can be verified against a
|
|
804
|
+
completely different checkout's `HEAD`. A linked worktree, or any
|
|
805
|
+
other checkout whose actual shell cwd diverges from the harness-
|
|
806
|
+
designated project root, is a real, constructible instance of this,
|
|
807
|
+
not a hypothetical one. `main`'s `verify-tag-ancestor` CLI dispatch
|
|
808
|
+
keeps its legacy 1-argument env-first-else-cwd resolution
|
|
809
|
+
(`CLAUDE_PROJECT_DIR` first, falling back to `os.getcwd()` -- the
|
|
810
|
+
identical precedence `default_targets_path`/`default_backfill_root`
|
|
811
|
+
apply) for backward compatibility only; SKILL.md's Pre-flight
|
|
812
|
+
sequence instead resolves ONE `$PROJECT_ROOT` value up front and
|
|
813
|
+
threads it explicitly -- both as the optional SECOND CLI argument
|
|
814
|
+
here (so this function's own `project_root` is never re-derived by a
|
|
815
|
+
second, independent environment read) and via `-C` on its own bare
|
|
816
|
+
`git tag -l`/`git fetch --tags origin`/`git fetch origin
|
|
817
|
+
"$DEFAULT_BRANCH"`/`git branch --show-current` calls -- so every
|
|
818
|
+
consumer of "the project root" in that sequence reads the SAME
|
|
819
|
+
resolved value, never two reads that merely usually agree."""
|
|
820
|
+
if tag == NONE_SENTINEL:
|
|
821
|
+
return _ANCESTRY_ZERO_TAG
|
|
822
|
+
if (not isinstance(tag, str) or not tag
|
|
823
|
+
or tag.startswith("-") or "\0" in tag):
|
|
824
|
+
return _ANCESTRY_UNRESOLVABLE
|
|
825
|
+
if (not isinstance(candidate, str) or not candidate
|
|
826
|
+
or candidate.startswith("-") or "\0" in candidate):
|
|
827
|
+
return _ANCESTRY_UNRESOLVABLE
|
|
828
|
+
try:
|
|
829
|
+
probe = subprocess.run(
|
|
830
|
+
[git_executable(), "merge-base", "--is-ancestor", "--",
|
|
831
|
+
tag, candidate],
|
|
832
|
+
cwd=project_root, capture_output=True, text=True, timeout=30,
|
|
833
|
+
env=_sanitized_git_environment())
|
|
834
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
835
|
+
return _ANCESTRY_UNRESOLVABLE
|
|
836
|
+
if probe.returncode == 0:
|
|
837
|
+
return _ANCESTRY_ANCESTOR
|
|
838
|
+
if probe.returncode == 1:
|
|
839
|
+
return _ANCESTRY_NOT_ANCESTOR
|
|
840
|
+
return _ANCESTRY_UNRESOLVABLE
|
|
841
|
+
|
|
842
|
+
|
|
739
843
|
def notes_heading_matches(notes_text, tag, policy="semver", initial_version=None):
|
|
740
844
|
"""True iff the FIRST changelog heading names `tag` under `policy`.
|
|
741
845
|
|
|
@@ -1389,6 +1493,14 @@ def classify_window(commits, reconciliations=None, published_shas=None):
|
|
|
1389
1493
|
list. ``reconciliations`` may supply exact-SHA changelog text only for
|
|
1390
1494
|
SHAs independently present in ``published_shas``. It never changes the
|
|
1391
1495
|
commit classification or bump.
|
|
1496
|
+
|
|
1497
|
+
Each row in `commits` carries `classify_commit`'s full per-commit
|
|
1498
|
+
verdict -- including `breaking` -- unchanged and in the input list's
|
|
1499
|
+
own order (never reordered or recomputed here). SKILL.md's Phase 1
|
|
1500
|
+
step 5 changelog composition and its `<summary>` headline rule rely on
|
|
1501
|
+
exactly that: reusing `breaking` to route a commit into its own
|
|
1502
|
+
`### Breaking` group, and scanning `commits` in this same order to find
|
|
1503
|
+
the first breaking entry for a major-bump title.
|
|
1392
1504
|
"""
|
|
1393
1505
|
rows = []
|
|
1394
1506
|
reconciliations = reconciliations if isinstance(reconciliations, dict) else {}
|
|
@@ -2004,16 +2116,32 @@ def governance_scratch_exclusions(row):
|
|
|
2004
2116
|
|
|
2005
2117
|
|
|
2006
2118
|
def release_tree_status(row, project_root):
|
|
2007
|
-
"""Run the target-aware repository-wide clean-tree probe.
|
|
2119
|
+
"""Run the target-aware repository-wide clean-tree probe.
|
|
2120
|
+
|
|
2121
|
+
Bounded with `timeout=30`, matching every other internal Git-probe call
|
|
2122
|
+
site in this module (#627 Finding 1). A hang (index lock, network-mounted
|
|
2123
|
+
repo, credential-helper prompt) must not hang the release lane forever
|
|
2124
|
+
with no diagnostic: `subprocess.TimeoutExpired` is caught and converted
|
|
2125
|
+
into a synthetic failed `CompletedProcess`, which every existing caller
|
|
2126
|
+
already treats as a probe failure (`clean-tree-status` returns
|
|
2127
|
+
`probe.returncode` directly; `run-pre-tag`'s `_tree_state()` treats a
|
|
2128
|
+
nonzero returncode as "the probe itself failed", never as a detected
|
|
2129
|
+
mutation) — no caller-side change is required.
|
|
2130
|
+
"""
|
|
2008
2131
|
scratch_pathspecs = [
|
|
2009
2132
|
f":(exclude,top){path}"
|
|
2010
2133
|
for path in governance_scratch_exclusions(row)
|
|
2011
2134
|
]
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2135
|
+
args = [git_executable(), "status", "--porcelain", "--", ":/",
|
|
2136
|
+
*scratch_pathspecs]
|
|
2137
|
+
try:
|
|
2138
|
+
return subprocess.run(
|
|
2139
|
+
args, capture_output=True, text=True, cwd=project_root,
|
|
2140
|
+
timeout=30, env=_sanitized_git_environment())
|
|
2141
|
+
except subprocess.TimeoutExpired:
|
|
2142
|
+
return subprocess.CompletedProcess(
|
|
2143
|
+
args, 1, "",
|
|
2144
|
+
"git status probe timed out after 30 seconds")
|
|
2017
2145
|
|
|
2018
2146
|
|
|
2019
2147
|
def parse_release_targets(text):
|
|
@@ -2305,8 +2433,9 @@ def detect_candidate_target(manifest_candidates, changelog_candidates,
|
|
|
2305
2433
|
(`scan_backfill_candidates` is the one filesystem reader, kept separate
|
|
2306
2434
|
per this module's read-isolation convention). Returns a row dict shaped
|
|
2307
2435
|
like one `load_targets` entry (`target`, `prefix`, `manifest`,
|
|
2308
|
-
`changelog`, `payload`, `payload_exclude`,
|
|
2309
|
-
|
|
2436
|
+
`changelog`, `changelog_reconciliations`, `payload`, `payload_exclude`,
|
|
2437
|
+
`latest_eligible`) ONLY when exactly one manifest candidate and exactly
|
|
2438
|
+
one changelog candidate were found. Raises
|
|
2310
2439
|
`BackfillAmbiguousError` for every other case — zero or multiple of
|
|
2311
2440
|
either — naming which side was ambiguous and what was found, so a caller
|
|
2312
2441
|
surfacing the error has something concrete to show the user.
|
|
@@ -2356,6 +2485,13 @@ def detect_candidate_target(manifest_candidates, changelog_candidates,
|
|
|
2356
2485
|
"prefix": prefix,
|
|
2357
2486
|
"manifest": [manifest_candidates[0]],
|
|
2358
2487
|
"changelog": changelog_candidates[0],
|
|
2488
|
+
# A first release may have real, already-published history from
|
|
2489
|
+
# before CHANGELOG footers were required. Back-fill must declare the
|
|
2490
|
+
# existing exact-SHA reconciliation mechanism up front so that the
|
|
2491
|
+
# adoption commit can land the operator-authored ledger on the
|
|
2492
|
+
# default branch before the release lane widens its first window.
|
|
2493
|
+
"changelog_reconciliations":
|
|
2494
|
+
".codearbiter/release-changelog-reconciliations.json",
|
|
2359
2495
|
"payload": ".",
|
|
2360
2496
|
# Back-fill is the release-only adoption lane. Its own hooks create
|
|
2361
2497
|
# governance scratch under .codearbiter/, so a root payload without
|
|
@@ -2374,7 +2510,8 @@ def format_release_targets_block(row):
|
|
|
2374
2510
|
back-fill lane persists, so a caller never hand-assembles the delimiter
|
|
2375
2511
|
grammar itself.
|
|
2376
2512
|
|
|
2377
|
-
Emits a `
|
|
2513
|
+
Emits a `changelog-reconciliations` line when `row` declares one, and a
|
|
2514
|
+
`latest-eligible` line when `row` declares one (HIGH-2,
|
|
2378
2515
|
adversarial review 2026-07-31) — `detect_candidate_target` always does,
|
|
2379
2516
|
since it can only ever propose a single-target row — rendered as the
|
|
2380
2517
|
grammar's own `true`/`false` literal, never a bare Python truthiness
|
|
@@ -2385,6 +2522,10 @@ def format_release_targets_block(row):
|
|
|
2385
2522
|
for manifest in row.get("manifest", []):
|
|
2386
2523
|
lines.append(f"manifest: {manifest}")
|
|
2387
2524
|
lines.append(f"changelog: {row['changelog']}")
|
|
2525
|
+
if row.get("changelog_reconciliations") is not None:
|
|
2526
|
+
lines.append(
|
|
2527
|
+
"changelog-reconciliations: "
|
|
2528
|
+
f"{row['changelog_reconciliations']}")
|
|
2388
2529
|
lines.append(f"payload: {row['payload']}")
|
|
2389
2530
|
for excluded in row.get("payload_exclude", []):
|
|
2390
2531
|
lines.append(f"payload-exclude: {excluded}")
|
|
@@ -2577,6 +2718,45 @@ def main(argv):
|
|
|
2577
2718
|
more than one).
|
|
2578
2719
|
last-tag <prefix> stdin = tags (whitespace/newline separated)
|
|
2579
2720
|
-> prints the selected tag or <none>.
|
|
2721
|
+
last-tag-for-policy <prefix> <policy> <initial_version>
|
|
2722
|
+
stdin = tags -> prints the selected tag or
|
|
2723
|
+
<none>, under a declared version policy
|
|
2724
|
+
(`semver` or `numeric-sequence`).
|
|
2725
|
+
verify-tag-ancestor <tag> [<project_root>]
|
|
2726
|
+
refuses an ambiguous release baseline
|
|
2727
|
+
before it is used for anything (#570
|
|
2728
|
+
finding BODY-03). `last-tag`/`last-tag-
|
|
2729
|
+
for-policy` select the highest tag by
|
|
2730
|
+
VALUE alone, with no git access; this
|
|
2731
|
+
confirms the SELECTED tag actually
|
|
2732
|
+
resolves to a commit reachable from HEAD.
|
|
2733
|
+
`<project_root>`, when given, is used
|
|
2734
|
+
VERBATIM and no environment read happens
|
|
2735
|
+
at all -- the caller's own single already-
|
|
2736
|
+
resolved root, threaded through explicitly
|
|
2737
|
+
rather than re-derived here a second time
|
|
2738
|
+
(R-02, Scope-C security review finding 2,
|
|
2739
|
+
MEDIUM: two independent env-first-else-cwd
|
|
2740
|
+
reads, taken at two different times, are
|
|
2741
|
+
"usually agree", never a guarantee).
|
|
2742
|
+
Omitted, this falls back to the prior
|
|
2743
|
+
1-argument behavior -- `CLAUDE_PROJECT_DIR`
|
|
2744
|
+
first, else this process's own cwd --
|
|
2745
|
+
unchanged, for backward compatibility.
|
|
2746
|
+
exit 0 -- `<tag>` is `<none>` (the
|
|
2747
|
+
zero-tag first-release path, unaffected)
|
|
2748
|
+
or a confirmed ancestor of HEAD -- 1 --
|
|
2749
|
+
`<tag>` resolves but is NOT an ancestor of
|
|
2750
|
+
HEAD, printing the tag and the cause; the
|
|
2751
|
+
caller MUST stop, never silently fall
|
|
2752
|
+
back to an older tag, reuse the tag
|
|
2753
|
+
namespace, or move/delete the tag -- 2 --
|
|
2754
|
+
`<tag>` or HEAD could not be resolved at
|
|
2755
|
+
all (an unresolvable ref, a probe error,
|
|
2756
|
+
or a `timeout=30` expiry). Never chooses a
|
|
2757
|
+
different baseline and never implements
|
|
2758
|
+
maintenance-branch allocation policy; it
|
|
2759
|
+
only detects and refuses ambiguity (P5).
|
|
2580
2760
|
notes-match <tag> <notes_file>
|
|
2581
2761
|
exit 0 iff the notes file's first heading
|
|
2582
2762
|
names the same version as `tag`.
|
|
@@ -2711,7 +2891,14 @@ def main(argv):
|
|
|
2711
2891
|
prints the exact `release-targets.md`
|
|
2712
2892
|
block text and exits 0; on zero or multiple
|
|
2713
2893
|
of either, writes the ambiguity to stderr
|
|
2714
|
-
|
|
2894
|
+
and exits 1 — it never prints a guess.
|
|
2895
|
+
validate-reconciliations <target> <ledger-file>
|
|
2896
|
+
validates a Back-fill ledger with the same
|
|
2897
|
+
strict parser the published release path
|
|
2898
|
+
uses. exit 0 valid - 4 missing, unreadable,
|
|
2899
|
+
non-UTF-8, oversized, or malformed. It does
|
|
2900
|
+
not claim the file is published; the later
|
|
2901
|
+
`classify-window` ancestry proof owns that.
|
|
2715
2902
|
show-row <target> [--field NAME]
|
|
2716
2903
|
prints every field the row declares, one
|
|
2717
2904
|
`name: value` line each (multi-valued
|
|
@@ -2751,12 +2938,13 @@ def main(argv):
|
|
|
2751
2938
|
if not argv:
|
|
2752
2939
|
sys.stderr.write(
|
|
2753
2940
|
"usage: _releaselib.py {tag-prefix|list-targets|show-row|"
|
|
2754
|
-
"payload-pathspec|clean-tree-status|last-tag|last-tag-for-policy|
|
|
2941
|
+
"payload-pathspec|clean-tree-status|last-tag|last-tag-for-policy|"
|
|
2942
|
+
"verify-tag-ancestor|notes-match|"
|
|
2755
2943
|
"changelog-section|dates-match|semver-greater|version-greater|"
|
|
2756
2944
|
"apply-bump|derive-version|render-release-assets|"
|
|
2757
2945
|
"verify-release-assets|classify|peel-tag|"
|
|
2758
2946
|
"run-pre-tag|adoption-commit|classify-window|check-manifests|"
|
|
2759
|
-
"backfill-detect} ...\n")
|
|
2947
|
+
"backfill-detect|validate-reconciliations} ...\n")
|
|
2760
2948
|
return 2
|
|
2761
2949
|
|
|
2762
2950
|
cmd, rest = argv[0], list(argv[1:])
|
|
@@ -2954,6 +3142,53 @@ def main(argv):
|
|
|
2954
3142
|
sys.stdin.read().split(), prefix, policy, initial))
|
|
2955
3143
|
return 0
|
|
2956
3144
|
|
|
3145
|
+
if cmd == "verify-tag-ancestor" and len(rest) in (1, 2):
|
|
3146
|
+
tag = rest[0]
|
|
3147
|
+
# R-02 (Scope-C security review finding 2, MEDIUM): an explicit
|
|
3148
|
+
# SECOND argument, when given, is the caller's own already-resolved
|
|
3149
|
+
# project root and wins outright -- no environment read happens at
|
|
3150
|
+
# all. Without this, this dispatch's own env-first-else-cwd
|
|
3151
|
+
# resolution and SKILL.md's shell-side `$PROJECT_ROOT` resolution
|
|
3152
|
+
# are two INDEPENDENT reads of "the project root", taken at two
|
|
3153
|
+
# different times (the shell's at bullet-time, this process's at
|
|
3154
|
+
# invocation-time) -- they agree only when `CLAUDE_PROJECT_DIR` is
|
|
3155
|
+
# set (trusted-harness input under Claude Code) and neither call's
|
|
3156
|
+
# own cwd has moved since. On a host with no such env var at all
|
|
3157
|
+
# (Codex, Pi) or in any degraded case, both legs independently fall
|
|
3158
|
+
# back to `os.getcwd()`/`$(pwd)`, which is "usually agree", not a
|
|
3159
|
+
# guarantee. Passing the value explicitly collapses this to ONE
|
|
3160
|
+
# resolution, computed once, consumed everywhere -- the 1-argument
|
|
3161
|
+
# form is kept for backward compatibility (any other caller of this
|
|
3162
|
+
# CLI, and this module's own test suite) and still resolves exactly
|
|
3163
|
+
# as before.
|
|
3164
|
+
if len(rest) == 2:
|
|
3165
|
+
project_root = rest[1]
|
|
3166
|
+
else:
|
|
3167
|
+
project_root = os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()
|
|
3168
|
+
verdict = verify_tag_ancestor(tag, project_root)
|
|
3169
|
+
if verdict in (_ANCESTRY_ZERO_TAG, _ANCESTRY_ANCESTOR):
|
|
3170
|
+
return 0
|
|
3171
|
+
if verdict == _ANCESTRY_NOT_ANCESTOR:
|
|
3172
|
+
sys.stderr.write(
|
|
3173
|
+
f"verify-tag-ancestor: refusing {tag!r} as the release "
|
|
3174
|
+
"baseline -- it does not resolve to a commit reachable "
|
|
3175
|
+
"from HEAD. A sibling branch that diverged before this "
|
|
3176
|
+
"tag, an abandoned branch's stray push, or an unrelated "
|
|
3177
|
+
"orphan history sharing this series' prefix can still win "
|
|
3178
|
+
"the highest-SemVer scan even though this release's own "
|
|
3179
|
+
"history never contains it. This is never resolved by "
|
|
3180
|
+
"silently falling back to an older tag, reusing the tag "
|
|
3181
|
+
"namespace, or moving/deleting the tag -- remove the "
|
|
3182
|
+
"stray tag by hand (never retarget or delete a PUBLISHED "
|
|
3183
|
+
"one; see \"Recovering from a bad release\") or confirm "
|
|
3184
|
+
"the correct baseline before re-entering Pre-flight.\n")
|
|
3185
|
+
return 1
|
|
3186
|
+
sys.stderr.write(
|
|
3187
|
+
f"verify-tag-ancestor: could not resolve {tag!r} or HEAD to "
|
|
3188
|
+
"verify ancestry -- refusing rather than trusting an "
|
|
3189
|
+
"unverified baseline.\n")
|
|
3190
|
+
return 2
|
|
3191
|
+
|
|
2957
3192
|
if cmd == "notes-match" and len(rest) in (2, 4):
|
|
2958
3193
|
tag, notes_path = rest[:2]
|
|
2959
3194
|
policy, initial = (rest[2], rest[3] or None) if len(rest) == 4 else (
|
|
@@ -3718,6 +3953,33 @@ def main(argv):
|
|
|
3718
3953
|
sys.stdout.write(format_release_targets_block(row))
|
|
3719
3954
|
return 0
|
|
3720
3955
|
|
|
3956
|
+
if cmd == "validate-reconciliations" and len(rest) == 2:
|
|
3957
|
+
target, ledger_path = rest
|
|
3958
|
+
try:
|
|
3959
|
+
if os.path.islink(ledger_path) or not os.path.isfile(ledger_path):
|
|
3960
|
+
raise ChangelogReconciliationError(
|
|
3961
|
+
"ledger must be one regular file")
|
|
3962
|
+
if os.path.getsize(ledger_path) > 1024 * 1024:
|
|
3963
|
+
raise ChangelogReconciliationError(
|
|
3964
|
+
"ledger exceeds the 1 MiB limit")
|
|
3965
|
+
with open(ledger_path, "rb") as handle:
|
|
3966
|
+
raw = handle.read(1024 * 1024 + 1)
|
|
3967
|
+
if len(raw) > 1024 * 1024:
|
|
3968
|
+
raise ChangelogReconciliationError(
|
|
3969
|
+
"ledger exceeds the 1 MiB limit")
|
|
3970
|
+
try:
|
|
3971
|
+
text = raw.decode("utf-8")
|
|
3972
|
+
except UnicodeError as exc:
|
|
3973
|
+
raise ChangelogReconciliationError(
|
|
3974
|
+
f"ledger is not UTF-8: {exc}") from None
|
|
3975
|
+
parse_changelog_reconciliations(text, target)
|
|
3976
|
+
except (OSError, ChangelogReconciliationError) as exc:
|
|
3977
|
+
sys.stderr.write(
|
|
3978
|
+
f"ChangelogReconciliationError: could not validate "
|
|
3979
|
+
f"{ledger_path!r}: {exc}\n")
|
|
3980
|
+
return 4
|
|
3981
|
+
return 0
|
|
3982
|
+
|
|
3721
3983
|
sys.stderr.write(f"_releaselib.py: bad invocation: {' '.join(argv)}\n")
|
|
3722
3984
|
return 2
|
|
3723
3985
|
|
|
@@ -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.9"
|
|
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,
|
|
@@ -40,11 +40,24 @@
|
|
|
40
40
|
#
|
|
41
41
|
# CLI:
|
|
42
42
|
# releasehash.py digest <target> print the executable-command digest
|
|
43
|
-
# releasehash.py check <target>
|
|
44
|
-
# confirmed, 3/4 declared-file states
|
|
43
|
+
# releasehash.py check <target> print the confirmation state
|
|
45
44
|
# releasehash.py record <target> mint the confirmation (the sanctioned
|
|
46
45
|
# producer; run only after an operator has
|
|
47
46
|
# actually READ the commands)
|
|
47
|
+
#
|
|
48
|
+
# Exit codes (T-05, spec P3 / AC-04, AC-05 -- disambiguated from a single
|
|
49
|
+
# overloaded 2 that used to mean three unrelated things):
|
|
50
|
+
# 64 malformed CLI usage (wrong arg count, or an unrecognised subcommand)
|
|
51
|
+
# -- never reaches target resolution, writes no marker
|
|
52
|
+
# 65 unknown release target (no declared row of that name) -- writes no
|
|
53
|
+
# marker
|
|
54
|
+
# 3/4 the declared-targets file itself could not be read/parsed for
|
|
55
|
+
# `<target>`; see `_releaselib._targets_error_exit_code` (3 genuinely
|
|
56
|
+
# absent, 4 every other declared-file error) -- unchanged by this split
|
|
57
|
+
# 0/1/2 `check`'s three valid-check outcomes, unchanged by this split:
|
|
58
|
+
# 0 confirmed or no-commands, 1 changed since last confirmation,
|
|
59
|
+
# 2 never confirmed -- NOW 2's sole remaining meaning
|
|
60
|
+
# 0 `digest`/`record` succeed once the target and declared file resolve
|
|
48
61
|
|
|
49
62
|
from __future__ import annotations
|
|
50
63
|
|
|
@@ -182,7 +195,7 @@ def main(argv=None):
|
|
|
182
195
|
argv = list(sys.argv[1:] if argv is None else argv)
|
|
183
196
|
if len(argv) != 2 or argv[0] not in ("digest", "check", "record"):
|
|
184
197
|
sys.stderr.write("usage: releasehash.py {digest|check|record} <target>\n")
|
|
185
|
-
return
|
|
198
|
+
return 64
|
|
186
199
|
command, target = argv
|
|
187
200
|
root = os.path.dirname(os.path.dirname(_releaselib.default_targets_path()))
|
|
188
201
|
|
|
@@ -193,7 +206,7 @@ def main(argv=None):
|
|
|
193
206
|
return _releaselib._targets_error_exit_code(error)
|
|
194
207
|
if executable_inputs is None:
|
|
195
208
|
sys.stderr.write(f"unknown release target: {target}\n")
|
|
196
|
-
return
|
|
209
|
+
return 65
|
|
197
210
|
commands, release_build = executable_inputs
|
|
198
211
|
|
|
199
212
|
if command == "digest":
|
|
@@ -1,80 +1,49 @@
|
|
|
1
1
|
# Redirect
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Clarification support for §6 **tiers 2 and 3**, loaded only when the intended outcome, a material
|
|
4
|
+
fact, or required authority is unresolved. Tier 1 does not use this file: select the existing
|
|
5
|
+
owner and preserve its gates without asking the user to retype or approve an internal route.
|
|
5
6
|
|
|
6
|
-
Tier
|
|
7
|
-
its command with a one-line statement of the route (ADR-0022): naming the command and then asking the
|
|
8
|
-
user to type it back is ceremony, not governance.
|
|
7
|
+
## Tier 2 — probable intent, or a required destructive confirmation
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
`/ca-reconcile`. A question pulled into the heavy spec lane is a misroute.
|
|
9
|
+
Inspect available context before asking. A second plausible implementation method is not a missing
|
|
10
|
+
user decision. Ask about the missing outcome or fact, with a recommendation and its strongest
|
|
11
|
+
counter-consideration; do not ask the user to select a command that expresses an understood request.
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
For irreversible or gate-bypassing operations, obtain the existing confirmation even when intent
|
|
14
|
+
is clear: `/ca-override`, merge to the default branch, branch or worktree deletion, and release
|
|
15
|
+
and tag publication. Name the exact operation and target, state the consequence, then ask for its
|
|
16
|
+
confirmation. A generic approval of a route is not authority for an unspecified destructive action.
|
|
17
|
+
A deterministic mode-token flip is not in this set; its existing mode contract still applies.
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
## Tier 3 — genuinely unclear outcome
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
That reads as <inferred intent> → <$ca- skill, prefilled with the user's own words>
|
|
21
|
+
Ask one focused question after checking the available evidence:
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
> The missing decision is <outcome or constraint>. I recommend <option> because <reason>;
|
|
24
|
+
> <strongest counter-consideration> favors <alternative>. Which outcome is intended?
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
friction, not a gate, which is why `mode --dangerous`/`mode --ops` entry is not in this set — see
|
|
29
|
-
`includes/ops-mode.md` and `includes/dangerous-mode.md`.)
|
|
26
|
+
When even the intended task is unknown, ask what result the user wants. Do not repeat a command
|
|
27
|
+
catalog when the user restates the goal in ordinary language. Resolve the actual uncertainty and
|
|
28
|
+
then select the owner; lack of slash syntax is not itself uncertainty.
|
|
30
29
|
|
|
31
|
-
##
|
|
30
|
+
## Questions and restricted requests
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Or pick a channel:
|
|
40
|
-
→ Start a new project: /ca-decompose
|
|
41
|
-
→ Start a feature: /ca-feature "describe it"
|
|
42
|
-
→ Ask a question: /ca-btw "your question"
|
|
43
|
-
→ Fix a bug: /ca-fix "describe it"
|
|
44
|
-
→ Bypass with audit trail: /ca-override "reason"
|
|
45
|
-
→ See everything open: /ca-status
|
|
46
|
-
→ See all commands: /ca-commands
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
When no intent is inferable at all, drop the candidate lines and lead with the channel list.
|
|
50
|
-
|
|
51
|
-
## Repeat — user insists off-channel after tier 3
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
Still need a channel for this one. Closest matches first:
|
|
55
|
-
<up to three prefilled $ca- skills>
|
|
56
|
-
|
|
57
|
-
Full list:
|
|
58
|
-
/ca-decompose /ca-create-context /ca-feature /ca-sprint /ca-fix /ca-refactor /ca-debug /ca-chore /ca-spike
|
|
59
|
-
/ca-commit /ca-pr /ca-watch /ca-review /ca-checkpoint /ca-release /ca-add-dep
|
|
60
|
-
/ca-threat-model /ca-adr /ca-adr-status /ca-reconcile /ca-conflict
|
|
61
|
-
/ca-init /ca-status /ca-metrics /ca-audit /ca-preview /ca-doctor /ca-standup /ca-cleanup /ca-task
|
|
62
|
-
/ca-new-skill /ca-btw /ca-commands
|
|
63
|
-
Or /ca-override "reason" to proceed anyway with an audit entry.
|
|
64
|
-
```
|
|
32
|
+
Use the requested outcome, not punctuation alone. "Explain this commit" and "draft the message,
|
|
33
|
+
but do not commit" authorize no staging, commit, task-board write, or other persistent mutation.
|
|
34
|
+
"Can you commit these changes?" can request an action; it still needs its owning gate. Answer an
|
|
35
|
+
informational question directly using read-only evidence as needed. If a relevant procedure has a
|
|
36
|
+
writing terminal step, do not enter that step for a report-only request.
|
|
65
37
|
|
|
66
38
|
## Never
|
|
67
39
|
|
|
68
|
-
A missing owner is a **routing gap**, not an override case.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
`/ca-chore`, which does not accept it, and then to `/ca-override`, which exists to be rare.
|
|
40
|
+
A genuinely missing owner is a **routing gap**, not an override case. Surface the precise missing
|
|
41
|
+
capability; never steer the user toward `/ca-override` to get past a coverage hole. Do not invent
|
|
42
|
+
an ungoverned procedure, resolve a `[CONFIRM-NN]`, or broaden scope merely to avoid asking.
|
|
72
43
|
|
|
73
44
|
## Exception — local runtime work
|
|
74
45
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
see `includes/ops-mode.md`. Anything that mutates tracked files, the index, git history, or
|
|
80
|
-
published state is still a routing gap, `ops` or not.
|
|
46
|
+
The existing `mode --ops` contract in `<plugin-root>/includes/ops-mode.md` owns local runtime
|
|
47
|
+
work that leaves tracked files and Git history unchanged. In `arbiter` mode, a runtime operation without an owner remains a routing gap;
|
|
48
|
+
this clarification card does not select or activate another mode. Tracked-file, index, Git-history,
|
|
49
|
+
and published-state mutations still require their ordinary governing workflow, `ops` or not.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# Routing table
|
|
2
2
|
|
|
3
|
-
Loaded
|
|
4
|
-
surface and destructive-operation registry
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Loaded when a scope-touch or route needs it, not every turn. This table is the authoritative
|
|
4
|
+
intent-to-owner surface and destructive-operation registry. Invocation cues are examples, not
|
|
5
|
+
required user syntax. For the governing document on a scope-touch, use `reference-map.md`.
|
|
6
|
+
Select the existing owner and preserve its prerequisites, modes, and gates; a direct resource
|
|
7
|
+
reference does not discard behavior owned by its command wrapper. A gate blocks advancement until
|
|
8
|
+
its exit condition is satisfied; routing never authorizes a bypass. Informational requests use
|
|
9
|
+
read-only behavior, not a writing close. The orchestrator routes; a skill dispatches an agent.
|
|
8
10
|
Routing to a skill means loading its body from `<plugin-root>/routines/<name>/SKILL.md` — a route
|
|
9
11
|
cell names the skill; this path convention locates it. That resolution never depends on the host's
|
|
10
12
|
skill registry: a chain-internal skill hidden from the registry (`disable-model-invocation`) is
|
|
@@ -4,7 +4,8 @@ dangerous, ops) at injection time; never injected alone. -->
|
|
|
4
4
|
# Safety core
|
|
5
5
|
|
|
6
6
|
**Precedence.** This file is prepended to every mode body and binds over it: no mode body may
|
|
7
|
-
weaken, omit, or override a clause stated here.
|
|
7
|
+
weaken, omit, or override a clause stated here. Command names identify their governing procedure;
|
|
8
|
+
natural-language routing preserves that procedure and its attribution and confirmation requirements.
|
|
8
9
|
|
|
9
10
|
**Section numbers are a public interface.** The enforcement hooks fire identically in every
|
|
10
11
|
mode and their block messages cite a `§N` from this file directly, so the numbering below must
|
|
@@ -59,9 +60,12 @@ preflight to the exact fetched base and PR head. Preserve a required true merge
|
|
|
59
60
|
`--match-head-commit`; green CI cannot substitute for source ancestry. Missing evidence blocks the
|
|
60
61
|
offer. This check never authorizes the merge itself.
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
Within already-authorized scope, choose reversible implementation parameters among reasonable
|
|
64
|
+
approaches and record material choices where the workflow requires. Multiple reasonable methods
|
|
65
|
+
alone do not require a user decision. This does not grant initial scope approval, resolve a
|
|
66
|
+
`[CONFIRM-NN]`, invent a product requirement, or expand provider, spending, data-disclosure, or
|
|
67
|
+
publication authority. When an unresolved choice changes the authorized outcome or requires
|
|
68
|
+
additional authority, ask. The irreversible-action confirmations above remain mandatory.
|
|
65
69
|
|
|
66
70
|
---
|
|
67
71
|
|
|
@@ -17,7 +17,7 @@ Skill bodies load on routing only. This index is the surface scan; never bulk-re
|
|
|
17
17
|
| [brainstorming](brainstorming/SKILL.md) | `/feature` (front), `/sprint` planning | Socratic idea→spec: split bundles, shape the approach, then decide parameters / ask forks in full; adversarial self-review before approval; writes new full-lane `specs/<slug>.html` through the installed engine while preserving existing Markdown authority. Its acceptance criteria become `tdd` obligations. Hard-gate: no code until the spec is approved. |
|
|
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 a same-format plan with bijective criterion↔task coverage (`.html` by default, `.md` for an existing Markdown spec). |
|
|
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
|
-
| [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.
|
|
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. A quality failure blocks acceptance; approved sprint recovery owns correction. Security CRITICAL and unresolved `[CONFIRM-NN]` remain stops. |
|
|
21
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. |
|