@junghanacs/entwurf 0.35.1 → 0.36.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 +2 -2
- package/CHANGELOG.md +209 -0
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +15 -0
- package/FAQ.md +3 -3
- package/README.md +30 -16
- package/VERIFY.md +9 -7
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/setup-clean-host.md +30 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +4 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +99 -18
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +59 -14
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-classify.js +44 -0
- package/mcp/entwurf-bridge/tsconfig.build.json +5 -0
- package/package.json +19 -14
- package/pi/pi-durable/bootstrap.mjs +12 -8
- package/pi/pi-durable/carrier/runtime.js +52 -2
- package/pi/pi-durable/carrier/subagent.js +6 -1
- package/pi/pi-durable/carrier/tui.js +31 -4
- package/pi/pi-durable/carrier-relocation.mjs +2 -2
- package/pi/pi-durable/overlay/upstream-pin.json +79 -36
- package/pi-extensions/lib/codex-fresh-preflight.ts +4 -3
- package/pi-extensions/lib/native-push/codex-ws-client.ts +129 -18
- package/pi-extensions/lib/pi-durable-runtime.ts +4 -4
- package/pi-extensions/meta-bridge-pi-durable.ts +77 -21
- package/run.sh +112 -38
- package/scripts/check-codex-app-server-launch.ts +55 -12
- package/scripts/check-codex-native-push.ts +231 -12
- package/scripts/check-gate-qualification.ts +3 -3
- package/scripts/codex-app-server-launch.sh +54 -29
- package/scripts/codex-socket-classify.ts +46 -0
- package/scripts/emit-pi-durable-carrier.ts +159 -168
- package/scripts/mutants/codex-app-server-launch.json +39 -12
- package/scripts/mutants/codex-native.json +15 -0
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +165 -16
- package/scripts/mutants/pi-floor.json +5 -5
- package/scripts/pi-durable-contact-host.mjs +1 -1
- package/pi/pi-durable/overlay/runtime-contacts.patch +0 -283
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"claim": "PI-CORE-PIN-TRACKS-FLOOR",
|
|
182
182
|
"title": "the queue oracle's pi-agent-core pin drifts off the floor the rest of the pi runtime is pinned to, so the oracle keeps answering honestly — about a pi nobody in this repo runs",
|
|
183
183
|
"subject": "package.json",
|
|
184
|
-
"find": ["\"@earendil-works/pi-agent-core\": \"1.
|
|
184
|
+
"find": ["\"@earendil-works/pi-agent-core\": \"1.2.0\","],
|
|
185
185
|
"replace": ["\"@earendil-works/pi-agent-core\": \"0.87.1\","],
|
|
186
186
|
"gate": ["bash", "run.sh", "check-dep-versions"],
|
|
187
187
|
"timeoutSeconds": 300,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"claim": "PACK-INSTALL-PIN-MATCHER-BOUNDED",
|
|
7
7
|
"title": "the pin-leak matcher loses its version boundary and blesses substring lookalikes — @0.85.10 reads as the pinned 0.85.1 and the gate announces a verified tree it never verified (independent review, 2026-08-25)",
|
|
8
8
|
"subject": "run.sh",
|
|
9
|
-
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.
|
|
9
|
+
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.2\\.0(_|$)' || true"],
|
|
10
10
|
"replace": [" grep '^@earendil-works+' | grep -v '@0\\.87\\.1' || true"],
|
|
11
11
|
"gate": ["bash", "run.sh", "check-pack-pin-matcher"],
|
|
12
12
|
"timeoutSeconds": 120,
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"claim": "PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE",
|
|
18
18
|
"title": "the pin-leak matcher narrows back to the pi-* families and goes blind to a non-pi member of the same runtime closure — @earendil-works/chord (new in pi 0.85.0, a runtime dependency of pi-coding-agent/pi-agent-core/pi-client/pi-protocol) floats unpinned while the gate prints a verified pin (measured 2026-09-06 on a real 0.85.1 install tree)",
|
|
19
19
|
"subject": "run.sh",
|
|
20
|
-
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.
|
|
21
|
-
"replace": [" grep '^@earendil-works+pi-' | grep -Ev '@1\\.
|
|
20
|
+
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.2\\.0(_|$)' || true"],
|
|
21
|
+
"replace": [" grep '^@earendil-works+pi-' | grep -Ev '@1\\.2\\.0(_|$)' || true"],
|
|
22
22
|
"gate": ["bash", "run.sh", "check-pack-pin-matcher"],
|
|
23
23
|
"timeoutSeconds": 120,
|
|
24
24
|
"signature": "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE]",
|
|
@@ -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\": \"
|
|
461
|
+
"find": ["\"sha256\": \"60c2965eadbdbbe78fce1b7eeb89d97eb3104a30b7d58ebca87009d2e9118403\""],
|
|
462
462
|
"replace": ["\"sha256\": \"0000000000000000000000000000000000000000000000000000000000000000\""],
|
|
463
463
|
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
464
464
|
"timeoutSeconds": 300,
|
|
@@ -833,19 +833,40 @@
|
|
|
833
833
|
}
|
|
834
834
|
},
|
|
835
835
|
{
|
|
836
|
-
"claim": "PI-DURABLE-CARRIER-
|
|
837
|
-
"title": "
|
|
836
|
+
"claim": "PI-DURABLE-CARRIER-EMIT-SOURCE-EQUAL",
|
|
837
|
+
"title": "the owned source changes and the carrier is not re-emitted, so the shipped app is no longer the emission of the source the repository reviews",
|
|
838
|
+
"subject": "pi/pi-durable/src/experimental/durable/tui.ts",
|
|
839
|
+
"find": ["\t\tconst forms = [...detailed, tasks, `⏳ ${n}`, `⏳${n}`, `*${n}`, \"*+\", \"*\"];"],
|
|
840
|
+
"replace": ["\t\tconst forms = [...detailed, tasks, `⏳ ${n}`, `⏳${n}`, `*${n}`, \"*\"];"],
|
|
841
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
842
|
+
"timeoutSeconds": 300,
|
|
843
|
+
"signature": "[QK:PI-DURABLE-CARRIER-EMIT-SOURCE-EQUAL]",
|
|
844
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
845
|
+
"build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
"claim": "PI-DURABLE-CARRIER-EMIT-NODE-EXACT",
|
|
849
|
+
"title": "the emitter stops comparing its own Node with the pinned one, so a carrier emitted under any Node reads as the pinned emission",
|
|
838
850
|
"subject": "scripts/emit-pi-durable-carrier.ts",
|
|
839
|
-
"find": ["\tif (
|
|
840
|
-
"replace": ["\tif (
|
|
851
|
+
"find": ["\tif (pinned !== running) {"],
|
|
852
|
+
"replace": ["\tif (false) {"],
|
|
841
853
|
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
842
854
|
"timeoutSeconds": 300,
|
|
843
|
-
"signature": "[QK:PI-DURABLE-CARRIER-
|
|
855
|
+
"signature": "[QK:PI-DURABLE-CARRIER-EMIT-NODE-EXACT]",
|
|
844
856
|
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
845
|
-
"build": {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
857
|
+
"build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
"claim": "PI-DURABLE-SOURCE-FENCE-RESOLVES",
|
|
861
|
+
"title": "the source fence inherits the root program's exclusion of pi/pi-durable/src, so the owned source sits in no typecheck program at all",
|
|
862
|
+
"subject": "pi/pi-durable/tsconfig.json",
|
|
863
|
+
"find": ["\t\"exclude\": []"],
|
|
864
|
+
"replace": ["\t\"files\": []"],
|
|
865
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
866
|
+
"timeoutSeconds": 300,
|
|
867
|
+
"signature": "[QK:PI-DURABLE-SOURCE-FENCE-RESOLVES]",
|
|
868
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
869
|
+
"build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
|
|
849
870
|
},
|
|
850
871
|
{
|
|
851
872
|
"claim": "PI-DURABLE-FOOTER-GRAPH-LIFETIME",
|
|
@@ -898,12 +919,8 @@
|
|
|
898
919
|
"claim": "PI-DURABLE-FOOTER-BADGE-PROTECTED",
|
|
899
920
|
"title": "the badge sits last in the dock, so the renderer's bottom clip drops it first on a short terminal",
|
|
900
921
|
"subject": "pi/pi-durable/carrier/tui.js",
|
|
901
|
-
"find": [
|
|
902
|
-
|
|
903
|
-
],
|
|
904
|
-
"replace": [
|
|
905
|
-
"\t\t{ component: new VStack([...dock, { component: badge, shrink: 0 }]), basis: \"auto\", grow: 0, shrink: 1, minSize: 1 },"
|
|
906
|
-
],
|
|
922
|
+
"find": ["\t\t\tcomponent: new VStack([{ component: badge, shrink: 0 }, ...dock]),"],
|
|
923
|
+
"replace": ["\t\t\tcomponent: new VStack([...dock, { component: badge, shrink: 0 }]),"],
|
|
907
924
|
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
908
925
|
"timeoutSeconds": 300,
|
|
909
926
|
"signature": "[QK:PI-DURABLE-FOOTER-BADGE-PROTECTED]",
|
|
@@ -1002,6 +1019,138 @@
|
|
|
1002
1019
|
"argv": ["bash", "scripts/build-bridge.sh"],
|
|
1003
1020
|
"output": "mcp/entwurf-bridge/dist"
|
|
1004
1021
|
}
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
"claim": "PI-DURABLE-SOURCE-NOT-SHIPPED",
|
|
1025
|
+
"title": "the files allowlist admits pi/pi-durable/src, so the owned TypeScript source rides the tarball beside the carrier an installed consumer runs",
|
|
1026
|
+
"subject": "package.json",
|
|
1027
|
+
"find": ["\t\t\"pi/pi-durable/carrier/\",\n"],
|
|
1028
|
+
"replace": ["\t\t\"pi/pi-durable/carrier/\",\n\t\t\"pi/pi-durable/src/\",\n"],
|
|
1029
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
1030
|
+
"timeoutSeconds": 300,
|
|
1031
|
+
"signature": "[QK:PI-DURABLE-SOURCE-NOT-SHIPPED]",
|
|
1032
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
1033
|
+
"build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
"claim": "PI-DURABLE-EMITTER-NODE-CI-BOUND",
|
|
1037
|
+
"title": "the CI check job floats its Node major again, so the floor that proves the carrier emission runs under a Node other than the pinned emitter",
|
|
1038
|
+
"subject": ".github/workflows/ci.yml",
|
|
1039
|
+
"find": [" node-version: 24.18.1\n"],
|
|
1040
|
+
"replace": [" node-version: 24\n"],
|
|
1041
|
+
"gate": ["bash", "run.sh", "check-node-floor-coherence"],
|
|
1042
|
+
"timeoutSeconds": 120,
|
|
1043
|
+
"signature": "[QK:PI-DURABLE-EMITTER-NODE-CI-BOUND]",
|
|
1044
|
+
"signatureSource": "run.sh"
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
"claim": "PI-DURABLE-EMITTER-NODE-CI-REQUIRED",
|
|
1048
|
+
"title": "the check:full step's ENTWURF_REQUIRE_EMITTER_NODE line is commented out, so an emitter-identity mismatch on the CI floor becomes a skip while the workflow text still names the variable",
|
|
1049
|
+
"subject": ".github/workflows/ci.yml",
|
|
1050
|
+
"find": [" ENTWURF_REQUIRE_EMITTER_NODE: \"1\"\n"],
|
|
1051
|
+
"replace": [" # ENTWURF_REQUIRE_EMITTER_NODE: \"1\"\n"],
|
|
1052
|
+
"gate": ["bash", "run.sh", "check-node-floor-coherence"],
|
|
1053
|
+
"timeoutSeconds": 120,
|
|
1054
|
+
"signature": "[QK:PI-DURABLE-EMITTER-NODE-CI-REQUIRED]",
|
|
1055
|
+
"signatureSource": "run.sh"
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
"claim": "PI-DURABLE-SUBAGENT-OUTPUT-MODEL-VISIBLE",
|
|
1059
|
+
"title": "the emitted subagent answers in `content`, the 1.1 field: on 1.2 the result still type-checks against the all-optional result and the parent model reads nothing of the child's answer",
|
|
1060
|
+
"subject": "pi/pi-durable/carrier/subagent.js",
|
|
1061
|
+
"find": ["\t\t\t\t\toutput: [{ type: \"text\", text }],"],
|
|
1062
|
+
"replace": ["\t\t\t\t\tcontent: [{ type: \"text\", text }],"],
|
|
1063
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
1064
|
+
"timeoutSeconds": 300,
|
|
1065
|
+
"signature": "[QK:PI-DURABLE-SUBAGENT-OUTPUT-MODEL-VISIBLE]",
|
|
1066
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
1067
|
+
"build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
"claim": "PI-DURABLE-BRIDGE-TOOL-OUTPUT-MODEL-VISIBLE",
|
|
1071
|
+
"title": "the contact's bridge tools answer in `content`, the 1.1 field, past the declared result type: the bridge still builds and the model reads none of the bridge's text",
|
|
1072
|
+
"subject": "pi-extensions/meta-bridge-pi-durable.ts",
|
|
1073
|
+
"find": ["\t\t\treturn { output: [{ type: \"text\", text: result.text }] };"],
|
|
1074
|
+
"replace": ["\t\t\treturn { content: [{ type: \"text\", text: result.text }] } as unknown as DurableToolResult;"],
|
|
1075
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
1076
|
+
"timeoutSeconds": 300,
|
|
1077
|
+
"signature": "[QK:PI-DURABLE-BRIDGE-TOOL-OUTPUT-MODEL-VISIBLE]",
|
|
1078
|
+
"signatureSource": "pi-extensions/meta-bridge-pi-durable.test.ts",
|
|
1079
|
+
"build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
"claim": "PI-DURABLE-FATAL-SESSION-VIEW",
|
|
1083
|
+
"title": "the close listener stops recording the failure: a Session that failed and closed itself looks open, and the host learns of it only if `closed` ever settles, which a tool ignoring its abort can hold off for good",
|
|
1084
|
+
"subject": "pi/pi-durable/carrier/runtime.js",
|
|
1085
|
+
"find": ["\t\t\tupdate({ failure: {} });"],
|
|
1086
|
+
"replace": ["\t\t\t// the failure is not recorded"],
|
|
1087
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
1088
|
+
"timeoutSeconds": 300,
|
|
1089
|
+
"signature": "[QK:PI-DURABLE-FATAL-SESSION-VIEW]",
|
|
1090
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
1091
|
+
"build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
"claim": "PI-DURABLE-FATAL-SESSION-SHOWN",
|
|
1095
|
+
"title": "the TUI stops drawing the failed session's line: the view knows, the screen keeps the last live frame and every later keystroke looks accepted",
|
|
1096
|
+
"subject": "pi/pi-durable/carrier/tui.js",
|
|
1097
|
+
"find": ["\tif (view.failure !== undefined) {"],
|
|
1098
|
+
"replace": ["\tif (view.failure === null) {"],
|
|
1099
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
1100
|
+
"timeoutSeconds": 300,
|
|
1101
|
+
"signature": "[QK:PI-DURABLE-FATAL-SESSION-SHOWN]",
|
|
1102
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
1103
|
+
"build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
"claim": "PI-DURABLE-FATAL-SUBMIT-REFUSED",
|
|
1107
|
+
"title": "input to a failed session is handed to the SDK instead of being refused by name: it fails later with the SDK's generic error and the user is not told the session takes no input",
|
|
1108
|
+
"subject": "pi/pi-durable/carrier/runtime.js",
|
|
1109
|
+
"find": ["\t\t\t\t\tif (state.failure !== undefined) {"],
|
|
1110
|
+
"replace": ["\t\t\t\t\tif (state.failure === null) {"],
|
|
1111
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
1112
|
+
"timeoutSeconds": 300,
|
|
1113
|
+
"signature": "[QK:PI-DURABLE-FATAL-SUBMIT-REFUSED]",
|
|
1114
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
1115
|
+
"build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
"claim": "PI-DURABLE-RECEIVER-RETIRE-ON-FATAL",
|
|
1119
|
+
"title": "the doorbell ignores the session's failure: its marker stays and senders keep waking a host that can admit nothing, until a ring happens to be rejected",
|
|
1120
|
+
"subject": "pi-extensions/meta-bridge-pi-durable.ts",
|
|
1121
|
+
"find": ["const problems = retire(\"session-failed\", \"the durable session failed and closed itself\");"],
|
|
1122
|
+
"replace": ["const problems: string[] = [];"],
|
|
1123
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
1124
|
+
"timeoutSeconds": 300,
|
|
1125
|
+
"signature": "[QK:PI-DURABLE-RECEIVER-RETIRE-ON-FATAL]",
|
|
1126
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
1127
|
+
"build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
"claim": "PI-DURABLE-SELF-CLOSE-NOT-FAILURE",
|
|
1131
|
+
"title": "close() keeps its close listener subscribed: every normal exit reports a failed session and retires its listeners as if the storage had failed",
|
|
1132
|
+
"subject": "pi/pi-durable/carrier/runtime.js",
|
|
1133
|
+
"find": [
|
|
1134
|
+
"\t\t\t\t\t// First: the Session runs its close listeners inside its own close call.\n\t\t\t\t\tunsubscribeClose();"
|
|
1135
|
+
],
|
|
1136
|
+
"replace": ["\t\t\t\t\t// First: the Session runs its close listeners inside its own close call."],
|
|
1137
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
1138
|
+
"timeoutSeconds": 300,
|
|
1139
|
+
"signature": "[QK:PI-DURABLE-SELF-CLOSE-NOT-FAILURE]",
|
|
1140
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
1141
|
+
"build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
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",
|
|
1146
|
+
"subject": "pi/pi-durable/carrier/tui.js",
|
|
1147
|
+
"find": ["\tif (compaction !== undefined) {\n\t\tlines.push"],
|
|
1148
|
+
"replace": ["\tif (compaction === null) {\n\t\tlines.push"],
|
|
1149
|
+
"gate": ["bash", "run.sh", "check-pi-durable-beside"],
|
|
1150
|
+
"timeoutSeconds": 300,
|
|
1151
|
+
"signature": "[QK:PI-DURABLE-COMPACTION-FAILURE-SHOWN]",
|
|
1152
|
+
"signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
|
|
1153
|
+
"build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
|
|
1005
1154
|
}
|
|
1006
1155
|
]
|
|
1007
1156
|
}
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
"title": "one row of the pnpm workspace closure is left on the previous floor while every other surface moved — the shape a version bump actually fails in. Nothing else reads that file, so the stranded row surfaces only at install time, in a different command, far from the change that stranded it",
|
|
8
8
|
"subject": "pnpm-workspace.yaml",
|
|
9
9
|
"find": [
|
|
10
|
-
" - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2 || 1.0.0 || 1.0.2 || 1.0.4 || 1.1.0'"
|
|
10
|
+
" - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2 || 1.0.0 || 1.0.2 || 1.0.4 || 1.1.0 || 1.2.0'"
|
|
11
11
|
],
|
|
12
12
|
"replace": [
|
|
13
|
-
" - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2 || 1.0.0 || 1.0.2 || 1.0.4'"
|
|
13
|
+
" - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2 || 1.0.0 || 1.0.2 || 1.0.4 || 1.1.0'"
|
|
14
14
|
],
|
|
15
15
|
"gate": ["bash", "run.sh", "check-dep-versions"],
|
|
16
16
|
"timeoutSeconds": 300,
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"claim": "PI-SDK-PRODUCTION-SET",
|
|
22
22
|
"title": "the durable library's production pin falls one release behind the SDK set the pin declares, so the carrier would bind a pi-durable nobody verified it against",
|
|
23
23
|
"subject": "package.json",
|
|
24
|
-
"find": ["\"@earendil-works/pi-durable\": \"1.
|
|
25
|
-
"replace": ["\"@earendil-works/pi-durable\": \"1.0
|
|
24
|
+
"find": ["\"@earendil-works/pi-durable\": \"1.2.0\","],
|
|
25
|
+
"replace": ["\"@earendil-works/pi-durable\": \"1.1.0\","],
|
|
26
26
|
"gate": ["bash", "run.sh", "check-dep-versions"],
|
|
27
27
|
"timeoutSeconds": 300,
|
|
28
28
|
"signature": "[QK:PI-SDK-PRODUCTION-SET]",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"title": "an @earendil-works package returns as a dev dependency beside its production pin, one name in two dependency classes",
|
|
34
34
|
"subject": "package.json",
|
|
35
35
|
"find": ["\"@biomejs/biome\": \"^2.4.13\","],
|
|
36
|
-
"replace": ["\"@biomejs/biome\": \"^2.4.13\",\n\t\t\"@earendil-works/pi-ai\": \"1.
|
|
36
|
+
"replace": ["\"@biomejs/biome\": \"^2.4.13\",\n\t\t\"@earendil-works/pi-ai\": \"1.2.0\","],
|
|
37
37
|
"gate": ["bash", "run.sh", "check-dep-versions"],
|
|
38
38
|
"timeoutSeconds": 300,
|
|
39
39
|
"signature": "[QK:PI-SDK-NO-DUAL-DECLARATION]",
|
|
@@ -68,7 +68,7 @@ try {
|
|
|
68
68
|
const tool = citizen.contact.extension.tools.find((t) => t.name === name);
|
|
69
69
|
if (tool === undefined) throw new Error(`the contact registers no ${name}`);
|
|
70
70
|
const result = await tool.execute({}, undefined, {});
|
|
71
|
-
return result.
|
|
71
|
+
return result.output.map((block) => block.text).join("\n");
|
|
72
72
|
};
|
|
73
73
|
const view = citizen.durable.view.current();
|
|
74
74
|
const report = {
|
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
diff --git a/packages/coding-agent/src/experimental/durable/runtime.ts b/packages/coding-agent/src/experimental/durable/runtime.ts
|
|
2
|
-
index fcd6e523..7f00e27f 100644
|
|
3
|
-
--- a/packages/coding-agent/src/experimental/durable/runtime.ts
|
|
4
|
-
+++ b/packages/coding-agent/src/experimental/durable/runtime.ts
|
|
5
|
-
@@ -8,7 +8,9 @@ import {
|
|
6
|
-
type ConversationView,
|
|
7
|
-
type Cursor,
|
|
8
|
-
type EntryRecord,
|
|
9
|
-
+ type Extension,
|
|
10
|
-
Harness,
|
|
11
|
-
+ type InputSubmissionDraft,
|
|
12
|
-
type ModelRef,
|
|
13
|
-
ROOT_CONVERSATION_ID,
|
|
14
|
-
type Submission,
|
|
15
|
-
@@ -56,8 +58,10 @@ export interface DurableView {
|
|
16
|
-
readonly conversations: readonly ConversationSummary[];
|
|
17
|
-
readonly models: readonly ModelSummary[];
|
|
18
|
-
readonly notices: readonly Notice[];
|
|
19
|
-
- /** The live task graph while the task panel is open. */
|
|
20
|
-
+ /** The live task graph, observed for the session's lifetime whether or not its panel is shown. */
|
|
21
|
-
readonly tasks?: TaskGraph;
|
|
22
|
-
+ /** Whether the task panel is shown; /tasks toggles only this. */
|
|
23
|
-
+ readonly tasksPanel: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface DurableViewSource {
|
|
27
|
-
@@ -81,6 +85,10 @@ export interface DurableController {
|
|
28
|
-
export interface OpenDurableOptions {
|
|
29
|
-
readonly cwd?: string;
|
|
30
|
-
readonly continueSession?: boolean;
|
|
31
|
-
+ /** Installed after the built-in extensions and before the Harness opens, so recovered work finds their tools. */
|
|
32
|
-
+ readonly extensions?: readonly Extension[];
|
|
33
|
-
+ /** A new session's explicit model, resolved as pi's `--provider`/`--model`; unresolved, the open fails. */
|
|
34
|
-
+ readonly model?: { readonly provider: string; readonly model: string };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface OpenDurableResult {
|
|
38
|
-
@@ -88,6 +96,8 @@ export interface OpenDurableResult {
|
|
39
|
-
readonly controller: DurableController;
|
|
40
|
-
/** pi's settings, for the TUI's theme and terminal capabilities. */
|
|
41
|
-
readonly settings: SettingsManager;
|
|
42
|
-
+ /** Admits input to the root conversation, whichever one is on screen; resolves at admission, not at the answer. */
|
|
43
|
-
+ submitToRoot(draft: InputSubmissionDraft): Promise<Submission>;
|
|
44
|
-
close(): Promise<void>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
@@ -122,6 +132,9 @@ function titleOf(entry: EntryRecord | undefined): { title?: string } {
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function openDurable(options: OpenDurableOptions = {}): Promise<OpenDurableResult> {
|
|
51
|
-
+ if (options.model !== undefined && options.continueSession) {
|
|
52
|
-
+ throw new Error("An explicit model applies only to a new session, not to a continued one");
|
|
53
|
-
+ }
|
|
54
|
-
const location = await selectSession(options.cwd ?? process.cwd(), options.continueSession ?? false);
|
|
55
|
-
const envs = new ExecutionEnvs(location.cwd);
|
|
56
|
-
let harness: Harness | undefined;
|
|
57
|
-
@@ -132,6 +145,7 @@ export async function openDurable(options: OpenDurableOptions = {}): Promise<Ope
|
|
58
|
-
const settings = createHarnessSettings(settingsManager);
|
|
59
|
-
const registry = createCodingRegistry(settingsManager, location.cwd);
|
|
60
|
-
registry.install(Subagent);
|
|
61
|
-
+ for (const extension of options.extensions ?? []) registry.install(extension);
|
|
62
|
-
|
|
63
|
-
const pendingReports: unknown[] = [];
|
|
64
|
-
let report: (error: unknown) => void = (error) => pendingReports.push(error);
|
|
65
|
-
@@ -146,7 +160,29 @@ export async function openDurable(options: OpenDurableOptions = {}): Promise<Ope
|
|
66
|
-
},
|
|
67
|
-
context,
|
|
68
|
-
);
|
|
69
|
-
- const initial = location.created ? await findInitialAgentModel(settingsManager, modelRuntime) : undefined;
|
|
70
|
-
+ const initial = location.created
|
|
71
|
-
+ ? await findInitialAgentModel(settingsManager, modelRuntime, options.model)
|
|
72
|
-
+ : undefined;
|
|
73
|
-
+ if (options.model !== undefined) {
|
|
74
|
-
+ // pi resolves case-insensitive, partial and unknown ids too (a custom-id fallback with only a warning);
|
|
75
|
-
+ // an explicit model must be exactly one registered catalog model, named by its canonical id.
|
|
76
|
-
+ const asked = options.model;
|
|
77
|
-
+ const ref = initial?.model;
|
|
78
|
-
+ const thinking = initial?.thinkingLevel;
|
|
79
|
-
+ const registered = ref !== undefined && modelRuntime.getModel(ref.provider, ref.modelId) !== undefined;
|
|
80
|
-
+ const named =
|
|
81
|
-
+ ref !== undefined &&
|
|
82
|
-
+ (asked.model === ref.modelId || (thinking !== undefined && asked.model === `${ref.modelId}:${thinking}`));
|
|
83
|
-
+ if (ref === undefined || ref.provider !== asked.provider || !registered || !named) {
|
|
84
|
-
+ const resolved =
|
|
85
|
-
+ ref === undefined
|
|
86
|
-
+ ? "none"
|
|
87
|
-
+ : `${ref.provider}/${ref.modelId}${thinking === undefined ? "" : `:${thinking}`}`;
|
|
88
|
-
+ throw new Error(
|
|
89
|
-
+ `explicit-model-not-exact: asked ${asked.provider}/${asked.model}, resolved ${resolved} (registered=${registered})`,
|
|
90
|
-
+ );
|
|
91
|
-
+ }
|
|
92
|
-
+ }
|
|
93
|
-
const root = await harness.root(context, {
|
|
94
|
-
agent: {
|
|
95
|
-
cwd: location.cwd,
|
|
96
|
-
@@ -179,6 +215,7 @@ export async function openDurable(options: OpenDurableOptions = {}): Promise<Ope
|
|
97
|
-
conversations: summaries,
|
|
98
|
-
models: models(),
|
|
99
|
-
notices: [],
|
|
100
|
-
+ tasksPanel: true,
|
|
101
|
-
};
|
|
102
|
-
const listeners = new Set<() => void>();
|
|
103
|
-
let notifying = false;
|
|
104
|
-
@@ -296,17 +333,8 @@ export async function openDurable(options: OpenDurableOptions = {}): Promise<Ope
|
|
105
|
-
const thinking: ModelThinkingLevel = agentOf(state.conversation).thinkingLevel ?? "off";
|
|
106
|
-
await current.configure({ model: ref, thinkingLevel: clampThinkingLevel(model, thinking) }, context);
|
|
107
|
-
}),
|
|
108
|
-
- toggleTasks: () =>
|
|
109
|
-
- command(async () => {
|
|
110
|
-
- if (tasks !== undefined) {
|
|
111
|
-
- closeTasks();
|
|
112
|
-
- update({ tasks: undefined });
|
|
113
|
-
- return;
|
|
114
|
-
- }
|
|
115
|
-
- const graph = await opened.taskGraph(context);
|
|
116
|
-
- tasks = graph;
|
|
117
|
-
- unsubscribeTasks = graph.subscribe((value) => update({ tasks: value }));
|
|
118
|
-
- }),
|
|
119
|
-
+ // Only the panel: the graph stays observed, so live background work is never out of view.
|
|
120
|
-
+ toggleTasks: () => command(async () => update({ tasksPanel: !state.tasksPanel })),
|
|
121
|
-
switchConversation: (id) =>
|
|
122
|
-
command(async () => {
|
|
123
|
-
const next = await opened.conversation(id, context);
|
|
124
|
-
@@ -326,8 +354,11 @@ export async function openDurable(options: OpenDurableOptions = {}): Promise<Ope
|
|
125
|
-
notice("warning", `Saved model is unavailable: ${saved.provider}/${saved.modelId}`);
|
|
126
|
-
}
|
|
127
|
-
if (initial?.fallbackMessage !== undefined) notice("info", initial.fallbackMessage);
|
|
128
|
-
- // The task panel starts open; /tasks hides it.
|
|
129
|
-
- await controller.toggleTasks();
|
|
130
|
-
+ // The task graph is observed until close; its panel starts open and /tasks hides it.
|
|
131
|
-
+ const graph = await opened.taskGraph(context);
|
|
132
|
-
+ tasks = graph;
|
|
133
|
-
+ update({ tasks: graph.value });
|
|
134
|
-
+ unsubscribeTasks = graph.subscribe((value) => update({ tasks: value }));
|
|
135
|
-
// Recovered work from an interrupted turn continues now.
|
|
136
|
-
harness.resume();
|
|
137
|
-
|
|
138
|
-
@@ -342,6 +373,7 @@ export async function openDurable(options: OpenDurableOptions = {}): Promise<Ope
|
|
139
|
-
},
|
|
140
|
-
controller,
|
|
141
|
-
settings: settingsManager,
|
|
142
|
-
+ submitToRoot: (draft) => root.submit(draft, context),
|
|
143
|
-
close() {
|
|
144
|
-
closing ??= (async () => {
|
|
145
|
-
unsubscribe();
|
|
146
|
-
diff --git a/packages/coding-agent/src/experimental/durable/tui.ts b/packages/coding-agent/src/experimental/durable/tui.ts
|
|
147
|
-
index 397b276c..6a8fce8f 100644
|
|
148
|
-
--- a/packages/coding-agent/src/experimental/durable/tui.ts
|
|
149
|
-
+++ b/packages/coding-agent/src/experimental/durable/tui.ts
|
|
150
|
-
@@ -23,12 +23,14 @@ import {
|
|
151
|
-
SelectList,
|
|
152
|
-
type SelectListTheme,
|
|
153
|
-
Spacer,
|
|
154
|
-
+ type StackEntry,
|
|
155
|
-
setCapabilityOverrides,
|
|
156
|
-
setKeybindings,
|
|
157
|
-
Text,
|
|
158
|
-
TruncatedText,
|
|
159
|
-
TuiAltScreen,
|
|
160
|
-
VStack,
|
|
161
|
-
+ visibleWidth,
|
|
162
|
-
} from "@earendil-works/pi-tui";
|
|
163
|
-
import { getAgentDir } from "../../config.ts";
|
|
164
|
-
import { KeybindingsManager } from "../../core/keybindings.ts";
|
|
165
|
-
@@ -150,10 +152,75 @@ interface Handlers {
|
|
166
|
-
cycleThinking(): void;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
+/**
|
|
170
|
-
+ * The live background-task badge: the dock's first row, never shrunk, so live background work stays on
|
|
171
|
-
+ * screen however small the terminal gets. It counts the graph's background tasks whatever their kind and
|
|
172
|
-
+ * names the ones that are not running; with none it renders nothing and the dock is upstream's.
|
|
173
|
-
+ */
|
|
174
|
-
+export class TaskBadge implements Component {
|
|
175
|
-
+ #count = 0;
|
|
176
|
-
+ #detail = "";
|
|
177
|
-
+
|
|
178
|
-
+ get count(): number {
|
|
179
|
-
+ return this.#count;
|
|
180
|
-
+ }
|
|
181
|
-
+
|
|
182
|
-
+ set(graph: TaskGraph | undefined): void {
|
|
183
|
-
+ const nodes = Object.values(graph?.tasks ?? {}).filter((node) => node.background);
|
|
184
|
-
+ const tally = (state: string, match: (node: TaskGraphNode) => boolean): string[] => {
|
|
185
|
-
+ const n = nodes.filter(match).length;
|
|
186
|
-
+ return n === 0 ? [] : [`${n} ${state}`];
|
|
187
|
-
+ };
|
|
188
|
-
+ this.#count = nodes.length;
|
|
189
|
-
+ this.#detail = [
|
|
190
|
-
+ ...tally("aborting", (node) => node.abortRequested),
|
|
191
|
-
+ ...tally("completing", (node) => !node.abortRequested && node.state.status === "completing"),
|
|
192
|
-
+ ...tally("waiting", (node) => !node.abortRequested && node.state.status === "waiting"),
|
|
193
|
-
+ ...tally("pending", (node) => !node.abortRequested && node.state.status === "pending"),
|
|
194
|
-
+ ].join(", ");
|
|
195
|
-
+ }
|
|
196
|
-
+
|
|
197
|
-
+ /** The widest form that fits, padded when there is room; narrower terminals drop detail, then words. */
|
|
198
|
-
+ render(width: number): string[] {
|
|
199
|
-
+ const n = this.#count;
|
|
200
|
-
+ if (n === 0) return [];
|
|
201
|
-
+ const tasks = `⏳ ${n} ${n === 1 ? "task" : "tasks"}`;
|
|
202
|
-
+ const detailed = this.#detail ? [`${tasks} (${this.#detail})`] : [];
|
|
203
|
-
+ const forms = [...detailed, tasks, `⏳ ${n}`, `⏳${n}`, `*${n}`, "*+", "*"];
|
|
204
|
-
+ for (const form of forms) {
|
|
205
|
-
+ if (visibleWidth(form) < width) return [theme.fg("accent", ` ${form}`)];
|
|
206
|
-
+ if (visibleWidth(form) === width) return [theme.fg("accent", form)];
|
|
207
|
-
+ }
|
|
208
|
-
+ return [theme.fg("accent", "*")];
|
|
209
|
-
+ }
|
|
210
|
-
+
|
|
211
|
-
+ invalidate(): void {}
|
|
212
|
-
+}
|
|
213
|
-
+
|
|
214
|
-
+/**
|
|
215
|
-
+ * The screen: the transcript above the dock. The badge is the dock's first row and never shrinks, so the
|
|
216
|
-
+ * renderer, which clips from the bottom, drops the editor and the footer before it; a one-row terminal
|
|
217
|
-
+ * with live background work shows the badge instead of the transcript.
|
|
218
|
-
+ */
|
|
219
|
-
+export function durableLayout(transcript: Component, badge: TaskBadge, dock: readonly StackEntry[]): VStack {
|
|
220
|
-
+ return new VStack([
|
|
221
|
-
+ {
|
|
222
|
-
+ component: transcript,
|
|
223
|
-
+ basis: 0,
|
|
224
|
-
+ grow: 1,
|
|
225
|
-
+ shrink: 1,
|
|
226
|
-
+ minSize: 1,
|
|
227
|
-
+ visible: (viewport) => badge.count === 0 || viewport.height > 1,
|
|
228
|
-
+ },
|
|
229
|
-
+ { component: new VStack([{ component: badge, shrink: 0 }, ...dock]), basis: "auto", grow: 0, shrink: 1, minSize: 1 },
|
|
230
|
-
+ ]);
|
|
231
|
-
+}
|
|
232
|
-
+
|
|
233
|
-
class DurableTui {
|
|
234
|
-
static readonly #renderers: Record<string, ToolRenderers> = createAllToolRenderers();
|
|
235
|
-
readonly #ui: TuiAltScreen;
|
|
236
|
-
readonly #chat = new Container();
|
|
237
|
-
+ readonly #taskBadge = new TaskBadge();
|
|
238
|
-
readonly #tasks = new Container();
|
|
239
|
-
readonly #queue = new Container();
|
|
240
|
-
readonly #notices = new Container();
|
|
241
|
-
@@ -216,15 +283,9 @@ class DurableTui {
|
|
242
|
-
content.addChild(new Spacer(1));
|
|
243
|
-
const transcript = new ScrollView(content, { follow: "end", primary: true, overscroll: "chain" });
|
|
244
|
-
this.#transcript = transcript;
|
|
245
|
-
- const dock = new VStack([
|
|
246
|
-
- { component: this.#tasks, shrink: 1, minSize: 0 },
|
|
247
|
-
- { component: this.#queue, shrink: 1, minSize: 0 },
|
|
248
|
-
- { component: this.#notices, shrink: 1, minSize: 0 },
|
|
249
|
-
- { component: this.#editorContainer, shrink: 1, minSize: 3 },
|
|
250
|
-
- { component: this.#footer, shrink: 1, minSize: 0 },
|
|
251
|
-
- ]);
|
|
252
|
-
for (const component of [
|
|
253
|
-
this.#chat,
|
|
254
|
-
+ this.#taskBadge,
|
|
255
|
-
this.#tasks,
|
|
256
|
-
this.#queue,
|
|
257
|
-
this.#notices,
|
|
258
|
-
@@ -234,9 +295,12 @@ class DurableTui {
|
|
259
|
-
this.#ui.addChild(component);
|
|
260
|
-
}
|
|
261
|
-
this.#ui.setLayoutRoot(
|
|
262
|
-
- new VStack([
|
|
263
|
-
- { component: transcript, basis: 0, grow: 1, shrink: 1, minSize: 1 },
|
|
264
|
-
- { component: dock, basis: "auto", grow: 0, shrink: 1, minSize: 1 },
|
|
265
|
-
+ durableLayout(transcript, this.#taskBadge, [
|
|
266
|
-
+ { component: this.#tasks, shrink: 1, minSize: 0 },
|
|
267
|
-
+ { component: this.#queue, shrink: 1, minSize: 0 },
|
|
268
|
-
+ { component: this.#notices, shrink: 1, minSize: 0 },
|
|
269
|
-
+ { component: this.#editorContainer, shrink: 1, minSize: 3 },
|
|
270
|
-
+ { component: this.#footer, shrink: 1, minSize: 0 },
|
|
271
|
-
]),
|
|
272
|
-
);
|
|
273
|
-
this.#ui.setFocus(this.#editor);
|
|
274
|
-
@@ -299,7 +363,8 @@ class DurableTui {
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
- this.#syncTasks(view.tasks);
|
|
279
|
-
+ this.#taskBadge.set(view.tasks);
|
|
280
|
-
+ this.#syncTasks(view.tasksPanel ? view.tasks : undefined);
|
|
281
|
-
this.#syncQueue((view.conversation.docs["pi.inbox"] ?? { items: [] }) as InboxState);
|
|
282
|
-
this.#syncNotices(view);
|
|
283
|
-
this.#editor.borderColor = theme.getThinkingBorderColor(agentOf(view.conversation).thinkingLevel ?? "off");
|