@junghanacs/entwurf 0.41.2 → 0.50.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 +4 -2
- package/BASELINE.md +110 -1
- package/CHANGELOG.md +328 -0
- package/CONTRIBUTING.md +1 -1
- package/ENTWURF-CLI.json +1229 -0
- package/ENTWURF.json +284 -0
- package/FAQ.md +4 -4
- package/README.md +125 -184
- package/VERIFY.md +2 -2
- package/docs/external-mcp-host.md +3 -3
- package/docs/setup-clean-host.md +37 -12
- package/mcp/entwurf-bridge/dist/ENTWURF.json +284 -0
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +46 -196
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/agent-guidance.js +299 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +21 -25
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +498 -29
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +613 -33
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-resume-call.js +497 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +23 -20
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +68 -21
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-location.js +347 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js +1132 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +7 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +18 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +53 -60
- package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +40 -7
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +1 -27
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +1 -1
- package/mcp/entwurf-bridge/src/index.ts +149 -327
- package/mcp/entwurf-bridge/tsconfig.build.json +7 -1
- package/native/win32-x64/manifest.json +1 -1
- package/package.json +8 -4
- package/pi/pi-durable/bootstrap.mjs +179 -6
- package/pi/pi-durable/carrier/runtime.js +108 -9
- package/pi/pi-durable/carrier/session-name.js +11 -0
- package/pi/pi-durable/carrier/sessions.js +96 -20
- package/pi/pi-durable/overlay/upstream-pin.json +11 -5
- package/pi-extensions/lib/agent-guidance.ts +341 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +25 -31
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +725 -54
- package/pi-extensions/lib/fresh-call-dispatch.ts +64 -12
- package/pi-extensions/lib/herdr-fresh-call.ts +919 -40
- package/pi-extensions/lib/herdr-resume-call.ts +810 -0
- package/pi-extensions/lib/meta-session.ts +13 -8
- package/pi-extensions/lib/mux-fresh-call.ts +23 -26
- package/pi-extensions/lib/mux-resume-call.ts +95 -23
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +9 -2
- package/pi-extensions/lib/pi-durable-location.ts +446 -0
- package/pi-extensions/lib/pi-durable-owner-fence.ts +1461 -0
- package/pi-extensions/lib/pi-durable-runtime.ts +4 -0
- package/pi-extensions/lib/remote-pair-send.ts +7 -4
- package/pi-extensions/lib/resume-launch-identity.ts +31 -12
- package/pi-extensions/meta-bridge-pi-durable.ts +52 -79
- package/run.sh +417 -654
- package/scripts/build-bridge.sh +10 -0
- package/scripts/check-capability-bundle-reach.ts +86 -0
- package/scripts/check-claude-launch.ts +341 -0
- package/scripts/check-entwurf-self-address.ts +3 -3
- package/scripts/check-entwurf-v2-surface.ts +94 -98
- package/scripts/check-fresh-call-dispatch.ts +70 -9
- package/scripts/check-fresh-cut-gate.sh +23 -9
- package/scripts/check-gate-qualification.ts +24 -17
- package/scripts/check-herdr-activation.ts +301 -23
- package/scripts/check-herdr-fresh-call.ts +161 -25
- package/scripts/check-herdr-plugin-build.ts +173 -1
- package/scripts/check-herdr-plugin.ts +28 -2
- package/scripts/check-herdr-runtime-bootstrap.ts +136 -0
- package/scripts/check-install-surface.ts +1236 -6
- package/scripts/check-meta-doctor-oracle.sh +1 -1
- package/scripts/check-meta-facts.ts +20 -1
- package/scripts/check-mux-resume-call.ts +48 -3
- package/scripts/check-peer-facts.ts +39 -2
- package/scripts/cli-help.mjs +222 -0
- package/scripts/dev-bin.sh +39 -2
- package/scripts/herdr-activation.mjs +28 -6
- package/scripts/herdr-operator-link.mjs +389 -0
- package/scripts/herdr-plugin-activate.mjs +5 -0
- package/scripts/herdr-plugin-deactivate.mjs +18 -5
- package/scripts/herdr-rails.mjs +43 -7
- package/scripts/herdr-runtime.mjs +27 -0
- package/scripts/meta-bridge-fresh-cut.ts +1 -27
- package/scripts/meta-bridge-prune.ts +1 -1
- package/scripts/meta-facts.ts +1 -1
- package/scripts/mutants/agent-guidance.json +160 -0
- package/scripts/mutants/bridge-boot-resume.json +6 -8
- package/scripts/mutants/capability-bundle-reach.json +124 -0
- package/scripts/mutants/claude-launch.json +142 -0
- package/scripts/mutants/cli-help-source.json +420 -0
- package/scripts/mutants/control-send-receipt.json +21 -4
- package/scripts/mutants/dev-gate-dispatch.json +109 -0
- package/scripts/mutants/fresh-call-dispatch.json +38 -2
- package/scripts/mutants/fresh-cut.json +15 -0
- package/scripts/mutants/herdr-activation.json +128 -0
- package/scripts/mutants/herdr-fresh-call.json +278 -5
- package/scripts/mutants/herdr-operator-link.json +318 -0
- package/scripts/mutants/herdr-plugin-build.json +55 -0
- package/scripts/mutants/herdr-plugin.json +17 -0
- package/scripts/mutants/herdr-resume-call.json +288 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +48 -0
- package/scripts/mutants/meta-facts.json +15 -0
- package/scripts/mutants/mux-fresh-call.json +14 -3
- package/scripts/mutants/mux-resume-call.json +89 -3
- package/scripts/mutants/peer-facts.json +15 -0
- package/scripts/mutants/pi-durable.json +996 -6
- package/scripts/mutants/remote-pair-send.json +21 -2
- package/scripts/mutants/self-address.json +10 -10
- package/scripts/mutants/source-install.json +35 -0
- package/scripts/mutants/v2-surface.json +50 -64
- package/scripts/mutants/v2-visible-resume.json +266 -6
- package/scripts/peer-facts.ts +1 -1
- package/scripts/smoke-agy-install-state.sh +66 -0
- package/scripts/smoke-meta-prune.sh +23 -0
- package/scripts/smoke-omp-fresh-live.ts +12 -3
- package/scripts/smoke-omp-receive-live.ts +7 -1
- package/scripts/smoke-pi-durable-fresh-live.ts +12 -2
- package/team.json.example +39 -0
|
@@ -4,6 +4,55 @@ import { mkdir, readdir, realpath, stat } from "node:fs/promises";
|
|
|
4
4
|
import { basename, join, resolve } from "node:path";
|
|
5
5
|
import lockfile from "proper-lockfile";
|
|
6
6
|
import { getAgentDir } from "entwurf-pi-dist:config.js";
|
|
7
|
+
import { isSessionName } from "./session-name.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* An owner's end as the opener observed it (#157 B3): the storage outcome of the first actual close, and the
|
|
11
|
+
* lease outcome of its release.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/** One admitted owner of a session directory, from before its native lease through its end. */
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/** What an owned open hands the fence once its directory is chosen, before its native lease. */
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The owner fence every owned open takes before its native lease (#157 B3). Supplied by the host, never imported
|
|
51
|
+
* here; it admits this opener or refuses, and a refusal opens nothing.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
7
56
|
|
|
8
57
|
/** One session directory holding `session.sqlite`, locked by this process. */
|
|
9
58
|
|
|
@@ -13,6 +62,8 @@ import { getAgentDir } from "entwurf-pi-dist:config.js";
|
|
|
13
62
|
|
|
14
63
|
|
|
15
64
|
|
|
65
|
+
|
|
66
|
+
|
|
16
67
|
|
|
17
68
|
|
|
18
69
|
/** One session of a directory as its files show it (#143): nothing in it is opened. */
|
|
@@ -27,8 +78,6 @@ import { getAgentDir } from "entwurf-pi-dist:config.js";
|
|
|
27
78
|
|
|
28
79
|
|
|
29
80
|
|
|
30
|
-
const SESSION_NAME = /^\d{13}-[0-9a-f-]{36}$/u;
|
|
31
|
-
|
|
32
81
|
/** A path's stat, or undefined when nothing is there; any other failure is thrown, never read as absence. */
|
|
33
82
|
async function statIfPresent(path ) {
|
|
34
83
|
try {
|
|
@@ -39,15 +88,17 @@ async function statIfPresent(path ) {
|
|
|
39
88
|
}
|
|
40
89
|
}
|
|
41
90
|
|
|
42
|
-
async function sessionRoot(cwdInput )
|
|
91
|
+
async function sessionRoot(cwdInput ) {
|
|
43
92
|
const cwd = await realpath(resolve(cwdInput));
|
|
93
|
+
// Read once: the root is joined from this value, and the fence is handed this same value as it came (#157 B3).
|
|
94
|
+
const agentDir = getAgentDir();
|
|
44
95
|
const root = join(
|
|
45
|
-
|
|
96
|
+
agentDir,
|
|
46
97
|
"experimental",
|
|
47
98
|
"durable-sessions",
|
|
48
99
|
createHash("sha256").update(cwd).digest("hex").slice(0, 24),
|
|
49
100
|
);
|
|
50
|
-
return { cwd, root };
|
|
101
|
+
return { cwd, root, profile: agentDir };
|
|
51
102
|
}
|
|
52
103
|
|
|
53
104
|
/** The sessions of `cwd`, most recently written first. Reads names, file times and locks; creates nothing. */
|
|
@@ -62,7 +113,7 @@ export async function listSessions(cwdInput ) {
|
|
|
62
113
|
}
|
|
63
114
|
const sessions = [];
|
|
64
115
|
for (const entry of entries) {
|
|
65
|
-
if (!entry.isDirectory() || !
|
|
116
|
+
if (!entry.isDirectory() || !isSessionName(entry.name)) continue;
|
|
66
117
|
const directory = join(root, entry.name);
|
|
67
118
|
let lastWriteAt ;
|
|
68
119
|
for (const file of ["session.sqlite", "session.sqlite-wal"]) {
|
|
@@ -80,19 +131,23 @@ export async function listSessions(cwdInput ) {
|
|
|
80
131
|
return sessions.sort((a, b) => (b.lastWriteAt ?? b.createdAt) - (a.lastWriteAt ?? a.createdAt));
|
|
81
132
|
}
|
|
82
133
|
|
|
83
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* A new session for `cwd`, its newest one with `continueSession`, or the existing one named by `sessionId`, admitted
|
|
136
|
+
* by `fence` before its native lease is taken.
|
|
137
|
+
*/
|
|
84
138
|
export async function selectSession(
|
|
139
|
+
fence ,
|
|
85
140
|
cwdInput ,
|
|
86
141
|
continueSession ,
|
|
87
142
|
sessionId ,
|
|
88
143
|
) {
|
|
89
|
-
const { cwd, root } = await sessionRoot(cwdInput);
|
|
144
|
+
const { cwd, root, profile } = await sessionRoot(cwdInput);
|
|
90
145
|
await mkdir(root, { recursive: true });
|
|
91
146
|
|
|
92
147
|
let directory ;
|
|
93
148
|
let created = false;
|
|
94
149
|
if (sessionId !== undefined) {
|
|
95
|
-
const chosen =
|
|
150
|
+
const chosen = isSessionName(sessionId) ? join(root, sessionId) : undefined;
|
|
96
151
|
if (chosen === undefined || !(await statIfPresent(chosen))?.isDirectory()) {
|
|
97
152
|
throw new Error(`No durable session ${sessionId} exists for ${cwd}`);
|
|
98
153
|
}
|
|
@@ -100,7 +155,7 @@ export async function selectSession(
|
|
|
100
155
|
} else if (continueSession) {
|
|
101
156
|
const entries = await readdir(root, { withFileTypes: true });
|
|
102
157
|
const newest = entries
|
|
103
|
-
.filter((entry) => entry.isDirectory() &&
|
|
158
|
+
.filter((entry) => entry.isDirectory() && isSessionName(entry.name))
|
|
104
159
|
.map((entry) => entry.name)
|
|
105
160
|
.sort()
|
|
106
161
|
.at(-1);
|
|
@@ -112,15 +167,36 @@ export async function selectSession(
|
|
|
112
167
|
created = true;
|
|
113
168
|
}
|
|
114
169
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
170
|
+
// The owner fence admits this opener BEFORE the native lease, or refuses with nothing taken (#157 B3).
|
|
171
|
+
const ticket = await fence.admit({ directory, selectedN: basename(directory), created, profile });
|
|
172
|
+
// A lock left by a crashed process goes stale after 10 s; wait that long before giving up.
|
|
173
|
+
const lease = lockfile.lock(directory, {
|
|
174
|
+
realpath: false,
|
|
175
|
+
retries: { retries: 12, minTimeout: 1000, maxTimeout: 1000 },
|
|
176
|
+
});
|
|
177
|
+
const release = await lease.catch(async (error) => {
|
|
178
|
+
const refused = new Error(`Session is already open in another process: ${directory}`, { cause: error });
|
|
179
|
+
// No acquisition was observed: this owner ends with nothing opened and no lease held.
|
|
180
|
+
await ticket.endUnleased().catch((fenceError) => {
|
|
181
|
+
throw new AggregateError([refused, fenceError], `${refused.message}; its owner end was not recorded`);
|
|
121
182
|
});
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
183
|
+
throw refused;
|
|
184
|
+
});
|
|
185
|
+
// Apart from the lease's own failure: the lease IS held here, so a failed checkpoint is never recorded as an
|
|
186
|
+
// acquire failure. The open fails and releases the lease; the claim stays fenced under this live owner.
|
|
187
|
+
await ticket.leased().catch(async (error) => {
|
|
188
|
+
await release().catch((releaseError) => {
|
|
189
|
+
throw new AggregateError([error, releaseError], `lease checkpoint and release both failed: ${directory}`);
|
|
190
|
+
});
|
|
191
|
+
throw error;
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
id: basename(directory),
|
|
195
|
+
directory,
|
|
196
|
+
database: join(directory, "session.sqlite"),
|
|
197
|
+
cwd,
|
|
198
|
+
created,
|
|
199
|
+
release,
|
|
200
|
+
ticket,
|
|
201
|
+
};
|
|
126
202
|
}
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
"carrier": {
|
|
58
|
-
"comment": "files: the six app files bootstrap.mjs needs (main.ts is replaced by the bootstrap), each emitted from its owned `source`. distTargets: every pi-coding-agent internal the stripped files import, as paths under the published dist/; the emitter refuses a set that differs and the resolver answers no other.",
|
|
58
|
+
"comment": "files: the six app files bootstrap.mjs needs (main.ts is replaced by the bootstrap) and the import-free session-name grammar Entwurf owns beside them (#157 G3 B-L1, no upstream origin), each emitted from its owned `source`. distTargets: every pi-coding-agent internal the stripped files import, as paths under the published dist/; the emitter refuses a set that differs and the resolver answers no other.",
|
|
59
59
|
"emitter": {
|
|
60
60
|
"tool": "node:module.stripTypeScriptTypes",
|
|
61
61
|
"mode": "strip",
|
|
@@ -70,8 +70,8 @@
|
|
|
70
70
|
{
|
|
71
71
|
"name": "runtime.js",
|
|
72
72
|
"source": "pi/pi-durable/src/experimental/durable/runtime.ts",
|
|
73
|
-
"blob": "
|
|
74
|
-
"sha256": "
|
|
73
|
+
"blob": "bb5715ab143945e9f2c067ff71d890fe29875d09",
|
|
74
|
+
"sha256": "437834997b9870ab0383aea7d86ff7880ef7e2ebbbfe4c6360f67d8d36f777ec"
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
"name": "tui.js",
|
|
@@ -94,14 +94,20 @@
|
|
|
94
94
|
{
|
|
95
95
|
"name": "sessions.js",
|
|
96
96
|
"source": "pi/pi-durable/src/experimental/durable/sessions.ts",
|
|
97
|
-
"blob": "
|
|
98
|
-
"sha256": "
|
|
97
|
+
"blob": "f107238fefb63c95b29add5aa76fcbd9153c94db",
|
|
98
|
+
"sha256": "0ac7c919f2bf8a3b0e5dbd307045108a803b161f87298cd9fe440f239784c7ac"
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
"name": "subagent.js",
|
|
102
102
|
"source": "pi/pi-durable/src/experimental/durable/subagent.ts",
|
|
103
103
|
"blob": "3fa9aebbe93e82cfff436e7b9d4c9877e0ed0e27",
|
|
104
104
|
"sha256": "1cb2390b673a0d914117bc07708c39b004fb10e48b31436bf9d9d2b953a64220"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "session-name.js",
|
|
108
|
+
"source": "pi/pi-durable/src/experimental/durable/session-name.ts",
|
|
109
|
+
"blob": "c57c760ca3c14363f195ac9d9c2add984e4118d5",
|
|
110
|
+
"sha256": "9eadecfe9a48bba0d221370f4a236286e0d6f10d663b15ff4cb5975adee67018"
|
|
105
111
|
}
|
|
106
112
|
],
|
|
107
113
|
"distTargets": [
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product-owned natural-language logic, authored only in root ENTWURF.json (#154).
|
|
3
|
+
* One module-relative file, read once. The compiled layout carries an exact copy at
|
|
4
|
+
* dist/ENTWURF.json; no cwd/HOME search, override, old-text fallback or hot reload.
|
|
5
|
+
* Existing operator engraving and user-configuration contracts are unrelated.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
|
|
9
|
+
const TOOL_SPEC = {
|
|
10
|
+
entwurf_v2: { durable: true, parameters: ["target", "intent", "message", "mode", "wants_reply"] },
|
|
11
|
+
entwurf_self: { durable: true, parameters: [] },
|
|
12
|
+
entwurf_peers: { durable: true, parameters: [] },
|
|
13
|
+
entwurf_inbox_read: { durable: true, parameters: ["gardenId"] },
|
|
14
|
+
entwurf_register_native: { durable: false, parameters: ["backend", "nativeSessionId", "cwd"] },
|
|
15
|
+
entwurf_fresh_call: {
|
|
16
|
+
durable: true,
|
|
17
|
+
parameters: ["backend", "model", "task", "cwd", "placement.tmuxSession", "placement"],
|
|
18
|
+
},
|
|
19
|
+
entwurf_resume_call: { durable: true, parameters: ["target"] },
|
|
20
|
+
entwurf_retire_call: { durable: true, parameters: ["target"] },
|
|
21
|
+
entwurf_callback: { durable: true, parameters: [] },
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
/** Frozen entwurf_v2 reject wire ids whose static next-step hint text lives in the root file. */
|
|
25
|
+
const REJECT_HINT_REASONS = [
|
|
26
|
+
"dormant-fire-forget-unsupported",
|
|
27
|
+
"indeterminate-no-spawn",
|
|
28
|
+
"native-push-target-dead",
|
|
29
|
+
"native-push-probe-indeterminate",
|
|
30
|
+
"native-push-paused",
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
export type RejectHintReason = (typeof REJECT_HINT_REASONS)[number];
|
|
34
|
+
|
|
35
|
+
/** Frozen entwurf_resume_call record/liveness reject wire ids whose static hint text lives in the root file. */
|
|
36
|
+
const RESUME_REJECT_HINT_REASONS = [
|
|
37
|
+
"target-invalid",
|
|
38
|
+
"target-backend-unsupported",
|
|
39
|
+
"target-locked",
|
|
40
|
+
"target-live",
|
|
41
|
+
"target-indeterminate",
|
|
42
|
+
"target-receiver-absent",
|
|
43
|
+
"target-receiver-corrupt",
|
|
44
|
+
"target-receiver-mismatch",
|
|
45
|
+
"target-receiver-unbound",
|
|
46
|
+
"target-address-conflict",
|
|
47
|
+
"launch-refused",
|
|
48
|
+
] as const;
|
|
49
|
+
|
|
50
|
+
export type ResumeRejectHintReason = (typeof RESUME_REJECT_HINT_REASONS)[number];
|
|
51
|
+
|
|
52
|
+
/** Frozen entwurf_fresh_call tmux launch-stage reject wire ids whose static hint text lives in the root file. */
|
|
53
|
+
const FRESH_LAUNCH_REJECT_HINT_REASONS = [
|
|
54
|
+
"no-tmux-context",
|
|
55
|
+
"anchor-malformed",
|
|
56
|
+
"anchor-unresolved",
|
|
57
|
+
"anchor-mismatch",
|
|
58
|
+
"caller-identity-unavailable",
|
|
59
|
+
"cwd-not-absolute",
|
|
60
|
+
"cwd-format-token",
|
|
61
|
+
"cwd-missing",
|
|
62
|
+
"cwd-not-directory",
|
|
63
|
+
"tmux-session-name-invalid",
|
|
64
|
+
"tmux-session-missing",
|
|
65
|
+
"model-empty",
|
|
66
|
+
"task-empty",
|
|
67
|
+
"runtime-unresolved",
|
|
68
|
+
"runtime-not-absolute",
|
|
69
|
+
"runtime-path-whitespace",
|
|
70
|
+
"runtime-missing",
|
|
71
|
+
"runtime-not-regular-file",
|
|
72
|
+
"runtime-not-executable",
|
|
73
|
+
] as const;
|
|
74
|
+
|
|
75
|
+
export type FreshLaunchRejectHintReason = (typeof FRESH_LAUNCH_REJECT_HINT_REASONS)[number];
|
|
76
|
+
|
|
77
|
+
/** Frozen entwurf_resume_call tmux launch-stage reject wire ids whose static hint text lives in the root file. */
|
|
78
|
+
const RESUME_LAUNCH_REJECT_HINT_REASONS = [
|
|
79
|
+
"no-tmux-context",
|
|
80
|
+
"anchor-malformed",
|
|
81
|
+
"anchor-unresolved",
|
|
82
|
+
"anchor-mismatch",
|
|
83
|
+
"cwd-not-absolute",
|
|
84
|
+
"cwd-format-token",
|
|
85
|
+
"cwd-missing",
|
|
86
|
+
"cwd-not-directory",
|
|
87
|
+
"herdr-resume-unsupported",
|
|
88
|
+
"runtime-unresolved",
|
|
89
|
+
"runtime-not-absolute",
|
|
90
|
+
"runtime-path-whitespace",
|
|
91
|
+
"runtime-missing",
|
|
92
|
+
"runtime-not-regular-file",
|
|
93
|
+
"runtime-not-executable",
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
export type ResumeLaunchRejectHintReason = (typeof RESUME_LAUNCH_REJECT_HINT_REASONS)[number];
|
|
97
|
+
|
|
98
|
+
/** Frozen keys of the static delivery-result clauses whose text lives in the root file; the
|
|
99
|
+
* receipts around them (syntax, branch conditions, slots, punctuation) stay code-owned. */
|
|
100
|
+
const DELIVERY_RESULT_CLAUSE_KEYS = [
|
|
101
|
+
"finalized-rejected",
|
|
102
|
+
"dirty-lock",
|
|
103
|
+
"remote-accepted",
|
|
104
|
+
"remote-reported-error",
|
|
105
|
+
"remote-indeterminate",
|
|
106
|
+
] as const;
|
|
107
|
+
|
|
108
|
+
export type DeliveryResultClauseKey = (typeof DELIVERY_RESULT_CLAUSE_KEYS)[number];
|
|
109
|
+
|
|
110
|
+
/** Frozen wire ids of the reject hints whose authored text sits on either side of ONE code-owned
|
|
111
|
+
* slot, and each one's exact segment keys. The slot value, the reason branch and the
|
|
112
|
+
* concatenation stay code-owned; the root file holds only the text around the slot. */
|
|
113
|
+
const SLOTTED_REJECT_HINT_SEGMENTS = {
|
|
114
|
+
"record-less-socket": ["beforeFreshCutPrescription", "afterFreshCutPrescription"],
|
|
115
|
+
} as const;
|
|
116
|
+
|
|
117
|
+
export type SlottedRejectHintReason = keyof typeof SLOTTED_REJECT_HINT_SEGMENTS;
|
|
118
|
+
type SlottedRejectHints = {
|
|
119
|
+
readonly [R in SlottedRejectHintReason]: Readonly<Record<(typeof SLOTTED_REJECT_HINT_SEGMENTS)[R][number], string>>;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
type ToolName = keyof typeof TOOL_SPEC;
|
|
123
|
+
type ToolGuidance<K extends ToolName> = {
|
|
124
|
+
readonly descriptions: (typeof TOOL_SPEC)[K]["durable"] extends true
|
|
125
|
+
? { readonly mcp: string; readonly durable: string }
|
|
126
|
+
: { readonly mcp: string };
|
|
127
|
+
readonly parameters: Readonly<Record<(typeof TOOL_SPEC)[K]["parameters"][number], string>>;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export interface EntwurfGuidance {
|
|
131
|
+
readonly _about: Readonly<Record<"purpose" | "ownership" | "scope" | "editing", string>>;
|
|
132
|
+
readonly schemaVersion: 6;
|
|
133
|
+
readonly revision: number;
|
|
134
|
+
readonly history: readonly {
|
|
135
|
+
readonly revision: number;
|
|
136
|
+
readonly source: string;
|
|
137
|
+
readonly issue: string;
|
|
138
|
+
readonly change: string;
|
|
139
|
+
}[];
|
|
140
|
+
readonly tools: { readonly [K in ToolName]: ToolGuidance<K> };
|
|
141
|
+
readonly rejectHints: Readonly<Record<RejectHintReason, string>>;
|
|
142
|
+
readonly resumeRejectHints: Readonly<Record<ResumeRejectHintReason, string>>;
|
|
143
|
+
readonly freshLaunchRejectHints: Readonly<Record<FreshLaunchRejectHintReason, string>>;
|
|
144
|
+
readonly resumeLaunchRejectHints: Readonly<Record<ResumeLaunchRejectHintReason, string>>;
|
|
145
|
+
readonly deliveryResultClauses: Readonly<Record<DeliveryResultClauseKey, string>>;
|
|
146
|
+
readonly slottedRejectHints: SlottedRejectHints;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function record(value: unknown, at: string): asserts value is Record<string, unknown> {
|
|
150
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
151
|
+
throw new Error(`ENTWURF.json ${at}: expected an object`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function keys(value: Record<string, unknown>, expected: readonly string[], at: string): void {
|
|
156
|
+
const actual = Object.keys(value).sort();
|
|
157
|
+
const wanted = [...expected].sort();
|
|
158
|
+
if (actual.length !== wanted.length || actual.some((key, i) => key !== wanted[i])) {
|
|
159
|
+
throw new Error(`ENTWURF.json ${at}: keys must be exactly ${wanted.join(", ")}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function text(value: unknown, at: string): void {
|
|
164
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
165
|
+
throw new Error(`ENTWURF.json ${at}: expected a non-empty string`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* JSON.parse owns strict grammar, but its last-key-wins value is never enough.
|
|
171
|
+
* Walk the ORIGINAL, already grammar-checked tokens and reject duplicate DECODED
|
|
172
|
+
* object keys before returning data. Object scopes are independent; strings that
|
|
173
|
+
* are values (including punctuation and escaped quotes) are never key tokens.
|
|
174
|
+
*/
|
|
175
|
+
function rejectDuplicateKeys(source: string): void {
|
|
176
|
+
let pos = 0;
|
|
177
|
+
const space = (): void => {
|
|
178
|
+
while (pos < source.length && /[\x20\t\r\n]/.test(source[pos])) pos++;
|
|
179
|
+
};
|
|
180
|
+
const string = (): string => {
|
|
181
|
+
const start = pos++;
|
|
182
|
+
while (pos < source.length) {
|
|
183
|
+
if (source[pos] === "\\") pos += 2;
|
|
184
|
+
else if (source[pos++] === '"') return JSON.parse(source.slice(start, pos)) as string;
|
|
185
|
+
}
|
|
186
|
+
throw new Error("ENTWURF.json: unterminated string");
|
|
187
|
+
};
|
|
188
|
+
const value = (): void => {
|
|
189
|
+
space();
|
|
190
|
+
if (source[pos] === "{") {
|
|
191
|
+
pos++;
|
|
192
|
+
space();
|
|
193
|
+
const seen = new Set<string>();
|
|
194
|
+
if (source[pos] !== "}") {
|
|
195
|
+
for (;;) {
|
|
196
|
+
space();
|
|
197
|
+
const key = string();
|
|
198
|
+
if (seen.has(key)) throw new Error(`ENTWURF.json: duplicate object key ${JSON.stringify(key)}`);
|
|
199
|
+
seen.add(key);
|
|
200
|
+
space();
|
|
201
|
+
pos++; // colon; JSON.parse already certified the grammar
|
|
202
|
+
value();
|
|
203
|
+
space();
|
|
204
|
+
if (source[pos] !== ",") break;
|
|
205
|
+
pos++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
pos++;
|
|
209
|
+
} else if (source[pos] === "[") {
|
|
210
|
+
pos++;
|
|
211
|
+
space();
|
|
212
|
+
if (source[pos] !== "]") {
|
|
213
|
+
for (;;) {
|
|
214
|
+
value();
|
|
215
|
+
space();
|
|
216
|
+
if (source[pos] !== ",") break;
|
|
217
|
+
pos++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
pos++;
|
|
221
|
+
} else if (source[pos] === '"') {
|
|
222
|
+
string();
|
|
223
|
+
} else {
|
|
224
|
+
while (pos < source.length && !/[\x20\t\r\n,}\]]/.test(source[pos])) pos++;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
value();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function freeze(value: unknown): void {
|
|
231
|
+
if (value === null || typeof value !== "object") return;
|
|
232
|
+
for (const child of Object.values(value)) freeze(child);
|
|
233
|
+
Object.freeze(value);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Pure data-validation seam; production always reads the one fixed module URL below. */
|
|
237
|
+
export function parseEntwurfGuidance(source: string): EntwurfGuidance {
|
|
238
|
+
const data: unknown = JSON.parse(source);
|
|
239
|
+
rejectDuplicateKeys(source);
|
|
240
|
+
record(data, "root");
|
|
241
|
+
if (data.schemaVersion !== 6) throw new Error("ENTWURF.json: unsupported schemaVersion");
|
|
242
|
+
keys(
|
|
243
|
+
data,
|
|
244
|
+
[
|
|
245
|
+
"_about",
|
|
246
|
+
"schemaVersion",
|
|
247
|
+
"revision",
|
|
248
|
+
"history",
|
|
249
|
+
"tools",
|
|
250
|
+
"rejectHints",
|
|
251
|
+
"resumeRejectHints",
|
|
252
|
+
"freshLaunchRejectHints",
|
|
253
|
+
"resumeLaunchRejectHints",
|
|
254
|
+
"deliveryResultClauses",
|
|
255
|
+
"slottedRejectHints",
|
|
256
|
+
],
|
|
257
|
+
"root",
|
|
258
|
+
);
|
|
259
|
+
if (!Number.isSafeInteger(data.revision) || (data.revision as number) < 1) {
|
|
260
|
+
throw new Error("ENTWURF.json: revision must be a positive safe integer");
|
|
261
|
+
}
|
|
262
|
+
record(data._about, "_about");
|
|
263
|
+
keys(data._about, ["purpose", "ownership", "scope", "editing"], "_about");
|
|
264
|
+
for (const [key, value] of Object.entries(data._about)) text(value, `_about.${key}`);
|
|
265
|
+
if (!Array.isArray(data.history) || data.history.length === 0) {
|
|
266
|
+
throw new Error("ENTWURF.json: history must be a non-empty array");
|
|
267
|
+
}
|
|
268
|
+
for (const entry of data.history) {
|
|
269
|
+
record(entry, "history entry");
|
|
270
|
+
keys(entry, ["revision", "source", "issue", "change"], "history entry");
|
|
271
|
+
if (
|
|
272
|
+
!Number.isSafeInteger(entry.revision) ||
|
|
273
|
+
(entry.revision as number) < 1 ||
|
|
274
|
+
(entry.revision as number) > (data.revision as number)
|
|
275
|
+
) {
|
|
276
|
+
throw new Error("ENTWURF.json: history revision must name an existing payload revision");
|
|
277
|
+
}
|
|
278
|
+
for (const key of ["source", "issue", "change"] as const) text(entry[key], `history.${key}`);
|
|
279
|
+
}
|
|
280
|
+
record(data.tools, "tools");
|
|
281
|
+
keys(data.tools, Object.keys(TOOL_SPEC), "tools");
|
|
282
|
+
for (const name of Object.keys(TOOL_SPEC) as ToolName[]) {
|
|
283
|
+
const tool = data.tools[name];
|
|
284
|
+
record(tool, name);
|
|
285
|
+
keys(tool, ["descriptions", "parameters"], name);
|
|
286
|
+
record(tool.descriptions, `${name}.descriptions`);
|
|
287
|
+
keys(tool.descriptions, TOOL_SPEC[name].durable ? ["mcp", "durable"] : ["mcp"], `${name}.descriptions`);
|
|
288
|
+
for (const [rail, value] of Object.entries(tool.descriptions)) text(value, `${name}.descriptions.${rail}`);
|
|
289
|
+
record(tool.parameters, `${name}.parameters`);
|
|
290
|
+
keys(tool.parameters, TOOL_SPEC[name].parameters, `${name}.parameters`);
|
|
291
|
+
for (const [field, value] of Object.entries(tool.parameters)) text(value, `${name}.parameters.${field}`);
|
|
292
|
+
}
|
|
293
|
+
record(data.rejectHints, "rejectHints");
|
|
294
|
+
keys(data.rejectHints, REJECT_HINT_REASONS, "rejectHints");
|
|
295
|
+
for (const [reason, value] of Object.entries(data.rejectHints)) text(value, `rejectHints.${reason}`);
|
|
296
|
+
record(data.resumeRejectHints, "resumeRejectHints");
|
|
297
|
+
keys(data.resumeRejectHints, RESUME_REJECT_HINT_REASONS, "resumeRejectHints");
|
|
298
|
+
for (const [reason, value] of Object.entries(data.resumeRejectHints)) text(value, `resumeRejectHints.${reason}`);
|
|
299
|
+
record(data.freshLaunchRejectHints, "freshLaunchRejectHints");
|
|
300
|
+
keys(data.freshLaunchRejectHints, FRESH_LAUNCH_REJECT_HINT_REASONS, "freshLaunchRejectHints");
|
|
301
|
+
for (const [reason, value] of Object.entries(data.freshLaunchRejectHints))
|
|
302
|
+
text(value, `freshLaunchRejectHints.${reason}`);
|
|
303
|
+
record(data.resumeLaunchRejectHints, "resumeLaunchRejectHints");
|
|
304
|
+
keys(data.resumeLaunchRejectHints, RESUME_LAUNCH_REJECT_HINT_REASONS, "resumeLaunchRejectHints");
|
|
305
|
+
for (const [reason, value] of Object.entries(data.resumeLaunchRejectHints))
|
|
306
|
+
text(value, `resumeLaunchRejectHints.${reason}`);
|
|
307
|
+
record(data.deliveryResultClauses, "deliveryResultClauses");
|
|
308
|
+
keys(data.deliveryResultClauses, DELIVERY_RESULT_CLAUSE_KEYS, "deliveryResultClauses");
|
|
309
|
+
for (const [key, value] of Object.entries(data.deliveryResultClauses)) text(value, `deliveryResultClauses.${key}`);
|
|
310
|
+
record(data.slottedRejectHints, "slottedRejectHints");
|
|
311
|
+
keys(data.slottedRejectHints, Object.keys(SLOTTED_REJECT_HINT_SEGMENTS), "slottedRejectHints");
|
|
312
|
+
for (const reason of Object.keys(SLOTTED_REJECT_HINT_SEGMENTS) as SlottedRejectHintReason[]) {
|
|
313
|
+
const segments = data.slottedRejectHints[reason];
|
|
314
|
+
record(segments, `slottedRejectHints.${reason}`);
|
|
315
|
+
keys(segments, SLOTTED_REJECT_HINT_SEGMENTS[reason], `slottedRejectHints.${reason}`);
|
|
316
|
+
for (const [key, value] of Object.entries(segments)) text(value, `slottedRejectHints.${reason}.${key}`);
|
|
317
|
+
}
|
|
318
|
+
freeze(data);
|
|
319
|
+
return data as unknown as EntwurfGuidance;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const guidance = parseEntwurfGuidance(readFileSync(new URL("../../ENTWURF.json", import.meta.url), "utf8"));
|
|
323
|
+
|
|
324
|
+
/** Only these exact payload strings enter tool schemas; _about/history never do. */
|
|
325
|
+
export const ENTWURF_TOOL_GUIDANCE = guidance.tools;
|
|
326
|
+
/** Static next-step text appended to these entwurf_v2 rejections; the decider and wire ids stay code-owned. */
|
|
327
|
+
export const ENTWURF_REJECT_HINTS = guidance.rejectHints;
|
|
328
|
+
/** Static next-step text in these entwurf_resume_call record/liveness rejections; the verb and the shared ACP model hint stay code-owned. */
|
|
329
|
+
export const ENTWURF_RESUME_REJECT_HINTS = guidance.resumeRejectHints;
|
|
330
|
+
/** Static tmux launch-stage hints of entwurf_fresh_call; slot-bearing, ACP and preflight-leaf hints stay code-owned. */
|
|
331
|
+
export const ENTWURF_FRESH_LAUNCH_REJECT_HINTS = guidance.freshLaunchRejectHints;
|
|
332
|
+
/** Static tmux launch-stage hints of entwurf_resume_call, kept apart from the fresh verb even where the text matches. */
|
|
333
|
+
export const ENTWURF_RESUME_LAUNCH_REJECT_HINTS = guidance.resumeLaunchRejectHints;
|
|
334
|
+
/** Static delivery-result clauses of the entwurf_v2 dirty-lock and remote-pair receipts; the receipt syntax around them stays code-owned. */
|
|
335
|
+
export const ENTWURF_DELIVERY_RESULT_CLAUSES = guidance.deliveryResultClauses;
|
|
336
|
+
/** Text on either side of one code-owned slot per reject reason; the slot value and the concatenation stay with the consumer. */
|
|
337
|
+
export const ENTWURF_SLOTTED_REJECT_HINTS = guidance.slottedRejectHints;
|
|
338
|
+
export const ENTWURF_GUIDANCE_VERSION = Object.freeze({
|
|
339
|
+
schemaVersion: guidance.schemaVersion,
|
|
340
|
+
revision: guidance.revision,
|
|
341
|
+
});
|