@junghanacs/entwurf 0.18.1 → 0.18.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/AGENTS.md +1 -1
- package/CHANGELOG.md +69 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +5 -3
- package/VERIFY.md +2 -2
- package/package.json +1 -1
- package/scripts/check-gate-qualification.ts +1 -1
- package/scripts/check-release-gate-outcomes.ts +224 -9
- package/scripts/ci-qualify-decide.sh +159 -0
- package/scripts/fixtures/qualify-replay.json +149 -0
- package/scripts/mutants/release-gate.json +45 -1
package/AGENTS.md
CHANGED
|
@@ -157,7 +157,7 @@ implement → affected focused gates → independent review → one amendment bu
|
|
|
157
157
|
|
|
158
158
|
- **Inner loop:** run only the gates whose subject changed. Do not open the full floor to learn what a focused gate already answers.
|
|
159
159
|
- **Review before floor:** independent review and its corrections close as one bundle before the full floor runs.
|
|
160
|
-
- **Qualification is scheduled, not ambient — but its HEAD is not.** The mutant-EXECUTING body, `check-gate-qualification`, is not in the default check chains (core or full), so the operator inner loop never re-pays the full mutant inventory. It runs standalone once when a lane changed a gate, mutant, or matrix; machine time re-proves it everywhere else — the CI `check` job runs it on
|
|
160
|
+
- **Qualification is scheduled, not ambient — but its HEAD is not.** The mutant-EXECUTING body, `check-gate-qualification`, is not in the default check chains (core or full), so the operator inner loop never re-pays the full mutant inventory. It runs standalone once when a lane changed a gate, mutant, or matrix; machine time re-proves it everywhere else — the CI `check` job runs it on a branch push whose two-dot range touched the qualification surface (`scripts/ci-qualify-decide.sh` derives that path set from the manifests themselves), unconditionally on `workflow_dispatch -f qualify=true` and a weekly schedule, and release-gate carries it as a MUST step. The exact-SHA release oracle refuses a SHA whose body step did not conclude success, so a filtered-out release commit is a named failure with a documented dispatch recovery, never a quiet pass. Its head — runner self-test, manifest-set validation, declared lane inventory — is `check-gate-manifests`, which executes no mutant and never snapshots the repo, and it IS in `check:hermetic`: three of the five reds qualification has ever produced in CI died there in under five seconds. A tag push runs no CI at all, and the exact-SHA evidence a release quotes is whichever run at that commit carries the body concluding success — the branch run when the push moved the qualification surface, else the dispatch run the release skill's recovery creates.
|
|
161
161
|
- **Full floor once** (`pnpm run check:full`), on the frozen commit candidate. While it runs, nothing edits the worktree or index — including the NEXT boot sectors; a moved candidate voids the run's evidence.
|
|
162
162
|
- **pre-commit is not the floor.** `.husky/pre-commit` carries only fast static checks (whitespace, lint, typecheck); the full floor is owned by this protocol, not by the hook. Do not grow the hook back, and do not build receipt/cache machinery to prove the protocol was followed.
|
|
163
163
|
- **Release/LIVE acceptance is untouched.** VERIFY.md floors keep full strength; a shorter inner loop never lowers release evidence.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,75 @@ All notable changes to this project will be documented here. Format follows [Kee
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.18.2 - 2026-09-07
|
|
8
|
+
|
|
9
|
+
One lane: #103, the second stage of the CI evidence budget (#99). The mutant body — 373 replants,
|
|
10
|
+
~30 minutes — stops running on every branch push, and the release oracle stops accepting a run that
|
|
11
|
+
did not carry it. Nothing in the runtime surface moves.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **The exact-SHA release oracle requires the qualification BODY, not three green job names (#103
|
|
16
|
+
piece 1).** `verify-exact-ci.sh` reads a fourth axis over one guard: the `check` job's
|
|
17
|
+
`Run ./run.sh check-gate-qualification` step must conclude `success`, with absent, skipped and
|
|
18
|
+
failed each named separately in the refusal. It also stops filtering to `--event push`, so a
|
|
19
|
+
`workflow_dispatch` run at the same SHA is admissible evidence — which is exactly when the
|
|
20
|
+
always-present `headSha` check starts carrying load, because `gh workflow run --ref` takes a
|
|
21
|
+
branch and never a SHA. A skipped body is not evidence: the oracle refuses that SHA and prints the
|
|
22
|
+
dispatch command that fixes it. This landed BEFORE the skip could exist.
|
|
23
|
+
- **The body runs only on a branch push that moved the qualification surface (#103 piece 2).**
|
|
24
|
+
`scripts/ci-qualify-decide.sh` diffs the two-dot push range GitHub itself compares and intersects
|
|
25
|
+
it with a path set **derived from `scripts/mutants/*.json` at runtime** — no copy in YAML, so a new
|
|
26
|
+
mutant subject cannot land outside the filter. Five numbered fail-opens, each naming itself in the
|
|
27
|
+
log: an all-zero base, a forced push (whose base GitHub does not document), a dispatch without
|
|
28
|
+
`qualify=true` or a schedule, an unreadable range, and `pull_request`. `workflow_dispatch -f
|
|
29
|
+
qualify=true` and a weekly schedule run the body unconditionally; `fetch-depth: 0` on the check job
|
|
30
|
+
is what keeps the base readable, and it measured free (2s checkout, the same as depth 1).
|
|
31
|
+
- **Measured on real runs, not argued.** A code push ran the body and took 36m24s
|
|
32
|
+
([34047559085](https://github.com/junghan0611/entwurf/actions/runs/34047559085)); a docs-only push
|
|
33
|
+
skipped it and took 6m13s
|
|
34
|
+
([34065841309](https://github.com/junghan0611/entwurf/actions/runs/34065841309)); the four-axis
|
|
35
|
+
oracle **refused** that second SHA while all three of its jobs were green, and the documented
|
|
36
|
+
dispatch recovery turned the refusal back into a pass
|
|
37
|
+
([34066181211](https://github.com/junghan0611/entwurf/actions/runs/34066181211)). The historical
|
|
38
|
+
justification is replayed rather than remembered: all five qualification reds this repo's CI has
|
|
39
|
+
ever produced still run the body under the filter.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- Three kill-qualified claims and their exact-once replants, inventory 370 → 373:
|
|
44
|
+
`RELEASE-SHA-QUALIFIED-IN-CI` (the fourth axis), `QUALIFY-FILTER-COVERS-SUBJECTS` (every manifest
|
|
45
|
+
subject and signatureSource decides `run_body=true`, asserted behaviourally against the shipped
|
|
46
|
+
script), and `QUALIFY-FILTER-READS-PUSH-RANGE` (a hermetic throwaway repo with a docs-only tip over
|
|
47
|
+
a commit that touched a subject — the two-dot reading, proven without repo history).
|
|
48
|
+
`QUALIFY-FILTER-REPLAYS-PAST-CATCHES` replays the five reds from recorded file lists.
|
|
49
|
+
- `scripts/fixtures/qualify-replay.json` — the five historical reds as the push ranges GitHub
|
|
50
|
+
compared, measured once here because history is not readable everywhere the gate runs.
|
|
51
|
+
|
|
52
|
+
### Verification
|
|
53
|
+
|
|
54
|
+
All on oracle (Linux, node 24.18.1, pi 0.85.1, omp 18.1.12 — updated from 18.0.0 during this lane).
|
|
55
|
+
|
|
56
|
+
- **`LIVE=1 ./run.sh release-gate /tmp/entwurf-release-gate-0.18.2.JlEU88 --cut` → `cut: OK`.**
|
|
57
|
+
**MUST: PASS=23 FAIL=0 SKIP=0**, BEHAVIOR: PASS=1 FAIL=0 SKIP=0, exit 0, 3305s (55m05s).
|
|
58
|
+
`check-gate-qualification` as its MUST step: **373/373 KILLED**.
|
|
59
|
+
- `pnpm run check:full` on the prepared tree: exit 0, 504s.
|
|
60
|
+
- Pre-version landing run for `0f6667d`: CI run
|
|
61
|
+
[34070192960](https://github.com/junghan0611/entwurf/actions/runs/34070192960), all three jobs
|
|
62
|
+
success — and this is the first release landing judged by the fourth axis the same release adds:
|
|
63
|
+
the oracle read that run's qualification step, not just its job names.
|
|
64
|
+
|
|
65
|
+
### Notes
|
|
66
|
+
|
|
67
|
+
- **A gate that reads repo history cannot live in the qualification snapshot.** The replay cell first
|
|
68
|
+
read live git objects and went CONTROL-RED inside the snapshot's fresh baseline, costing the whole
|
|
69
|
+
release-gate lane its 17 kills. That is why the fixture carries file lists and the range semantics
|
|
70
|
+
are proven hermetically instead.
|
|
71
|
+
- **Key order in `ci.yml` is a contract.** The body step keeps `run:` as its leading key: cell 8a
|
|
72
|
+
counts that sequence-item form exactly once, and the oracle matches GitHub's rendered
|
|
73
|
+
`Run <command>` step name, so an `if:` placed first — or any `name:` — would break both. Confirmed
|
|
74
|
+
on a real run after the filter landed.
|
|
75
|
+
|
|
7
76
|
## 0.18.1 - 2026-09-06
|
|
8
77
|
|
|
9
78
|
Two lanes, both landing on `main` after `v0.18.0`: the CI evidence-budget stage 1 (#102, from
|
package/CONTRIBUTING.md
CHANGED
|
@@ -30,7 +30,7 @@ pnpm check # everyday core (prints wall time; <=60s on the referenc
|
|
|
30
30
|
pnpm run check:full # full deterministic floor — the required PR gate
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
The deterministic floor is tiered (#70). `pnpm check` is the everyday core (biome, tsc, the vitest lanes, and the fast contract gates); `pnpm run check:full` adds the hermetic-integration and package/install tiers — including `check-gate-manifests`, the qualification HEAD (runner self-test, manifest-set validation, declared lane inventory, zero mutants executed, ~8s), but not the separately scheduled mutant-EXECUTING body `check-gate-qualification`, which CI runs on
|
|
33
|
+
The deterministic floor is tiered (#70). `pnpm check` is the everyday core (biome, tsc, the vitest lanes, and the fast contract gates); `pnpm run check:full` adds the hermetic-integration and package/install tiers — including `check-gate-manifests`, the qualification HEAD (runner self-test, manifest-set validation, declared lane inventory, zero mutants executed, ~8s), but not the separately scheduled mutant-EXECUTING body `check-gate-qualification`, which CI runs on a branch push whose diff touched the qualification surface (and unconditionally on dispatch or the weekly schedule) and a gate-changing PR must run once itself. Exact membership is the named `check:*` scripts in `package.json`. Run `check:full` once on your frozen commit candidate — the pre-commit hook (`.husky/pre-commit`) carries only fast static checks (whitespace, lint, typecheck), not the full floor, so a green `pnpm run check:full` before commit is the evidence that your change holds (scheduling contract: AGENTS.md "Verification scheduling").
|
|
34
34
|
|
|
35
35
|
For changes that touch backend launch, session lifecycle, or `_meta` shape, also run
|
|
36
36
|
the live ACP smokes that cover the touched rail — at minimum:
|
package/README.md
CHANGED
|
@@ -454,9 +454,11 @@ LIVE=1 ENTWURF_ACP_CORTEX_CONNECTION=<conn> ./run.sh smoke-acp-cortex-live # Co
|
|
|
454
454
|
`pnpm run check:full` includes the AGY permission contract matrix and the qualification
|
|
455
455
|
HEAD (`check-gate-manifests` — runner self-test, manifest-set validation, declared lane
|
|
456
456
|
inventory, zero mutants executed); the committed-mutant EXECUTION is scheduled separately
|
|
457
|
-
(`./run.sh check-gate-qualification` — the CI `check` job runs it on
|
|
458
|
-
|
|
459
|
-
|
|
457
|
+
(`./run.sh check-gate-qualification` — the CI `check` job runs it on a branch push that touched the qualification surface,
|
|
458
|
+
unconditionally on `workflow_dispatch -f qualify=true` and a weekly schedule, and release-gate
|
|
459
|
+
carries it as a MUST step; a tag push runs no CI at all). A branch run whose body was filtered out
|
|
460
|
+
is not release evidence: the exact-SHA oracle requires the run it reads to carry that step
|
|
461
|
+
concluding success. A gate a
|
|
460
462
|
release touches must kill its known defect for the claimed `[QK:<claim>]` reason —
|
|
461
463
|
the descriptions above name what each smoke covers, and no check count is quality
|
|
462
464
|
evidence on its own. Gate qualification needs the git work surface, while the matrix
|
package/VERIFY.md
CHANGED
|
@@ -41,7 +41,7 @@ Verification here is not a benchmark. In production we exchange short turns and
|
|
|
41
41
|
### The canonical floor — two entry points
|
|
42
42
|
|
|
43
43
|
- **Deterministic floor — two tiers (#70):** `pnpm check` is the everyday CORE — toolchain (lint + typecheck), the vitest lanes, and the pure-unit / behavioral-contract / source-topology gates plus the cheap static coherence checks. It prints its own total wall time; acceptance is ≤60s on the reference host `oracle` (an operator measurement, never a hard wall-clock gate on arbitrary hosts). `pnpm run check:full` is the FULL deterministic floor — the core plus the hermetic-integration and package/install tiers — and is what the frozen-candidate protocol, push CI, release-gate, and `prepublishOnly` run. Exact membership is the named `check:*` group scripts in `package.json` (the executable SSOT — this document records meaning and principles, not the command list); a gate changes tier by semantic-class decision, never by getting faster or slower. The FULL tier carries the qualification HEAD (`check-gate-manifests`, through `check:hermetic`); the everyday core does not, and neither tier carries the separately scheduled mutant-executing body (`check-gate-qualification`).
|
|
44
|
-
- **Discriminating power of that floor:** `./run.sh check-gate-qualification` (scheduled, not ambient: standalone when a lane changes a gate/mutant/matrix, in the CI `check` job on
|
|
44
|
+
- **Discriminating power of that floor:** `./run.sh check-gate-qualification` (scheduled, not ambient: standalone when a lane changes a gate/mutant/matrix, in the CI `check` job on a branch push that touched the qualification surface, and as a release-gate MUST step — no longer inside the default check chains; a semver TAG push triggers no CI at all, and the exact-SHA evidence a release quotes is whichever run at that commit CARRIES the body — the branch run when the push touched the qualification surface, else a dispatch run; a push that touched none of the qualification surface skips the body, and `workflow_dispatch -f qualify=true` plus a weekly `schedule` run it unconditionally — the exact-SHA release oracle refuses a SHA whose body step did not conclude success) re-plants committed defect mutants (`scripts/mutants/*.json`, one per closed defect class) in an isolated snapshot repo and requires each to turn its gate red **bounded and at its claimed `[QK:<claim>]` signature** — a wrong-reason red fails, a baseline-red control voids the whole group, and the runner is negative-controlled on every run (zero-match/multi-match/survived/wrong-reason/hang/control-red/impurity). This measures whether the deterministic gates still *block* what they claim to block; it is **not a new evidence level** (L0–L5 are untouched) and never substitutes for LIVE evidence. Per-cut records cite claim IDs + killed mutant IDs — "N checks passed" alone is not evidence. `check-agy-permission-matrix` complements it with the enumerated permission contract space (literal cells + stated exclusion rules, oracle independent of the SUT). The gate's HEAD is separable and IS in the floor: `check-gate-manifests` runs the runner self-test, validates the committed manifest set against the origin index, and asserts the declared lane inventory, while executing zero mutants and never snapshotting the repo — three of the five reds qualification has ever produced in CI died in that head, in under five seconds.
|
|
45
45
|
- **Live floor:** `LIVE=1 ./run.sh release-gate <scratch-project-dir> --cut` — `pnpm run check:full` + the v2-native live gates + the ACP plugin acceptance floor. It reports a **two-tier summary**:
|
|
46
46
|
- **MUST tier** (release-blocking — owns the exit code; "green" applies only here): `pnpm run check:full`, `check-gate-qualification`, `smoke-entwurf-v2-matrix-live`, `check-bridge`, `doctor-pi-provider` (#81 — `check-bridge` proves the launcher this checkout SHIPS; this proves the invocation the operator's pi provider actually EXECS, by booting it and requiring the entwurf verb set back, because `command -v` answers yes for a command that exits 127; wired as a step on 2026-08-19 after a relocated pnpm cmd-shim cost a cut sixteen LIVE steps before the same fact surfaced at `smoke-acp-bundled-mcp-live`), the resident-garden-guard zero-token half (record birth / record-keyed socket / attach-on-reopen), the `smoke-acp-*-live` ACP plugin smokes (socket-citizen / raw-turn / overlay / provider / session-reuse / carrier-augment / memory-containment / rgg / mcp / skill / bundled-mcp / v2-send), the axis wired in on 2026-07-31 that the aggregate had simply never listed (`smoke-claude-native-resume-live`), `smoke-entwurf-chain-live` — the cross-harness delivery chain (native Claude Code → pi GPT → pi ACP Sonnet → mailbox terminus) proving sender identity and replyability at every hop plus a real read receipt at the end — the integrated `smoke-mux-lifecycle-live` (its own note below), and the two OMP steps 0.16.0 wired: `smoke-omp-receive-live` (the addressed roundtrip into a live omp citizen; it reads the capability registry and decides its own outcome rather than passing by default) and `smoke-omp-fresh-live` (the clause 7 receipt the release stop now requires). (`smoke-session-id-name` is gone — #50 C3: its `--session-id`/`--name` substrate has no entwurf consumer anymore.)
|
|
47
47
|
- **BEHAVIOR tier** (advisory, non-blocking): the resident-garden-guard positive (a model-in-loop `entwurf_self` turn). A BEHAVIOR FAIL is surfaced with its artifact path but **never blocks the cut**. The lane holds what the model *chooses*, never what our wiring fails to deliver — a gate that TELLS the model which tool to call stays MUST, because its failure is ours — measured 2026-07-24, when the tool turned out to be absent from the session schema in both observed failures (the bundled-MCP readiness gap recorded in `scripts/smoke-acp-v2-send-live.ts`).
|
|
@@ -76,7 +76,7 @@ Do not collapse source, package, fixture, and native-host evidence into one “g
|
|
|
76
76
|
| Source | `pnpm run check:full` + `./run.sh check-gate-qualification` | Does not prove an installed consumer. |
|
|
77
77
|
| Packed install | `check-pack-install` | Real tarball, but checkout-visible. |
|
|
78
78
|
| Linux artifact consumer | required `check-install-container` CI job against one preserved candidate | Fixtures prove package/oracle shape, not a real Claude lifecycle. |
|
|
79
|
-
| Exact release commit | all required CI jobs green at the exact SHA | A different green SHA is not transferable evidence. |
|
|
79
|
+
| Exact release commit | all required CI jobs green, and the `check` job's qualification body step concluded success, at the exact SHA | A different green SHA is not transferable evidence. |
|
|
80
80
|
| LIVE runtime | `LIVE=1 ./run.sh release-gate <scratch> --cut` plus any shipped on-demand backend axis | `--cut` enforces `SKIP=0`; a red wired gate blocks the cut. |
|
|
81
81
|
| Native Claude host | installed strict doctor against a new real session | Missing live join is `NOT CERTIFIED`, not a fixture PASS. |
|
|
82
82
|
| Native agy host | three doctors plus conversation-id-gated native-push round trip | Aggregate release-gate does not own an agy conversation id. |
|
package/package.json
CHANGED
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
|
|
45
45
|
import { strict as assert } from "node:assert";
|
|
46
46
|
import { execFileSync } from "node:child_process";
|
|
47
|
-
import { globSync, readFileSync } from "node:fs";
|
|
47
|
+
import { copyFileSync, globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
48
|
+
import { tmpdir } from "node:os";
|
|
48
49
|
import { basename, join } from "node:path";
|
|
49
50
|
import { fileURLToPath } from "node:url";
|
|
50
51
|
import { LIVE_SKIP_EXIT, LIVE_SKIP_MARKER } from "./lib/live-skip.ts";
|
|
@@ -337,17 +338,20 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
337
338
|
// check-gate-qualification left the default check chains (operator
|
|
338
339
|
// inner-loop cost, 2026-08 subtraction). That move is a gate/release
|
|
339
340
|
// contract: the step must stay REACHABLE on the axes that now own it — the
|
|
340
|
-
// CI check job on
|
|
341
|
+
// CI check job (on the pushes #103's filter still sends it) and release_gate
|
|
342
|
+
// as its own MUST step — and
|
|
341
343
|
// must not silently return to the default chain. Without this cell,
|
|
342
344
|
// deleting the release_gate qualification block or the CI line leaves every
|
|
343
345
|
// focused gate green while a cut quietly loses its discriminating-power
|
|
344
346
|
// step.
|
|
345
347
|
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
348
|
+
// THREE claims, because each is an independent contract. 8a is REACHABILITY
|
|
349
|
+
// (absent from the default chain; present exactly once in CI and exactly once
|
|
350
|
+
// as a wired release_gate MUST step; named in VERIFY). 8b is what the CI step
|
|
351
|
+
// qualifies (the FULL floor, before the qualification run), added by #70. 8c is
|
|
352
|
+
// what the RELEASE path ACCEPTS as evidence for one SHA — that the body step
|
|
353
|
+
// actually ran there (#103). Each carries its own replant — one mutant must
|
|
354
|
+
// never stand in for another.
|
|
351
355
|
// ===========================================================================
|
|
352
356
|
{
|
|
353
357
|
// The default chain is tiered (#70): `check` (core) and `check:full` compose the
|
|
@@ -382,7 +386,11 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
382
386
|
!qualGateBody.includes('results+=("FAIL check-gate-qualification")')
|
|
383
387
|
)
|
|
384
388
|
holes.push("the release_gate qualification step does not wire PASS/FAIL into the MUST counters");
|
|
385
|
-
if (
|
|
389
|
+
if (
|
|
390
|
+
!verifyDoc.includes(
|
|
391
|
+
"in the CI `check` job on a branch push that touched the qualification surface, and as a release-gate MUST step",
|
|
392
|
+
)
|
|
393
|
+
)
|
|
386
394
|
holes.push("VERIFY.md no longer names the owners of the moved qualification step");
|
|
387
395
|
assert.ok(
|
|
388
396
|
holes.length === 0,
|
|
@@ -412,6 +420,210 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
412
420
|
"downgrade/omission axis; ordering is directly asserted by this same oracle. " +
|
|
413
421
|
`Broken: check:full at index ${ciFloorAt}, qualification at index ${ciQualAt}.`,
|
|
414
422
|
);
|
|
423
|
+
|
|
424
|
+
// 8c. The RELEASE oracle requires that body to have actually run at the release
|
|
425
|
+
// SHA -- its own contract, not a branch of 8a. 8a is REACHABILITY (the step
|
|
426
|
+
// exists in the CI job and in release_gate); this is about what the release
|
|
427
|
+
// path ACCEPTS as evidence for one SHA. `verify-exact-ci.sh` read only three
|
|
428
|
+
// job names, and a job conclusion says nothing about whether the step inside
|
|
429
|
+
// it ran: the moment ci.yml can skip that step (#103 piece 2), a green oracle
|
|
430
|
+
// would certify a SHA whose qualification body never executed. That is the
|
|
431
|
+
// "green with no evidence" class this repo fails closed on, which is why the
|
|
432
|
+
// oracle gets the fourth axis BEFORE the skip exists.
|
|
433
|
+
//
|
|
434
|
+
// This is a TEXT oracle, the same grade as check-install-surface S7g. A
|
|
435
|
+
// behavioural oracle would need either a second file (S7a/S7g bind the
|
|
436
|
+
// release surface to one script read from the index) or an injectable
|
|
437
|
+
// RUN_JSON seam -- and that seam would be a way to launder release evidence
|
|
438
|
+
// past gh. Both cost more than the axis is worth here.
|
|
439
|
+
const ciOracle = readFileSync(join(REPO_DIR, ".claude/skills/entwurf-release/scripts/verify-exact-ci.sh"), "utf8");
|
|
440
|
+
const axis: string[] = [];
|
|
441
|
+
if (!ciOracle.includes('QUAL_JOB = "check"') || !ciOracle.includes('"Run ./run.sh check-gate-qualification"'))
|
|
442
|
+
axis.push("the oracle does not name the check job's check-gate-qualification step");
|
|
443
|
+
if (!ciOracle.includes('qual != "success"'))
|
|
444
|
+
axis.push("the oracle does not guard that step's conclusion against 'success'");
|
|
445
|
+
if (!ciOracle.includes('qual == "skipped"'))
|
|
446
|
+
axis.push("the oracle does not classify a SKIPPED body as a failure of its own");
|
|
447
|
+
assert.ok(
|
|
448
|
+
axis.length === 0,
|
|
449
|
+
"[QK:RELEASE-SHA-QUALIFIED-IN-CI] the exact-SHA CI oracle must require the qualification BODY to have run " +
|
|
450
|
+
"at the release SHA -- the `check` job's `Run ./run.sh check-gate-qualification` step concluding 'success', " +
|
|
451
|
+
"with skipped named as its own failure. Three green job names do not prove the step inside one of them " +
|
|
452
|
+
`executed. Broken: ${axis.join("; ")}`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ===========================================================================
|
|
457
|
+
// 8d. The qualification filter COVERS every mutant subject (#103 piece 2)
|
|
458
|
+
//
|
|
459
|
+
// The body no longer runs on every branch push; a decision script reads the
|
|
460
|
+
// push range and answers. Its whole safety argument is that the path set is
|
|
461
|
+
// DERIVED from the committed manifests rather than copied into a list, so a
|
|
462
|
+
// new mutant subject cannot land outside the filter and quietly stop being
|
|
463
|
+
// re-proven in CI. This asserts that property behaviourally: every subject
|
|
464
|
+
// and signatureSource in scripts/mutants/*.json, fed to the script as a
|
|
465
|
+
// one-file change, must decide `run_body=true`. The oracle is the manifests
|
|
466
|
+
// themselves, read here independently of the script.
|
|
467
|
+
// ===========================================================================
|
|
468
|
+
{
|
|
469
|
+
const decider = join(REPO_DIR, "scripts/ci-qualify-decide.sh");
|
|
470
|
+
const paths = new Set<string>();
|
|
471
|
+
for (const file of globSync("scripts/mutants/*.json", { cwd: REPO_DIR })) {
|
|
472
|
+
const manifest = JSON.parse(readFileSync(join(REPO_DIR, file), "utf8")) as {
|
|
473
|
+
mutants?: { subject?: string; signatureSource?: string }[];
|
|
474
|
+
};
|
|
475
|
+
for (const mutant of manifest.mutants ?? []) {
|
|
476
|
+
if (mutant.subject) paths.add(mutant.subject);
|
|
477
|
+
if (mutant.signatureSource) paths.add(mutant.signatureSource);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
assert.ok(paths.size > 50, `read only ${paths.size} mutant paths from the manifests`);
|
|
481
|
+
const uncovered = [...paths].filter((path) => {
|
|
482
|
+
const out = execFileSync("bash", [decider, "--files-from", "-"], {
|
|
483
|
+
cwd: REPO_DIR,
|
|
484
|
+
encoding: "utf8",
|
|
485
|
+
input: `${path}\n`,
|
|
486
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
487
|
+
});
|
|
488
|
+
return out.trim() !== "run_body=true";
|
|
489
|
+
});
|
|
490
|
+
assert.ok(
|
|
491
|
+
uncovered.length === 0,
|
|
492
|
+
"[QK:QUALIFY-FILTER-COVERS-SUBJECTS] the CI qualification filter must run the body for a change to ANY " +
|
|
493
|
+
"committed mutant subject or signature source — it derives that set from scripts/mutants/*.json for " +
|
|
494
|
+
"exactly this reason, so a path it cannot see is a claim that silently stops being re-proven in CI. " +
|
|
495
|
+
`Uncovered (${uncovered.length} of ${paths.size}): ${uncovered.slice(0, 8).join(", ")}`,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ===========================================================================
|
|
500
|
+
// 8e. The filter still runs the body for every RED the body has ever produced
|
|
501
|
+
//
|
|
502
|
+
// Five reds in 549 runs (#99 stage-2). The first reading of them used the
|
|
503
|
+
// tip COMMIT and concluded the filter would have missed four; replaying the
|
|
504
|
+
// real two-dot push range — what GitHub actually compares — showed all five
|
|
505
|
+
// hit. That reversal is the whole evidentiary basis for this filter.
|
|
506
|
+
//
|
|
507
|
+
// It is asserted from RECORDED file lists, not from live history, and that
|
|
508
|
+
// is a constraint rather than a convenience: check-gate-qualification runs
|
|
509
|
+
// every gate inside a snapshot with its own fresh git baseline, where these
|
|
510
|
+
// 2026-07/08 commits do not exist. A cell that read history there would be
|
|
511
|
+
// CONTROL-RED for the whole lane — measured, not predicted (this cell did
|
|
512
|
+
// exactly that on 2026-09-06 and cost the release-gate lane its 17 kills).
|
|
513
|
+
// The fixture carries what history said, measured once, in a repo where the
|
|
514
|
+
// objects are present.
|
|
515
|
+
//
|
|
516
|
+
// It has its own replant, and the reason is the SHAPE of the input: 8d feeds
|
|
517
|
+
// ONE path per call, this feeds a whole push at once. A matcher that only
|
|
518
|
+
// looked at the first entry would leave 8d green — every single-file call is
|
|
519
|
+
// its own first entry — while every one of these five pushes opens with a
|
|
520
|
+
// non-surface path (DELIVERY.md, AGENTS.md, NEXT.md, AGENTS.md,
|
|
521
|
+
// demo/demo-baseline.sh). Invisible to 8d, fatal here.
|
|
522
|
+
//
|
|
523
|
+
// The two-dot READING itself is proven separately, below.
|
|
524
|
+
// ===========================================================================
|
|
525
|
+
{
|
|
526
|
+
const decider = join(REPO_DIR, "scripts/ci-qualify-decide.sh");
|
|
527
|
+
const fixture = JSON.parse(readFileSync(join(REPO_DIR, "scripts/fixtures/qualify-replay.json"), "utf8")) as {
|
|
528
|
+
runs: { runId: string; before: string; head: string; files: string[]; tipOnlyFiles: string[] }[];
|
|
529
|
+
};
|
|
530
|
+
assert.equal(fixture.runs.length, 5, "the replay fixture holds all five historical qualification reds");
|
|
531
|
+
const wrong: string[] = [];
|
|
532
|
+
for (const run of fixture.runs) {
|
|
533
|
+
assert.ok(run.files.length > 0, `run ${run.runId} has no recorded push range`);
|
|
534
|
+
const out = execFileSync("bash", [decider, "--files-from", "-"], {
|
|
535
|
+
cwd: REPO_DIR,
|
|
536
|
+
encoding: "utf8",
|
|
537
|
+
input: `${run.files.join("\n")}\n`,
|
|
538
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
539
|
+
}).trim();
|
|
540
|
+
if (out !== "run_body=true") wrong.push(`run ${run.runId} (${run.files.length} files) decided ${out}`);
|
|
541
|
+
}
|
|
542
|
+
assert.ok(
|
|
543
|
+
wrong.length === 0,
|
|
544
|
+
"[QK:QUALIFY-FILTER-REPLAYS-PAST-CATCHES] every qualification RED in this repo's CI history must still " +
|
|
545
|
+
"run the body under the filter, over the two-dot push range GitHub compares — the measurement that " +
|
|
546
|
+
"overturned the tip-commit reading and justified filtering at all. This reads a whole push at once, " +
|
|
547
|
+
"which 8d cannot: each of these five opens with a non-surface path, so a matcher that stopped after " +
|
|
548
|
+
"the first entry would pass every single-file check 8d makes and fail here. " +
|
|
549
|
+
`Broken: ${wrong.join("; ")}`,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ===========================================================================
|
|
554
|
+
// 8f. The filter reads the two-dot PUSH RANGE, not the tip commit
|
|
555
|
+
//
|
|
556
|
+
// The fixture above proves the matcher's verdict on recorded file lists; it
|
|
557
|
+
// cannot prove which git range produced them, because it calls no git. This
|
|
558
|
+
// does, hermetically: a throwaway repo shaped like the real defect — a push
|
|
559
|
+
// of two commits whose TIP is docs-only while the commit under it touched a
|
|
560
|
+
// mutant subject. The two-dot range sees the code; the tip alone does not.
|
|
561
|
+
// That is the exact misreading #99 corrected, and it is what four of the
|
|
562
|
+
// five recorded pushes look like when read the wrong way.
|
|
563
|
+
//
|
|
564
|
+
// Hermetic on purpose: no repo history, so it runs identically inside the
|
|
565
|
+
// qualification snapshot.
|
|
566
|
+
// ===========================================================================
|
|
567
|
+
{
|
|
568
|
+
const decider = join(REPO_DIR, "scripts/ci-qualify-decide.sh");
|
|
569
|
+
const tmp = mkdtempSync(join(tmpdir(), "entwurf-qualify-range-"));
|
|
570
|
+
// core.hooksPath is set globally on this operator's machine; a fixture repo
|
|
571
|
+
// must not run their hooks.
|
|
572
|
+
const git = (...args: string[]) =>
|
|
573
|
+
execFileSync("git", ["-c", "core.hooksPath=/dev/null", "-c", "user.email=g@e", "-c", "user.name=g", ...args], {
|
|
574
|
+
cwd: tmp,
|
|
575
|
+
stdio: "ignore",
|
|
576
|
+
});
|
|
577
|
+
try {
|
|
578
|
+
git("init", "-q", "-b", "main");
|
|
579
|
+
// The decider reads the repo it SITS IN, so the fixture gets a real copy
|
|
580
|
+
// of it and its own one-entry manifest: no seam, no env override, and the
|
|
581
|
+
// manifest-reading path is exercised too.
|
|
582
|
+
mkdirSync(join(tmp, "scripts/mutants"), { recursive: true });
|
|
583
|
+
mkdirSync(join(tmp, "pi-extensions/lib"), { recursive: true });
|
|
584
|
+
copyFileSync(decider, join(tmp, "scripts/ci-qualify-decide.sh"));
|
|
585
|
+
writeFileSync(
|
|
586
|
+
join(tmp, "scripts/mutants/fixture.json"),
|
|
587
|
+
`${JSON.stringify(
|
|
588
|
+
{
|
|
589
|
+
schemaVersion: 1,
|
|
590
|
+
lane: "fixture",
|
|
591
|
+
mutants: [{ claim: "FIXTURE", subject: "pi-extensions/lib/fixture-subject.ts" }],
|
|
592
|
+
},
|
|
593
|
+
null,
|
|
594
|
+
"\t",
|
|
595
|
+
)}\n`,
|
|
596
|
+
);
|
|
597
|
+
writeFileSync(join(tmp, "seed.txt"), "seed\n");
|
|
598
|
+
git("add", "-A");
|
|
599
|
+
git("commit", "-qm", "seed");
|
|
600
|
+
const base = execFileSync("git", ["rev-parse", "HEAD"], { cwd: tmp, encoding: "utf8" }).trim();
|
|
601
|
+
writeFileSync(join(tmp, "pi-extensions/lib/fixture-subject.ts"), "// a mutant subject\n");
|
|
602
|
+
git("add", "-A");
|
|
603
|
+
git("commit", "-qm", "code commit (mid-push)");
|
|
604
|
+
writeFileSync(join(tmp, "README.md"), "docs only\n");
|
|
605
|
+
git("add", "-A");
|
|
606
|
+
git("commit", "-qm", "docs commit (tip)");
|
|
607
|
+
const head = execFileSync("git", ["rev-parse", "HEAD"], { cwd: tmp, encoding: "utf8" }).trim();
|
|
608
|
+
const decide = (from: string) =>
|
|
609
|
+
execFileSync("bash", [join(tmp, "scripts/ci-qualify-decide.sh"), from, head], {
|
|
610
|
+
cwd: tmp,
|
|
611
|
+
encoding: "utf8",
|
|
612
|
+
env: { ...process.env, CI_EVENT_NAME: "push", CI_FORCED: "false" },
|
|
613
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
614
|
+
}).trim();
|
|
615
|
+
const overRange = decide(base);
|
|
616
|
+
const overTip = decide(`${head}~1`);
|
|
617
|
+
assert.ok(
|
|
618
|
+
overRange === "run_body=true" && overTip === "run_body=false",
|
|
619
|
+
"[QK:QUALIFY-FILTER-READS-PUSH-RANGE] the filter must diff the whole two-dot push range, not the tip " +
|
|
620
|
+
"commit: a push whose tip is docs-only can still carry a mutant subject underneath it, which is how " +
|
|
621
|
+
"four of the five historical reds look like docs pushes when read tip-first. " +
|
|
622
|
+
`Broken: range=${overRange}, tip-only=${overTip} (tip-only must be false, or this fixture proves nothing).`,
|
|
623
|
+
);
|
|
624
|
+
} finally {
|
|
625
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
626
|
+
}
|
|
415
627
|
}
|
|
416
628
|
|
|
417
629
|
// ===========================================================================
|
|
@@ -655,7 +867,10 @@ console.log(
|
|
|
655
867
|
"a run.sh wrapper declining its own prerequisite (including the measured LIVE=1 no-cortex-connection cell); and every " +
|
|
656
868
|
"LIVE smoke is either wired into release_gate or excluded by a sentence the docs still carry; and the moved " +
|
|
657
869
|
"check-gate-qualification stays reachable on its owners (absent from the default chain, exactly once in CI, " +
|
|
658
|
-
"exactly once as a release-gate MUST step) and the CI step qualifies the FULL floor, which runs before it
|
|
870
|
+
"exactly once as a release-gate MUST step) and the CI step qualifies the FULL floor, which runs before it, " +
|
|
871
|
+
"while the exact-SHA release oracle requires that BODY step to have concluded success at the release SHA, " +
|
|
872
|
+
"and the CI filter that decides when that body runs covers every mutant subject and still runs it for all " +
|
|
873
|
+
"five historical reds; and " +
|
|
659
874
|
"every gate a committed mutant names is itself inside check:full or states its exclusion in prose an operator " +
|
|
660
875
|
"reads; and the CI push trigger is filtered to branch refs, so a release tag creates no duplicate run; and " +
|
|
661
876
|
"the operator's CONFIGURED bridge invocation is booted exactly once through run_step, before the ACP LIVE tier",
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Decide whether a push needs the qualification BODY (#103 piece 2).
|
|
3
|
+
#
|
|
4
|
+
# The body re-proves every committed mutant's kill-power and costs ~28 minutes.
|
|
5
|
+
# Across 549 CI runs it produced five reds, and replaying the real push ranges
|
|
6
|
+
# shows this filter would have run the body for all five (#99 stage-2 §1). So a
|
|
7
|
+
# push that cannot have touched the qualification surface skips it -- and the
|
|
8
|
+
# surface is READ FROM THE MANIFESTS, never copied into a list that can drift.
|
|
9
|
+
#
|
|
10
|
+
# TWO ENTRYPOINTS, ONE MATCHER:
|
|
11
|
+
# ci-qualify-decide.sh <before-sha> <head-sha> # CI and replay: a git range
|
|
12
|
+
# ci-qualify-decide.sh --files-from <file|-> # a literal changed-file list
|
|
13
|
+
# Both print `run_body=true|false` on stdout and their reasoning on stderr.
|
|
14
|
+
#
|
|
15
|
+
# Pure local git plus python3 for the manifest read: no gh, no network, so the
|
|
16
|
+
# replay fixture gate runs offline and exercises this exact code.
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
20
|
+
|
|
21
|
+
# Fail-open is numbered so a log line says WHICH one fired. Every one of these
|
|
22
|
+
# means "we cannot compute an honest diff", and the honest answer to that is to
|
|
23
|
+
# run the body, never to skip it.
|
|
24
|
+
fail_open() {
|
|
25
|
+
echo "ci-qualify-decide: fail-open $1 ($2) -> run_body=true" >&2
|
|
26
|
+
echo "run_body=true"
|
|
27
|
+
exit 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
collect_paths_from_manifests() {
|
|
31
|
+
python3 - "$REPO_DIR" <<'PY'
|
|
32
|
+
import glob
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
repo = sys.argv[1]
|
|
38
|
+
out = set()
|
|
39
|
+
for path in sorted(glob.glob(os.path.join(repo, "scripts/mutants/*.json"))):
|
|
40
|
+
with open(path, encoding="utf-8") as handle:
|
|
41
|
+
manifest = json.load(handle)
|
|
42
|
+
for mutant in manifest.get("mutants", []):
|
|
43
|
+
subject = mutant.get("subject")
|
|
44
|
+
if subject:
|
|
45
|
+
out.add(subject)
|
|
46
|
+
signature_source = mutant.get("signatureSource")
|
|
47
|
+
if signature_source:
|
|
48
|
+
out.add(signature_source)
|
|
49
|
+
for entry in sorted(out):
|
|
50
|
+
print(entry)
|
|
51
|
+
PY
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# A changed path matters when it is a mutant's subject or signature source, or
|
|
55
|
+
# when it sits in the machinery that decides what those mutants prove.
|
|
56
|
+
matches_qualification_surface() {
|
|
57
|
+
local file="$1"
|
|
58
|
+
local subject
|
|
59
|
+
while IFS= read -r subject; do
|
|
60
|
+
[ "$file" = "$subject" ] && { echo "manifest-subject"; return 0; }
|
|
61
|
+
done <<< "$MANIFEST_PATHS"
|
|
62
|
+
case "$file" in
|
|
63
|
+
scripts/mutants/*) echo "glob:scripts/mutants/**"; return 0 ;;
|
|
64
|
+
scripts/check-*) echo "glob:scripts/check-*"; return 0 ;;
|
|
65
|
+
scripts/lib/*) echo "glob:scripts/lib/**"; return 0 ;;
|
|
66
|
+
# The replay fixture is the independent oracle input cell 8e reads. Once a
|
|
67
|
+
# claim rests on it, changing it changes what the body is qualified against.
|
|
68
|
+
scripts/fixtures/*) echo "glob:scripts/fixtures/**"; return 0 ;;
|
|
69
|
+
.github/workflows/*) echo "glob:.github/workflows/**"; return 0 ;;
|
|
70
|
+
run.sh) echo "exact:run.sh"; return 0 ;;
|
|
71
|
+
package.json) echo "exact:package.json"; return 0 ;;
|
|
72
|
+
# The decider itself: change the filter and the body runs, so a filter
|
|
73
|
+
# that stopped covering something cannot hide behind its own change.
|
|
74
|
+
scripts/ci-qualify-decide.sh) echo "exact:scripts/ci-qualify-decide.sh"; return 0 ;;
|
|
75
|
+
esac
|
|
76
|
+
return 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
decide_from_files() {
|
|
80
|
+
local files="$1"
|
|
81
|
+
local count=0
|
|
82
|
+
local hits=0
|
|
83
|
+
local file rule
|
|
84
|
+
while IFS= read -r file; do
|
|
85
|
+
[ -n "$file" ] || continue
|
|
86
|
+
count=$((count + 1))
|
|
87
|
+
if rule="$(matches_qualification_surface "$file")"; then
|
|
88
|
+
hits=$((hits + 1))
|
|
89
|
+
[ "$hits" -le 5 ] && echo "ci-qualify-decide: hit $file ($rule)" >&2
|
|
90
|
+
fi
|
|
91
|
+
done <<< "$files"
|
|
92
|
+
if [ "$hits" -gt 0 ]; then
|
|
93
|
+
echo "ci-qualify-decide: $hits of $count changed files touch the qualification surface -> run_body=true" >&2
|
|
94
|
+
echo "run_body=true"
|
|
95
|
+
else
|
|
96
|
+
echo "ci-qualify-decide: no qualification-surface path among $count changed files -> run_body=false" >&2
|
|
97
|
+
echo "run_body=false"
|
|
98
|
+
fi
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
MANIFEST_PATHS="$(collect_paths_from_manifests)"
|
|
102
|
+
[ -n "$MANIFEST_PATHS" ] || { echo "ABORT: no mutant manifests read from $REPO_DIR/scripts/mutants" >&2; exit 1; }
|
|
103
|
+
|
|
104
|
+
if [ "${1:-}" = "--files-from" ]; then
|
|
105
|
+
SRC="${2:-}"
|
|
106
|
+
[ -n "$SRC" ] || { echo "ABORT: --files-from needs a path or -" >&2; exit 1; }
|
|
107
|
+
if [ "$SRC" = "-" ]; then FILES="$(cat)"; else FILES="$(cat "$SRC")"; fi
|
|
108
|
+
decide_from_files "$FILES"
|
|
109
|
+
exit 0
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
BEFORE="${1:-}"
|
|
113
|
+
HEAD_SHA="${2:-}"
|
|
114
|
+
EVENT="${CI_EVENT_NAME:-push}"
|
|
115
|
+
FORCED="${CI_FORCED:-false}"
|
|
116
|
+
|
|
117
|
+
# 3. A human or the schedule asked for this run; there is no push range to read.
|
|
118
|
+
# `qualify` is what makes the dispatch an explicit request for the BODY. A
|
|
119
|
+
# dispatch WITHOUT it is a floor-only rerun, so it must not silently satisfy
|
|
120
|
+
# the release oracle's fourth axis -- the input would be dead configuration if
|
|
121
|
+
# every dispatch ran the body regardless of its value.
|
|
122
|
+
case "$EVENT" in
|
|
123
|
+
workflow_dispatch)
|
|
124
|
+
[ "${CI_QUALIFY:-false}" = "true" ] && fail_open 3 "event=workflow_dispatch with qualify=true asked for the body"
|
|
125
|
+
echo "ci-qualify-decide: dispatch without qualify=true is a floor-only rerun; the release oracle will still refuse this SHA until the body runs -> run_body=false" >&2
|
|
126
|
+
echo "run_body=false"
|
|
127
|
+
exit 0
|
|
128
|
+
;;
|
|
129
|
+
schedule) fail_open 3 "event=schedule runs the body unconditionally (drift ceiling)" ;;
|
|
130
|
+
# 5. A pull_request event carries no `before`, and GitHub compares it
|
|
131
|
+
# three-dot. Narrowing PRs would need a second comparison rule for an
|
|
132
|
+
# event this repo barely uses, so PRs always run the body.
|
|
133
|
+
pull_request) fail_open 5 "event=pull_request has no two-dot push range" ;;
|
|
134
|
+
esac
|
|
135
|
+
|
|
136
|
+
[ -n "$BEFORE" ] && [ -n "$HEAD_SHA" ] || { echo "ABORT: usage: $0 <before-sha> <head-sha>" >&2; exit 1; }
|
|
137
|
+
|
|
138
|
+
# 1. A new branch's first push: GitHub sends all-zeros, and its own documented
|
|
139
|
+
# base ("the parent of the ancestor of the deepest commit pushed") is not
|
|
140
|
+
# what a two-dot diff from zeros would give.
|
|
141
|
+
case "$BEFORE" in
|
|
142
|
+
*[!0]*) ;;
|
|
143
|
+
*) fail_open 1 "before=$BEFORE is the all-zero SHA (new branch)" ;;
|
|
144
|
+
esac
|
|
145
|
+
|
|
146
|
+
# 2. Force push. What `before` even means here is UNDOCUMENTED (#99 stage-2,
|
|
147
|
+
# "measured not"): if it is the old head, the two-dot diff runs backwards and
|
|
148
|
+
# the verdict can invert. Never guess -- run the body.
|
|
149
|
+
[ "$FORCED" = "true" ] && fail_open 2 "the push was forced; its two-dot base is undocumented"
|
|
150
|
+
|
|
151
|
+
# 4. The range is unreadable on this checkout -- a shallow clone, or an object
|
|
152
|
+
# the remote no longer has (which is also how a force push's old base
|
|
153
|
+
# disappears, so 2 and 4 can both be true and each still names itself).
|
|
154
|
+
git -C "$REPO_DIR" cat-file -e "$BEFORE^{commit}" 2>/dev/null || fail_open 4 "before=$BEFORE is not a commit object in this checkout"
|
|
155
|
+
git -C "$REPO_DIR" cat-file -e "$HEAD_SHA^{commit}" 2>/dev/null || fail_open 4 "head=$HEAD_SHA is not a commit object in this checkout"
|
|
156
|
+
|
|
157
|
+
CHANGED="$(git -C "$REPO_DIR" diff --name-only "$BEFORE".."$HEAD_SHA" 2>/dev/null)" || fail_open 4 "git diff $BEFORE..$HEAD_SHA failed"
|
|
158
|
+
|
|
159
|
+
decide_from_files "$CHANGED"
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": "Every qualification RED this repo's CI has ever produced (#99 stage-2 SS1), with the file list of the two-dot push range GitHub actually compared. `before` is the head of the previous ci.yml run on the same branch. `files` was measured from this clone's history on 2026-09-06 and is committed BECAUSE history is not readable everywhere the gate runs: check-gate-qualification executes gates inside a snapshot with its own fresh git baseline, where these commits do not exist. `tipOnlyFiles` is the same push read the wrong way -- the tip commit alone -- which is how four of these five once looked like docs-only pushes; it is kept as the record of the misreading, not as an input.",
|
|
3
|
+
"runs": [
|
|
4
|
+
{
|
|
5
|
+
"runId": "32421631735",
|
|
6
|
+
"branch": "issue-82-copilot-citizen",
|
|
7
|
+
"before": "1da40c9994d5",
|
|
8
|
+
"head": "dff731d415d5",
|
|
9
|
+
"diedAt": "manifest validation (head)",
|
|
10
|
+
"files": [
|
|
11
|
+
"DELIVERY.md",
|
|
12
|
+
"NEXT--issue-82-copilot-citizen.md",
|
|
13
|
+
"ROADMAP.md",
|
|
14
|
+
"mcp/entwurf-bridge/tsconfig.build.json",
|
|
15
|
+
"package.json",
|
|
16
|
+
"pi-extensions/lib/meta-session.ts",
|
|
17
|
+
"pi-extensions/meta-bridge-hook-copilot.ts",
|
|
18
|
+
"pi/entwurf-capabilities.json",
|
|
19
|
+
"pi/meta-bridge-copilot/.claude-plugin/marketplace.json",
|
|
20
|
+
"pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json",
|
|
21
|
+
"pi/meta-bridge-copilot/entwurf-meta-receive-copilot/hooks/hooks.json",
|
|
22
|
+
"pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh",
|
|
23
|
+
"run.sh",
|
|
24
|
+
"scripts/check-copilot-birth-hook.ts",
|
|
25
|
+
"scripts/check-entwurf-capabilities.ts",
|
|
26
|
+
"scripts/check-gate-qualification.ts",
|
|
27
|
+
"scripts/check-meta-doctor-oracle.sh",
|
|
28
|
+
"scripts/check-meta-manifest-schema.py",
|
|
29
|
+
"scripts/copilot-bridge-doctor.sh",
|
|
30
|
+
"scripts/copilot-bridge-install.sh",
|
|
31
|
+
"scripts/meta-bridge-hook-log.sh",
|
|
32
|
+
"scripts/mutants/copilot-birth.json",
|
|
33
|
+
"scripts/tsconfig.json",
|
|
34
|
+
"tsconfig.json"
|
|
35
|
+
],
|
|
36
|
+
"tipOnlyFiles": ["NEXT--issue-82-copilot-citizen.md"]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"runId": "32450955048",
|
|
40
|
+
"branch": "issue-82-copilot-citizen",
|
|
41
|
+
"before": "0aed67df2f22",
|
|
42
|
+
"head": "88d0641b001e",
|
|
43
|
+
"diedAt": "lane inventory (head)",
|
|
44
|
+
"files": [
|
|
45
|
+
"AGENTS.md",
|
|
46
|
+
"NEXT--issue-82-copilot-citizen.md",
|
|
47
|
+
"VERIFY.md",
|
|
48
|
+
"docs/adding-a-harness.md",
|
|
49
|
+
"docs/external-mcp-host.md",
|
|
50
|
+
"package.json",
|
|
51
|
+
"pi-extensions/lib/meta-sender-identity.ts",
|
|
52
|
+
"pi-extensions/lib/meta-session.ts",
|
|
53
|
+
"pi-extensions/meta-bridge-hook-copilot.ts",
|
|
54
|
+
"pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json",
|
|
55
|
+
"pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh",
|
|
56
|
+
"run.sh",
|
|
57
|
+
"scripts/check-copilot-birth-hook.ts",
|
|
58
|
+
"scripts/check-meta-session.ts",
|
|
59
|
+
"scripts/copilot-bridge-doctor.sh",
|
|
60
|
+
"scripts/copilot-mcp-bridge.sh",
|
|
61
|
+
"scripts/copilot-mcp-config.py",
|
|
62
|
+
"scripts/copilot-statusline-bridge.sh",
|
|
63
|
+
"scripts/copilot-statusline-config.py",
|
|
64
|
+
"scripts/mutants/copilot-birth.json",
|
|
65
|
+
"scripts/smoke-copilot-mcp-state.sh",
|
|
66
|
+
"scripts/smoke-copilot-statusline-state.sh"
|
|
67
|
+
],
|
|
68
|
+
"tipOnlyFiles": [
|
|
69
|
+
"AGENTS.md",
|
|
70
|
+
"NEXT--issue-82-copilot-citizen.md",
|
|
71
|
+
"docs/adding-a-harness.md",
|
|
72
|
+
"docs/external-mcp-host.md",
|
|
73
|
+
"pi-extensions/lib/meta-sender-identity.ts",
|
|
74
|
+
"pi-extensions/meta-bridge-hook-copilot.ts",
|
|
75
|
+
"pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json",
|
|
76
|
+
"pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh",
|
|
77
|
+
"run.sh",
|
|
78
|
+
"scripts/check-copilot-birth-hook.ts",
|
|
79
|
+
"scripts/copilot-bridge-doctor.sh",
|
|
80
|
+
"scripts/mutants/copilot-birth.json"
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"runId": "33154263432",
|
|
85
|
+
"branch": "issue-87-omp-vendor-measurement",
|
|
86
|
+
"before": "7c414f55bd69",
|
|
87
|
+
"head": "9b70a4938fd0",
|
|
88
|
+
"diedAt": "lane inventory (head)",
|
|
89
|
+
"files": [
|
|
90
|
+
"NEXT.md",
|
|
91
|
+
"mcp/entwurf-bridge/src/index.ts",
|
|
92
|
+
"package.json",
|
|
93
|
+
"pi-extensions/lib/entwurf-self-address.ts",
|
|
94
|
+
"run.sh",
|
|
95
|
+
"scripts/check-entwurf-self-address.ts",
|
|
96
|
+
"scripts/mutants/self-address.json",
|
|
97
|
+
"scripts/omp-mcp-bridge.sh",
|
|
98
|
+
"scripts/omp-tool-surface.py",
|
|
99
|
+
"scripts/smoke-omp-mcp-state.sh"
|
|
100
|
+
],
|
|
101
|
+
"tipOnlyFiles": ["NEXT.md"]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"runId": "30991752256",
|
|
105
|
+
"branch": "mux-placement",
|
|
106
|
+
"before": "e5689808c9ba",
|
|
107
|
+
"head": "e05af7294724",
|
|
108
|
+
"diedAt": "CONTROL pre-red",
|
|
109
|
+
"files": [
|
|
110
|
+
"AGENTS.md",
|
|
111
|
+
"NEXT--mux-placement.md",
|
|
112
|
+
"README.md",
|
|
113
|
+
"ROADMAP.md",
|
|
114
|
+
"VERIFY.md",
|
|
115
|
+
"docs/mux-launch-rail.md",
|
|
116
|
+
"mcp/entwurf-bridge/src/index.ts",
|
|
117
|
+
"package.json",
|
|
118
|
+
"pi-extensions/entwurf-control.ts",
|
|
119
|
+
"pi-extensions/lib/entwurf-v2-contract.ts",
|
|
120
|
+
"pi-extensions/lib/mux-fresh-call.ts",
|
|
121
|
+
"pi-extensions/lib/mux-launch.ts",
|
|
122
|
+
"pi-extensions/lib/mux-placement.ts",
|
|
123
|
+
"run.sh",
|
|
124
|
+
"scripts/check-gate-qualification.ts",
|
|
125
|
+
"scripts/check-mux-fresh-call.ts",
|
|
126
|
+
"scripts/check-mux-launch.ts",
|
|
127
|
+
"scripts/check-release-gate-outcomes.ts",
|
|
128
|
+
"scripts/mutants/mux-fresh-call.json",
|
|
129
|
+
"scripts/smoke-mux-fresh-call-live.ts",
|
|
130
|
+
"tsconfig.json"
|
|
131
|
+
],
|
|
132
|
+
"tipOnlyFiles": ["NEXT--mux-placement.md"]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"runId": "31062287903",
|
|
136
|
+
"branch": "mux-placement",
|
|
137
|
+
"before": "e05af7294724",
|
|
138
|
+
"head": "79edfe7738a4",
|
|
139
|
+
"diedAt": "CONTROL pre-red",
|
|
140
|
+
"files": [
|
|
141
|
+
"demo/demo-baseline.sh",
|
|
142
|
+
"demo/demo.sh",
|
|
143
|
+
"scripts/lib/mutation-qualify.ts",
|
|
144
|
+
"scripts/new-session-id.ts"
|
|
145
|
+
],
|
|
146
|
+
"tipOnlyFiles": ["demo/demo-baseline.sh", "demo/demo.sh", "scripts/new-session-id.ts"]
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
"claim": "CI-FULL-FLOOR-QUALIFIED",
|
|
117
|
-
"title": "the CI check job is downgraded from the full deterministic floor back to the ≤60s everyday core — the #70 tier rename regression, where qualification still runs once on
|
|
117
|
+
"title": "the CI check job is downgraded from the full deterministic floor back to the ≤60s everyday core — the #70 tier rename regression, where qualification still runs once on the pushes the filter sends it but now certifies kill-power over a floor no candidate ships on, and the hermetic/package tiers reach machine time nowhere",
|
|
118
118
|
"subject": ".github/workflows/ci.yml",
|
|
119
119
|
"find": [" - run: pnpm run check:full"],
|
|
120
120
|
"replace": [" - run: pnpm check"],
|
|
@@ -123,6 +123,50 @@
|
|
|
123
123
|
"signature": "[QK:CI-FULL-FLOOR-QUALIFIED]",
|
|
124
124
|
"signatureSource": "scripts/check-release-gate-outcomes.ts"
|
|
125
125
|
},
|
|
126
|
+
{
|
|
127
|
+
"claim": "RELEASE-SHA-QUALIFIED-IN-CI",
|
|
128
|
+
"title": "the exact-SHA CI oracle stops requiring the qualification BODY step, so a release SHA whose body never executed is certified by three green job names",
|
|
129
|
+
"subject": ".claude/skills/entwurf-release/scripts/verify-exact-ci.sh",
|
|
130
|
+
"find": ["if qual != \"success\":"],
|
|
131
|
+
"replace": ["if False:"],
|
|
132
|
+
"gate": ["bash", "run.sh", "check-release-gate-outcomes"],
|
|
133
|
+
"timeoutSeconds": 180,
|
|
134
|
+
"signature": "[QK:RELEASE-SHA-QUALIFIED-IN-CI]",
|
|
135
|
+
"signatureSource": "scripts/check-release-gate-outcomes.ts"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"claim": "QUALIFY-FILTER-COVERS-SUBJECTS",
|
|
139
|
+
"title": "the CI qualification filter stops collecting signatureSource paths, so a change to a gate that owns a claim's signature no longer re-proves it in CI",
|
|
140
|
+
"subject": "scripts/ci-qualify-decide.sh",
|
|
141
|
+
"find": ["\t\tsignature_source = mutant.get(\"signatureSource\")"],
|
|
142
|
+
"replace": ["\t\tsignature_source = None"],
|
|
143
|
+
"gate": ["bash", "run.sh", "check-release-gate-outcomes"],
|
|
144
|
+
"timeoutSeconds": 180,
|
|
145
|
+
"signature": "[QK:QUALIFY-FILTER-COVERS-SUBJECTS]",
|
|
146
|
+
"signatureSource": "scripts/check-release-gate-outcomes.ts"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"claim": "QUALIFY-FILTER-READS-PUSH-RANGE",
|
|
150
|
+
"title": "the filter reads the tip commit instead of the two-dot push range - the exact misreading that once concluded four of the five historical qualification reds would have been skipped",
|
|
151
|
+
"subject": "scripts/ci-qualify-decide.sh",
|
|
152
|
+
"find": ["CHANGED=\"$(git -C \"$REPO_DIR\" diff --name-only \"$BEFORE\"..\"$HEAD_SHA\" 2>/dev/null)\""],
|
|
153
|
+
"replace": ["CHANGED=\"$(git -C \"$REPO_DIR\" diff --name-only \"$HEAD_SHA~1\"..\"$HEAD_SHA\" 2>/dev/null)\""],
|
|
154
|
+
"gate": ["bash", "run.sh", "check-release-gate-outcomes"],
|
|
155
|
+
"timeoutSeconds": 180,
|
|
156
|
+
"signature": "[QK:QUALIFY-FILTER-READS-PUSH-RANGE]",
|
|
157
|
+
"signatureSource": "scripts/check-release-gate-outcomes.ts"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"claim": "QUALIFY-FILTER-REPLAYS-PAST-CATCHES",
|
|
161
|
+
"title": "the matcher stops after the first changed path, so a push whose first file is outside the qualification surface decides false no matter what rides behind it - invisible to every single-file check, fatal to all five historical ranges",
|
|
162
|
+
"subject": "scripts/ci-qualify-decide.sh",
|
|
163
|
+
"find": ["\tdone <<< \"$files\""],
|
|
164
|
+
"replace": ["\tdone <<< \"${files%%$'\\n'*}\""],
|
|
165
|
+
"gate": ["bash", "run.sh", "check-release-gate-outcomes"],
|
|
166
|
+
"timeoutSeconds": 180,
|
|
167
|
+
"signature": "[QK:QUALIFY-FILTER-REPLAYS-PAST-CATCHES]",
|
|
168
|
+
"signatureSource": "scripts/check-release-gate-outcomes.ts"
|
|
169
|
+
},
|
|
126
170
|
{
|
|
127
171
|
"claim": "MUX-LIFECYCLE-IS-RELEASE-MUST",
|
|
128
172
|
"title": "the integrated mux lifecycle stays listed but bypasses run_live_step, so a missing prerequisite returns SKIP into a branch that never classifies it and the cut reads green",
|