@junghanacs/entwurf 0.18.0 → 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.
@@ -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
+ }
@@ -19,7 +19,7 @@
19
19
  "subject": "scripts/agy-bridge.sh",
20
20
  "find": [" if command_boots \"$invocation\"; then"],
21
21
  "replace": [" if true; then"],
22
- "gate": ["bash", "scripts/smoke-agy-install-state.sh"],
22
+ "gate": ["bash", "run.sh", "smoke-agy-install-state"],
23
23
  "timeoutSeconds": 300,
24
24
  "signature": "[QK:AGY-DOCTOR-BOOT-NEGATIVE]",
25
25
  "signatureSource": "scripts/smoke-agy-install-state.sh"
@@ -43,7 +43,7 @@
43
43
  "subject": "scripts/agy-bridge-config.py",
44
44
  "find": [" args = server.get(\"args\", [])"],
45
45
  "replace": [" args = []"],
46
- "gate": ["bash", "scripts/smoke-agy-install-state.sh"],
46
+ "gate": ["bash", "run.sh", "smoke-agy-install-state"],
47
47
  "timeoutSeconds": 300,
48
48
  "signature": "[QK:AGY-DOCTOR-PROBES-ARGS]",
49
49
  "signatureSource": "scripts/smoke-agy-install-state.sh"
@@ -54,7 +54,7 @@
54
54
  "subject": "scripts/agy-bridge-config.py",
55
55
  "find": [" env = server.get(\"env\", {})"],
56
56
  "replace": [" env = {}"],
57
- "gate": ["bash", "scripts/smoke-agy-install-state.sh"],
57
+ "gate": ["bash", "run.sh", "smoke-agy-install-state"],
58
58
  "timeoutSeconds": 300,
59
59
  "signature": "[QK:AGY-DOCTOR-PROBES-ENV]",
60
60
  "signatureSource": "scripts/smoke-agy-install-state.sh"
@@ -0,0 +1,28 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "gate-qualification",
4
+ "mutants": [
5
+ {
6
+ "claim": "MANIFEST-SET-INTEGRITY-REFUSED",
7
+ "title": "a committed manifest points a claim at a gate source that does not carry its token — the cross-manifest contract that makes a KILLED verdict mean anything is broken, and the run must refuse rather than certify kills whose signature nothing can attribute (the 08-20 CI catch, run 32421631735, which died in validateManifestSet in 5s)",
8
+ "subject": "scripts/mutants/probe-ordering.json",
9
+ "find": ["\"signatureSource\": \"scripts/check-probe-ordering.ts\""],
10
+ "replace": ["\"signatureSource\": \"run.sh\""],
11
+ "gate": ["bash", "run.sh", "check-gate-manifests"],
12
+ "timeoutSeconds": 120,
13
+ "signature": "[QK:MANIFEST-SET-INTEGRITY-REFUSED]",
14
+ "signatureSource": "scripts/check-gate-qualification.ts"
15
+ },
16
+ {
17
+ "claim": "LANE-INVENTORY-DECLARED",
18
+ "title": "a lane is renamed in its manifest and the declared inventory is not moved with it — the vacuous-green shape where the run reports n/n killed over whatever set happened to be on disk (the 08-21 and 08-28 CI catches, runs 32450955048 and 33154263432, both dead at the inventory assertion in under 5s)",
19
+ "subject": "scripts/mutants/capability-cache.json",
20
+ "find": ["\"lane\": \"capability-cache\","],
21
+ "replace": ["\"lane\": \"capability-cache-renamed\","],
22
+ "gate": ["bash", "run.sh", "check-gate-manifests"],
23
+ "timeoutSeconds": 120,
24
+ "signature": "[QK:LANE-INVENTORY-DECLARED]",
25
+ "signatureSource": "scripts/check-gate-qualification.ts"
26
+ }
27
+ ]
28
+ }
@@ -4,14 +4,25 @@
4
4
  "mutants": [
5
5
  {
6
6
  "claim": "PACK-INSTALL-PIN-MATCHER-BOUNDED",
7
- "title": "the pin-leak matcher loses its version boundary and blesses substring lookalikes — @0.84.30 reads as the pinned 0.84.3 and the gate announces a verified tree it never verified (independent review, 2026-08-25)",
7
+ "title": "the pin-leak matcher loses its version boundary and blesses substring lookalikes — @0.85.10 reads as the pinned 0.85.1 and the gate announces a verified tree it never verified (independent review, 2026-08-25)",
8
8
  "subject": "run.sh",
9
- "find": [" grep '^@earendil-works+pi-' | grep -Ev '@0\\.84\\.4(_|$)' || true"],
10
- "replace": [" grep '^@earendil-works+pi-' | grep -v '@0\\.84\\.4' || true"],
9
+ "find": [" grep '^@earendil-works+' | grep -Ev '@0\\.85\\.1(_|$)' || true"],
10
+ "replace": [" grep '^@earendil-works+' | grep -v '@0\\.85\\.1' || true"],
11
11
  "gate": ["bash", "run.sh", "check-pack-pin-matcher"],
12
12
  "timeoutSeconds": 120,
13
13
  "signature": "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED]",
14
14
  "signatureSource": "run.sh"
15
+ },
16
+ {
17
+ "claim": "PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE",
18
+ "title": "the pin-leak matcher narrows back to the pi-* families and goes blind to a non-pi member of the same runtime closure — @earendil-works/chord (new in pi 0.85.0, a runtime dependency of pi-coding-agent/pi-agent-core/pi-client/pi-protocol) floats unpinned while the gate prints a verified pin (measured 2026-09-06 on a real 0.85.1 install tree)",
19
+ "subject": "run.sh",
20
+ "find": [" grep '^@earendil-works+' | grep -Ev '@0\\.85\\.1(_|$)' || true"],
21
+ "replace": [" grep '^@earendil-works+pi-' | grep -Ev '@0\\.85\\.1(_|$)' || true"],
22
+ "gate": ["bash", "run.sh", "check-pack-pin-matcher"],
23
+ "timeoutSeconds": 120,
24
+ "signature": "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE]",
25
+ "signatureSource": "run.sh"
15
26
  }
16
27
  ]
17
28
  }
@@ -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 every push but now certifies kill-power over a floor no candidate ships on, and the hermetic/package tiers reach machine time nowhere",
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",
@@ -136,6 +180,28 @@
136
180
  "signature": "[QK:MUX-LIFECYCLE-IS-RELEASE-MUST]",
137
181
  "signatureSource": "scripts/check-release-gate-outcomes.ts"
138
182
  },
183
+ {
184
+ "claim": "MUTANT-GATES-INSIDE-FULL-FLOOR",
185
+ "title": "a gate a committed mutant names drops out of check:full — the #99 B-4 shape, where check-omp-birth-hook was reachable in the whole repo only through qualification's control-pre, so the one CI class that half has ever caught (a gate already red on a clean tree) could be found only by paying the 28-minute mutant body",
186
+ "subject": "package.json",
187
+ "find": [" && ./run.sh check-omp-birth-hook"],
188
+ "replace": [""],
189
+ "gate": ["bash", "run.sh", "check-release-gate-outcomes"],
190
+ "timeoutSeconds": 180,
191
+ "signature": "[QK:MUTANT-GATES-INSIDE-FULL-FLOOR]",
192
+ "signatureSource": "scripts/check-release-gate-outcomes.ts"
193
+ },
194
+ {
195
+ "claim": "CI-TAG-PUSH-NOT-REBUILT",
196
+ "title": "the CI push trigger loses its branch ref filter, so every release tag rebuilds a SHA its branch run already built — 66/66 historically, 28% of a release window's runner minutes, and not once a fact the branch run had not already reported",
197
+ "subject": ".github/workflows/ci.yml",
198
+ "find": [" branches: ['**']"],
199
+ "replace": [" # the branch ref filter was dropped, so a tag push rebuilds the SHA again"],
200
+ "gate": ["bash", "run.sh", "check-release-gate-outcomes"],
201
+ "timeoutSeconds": 180,
202
+ "signature": "[QK:CI-TAG-PUSH-NOT-REBUILT]",
203
+ "signatureSource": "scripts/check-release-gate-outcomes.ts"
204
+ },
139
205
  {
140
206
  "claim": "PI-DOCTOR-IS-RELEASE-MUST",
141
207
  "title": "the pi-provider boot doctor is quietly dropped from the gate — the operator's CONFIGURED bridge invocation is never booted, so a 127 launcher (the #81 relocated cmd-shim class) stays invisible until smoke-acp-bundled-mcp-live sixteen LIVE steps and real model spend later",
@@ -0,0 +1,77 @@
1
+ # raw-acp-compaction-measure — what a compacting turn looks like after claude-agent-acp 0.75.0
2
+
3
+ Measurement receipts for the ONE change in the `claude-agent-acp 0.73.0 → 0.75.1` bump that
4
+ reaches entwurf. Not a gate; nothing here runs in any check tier.
5
+
6
+ ```
7
+ LIVE=1 node --experimental-strip-types scripts/raw-acp-compaction-measure/probe.ts
8
+ ```
9
+
10
+ ## The change
11
+
12
+ `0.75.0` (#991, `f74a517`, "surface context compaction as an ACP tool lifecycle") stopped
13
+ sending compaction as assistant text and started sending it as a **synthetic tool call**.
14
+
15
+ `[읽음, ~/repos/3rd/claude-agent-acp]` at `v0.73.0 src/acp-agent.ts:3437-3457` a compaction
16
+ result produced two `agent_message_chunk` texts — `"\n\nCompacting completed."` and
17
+ `"Compacting failed<reason>"`. At `v0.75.1` those are gone; `ContextCompactionLifecycle`
18
+ (`git grep -c` in `src/`: **0** at v0.73.0 and v0.74.0, **2** at v0.75.0 and v0.75.1) drives
19
+ `tool_call` / `tool_call_update` notifications instead
20
+ (`dist/acp-agent.js:31`, `:1877`, `:2711-2742`).
21
+
22
+ **What did NOT change:** the post-compaction occupancy refresh. `v0.73.0
23
+ src/acp-agent.ts:3460+` already emitted a `usage_update` at `compact_boundary`, and 0.75.1
24
+ still does (`dist/acp-agent.js:2740-2752`) — only its source moved, from a
25
+ `getContextUsage` control request to `compact_metadata.post_tokens`. So the
26
+ `used_end`-shrinks-mid-turn case that `backend.ts:1286-1288` already names as #96's weak
27
+ floor is **not newly triggered by this bump**.
28
+
29
+ ## M1 — one live `/compact` turn, and the same notifications replayed through our mapper
30
+
31
+ `[측정 2026-09-06, oracle, claude-agent-acp 0.75.1, model claude-sonnet-5]`
32
+ `compact stopReason=end_turn`, 2 tool-lifecycle notifications. Verbatim from the probe:
33
+
34
+ ```
35
+ tool_call kind=think status=in_progress title="Compact conversation"
36
+ meta={"contextCompaction":{"version":1},"claudeCode":{"toolName":"compact"}}
37
+ tool_call_update kind=- status=failed title=null
38
+ meta={"contextCompaction":{"version":1,"error":"Not enough messages to compact."},
39
+ "claudeCode":{"toolName":"compact"}}
40
+ ```
41
+
42
+ Those exact objects, replayed through the PRODUCTION `applyAcpSessionUpdate`
43
+ (`pi-extensions/lib/acp/event-mapper.ts`) — the same function `backend.ts` calls — produce:
44
+
45
+ ```json
46
+ [{"type":"text","text":"\n[tool:start] Compact conversation\n"},
47
+ {"type":"text","text":"\n[tool:failed] Compact conversation — Compaction failed: Not enough messages to compact.\n"}]
48
+ ```
49
+
50
+ ### What this establishes
51
+
52
+ 1. **No type or contract break.** `renderToolUpdate` (`event-mapper.ts:236-270`) routes every
53
+ `tool_call`/`tool_call_update` regardless of `kind`, so a `kind: "think"` synthetic call
54
+ needs no new branch. The turn ends `end_turn`, not an error.
55
+ 2. **`_meta.contextCompaction` reaches nothing.** It is carried on the notification and
56
+ dropped by the mapper, which reads only `toolCallId` / `title` / `status` / `content` /
57
+ `rawOutput`. No accounting path sees it — `usage.acp`, `_meta.quota` and the four pi
58
+ fields are untouched by this lifecycle.
59
+ 3. **What an operator now SEES changed.** Where 0.73.0 put `Compacting completed.` in the
60
+ assistant's own text, 0.75.1 puts a `[tool:start]` / `[tool:failed]` notice pair in the
61
+ transcript — that pair is what was OBSERVED. On a compaction that succeeds the second
62
+ notice is `[tool:done]`, which is inferred from the mapper's own status branch
63
+ (`event-mapper.ts:262-266`) and not observed here; see the limits below. Either way,
64
+ that notice pair is the whole reachable delta of this bump.
65
+
66
+ ### What this does NOT establish (measured limits, stated rather than rounded off)
67
+
68
+ - **The `completed` branch was not observed.** The seeded session was too short, so the
69
+ vendor answered `"Not enough messages to compact."` and the lifecycle terminated at
70
+ `status: "failed"`. The success path — `status: "completed"` plus
71
+ `_meta.contextCompaction` carrying `trigger` / `preTokens` / `postTokens` / `durationMs`
72
+ (`f74a517` commit message; `contextCompactionMetadataFromBoundary`) — is **inferred from
73
+ source, not measured here.** In the mapper it is the same `status`-transition branch that
74
+ produced `[tool:failed]` above, one literal apart (`[tool:done]`,
75
+ `event-mapper.ts:262-266`).
76
+ - **No organic (auto-trigger) compaction was driven.** Only the explicit `/compact` command.
77
+ - One host, one model, one run.