@junghanacs/entwurf 0.22.0 → 0.23.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.
- package/AGENTS.md +4 -1
- package/BASELINE.md +1 -0
- package/CHANGELOG.md +514 -0
- package/DELIVERY.md +10 -2
- package/README.md +43 -4
- package/VERIFY.md +47 -0
- package/docs/acp-backend-rail.md +96 -17
- package/docs/setup-clean-host.md +11 -1
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +39 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +95 -56
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +46 -273
- package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
- package/mcp/entwurf-bridge/src/index.ts +41 -41
- package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
- package/package.json +9 -8
- package/pi-extensions/entwurf-control.ts +91 -52
- package/pi-extensions/lib/acp/acp-client.ts +3 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
- package/pi-extensions/lib/acp/backend.ts +4 -4
- package/pi-extensions/lib/acp/event-mapper.ts +5 -5
- package/pi-extensions/lib/acp/tool-surface.ts +2 -1
- package/pi-extensions/lib/codex-declaration.js +612 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +107 -51
- package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
- package/pi-extensions/lib/entwurf-facts.ts +14 -1
- package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
- package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
- package/pi-extensions/lib/entwurf-self-address.ts +33 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
- package/pi-extensions/lib/fresh-call-composition.ts +493 -0
- package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
- package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
- package/pi-extensions/lib/herdr-placement.ts +276 -0
- package/pi-extensions/lib/mux-fresh-call.ts +95 -301
- package/run.sh +588 -204
- package/scripts/check-acp-usage-accounting.ts +9 -9
- package/scripts/check-codex-birth-hook.ts +187 -0
- package/scripts/check-entwurf-control-rpc.ts +138 -0
- package/scripts/check-entwurf-fact-provider.ts +4 -2
- package/scripts/check-entwurf-peers-surface.ts +3 -1
- package/scripts/check-entwurf-self-address.ts +68 -0
- package/scripts/check-fresh-call-dispatch.ts +301 -0
- package/scripts/check-gate-qualification.ts +17 -4
- package/scripts/check-herdr-activation.ts +1293 -0
- package/scripts/check-herdr-fresh-call.ts +1250 -0
- package/scripts/check-herdr-placement.ts +383 -0
- package/scripts/check-herdr-plugin-build.ts +842 -0
- package/scripts/check-herdr-plugin-profile.ts +371 -0
- package/scripts/check-herdr-plugin.ts +523 -0
- package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
- package/scripts/check-herdr-sandbox.ts +456 -0
- package/scripts/check-herdr-supply.ts +184 -0
- package/scripts/check-mux-launcher-fence.ts +138 -0
- package/scripts/check-peer-facts.ts +331 -0
- package/scripts/check-pi-launch.ts +358 -0
- package/scripts/check-release-gate-outcomes.ts +19 -0
- package/scripts/check-typing-call-fence.ts +296 -0
- package/scripts/codex-birth-doctor.sh +159 -16
- package/scripts/codex-birth-install.sh +178 -72
- package/scripts/codex-birth-uninstall.sh +162 -7
- package/scripts/fixtures/herdr-supply.json +29 -0
- package/scripts/herdr-activation.mjs +536 -0
- package/scripts/herdr-plugin-activate.mjs +270 -0
- package/scripts/herdr-plugin-deactivate.mjs +193 -0
- package/scripts/herdr-runtime.mjs +1203 -0
- package/scripts/install-herdr-ci.sh +96 -0
- package/scripts/inventory-verification-surface.ts +86 -10
- package/scripts/lib/codex-fresh-live-protocol.ts +2 -2
- package/scripts/lib/tree-digest.ts +97 -0
- package/scripts/meta-bridge-install.sh +19 -2
- package/scripts/meta-bridge-state.py +167 -11
- package/scripts/mutants/codex-caller-seat.json +6 -8
- package/scripts/mutants/codex-native.json +195 -29
- package/scripts/mutants/control-socket-disconnect.json +57 -0
- package/scripts/mutants/fresh-call-dispatch.json +154 -0
- package/scripts/mutants/herdr-activation.json +305 -0
- package/scripts/mutants/herdr-fresh-call.json +448 -0
- package/scripts/mutants/herdr-placement.json +161 -0
- package/scripts/mutants/herdr-plugin-build.json +160 -0
- package/scripts/mutants/herdr-plugin-profile.json +202 -0
- package/scripts/mutants/herdr-plugin.json +133 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
- package/scripts/mutants/herdr-supply.json +133 -0
- package/scripts/mutants/meta-hook-session-switch.json +2 -2
- package/scripts/mutants/mux-fresh-call.json +104 -17
- package/scripts/mutants/mux-launcher-fence.json +13 -0
- package/scripts/mutants/omp-fresh.json +4 -4
- package/scripts/mutants/peer-facts.json +98 -0
- package/scripts/mutants/pi-launch.json +113 -0
- package/scripts/mutants/resume-args.json +6 -6
- package/scripts/mutants/self-address.json +33 -0
- package/scripts/mutants/typing-call-fence.json +17 -0
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +17 -17
- package/scripts/peer-facts.ts +120 -0
- package/scripts/register-pi-package.py +8 -0
- package/scripts/register-pi-provider.py +88 -12
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-codex-birth.sh +281 -12
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
- package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
- package/scripts/smoke-herdr-raw-install-live.sh +849 -0
- package/scripts/tsconfig.json +5 -0
- package/scripts/check-entwurf-facts.ts +0 -385
- package/scripts/check-entwurf-resume-args.ts +0 -122
- package/scripts/check-entwurf-v2-decider.ts +0 -657
- package/scripts/check-entwurf-v2-lock.ts +0 -371
- package/scripts/check-entwurf-v2-mailbox.ts +0 -261
- package/scripts/check-entwurf-v2-matrix.ts +0 -401
- package/scripts/check-entwurf-v2-native-push.ts +0 -221
- package/scripts/check-entwurf-v2-release.ts +0 -151
- package/scripts/check-entwurf-v2-runner.ts +0 -448
- package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
- package/scripts/check-entwurf-v2-send.ts +0 -458
- package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
- package/scripts/check-socket-discovery.ts +0 -401
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"gate": ["bash", "run.sh", "check-entwurf-v2-send"],
|
|
26
26
|
"timeoutSeconds": 60,
|
|
27
27
|
"signature": "[QK:V2SEND-INBAND-REJECT-REASON]",
|
|
28
|
-
"signatureSource": "
|
|
28
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-send.test.ts"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"claim": "V2PROD-INBAND-ERROR-WIRED",
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
"gate": ["bash", "run.sh", "check-entwurf-v2-native-push"],
|
|
207
207
|
"timeoutSeconds": 120,
|
|
208
208
|
"signature": "[QK:NATIVE-PUSH-RETRY-BYTE-IDENTITY]",
|
|
209
|
-
"signatureSource": "
|
|
209
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-native-push.test.ts"
|
|
210
210
|
}
|
|
211
211
|
]
|
|
212
212
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
12
12
|
"timeoutSeconds": 60,
|
|
13
13
|
"signature": "[QK:V2RESUME-LIVE-REFUSED]",
|
|
14
|
-
"signatureSource": "
|
|
14
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
17
|
"claim": "V2RESUME-INDETERMINATE-NO-START",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
23
23
|
"timeoutSeconds": 60,
|
|
24
24
|
"signature": "[QK:V2RESUME-INDETERMINATE-NO-START]",
|
|
25
|
-
"signatureSource": "
|
|
25
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
"claim": "V2RESUME-CONFLICT-NO-START",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
34
34
|
"timeoutSeconds": 60,
|
|
35
35
|
"signature": "[QK:V2RESUME-CONFLICT-NO-START]",
|
|
36
|
-
"signatureSource": "
|
|
36
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"claim": "V2RESUME-LOCKED-NO-RESOLVE",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
45
45
|
"timeoutSeconds": 60,
|
|
46
46
|
"signature": "[QK:V2RESUME-LOCKED-NO-RESOLVE]",
|
|
47
|
-
"signatureSource": "
|
|
47
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"claim": "V2RESUME-NO-TRANSCRIPT-FAILS-LOUD",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
68
68
|
"timeoutSeconds": 60,
|
|
69
69
|
"signature": "[QK:V2RESUME-NO-TRANSCRIPT-FAILS-LOUD]",
|
|
70
|
-
"signatureSource": "
|
|
70
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
"claim": "V2RESUME-OBSERVE-IS-BOUNDED-WAIT",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
79
79
|
"timeoutSeconds": 60,
|
|
80
80
|
"signature": "[QK:V2RESUME-OBSERVE-IS-BOUNDED-WAIT]",
|
|
81
|
-
"signatureSource": "
|
|
81
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
"claim": "V2RESUME-UNOBSERVED-IS-REAL",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
90
90
|
"timeoutSeconds": 60,
|
|
91
91
|
"signature": "[QK:V2RESUME-UNOBSERVED-IS-REAL]",
|
|
92
|
-
"signatureSource": "
|
|
92
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
"claim": "V2RESUME-RELEASE-FAILURE-LOUD",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
101
101
|
"timeoutSeconds": 60,
|
|
102
102
|
"signature": "[QK:V2RESUME-RELEASE-FAILURE-LOUD]",
|
|
103
|
-
"signatureSource": "
|
|
103
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
104
104
|
},
|
|
105
105
|
{
|
|
106
106
|
"claim": "V2RESUME-ARGV-FROM-RECORD",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
112
112
|
"timeoutSeconds": 60,
|
|
113
113
|
"signature": "[QK:V2RESUME-ARGV-FROM-RECORD]",
|
|
114
|
-
"signatureSource": "
|
|
114
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
117
|
"claim": "V2RESUME-RECEIPTS-SEPARATE",
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
125
125
|
"timeoutSeconds": 60,
|
|
126
126
|
"signature": "[QK:V2RESUME-RECEIPTS-SEPARATE]",
|
|
127
|
-
"signatureSource": "
|
|
127
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
"claim": "V2RESUME-RENDER-SEPARATES",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
138
138
|
"timeoutSeconds": 60,
|
|
139
139
|
"signature": "[QK:V2RESUME-RENDER-SEPARATES]",
|
|
140
|
-
"signatureSource": "
|
|
140
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
"claim": "V2RESUME-IDENTITY-BEFORE-WINDOW",
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
149
149
|
"timeoutSeconds": 60,
|
|
150
150
|
"signature": "[QK:V2RESUME-IDENTITY-BEFORE-WINDOW]",
|
|
151
|
-
"signatureSource": "
|
|
151
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
154
|
"claim": "V2RESUME-TIMEOUT-RELEASES",
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
162
162
|
"timeoutSeconds": 60,
|
|
163
163
|
"signature": "[QK:V2RESUME-TIMEOUT-RELEASES]",
|
|
164
|
-
"signatureSource": "
|
|
164
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
165
165
|
},
|
|
166
166
|
{
|
|
167
167
|
"claim": "V2RESUME-LOCK-BEFORE-LIVENESS",
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
173
173
|
"timeoutSeconds": 60,
|
|
174
174
|
"signature": "[QK:V2RESUME-LOCK-BEFORE-LIVENESS]",
|
|
175
|
-
"signatureSource": "
|
|
175
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
176
176
|
},
|
|
177
177
|
{
|
|
178
178
|
"claim": "V2RESUME-NOT-PI-REFUSED",
|
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
184
184
|
"timeoutSeconds": 60,
|
|
185
185
|
"signature": "[QK:V2RESUME-NOT-PI-REFUSED]",
|
|
186
|
-
"signatureSource": "
|
|
186
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
"claim": "V2RESUME-NOT-PI-BY-FIELD",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
199
199
|
"timeoutSeconds": 60,
|
|
200
200
|
"signature": "[QK:V2RESUME-NOT-PI-BY-FIELD]",
|
|
201
|
-
"signatureSource": "
|
|
201
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
202
202
|
},
|
|
203
203
|
{
|
|
204
204
|
"claim": "V2RESUME-SHARED-ID-GRAMMAR",
|
|
@@ -209,7 +209,7 @@
|
|
|
209
209
|
"gate": ["bash", "run.sh", "check-entwurf-v2-visible-resume"],
|
|
210
210
|
"timeoutSeconds": 60,
|
|
211
211
|
"signature": "[QK:V2RESUME-SHARED-ID-GRAMMAR]",
|
|
212
|
-
"signatureSource": "
|
|
212
|
+
"signatureSource": "pi-extensions/lib/entwurf-v2-visible-resume.test.ts"
|
|
213
213
|
}
|
|
214
214
|
]
|
|
215
215
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* peer-facts — the read-only JSON projection of the OBSERVED peer listing (#116 M2-a).
|
|
4
|
+
*
|
|
5
|
+
* `meta-facts` (#65) emits what the meta-record STORE owns. This emits what the fact
|
|
6
|
+
* PROVIDER owns: the same `listEntwurfFacts` assembly `entwurf_peers` renders, serialized
|
|
7
|
+
* instead of drawn. It exists for the same reason #65 exists — so a consumer stops
|
|
8
|
+
* carrying a decaying copy of a join it does not own.
|
|
9
|
+
*
|
|
10
|
+
* THE COPY THIS REFUSES. The placement axis joins a herdr pane to a citizen on
|
|
11
|
+
* `nativeSessionId`, and on the pi side that key is recovered from a session FILENAME by a
|
|
12
|
+
* strict conversion measured against one vendor floor (`herdr-placement.ts`
|
|
13
|
+
* piNativeSessionIdFromPath). A consumer that re-implements that join forks a vendor floor
|
|
14
|
+
* into a file no gate covers and no document owns (`docs/mux-launch-rail.md` §7). With this
|
|
15
|
+
* verb the consumer's whole join becomes ONE opaque pane-id string equality, which is a
|
|
16
|
+
* thing a shell script may correctly do.
|
|
17
|
+
*
|
|
18
|
+
* ONE PROVIDER, ONE RENDERER, NO SECOND JOIN. The payload is `renderEntwurfPeers().payload`
|
|
19
|
+
* — the same provider (`listEntwurfFacts`) through the same renderer the MCP `entwurf_peers`
|
|
20
|
+
* surface uses, so the SHAPE is that surface's payload shape and each row is a `PeerFact`
|
|
21
|
+
* whose keyset `check-entwurf-peers-surface` pins to `{peers, diagnostics}`. Nothing here
|
|
22
|
+
* recomputes, filters, re-sorts or re-words a provider fact, and nothing here re-implements
|
|
23
|
+
* the join.
|
|
24
|
+
*
|
|
25
|
+
* It is NOT the same bytes as an `entwurf_peers` call, and the difference is deliberate, not
|
|
26
|
+
* a drift: that surface is a human listing, so it passes `observationLimit` and returns only
|
|
27
|
+
* rendered TEXT over the wire. This is a machine projection, so its observation is
|
|
28
|
+
* UNBOUNDED — a rationed row would say `unobserved`, which a machine consumer cannot tell
|
|
29
|
+
* apart from "nobody could look". Older rows can therefore read `exists`/`active` here where
|
|
30
|
+
* the human surface says `unobserved`. Same facts, measured for more rows.
|
|
31
|
+
*
|
|
32
|
+
* `placement` STAYS STRUCTURED. The human surface prints `herdr <pane>` so a reader never
|
|
33
|
+
* mistakes a pane id for one of ours; a machine consumer needs the tagged union itself
|
|
34
|
+
* (`{kind:"herdr-pane",paneId}` / `unobserved` / `none` / `ambiguous`), so `renderPlacement`
|
|
35
|
+
* is NOT applied here. A pane is an ephemeral view (Hard Rule 16), never an address.
|
|
36
|
+
*
|
|
37
|
+
* WHAT IS DELIBERATELY ABSENT. No herdr `agent_status`, no `interactive_ready`, no screen
|
|
38
|
+
* text: those are herdr's own display verdicts, they are not delivery evidence
|
|
39
|
+
* (`docs/herdr-launch-rail.md` §9), and keeping them out of every entwurf payload is what
|
|
40
|
+
* makes "observed activity" unable to drift into delivery liveness. A consumer that wants
|
|
41
|
+
* to show them reads herdr itself and labels them as herdr's report.
|
|
42
|
+
*
|
|
43
|
+
* ONE PLACEMENT READ. `readPlacementIndex` is left undefined, so the provider performs its
|
|
44
|
+
* own single read per listing — the anti-watcher shape `entwurf-fact-provider.ts` documents.
|
|
45
|
+
* This verb never loops, retries, or waits for a pane whose session reference has not landed.
|
|
46
|
+
*
|
|
47
|
+
* NO SOCKET COORDINATE LEAVES THIS VERB. `ENTWURF_DIR` selects the socket world that is
|
|
48
|
+
* PROBED, and that is all it does: the control dir is never emitted. #50 C4 retired the
|
|
49
|
+
* legacy `sessions` projection "with the `controlDir` it exposed" because the record is the
|
|
50
|
+
* sole address axis and socket paths are dispatch-internal transport
|
|
51
|
+
* (`entwurf-peers-render.ts` header). Re-publishing that path under a new verb would undo
|
|
52
|
+
* that retirement, and no renderer needs it.
|
|
53
|
+
*
|
|
54
|
+
* stdout (deterministic, 2-space indent, trailing newline):
|
|
55
|
+
* {
|
|
56
|
+
* "schemaVersion": 1, // of THIS projection
|
|
57
|
+
* "storeDir": "/abs", // the meta-record store that was read
|
|
58
|
+
* "peers": [ <PeerFact, verbatim, provider order> ],
|
|
59
|
+
* "diagnostics": [ <EntwurfDiagnostic, verbatim> ]
|
|
60
|
+
* }
|
|
61
|
+
*
|
|
62
|
+
* EXIT CONTRACT — identical in shape to meta-facts, for the same reason:
|
|
63
|
+
* 0 — the store was readable; the JSON above is on stdout (diagnostics in-band).
|
|
64
|
+
* A store that does not exist is a readable EMPTY store (ENOENT only).
|
|
65
|
+
* 2 — usage error (bad argv).
|
|
66
|
+
* 3 — the store could not be READ. No JSON is emitted: an unreadable host must
|
|
67
|
+
* never look like an empty one.
|
|
68
|
+
* A provider wiring invariant (duplicate identity / unprobed in-domain citizen) is left to
|
|
69
|
+
* THROW. It is not an unreadable store and must not be dressed as one (Hard Rule 15).
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
import os from "node:os";
|
|
73
|
+
import { defaultControlSocketDir } from "../pi-extensions/lib/control-socket-path.js";
|
|
74
|
+
import { listEntwurfFacts } from "../pi-extensions/lib/entwurf-fact-provider.ts";
|
|
75
|
+
import { renderEntwurfPeers } from "../pi-extensions/lib/entwurf-peers-render.ts";
|
|
76
|
+
import {
|
|
77
|
+
type ActiveStoreEntry,
|
|
78
|
+
defaultMetaSessionsDir,
|
|
79
|
+
makeStoreRecordReader,
|
|
80
|
+
readActiveStoreEntries,
|
|
81
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
82
|
+
|
|
83
|
+
const arg = process.argv[2];
|
|
84
|
+
// A dash argv is a flag this command does not have, not a store directory — treating it as
|
|
85
|
+
// a path would answer `--help` with "empty store, exit 0", a silent wrong fact.
|
|
86
|
+
if (process.argv.length > 3 || (arg !== undefined && arg.startsWith("-"))) {
|
|
87
|
+
console.error("usage: entwurf peer-facts [meta-sessions-dir]");
|
|
88
|
+
process.exit(2);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const storeDir = arg ?? defaultMetaSessionsDir();
|
|
92
|
+
// The SAME override the bridge honours (`mcp/entwurf-bridge/src/index.ts` ENTWURF_DIR), so
|
|
93
|
+
// this verb and `entwurf_peers` probe one socket world. The pi side has no such override;
|
|
94
|
+
// the path grammar is the shared leaf either way. This value is an INPUT to the probe and
|
|
95
|
+
// never an output — see NO SOCKET COORDINATE LEAVES THIS VERB above.
|
|
96
|
+
const controlDir = process.env.ENTWURF_DIR ?? defaultControlSocketDir(os.homedir());
|
|
97
|
+
|
|
98
|
+
let metaEntries: ActiveStoreEntry[];
|
|
99
|
+
try {
|
|
100
|
+
// Entries WITH their kind, never bare names: the listing must be able to refuse a
|
|
101
|
+
// symlinked record without following it (Hard Rule 7). ENOENT is the empty store.
|
|
102
|
+
metaEntries = readActiveStoreEntries(storeDir);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error(`FAIL: ${err instanceof Error ? err.message : String(err)}`);
|
|
105
|
+
process.exit(3); // see the EXIT CONTRACT above: 3 = unreadable store, never an empty one
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = await listEntwurfFacts({
|
|
109
|
+
metaEntries,
|
|
110
|
+
readRecord: makeStoreRecordReader(storeDir),
|
|
111
|
+
socket: { dir: controlDir },
|
|
112
|
+
// No `observationLimit`: a budget would emit `unobserved` for rows nobody chose to
|
|
113
|
+
// skip, and in a machine payload that is indistinguishable from "no herdr here". The
|
|
114
|
+
// human surface may ration its own rows; a projection may not ration its facts.
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const { payload } = renderEntwurfPeers(result);
|
|
118
|
+
const projection = { schemaVersion: 1, storeDir, ...payload };
|
|
119
|
+
|
|
120
|
+
process.stdout.write(`${JSON.stringify(projection, null, 2)}\n`);
|
|
@@ -120,6 +120,14 @@ from pathlib import Path
|
|
|
120
120
|
# the first one was closed (#53 B). sys.path[0] already holds this directory when the
|
|
121
121
|
# script is run by path, which is how run.sh and every gate invoke it; the explicit
|
|
122
122
|
# insert keeps the import true under any other invocation form.
|
|
123
|
+
# The sibling import below would otherwise leave `scripts/__pycache__/pi_settings_io.*.pyc`
|
|
124
|
+
# behind — a write into the checkout this script was merely READ from. It is git-ignored, so
|
|
125
|
+
# `git status --porcelain` never shows it, and on a host where the cache already exists nothing
|
|
126
|
+
# changes; but in the gate-qualification snapshot the file is NEW, and the tree manifest that
|
|
127
|
+
# certifies "the runner never wrote the tree" reads bytes, not porcelain. Measured on the #116
|
|
128
|
+
# M3-b3 candidate: that one path was the whole of `IMPURE: treeClean=false porcelainClean=true`.
|
|
129
|
+
# Same guard, same reason as codex-mcp-config.py / codex-statusline-config.py.
|
|
130
|
+
sys.dont_write_bytecode = True
|
|
123
131
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
124
132
|
from pi_settings_io import classify_installer_root, detect_indent, dumps # noqa: E402
|
|
125
133
|
|
|
@@ -76,6 +76,14 @@ import sys
|
|
|
76
76
|
# `install` — semantically a no-op, byte-wise a RED `pnpm check`. sys.path[0] already
|
|
77
77
|
# holds this directory when the script is run by path (how run.sh and the gates invoke
|
|
78
78
|
# it); the explicit insert keeps the import true under any other invocation form.
|
|
79
|
+
# The sibling import below would otherwise leave `scripts/__pycache__/pi_settings_io.*.pyc`
|
|
80
|
+
# behind — a write into the checkout this script was merely READ from. It is git-ignored, so
|
|
81
|
+
# `git status --porcelain` never shows it, and on a host where the cache already exists nothing
|
|
82
|
+
# changes; but in the gate-qualification snapshot the file is NEW, and the tree manifest that
|
|
83
|
+
# certifies "the runner never wrote the tree" reads bytes, not porcelain. Measured on the #116
|
|
84
|
+
# M3-b3 candidate: that one path was the whole of `IMPURE: treeClean=false porcelainClean=true`.
|
|
85
|
+
# Same guard, same reason as codex-mcp-config.py / codex-statusline-config.py.
|
|
86
|
+
sys.dont_write_bytecode = True
|
|
79
87
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
80
88
|
from pi_settings_io import classify_installer_root, detect_indent, dumps, unchanged # noqa: E402
|
|
81
89
|
|
|
@@ -170,11 +178,49 @@ def _now() -> str:
|
|
|
170
178
|
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
171
179
|
|
|
172
180
|
|
|
173
|
-
|
|
181
|
+
# #116 M3-b2 — the ONE alternative managed command, and it is DERIVED, never supplied.
|
|
182
|
+
# A Herdr plugin activates on a host where nothing entwurf is on PATH, so the bare bin cannot
|
|
183
|
+
# resolve. The plugin mode therefore names the bridge by its absolute path under the certified
|
|
184
|
+
# stable active runtime — and that is the whole freedom it gets: the caller passes the runtime ROOT,
|
|
185
|
+
# this function appends the fixed suffix, and any other string is refused. There is deliberately no
|
|
186
|
+
# flag or environment variable that accepts an arbitrary command or path: that would be a generic
|
|
187
|
+
# "write whatever you like into the operator's pi settings" authority wearing a plugin's clothes.
|
|
188
|
+
PLUGIN_BIN_SUFFIX = os.path.join("node_modules", ".bin", SERVER_KEY)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def stable_active_root() -> str:
|
|
192
|
+
"""The ONE runtime address, derived from this process's XDG/HOME exactly as the runtime owner
|
|
193
|
+
derives it (`scripts/herdr-runtime.mjs` resolveRuntimeLayout). A gate pins the two
|
|
194
|
+
implementations equal; nothing here reads a caller-supplied root as authority."""
|
|
195
|
+
data_home = os.environ.get("XDG_DATA_HOME") or os.path.join(os.path.expanduser("~"), ".local", "share")
|
|
196
|
+
return os.path.join(data_home, "entwurf", "herdr-plugin", "runtime", "active")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def plugin_command_for(runtime_root: str) -> str:
|
|
200
|
+
# The caller may NAME the root, but it is only accepted when it IS this host's stable root. An
|
|
201
|
+
# absolute path was never the contract — "the stable runtime" was — and a mode that takes any
|
|
202
|
+
# absolute directory is a way to point every pi session at an executable of the caller's choice.
|
|
203
|
+
if not isinstance(runtime_root, str) or not os.path.isabs(runtime_root):
|
|
204
|
+
_die(2, f"register-pi-provider: --plugin-runtime must be an ABSOLUTE stable runtime root, got {runtime_root!r}")
|
|
205
|
+
if runtime_root != os.path.normpath(runtime_root) or runtime_root.endswith(os.sep):
|
|
206
|
+
_die(2, f"register-pi-provider: --plugin-runtime must be a normalised path, got {runtime_root!r}")
|
|
207
|
+
expected = stable_active_root()
|
|
208
|
+
if runtime_root != expected:
|
|
209
|
+
_die(2, f"register-pi-provider: --plugin-runtime must be this host's stable runtime root {expected!r}, "
|
|
210
|
+
f"got {runtime_root!r} — an arbitrary absolute directory is not a plugin runtime.")
|
|
211
|
+
return os.path.join(runtime_root, PLUGIN_BIN_SUFFIX)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _classify(existing_cmd, repo_dir: str, managed_command: str = BARE_COMMAND) -> str:
|
|
174
215
|
if existing_cmd is None:
|
|
175
216
|
return "absent"
|
|
176
|
-
if existing_cmd ==
|
|
217
|
+
if existing_cmd == managed_command:
|
|
177
218
|
return "managed-current"
|
|
219
|
+
# The OTHER shape we own is adoptable, so bare↔plugin is a normalisation rather than a
|
|
220
|
+
# takeover. In DEFAULT mode this is exactly the historical set (the bare bin is
|
|
221
|
+
# managed-current above and never reaches here), so default bytes and verdicts are unchanged.
|
|
222
|
+
if managed_command != BARE_COMMAND and existing_cmd == BARE_COMMAND:
|
|
223
|
+
return "managed-legacy"
|
|
178
224
|
if isinstance(existing_cmd, str) and (
|
|
179
225
|
existing_cmd == f"{repo_dir}/mcp/{SERVER_KEY}/start.sh"
|
|
180
226
|
or existing_cmd.endswith(f"/entwurf/mcp/{SERVER_KEY}/start.sh")
|
|
@@ -210,7 +256,8 @@ def _load_state_file(state_path: str) -> dict | None:
|
|
|
210
256
|
|
|
211
257
|
|
|
212
258
|
def cmd_install(settings_path: str, repo_dir: str, scope: str, state_path: str, takeover: bool = False,
|
|
213
|
-
preflight: bool = False) -> None:
|
|
259
|
+
preflight: bool = False, plugin_runtime: str | None = None) -> None:
|
|
260
|
+
managed_command = plugin_command_for(plugin_runtime) if plugin_runtime else BARE_COMMAND
|
|
214
261
|
if os.path.islink(settings_path):
|
|
215
262
|
target = os.readlink(settings_path)
|
|
216
263
|
_die(3, f"register-pi-provider: refusing to adopt {settings_path} — it is a symlink to {target} "
|
|
@@ -254,7 +301,7 @@ def cmd_install(settings_path: str, repo_dir: str, scope: str, state_path: str,
|
|
|
254
301
|
_prune_legacy(servers, repo_dir) # independent of entwurf-bridge ownership
|
|
255
302
|
existing = servers.get(SERVER_KEY)
|
|
256
303
|
existing_cmd = existing.get("command") if isinstance(existing, dict) else existing
|
|
257
|
-
ownership = _classify(existing_cmd, repo_dir)
|
|
304
|
+
ownership = _classify(existing_cmd, repo_dir, managed_command)
|
|
258
305
|
|
|
259
306
|
if preflight:
|
|
260
307
|
# Read-only half of the atomic user-scope operation: the ownership decision
|
|
@@ -282,8 +329,8 @@ def cmd_install(settings_path: str, repo_dir: str, scope: str, state_path: str,
|
|
|
282
329
|
sys.stdout.write(f"install: no change — {settings_path} left untouched (bytes and mtime stable)\n")
|
|
283
330
|
return
|
|
284
331
|
|
|
285
|
-
# absent / managed-current / managed-legacy → normalize to the
|
|
286
|
-
newval = {"command":
|
|
332
|
+
# absent / managed-current / managed-legacy → normalize to the managed command for this mode.
|
|
333
|
+
newval = {"command": managed_command}
|
|
287
334
|
# preserve non-empty custom args if the operator set them; else default [].
|
|
288
335
|
if isinstance(existing, dict) and existing.get("args") not in (None, []):
|
|
289
336
|
newval["args"] = existing["args"]
|
|
@@ -292,7 +339,8 @@ def cmd_install(settings_path: str, repo_dir: str, scope: str, state_path: str,
|
|
|
292
339
|
servers[SERVER_KEY] = newval
|
|
293
340
|
wrote = _persist(settings_path, before, data, raw)
|
|
294
341
|
sys.stdout.write(
|
|
295
|
-
f"install: {ownership} → entwurfProvider.mcpServers.{SERVER_KEY} = {
|
|
342
|
+
f"install: {ownership} → entwurfProvider.mcpServers.{SERVER_KEY} = {managed_command} "
|
|
343
|
+
f"({'stable plugin runtime' if plugin_runtime else 'bare stable bin'})\n"
|
|
296
344
|
)
|
|
297
345
|
# The desired value AND the legacy prune both already held: nothing to say to the
|
|
298
346
|
# file. Reported so an operator (and the gate) can tell "already correct" from
|
|
@@ -317,7 +365,7 @@ def cmd_install(settings_path: str, repo_dir: str, scope: str, state_path: str,
|
|
|
317
365
|
"managedSettingsPath": os.path.abspath(settings_path),
|
|
318
366
|
"scope": "user",
|
|
319
367
|
"key": f"entwurfProvider.mcpServers.{SERVER_KEY}",
|
|
320
|
-
"command":
|
|
368
|
+
"command": managed_command,
|
|
321
369
|
"ownership": ownership, # absent | managed-current | managed-legacy
|
|
322
370
|
"installerRoot": repo_dir, # #86 C2: the root whose inverse may remove this key
|
|
323
371
|
"preimage": existing, # raw prior value (audit only; NOT restored)
|
|
@@ -387,8 +435,23 @@ def cmd_remove(settings_path: str, repo_dir: str, scope: str, state_path: str, o
|
|
|
387
435
|
before = _parse_settings(managed, raw) # dies 4 on corrupt — preflight and writer alike
|
|
388
436
|
data = _parse_settings(managed, raw)
|
|
389
437
|
provider, servers = _provider_servers(data, create=False)
|
|
438
|
+
# #116 M3-b2 — DRIFT IS NOT OURS TO DELETE. The user-scope inverse is admitted by the
|
|
439
|
+
# ownership record, not by the command string, so without this it would remove whatever now
|
|
440
|
+
# sits at our key. The state records the exact command we wrote (bare bin, or the absolute
|
|
441
|
+
# bridge under a stable plugin runtime); if the live value is PRESENT and different, someone
|
|
442
|
+
# replaced our key after we installed it and the honest move is to refuse by name rather
|
|
443
|
+
# than delete their override. An ABSENT key stays an idempotent no-op.
|
|
444
|
+
recorded_cmd = state.get("command")
|
|
445
|
+
live = servers.get(SERVER_KEY) if isinstance(servers, dict) else None
|
|
446
|
+
live_cmd = live.get("command") if isinstance(live, dict) else live
|
|
447
|
+
if isinstance(recorded_cmd, str) and live_cmd is not None and live_cmd != recorded_cmd:
|
|
448
|
+
_die(6, f"register-pi-provider: the user-scope {SERVER_KEY} command drifted since install — "
|
|
449
|
+
f"we wrote {recorded_cmd!r}, {managed} now holds {live_cmd!r}. That is somebody's override, "
|
|
450
|
+
"not our key; zero settings bytes written and the ownership state is left intact.")
|
|
390
451
|
if preflight:
|
|
391
|
-
sys.stdout.write(
|
|
452
|
+
sys.stdout.write(
|
|
453
|
+
f"preflight: remove ok (owner verified; managed target bound and parseable; command={recorded_cmd!r})\n"
|
|
454
|
+
)
|
|
392
455
|
return
|
|
393
456
|
if os.path.exists(managed):
|
|
394
457
|
# honest inverse: absent/managed-* → remove OUR key (a legacy repo path is NOT
|
|
@@ -442,6 +505,7 @@ def cmd_remove(settings_path: str, repo_dir: str, scope: str, state_path: str, o
|
|
|
442
505
|
|
|
443
506
|
|
|
444
507
|
def _parse(argv: list):
|
|
508
|
+
plugin_runtime = None
|
|
445
509
|
# positional: settings_path repo_dir ; flags: --scope <s> [--state <p>] [--takeover] [--orphan-cleanup] [--preflight]
|
|
446
510
|
pos, scope, state_path = [], None, ""
|
|
447
511
|
takeover, orphan, preflight = False, False, False
|
|
@@ -460,17 +524,24 @@ def _parse(argv: list):
|
|
|
460
524
|
orphan = True
|
|
461
525
|
elif a == "--preflight":
|
|
462
526
|
preflight = True
|
|
527
|
+
elif a == "--plugin-runtime":
|
|
528
|
+
i += 1
|
|
529
|
+
# A flag with no value must NOT fall through to bare mode: the caller asked for the
|
|
530
|
+
# plugin runtime and would otherwise be told, silently, that it got it.
|
|
531
|
+
if i >= len(argv) or not argv[i]:
|
|
532
|
+
_die(5, "register-pi-provider.py: --plugin-runtime requires a value")
|
|
533
|
+
plugin_runtime = argv[i]
|
|
463
534
|
else:
|
|
464
535
|
pos.append(a)
|
|
465
536
|
i += 1
|
|
466
|
-
return pos, scope, state_path, takeover, orphan, preflight
|
|
537
|
+
return pos, scope, state_path, takeover, orphan, preflight, plugin_runtime
|
|
467
538
|
|
|
468
539
|
|
|
469
540
|
def main(argv: list) -> None:
|
|
470
541
|
if len(argv) < 2:
|
|
471
542
|
_die(5, "usage: register-pi-provider.py <install|remove> <settings_path> <repo_dir> --scope <user|project> [--state <path>]")
|
|
472
543
|
sub = argv[1]
|
|
473
|
-
pos, scope, state_path, takeover, orphan, preflight = _parse(argv[2:])
|
|
544
|
+
pos, scope, state_path, takeover, orphan, preflight, plugin_runtime = _parse(argv[2:])
|
|
474
545
|
if sub not in ("install", "remove"):
|
|
475
546
|
_die(5, f"register-pi-provider.py: unknown subcommand {sub!r}")
|
|
476
547
|
if len(pos) != 2:
|
|
@@ -486,8 +557,13 @@ def main(argv: list) -> None:
|
|
|
486
557
|
_die(5, "register-pi-provider.py: --takeover is an install action")
|
|
487
558
|
if orphan and sub != "remove":
|
|
488
559
|
_die(5, "register-pi-provider.py: --orphan-cleanup is a remove action")
|
|
560
|
+
# The plugin mode is an INSTALL-side USER-scope decision only. `remove` needs no mode: it
|
|
561
|
+
# recognises every shape we own through the same classifier, so an inverse never has to be told
|
|
562
|
+
# which one it is undoing — and a mode flag on remove would be a way to aim a deletion.
|
|
563
|
+
if plugin_runtime is not None and (sub != "install" or scope != "user"):
|
|
564
|
+
_die(5, "register-pi-provider.py: --plugin-runtime is a user-scope install action")
|
|
489
565
|
if sub == "install":
|
|
490
|
-
cmd_install(settings_path, repo_dir, scope, state_path, takeover, preflight)
|
|
566
|
+
cmd_install(settings_path, repo_dir, scope, state_path, takeover, preflight, plugin_runtime)
|
|
491
567
|
else:
|
|
492
568
|
cmd_remove(settings_path, repo_dir, scope, state_path, orphan, preflight)
|
|
493
569
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// LIVE=1 ./run.sh smoke-acp-raw-turn-live
|
|
4
4
|
//
|
|
5
5
|
// What this proves (and ONLY this): the pinned Claude ACP adapter
|
|
6
|
-
// (@agentclientprotocol/claude-agent-acp@0.
|
|
6
|
+
// (@agentclientprotocol/claude-agent-acp@0.79.0) spawns, speaks the ACP wire
|
|
7
7
|
// protocol over stdio NDJSON, and returns one real model turn. It is the
|
|
8
8
|
// bytes-flow proof that the S2a dep surface is not just installable but
|
|
9
9
|
// actually drivable — before any provider/overlay/streamSimple code (S2b+).
|