@junghanacs/entwurf 0.14.1 → 0.15.0
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 +8 -5
- package/CHANGELOG.md +113 -0
- package/DELIVERY.md +73 -3
- package/README.md +55 -4
- package/VERIFY.md +6 -6
- package/demo/README.md +3 -1
- package/demo/demo-baseline.sh +12 -1
- package/demo/demo.sh +9 -1
- package/docs/acp-backend-rail.md +103 -4
- package/docs/external-mcp-host.md +26 -4
- package/docs/setup-clean-host.md +3 -3
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +8 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/copilot-fresh-preflight.js +253 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +10 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +102 -28
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +77 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-copilot.js +288 -0
- package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +139 -47
- package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +294 -0
- package/mcp/entwurf-bridge/src/index.ts +10 -6
- package/mcp/entwurf-bridge/tsconfig.build.json +17 -6
- package/package.json +23 -12
- package/pi/copilot-receive/entwurf-receive/extension.mjs +323 -0
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/meta-bridge-copilot/.claude-plugin/marketplace.json +11 -0
- package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/hooks/hooks.json +7 -0
- package/pi/meta-bridge-copilot/entwurf-meta-receive-copilot/scripts/copilot-hook-launch.sh +85 -0
- package/pi-extensions/entwurf-control.ts +17 -8
- package/pi-extensions/lib/acp/backend.ts +229 -9
- package/pi-extensions/lib/copilot-fresh-preflight.ts +251 -0
- package/pi-extensions/lib/meta-sender-identity.ts +10 -2
- package/pi-extensions/lib/meta-session.ts +102 -29
- package/pi-extensions/lib/mux-fresh-call.ts +82 -11
- package/pi-extensions/meta-bridge-hook-copilot.ts +323 -0
- package/run.sh +389 -26
- package/scripts/agy-bridge-config.py +47 -13
- package/scripts/agy-bridge.sh +73 -23
- package/scripts/check-acp-prompt-lifecycle.ts +221 -9
- package/scripts/check-copilot-birth-hook.ts +497 -0
- package/scripts/check-copilot-launch.ts +395 -0
- package/scripts/check-copilot-receive-arm.ts +734 -0
- package/scripts/check-copilot-statusline.ts +122 -0
- package/scripts/check-entwurf-bridge-boot.ts +28 -0
- package/scripts/check-entwurf-capabilities.ts +25 -6
- package/scripts/check-fresh-cut-gate.sh +92 -23
- package/scripts/check-gate-qualification.ts +10 -3
- package/scripts/check-install-container.sh +2 -2
- package/scripts/check-install-surface.ts +1 -1
- package/scripts/check-meta-capability-source.ts +89 -5
- package/scripts/check-meta-doctor-oracle.sh +26 -0
- package/scripts/check-meta-manifest-schema.py +38 -0
- package/scripts/check-meta-receiver-marker.ts +25 -2
- package/scripts/check-meta-session.ts +32 -0
- package/scripts/check-probe-bridge-command.ts +201 -0
- package/scripts/check-release-gate-outcomes.ts +54 -1
- package/scripts/copilot-bridge-doctor.sh +209 -0
- package/scripts/copilot-bridge-install.sh +224 -0
- package/scripts/copilot-launch.sh +236 -0
- package/scripts/copilot-mcp-bridge.sh +177 -0
- package/scripts/copilot-mcp-config.py +221 -0
- package/scripts/copilot-receive-bridge.sh +483 -0
- package/scripts/copilot-statusline-bridge.sh +149 -0
- package/scripts/copilot-statusline-config.py +201 -0
- package/scripts/copilot-statusline.sh +68 -0
- package/scripts/dev-bin.sh +4 -1
- package/scripts/doctor-pi-provider.ts +155 -51
- package/scripts/meta-bridge-hook-log.sh +17 -5
- package/scripts/mutants/acp-prompt-lifecycle.json +25 -3
- package/scripts/mutants/bridge-command-boot.json +107 -0
- package/scripts/mutants/capability-cache.json +64 -0
- package/scripts/mutants/copilot-birth.json +155 -0
- package/scripts/mutants/copilot-launch.json +187 -0
- package/scripts/mutants/copilot-receive.json +263 -0
- package/scripts/mutants/fresh-cut.json +17 -0
- package/scripts/mutants/mux-fresh-call.json +216 -2
- package/scripts/mutants/pack-install.json +17 -0
- package/scripts/mutants/release-gate.json +13 -0
- package/scripts/probe-bridge-command.ts +330 -0
- package/scripts/raw-async-delivery/README.md +203 -1
- package/scripts/raw-async-delivery/copilot-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/copilot-extension-receive/extension.mjs +123 -0
- package/scripts/raw-async-delivery/copilot-ui-server-probe.mjs +337 -0
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-agy-install-state.sh +76 -2
- package/scripts/smoke-copilot-mcp-state.sh +153 -0
- package/scripts/smoke-copilot-statusline-state.sh +131 -0
- package/scripts/smoke-entwurf-chain-live.ts +1 -1
- package/scripts/smoke-entwurf-v2-matrix-live.ts +2 -2
- package/scripts/smoke-mux-fresh-call-live.ts +3 -1
- package/scripts/smoke-mux-lifecycle-live.ts +4 -2
- package/scripts/smoke-pi-provider-state.sh +135 -6
- package/scripts/smoke-resident-garden-guard.sh +2 -2
- package/scripts/tsconfig.json +1 -0
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* 1. RECORD functions + types (mint / serialize / parse / certifyActiveStore /
|
|
13
13
|
* decideUpsert), the backend-agnostic authority. Pure beyond an injected
|
|
14
14
|
* `now`; backend capability (wakeMode/deliveryLevel) comes from the packaged
|
|
15
|
-
* registry
|
|
16
|
-
* below.
|
|
15
|
+
* registry, re-read on every load (loadMetaCapabilityRegistry holds NO cache, so a
|
|
16
|
+
* grade moved on disk is visible to a long-lived process) — see that seam below.
|
|
17
17
|
* 2. The thin FS-BOUND STORE (step 3): `upsertMetaSession` wraps the pure core
|
|
18
18
|
* (readdir → `certifyActiveStore` → `decideUpsert` → atomic write) with the real
|
|
19
19
|
* filesystem. It lives in this module (not a sibling `*-store.ts`) on purpose:
|
|
@@ -74,12 +74,14 @@ export class MetaRecordError extends Error {
|
|
|
74
74
|
// ---------------------------------------------------------------------------
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* Native meta-bridge backends. Discriminator on every record. Adding a member
|
|
78
|
+
* is a sibling-reader redeploy (`./run.sh install-meta-bridge` and the matching
|
|
79
|
+
* sibling install), not a store archive: writers certify the whole active store
|
|
80
|
+
* against THIS set, so a deployed plugin still carrying the old set refuses
|
|
81
|
+
* every write, including its own. `./run.sh doctor-meta-bridge` already flags a
|
|
82
|
+
* stale deployed writer — run it after the change.
|
|
81
83
|
*/
|
|
82
|
-
export const META_BACKENDS = ["claude-code", "antigravity", "codex"] as const;
|
|
84
|
+
export const META_BACKENDS = ["claude-code", "antigravity", "codex", "copilot"] as const;
|
|
83
85
|
export type MetaBackend = (typeof META_BACKENDS)[number];
|
|
84
86
|
|
|
85
87
|
/**
|
|
@@ -127,6 +129,47 @@ export const META_BACKEND_DESCRIPTORS: Record<MetaBackend, MetaBackendDescriptor
|
|
|
127
129
|
deliveryLevel: "D6",
|
|
128
130
|
nativeIdLabel: "threadId",
|
|
129
131
|
},
|
|
132
|
+
// SELF-FETCH citizen since #82 RAIL 5. Copilot CLI 1.0.80 runs our plugin hook (birth,
|
|
133
|
+
// garden address, who-sent) AND forks our first-party extension, whose `joinSession()`
|
|
134
|
+
// holds an `fs.watch` on the garden mailbox signal and can `session.send()` a doorbell
|
|
135
|
+
// into an idle session. That is the same shape Claude's mailbox has, reached through a
|
|
136
|
+
// different vendor surface. Hence:
|
|
137
|
+
// wakeMode self-fetch — the doorbell announces, the model drains its own inbox with
|
|
138
|
+
// `entwurf_inbox_read`, and THAT read is the receipt. The extension never
|
|
139
|
+
// injects the body, so this label promises exactly what happens. It was
|
|
140
|
+
// `direct-inject` while no doorbell existed (measured 2026-08-20: no
|
|
141
|
+
// `FileChanged`, `asyncRewake` or `watchPaths` in the bundle) — that
|
|
142
|
+
// absence was never a claim about the vendor's other surfaces, and the
|
|
143
|
+
// extension rail is the one it missed. Copilot is still NOT a
|
|
144
|
+
// `nativePushSupported` backend and has no native-push adapter.
|
|
145
|
+
// D6 the PRODUCT grade, earned by the managed LIVE acceptance of 2026-08-23
|
|
146
|
+
// (garden 20260823T181316-d9f6ba, native 20fe30c8-b2bc-4600-91a0-8a409131be51,
|
|
147
|
+
// CLI 1.0.80): the receive log joins that native id and arms that garden id,
|
|
148
|
+
// the mailbox state stamps lastEnqueuedAt 09:23:41.235Z and lastReadAt
|
|
149
|
+
// 09:23:56.480Z, and the model answered on the SAME record/native/gid chain.
|
|
150
|
+
// This is still the PRODUCT grade and not the raw probe's: the earlier
|
|
151
|
+
// transport probe was evidence about the mechanism, and it did not move this
|
|
152
|
+
// number — a managed round trip did.
|
|
153
|
+
// D7 is PARTIAL, deliberately: the reply and the read receipt were observed,
|
|
154
|
+
// but the completion taxonomy as a whole and any long-haul operation were
|
|
155
|
+
// not, and the reply envelope itself reaches this comment as an INHERITED
|
|
156
|
+
// fact (it was not re-read from a transcript when this was written). D8 is
|
|
157
|
+
// unproven and D3 (managed second-session isolation) is pending — its
|
|
158
|
+
// decisive log was lost to a scratch cleanup before it could be preserved.
|
|
159
|
+
// The route stays fail-closed on both ends: with no armed receiver marker
|
|
160
|
+
// every dispatch is still refused `mailbox-undeliverable`, so replyability
|
|
161
|
+
// is a fact about a live marker and never a constant of this backend.
|
|
162
|
+
// sessionId the native join key, measured to be ONE id across all three surfaces:
|
|
163
|
+
// the hook envelope, `record.nativeSessionId`, and the SDK's
|
|
164
|
+
// `session.sessionId` (record 20260823T112003-9d069a ==
|
|
165
|
+
// `ARMED sessionId=4fc16d8d-473d-4258-a1fd-f99d3cb375e9`, CLI 1.0.80).
|
|
166
|
+
// That agreement is what lets the extension bind its arm to the record.
|
|
167
|
+
copilot: {
|
|
168
|
+
backend: "copilot",
|
|
169
|
+
wakeMode: "self-fetch",
|
|
170
|
+
deliveryLevel: "D6",
|
|
171
|
+
nativeIdLabel: "sessionId",
|
|
172
|
+
},
|
|
130
173
|
};
|
|
131
174
|
|
|
132
175
|
// ---------------------------------------------------------------------------
|
|
@@ -140,9 +183,9 @@ export function requireNonEmptyString(value: unknown, field: string): string {
|
|
|
140
183
|
return value;
|
|
141
184
|
}
|
|
142
185
|
|
|
143
|
-
/** Validate the
|
|
186
|
+
/** Validate the native-bridge backend axis (sender/receiver markers, capability
|
|
144
187
|
* drift guard). Not a record-schema validator: identity records take
|
|
145
|
-
* `requireCitizenBackend` (which admits `pi`). Markers stay
|
|
188
|
+
* `requireCitizenBackend` (which admits `pi`). Markers stay on this set because the
|
|
146
189
|
* pi adapter carries its record-established garden id into children via env rather
|
|
147
190
|
* than using the native-hook pid marker rail. */
|
|
148
191
|
export function requireBackend(value: unknown): MetaBackend {
|
|
@@ -210,7 +253,7 @@ function isoNow(now: Date): string {
|
|
|
210
253
|
export const META_SCHEMA_VERSION_V3 = 3 as const;
|
|
211
254
|
|
|
212
255
|
/** Every backend admitted by the one V3 record-citizen schema. */
|
|
213
|
-
export const META_CITIZEN_BACKENDS = ["claude-code", "antigravity", "codex", "pi"] as const;
|
|
256
|
+
export const META_CITIZEN_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "pi"] as const;
|
|
214
257
|
export type MetaCitizenBackend = (typeof META_CITIZEN_BACKENDS)[number];
|
|
215
258
|
|
|
216
259
|
/**
|
|
@@ -655,19 +698,30 @@ export function metaCapabilitiesFilePath(): string {
|
|
|
655
698
|
// the rest of `delivery{}`, so today the registry is the sole home.
|
|
656
699
|
// ---------------------------------------------------------------------------
|
|
657
700
|
|
|
658
|
-
/** Memoized packaged registry; the file is immutable at runtime, so caching is honest (not stateful lying). */
|
|
659
|
-
let cachedMetaCapabilities: MetaCapabilityRegistry | null = null;
|
|
660
|
-
|
|
661
701
|
/**
|
|
662
|
-
* Load
|
|
663
|
-
*
|
|
664
|
-
*
|
|
702
|
+
* Load the packaged capability registry — the live source of backend honesty metadata
|
|
703
|
+
* as of 3D-3. A missing/corrupt file throws (the registry is a packaged invariant;
|
|
704
|
+
* check-pack guarantees its presence).
|
|
705
|
+
*
|
|
706
|
+
* NO CACHE, deliberately. This used to memoize into a process-lifetime singleton on the
|
|
707
|
+
* argument that "the file is immutable at runtime". That argument was false for the one
|
|
708
|
+
* process that matters: the entwurf-bridge MCP child lives as long as its harness
|
|
709
|
+
* session, so a grade or wakeMode moved by an install/upgrade was invisible to every
|
|
710
|
+
* already-running dispatcher until the operator restarted it — and nothing anywhere
|
|
711
|
+
* said so. The symptom is a citizen answering with last week's capability, which is the
|
|
712
|
+
* silent-wrong-answer class this repo refuses; "restart your session" is an instruction
|
|
713
|
+
* to work around a defect, not a fix for it (#82 RAIL 7).
|
|
714
|
+
*
|
|
715
|
+
* The subtraction is the whole repair, and it is deliberately not an invalidation
|
|
716
|
+
* scheme. Stat-based invalidation (mtime+size) cannot see an atomic same-size,
|
|
717
|
+
* same-timestamp replacement, so it would trade a certain staleness bug for an
|
|
718
|
+
* intermittent one; content hashing would have to read the file anyway. The registry is
|
|
719
|
+
* a few hundred bytes and both production callers — the v2 decider and production
|
|
720
|
+
* deliverability — reach it once per dispatch, not in a loop, so reading it there is
|
|
721
|
+
* cheaper than being wrong.
|
|
665
722
|
*/
|
|
666
723
|
export function loadMetaCapabilityRegistry(): MetaCapabilityRegistry {
|
|
667
|
-
|
|
668
|
-
cachedMetaCapabilities = parseMetaCapabilityRegistry(fs.readFileSync(metaCapabilitiesFilePath(), "utf8"));
|
|
669
|
-
}
|
|
670
|
-
return cachedMetaCapabilities;
|
|
724
|
+
return parseMetaCapabilityRegistry(fs.readFileSync(metaCapabilitiesFilePath(), "utf8"));
|
|
671
725
|
}
|
|
672
726
|
|
|
673
727
|
/**
|
|
@@ -928,15 +982,18 @@ export function certifyActiveStoreDir(dir: string): ActiveStoreCertification & {
|
|
|
928
982
|
|
|
929
983
|
/**
|
|
930
984
|
* The refusal an uncertifiable ACTIVE store earns, in the words both the runtime
|
|
931
|
-
* writers and the install doctor use.
|
|
932
|
-
*
|
|
933
|
-
*
|
|
985
|
+
* writers and the install doctor use. Rotten records (previous generation,
|
|
986
|
+
* corruption, drift, duplicate, symlink) are answered by archiving. An
|
|
987
|
+
* unknown-backend defect is a stale reader, not a rotten store — the listed
|
|
988
|
+
* defect says so; this headline tells the operator to distinguish. No branch:
|
|
989
|
+
* both sentences always print.
|
|
934
990
|
*/
|
|
935
991
|
export function activeStoreRefusal(cert: ActiveStoreCertification & { dir: string }, shown = 3): string {
|
|
936
992
|
const headline =
|
|
937
993
|
`meta-record store ${cert.dir} holds ${cert.defects.length} entry/entries this generation cannot certify — ` +
|
|
938
994
|
`refusing to write (the active store is v3-only and carries no cross-generation continuity). ` +
|
|
939
|
-
`Archive the generation and open a fresh one with ${FRESH_CUT_PRESCRIPTION}
|
|
995
|
+
`Archive the generation and open a fresh one with ${FRESH_CUT_PRESCRIPTION}. ` +
|
|
996
|
+
`If a listed defect says "backend" must be one of this reader's known set, the reader is stale — redeploy the sibling unit (the matching doctor names the install verb); do not archive those records.`;
|
|
940
997
|
// `shown = 0` is for a caller that ALREADY printed every cause per entry (the
|
|
941
998
|
// store-doctor): repeating them here doubles the wall on a large previous
|
|
942
999
|
// generation, which is exactly the aggregation lesson F8 taught. The count and
|
|
@@ -1608,12 +1665,28 @@ export function readMetaSenderMarker(opts: ReadMetaSenderMarkerOptions): MetaSen
|
|
|
1608
1665
|
// ── meta-receiver presence marker (SE-2 active-receiver signal) ──────────────
|
|
1609
1666
|
|
|
1610
1667
|
/**
|
|
1611
|
-
* The arm-capable
|
|
1612
|
-
*
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1668
|
+
* The arm-capable events, across every backend that can arm an idle wake.
|
|
1669
|
+
*
|
|
1670
|
+
* CLAUDE (`session-start`, `cwd-changed`, `file-changed`): only these hook events can
|
|
1671
|
+
* emit watchPaths (and therefore arm the idle-wake), so only these write a receiver
|
|
1672
|
+
* presence marker. UserPromptSubmit is deliberately absent: it can backfill the record
|
|
1673
|
+
* but cannot re-arm the watch, so it must NOT mint or refresh an "active receiver"
|
|
1674
|
+
* claim it cannot back.
|
|
1675
|
+
*
|
|
1676
|
+
* COPILOT (`extension-join`): the same rule, one backend over. Copilot's arm is not a
|
|
1677
|
+
* hook at all — the CLI forks a first-party extension and the extension's own
|
|
1678
|
+
* `joinSession()` is what puts a live `fs.watch` on the garden mailbox signal and gives
|
|
1679
|
+
* it a `session.send()` channel to ring. The membership test is unchanged and is about
|
|
1680
|
+
* capability, not event vocabulary: this provenance is admitted because the process that
|
|
1681
|
+
* writes it holds the watch and can start a turn on an idle session. A Copilot hook event
|
|
1682
|
+
* has neither, which is why the birth hook still arms nothing (#82 RAIL 5).
|
|
1615
1683
|
*/
|
|
1616
|
-
export const META_RECEIVER_ARM_PROVENANCES = [
|
|
1684
|
+
export const META_RECEIVER_ARM_PROVENANCES = [
|
|
1685
|
+
"session-start",
|
|
1686
|
+
"cwd-changed",
|
|
1687
|
+
"file-changed",
|
|
1688
|
+
"extension-join",
|
|
1689
|
+
] as const;
|
|
1617
1690
|
export type MetaReceiverArmProvenance = (typeof META_RECEIVER_ARM_PROVENANCES)[number];
|
|
1618
1691
|
|
|
1619
1692
|
function requireArmProvenance(value: unknown): MetaReceiverArmProvenance {
|
|
@@ -47,6 +47,11 @@
|
|
|
47
47
|
|
|
48
48
|
import { randomBytes } from "node:crypto";
|
|
49
49
|
import { classifyTmuxCwd, type TmuxCwdRejectReason } from "./classify-tmux-cwd.ts";
|
|
50
|
+
import {
|
|
51
|
+
COPILOT_PREFLIGHT_HINT,
|
|
52
|
+
type CopilotPreflightRejectReason,
|
|
53
|
+
copilotFreshPreflight,
|
|
54
|
+
} from "./copilot-fresh-preflight.ts";
|
|
50
55
|
import {
|
|
51
56
|
assertLaunchTarget,
|
|
52
57
|
LaunchPreconditionError,
|
|
@@ -66,27 +71,47 @@ import {
|
|
|
66
71
|
type WindowHandle,
|
|
67
72
|
} from "./mux-placement.ts";
|
|
68
73
|
|
|
69
|
-
/** The
|
|
70
|
-
* not a config entry. */
|
|
71
|
-
export const FRESH_CALL_BACKENDS = ["pi", "claude-code"] as const;
|
|
74
|
+
/** The backends this rail can open. Fixed set, not a profile — a further one is a decision,
|
|
75
|
+
* not a config entry. `copilot` was added by #82 RAIL 9 under the step 9 admission contract. */
|
|
76
|
+
export const FRESH_CALL_BACKENDS = ["pi", "claude-code", "copilot"] as const;
|
|
72
77
|
export type FreshCallBackend = (typeof FRESH_CALL_BACKENDS)[number];
|
|
73
78
|
|
|
74
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* The fixed runtime each backend resolves on PATH. Same reason `mux-launch` uses PATH rather
|
|
75
81
|
* than a compiled-in location: the official binary is whatever the operator's environment gives
|
|
76
|
-
* when they type the name.
|
|
82
|
+
* when they type the name.
|
|
83
|
+
*
|
|
84
|
+
* `copilot` resolves `entwurf`, NOT the vendor CLI, and that is the contract rather than a
|
|
85
|
+
* convenience. Step 9 clause 1 requires ONE fixed MANAGED runtime path, and a bare `copilot`
|
|
86
|
+
* is not one: it would start without the `COPILOT_CLI_ENABLED_FEATURE_FLAGS=EXTENSIONS` token
|
|
87
|
+
* whose absence skips the extension scan SILENTLY, so the sibling would look launched and could
|
|
88
|
+
* never be delivered to. `entwurf copilot` is the accepted managed invocation that owns that
|
|
89
|
+
* flag, its recursion fence and its receiver precondition; fresh call reaches Copilot only
|
|
90
|
+
* through it. The cost is named: a Copilot fresh call needs a current `entwurf` on PATH, the
|
|
91
|
+
* way a pi fresh call needs `pi`.
|
|
92
|
+
*/
|
|
77
93
|
export const FRESH_CALL_RUNTIME: Record<FreshCallBackend, string> = {
|
|
78
94
|
pi: "pi",
|
|
79
95
|
"claude-code": "claude",
|
|
96
|
+
copilot: "entwurf",
|
|
80
97
|
};
|
|
81
98
|
|
|
82
99
|
/**
|
|
83
100
|
* The callback tool NAME differs per backend and that is not cosmetic: native pi exposes the
|
|
84
|
-
* capability directly (`entwurf_v2`), while
|
|
85
|
-
*
|
|
101
|
+
* capability directly (`entwurf_v2`), while an MCP-hosted session reaches it under whatever
|
|
102
|
+
* name that harness composes. Naming the wrong one costs the whole first turn.
|
|
103
|
+
*
|
|
104
|
+
* `[측정]` Copilot CLI 1.0.80 composes `<mcpServerName>-<mcpToolName>` — NOT Claude Code's
|
|
105
|
+
* `mcp__<server>__<tool>`. Read from two independent sessions' own event logs
|
|
106
|
+
* (`~/.copilot/session-state/<id>/events.jsonl`): `assistant.message.toolRequests[].name` and
|
|
107
|
+
* `tool.execution_start.toolName` both carry `entwurf-bridge-entwurf_v2`, with
|
|
108
|
+
* `mcpServerName`/`mcpToolName` beside them as the parts. Derive-and-measure, never copy a
|
|
109
|
+
* sibling's spelling (`docs/adding-a-harness.md` step 5).
|
|
86
110
|
*/
|
|
87
111
|
export const FRESH_CALL_CALLBACK_TOOL: Record<FreshCallBackend, string> = {
|
|
88
112
|
pi: "entwurf_v2",
|
|
89
113
|
"claude-code": "mcp__entwurf-bridge__entwurf_v2",
|
|
114
|
+
copilot: "entwurf-bridge-entwurf_v2",
|
|
90
115
|
};
|
|
91
116
|
|
|
92
117
|
/** Mirrors the `entwurf_v2` message bound. This is an INTERFACE cap for symmetry with the
|
|
@@ -112,12 +137,41 @@ export function isSafeFreshCallModel(model: string): boolean {
|
|
|
112
137
|
* Flag-first submitted no message; Pi rejects the equals form for `--model`.
|
|
113
138
|
* claude-code — prompt, then `--allowedTools=` and `--model=` as ONE token each. The space form
|
|
114
139
|
* for allowedTools is variadic and eats the prompt as an option value.
|
|
140
|
+
* copilot — the managed VERB first, then the prompt as the value of `-i/--interactive`,
|
|
141
|
+
* `--model`, value as two tokens, and the policy as the explicit `--yolo`
|
|
142
|
+
* token. Measured from `copilot --help` (1.0.80).
|
|
115
143
|
*
|
|
116
|
-
* Both failures looked identical from outside: window open, record and socket minted,
|
|
144
|
+
* Both pi/claude failures looked identical from outside: window open, record and socket minted,
|
|
145
|
+
* no turn.
|
|
117
146
|
*
|
|
118
|
-
*
|
|
119
|
-
* permitted, so the option's effect was unobservable. What was observed is
|
|
120
|
-
* to the argv. Permission stays a documented host precondition.
|
|
147
|
+
* For pi and claude-code the equals form is NOT a permission guarantee — on the measured host
|
|
148
|
+
* the tool was already permitted, so the option's effect was unobservable. What was observed is
|
|
149
|
+
* that it does no harm to the argv. Permission stays a documented host precondition there.
|
|
150
|
+
*
|
|
151
|
+
* Copilot is the backend where the policy IS carried explicitly (step 9 clause 2), and three of
|
|
152
|
+
* its argv facts are load-bearing:
|
|
153
|
+
*
|
|
154
|
+
* - `copilot` is argv[0] of the RUNTIME `entwurf`, i.e. the managed verb — see
|
|
155
|
+
* `FRESH_CALL_RUNTIME`. Everything after it is forwarded byte-identical by
|
|
156
|
+
* `scripts/copilot-launch.sh`.
|
|
157
|
+
* - the prompt rides `--interactive`, never `-p/--prompt`: `-p` runs the prompt and EXITS,
|
|
158
|
+
* which would close the window on a sibling that is supposed to stay open and be delivered
|
|
159
|
+
* to. `--interactive <prompt>` is non-variadic, so the space form is safe here.
|
|
160
|
+
* - the policy token is `--yolo`, STATED HERE rather than left to the launcher: the launcher
|
|
161
|
+
* injects `--yolo` only when the argv names no policy, and step 9 clause 2 requires the
|
|
162
|
+
* fresh composition to state its model and permission policy explicitly, never to rely
|
|
163
|
+
* invisibly on someone else's default.
|
|
164
|
+
*
|
|
165
|
+
* `--yolo` is a GLG operator decision, not a drifted default. The first cut passed a
|
|
166
|
+
* callback-only `--allow-tool=entwurf-bridge(entwurf_v2)` grant, and GLG's 2026-08-25 operator
|
|
167
|
+
* LIVE measured the consequence: the fresh sibling's footer showed no `YOLO`, and every tool
|
|
168
|
+
* its task needed stopped on a confirmation prompt, which made the sibling impractical to work
|
|
169
|
+
* with. GLG then set the policy explicitly: a fresh Copilot sibling carries the same managed
|
|
170
|
+
* `--yolo` profile a human-typed `entwurf copilot` gets. (Copilot 1.0.80 help: `--yolo` = all
|
|
171
|
+
* tools + all paths + all URLs.) The permission GRAMMAR lesson from that first cut — Copilot's
|
|
172
|
+
* `--allow-tool` takes `<mcp-server-name>(tool-name?)`, a different dialect from the
|
|
173
|
+
* model-facing tool name — stays recorded in `docs/adding-a-harness.md` step 9's worked
|
|
174
|
+
* example; it is a measured vendor fact even though this argv no longer uses it.
|
|
121
175
|
*/
|
|
122
176
|
export function buildBackendArgs(backend: FreshCallBackend, prompt: string, model: string): string[] {
|
|
123
177
|
switch (backend) {
|
|
@@ -125,6 +179,8 @@ export function buildBackendArgs(backend: FreshCallBackend, prompt: string, mode
|
|
|
125
179
|
return [prompt, "--entwurf-control", "--model", model];
|
|
126
180
|
case "claude-code":
|
|
127
181
|
return [prompt, `--allowedTools=${FRESH_CALL_CALLBACK_TOOL["claude-code"]}`, `--model=${model}`];
|
|
182
|
+
case "copilot":
|
|
183
|
+
return ["copilot", "--interactive", prompt, "--model", model, "--yolo"];
|
|
128
184
|
}
|
|
129
185
|
}
|
|
130
186
|
|
|
@@ -167,6 +223,7 @@ export type FreshCallRejectReason =
|
|
|
167
223
|
| PlacementRejectReason
|
|
168
224
|
| LaunchRejectReason
|
|
169
225
|
| TmuxCwdRejectReason
|
|
226
|
+
| CopilotPreflightRejectReason
|
|
170
227
|
| "caller-identity-unavailable"
|
|
171
228
|
| "model-empty"
|
|
172
229
|
| "model-invalid"
|
|
@@ -270,6 +327,17 @@ export function freshCall(
|
|
|
270
327
|
throw err;
|
|
271
328
|
}
|
|
272
329
|
|
|
330
|
+
// Backend capability, still PRE-MUTATION (step 9 clause 3). It runs AFTER the runtime is
|
|
331
|
+
// proven, because "entwurf is not on PATH" is the more fundamental answer — telling an
|
|
332
|
+
// operator to run `entwurf install-copilot-bridge` when they have no `entwurf` at all sends
|
|
333
|
+
// them to the wrong repair. It runs BEFORE placement for the reason this whole ordering
|
|
334
|
+
// exists: a refusal here cannot leave a window behind, while the launcher's own equivalent
|
|
335
|
+
// check (receiver only, manual `entwurf copilot`) necessarily runs after one is open.
|
|
336
|
+
if (params.backend === "copilot") {
|
|
337
|
+
const missing = copilotFreshPreflight(env);
|
|
338
|
+
if (missing) return { ok: false, reason: missing };
|
|
339
|
+
}
|
|
340
|
+
|
|
273
341
|
const inspected = inspectPlacement(env);
|
|
274
342
|
if (!inspected.ok) return { ok: false, reason: inspected.reason };
|
|
275
343
|
const placement = inspected.placement;
|
|
@@ -319,6 +387,9 @@ export function freshCall(
|
|
|
319
387
|
/** Why each refusal happened, in the caller's terms. A reason a caller cannot act on is a reason
|
|
320
388
|
* they will guess about. */
|
|
321
389
|
const REJECT_HINT: Record<FreshCallRejectReason, string> = {
|
|
390
|
+
// The Copilot capability reasons keep their repair text on the leaf that decides them, so
|
|
391
|
+
// the sentence an operator reads cannot drift away from the predicate that produced it.
|
|
392
|
+
...COPILOT_PREFLIGHT_HINT,
|
|
322
393
|
"no-tmux-context": "this agent is not running inside tmux, so there is no session to open a sibling beside",
|
|
323
394
|
"anchor-malformed": "TMUX_PANE is not a native pane id",
|
|
324
395
|
"anchor-unresolved": "tmux resolved no pane for this agent's anchor",
|