@junghanacs/entwurf 0.36.0 → 0.37.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 (33) hide show
  1. package/CHANGELOG.md +290 -0
  2. package/DELIVERY.md +2 -0
  3. package/README.md +5 -0
  4. package/VERIFY.md +11 -4
  5. package/docs/acp-backend-rail.md +69 -5
  6. package/docs/setup-clean-host.md +1 -0
  7. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +16 -1
  8. package/package.json +5 -4
  9. package/pi/pi-durable/bootstrap.mjs +113 -11
  10. package/pi/pi-durable/carrier/harness-setup.js +4 -1
  11. package/pi/pi-durable/carrier/prompt.js +31 -2
  12. package/pi/pi-durable/carrier/runtime.js +11 -2
  13. package/pi/pi-durable/carrier/sessions.js +75 -5
  14. package/pi/pi-durable/carrier/tui.js +23 -0
  15. package/pi/pi-durable/overlay/upstream-pin.json +10 -10
  16. package/pi-extensions/meta-bridge-pi-durable.ts +20 -0
  17. package/run.sh +5 -2
  18. package/scripts/check-acp-prompt-lifecycle.ts +4 -2
  19. package/scripts/check-acp-session-reuse.ts +4 -2
  20. package/scripts/check-acp-stop-reason.ts +4 -2
  21. package/scripts/check-acp-stream-hooks.ts +4 -2
  22. package/scripts/check-acp-usage-accounting.ts +4 -2
  23. package/scripts/check-gate-qualification.ts +3 -3
  24. package/scripts/check-herdr-supply.ts +32 -7
  25. package/scripts/fixtures/herdr-supply.json +12 -10
  26. package/scripts/lib/acp-gate-home.ts +75 -0
  27. package/scripts/lib/codex-socket-listener.ts +429 -0
  28. package/scripts/mutants/acp-stop-reason.json +11 -0
  29. package/scripts/mutants/codex-native.json +52 -0
  30. package/scripts/mutants/herdr-supply.json +8 -8
  31. package/scripts/mutants/pi-durable.json +80 -8
  32. package/scripts/smoke-codex-fresh-live.ts +31 -7
  33. package/scripts/smoke-pi-durable-fresh-live.ts +22 -3
@@ -458,7 +458,7 @@
458
458
  "claim": "PI-DURABLE-CARRIER-PIN-MATCHES",
459
459
  "title": "the pin records a digest the committed carrier file does not carry, so a carrier edited after emission would still be the declared one",
460
460
  "subject": "pi/pi-durable/overlay/upstream-pin.json",
461
- "find": ["\"sha256\": \"60c2965eadbdbbe78fce1b7eeb89d97eb3104a30b7d58ebca87009d2e9118403\""],
461
+ "find": ["\"sha256\": \"725757156580ad2a2d170c790440763371bb8035052a0c6caae6af5159d4718f\""],
462
462
  "replace": ["\"sha256\": \"0000000000000000000000000000000000000000000000000000000000000000\""],
463
463
  "gate": ["bash", "run.sh", "check-pi-durable-beside"],
464
464
  "timeoutSeconds": 300,
@@ -701,8 +701,8 @@
701
701
  "claim": "PI-DURABLE-MODULE-ABSENT-UNCHANGED",
702
702
  "title": "the module phase runs without a --native-module, so a launch that names no module still tries to import one",
703
703
  "subject": "pi/pi-durable/bootstrap.mjs",
704
- "find": ["\tconst { nativeModules = [], ...args } = parseArgs(argv);"],
705
- "replace": ["\tconst { nativeModules = [undefined], ...args } = parseArgs(argv);"],
704
+ "find": ["\tconst { nativeModules = [], resume = false, ...args } = parseArgs(argv);"],
705
+ "replace": ["\tconst { nativeModules = [undefined], resume = false, ...args } = parseArgs(argv);"],
706
706
  "gate": ["bash", "run.sh", "check-pi-durable-beside"],
707
707
  "timeoutSeconds": 300,
708
708
  "signature": "[QK:PI-DURABLE-MODULE-ABSENT-UNCHANGED]",
@@ -735,12 +735,12 @@
735
735
  "\tconst extensions = await loadNativeModules(nativeModules);",
736
736
  "\t// Loaded before anything opens, as upstream main.ts imports it statically: a TUI that cannot",
737
737
  "\t// load opens no session, mints no citizen and leaves nothing to clean up.",
738
- "\tconst { runDurableTui } = await loadTui();"
738
+ "\tconst tui = await loadTui();"
739
739
  ],
740
740
  "replace": [
741
741
  "\t// Loaded before anything opens, as upstream main.ts imports it statically: a TUI that cannot",
742
742
  "\t// load opens no session, mints no citizen and leaves nothing to clean up.",
743
- "\tconst { runDurableTui } = await loadTui();",
743
+ "\tconst tui = await loadTui();",
744
744
  "\tconst extensions = await loadNativeModules(nativeModules);"
745
745
  ],
746
746
  "gate": ["bash", "run.sh", "check-pi-durable-beside"],
@@ -791,8 +791,8 @@
791
791
  "claim": "PI-DURABLE-MODULE-CWD-PINNED",
792
792
  "title": "the app is opened wherever the process is at open time, not in the caller's directory fixed before the module ran",
793
793
  "subject": "pi/pi-durable/bootstrap.mjs",
794
- "find": ["\tconst citizen = await openDurableCitizen({ ...args, cwd, extensions, bridgeEntry, open, contact });"],
795
- "replace": ["\tconst citizen = await openDurableCitizen({ ...args, extensions, bridgeEntry, open, contact });"],
794
+ "find": ["\t\t...(session === undefined ? {} : { session }),\n\t\tcwd,\n\t\textensions,\n\t\tbridgeEntry,"],
795
+ "replace": ["\t\t...(session === undefined ? {} : { session }),\n\t\textensions,\n\t\tbridgeEntry,"],
796
796
  "gate": ["bash", "run.sh", "check-pi-durable-beside"],
797
797
  "timeoutSeconds": 300,
798
798
  "signature": "[QK:PI-DURABLE-MODULE-CWD-PINNED]",
@@ -1142,7 +1142,7 @@
1142
1142
  },
1143
1143
  {
1144
1144
  "claim": "PI-DURABLE-COMPACTION-FAILURE-SHOWN",
1145
- "title": "the TUI drops the conversation's retained compaction failure: a failed background compaction vanishes from the screen once its status is removed",
1145
+ "title": "the TUI drops the conversation's retained compaction failure: a failed threshold compaction vanishes from the screen once its status is removed",
1146
1146
  "subject": "pi/pi-durable/carrier/tui.js",
1147
1147
  "find": ["\tif (compaction !== undefined) {\n\t\tlines.push"],
1148
1148
  "replace": ["\tif (compaction === null) {\n\t\tlines.push"],
@@ -1151,6 +1151,78 @@
1151
1151
  "signature": "[QK:PI-DURABLE-COMPACTION-FAILURE-SHOWN]",
1152
1152
  "signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
1153
1153
  "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1154
+ },
1155
+ {
1156
+ "claim": "PI-DURABLE-STREAM-TRANSPORT-WIRE",
1157
+ "title": "the Harness settings stop carrying the transport: the provider never continues a response, so every request of a long session resends the whole conversation",
1158
+ "subject": "pi/pi-durable/carrier/harness-setup.js",
1159
+ "find": ["\t\t\t\ttransport: settingsManager.getTransport(),"],
1160
+ "replace": ["\t\t\t\t// transport is not forwarded"],
1161
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
1162
+ "timeoutSeconds": 300,
1163
+ "signature": "[QK:PI-DURABLE-STREAM-TRANSPORT-WIRE]",
1164
+ "signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
1165
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1166
+ },
1167
+ {
1168
+ "claim": "PI-DURABLE-STREAM-TRANSPORT-FORWARDED",
1169
+ "title": "the Harness settings fix the transport instead of reading the operator's: a chosen sse or plain websocket is silently replaced by auto",
1170
+ "subject": "pi/pi-durable/carrier/harness-setup.js",
1171
+ "find": ["\t\t\t\ttransport: settingsManager.getTransport(),"],
1172
+ "replace": ["\t\t\t\ttransport: \"auto\","],
1173
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
1174
+ "timeoutSeconds": 300,
1175
+ "signature": "[QK:PI-DURABLE-STREAM-TRANSPORT-FORWARDED]",
1176
+ "signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
1177
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1178
+ },
1179
+ {
1180
+ "claim": "PI-DURABLE-COMPACTION-WAITS",
1181
+ "title": "the Harness settings hand the SDK pi's compaction settings alone: its default background policy summarizes beside a running turn instead of the turn waiting",
1182
+ "subject": "pi/pi-durable/carrier/harness-setup.js",
1183
+ "find": ["\t\t\treturn { ...settingsManager.getCompactionSettings(), backgroundTokens: 0 };"],
1184
+ "replace": ["\t\t\treturn settingsManager.getCompactionSettings();"],
1185
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
1186
+ "timeoutSeconds": 300,
1187
+ "signature": "[QK:PI-DURABLE-COMPACTION-WAITS]",
1188
+ "signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
1189
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1190
+ },
1191
+ {
1192
+ "claim": "PI-DURABLE-PROMPT-SKILL-SCOPE-BASE",
1193
+ "title": "the prompt hands the skills setting over as written: a relative project entry is read against the conversation's directory and its skills are silently not listed",
1194
+ "subject": "pi/pi-durable/carrier/prompt.js",
1195
+ "find": ["\t\t\t\t\tskillPaths: scopedSkillPaths(settings, agentDir, fallbackCwd),"],
1196
+ "replace": ["\t\t\t\t\tskillPaths: settings.getSkillPaths(),"],
1197
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
1198
+ "timeoutSeconds": 300,
1199
+ "signature": "[QK:PI-DURABLE-PROMPT-SKILL-SCOPE-BASE]",
1200
+ "signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
1201
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1202
+ },
1203
+ {
1204
+ "claim": "PI-DURABLE-RESUME-OPENS-CHOSEN",
1205
+ "title": "the runtime drops the chosen session id: --resume opens a new session instead of the conversation the operator picked",
1206
+ "subject": "pi/pi-durable/carrier/runtime.js",
1207
+ "find": ["\t\toptions.continueSession ?? false,\n\t\toptions.session,\n\t);"],
1208
+ "replace": ["\t\toptions.continueSession ?? false,\n\t);"],
1209
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
1210
+ "timeoutSeconds": 300,
1211
+ "signature": "[QK:PI-DURABLE-RESUME-OPENS-CHOSEN]",
1212
+ "signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
1213
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1214
+ },
1215
+ {
1216
+ "claim": "PI-DURABLE-RESUME-CANCEL-OPENS-NOTHING",
1217
+ "title": "a cancelled --resume falls back to the newest session: the operator said no and a conversation opens anyway",
1218
+ "subject": "pi/pi-durable/bootstrap.mjs",
1219
+ "find": ["\t\t\tprocess.stderr.write(\"[pi-durable] resume-cancelled: nothing was opened\\n\");\n\t\t\treturn;"],
1220
+ "replace": ["\t\t\targs.continueSession = true;"],
1221
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
1222
+ "timeoutSeconds": 300,
1223
+ "signature": "[QK:PI-DURABLE-RESUME-CANCEL-OPENS-NOTHING]",
1224
+ "signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
1225
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1154
1226
  }
1155
1227
  ]
1156
1228
  }
@@ -68,6 +68,7 @@ import {
68
68
  } from "../pi-extensions/lib/meta-session.ts";
69
69
  import { FRESH_CALL_BACKENDS } from "../pi-extensions/lib/mux-fresh-call.ts";
70
70
  import {
71
+ checkCodexSocketFile,
71
72
  readCodexThread,
72
73
  realCodexProtocolOpener,
73
74
  resolveCodexDefaultSocketPath,
@@ -95,6 +96,12 @@ import {
95
96
  sourceCleanupWindows,
96
97
  validateFinalMailboxBody,
97
98
  } from "./lib/codex-fresh-source-receipts.ts";
99
+ import {
100
+ describeCodexSocketOwnership,
101
+ judgeCodexSocketOwnership,
102
+ observeCodexSocketSubtree,
103
+ parseProcStat,
104
+ } from "./lib/codex-socket-listener.ts";
98
105
  import { skipLive } from "./lib/live-skip.ts";
99
106
  import { parseTmuxRow, TMUX_COORDINATE_FIELDS, TMUX_COORDINATE_FORMAT } from "./lib/tmux-coordinate-row.ts";
100
107
 
@@ -697,7 +704,8 @@ async function run(): Promise<void> {
697
704
  // `--cut` still refuses it as BLOCKED (MUST SKIP). Nothing is
698
705
  // excused — a cut without this receipt remains impossible.
699
706
  // PRESENT BUT WRONG -> FAIL, unchanged: a malformed or dead PID, a process whose
700
- // argv is not a Codex app-server, a foreign uid, A == S. The
707
+ // argv is not a Codex app-server, a foreign uid, a subtree that
708
+ // does not hold the endpoint's LISTEN socket, A == S. The
701
709
  // operator answered, and the answer is a defect.
702
710
  // Nothing is inferred either way, and no default is invented for a value the operator
703
711
  // did not give. The reproducible green is the invocation VERIFY and the release skill's
@@ -747,6 +755,10 @@ async function run(): Promise<void> {
747
755
  fs.existsSync(procDir),
748
756
  `missing ${procDir}`,
749
757
  );
758
+ // Read BEFORE the uid/argv/environment checks below and handed to the socket-ownership
759
+ // observer, so the process those checks read and the process that owns the socket are the
760
+ // same one: a pid reused in between changes this start time and fails closed (#128).
761
+ const namedStartTime = parseProcStat(fs.readFileSync(path.join(procDir, "stat"), "utf8")).startTime;
750
762
  const procStat = fs.statSync(procDir);
751
763
  if (typeof process.getuid === "function") {
752
764
  ok("the named app-server process belongs to this operator", procStat.uid === process.getuid());
@@ -774,15 +786,27 @@ async function run(): Promise<void> {
774
786
  // cannot be borrowed into a claim about what a Codex MCP child inherits.
775
787
  appServerEnv = { ...procEnv };
776
788
  codexSocketPath = resolveCodexDefaultSocketPath(appServerEnv);
777
- const expectedListen = `unix://${codexSocketPath}`;
778
- const listensOnDefaultSocket = argv.some(
779
- (arg, index) => (arg === "--listen" && argv[index + 1] === expectedListen) || arg === `--listen=${expectedListen}`,
789
+ // Ownership is read from the kernel, not from argv (#128). The endpoint is the one delivery
790
+ // itself connects to — the production classifier's verified physical socket — and the claim
791
+ // is that the named pid's OWN subtree holds its LISTEN inode. The argv `--listen` spelling
792
+ // is recorded in the receipt below and decides nothing: a wrapper whose child listens and a
793
+ // listener started with the vendor-default `--listen unix://` are both read as what they are.
794
+ const socketFile = checkCodexSocketFile(codexSocketPath);
795
+ ok(
796
+ "the default control path is a live socket or Codex's exact rendezvous alias — the endpoint delivery connects to",
797
+ socketFile.ok,
798
+ socketFile.ok ? "" : `${codexSocketPath}: ${socketFile.reason}`,
799
+ );
800
+ const socketOwnership = judgeCodexSocketOwnership(
801
+ observeCodexSocketSubtree(appServerPid, socketFile.connectPath, namedStartTime),
780
802
  );
781
803
  ok(
782
- "the explicit app-server PID owns the same default UDS the fresh Codex runtime will use",
783
- listensOnDefaultSocket,
784
- `expected --listen ${expectedListen}; argv=${JSON.stringify(argv)}`,
804
+ "the explicit app-server PID's own process subtree holds the LISTEN socket at that verified endpoint",
805
+ socketOwnership.verdict === "owned",
806
+ `${describeCodexSocketOwnership(socketOwnership)}; argv=${JSON.stringify(argv)}`,
785
807
  );
808
+ receipts["1a-codex-socket-ownership"] =
809
+ `advertised=${codexSocketPath}\nendpoint=${socketFile.connectPath}\n${describeCodexSocketOwnership(socketOwnership)}`;
786
810
  const appServerCoordinate = inspectTmuxCoordinate("Codex app-server/MCP inherited seat", appServerEnv);
787
811
  const fixtureCoordinate = inspectTmuxCoordinate("fixture/initial-Pi caller seat", process.env);
788
812
  ok(
@@ -538,9 +538,29 @@ ok(
538
538
  fs.existsSync(liveDb),
539
539
  liveDb,
540
540
  );
541
+ const sqlite = await import("node:sqlite");
542
+ // A doorbell input stays `placed` while the run that consumed it is active and turns `done` only when that run
543
+ // ends (pi-durable's SubmissionRecord). Pi's relay can land before pd's final generation commits, so the snapshot
544
+ // waits for THIS message's submission to settle (`done` or `unanswered`); the assertions below judge what settled.
545
+ const msgName = /meta-mailbox → enqueued \(([^)]+)\)/.exec(piToPd.text)?.[1] ?? "";
546
+ const doorbellRequestId = `entwurf-doorbell:${pdGid}:${msgName}`;
547
+ if (msgName !== "") {
548
+ receipts["6-native-doorbell-settled"] = await until(`pd's doorbell submission to settle`, TURN_WAIT_MS, () => {
549
+ const live = new sqlite.DatabaseSync(liveDb, { readOnly: true });
550
+ try {
551
+ const rows = live.prepare("SELECT status, record FROM submissions").all() as Array<{
552
+ status: string;
553
+ record: string;
554
+ }>;
555
+ const hit = rows.find((r) => (JSON.parse(r.record) as { requestId?: unknown }).requestId === doorbellRequestId);
556
+ return hit?.status === "done" || hit?.status === "unanswered" ? hit.status : null;
557
+ } finally {
558
+ live.close();
559
+ }
560
+ });
561
+ }
541
562
  const snapshot = path.join(receiptsDir, "pd-session.snapshot.sqlite");
542
563
  fs.mkdirSync(receiptsDir, { recursive: true });
543
- const sqlite = await import("node:sqlite");
544
564
  {
545
565
  const source = new sqlite.DatabaseSync(liveDb, { readOnly: true });
546
566
  try {
@@ -599,8 +619,7 @@ ok(
599
619
  JSON.stringify(pdToPi),
600
620
  );
601
621
  receipts["6-pd-to-pi-settled"] = JSON.stringify(pdToPi);
602
- const msgName = /meta-mailbox → enqueued \(([^)]+)\)/.exec(piToPd.text)?.[1] ?? "";
603
- const doorbell = pdDoorbellSubmission(nativeSubmissions, rootId, `entwurf-doorbell:${pdGid}:${msgName}`);
622
+ const doorbell = pdDoorbellSubmission(nativeSubmissions, rootId, doorbellRequestId);
604
623
  receipts["6-native-doorbell-submission"] = JSON.stringify(doorbell);
605
624
  ok(
606
625
  "the doorbell admitted THIS message's notice to the root conversation and the submission is done",