diff --git a/.codearbiter/gate-events.log b/.codearbiter/gate-events.log index 5e51005b..fa543968 100644 --- a/.codearbiter/gate-events.log +++ b/.codearbiter/gate-events.log @@ -1110,3 +1110,12 @@ Claude-Session: https://claude.ai/code/session_015ZDVU1BzUqnnPVHbZ397bX') stages [2026-07-28T22:28:54Z] BLOCK [H-09b] host=claude hook=pre-bash.py | This commit introduces crypto/TLS changes, but no security-gate pass is recorded (.codearbiter/.markers/security-gate-passed). Run the crypto-compliance gate (it records the pass), then commit. To bypass a security gate, /override requires its heavier security-acknowledgement path. [2026-07-28T22:32:50Z] REMIND [H-09] host=claude hook=post-write-edit.py | Crypto/TLS pattern detected. Run the crypto-compliance check + dispatch auth-crypto-reviewer (no MD5/SHA1/DES/3DES/RC2/RC4/Blowfish; do not disable TLS verification). The commit will block until the gate records a pass. [2026-07-28T22:33:11Z] BLOCK [H-09b] host=claude hook=pre-bash.py | This commit introduces crypto/TLS changes, but no security-gate pass is recorded (.codearbiter/.markers/security-gate-passed). Run the crypto-compliance gate (it records the pass), then commit. To bypass a security gate, /override requires its heavier security-acknowledgement path. +[2026-07-29T00:50:44Z] REMIND [H-12] host=claude hook=post-write-edit.py | core/surface/includes/maturity-coverage.md is governed by ADR-0011-multi-host-codex-plugin-shared-core (Multi-host support — third sibling plugin ca-codex via shared core + thin host adapters). If this change contradicts it, route to /ca:reconcile or /ca:adr — do not drift silently. +[2026-07-29T00:50:50Z] REMIND [H-12] host=claude hook=post-write-edit.py | core/surface/skills/tdd/SKILL.md is governed by ADR-0011-multi-host-codex-plugin-shared-core (Multi-host support — third sibling plugin ca-codex via shared core + thin host adapters). If this change contradicts it, route to /ca:reconcile or /ca:adr — do not drift silently. +[2026-07-29T00:51:04Z] REMIND [H-12] host=claude hook=post-write-edit.py | .codearbiter/tech-stack.md is governed by ADR-0001-hybrid-adr-living-docs-governance (Adopt a hybrid ADR + living-docs governance model). If this change contradicts it, route to /ca:reconcile or /ca:adr — do not drift silently. +[2026-07-29T00:51:14Z] REMIND [H-12] host=claude hook=post-write-edit.py | .codearbiter/tech-stack.md is governed by ADR-0001-hybrid-adr-living-docs-governance (Adopt a hybrid ADR + living-docs governance model). If this change contradicts it, route to /ca:reconcile or /ca:adr — do not drift silently. +[2026-07-29T00:52:13Z] REMIND [H-12] host=claude hook=post-write-edit.py | .github/workflows/ci.yml is governed by ADR-0014-pi-host-authentication-and-fail-closed-tool-boundary (Treat Pi host authentication as opaque runtime state and fail closed on unknown tools). If this change contradicts it, route to /ca:reconcile or /ca:adr — do not drift silently. +[2026-07-29T00:52:13Z] REMIND [H-15] host=claude hook=post-write-edit.py | CI/CD workflow changed. Dispatch security-reviewer before merging (it reviews workflow/secrets/permissions exposure). Advisory — not a commit block. +[2026-07-29T00:54:38Z] REMIND [H-12] host=claude hook=post-write-edit.py | .codearbiter/tech-stack.md is governed by ADR-0001-hybrid-adr-living-docs-governance (Adopt a hybrid ADR + living-docs governance model). If this change contradicts it, route to /ca:reconcile or /ca:adr — do not drift silently. +[2026-07-29T01:20:32Z] REMIND [H-12] host=claude hook=post-write-edit.py | .github/workflows/ci.yml is governed by ADR-0014-pi-host-authentication-and-fail-closed-tool-boundary (Treat Pi host authentication as opaque runtime state and fail closed on unknown tools). If this change contradicts it, route to /ca:reconcile or /ca:adr — do not drift silently. +[2026-07-29T01:20:32Z] REMIND [H-15] host=claude hook=post-write-edit.py | CI/CD workflow changed. Dispatch security-reviewer before merging (it reviews workflow/secrets/permissions exposure). Advisory — not a commit block. diff --git a/.codearbiter/tech-stack.md b/.codearbiter/tech-stack.md index 1334c788..70feb38d 100644 --- a/.codearbiter/tech-stack.md +++ b/.codearbiter/tech-stack.md @@ -153,8 +153,43 @@ npm --prefix plugins/ca-sandbox/tools run coverage Each prints a text summary and writes an html report to that tree's `coverage/` (gitignored — run output, never project state). Scope, provider and reporters -live in each tree's `vitest.config.ts`; the script takes no arguments so it is -identical on every platform. +live in each tree's `vitest.config.ts`. + +**The COMMAND is identical on every platform. The REPORT is not** (issue #521). +The script takes no arguments, so nothing about the invocation varies — but code +behind a platform fork cannot execute off its own platform, and a single-host +report scores the other platform's arm as uncovered no matter how well tested it +is. Measured here: `plugins/ca/tools` reads 66.18% branches on Windows and +65.35% on Linux, and `exec.ts` alone reads 87.50% against 76.38% — an 11-point +gap that is entirely `awaitTaskkill` plus the win32 `treeKill` arm on one side +and the POSIX arm on the other. + +**A quoted figure is the union across a tree's supported hosts**, per tree: + +| tree | forks on platform | union in CI | +| --- | --- | --- | +| `plugins/ca/tools` | yes — `exec.ts` on `process.platform` | **ubuntu + windows**, merged | +| `plugins/ca-pi/tools` | yes — Windows supervisor / process-tree paths | not yet — tracked in #537 | +| `plugins/ca-sandbox/tools` | no | single-host (ubuntu); docker caveat below | +| `site/` | no | single-host (ubuntu) | + +CI produces the union for `plugins/ca/tools` in +`[CHECK] | [REPO] | Coverage union` — one advisory matrix cell per host writing a +vitest **blob** report, merged with `vitest --merge-reports`. Merging is a +genuine union of executed code, not the last report winning: verified on two +disjoint suites, 7 + 65 branches merging to 72 and 18 + 69 lines to 87. The job +says so when only one host reported, so a partial figure is never mistaken for +the union. + +`ca-pi/tools` is platform-forked and therefore *in scope for the rule*, but its +union job is not built yet (#537). Until it is, quote its figures with the host +named — the rule above applies to how you REPORT a number regardless of whether +CI computes it for you. + +Locally, one host's report is a legitimate figure — **name the host** and say the +other's contribution is missing. The rule and its conditions live in +`plugins/ca/includes/maturity-coverage.md`; this table is the per-tree half it +refers to. **The threshold is not encoded in the tooling.** `tdd` Phase 5 and `refactor` Phase 2/6 apply it, reading `stage:` from `.codearbiter/CONTEXT.md` against @@ -163,13 +198,21 @@ clear it**; a report satisfying one and not the other does not pass. Putting the number in three `vitest.config.ts` files would fork that single source of truth and the copies would drift the first time the stage moves. -Measured baseline at stage 2 (≥ 70%), 2026-07-26: - -| tree | lines | branches | verdict | -| --- | --- | --- | --- | -| `plugins/ca/tools` | 67.31% | 59.46% | **below floor** — backfill tracked in #511 | -| `plugins/ca-pi/tools` | 85.37% | 78.73% | clears | -| `plugins/ca-sandbox/tools` | 86.13% | 79.96% | clears | +Measured baseline at stage 2 (≥ 70%), 2026-07-26, **windows-latest only** — taken +before the union rule existed, and left labelled rather than restated so the +figures stay comparable to what #511 was driven against: + +| tree | lines | branches | verdict | host | +| --- | --- | --- | --- | --- | +| `plugins/ca/tools` | 67.31% | 59.46% | **below floor** — backfill tracked in #511 | windows | +| `plugins/ca-pi/tools` | 85.37% | 78.73% | clears | windows | +| `plugins/ca-sandbox/tools` | 86.13% | 79.96% | clears | windows | + +#511's closing measurement was 72.03% lines / 66.18% branches on Windows against +70.93% / 65.35% on Linux — the divergence that produced #521. Under the union +rule the authoritative figure for that tree is the merged one, which the CI job +above now publishes; the single-host numbers above are superseded as a basis for +a threshold decision the next time this table is refreshed. Two caveats when reading a local report: diff --git a/.github/scripts/test_ci_impact.py b/.github/scripts/test_ci_impact.py index dbe5ffee..eee0368a 100644 --- a/.github/scripts/test_ci_impact.py +++ b/.github/scripts/test_ci_impact.py @@ -1021,10 +1021,23 @@ def test_host_independent_pi_job_is_registered_in_both_needs_and_required_result self.assertIn( "ca-pi-checks", required, "ci-passed.required_results is missing ca-pi-checks" ) + # The sanctioned advisory set. Each entry is awaited so the run settles + # and a hang is visible, but never enforced: + # ca-pi-latest - the upstream Pi canary (#381). + # coverage-union* - #521 / DECISION-0031. The coverage GATE is + # orchestrator-run (`tdd` Phase 5); these jobs + # PRODUCE the figure it quotes and gate nothing. + # Enforcing them would let a Windows runner flake + # (#535 is live) block a merge on a number no + # check consumes - the precise trade the decision + # rejected when it weighed Available. + # Adding to this list is a deliberate contract change, not a fix for a + # red build: anything here can fail silently forever. self.assertEqual( sorted(set(needs) - set(required)), - ["ca-pi-latest"], - "awaited but unenforced jobs (only the advisory Pi canary may appear here)", + ["ca-pi-latest", "coverage-union", "coverage-union-merge"], + "awaited but unenforced jobs (only the advisory Pi canary and the " + "#521 coverage-union jobs may appear here)", ) self.assertEqual( sorted(set(required) - set(needs)), @@ -1196,6 +1209,45 @@ def test_the_version_gates_ask_payload_scope_not_the_whole_plugin_directory(self guard = (REPO_ROOT / "tools" / "build-host-packages.py").read_text(encoding="utf-8") self.assertIn('":(exclude)plugins/ca-pi/tools"', guard) + def test_the_coverage_union_actually_merges_more_than_one_host(self): + """Issue #521 / DECISION-0031. + + The union is the whole point: a single-host report scores the other + platform's arm as permanently uncovered. A job that quietly lost its + second host, or stopped merging, would still be green and still print a + coverage number — just the wrong one, under a name claiming otherwise. + """ + ci = CI_WORKFLOW.read_text(encoding="utf-8") + jobs = workflow_jobs(ci) + self.assertIn("coverage-union", jobs) + self.assertIn("coverage-union-merge", jobs) + + cells = jobs["coverage-union"] + for host in ("ubuntu-latest", "windows-latest"): + self.assertIn(host, cells, f"the coverage union no longer measures on {host}") + # A blob is what `--merge-reports` can recombine; a text summary is not. + self.assertIn("--reporter=blob", cells, "the union cells stopped emitting mergeable reports") + self.assertIn("--coverage", cells) + + merge = jobs["coverage-union-merge"] + self.assertIn("--merge-reports", merge, "the union job stopped merging") + self.assertIn("coverage-blob-ca-*", merge, "the merge job no longer collects the host blobs") + + def test_the_coverage_union_is_advisory_not_a_required_check(self): + """DECISION-0031 weighed Available and made this explicitly non-blocking. + + The coverage gate is orchestrator-run; these jobs only produce a figure. + Enforcing them would let a Windows runner flake block a merge on a + number no check consumes. + """ + ci = CI_WORKFLOW.read_text(encoding="utf-8") + required = aggregate_required_results(ci) + for job in ("coverage-union", "coverage-union-merge"): + self.assertNotIn( + job, required, + f"{job} became a required check - see DECISION-0031; it gates nothing", + ) + def test_every_declared_shipped_artifact_has_a_staleness_gate(self): """Issues #377 / #407: a committed bundle that nothing rebuild-checks rots. diff --git a/.github/scripts/test_pi_package.py b/.github/scripts/test_pi_package.py index 7b88f9f6..8ebcf7e1 100644 --- a/.github/scripts/test_pi_package.py +++ b/.github/scripts/test_pi_package.py @@ -1240,11 +1240,23 @@ def test_ci_has_pi_path_output_matrix_and_independent_version_guard(self): "every upstream-compatibility probe must absorb its own failure at step level", ) - latest_receipt_dropped = ci.replace( + # Scoped to the ca-pi-latest job body, NOT the whole file. `if: always()` + # is a generic line, and a bare first-occurrence replace mutates whichever + # job happens to sit earliest in ci.yml - so this assertion silently + # stopped testing the Pi receipt the moment an unrelated job above it grew + # one (issue #521's coverage-union upload, which needs `always()` so a + # FAILING host still publishes its blob). The mutation must hit the thing + # the assertion is about, or it proves nothing about it. + latest_start = ci.index(" ca-pi-latest:\n") + latest_receipt_dropped = ci[:latest_start] + ci[latest_start:].replace( " if: always()\n", " if: success()\n", 1, ) + self.assertNotEqual( + latest_receipt_dropped, ci, + "the ca-pi-latest receipt no longer carries an `if: always()` to drop", + ) self.assertTrue( pi_ci_contract_violations(latest_receipt_dropped), "the advisory receipt must publish even when a probe fails", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 148aa29b..247fad6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -303,6 +303,128 @@ jobs: # the untagged-but-already-shipped case that produced the issue. run: python .github/scripts/test_payload_version_gate.py + coverage-union: + # Issue #521 / DECISION-0031. The coverage GATE is orchestrator-run (`tdd` + # Phase 5), not a required check - this job exists to PRODUCE the number + # that gate quotes, on every host the tree actually forks across. + # + # A single-host report scores the other platform's arm as uncovered no + # matter how well tested it is: exec.ts's `awaitTaskkill` and win32 + # `treeKill` cannot execute on Linux, and the POSIX arm cannot execute on + # Windows - an 11-point gap on that file alone. Neither number is wrong; + # both are incomplete. + # + # ADVISORY BY DESIGN. `ci-passed` awaits it so a hang stays visible, but + # never enforces it: a Windows runner flake (#535 is live) must not block a + # merge on a figure no check consumes. That is the trade DECISION-0031 made + # when it weighed Available, and test_ci_impact.py pins it. + # + # KNOWN RED on windows-latest, tracked in #539. This tree had never run on + # Windows in CI - the `tools` job is ubuntu-only - and the first run here + # surfaced 15 failures, all downstream of one lexical containment check that + # refuses a legitimate worktree root when the 8.3 short path and the long + # path spell the same directory differently. The failure is real, it predates + # this job, and the cell is advisory so it blocks nothing. It goes green when + # #539 lands; do NOT silence it with continue-on-error in the meantime. + # + # Scoped to plugins/ca/tools. ca-pi/tools is the other platform-forked tree + # per tech-stack.md and is NOT covered here - deliberately, and tracked: + # this job installs a graph, and the audited-graph contract is satisfied by + # the sibling job that audits the SAME literal directory, which requires the + # working directory to be literal rather than matrix-templated. + name: "Coverage union " + needs: changes + if: needs.changes.outputs.ca == 'true' + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + defaults: + run: + working-directory: plugins/ca/tools + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 20 + cache: npm + cache-dependency-path: plugins/ca/tools/package-lock.json + - name: Install (locked) + # --ignore-scripts for the same reason every sibling job uses it: this + # graph's output is a committed artifact. The graph itself is audited by + # the `tools` job, which runs under the identical `ca` path filter - the + # derived audited-graph rule in test_ci_impact.py checks exactly that. + run: npm ci --ignore-scripts + - name: Coverage (blob) + # A BLOB report is what `--merge-reports` recombines, and vitest + # recomputes coverage across blobs rather than letting the last one win. + # Verified on two disjoint suites - 7 + 65 branches merge to 72, 18 + 69 + # lines to 87. + # + # `default` is requested ALONGSIDE it because blob REPLACES the console + # reporter: the first run of this job failed on Windows and the log + # showed only "exit code 1" with no summary, no failing test name and no + # assertion. A cell that cannot say what broke is not worth having. + run: npx vitest run --coverage --reporter=blob --reporter=default --outputFile.blob=.vitest-reports/${{ matrix.os }}.json + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + # always(): a FAILING host's blob is the one most worth having. Without + # this the upload is skipped exactly when the run needs diagnosing, and + # the merge job silently reports a one-host figure as though the other + # host had nothing to add. + if: always() + with: + name: coverage-blob-ca-${{ matrix.os }} + path: plugins/ca/tools/.vitest-reports/ + retention-days: 5 + + coverage-union-merge: + # The union half of #521: every host's blob merged into the single figure + # `tdd` Phase 5 is meant to quote. Advisory, like the cells that feed it. + name: "[CHECK] | [REPO] | Coverage union" + needs: [changes, coverage-union] + if: always() && needs.changes.outputs.ca == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + defaults: + run: + working-directory: plugins/ca/tools + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 20 + cache: npm + cache-dependency-path: plugins/ca/tools/package-lock.json + - name: Install (locked) + run: npm ci --ignore-scripts + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: coverage-blob-ca-* + merge-multiple: true + path: plugins/ca/tools/.vitest-reports/ + - name: Merge and report the union + # A missing blob (a cancelled or flaked host cell) degrades to "report + # what we have and name what is missing" rather than failing a lane + # nothing gates on - the same thing maturity-coverage.md tells a human + # reading a partial local report. + run: | + echo "### Coverage union - plugins/ca/tools" >> "$GITHUB_STEP_SUMMARY" + blobs=$(ls .vitest-reports/*.json 2>/dev/null | wc -l) + echo "blobs merged: $blobs" | tee -a "$GITHUB_STEP_SUMMARY" + ls .vitest-reports/ 2>/dev/null | sed 's/^/ - /' >> "$GITHUB_STEP_SUMMARY" || true + if [ "$blobs" -eq 0 ]; then + echo "no host produced a blob - nothing to merge (advisory)" | tee -a "$GITHUB_STEP_SUMMARY" + exit 0 + fi + if [ "$blobs" -lt 2 ]; then + echo "PARTIAL: only $blobs of 2 hosts reported - this is NOT the union figure" | tee -a "$GITHUB_STEP_SUMMARY" + fi + echo '```' >> "$GITHUB_STEP_SUMMARY" + npx vitest --merge-reports .vitest-reports --coverage --coverage.reporter=text-summary 2>&1 | tee -a "$GITHUB_STEP_SUMMARY" || true + echo '```' >> "$GITHUB_STEP_SUMMARY" + tools: name: "[CHECK] | [CA ] | Farm dispatcher contract" needs: changes @@ -1601,6 +1723,8 @@ jobs: - ca-pi-checks - ca-pi-tools - ca-pi-latest + - coverage-union + - coverage-union-merge - ca-pi-codeql - hooks - version-bump diff --git a/CHANGELOG.md b/CHANGELOG.md index 60762b13..a66190fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,26 @@ predate the plugin rewrite and are grouped by date. ## [Unreleased] +## [2.10.2] — 2026-07-28 + +### Changed + +- **A quoted coverage figure is now the union across a tree's supported hosts, + per tree (#521).** The command is identical on every platform; the report is + not. Code behind a platform fork cannot execute off its own platform, so a + single-host report scores the other platform's arm as uncovered no matter how + well tested it is — measured here, `exec.ts` reads 87.50% branches on Windows + against 76.38% on Linux, an 11-point gap that is entirely `awaitTaskkill` plus + the win32 `treeKill` arm on one side and the POSIX arm on the other. + + That matters beyond arithmetic: `treeKill` is a process-containment path, so + under a single-host rule a genuine gap in it is indistinguishable from the + platform artifact. + + `includes/maturity-coverage.md` gains the rule and `tdd` Phase 5 now requires + the measuring host to be named. Trees with no platform fork stay single-host — + a union of identical reports is the same report. + ## [2.10.1] — 2026-07-28 ### Fixed diff --git a/README.md b/README.md index 7db6d416..89e6b000 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Every intent routes through a gated skill or reviewer agent. Nothing commits unt Claude Code plugin Codex plugin Pi Feature Forge preview -version 2.10.1 +version 2.10.2 commands skills agents diff --git a/core/surface/includes/maturity-coverage.md b/core/surface/includes/maturity-coverage.md index 41281c31..d644d0e8 100644 --- a/core/surface/includes/maturity-coverage.md +++ b/core/surface/includes/maturity-coverage.md @@ -34,6 +34,34 @@ the gap it closed, because it converts an honest red into a green that asserts n backfilling to clear this bar, work the uncovered *report* — error and refusal paths first — and let the number follow. +## Which host (issue #521) + +**A quoted coverage figure is the UNION of the supported hosts' reports for that tree.** Not any +single host's. + +The command is identical everywhere; the REPORT is not. Code behind a platform fork cannot execute +off its own platform, so a single-host report scores the other platform's arm as uncovered — and it +stays uncovered no matter how many tests are written for it. Measured on this repo: `exec.ts` reads +87.50% branches on Windows and 76.38% on Linux, an 11-point gap that is entirely `awaitTaskkill` and +the win32 `treeKill` arm on one side and the POSIX arm on the other. Neither number is wrong; both +are incomplete. + +That matters beyond arithmetic. `treeKill` is a process-containment path, so under a single-host +rule a GENUINE gap in it is indistinguishable from the platform artifact — the figure stops +measuring test quality and starts measuring how much of the tree is POSIX. + +**Applies per tree, and only where it earns its cost.** A tree with platform-forked code is measured +on more than one host and merged; a tree with none stays single-host, because a union of identical +reports is the same report. `tech-stack.md` names which trees are which, and which hosts a tree is +measured on. + +**When quoting a figure — in an issue, an ADR, a phase record — name the host or hosts it came +from.** An unattributed number is not reproducible, and this is the ambiguity #521 was filed for. + +Where only one host's report is available, that is a legitimate figure: state the host and say the +other's contribution is missing. A partial measurement that says so is worth more than a merged one +that cannot be reproduced. + ## The no-tooling exemption — cite it, never assert it Where a surface has no coverage tooling at all, there is no numeric floor to check. Record that diff --git a/core/surface/skills/tdd/SKILL.md b/core/surface/skills/tdd/SKILL.md index fe1178c5..2f14f40e 100644 --- a/core/surface/skills/tdd/SKILL.md +++ b/core/surface/skills/tdd/SKILL.md @@ -88,6 +88,12 @@ Run the coverage command from `tech-stack.md`. **Lines and branches must both cl — a report satisfying one and not the other does not pass (issue #507). Below it on either → add tests until both are met. +**Name the host you measured on**, and for a tree `tech-stack.md` marks as platform-forked, the +figure is the UNION across its supported hosts — a single-host report scores the other platform's +arm as permanently uncovered (issue #521, conditions in `{{PLUGIN_ROOT}}/includes/maturity-coverage.md`). +Where only one host is available, say so and name what is missing rather than quoting it as the +whole. + Where the surface has no coverage tooling at all, take the no-tooling exemption in `{{PLUGIN_ROOT}}/includes/maturity-coverage.md` — which requires QUOTING the `tech-stack.md` Coverage section that omits a command for this surface — and pass the phase on the Phase 4 diff --git a/package.json b/package.json index b7303a08..2c440d87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ca-pi", - "version": "0.1.38", + "version": "0.1.39", "private": true, "license": "AGPL-3.0-only", "engines": { diff --git a/plugins/ca-codex/.codex-plugin/plugin.json b/plugins/ca-codex/.codex-plugin/plugin.json index 9bb66a9b..02c76a0d 100644 --- a/plugins/ca-codex/.codex-plugin/plugin.json +++ b/plugins/ca-codex/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ca-codex", "description": "Governance kernel for OpenAI Codex CLI: the full codeArbiter surface — 37 ca-prefixed governance skills (spec-driven /feature pipeline, nine-gate commit gate, ADRs, audits) plus enforcement hooks (persona injection, blocking pre-exec and pre-write gates, append-only audit trail) — sharing one .codearbiter/ store with the Claude Code sibling plugin. Standalone: opt a repo in with ca-init; enforcement stays dormant until .codearbiter/CONTEXT.md carries 'arbiter: enabled'. Requires Python 3 and Codex >= 0.143.0; live-verified on 0.144.1.", - "version": "0.3.0", + "version": "0.3.1", "author": { "name": "arbiterForge" }, "license": "AGPL-3.0-only", "homepage": "https://github.com/arbiterForge/codeArbiter", diff --git a/plugins/ca-codex/CHANGELOG.md b/plugins/ca-codex/CHANGELOG.md index 61b57443..c4906ffc 100644 --- a/plugins/ca-codex/CHANGELOG.md +++ b/plugins/ca-codex/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to the **ca-codex** plugin are recorded here. Format follows --- +## [0.3.1] — 2026-07-28 + +### Changed + +- Projection of the shared surface change in ca 2.10.2: coverage figures are the + union across a tree's supported hosts, and `tdd` Phase 5 requires the + measuring host to be named (#521). No Codex-specific behaviour changed. + ## [0.3.0] — 2026-07-12 — Shared-state concurrency hardening ### Added diff --git a/plugins/ca-codex/includes/maturity-coverage.md b/plugins/ca-codex/includes/maturity-coverage.md index 41281c31..d644d0e8 100644 --- a/plugins/ca-codex/includes/maturity-coverage.md +++ b/plugins/ca-codex/includes/maturity-coverage.md @@ -34,6 +34,34 @@ the gap it closed, because it converts an honest red into a green that asserts n backfilling to clear this bar, work the uncovered *report* — error and refusal paths first — and let the number follow. +## Which host (issue #521) + +**A quoted coverage figure is the UNION of the supported hosts' reports for that tree.** Not any +single host's. + +The command is identical everywhere; the REPORT is not. Code behind a platform fork cannot execute +off its own platform, so a single-host report scores the other platform's arm as uncovered — and it +stays uncovered no matter how many tests are written for it. Measured on this repo: `exec.ts` reads +87.50% branches on Windows and 76.38% on Linux, an 11-point gap that is entirely `awaitTaskkill` and +the win32 `treeKill` arm on one side and the POSIX arm on the other. Neither number is wrong; both +are incomplete. + +That matters beyond arithmetic. `treeKill` is a process-containment path, so under a single-host +rule a GENUINE gap in it is indistinguishable from the platform artifact — the figure stops +measuring test quality and starts measuring how much of the tree is POSIX. + +**Applies per tree, and only where it earns its cost.** A tree with platform-forked code is measured +on more than one host and merged; a tree with none stays single-host, because a union of identical +reports is the same report. `tech-stack.md` names which trees are which, and which hosts a tree is +measured on. + +**When quoting a figure — in an issue, an ADR, a phase record — name the host or hosts it came +from.** An unattributed number is not reproducible, and this is the ambiguity #521 was filed for. + +Where only one host's report is available, that is a legitimate figure: state the host and say the +other's contribution is missing. A partial measurement that says so is worth more than a merged one +that cannot be reproduced. + ## The no-tooling exemption — cite it, never assert it Where a surface has no coverage tooling at all, there is no numeric floor to check. Record that diff --git a/plugins/ca-codex/routines/tdd/SKILL.md b/plugins/ca-codex/routines/tdd/SKILL.md index 0f3d20f6..c59f9d4e 100644 --- a/plugins/ca-codex/routines/tdd/SKILL.md +++ b/plugins/ca-codex/routines/tdd/SKILL.md @@ -88,6 +88,12 @@ Run the coverage command from `tech-stack.md`. **Lines and branches must both cl — a report satisfying one and not the other does not pass (issue #507). Below it on either → add tests until both are met. +**Name the host you measured on**, and for a tree `tech-stack.md` marks as platform-forked, the +figure is the UNION across its supported hosts — a single-host report scores the other platform's +arm as permanently uncovered (issue #521, conditions in `${CLAUDE_PLUGIN_ROOT}/includes/maturity-coverage.md`). +Where only one host is available, say so and name what is missing rather than quoting it as the +whole. + Where the surface has no coverage tooling at all, take the no-tooling exemption in `${CLAUDE_PLUGIN_ROOT}/includes/maturity-coverage.md` — which requires QUOTING the `tech-stack.md` Coverage section that omits a command for this surface — and pass the phase on the Phase 4 diff --git a/plugins/ca-pi/CHANGELOG.md b/plugins/ca-pi/CHANGELOG.md index abfc3d0e..cfcf609a 100644 --- a/plugins/ca-pi/CHANGELOG.md +++ b/plugins/ca-pi/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to `ca-pi` are documented in this file. +## [0.1.39] - 2026-07-28 + +### Changed + +- Projection of the shared surface change in ca 2.10.2: coverage figures are the + union across a tree's supported hosts, and `tdd` Phase 5 requires the + measuring host to be named (#521). `plugins/ca-pi/tools` is platform-forked + and therefore in scope for the rule; its union job is tracked in #537. No + Pi-specific behaviour changed. + ## [0.1.38] - 2026-07-27 ### Fixed diff --git a/plugins/ca-pi/includes/maturity-coverage.md b/plugins/ca-pi/includes/maturity-coverage.md index 41281c31..d644d0e8 100644 --- a/plugins/ca-pi/includes/maturity-coverage.md +++ b/plugins/ca-pi/includes/maturity-coverage.md @@ -34,6 +34,34 @@ the gap it closed, because it converts an honest red into a green that asserts n backfilling to clear this bar, work the uncovered *report* — error and refusal paths first — and let the number follow. +## Which host (issue #521) + +**A quoted coverage figure is the UNION of the supported hosts' reports for that tree.** Not any +single host's. + +The command is identical everywhere; the REPORT is not. Code behind a platform fork cannot execute +off its own platform, so a single-host report scores the other platform's arm as uncovered — and it +stays uncovered no matter how many tests are written for it. Measured on this repo: `exec.ts` reads +87.50% branches on Windows and 76.38% on Linux, an 11-point gap that is entirely `awaitTaskkill` and +the win32 `treeKill` arm on one side and the POSIX arm on the other. Neither number is wrong; both +are incomplete. + +That matters beyond arithmetic. `treeKill` is a process-containment path, so under a single-host +rule a GENUINE gap in it is indistinguishable from the platform artifact — the figure stops +measuring test quality and starts measuring how much of the tree is POSIX. + +**Applies per tree, and only where it earns its cost.** A tree with platform-forked code is measured +on more than one host and merged; a tree with none stays single-host, because a union of identical +reports is the same report. `tech-stack.md` names which trees are which, and which hosts a tree is +measured on. + +**When quoting a figure — in an issue, an ADR, a phase record — name the host or hosts it came +from.** An unattributed number is not reproducible, and this is the ambiguity #521 was filed for. + +Where only one host's report is available, that is a legitimate figure: state the host and say the +other's contribution is missing. A partial measurement that says so is worth more than a merged one +that cannot be reproduced. + ## The no-tooling exemption — cite it, never assert it Where a surface has no coverage tooling at all, there is no numeric floor to check. Record that diff --git a/plugins/ca-pi/package.json b/plugins/ca-pi/package.json index a9f5f029..59f3015b 100644 --- a/plugins/ca-pi/package.json +++ b/plugins/ca-pi/package.json @@ -1,6 +1,6 @@ { "name": "ca-pi", - "version": "0.1.38", + "version": "0.1.39", "private": true, "license": "AGPL-3.0-only", "type": "module", diff --git a/plugins/ca-pi/routines/tdd/SKILL.md b/plugins/ca-pi/routines/tdd/SKILL.md index 41eb0e8d..4d89d67b 100644 --- a/plugins/ca-pi/routines/tdd/SKILL.md +++ b/plugins/ca-pi/routines/tdd/SKILL.md @@ -88,6 +88,12 @@ Run the coverage command from `tech-stack.md`. **Lines and branches must both cl — a report satisfying one and not the other does not pass (issue #507). Below it on either → add tests until both are met. +**Name the host you measured on**, and for a tree `tech-stack.md` marks as platform-forked, the +figure is the UNION across its supported hosts — a single-host report scores the other platform's +arm as permanently uncovered (issue #521, conditions in `/includes/maturity-coverage.md`). +Where only one host is available, say so and name what is missing rather than quoting it as the +whole. + Where the surface has no coverage tooling at all, take the no-tooling exemption in `/includes/maturity-coverage.md` — which requires QUOTING the `tech-stack.md` Coverage section that omits a command for this surface — and pass the phase on the Phase 4 diff --git a/plugins/ca/.claude-plugin/plugin.json b/plugins/ca/.claude-plugin/plugin.json index 7212bf90..faa9bf28 100644 --- a/plugins/ca/.claude-plugin/plugin.json +++ b/plugins/ca/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "ca", "displayName": "codeArbiter", "description": "Orchestration layer for Claude Code. Routes every intent through gated skills and reviewer agents, drives spec-driven TDD, mechanically enforces the commit and audit-trail gates, decides via SMARTS, and keeps an append-only audit trail. Requires Python 3 on PATH. Dormant until you opt a repo in; run /ca:init to activate.", - "version": "2.10.1", + "version": "2.10.2", "author": { "name": "arbiterForge" }, "license": "AGPL-3.0-only", "homepage": "https://github.com/arbiterForge/codeArbiter", diff --git a/plugins/ca/includes/maturity-coverage.md b/plugins/ca/includes/maturity-coverage.md index 41281c31..d644d0e8 100644 --- a/plugins/ca/includes/maturity-coverage.md +++ b/plugins/ca/includes/maturity-coverage.md @@ -34,6 +34,34 @@ the gap it closed, because it converts an honest red into a green that asserts n backfilling to clear this bar, work the uncovered *report* — error and refusal paths first — and let the number follow. +## Which host (issue #521) + +**A quoted coverage figure is the UNION of the supported hosts' reports for that tree.** Not any +single host's. + +The command is identical everywhere; the REPORT is not. Code behind a platform fork cannot execute +off its own platform, so a single-host report scores the other platform's arm as uncovered — and it +stays uncovered no matter how many tests are written for it. Measured on this repo: `exec.ts` reads +87.50% branches on Windows and 76.38% on Linux, an 11-point gap that is entirely `awaitTaskkill` and +the win32 `treeKill` arm on one side and the POSIX arm on the other. Neither number is wrong; both +are incomplete. + +That matters beyond arithmetic. `treeKill` is a process-containment path, so under a single-host +rule a GENUINE gap in it is indistinguishable from the platform artifact — the figure stops +measuring test quality and starts measuring how much of the tree is POSIX. + +**Applies per tree, and only where it earns its cost.** A tree with platform-forked code is measured +on more than one host and merged; a tree with none stays single-host, because a union of identical +reports is the same report. `tech-stack.md` names which trees are which, and which hosts a tree is +measured on. + +**When quoting a figure — in an issue, an ADR, a phase record — name the host or hosts it came +from.** An unattributed number is not reproducible, and this is the ambiguity #521 was filed for. + +Where only one host's report is available, that is a legitimate figure: state the host and say the +other's contribution is missing. A partial measurement that says so is worth more than a merged one +that cannot be reproduced. + ## The no-tooling exemption — cite it, never assert it Where a surface has no coverage tooling at all, there is no numeric floor to check. Record that diff --git a/plugins/ca/skills/tdd/SKILL.md b/plugins/ca/skills/tdd/SKILL.md index eb3ee463..78a668ce 100644 --- a/plugins/ca/skills/tdd/SKILL.md +++ b/plugins/ca/skills/tdd/SKILL.md @@ -88,6 +88,12 @@ Run the coverage command from `tech-stack.md`. **Lines and branches must both cl — a report satisfying one and not the other does not pass (issue #507). Below it on either → add tests until both are met. +**Name the host you measured on**, and for a tree `tech-stack.md` marks as platform-forked, the +figure is the UNION across its supported hosts — a single-host report scores the other platform's +arm as permanently uncovered (issue #521, conditions in `${CLAUDE_PLUGIN_ROOT}/includes/maturity-coverage.md`). +Where only one host is available, say so and name what is missing rather than quoting it as the +whole. + Where the surface has no coverage tooling at all, take the no-tooling exemption in `${CLAUDE_PLUGIN_ROOT}/includes/maturity-coverage.md` — which requires QUOTING the `tech-stack.md` Coverage section that omits a command for this surface — and pass the phase on the Phase 4