@junghanacs/entwurf 0.16.0 → 0.17.1

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.
Files changed (48) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +337 -0
  3. package/README.md +8 -11
  4. package/VERIFY.md +8 -1
  5. package/demo/README.md +1 -1
  6. package/docs/acp-backend-rail.md +25 -14
  7. package/docs/setup-clean-host.md +24 -10
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +1 -1
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +34 -10
  10. package/package.json +10 -10
  11. package/pi-extensions/lib/acp/acp-client.ts +57 -4
  12. package/pi-extensions/lib/acp/backend-adapter.ts +78 -9
  13. package/pi-extensions/lib/acp/backend.ts +578 -18
  14. package/pi-extensions/lib/acp/claude-acp-launch.js +100 -0
  15. package/pi-extensions/lib/acp/event-mapper.ts +43 -6
  16. package/run.sh +129 -32
  17. package/scripts/check-acp-launch-namespace.ts +127 -0
  18. package/scripts/check-acp-prompt-lifecycle.ts +145 -2
  19. package/scripts/check-acp-stop-reason.ts +8 -2
  20. package/scripts/check-acp-usage-accounting.ts +1074 -0
  21. package/scripts/check-copilot-birth-hook.ts +28 -1
  22. package/scripts/check-gate-qualification.ts +4 -2
  23. package/scripts/check-omp-fresh-preflight.ts +27 -0
  24. package/scripts/check-setup-qualification.sh +40 -2
  25. package/scripts/copilot-bridge-oracle.sh +14 -6
  26. package/scripts/fake-copilot-vendor.sh +4 -2
  27. package/scripts/lib/pi-record-discovery.ts +47 -0
  28. package/scripts/mutants/acp-launch-namespace.json +34 -0
  29. package/scripts/mutants/acp-prompt-lifecycle.json +67 -2
  30. package/scripts/mutants/acp-stream-hooks.json +4 -2
  31. package/scripts/mutants/acp-usage-accounting.json +181 -0
  32. package/scripts/mutants/copilot-birth.json +3 -5
  33. package/scripts/mutants/pack-install.json +2 -2
  34. package/scripts/mutants/setup-verdict.json +35 -0
  35. package/scripts/omp-config-xdev.py +310 -0
  36. package/scripts/omp-config-xdev.sh +76 -0
  37. package/scripts/omp-tool-surface.py +61 -10
  38. package/scripts/raw-acp-child-exit-measure/README.md +285 -0
  39. package/scripts/raw-acp-child-exit-measure/acp-turn-population.py +89 -0
  40. package/scripts/raw-acp-child-exit-measure/reaper-correlation.py +47 -0
  41. package/scripts/smoke-acp-bundled-mcp-live.ts +2 -2
  42. package/scripts/smoke-acp-cortex-live.ts +2 -2
  43. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  44. package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
  45. package/scripts/smoke-acp-v2-send-live.ts +2 -2
  46. package/scripts/smoke-entwurf-v2-matrix-live.ts +60 -10
  47. package/scripts/smoke-mux-lifecycle-live.ts +46 -2
  48. package/scripts/smoke-setup-verdict.sh +48 -3
@@ -495,7 +495,7 @@ function makeFakeHost(label: string, opts: FakeOpts): FakeHost {
495
495
  ? ' "plugin list") echo "not authenticated" >&2; exit 1 ;;'
496
496
  : opts.pluginListRaw !== undefined
497
497
  ? ` "plugin list") echo "Installed plugins:"; cat ${JSON.stringify(rawList)}; exit 0 ;;`
498
- : ' "plugin list") echo "Installed plugins:"; sed "s/^/ • /;s/$/ (v$VER)/" "$STATE"; exit 0 ;;',
498
+ : ' "plugin list") echo "Live Plugins (loaded from a local marketplace directory, never copied):"; sed "s/^/ • /;s/$/ (v$VER) (enabled)/" "$STATE"; exit 0 ;;',
499
499
  ' "plugin uninstall")',
500
500
  opts.uninstallFails
501
501
  ? ' echo "boom" >&2; exit 1 ;;'
@@ -1065,6 +1065,33 @@ function writeBoundState(host: FakeHost): void {
1065
1065
  inst.status !== 0 && (inst.stderr ?? "").includes("malformed") && !existsSync(instHost.stateFile),
1066
1066
  );
1067
1067
  }
1068
+ {
1069
+ // Copilot CLI 1.0.81 (measured 2026-08-31) appends its own state token after the
1070
+ // version and an indented `from <path>` continuation line. The grammar admits that
1071
+ // ONE optional `(enabled)`/`(disabled)` token — reading it as part of the version
1072
+ // made every surface refuse a perfectly healthy host. Anything else in the tail
1073
+ // stays malformed, so the admission cannot widen into "ignore whatever follows".
1074
+ const liveRaw = `Live Plugins (loaded from a local marketplace directory, never copied):\n • ${OURS} (v${SHIPPED_VERSION}) (enabled)\n from /home/nobody/.assembled`;
1075
+ const docHost = makeFakeHost("doctor-state-token-row", { installed: [OURS], pluginListRaw: liveRaw });
1076
+ writeBoundState(docHost);
1077
+ writeFileSync(docHost.mktState, `${MKT}\t${docHost.asm}\n`);
1078
+ mkdirSync(path.join(docHost.asm, PLUGIN), { recursive: true });
1079
+ const doc = runVerb(docHost, "doctor-copilot-bridge");
1080
+ ok(
1081
+ "[QK:COPILOT-ROW-STATE-TOKEN-ADMITTED] a `(vX) (enabled)` row plus its `from` continuation line parses as the installed version — no malformed refusal on a healthy 1.0.81 host",
1082
+ doc.stdout.includes(`${OURS} (v${SHIPPED_VERSION}) is registered in Copilot`) &&
1083
+ !doc.stdout.includes("malformed") &&
1084
+ !(doc.stderr ?? "").includes("malformed"),
1085
+ );
1086
+ const foreignHost = makeFakeHost("install-foreign-tail-row", {
1087
+ pluginListRaw: ` • ${OURS} (v${SHIPPED_VERSION}) (whatever)`,
1088
+ });
1089
+ const foreign = runVerb(foreignHost, "install-copilot-bridge");
1090
+ ok(
1091
+ "an UNKNOWN trailing token is still malformed — the admission is exactly the two measured state words",
1092
+ foreign.status !== 0 && (foreign.stderr ?? "").includes("malformed") && !existsSync(foreignHost.stateFile),
1093
+ );
1094
+ }
1068
1095
  {
1069
1096
  // B defect 4: a pluginVersion carrying whitespace would be truncated by the
1070
1097
  // space-separated fact transport (`cut -d' ' -f3`) into a FABRICATED version and a
@@ -799,10 +799,12 @@ console.log(`\n[gate-qualification] self-test: ${passed} checks passed`);
799
799
  const EXPECTED_LANE_MUTANTS: Record<string, number> = {
800
800
  "acp-augment": 10,
801
801
  "acp-cortex": 12,
802
+ "acp-launch-namespace": 2,
802
803
  "acp-overlay": 1,
803
- "acp-prompt-lifecycle": 10,
804
+ "acp-prompt-lifecycle": 15,
804
805
  "acp-stop-reason": 6,
805
806
  "acp-stream-hooks": 10,
807
+ "acp-usage-accounting": 12,
806
808
  "agy-permission": 6,
807
809
  "bridge-boot-resume": 3,
808
810
  "bridge-command-boot": 9,
@@ -829,7 +831,7 @@ console.log(`\n[gate-qualification] self-test: ${passed} checks passed`);
829
831
  "resume-args": 6,
830
832
  "resume-launch-identity": 6,
831
833
  "self-address": 5,
832
- "setup-verdict": 10,
834
+ "setup-verdict": 13,
833
835
  "source-install": 2,
834
836
  "v2-surface": 7,
835
837
  "v2-visible-resume": 17,
@@ -155,6 +155,16 @@ function pythonXdev(agentDir: string): "false" | "true" | "unreadable" {
155
155
  ["a top-level xdev outside tools", "xdev: false\ntools:\n approvalMode: yolo\n"],
156
156
  ["tab indentation (not YAML for the vendor)", "tools:\n\txdev: false\n"],
157
157
  ["broken flow scalar", "tools: [oops\n"],
158
+ // The shape the VENDOR's own settings writer produces: `key:` on one line with an
159
+ // indented flow collection under it. The python leaf's block-only reader returned
160
+ // None for the WHOLE file here, so an untouched operator config classified as
161
+ // `unreadable` and doctor-omp-mcp went RED for a reason unrelated to tools.xdev
162
+ // (measured on a real host, omp 18.0.0). Agreement alone could never catch it —
163
+ // both halves collapse `unreadable` and `true` into "not false" — so the direct
164
+ // assertion below is the one that holds the reader to the vendor's own output.
165
+ ["the vendor's own writer output (empty flow map sibling)", "modelRoles: \n {}\ntools: \n xdev: false\n"],
166
+ ["a populated flow map sibling", "modelRoles: \n {default: xai/grok}\ntools: \n xdev: false\n"],
167
+ ["a flow sequence sibling", "disabledProviders: \n [openrouter, google]\ntools: \n xdev: false\n"],
158
168
  ];
159
169
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-omp-xdev-"));
160
170
  try {
@@ -173,6 +183,23 @@ function pythonXdev(agentDir: string): "false" | "true" | "unreadable" {
173
183
  pythonSaysFalse === (ts === false),
174
184
  );
175
185
  }
186
+ // Agreement is not enough on the vendor's own shapes: two readers that BOTH fail
187
+ // closed agree perfectly and still leave the doctor red on a healthy host. These
188
+ // name the answer instead of comparing the halves.
189
+ for (const [label, yaml] of CONFIGS) {
190
+ if (
191
+ !label.startsWith("the vendor's own writer output") &&
192
+ !label.startsWith("a populated flow map") &&
193
+ !label.startsWith("a flow sequence")
194
+ )
195
+ continue;
196
+ const dir = fs.mkdtempSync(path.join(root, "vendor-"));
197
+ fs.writeFileSync(path.join(dir, "config.yml"), yaml as string);
198
+ ok(
199
+ `[QK:OMP-XDEV-VENDOR-SHAPE-READABLE] the python leaf READS tools.xdev on ${label} — a flow collection elsewhere in the file is not an unreadable config`,
200
+ pythonXdev(dir) === "false",
201
+ );
202
+ }
176
203
  } finally {
177
204
  fs.rmSync(root, { recursive: true, force: true });
178
205
  }
@@ -42,11 +42,23 @@ for _f in copilot-bridge-install.sh copilot-bridge-oracle.sh copilot-mcp-bridge.
42
42
  copilot-receive-bridge.sh copilot-statusline-bridge.sh copilot-statusline-config.py; do
43
43
  cp "$REPO_DIR/scripts/$_f" "$PKG/scripts/$_f"
44
44
  done
45
+ # OMP composition surface (Cells E/F): same discipline — every omp unit script is a real
46
+ # tracked file here, because the claims are about what the composition actually reaches.
47
+ for _f in omp-bridge-install.sh omp-bridge-oracle.sh omp-mcp-bridge.sh omp-mcp-config.py \
48
+ omp-tool-surface.py omp-config-xdev.sh omp-config-xdev.py omp-receive-install.sh; do
49
+ cp "$REPO_DIR/scripts/$_f" "$PKG/scripts/$_f"
50
+ done
45
51
  cp -r "$REPO_DIR/pi" "$PKG/pi"
46
52
  cp "$REPO_DIR/pi-extensions/lib/session-id.js" "$PKG/pi-extensions/lib/session-id.js"
47
53
  printf '%s\n' '// dist stub: copied+digested by the receive installer, never executed here' \
48
54
  > "$PKG/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
49
55
  cp "$PKG/pi-extensions/lib/session-id.js" "$PKG/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js"
56
+ # The omp units select the same compiled closure in installed mode. Same stub discipline:
57
+ # the installers copy and digest these bytes, they never execute them here.
58
+ for _e in meta-bridge-omp.js meta-bridge-receive-omp.js; do
59
+ printf '%s\n' '// dist stub: copied+digested by the omp installers, never executed here' \
60
+ > "$PKG/mcp/entwurf-bridge/dist/pi-extensions/$_e"
61
+ done
50
62
 
51
63
  # Fake pnpm: resolvable (so an unconditional require_cmd would pass), but any
52
64
  # INVOCATION writes a marker and exits uniquely — the bootstrap tripwire.
@@ -56,12 +68,12 @@ printf '#!/usr/bin/env bash\necho invoked > "%s"\nexit 97\n' "$MARKER" > "$SB/bi
56
68
  chmod +x "$SB/bin/pnpm"
57
69
 
58
70
  ABSENT="$SB/definitely-absent"
59
- run_setup() { # $1=HOME-root $2=project $3=PATH $4=PI_BIN $5=COPILOT_BIN(optional, default absent) → OUT/RC
71
+ run_setup() { # $1=HOME-root $2=project $3=PATH $4=PI_BIN $5=COPILOT_BIN(opt) $6=OMP_BIN(opt) → OUT/RC
60
72
  mkdir -p "$1/.pi/agent" "$2"
61
73
  set +e
62
74
  # ONE physical line by contract: check-install-surface S5c is a line-scoped static tripwire,
63
75
  # so the sandbox env assignments must ride the same line as the run.sh drive they guard.
64
- OUT="$(HOME="$1" XDG_DATA_HOME="$1/.local/share" XDG_STATE_HOME="$1/.local/state" XDG_CACHE_HOME="$1/.cache" XDG_CONFIG_HOME="$1/.config" PI_CODING_AGENT_DIR="$1/.pi/agent" PATH="$3" PI_BIN="$4" CLAUDE_BIN="$ABSENT" AGY_BIN="$ABSENT" COPILOT_BIN="${5:-$ABSENT}" bash "$PKG/run.sh" setup "$2" 2>&1)"
76
+ OUT="$(HOME="$1" XDG_DATA_HOME="$1/.local/share" XDG_STATE_HOME="$1/.local/state" XDG_CACHE_HOME="$1/.cache" XDG_CONFIG_HOME="$1/.config" PI_CODING_AGENT_DIR="$1/.pi/agent" PATH="$3" PI_BIN="$4" CLAUDE_BIN="$ABSENT" AGY_BIN="$ABSENT" COPILOT_BIN="${5:-$ABSENT}" OMP_BIN="${6:-$ABSENT}" ENTWURF_OMP_AGENT_DIR="$1/.omp/agent" bash "$PKG/run.sh" setup "$2" 2>&1)"
65
77
  RC=$?
66
78
  set -e
67
79
  }
@@ -79,6 +91,8 @@ want "A: no auth.json.bak and credential bytes identical [QK:SETUP-CREDENTIAL-FR
79
91
  "[ ! -e '$HOME_A/.pi/agent/auth.json.bak' ] && [ \"\$(sha256sum '$HOME_A/.pi/agent/auth.json' | cut -d' ' -f1)\" = '$AUTH_SHA' ]"
80
92
  want "A: an absent copilot is one zero-state SKIP — no unit composed, no .copilot written [QK:SETUP-COPILOT-ABSENT-SKIP]" \
81
93
  "printf '%s' \"\$OUT\" | grep -q 'copilot: SKIP' && [ ! -e '$HOME_A/.copilot' ]"
94
+ want "A: an absent omp is one zero-state SKIP — no unit composed, no .omp written [QK:SETUP-OMP-ABSENT-SKIP]" \
95
+ "printf '%s' \"\$OUT\" | grep -q 'omp: SKIP' && [ ! -e '$HOME_A/.omp' ]"
82
96
  want "A control: mode named first, pi/claude/agy SKIP, bins PASS, core FAIL, NON-GREEN summary" \
83
97
  "printf '%s' \"\$OUT\" | head -n 1 | grep -q 'mode: installed package' && printf '%s' \"\$OUT\" | grep -q 'pi: SKIP' && printf '%s' \"\$OUT\" | grep -q 'claude: SKIP' && printf '%s' \"\$OUT\" | grep -q 'agy: SKIP' && printf '%s' \"\$OUT\" | grep -q 'bins: PASS' && printf '%s' \"\$OUT\" | grep -q 'core: FAIL' && printf '%s' \"\$OUT\" | grep -q 'NON-GREEN'"
84
98
 
@@ -122,5 +136,29 @@ want "D: the failing-vendor birth is a named FAIL, never a cosmetic PASS [QK:SET
122
136
  want "D control: detected copilot never reads SKIP, and the summary names copilot-birth NON-GREEN" \
123
137
  "! printf '%s' \"\$OUT\" | grep -q 'copilot: SKIP' && printf '%s' \"\$OUT\" | grep -q 'NON-GREEN' && printf '%s' \"\$OUT\" | grep -q 'copilot-birth'"
124
138
 
139
+ # ── Cell E: omp PRESENT — the four units compose, and the SETTING is a real writer ──
140
+ # The omp unit scripts never spawn the vendor (they probe `omp` on PATH and write into the
141
+ # agent dir), so a stub binary is a faithful presence pin. Two claims live here: the
142
+ # composition reaches omp at all, and the tools.xdev writer refuses an EXPLICIT operator
143
+ # `true` by name rather than overwriting a decision it disagrees with.
144
+ STUB_OMP="$SB/stub-omp"; mkdir -p "$STUB_OMP"
145
+ printf '#!/usr/bin/env bash\necho "omp/18.0.0"\n' > "$STUB_OMP/omp"
146
+ chmod +x "$STUB_OMP/omp"
147
+ run_setup "$SB/home-e" "$SB/proj-e" "$STUB_OMP:$SB/bin:$PATH" "$ABSENT" "" "$STUB_OMP/omp"
148
+ # One assertion on purpose: the four PASS rows AND the artifacts behind them. Split in two,
149
+ # the row half alone passes a composition that reports PASS without running the unit.
150
+ want "E: a detected omp composes all four units — own rows, each backed by its ARTIFACT rather than an exit code [QK:SETUP-OMP-INDEPENDENT]" \
151
+ "printf '%s' \"\$OUT\" | grep -q 'omp-birth: PASS' && printf '%s' \"\$OUT\" | grep -q 'omp-mcp: PASS' && printf '%s' \"\$OUT\" | grep -q 'omp-config: PASS' && printf '%s' \"\$OUT\" | grep -q 'omp-receive: PASS' && ! printf '%s' \"\$OUT\" | grep -q 'omp: SKIP' && [ -d '$SB/home-e/.omp/agent/extensions/entwurf-meta-omp' ] && [ -d '$SB/home-e/.omp/agent/extensions/entwurf-receive-omp' ] && [ -f '$SB/home-e/.omp/agent/mcp.json' ]"
152
+ want "E control: the setting reached the config the vendor reads (effective xdev-off)" \
153
+ "[ \"\$(python3 '$PKG/scripts/omp-tool-surface.py' '$SB/home-e/.omp/agent' | awk '/^verdict /{print \$2}')\" = 'xdev-off' ]"
154
+ # Now the disagreement branch: an operator who wrote xdev: true explicitly owns that value.
155
+ mkdir -p "$SB/home-f/.omp/agent"
156
+ printf 'tools: \n xdev: true\n' > "$SB/home-f/.omp/agent/config.yml"
157
+ run_setup "$SB/home-f" "$SB/proj-f" "$STUB_OMP:$SB/bin:$PATH" "$ABSENT" "" "$STUB_OMP/omp"
158
+ want "F: an EXPLICIT operator tools.xdev:true is refused by name, never overwritten [QK:SETUP-OMP-CONFIG-NO-OVERWRITE]" \
159
+ "printf '%s' \"\$OUT\" | grep -q 'omp-config: FAIL' && grep -q 'xdev: true' '$SB/home-f/.omp/agent/config.yml' && ! grep -q 'xdev: false' '$SB/home-f/.omp/agent/config.yml'"
160
+ want "F control: the disagreement is a component FAIL that leaves the other omp units composed" \
161
+ "printf '%s' \"\$OUT\" | grep -q 'omp-birth: PASS' && printf '%s' \"\$OUT\" | grep -q 'omp-receive: PASS' && printf '%s' \"\$OUT\" | grep -q 'NON-GREEN'"
162
+
125
163
  echo ""
126
164
  echo "check-setup-qualification: $PASS checks passed (mutation-attribution oracle only — behavior evidence lives in smoke-setup-verdict and check-pack-install)"
@@ -77,13 +77,20 @@ PY
77
77
  # QUALIFIED is absent, present exactly once with a parsed nonempty version, or in a
78
78
  # shape nobody may act on. rc 0 prints `absent` or `one <version>`; rc 1 with the
79
79
  # reason on stderr for a MALFORMED exact row (claims our qualified id but does not
80
- # parse as `<qualified> (v<nonempty>)`) or MULTIPLE exact rows. Longer tokens that
81
- # merely contain the qualified id remain foreign and are ignored, same as above.
80
+ # parse as `<qualified> (v<nonempty>)`, optionally followed by the vendor's own
81
+ # `(enabled)`/`(disabled)` state token measured on Copilot CLI 1.0.81) or MULTIPLE
82
+ # exact rows. Longer tokens that merely contain the qualified id remain foreign and
83
+ # are ignored, same as above.
82
84
  copilot_exact_row_version() {
83
85
  local list_text="$1" qualified="$2"
84
86
  LIST_TEXT_ENV="$list_text" QUALIFIED_ENV="$qualified" python3 - <<'PY'
85
- import os, sys
87
+ import os, re, sys
86
88
  qualified = os.environ["QUALIFIED_ENV"]
89
+ # The measured row tail. Copilot CLI 1.0.81 appends its own state token after the
90
+ # version (`... (v0.1.0) (enabled)`), so the grammar carries that OPTIONAL trailing
91
+ # `(enabled)`/`(disabled)` explicitly instead of reading it as a garbled version.
92
+ # Anything else in the tail is still malformed.
93
+ ROW_TAIL = re.compile(r"\(v(?P<version>[^()]*)\)(?:[ \t]+\((?:enabled|disabled)\))?\Z")
87
94
  versions = []
88
95
  for raw in os.environ["LIST_TEXT_ENV"].splitlines():
89
96
  row = raw.strip()
@@ -100,10 +107,11 @@ for raw in os.environ["LIST_TEXT_ENV"].splitlines():
100
107
  if not parts or parts[0] != qualified:
101
108
  continue # foreign row, including longer ids that merely contain ours
102
109
  rest = parts[1] if len(parts) == 2 else ""
103
- if not (rest.startswith("(v") and rest.endswith(")")):
104
- print(f"malformed exact row for {qualified}: {row!r} does not parse as '<qualified> (v<version>)'", file=sys.stderr)
110
+ match = ROW_TAIL.fullmatch(rest)
111
+ if match is None:
112
+ print(f"malformed exact row for {qualified}: {row!r} does not parse as '<qualified> (v<version>)' with an optional '(enabled)'/'(disabled)' state token", file=sys.stderr)
105
113
  sys.exit(1)
106
- version = rest[2:-1]
114
+ version = match.group("version")
107
115
  if not version:
108
116
  print(f"malformed exact row for {qualified}: empty version in {row!r}", file=sys.stderr)
109
117
  sys.exit(1)
@@ -4,7 +4,9 @@
4
4
  # cannot drift: smoke-setup-verdict's copilot-present cell and check-pack-install's
5
5
  # installed copilot-present consumer row. The answer shapes are the MEASURED CLI's
6
6
  # (copilot 1.0.80, 2026-08-27) — the same shapes check-copilot-birth-hook.ts bakes
7
- # into its own TS fake: `plugin list` prints qualified ids with a `(vX)` suffix,
7
+ # into its own TS fake: `plugin list` prints qualified ids with a `(vX)` suffix
8
+ # followed by the vendor's `(enabled)` state token and an indented `from <path>`
9
+ # continuation line (measured on copilot 1.0.81, 2026-08-31),
8
10
  # `plugin marketplace list` prints `<name> (Local: <abs path>)`, and `--force`
9
11
  # anywhere is refused loudly (the real `marketplace remove --force` uninstalls that
10
12
  # marketplace's plugins as a side effect; no entwurf surface may reach for it).
@@ -51,7 +53,7 @@ case "\$1 \$2 \$3" in
51
53
  "plugin marketplace remove") awk -F"\t" -v n="\$4" '\$1 != n' "\$MKTS" > "\$MKTS.tmp"; mv "\$MKTS.tmp" "\$MKTS"; exit 0 ;;
52
54
  esac
53
55
  case "\$1 \$2" in
54
- "plugin list") echo "Installed plugins:"; sed "s/^//;s/\$/ (v\$VER)/" "\$STATE"; exit 0 ;;
56
+ "plugin list") echo "Live Plugins (loaded from a local marketplace directory, never copied):"; while read -r id; do [ -n "\$id" ] && { echo "\$id (v\$VER) (enabled)"; echo " from $dir"; }; done < "\$STATE"; exit 0 ;;
55
57
  "plugin uninstall") grep -Fvx "\$3" "\$STATE" > "\$STATE.tmp"; mv "\$STATE.tmp" "\$STATE"; exit 0 ;;
56
58
  "plugin install") echo "\$3" >> "\$STATE"; exit 0 ;;
57
59
  esac
@@ -16,13 +16,60 @@
16
16
  * runs of the acp socket smokes).
17
17
  */
18
18
 
19
+ import { existsSync } from "node:fs";
19
20
  import * as fsp from "node:fs/promises";
21
+ import * as os from "node:os";
20
22
  import * as path from "node:path";
21
23
 
22
24
  function sleep(ms: number): Promise<void> {
23
25
  return new Promise((resolve) => setTimeout(resolve, ms));
24
26
  }
25
27
 
28
+ /**
29
+ * pi's own cross-process locks. `FileAuthStorageBackend` (pi `dist/core/auth-storage.js`) guards
30
+ * `auth.json` AND `models-store.json` with `proper-lockfile`, and every boot reads through it, so
31
+ * these two directories are the ones a killed resident can leave behind.
32
+ */
33
+ const PI_LOCK_PATHS = ["auth.json.lock", "models-store.json.lock"].map((name) =>
34
+ path.join(os.homedir(), ".pi", "agent", name),
35
+ );
36
+
37
+ /**
38
+ * The bound a LIVE smoke must give a `pi` boot — and the reason it is NOT "how long a boot takes".
39
+ *
40
+ * MEASURED 2026-09-03 on the release host, in the smokes' own spawn shape:
41
+ * - an undisturbed boot → V3 record is **1008–1212ms across 80 consecutive boots** (5.1–5.4s under
42
+ * 4× CPU oversubscription), so a bound in the tens of seconds is not about boot cost;
43
+ * - pi reads its auth/models store under a `proper-lockfile` lock whose stale window is
44
+ * `staleMs = 30_000` (pi `dist/core/auth-storage.js`), and a contender that finds the lock held
45
+ * retries for exactly that long before taking it over;
46
+ * - SIGTERM lands inside that window often enough to matter — sweeping 24 kill offsets across a
47
+ * boot left `~/.pi/agent/models-store.json.lock` orphaned once (at +375ms), because the signal
48
+ * ends the process before `proper-lockfile`'s release ever runs;
49
+ * - with such an orphan present, the very next boot measured **30_148ms** (and 1_114ms immediately
50
+ * after, once the stale takeover had cleared it).
51
+ *
52
+ * So a 30_000 bound is the single worst value available: it expires 148ms INSIDE the takeover, and
53
+ * the record lands just after the smoke has stopped looking — an empty stderr, a live child, and no
54
+ * record in any store. That is what blocked two of the three 0.17.0 `--cut` runs on C1b, while
55
+ * `smoke-entwurf-chain-live` — same two-resident dance, 45_000 — passed all three. This constant is
56
+ * that value, shared so no smoke sits on the cliff again. Raise it if pi's `staleMs` ever grows;
57
+ * it must stay strictly greater than that window plus a boot.
58
+ */
59
+ export const PI_BOOT_TIMEOUT_MS = 45_000;
60
+
61
+ /**
62
+ * Which pi locks are on disk right now, for a failure diagnostic — a boot that overran its bound
63
+ * while one of these exists overran it for a NAMED reason, not a mysterious one. Read-only: a lock
64
+ * is arbitrated by pi's own stale protocol and must never be deleted by a smoke, because a live
65
+ * holder and an orphan look identical from here.
66
+ */
67
+ export function describePiLockResidue(): string {
68
+ const present = PI_LOCK_PATHS.filter((lock) => existsSync(lock));
69
+ if (present.length === 0) return "none held (so a boot overrun here is not the pi lock-stale window)";
70
+ return `${present.join(", ")} — a boot contending with this waits out pi's ${30_000}ms stale window before taking over`;
71
+ }
72
+
26
73
  export async function waitForPiRecord(storeDir: string, timeoutMs: number, pollMs = 100): Promise<string | null> {
27
74
  const deadline = Date.now() + timeoutMs;
28
75
  while (Date.now() < deadline) {
@@ -0,0 +1,34 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "acp-launch-namespace",
4
+ "mutants": [
5
+ {
6
+ "claim": "CLAUDE-LAUNCH-IS-NAMESPACED",
7
+ "title": "the default launch names the vendor bin directly again — the child re-enters the process-name space a janitor for another harness scans by substring, which is the whole of #72",
8
+ "subject": "pi-extensions/lib/acp/backend-adapter.ts",
9
+ "find": ["\tconst launcher = fileURLToPath(new URL(\"./claude-acp-launch.js\", import.meta.url));"],
10
+ "replace": ["\tconst launcher = require.resolve(\"@agentclientprotocol/claude-agent-acp/dist/index.js\");"],
11
+ "gate": ["bash", "run.sh", "check-acp-launch-namespace"],
12
+ "timeoutSeconds": 240,
13
+ "signature": "[QK:CLAUDE-LAUNCH-IS-NAMESPACED]",
14
+ "signatureSource": "scripts/check-acp-launch-namespace.ts"
15
+ },
16
+ {
17
+ "claim": "CLAUDE-LAUNCH-IS-TRANSPARENT",
18
+ "title": "the launcher consumes an argv slot of its own — the vendor's argv.slice(1) self-reinvocation and its own flags stop working behind a name split that still looks correct",
19
+ "subject": "pi-extensions/lib/acp/claude-acp-launch.js",
20
+ "find": ["\tawait import(pathToFileURL(join(dirname(pkgJsonPath), binPath)).href);"],
21
+ "replace": [
22
+ "\tif (process.argv.includes(\"--version\")) {",
23
+ "\t\tconsole.log(\"entwurf launcher\");",
24
+ "\t\tprocess.exit(0);",
25
+ "\t}",
26
+ "\tawait import(pathToFileURL(join(dirname(pkgJsonPath), binPath)).href);"
27
+ ],
28
+ "gate": ["bash", "run.sh", "check-acp-launch-namespace"],
29
+ "timeoutSeconds": 240,
30
+ "signature": "[QK:CLAUDE-LAUNCH-IS-TRANSPARENT]",
31
+ "signatureSource": "scripts/check-acp-launch-namespace.ts"
32
+ }
33
+ ]
34
+ }
@@ -54,8 +54,8 @@
54
54
  "claim": "REUSE-CARRIES-CHILD-DIAGNOSTICS",
55
55
  "title": "the reuse path drops the child's stderr tail again — exactly the live sonnet failure that showed only \"ACP connection closed\"",
56
56
  "subject": "pi-extensions/lib/acp/backend.ts",
57
- "find": ["\t\t\tfinishError(err, aborted, session.stderrTail, lifecycle);"],
58
- "replace": ["\t\t\tfinishError(err, aborted, undefined, lifecycle);"],
57
+ "find": ["\t\t\tfinishError(err, aborted, session.stderrTail, lifecycle, session.launchObservation);"],
58
+ "replace": ["\t\t\tfinishError(err, aborted, undefined, lifecycle, session.launchObservation);"],
59
59
  "gate": ["bash", "run.sh", "check-acp-prompt-lifecycle"],
60
60
  "timeoutSeconds": 180,
61
61
  "signature": "[QK:REUSE-CARRIES-CHILD-DIAGNOSTICS]",
@@ -117,6 +117,71 @@
117
117
  "timeoutSeconds": 180,
118
118
  "signature": "[QK:CHILD-END-SILENCE-BOUNDED]",
119
119
  "signatureSource": "scripts/check-acp-prompt-lifecycle.ts"
120
+ },
121
+ {
122
+ "claim": "LAUNCH-SIGNAL-EVIDENCE-STRUCTURED",
123
+ "title": "the caught-signal observation is dropped from the report — an externally killed child again reads as a clean vendor exit, the exact ambiguity #72 cost three diagnosis passes",
124
+ "subject": "pi-extensions/lib/acp/backend.ts",
125
+ "find": ["\t\tconst diagnosed = observed ? `${withLifecycle}\\n${observed}` : withLifecycle;"],
126
+ "replace": ["\t\tconst diagnosed = withLifecycle;"],
127
+ "gate": ["bash", "run.sh", "check-acp-prompt-lifecycle"],
128
+ "timeoutSeconds": 180,
129
+ "signature": "[QK:LAUNCH-SIGNAL-EVIDENCE-STRUCTURED]",
130
+ "signatureSource": "scripts/check-acp-prompt-lifecycle.ts"
131
+ },
132
+ {
133
+ "claim": "LAUNCH-FRAME-NOT-IN-TAIL",
134
+ "title": "the launcher's control frame is echoed into the vendor stderr tail — entwurf's own fact is laundered back into vendor evidence",
135
+ "subject": "pi-extensions/lib/acp/backend.ts",
136
+ "find": ["\t\t\tspawned.stderr.on(\"data\", (c: Buffer) => consumeStderr.write(c.toString()));"],
137
+ "replace": [
138
+ "\t\t\tspawned.stderr.on(\"data\", (c: Buffer) => {",
139
+ "\t\t\t\tconsumeStderr.write(c.toString());",
140
+ "\t\t\t\tstderrTail.push(c.toString());",
141
+ "\t\t\t});"
142
+ ],
143
+ "gate": ["bash", "run.sh", "check-acp-prompt-lifecycle"],
144
+ "timeoutSeconds": 180,
145
+ "signature": "[QK:LAUNCH-FRAME-NOT-IN-TAIL]",
146
+ "signatureSource": "scripts/check-acp-prompt-lifecycle.ts"
147
+ },
148
+ {
149
+ "claim": "LAUNCH-SIGNAL-EXACT-FRAME-ONLY",
150
+ "title": "the frame is matched by prefix without its fixed enum — vendor prose can forge an entwurf-owned observation",
151
+ "subject": "pi-extensions/lib/acp/backend.ts",
152
+ "find": [
153
+ "\t\t\t\tif (",
154
+ "\t\t\t\t\tline.startsWith(LAUNCH_SIGNAL_FRAME_PREFIX) &&",
155
+ "\t\t\t\t\tLAUNCH_SIGNAL_FRAME_VALUES.has(line.slice(LAUNCH_SIGNAL_FRAME_PREFIX.length))",
156
+ "\t\t\t\t) {"
157
+ ],
158
+ "replace": ["\t\t\t\tif (line.includes(LAUNCH_SIGNAL_FRAME_PREFIX)) {"],
159
+ "gate": ["bash", "run.sh", "check-acp-prompt-lifecycle"],
160
+ "timeoutSeconds": 180,
161
+ "signature": "[QK:LAUNCH-SIGNAL-EXACT-FRAME-ONLY]",
162
+ "signatureSource": "scripts/check-acp-prompt-lifecycle.ts"
163
+ },
164
+ {
165
+ "claim": "STDERR-TAIL-FLUSHES-PARTIAL-LINE",
166
+ "title": "the line buffer never flushes its unterminated remainder — a child that dies mid-write loses the dying words the stderr tail exists to carry",
167
+ "subject": "pi-extensions/lib/acp/backend.ts",
168
+ "find": ["\t\t\tspawned.stderr.once?.(\"close\", () => consumeStderr.flush());"],
169
+ "replace": ["\t\t\tvoid consumeStderr;"],
170
+ "gate": ["bash", "run.sh", "check-acp-prompt-lifecycle"],
171
+ "timeoutSeconds": 180,
172
+ "signature": "[QK:STDERR-TAIL-FLUSHES-PARTIAL-LINE]",
173
+ "signatureSource": "scripts/check-acp-prompt-lifecycle.ts"
174
+ },
175
+ {
176
+ "claim": "LAUNCH-FRAME-SPANS-CHUNKS",
177
+ "title": "the frame is matched per read instead of per line — a frame split across a chunk boundary is missed, and the external kill goes unreported exactly as it did in the field",
178
+ "subject": "pi-extensions/lib/acp/backend.ts",
179
+ "find": ["\t\t\theld += chunk;"],
180
+ "replace": ["\t\t\theld = chunk;"],
181
+ "gate": ["bash", "run.sh", "check-acp-prompt-lifecycle"],
182
+ "timeoutSeconds": 180,
183
+ "signature": "[QK:LAUNCH-FRAME-SPANS-CHUNKS]",
184
+ "signatureSource": "scripts/check-acp-prompt-lifecycle.ts"
120
185
  }
121
186
  ]
122
187
  }
@@ -144,9 +144,11 @@
144
144
  "claim": "ACPHOOK-ONRESPONSE-NEVER-CALLED",
145
145
  "title": "someone 'completes' the hook contract by fabricating an HTTP 200 response on turn success — exactly the false evidence the exemption exists to forbid",
146
146
  "subject": "pi-extensions/lib/acp/backend.ts",
147
- "find": ["\tfunction finishSuccess(promptResult: { stopReason?: string }): void {"],
147
+ "find": [
148
+ "\tfunction finishSuccess(adapter: AcpBackendAdapter, session: BridgeSession, promptResult: AcpPromptResponse): void {"
149
+ ],
148
150
  "replace": [
149
- "\tfunction finishSuccess(promptResult: { stopReason?: string }): void {",
151
+ "\tfunction finishSuccess(adapter: AcpBackendAdapter, session: BridgeSession, promptResult: AcpPromptResponse): void {",
150
152
  "\t\tvoid options?.onResponse?.({ status: 200, headers: {} }, model);"
151
153
  ],
152
154
  "gate": ["bash", "run.sh", "check-acp-stream-hooks"],
@@ -0,0 +1,181 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "acp-usage-accounting",
4
+ "mutants": [
5
+ {
6
+ "claim": "ACP-TURN-AGGREGATE-NOT-PROJECTED",
7
+ "title": "the turn's ROUND-TRIP AGGREGATE is projected back onto pi's four per-request usage fields — re-plants the exact 2026-09-02 defect: pi's own isContextOverflow then read 42 + 4,185,084 against a 1,000,000 window and compacted a live session whose context was 223,516",
8
+ "subject": "pi-extensions/lib/acp/backend.ts",
9
+ "find": ["\t\tsealTurnUsage(adapter, session, promptResult);"],
10
+ "replace": [
11
+ "\t\tsealTurnUsage(adapter, session, promptResult);",
12
+ "\t\tconst reprojected = promptResult?.usage;",
13
+ "\t\tif (adapter.sealsTurnAccounting && reprojected) {",
14
+ "\t\t\tstate.output.usage.input = reprojected.inputTokens ?? 0;",
15
+ "\t\t\tstate.output.usage.output = reprojected.outputTokens ?? 0;",
16
+ "\t\t\tstate.output.usage.cacheRead = reprojected.cachedReadTokens ?? 0;",
17
+ "\t\t\tstate.output.usage.cacheWrite = reprojected.cachedWriteTokens ?? 0;",
18
+ "\t\t}"
19
+ ],
20
+ "gate": ["bash", "run.sh", "check-acp-usage-accounting"],
21
+ "timeoutSeconds": 240,
22
+ "signature": "[QK:ACP-TURN-AGGREGATE-NOT-PROJECTED]",
23
+ "signatureSource": "scripts/check-acp-usage-accounting.ts"
24
+ },
25
+ {
26
+ "claim": "ACP-CONTEXT-OCCUPANCY-PRESERVED",
27
+ "title": "the seal stops writing the session's context occupancy into totalTokens — pi's calculateContextTokens then falls through to summing the four (all zero) usage fields, so the status-line percentage and auto-compaction read a long session as empty",
28
+ "subject": "pi-extensions/lib/acp/backend.ts",
29
+ "find": [
30
+ "\t\tif (typeof session.contextOccupancyTokens === \"number\") {",
31
+ "\t\t\tstate.output.usage.totalTokens = session.contextOccupancyTokens;",
32
+ "\t\t}"
33
+ ],
34
+ "replace": [
35
+ "\t\tif (typeof session.contextOccupancyTokens === \"number\") {",
36
+ "\t\t\tstate.output.usage.totalTokens = 0;",
37
+ "\t\t}"
38
+ ],
39
+ "gate": ["bash", "run.sh", "check-acp-usage-accounting"],
40
+ "timeoutSeconds": 240,
41
+ "signature": "[QK:ACP-CONTEXT-OCCUPANCY-PRESERVED]",
42
+ "signatureSource": "scripts/check-acp-usage-accounting.ts"
43
+ },
44
+ {
45
+ "claim": "ACP-CONTEXT-OCCUPANCY-CARRIED",
46
+ "title": "the seal stops carrying the session's last known context occupancy — a turn whose usage_update never arrived now emits a token partition with a zero totalTokens, so pi falls through to that partition and reads a long session as nearly empty",
47
+ "subject": "pi-extensions/lib/acp/backend.ts",
48
+ "find": [
49
+ "\t\tif (typeof occupancy === \"number\") session.contextOccupancyTokens = occupancy;",
50
+ "\t\tif (typeof session.contextOccupancyTokens === \"number\") {",
51
+ "\t\t\tstate.output.usage.totalTokens = session.contextOccupancyTokens;",
52
+ "\t\t}"
53
+ ],
54
+ "replace": ["\t\tif (typeof occupancy === \"number\") session.contextOccupancyTokens = occupancy;"],
55
+ "gate": ["bash", "run.sh", "check-acp-usage-accounting"],
56
+ "timeoutSeconds": 240,
57
+ "signature": "[QK:ACP-CONTEXT-OCCUPANCY-CARRIED]",
58
+ "signatureSource": "scripts/check-acp-usage-accounting.ts"
59
+ },
60
+ {
61
+ "claim": "ACP-TURN-COST-SUM-MATCHES-SDK",
62
+ "title": "the backend's RUNNING SESSION TOTAL is assigned to the turn cost again instead of the adjacent diff — the arithmetic that displayed a $24.261 session as $444.370",
63
+ "subject": "pi-extensions/lib/acp/backend.ts",
64
+ "find": [
65
+ "\t\t\t\tsession.sdkCumulativeCostUsd = observed;",
66
+ "\t\t\t\tstate.output.usage.cost.total = diff;",
67
+ "\t\t\t\tturnCostUsd = diff;"
68
+ ],
69
+ "replace": [
70
+ "\t\t\t\tsession.sdkCumulativeCostUsd = observed;",
71
+ "\t\t\t\tstate.output.usage.cost.total = observed;",
72
+ "\t\t\t\tturnCostUsd = diff;"
73
+ ],
74
+ "gate": ["bash", "run.sh", "check-acp-usage-accounting"],
75
+ "timeoutSeconds": 240,
76
+ "signature": "[QK:ACP-TURN-COST-SUM-MATCHES-SDK]",
77
+ "signatureSource": "scripts/check-acp-usage-accounting.ts"
78
+ },
79
+ {
80
+ "claim": "ACP-COST-BASELINE-HELD-WHEN-MISSING",
81
+ "title": "a turn with no cost notification zeroes the baseline instead of holding it — the next diff then re-attributes every dollar the session had already spent",
82
+ "subject": "pi-extensions/lib/acp/backend.ts",
83
+ "find": [
84
+ "\t\t\tstate.output.usage.cost.total = 0;",
85
+ "\t\t} else {",
86
+ "\t\t\tconst diff = observed - (session.sdkCumulativeCostUsd ?? 0);"
87
+ ],
88
+ "replace": [
89
+ "\t\t\tsession.sdkCumulativeCostUsd = 0;",
90
+ "\t\t\tstate.output.usage.cost.total = 0;",
91
+ "\t\t} else {",
92
+ "\t\t\tconst diff = observed - (session.sdkCumulativeCostUsd ?? 0);"
93
+ ],
94
+ "gate": ["bash", "run.sh", "check-acp-usage-accounting"],
95
+ "timeoutSeconds": 240,
96
+ "signature": "[QK:ACP-COST-BASELINE-HELD-WHEN-MISSING]",
97
+ "signatureSource": "scripts/check-acp-usage-accounting.ts"
98
+ },
99
+ {
100
+ "claim": "ACP-COST-RESET-NOT-SILENT",
101
+ "title": "a running total that goes backwards is absorbed silently — the turn reports a negative cost, nobody is told, and the only observation that could settle what a conversation reset does to the total is destroyed",
102
+ "subject": "pi-extensions/lib/acp/backend.ts",
103
+ "find": ["\t\tif (diff < 0) {"],
104
+ "replace": ["\t\tif (diff < -1_000_000) {"],
105
+ "gate": ["bash", "run.sh", "check-acp-usage-accounting"],
106
+ "timeoutSeconds": 240,
107
+ "signature": "[QK:ACP-COST-RESET-NOT-SILENT]",
108
+ "signatureSource": "scripts/check-acp-usage-accounting.ts"
109
+ },
110
+ {
111
+ "claim": "ACP-CORTEX-USAGE-UNTOUCHED",
112
+ "title": "the seal stops being gated on the adapter DECLARING measured semantics — claude's accounting is sealed onto a backend nobody measured, minting the same unmeasured accounting this lane exists to end",
113
+ "subject": "pi-extensions/lib/acp/backend.ts",
114
+ "find": ["\t\tif (!adapter.sealsTurnAccounting) return;"],
115
+ "replace": ["\t\tvoid adapter.sealsTurnAccounting;"],
116
+ "gate": ["bash", "run.sh", "check-acp-usage-accounting"],
117
+ "timeoutSeconds": 240,
118
+ "signature": "[QK:ACP-CORTEX-USAGE-UNTOUCHED]",
119
+ "signatureSource": "scripts/check-acp-usage-accounting.ts"
120
+ },
121
+ {
122
+ "claim": "ACP-TURN-ACCOUNTING-ATTACHED",
123
+ "title": "the vendor's four turn totals stop riding their own key — pi's four fields are 0 and nothing else carries the numbers, so the cache-effect badge has no inputs and a paid-for prefix rewrite reaches the operator as silence",
124
+ "subject": "pi-extensions/lib/acp/backend.ts",
125
+ "find": ["\t\t\t(state.output.usage as unknown as { acp?: AcpTurnAccounting }).acp = aggregate;"],
126
+ "replace": ["\t\t\tvoid aggregate;"],
127
+ "gate": ["bash", "run.sh", "check-acp-usage-accounting"],
128
+ "timeoutSeconds": 240,
129
+ "signature": "[QK:ACP-TURN-ACCOUNTING-ATTACHED]",
130
+ "signatureSource": "scripts/check-acp-usage-accounting.ts"
131
+ },
132
+ {
133
+ "claim": "ACP-CACHE-REBILL-REPORTED",
134
+ "title": "the re-billed-prefix bound drops its ΣcacheWrite term, so a full prefix rewrite after an idle gap no longer clears the notice floor — the operator runs on a cache-effect badge while having already paid to rewrite the whole prefix",
135
+ "subject": "pi-extensions/lib/acp/backend.ts",
136
+ "find": [
137
+ "\t\t\t? (priorOccupancy as number) - (priorIoSum as number) - Math.max(0, (occupancy as number) - cacheWriteForBound)"
138
+ ],
139
+ "replace": ["\t\t\t? (priorOccupancy as number) - (priorIoSum as number) - Math.max(0, occupancy as number)"],
140
+ "gate": ["bash", "run.sh", "check-acp-usage-accounting"],
141
+ "timeoutSeconds": 240,
142
+ "signature": "[QK:ACP-CACHE-REBILL-REPORTED]",
143
+ "signatureSource": "scripts/check-acp-usage-accounting.ts"
144
+ },
145
+ {
146
+ "claim": "ACP-REBILL-NEVER-EXCEEDS-WRITE",
147
+ "title": "the re-billed claim loses its physical cap, so a context shrink (organic compaction) announces a six-figure cache miss over a turn that wrote a thousand tokens — a fact the operator has no way to disbelieve",
148
+ "subject": "pi-extensions/lib/acp/backend.ts",
149
+ "find": [
150
+ "\t\tconst missLowerBound = rawBound === undefined ? undefined : Math.min(rawBound, cacheWriteForBound);"
151
+ ],
152
+ "replace": ["\t\tconst missLowerBound = rawBound;"],
153
+ "gate": ["bash", "run.sh", "check-acp-usage-accounting"],
154
+ "timeoutSeconds": 240,
155
+ "signature": "[QK:ACP-REBILL-NEVER-EXCEEDS-WRITE]",
156
+ "signatureSource": "scripts/check-acp-usage-accounting.ts"
157
+ },
158
+ {
159
+ "claim": "ACP-ACCOUNTING-PREFERS-WIDEST",
160
+ "title": "the accounting-grade model_usage rows stop being preferred, so the turn silently reports MAIN-LOOP-ONLY tokens against an all-inclusive cost denominator — the cache-effect badge understates itself exactly when compaction or a subagent ran, and nothing says so",
161
+ "subject": "pi-extensions/lib/acp/backend.ts",
162
+ "find": ["\tif (Array.isArray(rows) && rows.length > 0) {"],
163
+ "replace": ["\tif (Array.isArray(rows) && rows.length > 99) {"],
164
+ "gate": ["bash", "run.sh", "check-acp-usage-accounting"],
165
+ "timeoutSeconds": 240,
166
+ "signature": "[QK:ACP-ACCOUNTING-PREFERS-WIDEST]",
167
+ "signatureSource": "scripts/check-acp-usage-accounting.ts"
168
+ },
169
+ {
170
+ "claim": "ACP-REBILL-MAIN-LOOP-SCOPE",
171
+ "title": "the re-billed bound takes cacheWrite from the WIDE accounting rows instead of the main loop, so a warm main prefix announces a six-figure miss (and this turn's dollar figure) because an internal/compaction call wrote extra cache — mixing occupancy's scope with model_usage's",
172
+ "subject": "pi-extensions/lib/acp/backend.ts",
173
+ "find": ["\t\tconst cacheWriteForBound = mainLoop !== undefined ? mainLoop.cacheWrite : 0;"],
174
+ "replace": ["\t\tconst cacheWriteForBound = aggregate !== undefined ? aggregate.cacheWrite : 0;"],
175
+ "gate": ["bash", "run.sh", "check-acp-usage-accounting"],
176
+ "timeoutSeconds": 240,
177
+ "signature": "[QK:ACP-REBILL-MAIN-LOOP-SCOPE]",
178
+ "signatureSource": "scripts/check-acp-usage-accounting.ts"
179
+ }
180
+ ]
181
+ }