@junghanacs/entwurf 0.45.0 → 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.
Files changed (105) hide show
  1. package/AGENTS.md +3 -2
  2. package/BASELINE.md +110 -1
  3. package/CHANGELOG.md +169 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/ENTWURF-CLI.json +1229 -0
  6. package/ENTWURF.json +63 -15
  7. package/FAQ.md +4 -4
  8. package/README.md +124 -188
  9. package/VERIFY.md +2 -2
  10. package/docs/external-mcp-host.md +3 -3
  11. package/docs/setup-clean-host.md +37 -12
  12. package/mcp/entwurf-bridge/dist/ENTWURF.json +63 -15
  13. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +19 -8
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +483 -18
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -10
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +613 -33
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-resume-call.js +497 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +3 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +50 -5
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +7 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-location.js +347 -0
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js +1132 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +1 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +18 -11
  25. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +30 -15
  26. package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +40 -7
  27. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +1 -27
  28. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +1 -1
  29. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
  30. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +1 -1
  31. package/mcp/entwurf-bridge/src/index.ts +28 -8
  32. package/mcp/entwurf-bridge/tsconfig.build.json +7 -1
  33. package/native/win32-x64/manifest.json +1 -1
  34. package/package.json +6 -3
  35. package/pi/pi-durable/bootstrap.mjs +179 -6
  36. package/pi/pi-durable/carrier/runtime.js +108 -9
  37. package/pi/pi-durable/carrier/session-name.js +11 -0
  38. package/pi/pi-durable/carrier/sessions.js +96 -20
  39. package/pi/pi-durable/overlay/upstream-pin.json +11 -5
  40. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +710 -38
  41. package/pi-extensions/lib/fresh-call-dispatch.ts +64 -12
  42. package/pi-extensions/lib/herdr-fresh-call.ts +919 -40
  43. package/pi-extensions/lib/herdr-resume-call.ts +810 -0
  44. package/pi-extensions/lib/meta-session.ts +13 -8
  45. package/pi-extensions/lib/mux-resume-call.ts +77 -4
  46. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +9 -2
  47. package/pi-extensions/lib/pi-durable-location.ts +446 -0
  48. package/pi-extensions/lib/pi-durable-owner-fence.ts +1461 -0
  49. package/pi-extensions/lib/pi-durable-runtime.ts +4 -0
  50. package/pi-extensions/lib/resume-launch-identity.ts +31 -12
  51. package/pi-extensions/meta-bridge-pi-durable.ts +29 -15
  52. package/run.sh +411 -657
  53. package/scripts/check-claude-launch.ts +341 -0
  54. package/scripts/check-entwurf-v2-surface.ts +2 -2
  55. package/scripts/check-fresh-call-dispatch.ts +70 -9
  56. package/scripts/check-fresh-cut-gate.sh +23 -9
  57. package/scripts/check-gate-qualification.ts +18 -13
  58. package/scripts/check-herdr-activation.ts +301 -23
  59. package/scripts/check-herdr-fresh-call.ts +161 -25
  60. package/scripts/check-herdr-plugin-build.ts +173 -1
  61. package/scripts/check-herdr-plugin.ts +28 -2
  62. package/scripts/check-herdr-runtime-bootstrap.ts +136 -0
  63. package/scripts/check-install-surface.ts +1236 -6
  64. package/scripts/check-meta-doctor-oracle.sh +1 -1
  65. package/scripts/check-meta-facts.ts +20 -1
  66. package/scripts/check-mux-resume-call.ts +28 -0
  67. package/scripts/check-peer-facts.ts +39 -2
  68. package/scripts/cli-help.mjs +222 -0
  69. package/scripts/dev-bin.sh +39 -2
  70. package/scripts/herdr-activation.mjs +28 -6
  71. package/scripts/herdr-operator-link.mjs +389 -0
  72. package/scripts/herdr-plugin-activate.mjs +5 -0
  73. package/scripts/herdr-plugin-deactivate.mjs +18 -5
  74. package/scripts/herdr-rails.mjs +43 -7
  75. package/scripts/herdr-runtime.mjs +27 -0
  76. package/scripts/meta-bridge-fresh-cut.ts +1 -27
  77. package/scripts/meta-bridge-prune.ts +1 -1
  78. package/scripts/meta-facts.ts +1 -1
  79. package/scripts/mutants/agent-guidance.json +4 -4
  80. package/scripts/mutants/claude-launch.json +142 -0
  81. package/scripts/mutants/cli-help-source.json +420 -0
  82. package/scripts/mutants/dev-gate-dispatch.json +109 -0
  83. package/scripts/mutants/fresh-call-dispatch.json +38 -2
  84. package/scripts/mutants/fresh-cut.json +15 -0
  85. package/scripts/mutants/herdr-activation.json +128 -0
  86. package/scripts/mutants/herdr-fresh-call.json +278 -5
  87. package/scripts/mutants/herdr-operator-link.json +318 -0
  88. package/scripts/mutants/herdr-plugin-build.json +55 -0
  89. package/scripts/mutants/herdr-plugin.json +17 -0
  90. package/scripts/mutants/herdr-resume-call.json +288 -0
  91. package/scripts/mutants/herdr-runtime-bootstrap.json +48 -0
  92. package/scripts/mutants/meta-facts.json +15 -0
  93. package/scripts/mutants/mux-resume-call.json +63 -2
  94. package/scripts/mutants/peer-facts.json +15 -0
  95. package/scripts/mutants/pi-durable.json +979 -1
  96. package/scripts/mutants/source-install.json +35 -0
  97. package/scripts/mutants/v2-surface.json +2 -2
  98. package/scripts/mutants/v2-visible-resume.json +255 -6
  99. package/scripts/peer-facts.ts +1 -1
  100. package/scripts/smoke-agy-install-state.sh +66 -0
  101. package/scripts/smoke-meta-prune.sh +23 -0
  102. package/scripts/smoke-omp-fresh-live.ts +12 -3
  103. package/scripts/smoke-omp-receive-live.ts +7 -1
  104. package/scripts/smoke-pi-durable-fresh-live.ts +12 -2
  105. 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
- getAgentDir(),
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() || !SESSION_NAME.test(entry.name)) continue;
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
- /** A new session for `cwd`, its newest one with `continueSession`, or the existing one named by `sessionId`. */
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 = SESSION_NAME.test(sessionId) ? join(root, sessionId) : undefined;
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() && SESSION_NAME.test(entry.name))
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
- let release ;
116
- try {
117
- // A lock left by a crashed process goes stale after 10 s; wait that long before giving up.
118
- release = await lockfile.lock(directory, {
119
- realpath: false,
120
- retries: { retries: 12, minTimeout: 1000, maxTimeout: 1000 },
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
- } catch (error) {
123
- throw new Error(`Session is already open in another process: ${directory}`, { cause: error });
124
- }
125
- return { id: basename(directory), directory, database: join(directory, "session.sqlite"), cwd, created, release };
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": "75d7bf2b4fdc36aa775f73f9d7f3de8659f19187",
74
- "sha256": "725757156580ad2a2d170c790440763371bb8035052a0c6caae6af5159d4718f"
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": "eed4b3ca51c6f5a3bab8106adaf0062f3f7d5b56",
98
- "sha256": "668373feadabcf13bd4dd7431e5429b7d7376c47c367e4b15353c57cb8711892"
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": [