@tt-a1i/openpi 0.4.0 → 0.5.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/README.md +102 -40
- package/SETUP.md +22 -6
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +13 -5
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/render.ts +17 -257
- package/extensions/file-search/src/binaries.ts +57 -41
- package/extensions/git-read/index.ts +1 -3
- package/extensions/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +153 -86
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +123 -127
- package/extensions/shared/activity-status.ts +30 -0
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/capability-intent.ts +1 -1
- package/extensions/shared/child-session.ts +437 -21
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +73 -33
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +72 -15
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +29 -2
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +270 -59
- package/extensions/subagents/navigation.ts +34 -5
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/manager.ts +34 -2
- package/extensions/subagents/src/prompt.ts +32 -4
- package/extensions/subagents/src/result-artifact.ts +4 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +15 -1
- package/extensions/subagents/src/ui/takeover.ts +73 -257
- package/extensions/subagents/src/ui/transcript.ts +38 -535
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/ui-customization/footer.ts +0 -40
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +1 -1
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +167 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +488 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +74 -4
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +66 -6
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +128 -64
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +25 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +1 -1
- package/skills/workflows/REFERENCE.md +4 -2
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -303
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
type Origin = "baseline" | "session_created";
|
|
5
|
+
|
|
6
|
+
interface PendingEffects {
|
|
7
|
+
creations: Array<{
|
|
8
|
+
path: string;
|
|
9
|
+
existed: boolean;
|
|
10
|
+
observeOnCommandError: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
removals: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface BashAttempt {
|
|
16
|
+
id: string;
|
|
17
|
+
command: string;
|
|
18
|
+
cwd: string;
|
|
19
|
+
confirmDelete: (paths: readonly string[]) => Promise<boolean>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface WriteAttempt {
|
|
23
|
+
id: string;
|
|
24
|
+
path: string;
|
|
25
|
+
cwd: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ShellInspection {
|
|
29
|
+
creations: string[];
|
|
30
|
+
removals: string[];
|
|
31
|
+
opaqueDestructiveCommand: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DYNAMIC_PATH = /[*?[$`]/u;
|
|
35
|
+
const STANDALONE_CONTROL_CHARACTERS = ";&|<>(){}$`*?[]#";
|
|
36
|
+
const COMMAND_FORWARDERS = new Set([
|
|
37
|
+
"!",
|
|
38
|
+
"alias",
|
|
39
|
+
"bash",
|
|
40
|
+
"builtin",
|
|
41
|
+
"chroot",
|
|
42
|
+
"command",
|
|
43
|
+
"dash",
|
|
44
|
+
"doas",
|
|
45
|
+
"env",
|
|
46
|
+
"eval",
|
|
47
|
+
"exec",
|
|
48
|
+
"find",
|
|
49
|
+
"hash",
|
|
50
|
+
"ksh",
|
|
51
|
+
"nice",
|
|
52
|
+
"nohup",
|
|
53
|
+
"sandbox-exec",
|
|
54
|
+
"setsid",
|
|
55
|
+
"sh",
|
|
56
|
+
"sudo",
|
|
57
|
+
"time",
|
|
58
|
+
"timeout",
|
|
59
|
+
"trap",
|
|
60
|
+
"watch",
|
|
61
|
+
"xargs",
|
|
62
|
+
"zsh",
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
function splitShellSegments(command: string) {
|
|
66
|
+
const segments: string[] = [];
|
|
67
|
+
let current = "";
|
|
68
|
+
let quote: "'" | '"' | undefined;
|
|
69
|
+
let escaped = false;
|
|
70
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
71
|
+
const char = command[index] ?? "";
|
|
72
|
+
const next = command[index + 1] ?? "";
|
|
73
|
+
if (escaped) {
|
|
74
|
+
current += char;
|
|
75
|
+
escaped = false;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (char === "\\" && quote !== "'") {
|
|
79
|
+
current += char;
|
|
80
|
+
escaped = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (quote) {
|
|
84
|
+
current += char;
|
|
85
|
+
if (char === quote) quote = undefined;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (char === "'" || char === '"') {
|
|
89
|
+
quote = char;
|
|
90
|
+
current += char;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (
|
|
94
|
+
char === "\n" ||
|
|
95
|
+
char === ";" ||
|
|
96
|
+
(char === "&" && next === "&") ||
|
|
97
|
+
(char === "|" && next === "|")
|
|
98
|
+
) {
|
|
99
|
+
if (current.trim()) segments.push(current.trim());
|
|
100
|
+
current = "";
|
|
101
|
+
if ((char === "&" || char === "|") && next === char) index += 1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
current += char;
|
|
105
|
+
}
|
|
106
|
+
if (current.trim()) segments.push(current.trim());
|
|
107
|
+
return segments;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function shellTokens(segment: string) {
|
|
111
|
+
const tokens: string[] = [];
|
|
112
|
+
let current = "";
|
|
113
|
+
let quote: "'" | '"' | undefined;
|
|
114
|
+
let escaped = false;
|
|
115
|
+
const push = () => {
|
|
116
|
+
if (!current) return;
|
|
117
|
+
tokens.push(current);
|
|
118
|
+
current = "";
|
|
119
|
+
};
|
|
120
|
+
for (const char of segment) {
|
|
121
|
+
if (escaped) {
|
|
122
|
+
current += char;
|
|
123
|
+
escaped = false;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (char === "\\" && quote !== "'") {
|
|
127
|
+
escaped = true;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (quote) {
|
|
131
|
+
if (char === quote) quote = undefined;
|
|
132
|
+
else current += char;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (char === "'" || char === '"') {
|
|
136
|
+
quote = char;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (/\s/u.test(char)) {
|
|
140
|
+
push();
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (char === ">") {
|
|
144
|
+
push();
|
|
145
|
+
tokens.push(">");
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
current += char;
|
|
149
|
+
}
|
|
150
|
+
push();
|
|
151
|
+
return tokens;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function literalPath(value: string | undefined) {
|
|
155
|
+
if (!value || value === "--" || DYNAMIC_PATH.test(value)) return undefined;
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function standaloneShellTokens(command: string) {
|
|
160
|
+
const source = command.replace(/^[ \t]+|[ \t]+$/gu, "");
|
|
161
|
+
const tokens: string[] = [];
|
|
162
|
+
let current = "";
|
|
163
|
+
let quote: "'" | '"' | undefined;
|
|
164
|
+
let escaped = false;
|
|
165
|
+
let tokenStarted = false;
|
|
166
|
+
const push = () => {
|
|
167
|
+
if (!tokenStarted) return;
|
|
168
|
+
tokens.push(current);
|
|
169
|
+
current = "";
|
|
170
|
+
tokenStarted = false;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
for (const character of source) {
|
|
174
|
+
if (quote === "'") {
|
|
175
|
+
if (character === "'") quote = undefined;
|
|
176
|
+
else current += character;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (quote === '"') {
|
|
180
|
+
if (escaped) {
|
|
181
|
+
if (character !== "\n") {
|
|
182
|
+
current += '$`"\\'.includes(character) ? character : `\\${character}`;
|
|
183
|
+
}
|
|
184
|
+
escaped = false;
|
|
185
|
+
} else if (character === '"') {
|
|
186
|
+
quote = undefined;
|
|
187
|
+
} else if (character === "\\") {
|
|
188
|
+
escaped = true;
|
|
189
|
+
} else if (character === "$" || character === "`") {
|
|
190
|
+
return undefined;
|
|
191
|
+
} else {
|
|
192
|
+
current += character;
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (escaped) {
|
|
197
|
+
if (character === "\n" || character === "\r") return undefined;
|
|
198
|
+
current += character;
|
|
199
|
+
tokenStarted = true;
|
|
200
|
+
escaped = false;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (character === "\\") {
|
|
204
|
+
escaped = true;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (character === "'" || character === '"') {
|
|
208
|
+
quote = character;
|
|
209
|
+
tokenStarted = true;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (character === "\n" || character === "\r") return undefined;
|
|
213
|
+
if (character === " " || character === "\t") {
|
|
214
|
+
push();
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (STANDALONE_CONTROL_CHARACTERS.includes(character)) return undefined;
|
|
218
|
+
current += character;
|
|
219
|
+
tokenStarted = true;
|
|
220
|
+
}
|
|
221
|
+
if (quote || escaped) return undefined;
|
|
222
|
+
push();
|
|
223
|
+
return tokens;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function directRmTargets(command: string) {
|
|
227
|
+
const tokens = standaloneShellTokens(command);
|
|
228
|
+
if (!tokens || tokens[0] !== "rm") return undefined;
|
|
229
|
+
|
|
230
|
+
const targets: string[] = [];
|
|
231
|
+
let afterOptions = false;
|
|
232
|
+
for (const token of tokens.slice(1)) {
|
|
233
|
+
if (!afterOptions && token === "--") {
|
|
234
|
+
afterOptions = true;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (!afterOptions && token.startsWith("-")) continue;
|
|
238
|
+
if (!token || token.startsWith("~") || path.isAbsolute(token)) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
targets.push(token);
|
|
242
|
+
}
|
|
243
|
+
return targets.length > 0 ? targets : undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function inspectCreations(command: string) {
|
|
247
|
+
const creations = new Set<string>();
|
|
248
|
+
for (const segment of splitShellSegments(command)) {
|
|
249
|
+
const tokens = shellTokens(segment);
|
|
250
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
251
|
+
if (tokens[index] !== ">") continue;
|
|
252
|
+
const target = literalPath(tokens[index + 1]);
|
|
253
|
+
if (target) creations.add(target);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (tokens[0]?.split("/").at(-1) !== "mkdir") continue;
|
|
257
|
+
const targets: string[] = [];
|
|
258
|
+
let afterOptions = false;
|
|
259
|
+
let supported = true;
|
|
260
|
+
for (const token of tokens.slice(1)) {
|
|
261
|
+
if (!afterOptions && token === "--") {
|
|
262
|
+
afterOptions = true;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (!afterOptions && (token === "-p" || token === "--parents")) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (!afterOptions && token.startsWith("-")) {
|
|
269
|
+
supported = false;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
const target = literalPath(token);
|
|
273
|
+
if (!target) {
|
|
274
|
+
supported = false;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
targets.push(target);
|
|
278
|
+
}
|
|
279
|
+
if (supported) {
|
|
280
|
+
for (const target of targets) creations.add(target);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return [...creations];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function containsRmReference(command: string) {
|
|
287
|
+
let decoded = "";
|
|
288
|
+
let quote: "'" | '"' | undefined;
|
|
289
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
290
|
+
const character = command[index] ?? "";
|
|
291
|
+
const next = command[index + 1] ?? "";
|
|
292
|
+
if (!quote && character === "$" && (next === "'" || next === '"')) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (character === "\\" && quote !== "'") {
|
|
296
|
+
if (next !== "\n") {
|
|
297
|
+
decoded +=
|
|
298
|
+
quote === '"' && !'$`"\\'.includes(next) ? `\\${next}` : next;
|
|
299
|
+
}
|
|
300
|
+
index += 1;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (character === "'" || character === '"') {
|
|
304
|
+
if (!quote) quote = character;
|
|
305
|
+
else if (quote === character) quote = undefined;
|
|
306
|
+
else decoded += character;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
decoded += character;
|
|
310
|
+
}
|
|
311
|
+
return /(^|[^A-Za-z0-9_.-])(?:\/[A-Za-z0-9_.-]+)*\/?rm(?=$|[^A-Za-z0-9_.-])/u.test(
|
|
312
|
+
decoded,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function stripShellComment(command: string) {
|
|
317
|
+
let quote: "'" | '"' | undefined;
|
|
318
|
+
let escaped = false;
|
|
319
|
+
let comment = false;
|
|
320
|
+
let source = "";
|
|
321
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
322
|
+
const character = command[index] ?? "";
|
|
323
|
+
if (comment) {
|
|
324
|
+
if (character === "\n") {
|
|
325
|
+
comment = false;
|
|
326
|
+
source += character;
|
|
327
|
+
}
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (escaped) {
|
|
331
|
+
escaped = false;
|
|
332
|
+
source += character;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (character === "\\" && quote !== "'") {
|
|
336
|
+
escaped = true;
|
|
337
|
+
source += character;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (character === "'" || character === '"') {
|
|
341
|
+
if (!quote) quote = character;
|
|
342
|
+
else if (quote === character) quote = undefined;
|
|
343
|
+
source += character;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (
|
|
347
|
+
!quote &&
|
|
348
|
+
character === "#" &&
|
|
349
|
+
(index === 0 || /[\s;&|()]/u.test(command[index - 1] ?? ""))
|
|
350
|
+
) {
|
|
351
|
+
comment = true;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
source += character;
|
|
355
|
+
}
|
|
356
|
+
return source.trimEnd();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function heredocContainsExecutableRm(command: string) {
|
|
360
|
+
const lines = command.split("\n");
|
|
361
|
+
if (lines.length < 3) return undefined;
|
|
362
|
+
const header = lines[0] ?? "";
|
|
363
|
+
const match = /<<(-?)[ \t]*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\2[ \t]*$/u.exec(
|
|
364
|
+
header,
|
|
365
|
+
);
|
|
366
|
+
if (!match) return undefined;
|
|
367
|
+
const stripTabs = match[1] === "-";
|
|
368
|
+
const delimiterQuoted = Boolean(match[2]);
|
|
369
|
+
const delimiter = match[3] ?? "";
|
|
370
|
+
const closing = lines.findIndex((line, index) => {
|
|
371
|
+
if (index === 0) return false;
|
|
372
|
+
return (stripTabs ? line.replace(/^\t+/u, "") : line) === delimiter;
|
|
373
|
+
});
|
|
374
|
+
if (closing < 0 || lines.slice(closing + 1).some((line) => line.trim())) {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
if (containsRmReference(stripShellComment(header))) return true;
|
|
378
|
+
if (delimiterQuoted) return false;
|
|
379
|
+
const body = lines.slice(1, closing).join("\n");
|
|
380
|
+
return /\$\(|`/u.test(body) && containsRmReference(body);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function containsExecutableRmReference(command: string) {
|
|
384
|
+
const heredoc = heredocContainsExecutableRm(command);
|
|
385
|
+
if (heredoc !== undefined) return heredoc;
|
|
386
|
+
|
|
387
|
+
const source = stripShellComment(command);
|
|
388
|
+
if (!containsRmReference(source)) return false;
|
|
389
|
+
const tokens = standaloneShellTokens(source);
|
|
390
|
+
if (!tokens) return true;
|
|
391
|
+
|
|
392
|
+
const executableIndex = tokens.findIndex(
|
|
393
|
+
(token) => !/^[A-Za-z_][A-Za-z0-9_]*=/u.test(token),
|
|
394
|
+
);
|
|
395
|
+
const executable = tokens[executableIndex]?.split("/").at(-1) ?? "";
|
|
396
|
+
if (
|
|
397
|
+
executable === "command" &&
|
|
398
|
+
(tokens[executableIndex + 1] === "-v" ||
|
|
399
|
+
tokens[executableIndex + 1] === "-V")
|
|
400
|
+
) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
return COMMAND_FORWARDERS.has(executable) || executable === "rm";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function inspectShell(command: string): ShellInspection {
|
|
407
|
+
const removals = directRmTargets(command);
|
|
408
|
+
if (removals) {
|
|
409
|
+
return {
|
|
410
|
+
creations: [],
|
|
411
|
+
removals,
|
|
412
|
+
opaqueDestructiveCommand: false,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (!containsExecutableRmReference(command)) {
|
|
416
|
+
return {
|
|
417
|
+
creations: inspectCreations(command),
|
|
418
|
+
removals: [],
|
|
419
|
+
opaqueDestructiveCommand: false,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
creations: [],
|
|
424
|
+
removals: [],
|
|
425
|
+
opaqueDestructiveCommand: true,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function containedPath(cwd: string, candidate: string) {
|
|
430
|
+
const absolute = path.resolve(cwd, candidate);
|
|
431
|
+
const relative = path.relative(cwd, absolute);
|
|
432
|
+
if (
|
|
433
|
+
relative === "" ||
|
|
434
|
+
relative === ".." ||
|
|
435
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
436
|
+
path.isAbsolute(relative)
|
|
437
|
+
) {
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
return { absolute, relative: relative.split(path.sep).join("/") };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function exists(candidate: string) {
|
|
444
|
+
try {
|
|
445
|
+
await lstat(candidate);
|
|
446
|
+
return true;
|
|
447
|
+
} catch (error) {
|
|
448
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export function createWorkspaceCleanupGuard() {
|
|
456
|
+
const origins = new Map<string, Origin>();
|
|
457
|
+
const pending = new Map<string, PendingEffects>();
|
|
458
|
+
|
|
459
|
+
const prepareCreation = async (
|
|
460
|
+
cwd: string,
|
|
461
|
+
candidate: string,
|
|
462
|
+
observeOnCommandError: boolean,
|
|
463
|
+
) => {
|
|
464
|
+
const contained = containedPath(cwd, candidate);
|
|
465
|
+
if (!contained) return undefined;
|
|
466
|
+
const existed = await exists(contained.absolute);
|
|
467
|
+
if (existed && !origins.has(contained.absolute)) {
|
|
468
|
+
origins.set(contained.absolute, "baseline");
|
|
469
|
+
}
|
|
470
|
+
return { path: contained.absolute, existed, observeOnCommandError };
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const unverifiedCleanup = () => ({
|
|
474
|
+
kind: "block" as const,
|
|
475
|
+
protectedPaths: [],
|
|
476
|
+
reason:
|
|
477
|
+
"Blocked cleanup: OpenPI recognized a source-visible deletion outside its supported direct rm command grammar. Use a direct rm command with literal workspace-relative paths so OpenPI can determine whether each target is session-created scratch or a pre-existing path requiring confirmation.",
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
async beforeWrite(attempt: WriteAttempt) {
|
|
482
|
+
const creation = await prepareCreation(attempt.cwd, attempt.path, false);
|
|
483
|
+
pending.set(attempt.id, {
|
|
484
|
+
creations: creation ? [creation] : [],
|
|
485
|
+
removals: [],
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
async before(attempt: BashAttempt) {
|
|
490
|
+
const inspected = inspectShell(attempt.command);
|
|
491
|
+
if (inspected.opaqueDestructiveCommand) return unverifiedCleanup();
|
|
492
|
+
|
|
493
|
+
const containedRemovals = inspected.removals.map((candidate) =>
|
|
494
|
+
containedPath(attempt.cwd, candidate),
|
|
495
|
+
);
|
|
496
|
+
if (containedRemovals.some((candidate) => !candidate)) {
|
|
497
|
+
return unverifiedCleanup();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const creations: PendingEffects["creations"] = [];
|
|
501
|
+
for (const candidate of inspected.creations) {
|
|
502
|
+
const creation = await prepareCreation(attempt.cwd, candidate, true);
|
|
503
|
+
if (creation) creations.push(creation);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const removals: string[] = [];
|
|
507
|
+
const protectedPaths: string[] = [];
|
|
508
|
+
for (const contained of containedRemovals) {
|
|
509
|
+
if (!contained) continue;
|
|
510
|
+
const origin = origins.get(contained.absolute);
|
|
511
|
+
const present = await exists(contained.absolute);
|
|
512
|
+
if (present && !origin) origins.set(contained.absolute, "baseline");
|
|
513
|
+
if (present && origins.get(contained.absolute) !== "session_created") {
|
|
514
|
+
protectedPaths.push(contained.relative);
|
|
515
|
+
}
|
|
516
|
+
removals.push(contained.absolute);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (
|
|
520
|
+
protectedPaths.length > 0 &&
|
|
521
|
+
!(await attempt.confirmDelete(protectedPaths))
|
|
522
|
+
) {
|
|
523
|
+
return {
|
|
524
|
+
kind: "block" as const,
|
|
525
|
+
protectedPaths,
|
|
526
|
+
reason: `Blocked cleanup: ${protectedPaths.join(", ")} existed before this agent changed it and is not proven session-created scratch. Retry the cleanup without that path, or obtain explicit user confirmation to delete it.`,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
pending.set(attempt.id, { creations, removals });
|
|
531
|
+
return { kind: "allow" as const };
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
async after(result: { id: string; isError: boolean }) {
|
|
535
|
+
const effects = pending.get(result.id);
|
|
536
|
+
pending.delete(result.id);
|
|
537
|
+
if (!effects) return;
|
|
538
|
+
for (const creation of effects.creations) {
|
|
539
|
+
if (
|
|
540
|
+
!creation.existed &&
|
|
541
|
+
(!result.isError || creation.observeOnCommandError) &&
|
|
542
|
+
!origins.has(creation.path) &&
|
|
543
|
+
(await exists(creation.path))
|
|
544
|
+
) {
|
|
545
|
+
origins.set(creation.path, "session_created");
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
for (const removed of effects.removals) {
|
|
549
|
+
if (
|
|
550
|
+
origins.get(removed) === "session_created" &&
|
|
551
|
+
!(await exists(removed))
|
|
552
|
+
) {
|
|
553
|
+
origins.delete(removed);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
|
|
558
|
+
reset() {
|
|
559
|
+
origins.clear();
|
|
560
|
+
pending.clear();
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tt-a1i/openpi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "OpenPI — a Pi-native multi-agent workbench with background execution, isolated subagents, replay-safe workflows, goals, tasks, and observable TUI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "tt-a1i",
|
|
7
|
+
"bin": {
|
|
8
|
+
"openpi": "./bin/openpi.js"
|
|
9
|
+
},
|
|
7
10
|
"keywords": [
|
|
8
11
|
"pi-package",
|
|
9
12
|
"pi",
|
|
@@ -14,18 +17,22 @@
|
|
|
14
17
|
],
|
|
15
18
|
"repository": {
|
|
16
19
|
"type": "git",
|
|
17
|
-
"url": "git+https://github.com/
|
|
20
|
+
"url": "git+https://github.com/openpi-dev/openpi.git"
|
|
18
21
|
},
|
|
19
|
-
"homepage": "https://github.com/
|
|
22
|
+
"homepage": "https://github.com/openpi-dev/openpi#readme",
|
|
20
23
|
"bugs": {
|
|
21
|
-
"url": "https://github.com/
|
|
24
|
+
"url": "https://github.com/openpi-dev/openpi/issues"
|
|
22
25
|
},
|
|
23
26
|
"files": [
|
|
27
|
+
"bin",
|
|
28
|
+
"web",
|
|
24
29
|
"extensions",
|
|
25
30
|
"!extensions/**/*.test.ts",
|
|
26
31
|
"!extensions/**/*.spec.ts",
|
|
27
32
|
"!extensions/**/tsconfig.json",
|
|
33
|
+
"!extensions/**/test-support/**",
|
|
28
34
|
"!extensions/*/docs",
|
|
35
|
+
"!tests/**",
|
|
29
36
|
"skills",
|
|
30
37
|
"themes",
|
|
31
38
|
"assets",
|
|
@@ -44,12 +51,15 @@
|
|
|
44
51
|
"themes": [
|
|
45
52
|
"./themes"
|
|
46
53
|
],
|
|
47
|
-
"image": "https://raw.githubusercontent.com/
|
|
54
|
+
"image": "https://raw.githubusercontent.com/openpi-dev/openpi/main/assets/openpi-package.png"
|
|
48
55
|
},
|
|
49
56
|
"dependencies": {
|
|
50
57
|
"@effect/platform-node": "^4.0.0-beta.99",
|
|
51
58
|
"acorn": "^8.17.0",
|
|
52
|
-
"effect": "^4.0.0-beta.99"
|
|
59
|
+
"effect": "^4.0.0-beta.99",
|
|
60
|
+
"jiti": "2.7.0",
|
|
61
|
+
"marked": "^18.0.5",
|
|
62
|
+
"undici": "8.9.0"
|
|
53
63
|
},
|
|
54
64
|
"devDependencies": {
|
|
55
65
|
"@biomejs/biome": "2.5.8",
|
|
@@ -61,6 +71,7 @@
|
|
|
61
71
|
"@types/node": "^26.1.1",
|
|
62
72
|
"typebox": "^1.3.6",
|
|
63
73
|
"typescript": "^7.0.2",
|
|
74
|
+
"vite": "^8.2.0",
|
|
64
75
|
"vitest": "4.1.10"
|
|
65
76
|
},
|
|
66
77
|
"peerDependencies": {
|
|
@@ -75,13 +86,22 @@
|
|
|
75
86
|
},
|
|
76
87
|
"scripts": {
|
|
77
88
|
"prepublishOnly": "bun run check && bun run test",
|
|
78
|
-
"check": "bun run format:check && bun run lint && bun run typecheck",
|
|
89
|
+
"check": "bun run check:config-contract && bun run check:discipline && bun run check:web && bun run format:check && bun run lint && bun run typecheck",
|
|
90
|
+
"check:config-contract": "node scripts/check-config-contract.mjs",
|
|
91
|
+
"check:discipline": "node scripts/check-discipline-ledger.mjs",
|
|
79
92
|
"prepare": "node scripts/prepare-effect-tsgo.mjs",
|
|
80
93
|
"format": "biome format --write .",
|
|
81
94
|
"format:check": "biome format .",
|
|
82
95
|
"lint": "biome lint . --error-on-warnings",
|
|
83
96
|
"typecheck": "tsc --noEmit",
|
|
84
|
-
"
|
|
97
|
+
"benchmark:workflow-journal": "node --experimental-strip-types --expose-gc benchmarks/workflow-journal.ts",
|
|
98
|
+
"dev:web": "node --experimental-strip-types scripts/dev-web.mjs",
|
|
99
|
+
"dev:web:ui": "vite --config web/vite.config.mjs --host 127.0.0.1",
|
|
100
|
+
"dev:web:backend": "node scripts/dev-web-backend.mjs",
|
|
101
|
+
"check:web": "node --check scripts/dev-web.mjs && node --check web/vite.config.mjs && node --check web/ui/app.js",
|
|
102
|
+
"benchmark:workflow-child-startup": "node --experimental-strip-types scripts/benchmark-workflow-child-startup.mjs",
|
|
103
|
+
"test": "node scripts/run-tests.mjs",
|
|
104
|
+
"provenance": "node scripts/provenance.mjs"
|
|
85
105
|
},
|
|
86
106
|
"packageManager": "bun@1.3.14"
|
|
87
107
|
}
|