@junghanacs/entwurf 0.14.2 → 0.15.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 (87) hide show
  1. package/AGENTS.md +9 -5
  2. package/CHANGELOG.md +149 -0
  3. package/DELIVERY.md +72 -59
  4. package/README.md +120 -20
  5. package/VERIFY.md +10 -7
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +2 -2
  8. package/docs/external-mcp-host.md +26 -4
  9. package/docs/setup-clean-host.md +118 -18
  10. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +8 -6
  11. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/copilot-fresh-preflight.js +253 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +10 -2
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +102 -28
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +77 -11
  16. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-copilot.js +288 -0
  17. package/mcp/entwurf-bridge/src/index.ts +10 -6
  18. package/mcp/entwurf-bridge/tsconfig.build.json +2 -1
  19. package/package.json +22 -11
  20. package/pi/copilot-receive/entwurf-receive/extension.mjs +323 -0
  21. package/pi/entwurf-capabilities.json +1 -0
  22. package/pi/meta-bridge-copilot/.claude-plugin/marketplace.json +11 -0
  23. package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json +5 -0
  24. package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/hooks/hooks.json +7 -0
  25. package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh +85 -0
  26. package/pi-extensions/entwurf-control.ts +17 -8
  27. package/pi-extensions/lib/copilot-fresh-preflight.ts +251 -0
  28. package/pi-extensions/lib/meta-sender-identity.ts +10 -2
  29. package/pi-extensions/lib/meta-session.ts +102 -29
  30. package/pi-extensions/lib/mux-fresh-call.ts +82 -11
  31. package/pi-extensions/meta-bridge-hook-copilot.ts +323 -0
  32. package/run.sh +1079 -186
  33. package/scripts/agy-bridge.sh +4 -2
  34. package/scripts/check-copilot-birth-hook.ts +1174 -0
  35. package/scripts/check-copilot-launch.ts +395 -0
  36. package/scripts/check-copilot-receive-arm.ts +734 -0
  37. package/scripts/check-copilot-statusline.ts +122 -0
  38. package/scripts/check-entwurf-capabilities.ts +25 -6
  39. package/scripts/check-fresh-cut-gate.sh +104 -27
  40. package/scripts/check-gate-qualification.ts +10 -1
  41. package/scripts/check-install-container.sh +2 -2
  42. package/scripts/check-install-surface.ts +1 -1
  43. package/scripts/check-meta-capability-source.ts +89 -5
  44. package/scripts/check-meta-doctor-oracle.sh +26 -0
  45. package/scripts/check-meta-manifest-schema.py +38 -0
  46. package/scripts/check-meta-receiver-marker.ts +25 -2
  47. package/scripts/check-meta-session.ts +32 -0
  48. package/scripts/check-setup-qualification.sh +126 -0
  49. package/scripts/copilot-bridge-doctor.sh +242 -0
  50. package/scripts/copilot-bridge-install.sh +384 -0
  51. package/scripts/copilot-bridge-oracle.sh +223 -0
  52. package/scripts/copilot-bridge-uninstall.sh +137 -0
  53. package/scripts/copilot-launch.sh +236 -0
  54. package/scripts/copilot-mcp-bridge.sh +177 -0
  55. package/scripts/copilot-mcp-config.py +221 -0
  56. package/scripts/copilot-receive-bridge.sh +483 -0
  57. package/scripts/copilot-statusline-bridge.sh +149 -0
  58. package/scripts/copilot-statusline-config.py +201 -0
  59. package/scripts/copilot-statusline.sh +68 -0
  60. package/scripts/dev-bin.sh +53 -9
  61. package/scripts/fake-copilot-vendor.sh +67 -0
  62. package/scripts/meta-bridge-hook-log.sh +17 -5
  63. package/scripts/mutants/capability-cache.json +64 -0
  64. package/scripts/mutants/copilot-birth.json +244 -0
  65. package/scripts/mutants/copilot-launch.json +187 -0
  66. package/scripts/mutants/copilot-receive.json +263 -0
  67. package/scripts/mutants/fresh-cut.json +17 -0
  68. package/scripts/mutants/mux-fresh-call.json +216 -2
  69. package/scripts/mutants/pack-install.json +17 -0
  70. package/scripts/mutants/pi-package-ownership.json +92 -0
  71. package/scripts/mutants/setup-verdict.json +148 -0
  72. package/scripts/mutants/source-install.json +32 -0
  73. package/scripts/pi_settings_io.py +22 -0
  74. package/scripts/raw-async-delivery/README.md +170 -125
  75. package/scripts/raw-async-delivery/copilot-enqueue-addressed.sh +35 -0
  76. package/scripts/raw-async-delivery/copilot-extension-receive/extension.mjs +123 -0
  77. package/scripts/register-pi-package.py +525 -7
  78. package/scripts/register-pi-provider.py +162 -12
  79. package/scripts/smoke-agy-install-state.sh +86 -30
  80. package/scripts/smoke-agy-statusline-state.sh +9 -6
  81. package/scripts/smoke-copilot-mcp-state.sh +153 -0
  82. package/scripts/smoke-copilot-statusline-state.sh +131 -0
  83. package/scripts/smoke-mux-fresh-call-live.ts +2 -0
  84. package/scripts/smoke-mux-lifecycle-live.ts +3 -1
  85. package/scripts/smoke-setup-verdict.sh +291 -0
  86. package/scripts/smoke-user-scope-citizen.sh +388 -1
  87. package/scripts/tsconfig.json +1 -0
@@ -0,0 +1,148 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "setup-verdict",
4
+ "mutants": [
5
+ {
6
+ "claim": "SETUP-FALSE-GREEN",
7
+ "title": "aggregate setup prints its computed NON-GREEN summary but exits 0 again, resurrecting the retired whole-host false success in the only surface an operator reads",
8
+ "subject": "run.sh",
9
+ "find": [
10
+ " echo \"DONE: entwurf setup — result: NON-GREEN (FAIL:$s_fails). Valid components above remain installed; repair the named ones and re-run setup.\"\n return 1"
11
+ ],
12
+ "replace": [
13
+ " echo \"DONE: entwurf setup — result: NON-GREEN (FAIL:$s_fails). Valid components above remain installed; repair the named ones and re-run setup.\"\n return 0"
14
+ ],
15
+ "gate": ["bash", "scripts/check-setup-qualification.sh"],
16
+ "timeoutSeconds": 120,
17
+ "signature": "[QK:SETUP-FALSE-GREEN]",
18
+ "signatureSource": "scripts/check-setup-qualification.sh"
19
+ },
20
+ {
21
+ "claim": "SETUP-PI-FLOOR-SKIP",
22
+ "title": "a resolvable pi below the supported floor is relabeled zero-state SKIP, so a stale-harness host reads green instead of being told its detected integration is incomplete",
23
+ "subject": "run.sh",
24
+ "find": [
25
+ " setup_result pi FAIL \"detected pi ${pi_ver:-<unreadable version>} is outside the supported range $pi_range — Pi wiring not written; align pi, then re-run setup\""
26
+ ],
27
+ "replace": [
28
+ " setup_result pi SKIP \"detected pi ${pi_ver:-<unreadable version>} is outside the supported range $pi_range — Pi wiring not written; align pi, then re-run setup\""
29
+ ],
30
+ "gate": ["bash", "scripts/check-setup-qualification.sh"],
31
+ "timeoutSeconds": 120,
32
+ "signature": "[QK:SETUP-PI-FLOOR-SKIP]",
33
+ "signatureSource": "scripts/check-setup-qualification.sh"
34
+ },
35
+ {
36
+ "claim": "SETUP-INSTALLED-NO-BOOTSTRAP",
37
+ "title": "installed-package setup re-enters the source pnpm bootstrap inside node_modules, so the mode-first contract collapses and a neutral npm consumer's setup dies or mutates the installed tree",
38
+ "subject": "run.sh",
39
+ "find": [
40
+ " if [ \"$mode\" = \"source\" ]; then\n # Source bootstrap is the ONLY install-mode difference (#86): the pinned"
41
+ ],
42
+ "replace": [
43
+ " if [ -n \"$mode\" ]; then\n # Source bootstrap is the ONLY install-mode difference (#86): the pinned"
44
+ ],
45
+ "gate": ["bash", "scripts/check-setup-qualification.sh"],
46
+ "timeoutSeconds": 120,
47
+ "signature": "[QK:SETUP-INSTALLED-NO-BOOTSTRAP]",
48
+ "signatureSource": "scripts/check-setup-qualification.sh"
49
+ },
50
+ {
51
+ "claim": "SETUP-CREDENTIAL-FREE",
52
+ "title": "a credential-store write (auth.json backup) resurrects inside aggregate setup, re-violating the corrected installation rule that setup never reads, copies, aliases or backs up harness credentials",
53
+ "subject": "run.sh",
54
+ "find": [" SETUP_RESULTS=()\n\n if [ \"$mode\" = \"source\" ]; then"],
55
+ "replace": [
56
+ " SETUP_RESULTS=()\n local _auth=\"${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}/auth.json\"\n if [ -f \"$_auth\" ]; then cp \"$_auth\" \"$_auth.bak\"; fi\n\n if [ \"$mode\" = \"source\" ]; then"
57
+ ],
58
+ "gate": ["bash", "scripts/check-setup-qualification.sh"],
59
+ "timeoutSeconds": 120,
60
+ "signature": "[QK:SETUP-CREDENTIAL-FREE]",
61
+ "signatureSource": "scripts/check-setup-qualification.sh"
62
+ },
63
+ {
64
+ "claim": "DEV-BIN-ATTEMPTS-ALL",
65
+ "title": "the no-arg dev-bin expose loop aborts at the first foreign refusal again, so units after it are never attempted while the caller's summary implies they ran",
66
+ "subject": "scripts/dev-bin.sh",
67
+ "find": [
68
+ " rc=0; expose_one \"$b\" || rc=$?\n if [ \"$rc\" -eq 0 ]; then exposed=$((exposed + 1)); else refused=\"$refused $b\"; fi"
69
+ ],
70
+ "replace": [
71
+ " rc=0; expose_one \"$b\" || rc=$?\n if [ \"$rc\" -eq 0 ]; then exposed=$((exposed + 1)); else warn \"[dev-bin] REFUSE aborts the loop\"; exit 3; fi"
72
+ ],
73
+ "gate": ["bash", "run.sh", "smoke-agy-install-state"],
74
+ "timeoutSeconds": 600,
75
+ "signature": "[QK:DEV-BIN-ATTEMPTS-ALL]",
76
+ "signatureSource": "scripts/smoke-agy-install-state.sh"
77
+ },
78
+ {
79
+ "claim": "SETUP-INSTALLED-NO-PNPM",
80
+ "title": "installed setup regains an unconditional pnpm requirement, so a neutral npm consumer without pnpm is refused by an accidental Missing-command instead of composing — the mode decision stops being independent of the source toolchain",
81
+ "subject": "run.sh",
82
+ "find": [
83
+ " if [ \"$mode\" = \"source\" ]; then\n # Source-only dev fixtures: the pinned repo dependencies are build/test\n # fixtures, never a product promise that consumers receive them (#86 A4).\n require_cmd pnpm\n fi"
84
+ ],
85
+ "replace": [" require_cmd pnpm"],
86
+ "gate": ["bash", "scripts/check-setup-qualification.sh"],
87
+ "timeoutSeconds": 120,
88
+ "signature": "[QK:SETUP-INSTALLED-NO-PNPM]",
89
+ "signatureSource": "scripts/check-setup-qualification.sh"
90
+ },
91
+ {
92
+ "claim": "SETUP-COPILOT-ABSENT-SKIP",
93
+ "title": "the copilot presence gate collapses to always-detected, so an absent-copilot host loses its zero-state SKIP and setup composes (and writes) Copilot units nobody asked for",
94
+ "subject": "run.sh",
95
+ "find": [
96
+ " if ! command -v \"${COPILOT_BIN:-copilot}\" >/dev/null 2>&1; then\n setup_result copilot SKIP \"copilot not on PATH — zero Copilot wiring written\""
97
+ ],
98
+ "replace": [
99
+ " if false; then\n setup_result copilot SKIP \"copilot not on PATH — zero Copilot wiring written\""
100
+ ],
101
+ "gate": ["bash", "scripts/check-setup-qualification.sh"],
102
+ "timeoutSeconds": 120,
103
+ "signature": "[QK:SETUP-COPILOT-ABSENT-SKIP]",
104
+ "signatureSource": "scripts/check-setup-qualification.sh"
105
+ },
106
+ {
107
+ "claim": "SETUP-COPILOT-INDEPENDENT",
108
+ "title": "a failed Copilot birth aborts the whole composition, so the MCP/receiver/footer units are silently never attempted while the operator reads only the first failure",
109
+ "subject": "run.sh",
110
+ "find": [
111
+ " setup_result copilot-birth FAIL \"detected copilot, but the birth plugin install did not complete (see above) — repair, then re-run setup\"\n fi"
112
+ ],
113
+ "replace": [
114
+ " setup_result copilot-birth FAIL \"detected copilot, but the birth plugin install did not complete (see above) — repair, then re-run setup\"\n return 1\n fi"
115
+ ],
116
+ "gate": ["bash", "scripts/check-setup-qualification.sh"],
117
+ "timeoutSeconds": 120,
118
+ "signature": "[QK:SETUP-COPILOT-INDEPENDENT]",
119
+ "signatureSource": "scripts/check-setup-qualification.sh"
120
+ },
121
+ {
122
+ "claim": "SETUP-COPILOT-COSMETIC-PASS",
123
+ "title": "the birth unit's failure capture is swallowed, so a refused vendor preflight reads as copilot-birth PASS and a broken detected integration reports a green composition",
124
+ "subject": "run.sh",
125
+ "find": [" copilot_rc=0; (cd \"$REPO_DIR\" && bash scripts/copilot-bridge-install.sh) || copilot_rc=$?"],
126
+ "replace": [" copilot_rc=0; (cd \"$REPO_DIR\" && bash scripts/copilot-bridge-install.sh) || true"],
127
+ "gate": ["bash", "scripts/check-setup-qualification.sh"],
128
+ "timeoutSeconds": 120,
129
+ "signature": "[QK:SETUP-COPILOT-COSMETIC-PASS]",
130
+ "signatureSource": "scripts/check-setup-qualification.sh"
131
+ },
132
+ {
133
+ "claim": "SETUP-AGY-COSMETIC-RETURN",
134
+ "title": "wire_agy_bridge's failed-install path returns 0 again, restoring the WARN-then-success mechanism that let a detected-but-refused agy integration read as a green setup",
135
+ "subject": "run.sh",
136
+ "find": [
137
+ " echo \"[setup] Bridge NOT wired; verify with ./run.sh doctor-agy-bridge. setup continues.\" >&2\n ;;\n esac\n return 1\n}"
138
+ ],
139
+ "replace": [
140
+ " echo \"[setup] Bridge NOT wired; verify with ./run.sh doctor-agy-bridge. setup continues.\" >&2\n ;;\n esac\n return 0\n}"
141
+ ],
142
+ "gate": ["bash", "run.sh", "smoke-agy-install-state"],
143
+ "timeoutSeconds": 600,
144
+ "signature": "[QK:SETUP-AGY-COSMETIC-RETURN]",
145
+ "signatureSource": "scripts/smoke-agy-install-state.sh"
146
+ }
147
+ ]
148
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "source-install",
4
+ "mutants": [
5
+ {
6
+ "claim": "SOURCE-OPERATOR-BIN",
7
+ "title": "source setup again omits the entwurf operator command, so Copilot fresh rejects runtime-unresolved unless an unrelated global package happens to mask the defect",
8
+ "subject": "scripts/dev-bin.sh",
9
+ "find": [
10
+ "MANAGED_BINS=\"entwurf entwurf-bridge entwurf-agy-statusline entwurf-agy-imprint entwurf-copilot-statusline\""
11
+ ],
12
+ "replace": [
13
+ "MANAGED_BINS=\"entwurf-bridge entwurf-agy-statusline entwurf-agy-imprint entwurf-copilot-statusline\""
14
+ ],
15
+ "gate": ["bash", "run.sh", "smoke-agy-install-state"],
16
+ "timeoutSeconds": 300,
17
+ "signature": "[QK:SOURCE-OPERATOR-BIN]",
18
+ "signatureSource": "scripts/smoke-agy-install-state.sh"
19
+ },
20
+ {
21
+ "claim": "SOURCE-OPERATOR-FAILS-LOUD",
22
+ "title": "source setup reports green after the managed operator command is foreign, shadowed, or outside PATH, recreating the runtime-unresolved incident behind a cosmetic success",
23
+ "subject": "run.sh",
24
+ "find": [" if ! bash \"$REPO_DIR/scripts/dev-bin.sh\" verify entwurf; then\n return 1\n fi"],
25
+ "replace": [" if ! bash \"$REPO_DIR/scripts/dev-bin.sh\" verify entwurf; then\n return 0\n fi"],
26
+ "gate": ["bash", "run.sh", "smoke-agy-install-state"],
27
+ "timeoutSeconds": 300,
28
+ "signature": "[QK:SOURCE-OPERATOR-FAILS-LOUD]",
29
+ "signatureSource": "scripts/smoke-agy-install-state.sh"
30
+ }
31
+ ]
32
+ }
@@ -63,3 +63,25 @@ def unchanged(before: dict, after: dict) -> bool:
63
63
  writers mutate in place, so they parse the raw text twice rather than aliasing it.
64
64
  """
65
65
  return before == after
66
+
67
+
68
+ def classify_installer_root(state: dict) -> "tuple[str, str | None]":
69
+ """ONE typed verdict for a user-scope install-state's `installerRoot` (#86 C2).
70
+
71
+ Shared by register-pi-provider install/remove and the doctor-pi-package ownership
72
+ coupling, because two per-site checks (`isinstance(str)` in one arm, `is None` in
73
+ another) let every OTHER type — a number, an empty string, a bool, an object, an
74
+ array — fall between them and proceed unattributed. A state MORE unattributed than
75
+ legacy must never be treated MORE leniently than legacy.
76
+
77
+ ("legacy", None) key absent, or an explicit null — the pre-#86 shape; each
78
+ caller names its own adoption/refusal contract for it.
79
+ ("owner", root) a non-empty string — the recorded owner root.
80
+ ("corrupt", None) everything else — fail-closed before any write.
81
+ """
82
+ if "installerRoot" not in state or state["installerRoot"] is None:
83
+ return "legacy", None
84
+ root = state["installerRoot"]
85
+ if isinstance(root, str) and root:
86
+ return "owner", root
87
+ return "corrupt", None
@@ -12,12 +12,26 @@ Codex is split by launch surface:
12
12
  auto-attached to a default-path app-server): raw `turn/start` over
13
13
  WebSocket-over-UDS wakes the live thread — **demonstrated, no managed
14
14
  standalone, no cloud**.
15
- - **Copilot CLI 1.0.80 TUI+server**: official SDK `1.0.11` can resolve the
16
- foreground native session and enqueue into that exact idle TUI; D7 was
17
- demonstrated once (L4 direct-native, one Linux workstation). The launch flag
18
- `--ui-server` is hidden from CLI help and its loopback RPC authentication is
19
- not established, so this remains a probe, not a managed lane. Undocumented
15
+
16
+ Copilot is no longer blocked on transport, and the probe below is no longer the
17
+ product it is the receipt the product was built on:
18
+
19
+ - **Copilot CLI 1.0.80 extension**: the CLI forks a first-party extension and
20
+ speaks JSON-RPC over that child's **stdio**, so `fs.watch` -> `session.send()`
21
+ wakes the idle TUI with **no network listener or token-authentication axis**. Idle wake
22
+ and exact-marker attribution were demonstrated; addressed isolation was observed but its
23
+ decisive control log was not preserved (2026-08-23, one Linux workstation). It needs the experimental
24
+ `COPILOT_CLI_ENABLED_FEATURE_FLAGS=EXTENSIONS` flag at launch. The older
25
+ hidden `--ui-server` loopback probe is retired — it found the capability
26
+ through a door that could not pass admission. Undocumented
20
27
  `~/.copilot/run/ws.*` is still not a rail.
28
+ - **The MANAGED receiver now lives at `pi/copilot-receive/entwurf-receive/`** and is
29
+ installed with `run.sh install-copilot-receive` (#82 RAIL 5). Read it before reusing
30
+ anything here, because it deliberately differs on three points: it binds to the V3
31
+ record instead of publishing its own `ready.json`; its receiver marker is owned by the
32
+ extension pid, so a crash retires it; and it **announces** the inbox rather than
33
+ sending the body, leaving the drain (and the read-receipt) to `entwurf_inbox_read`.
34
+ The files below stay as the measurement — do not develop the product in them.
21
35
 
22
36
  ## TL;DR — three reception paths, ranked
23
37
 
@@ -88,7 +102,9 @@ do not ship that. Proven: deliver to A's sessionId → A wakes (FileChanged), B'
88
102
  - `raw-agy-send.sh <conv_id> …` — agy parity: PUSH into a live Antigravity session (LS gRPC)
89
103
  - `raw-codex-ws-turn-start.py <sock> <thread_id> …` — Codex parity (no managed standalone, no cloud): PUSH `turn/start` over WebSocket-over-UDS into a bare `app-server --listen` socket
90
104
  - `codex-local-appserver.sh [sock]` — start a bare local app-server so plain `codex` auto-attaches and becomes addressable
91
- - `copilot-ui-server-probe.mjs`official-SDK D0 probe by default; `LIVE=1` performs one addressed idle enqueue and prints D0–D8
105
+ - `copilot-enqueue-addressed.sh <session_id> …` Copilot raw sender: write `.msg` + poke one session's signal; refuses a missing marker, but deliberately does not claim production stale-receiver safety
106
+ - `copilot-extension-receive/extension.mjs` — Copilot reception unit: a first-party CLI extension that arms the per-session mailbox and calls `session.send()` (stdio JSON-RPC; no port, no token)
107
+ - `copilot-ui-server-probe.mjs` — RETIRED rail, kept as history; the hidden `--ui-server` loopback path whose authentication could not be established
92
108
 
93
109
  ### Reproduction drivers
94
110
  - `repro-plugin-idle-wake.sh` — single-session smoke.
@@ -251,7 +267,7 @@ send-message-v2` spins a fresh thread, not the live one.
251
267
  4. **Unix-socket transport is WebSocket** (tokio-tungstenite), not newline JSON-RPC,
252
268
  and requires **no auth token** on the UDS. A plain WS client suffices.
253
269
 
254
- ## Copilot CLI raw delivery status (1.0.80) — positive TUI+server probe, not a managed lane
270
+ ## Copilot CLI delivery status (1.0.80) — idle wake DEMONSTRATED on the extension rail
255
271
 
256
272
  Copilot is a GitHub **harness** (issues, PR, CI, model `auto`, mode `autopilot`,
257
273
  remote/delegate), not a second GPT provider next to pi. This makes a native lane
@@ -264,136 +280,156 @@ premium-interaction entitlement; model `auto` chooses a path within that budget.
264
280
  The reason to use it is GitHub specialization and auto routing, not an unlimited
265
281
  subscription claim. Model `auto` and mode `autopilot` are separate axes.
266
282
 
267
- ### Positive transport
268
-
269
- The first-party `@github/copilot-sdk` exposes TUI+server coordination:
270
- `getForegroundSessionId()`, `getSessionMetadata()`, `resumeSession(id)`, and
271
- session-scoped `send({mode:"enqueue"|"immediate"})`. The native TUI launch mode is:
283
+ ### The rail: a first-party CLI extension, stdio only
272
284
 
273
- ```bash
274
- copilot --ui-server --port 43817 --model auto
275
- ```
285
+ `copilot-extension-receive/extension.mjs` + `copilot-enqueue-addressed.sh`.
276
286
 
277
- `--ui-server` is accepted by CLI 1.0.80 and named by the official SDK API docs,
278
- but hidden from `copilot --help`. Launch mode is therefore part of the capability,
279
- and this evidence does not apply to an already-running plain `copilot` TUI.
280
-
281
- Measured once on 2026-08-19 with SDK 1.0.11, **using the earlier chronological-slice
282
- probe** (it scored the events following the marker's position in the history). The probe
283
- in the tree today scores a named turn instead — see "Current probe contract" below — and
284
- that contract has NOT been run LIVE, so nothing in this list is retroactively evidence
285
- for it. Evidence level for the list: **L4 direct-native, ONE Linux workstation, one run**;
286
- the receipt is host-local probe stdout and was NOT archived as a durable artifact, so what
287
- follows is reproducible by instruction, not citable to a stored file:
288
-
289
- - protocol v3 ping succeeded;
290
- - foreground session id joined to metadata containing cwd/git root/branch;
291
- - `resumeSession(id)` + idle `enqueue` woke the visible TUI with zero typing;
292
- - the unique body appeared as `user.message` with `delivery:"idle"`;
293
- - model `auto` selected `gpt-5.6-luna` and returned the exact marker;
294
- - `assistant.message`, `assistant.turn_end`, and (in the one-session run)
295
- ephemeral `session.idle` gave completion/reply evidence;
296
- - a two-session control then targeted A while B received no `user.message`, turn,
297
- or assistant event, so D3 passed too.
298
-
299
- D0–D7 passed; D8 is unproven. One robustness defect surfaced in the two-session
300
- shape: the target visibly replied and persisted `assistant.message` + `turn_end`,
301
- but that joining SDK client did not receive ephemeral `session.idle`, so SDK
302
- `sendAndWait()` timed out after 60 seconds. The probe therefore uses `send()` plus
303
- bounded polling of the official session event-history API (`session.getEvents()` /
304
- `getMessages()`) and reports completion at `turn_end`. Name that surface at its real
305
- size: it is the SDK's own full event history — no narrower, no more privileged — and it
306
- is **not** TUI, file, or database transcript scraping; the probe never reads Copilot's
307
- storage. This is evidence, not a product retry/polling design.
308
-
309
- ### Current probe contract — designed, not yet run LIVE
310
-
311
- Attribution is the load-bearing part, and it runs off the probe's own marker body:
287
+ The CLI forks an extension as its own child and speaks JSON-RPC over that
288
+ child's stdio. `joinSession()` attaches it to the foreground session; from
289
+ there `session.send({mode:"enqueue"})` injects a user message. So the whole
290
+ delivery path is:
312
291
 
313
- ```text
314
- unique marker body exactly one user.message
315
- → its interactionId
316
- → exactly one assistant.turn_start on that interaction
317
- → that turn_start's required turnId
318
- → only assistant.message / assistant.turn_end on that turnId
292
+ ```
293
+ external file write -> fs.watch in the extension -> session.send() -> the idle session takes a turn
319
294
  ```
320
295
 
321
- Every link is required and unambiguous; absent or matched twice, the probe FAILS CLOSED.
322
- There is no positional fallback and no "the turn after ours" rule, because scoring "the
323
- newest assistant.message" in a session a human is also typing into is how a probe reports
324
- someone else's turn as its own delivery.
296
+ `fs.watch` -> `session.send()` is not a mechanism found by inspection: the
297
+ bundled SDK documents it (`copilot-sdk/docs/examples.md`, "Detecting when the
298
+ plan file is created or edited"). The SDK also ships *inside* the CLI package
299
+ (`<platform-pkg>/copilot-sdk/`) and is injected into extension children by
300
+ `preloads/extension_bootstrap.mjs`, so nothing has to be installed and the SDK
301
+ version cannot drift from the CLI.
325
302
 
326
- What is deliberately NOT the key: `session.send()` on the bundled CLI 1.0.80 resolves to a
327
- `Promise<string>` that is the SDK's own submission handle. The server-side `user.message`
328
- does not carry that string, and it is a different axis from that event's
329
- `id`/`interactionId` a join on it cannot hold, so the probe logs it as a diagnostic and
330
- never matches on it.
303
+ **This is why the rail clears the network-boundary bar the old one could not.**
304
+ The `--ui-server` probe was refused because its loopback RPC authentication was
305
+ not established. An extension has no port, token, or listener, so that network
306
+ authentication axis does not exist. Product admission must instead certify the
307
+ installed extension's provenance and the CLI-owned parent/child lifecycle; the
308
+ fork is the transport boundary, not proof that every permission, liveness, and
309
+ integrity obligation is already closed.
331
310
 
332
- The D3 control cell uses the same sentence it prints: across `onEvent` and `getEvents`, the
333
- non-target session received no `user.message` and no `assistant.*` event of any kind.
311
+ ### Launch contract
334
312
 
335
- Lifecycle, stated precisely rather than flatteringly: the probe never deletes target
336
- session A and issues no `A.disconnect()` of its own. `client.stop()` DOES tear down every
337
- tracked session A included — as a wire `session.destroy`; because A's foreground
338
- ownership is re-confirmed immediately before teardown, the TUI keeps A as its foreground
339
- session, so the net effect on A is detach-equivalent, not removal. The probe deletes only
340
- the control session it created itself, and reads `client.stop()`'s returned error list so a
341
- failed teardown cannot exit 0 behind a printed verdict. Reaching past the SDK for a raw
342
- detach wrapper is out of bounds — it would be a second lifecycle authority.
313
+ Extensions sit behind an experimental feature flag. Without it the CLI never
314
+ scans for extensions and the receiver is inert **with no error at all**
315
+ budget for that when a receiver appears not to arm:
343
316
 
344
- This contract has not been exercised by a LIVE turn. The next LIVE run is what would
345
- demonstrate it, and it also confirms the real event key names.
317
+ ```bash
318
+ COPILOT_CLI_ENABLED_FEATURE_FLAGS=EXTENSIONS copilot --model auto
319
+ ```
346
320
 
347
- ### Reproduce with the pinned official SDK
321
+ `--experimental` is NOT required (measured: a session launched with the env var
322
+ alone armed its receiver). Discovery scopes are `user`
323
+ (`~/.copilot/extensions/`), `plugin`, `session`, and — interactive mode only —
324
+ `project` (`.github/extensions/`). Prompt mode (`-p`) drops `project` unless
325
+ `GITHUB_COPILOT_PROMPT_MODE_EXTENSIONS=true`. The `plugin` scope matters most
326
+ here: the existing entwurf Copilot plugin is the candidate install scope for
327
+ this receiver, so product design need not start from a per-repository
328
+ `.github/extensions/` directory.
348
329
 
349
- The SDK is deliberately not an entwurf production dependency while this is only
350
- a raw probe. Install it in scratch and point the probe at its ESM entry:
330
+ ### Reproduce
351
331
 
352
332
  ```bash
353
- mkdir -p /tmp/copilot-sdk-probe
354
- printf '{"private":true,"type":"module"}\n' >/tmp/copilot-sdk-probe/package.json
355
- pnpm --dir /tmp/copilot-sdk-probe add --ignore-scripts @github/copilot-sdk@1.0.11
356
-
357
- # terminal A: visible native session (accept folder trust)
358
- copilot --ui-server --port 43817 --model auto
333
+ mkdir -p /tmp/cop-lab/.github/extensions/entwurf-mailbox
334
+ cp copilot-extension-receive/extension.mjs /tmp/cop-lab/.github/extensions/entwurf-mailbox/
335
+
336
+ # terminal A: a visible session that arms its own receiver
337
+ cd /tmp/cop-lab
338
+ export COPILOT_MAILBOX_ROOT=/tmp/cop-lab/mbx COPILOT_CLI_ENABLED_FEATURE_FLAGS=EXTENSIONS
339
+ copilot --model auto --allow-all-tools # accept folder trust
340
+
341
+ # terminal B: find the armed receiver, then wake it with zero typing
342
+ ls /tmp/cop-lab/mbx/*/ready.json
343
+ COPILOT_MAILBOX_ROOT=/tmp/cop-lab/mbx \
344
+ ./copilot-enqueue-addressed.sh <session_id> "Reply with exactly PING and nothing else."
345
+ ```
359
346
 
360
- # terminal B: D0 only, no model call
361
- COPILOT_SDK_MODULE=/tmp/copilot-sdk-probe/node_modules/@github/copilot-sdk/dist/index.js \
362
- ./copilot-ui-server-probe.mjs
347
+ ### Measured 2026-08-23, CLI 1.0.80, model `auto`, one Linux workstation (oracle, arm64)
363
348
 
364
- # one paid/subscription turn: addressed idle enqueue through the official SDK
365
- LIVE=1 COPILOT_SDK_MODULE=/tmp/copilot-sdk-probe/node_modules/@github/copilot-sdk/dist/index.js \
366
- ./copilot-ui-server-probe.mjs
349
+ Evidence level **L4 direct-native, one host**. The receiver log lines that still
350
+ travel are pasted here. Scratch was cleaned before the second-session control and
351
+ second-turn lines were copied, so those two observations are explicitly downgraded
352
+ below rather than being laundered into durable receipts:
367
353
 
368
- # creates a second no-turn control session and proves only the target receives the marker
369
- LIVE=1 COPILOT_D3_CONTROL=1 \
370
- COPILOT_SDK_MODULE=/tmp/copilot-sdk-probe/node_modules/@github/copilot-sdk/dist/index.js \
371
- ./copilot-ui-server-probe.mjs
354
+ ```text
355
+ 02:19:33.996 ARMED sessionId=4fc16d8d-473d-4258-a1fd-f99d3cb375e9
356
+ 02:20:03.388 DELIVER (signal) 1787451603.msg bytes=96
357
+ 02:20:03.422 SENT 1787451603.msg
358
+ 02:20:06.141 EVENT user.message {"content":"...Reply with exactly ENTWURF-WAKE-1787451603..."}
359
+ 02:20:06.492 EVENT assistant.turn_start {"turnId":"0"}
360
+ 02:20:09.935 EVENT assistant.message {"turnId":"0","content":"ENTWURF-WAKE-1787451603"}
361
+ 02:20:10.024 EVENT session.idle {}
372
362
  ```
373
363
 
374
- ### Admission blockers and negative evidence
375
-
376
- - **Transport authentication is not established.** The TUI server bound loopback,
377
- but an SDK client without a token connected. Setting `COPILOT_CONNECTION_TOKEN`
378
- on the UI server did not enable auth: the unauthenticated client still connected,
379
- while a token-bearing client received `AUTHENTICATION_NOT_CONFIGURED`. Do not
380
- ship this as a rail until the supported same-user/fail-closed boundary is proved.
381
- - Shell command-hook `sessionStart` input is `{timestamp,cwd,source,initialPrompt?}`;
382
- it does **not** carry `sessionId`. SDK callbacks receive `sessionId` separately in
383
- their invocation object. The discarded command-hook probe therefore could not
384
- establish D0, and UUID-only process-log scraping did not satisfy D0's cwd/liveness
385
- join. Do not revive that path.
386
- - Hooks have no `FileChanged` / `watchPaths` / `asyncRewake`; the Claude mailbox
387
- mechanism cannot be copied.
388
- - `--acp` is a pi-host child path, not this native TUI citizen path. `--remote` is
389
- GitHub web/mobile steering, not a local `entwurf_v2` API. Undocumented
390
- `~/.copilot/run/ws.*` remains out of bounds.
391
-
392
- Do not add `backend:"copilot"`, `FRESH_CALL_BACKENDS`, a schema change, or an OPEN
393
- issue from these positive probes. Next evidence is active-turn enqueue/immediate
394
- behavior, TUI-vs-SDK permission ownership, the missing multi-session `session.idle`,
395
- endpoint staleness/crash behavior, and a supported authenticated or equivalent local
396
- boundary.
364
+ - **Idle wake: PASS.** The session had never been typed into — an empty timeline,
365
+ already past any turn boundary. An external file write started a turn. ~2.7 s
366
+ poke -> `user.message`, ~6.5 s poke -> reply. The visible TUI showed the prompt
367
+ and the answer.
368
+ - **Attribution: PASS.** A unique per-run marker went in and came back exactly,
369
+ on one `assistant.turn_start`/`assistant.message` pair.
370
+ - **Addressed routing: reported, receipt not preserved.** The measuring Opus
371
+ reported that a second armed process B stayed at one `ARMED` line across two
372
+ deliveries to A, with no `user.message` or `assistant.*` event. The decisive B
373
+ line and A's second delivery were not copied before scratch cleanup, so this is
374
+ a lead for the admission rerun, not durable D3 acceptance.
375
+ - **Continuity: one turn demonstrated.** The pasted chain shows one joined
376
+ session taking the marker turn and replying; no `-p` process was spawned. A
377
+ second same-session turn was reported but its lines were not preserved, so no
378
+ stronger repeatability claim crosses from this checkpoint.
379
+
380
+ ### Not proven do not describe these as working
381
+
382
+ - **Active-turn delivery.** Every measured send landed on an idle session.
383
+ `mode:"enqueue"` vs `"immediate"` against a busy turn is untested.
384
+ - **Multi-session inside ONE CLI process.** A and B were separate `copilot`
385
+ processes. Whether a *background* session inside one process (sidebar tabs)
386
+ can be woken while another holds the foreground is untested, and
387
+ `joinSession()` attaching to "the foreground session" is the reason to doubt it.
388
+ - **`/clear` and foreground replacement.** The docs say extensions reload there;
389
+ re-arming was not measured. `[문서]`, not `[측정]`.
390
+ - **Flag durability.** `EXTENSIONS` is an experimental flag with
391
+ `experimental`/`staff-or-experimental` availability. It can move or be
392
+ withdrawn between CLI releases; re-verify on upgrade. A flagged surface is an
393
+ admission question for a managed lane even when the transport is sound.
394
+ - **Permission ownership, crash and ordering behavior, delivery under load.**
395
+
396
+ That fence has since been walked, not deleted: `backend:"copilot"` receive capability
397
+ IS now real (`wakeMode: self-fetch` plus a record-bound receiver marker, #82 RAIL 5),
398
+ and it arrived through the managed unit with its own installer, doctor, gate and
399
+ mutants — not by widening anything in this directory. `FRESH_CALL_BACKENDS` now
400
+ includes `copilot` (#82 RAIL 9); that launch path lives in `mux-fresh-call`, not here.
401
+ This probe still does not open a sibling. Waking an already-running Copilot and
402
+ opening a new one remain different capabilities.
403
+ The registry's receive grade moved off D0 later, and NOT because of this probe: a managed
404
+ LIVE wake was observed on 2026-08-23 (garden `20260823T181316-d9f6ba`, CLI 1.0.80,
405
+ doorbell→`lastReadAt` 09:23:41.235Z→09:23:56.480Z) and that is what makes the shipped lane
406
+ D6, with D7 partial and D3 pending. This probe's receipt remains what it always was —
407
+ evidence about the MECHANISM, not about the shipped lane. Keep the two apart when citing
408
+ either.
409
+
410
+ ### Retired: the hidden `--ui-server` probe (kept for the lesson)
411
+
412
+ The 2026-08-19 probe reached the TUI through `copilot --ui-server --port 43817`
413
+ and the separately-installed SDK, and it did demonstrate an idle enqueue. It was
414
+ refused admission because the launch flag is hidden from `copilot --help` and its
415
+ loopback RPC authentication could not be established: an unauthenticated client
416
+ connected, while setting `COPILOT_CONNECTION_TOKEN` on the server made a
417
+ token-bearing client fail with `AUTHENTICATION_NOT_CONFIGURED`.
418
+
419
+ The lesson is not "that probe was wrong". It found a real capability through the
420
+ wrong door, and then the door — not the capability — was what failed admission.
421
+ The bundled extension door was in the same package the whole time: the SDK that
422
+ this probe installed from npm also ships inside the CLI, with an `extension.mjs` contract and
423
+ docs beside it. `copilot-ui-server-probe.mjs` remains in this tree as that history.
424
+ Do not revive `--ui-server` as a rail; use the extension.
425
+
426
+ Also still true, and still out of bounds: hooks have no `FileChanged` /
427
+ `watchPaths` / `asyncRewake`, so the Claude mailbox hook mechanism cannot be
428
+ copied; the shell command-hook `sessionStart` input carries no `sessionId`
429
+ (the receiver publishes its own `ready.json` instead of scraping for one);
430
+ `--acp` is a pi-host child path and `--remote` is GitHub web/mobile steering,
431
+ neither of which is a local `entwurf_v2` API; and undocumented
432
+ `~/.copilot/run/ws.*` is not a rail.
397
433
 
398
434
  ## Live SSOT for "is the target session alive?"
399
435
 
@@ -407,9 +443,18 @@ boundary.
407
443
  `$HOME/.codex/app-server-control/app-server-control.sock` (or any owned 0700
408
444
  socket via `codex app-server --listen unix://PATH`) is the delivery surface.
409
445
  threadId comes from the newest rollout's `session_meta.id`.
410
- - Copilot TUI+server probe: SDK `ping` + `getForegroundSessionId()` +
411
- `getSessionMetadata()` identify the currently displayed native session, and
412
- the official session event-history API (`getEvents()` / `getMessages()`) reads
413
- a turn back no transcript file or database is touched. The TCP port is a
414
- runtime endpoint, never an identity axis; no managed liveness join exists
415
- until its authentication and stale-endpoint behavior are proved.
446
+ - Copilot raw probe: `<COPILOT_MAILBOX_ROOT>/<session_id>/ready.json`, written by
447
+ the receiver extension about itself (sessionId, pid, cwd, armedAt). `[번들]`
448
+ `preloads/extension_bootstrap.mjs` monitors the CLI parent, but this is a
449
+ discovery marker for the experiment, **not a production liveness SSOT**: the
450
+ probe sender checks only that the file exists, so a crashed extension can
451
+ leave stale state and produce a false enqueue receipt.
452
+ - Copilot PRODUCT: `<pi-agent-dir>/meta-receivers/<gardenId>.json`, written by the
453
+ managed extension and keyed by the garden id a sender actually targets. Liveness is
454
+ pid + start-key on the EXTENSION child, so a crashed receiver reads as inactive at the
455
+ next read — that is what `ready.json` could not do. Parent monitoring never cleaned the
456
+ probe's marker by itself; the product does not depend on cleanup at all. Nothing
457
+ scrapes `~/.copilot/session-store.db` or the session-state dirs, and the shell
458
+ command-hook `sessionStart` input is NOT
459
+ a source — it carries no `sessionId`. The retired `--ui-server` TCP port was
460
+ never an identity axis.
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ # Deliver an async message INTO one already-running, IDLE Copilot CLI session.
3
+ #
4
+ # Addressed by sessionId: only the named session's receiver sees the poke, so
5
+ # siblings stay idle. Free — it is a file write plus a continuation of a session
6
+ # that is already running; no `copilot -p` spawn is involved.
7
+ #
8
+ # ./copilot-enqueue-addressed.sh <session_id> "your async message"
9
+ #
10
+ # The receiving session must be running the copilot-extension-receive extension
11
+ # and must agree on COPILOT_MAILBOX_ROOT. `ready.json` is raw-probe discovery,
12
+ # not production liveness authority: this script does not close stale pid/PID-reuse.
13
+ # Inspect armed candidates with:
14
+ # ls "${COPILOT_MAILBOX_ROOT:-$HOME/.copilot/mailbox}"/*/ready.json
15
+ set -euo pipefail
16
+
17
+ root="${COPILOT_MAILBOX_ROOT:-$HOME/.copilot/mailbox}"
18
+ session_id="${1:?usage: copilot-enqueue-addressed.sh <session_id> <message>}"
19
+ shift
20
+ message="${*:?usage: copilot-enqueue-addressed.sh <session_id> <message>}"
21
+
22
+ box="$root/$session_id"
23
+ # Refuse an address that never armed. A stale marker remains possible in this raw
24
+ # probe; the managed product must add record-backed pid + start-key certification.
25
+ if [[ ! -f "$box/ready.json" ]]; then
26
+ echo "no receiver marker at $box (no ready.json) — not queued" >&2
27
+ exit 1
28
+ fi
29
+
30
+ msg="$box/$(date +%s%N).msg"
31
+ printf '%s\n' "$message" > "$msg"
32
+ # The body lands before the doorbell rings, so the receiver never wakes to an
33
+ # empty box.
34
+ printf 'poke\n' >> "$box/inbox.signal"
35
+ echo "queued-and-poked $msg (raw probe; extension log is the delivery receipt)"