@ycheung5/pi-computer-use 0.0.0-stage → 0.1.1
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/GLOSSARY.md +38 -0
- package/LICENSE +21 -0
- package/README.md +165 -2
- package/assets/accessibility.cs +208 -0
- package/assets/browser-indicators.cs +37 -0
- package/assets/capture.cs +215 -0
- package/assets/clipboard.cs +197 -0
- package/assets/controls.cs +249 -0
- package/assets/indicator-preview.png +0 -0
- package/assets/indicators.cs +252 -0
- package/assets/input-timing.cs +42 -0
- package/assets/node-LICENSE.txt +2732 -0
- package/assets/ocr.cs +86 -0
- package/assets/ownership.cs +59 -0
- package/assets/pointer.cs +241 -0
- package/assets/puppeteer-LICENSE.txt +202 -0
- package/assets/readme-banner.svg +33 -0
- package/assets/stop.cs +197 -0
- package/assets/text.cs +279 -0
- package/assets/wgc.cs +167 -0
- package/assets/window-management.cs +217 -0
- package/assets/window-marker.cs +23 -0
- package/assets/window-references.cs +80 -0
- package/assets/windows.cs +127 -0
- package/dist/accessibility.d.ts +29 -0
- package/dist/accessibility.js +258 -0
- package/dist/browser-actions.d.ts +22 -0
- package/dist/browser-actions.js +246 -0
- package/dist/browser-indicators.d.ts +23 -0
- package/dist/browser-indicators.js +69 -0
- package/dist/browser-profiles.d.ts +10 -0
- package/dist/browser-profiles.js +174 -0
- package/dist/browser.d.ts +11 -0
- package/dist/browser.js +2071 -0
- package/dist/capture.d.ts +74 -0
- package/dist/capture.js +199 -0
- package/dist/client.d.ts +39 -0
- package/dist/client.js +384 -0
- package/dist/desktop.d.ts +20 -0
- package/dist/desktop.js +42 -0
- package/dist/extension.d.ts +4 -0
- package/dist/extension.js +409 -0
- package/dist/input-timing.d.ts +24 -0
- package/dist/input-timing.js +70 -0
- package/dist/launcher.d.ts +6 -0
- package/dist/launcher.js +85 -0
- package/dist/native.d.ts +11 -0
- package/dist/native.js +146 -0
- package/dist/ocr.d.ts +52 -0
- package/dist/ocr.js +246 -0
- package/dist/ownership.d.ts +4 -0
- package/dist/ownership.js +24 -0
- package/dist/pointer.d.ts +128 -0
- package/dist/pointer.js +198 -0
- package/dist/protocol.d.ts +15 -0
- package/dist/protocol.js +1 -0
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.js +72 -0
- package/dist/settings.d.ts +85 -0
- package/dist/settings.js +449 -0
- package/dist/stop.d.ts +24 -0
- package/dist/stop.js +156 -0
- package/dist/text.d.ts +120 -0
- package/dist/text.js +212 -0
- package/dist/waits.d.ts +19 -0
- package/dist/waits.js +262 -0
- package/dist/window-management.d.ts +25 -0
- package/dist/window-management.js +167 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +553 -0
- package/docs/README.md +55 -0
- package/docs/adr/0001-preserve-reference-behavior.md +9 -0
- package/docs/adr/0002-window-lifetime-references.md +11 -0
- package/docs/adr/0003-exclusive-desktop-control.md +11 -0
- package/docs/adr/0004-pi-first-acceptance.md +9 -0
- package/docs/adr/0005-default-apps-and-workspace-reuse.md +7 -0
- package/docs/agents/domain.md +9 -0
- package/docs/agents/issue-tracker.md +10 -0
- package/docs/agents/triage-labels.md +9 -0
- package/docs/browser.md +113 -0
- package/docs/implementation-plan.md +153 -0
- package/docs/indicators.md +53 -0
- package/docs/native-interop-research.md +17 -0
- package/docs/ownership.md +25 -0
- package/docs/pi-extension-research.md +57 -0
- package/docs/pointer.md +77 -0
- package/docs/runtime.md +42 -0
- package/docs/settings.md +33 -0
- package/docs/spec.md +230 -0
- package/docs/stopping.md +83 -0
- package/docs/text.md +99 -0
- package/docs/tickets.md +85 -0
- package/docs/validation.md +1420 -0
- package/docs/windows.md +171 -0
- package/package.json +134 -4
package/dist/worker.js
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { controlTools, executeAccessibility, observationTools, } from "./accessibility.js";
|
|
4
|
+
import { browse, browserListing, clearBrowserIndicators, configureBrowser, disposeBrowser, isBrowserRequest, } from "./browser.js";
|
|
5
|
+
import { browserActionRefusal, browserActionTools } from "./browser-actions.js";
|
|
6
|
+
import { screenshot, screenshotParameters } from "./capture.js";
|
|
7
|
+
import { disposeDesktop, listWindows, listWindowsParameters, retireDesktop, } from "./desktop.js";
|
|
8
|
+
import { runNativeOperation } from "./native.js";
|
|
9
|
+
import { disposeOcr, ocrDesktop, ocrParameters } from "./ocr.js";
|
|
10
|
+
import { acquireDesktop, disposeOwnership, releaseDesktop, } from "./ownership.js";
|
|
11
|
+
import { awaitPointerCleanup, executePointer, pointerTools, validatePointer, } from "./pointer.js";
|
|
12
|
+
import { PersonalSettingsManager } from "./settings.js";
|
|
13
|
+
import { StopListener } from "./stop.js";
|
|
14
|
+
import { executeText, prepareTextInput, textTools, validateText, } from "./text.js";
|
|
15
|
+
import { waitDesktop, waitTools } from "./waits.js";
|
|
16
|
+
import { manageWindow, windowTools } from "./window-management.js";
|
|
17
|
+
const settings = new PersonalSettingsManager(new Map([
|
|
18
|
+
[
|
|
19
|
+
"health",
|
|
20
|
+
{
|
|
21
|
+
description: "Check automation worker health",
|
|
22
|
+
parameters: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {},
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
"list_windows",
|
|
31
|
+
{
|
|
32
|
+
description: "Discover native windows and browser tabs",
|
|
33
|
+
parameters: listWindowsParameters,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
"screenshot",
|
|
38
|
+
{
|
|
39
|
+
description: "Capture desktop, native window or browser page",
|
|
40
|
+
parameters: screenshotParameters,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
"ocr",
|
|
45
|
+
{
|
|
46
|
+
description: "Read literal screen text and coordinates",
|
|
47
|
+
parameters: ocrParameters,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
...[
|
|
51
|
+
...observationTools,
|
|
52
|
+
...controlTools,
|
|
53
|
+
...pointerTools,
|
|
54
|
+
...textTools,
|
|
55
|
+
...waitTools,
|
|
56
|
+
...windowTools,
|
|
57
|
+
...browserActionTools,
|
|
58
|
+
].map((tool) => [
|
|
59
|
+
tool.action,
|
|
60
|
+
{
|
|
61
|
+
description: `Windows computer use ${tool.action}`,
|
|
62
|
+
parameters: tool.parameters,
|
|
63
|
+
},
|
|
64
|
+
]),
|
|
65
|
+
]));
|
|
66
|
+
const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
67
|
+
let queue = Promise.resolve();
|
|
68
|
+
const requests = new Map();
|
|
69
|
+
const blocked = new Set();
|
|
70
|
+
const lastTargets = new Map();
|
|
71
|
+
function rememberTarget(operationId, result) {
|
|
72
|
+
const activated = result.action === "activate_window" && result.activated === true;
|
|
73
|
+
const handle = activated
|
|
74
|
+
? result.nativeWindowHandle
|
|
75
|
+
: result.inputTargetHandle;
|
|
76
|
+
const owner = activated ? result.processId : result.inputTargetProcessId;
|
|
77
|
+
if (result.ok && Number.isSafeInteger(handle) && Number.isSafeInteger(owner))
|
|
78
|
+
lastTargets.set(operationId, {
|
|
79
|
+
nativeWindowHandle: Number(handle),
|
|
80
|
+
processId: Number(owner),
|
|
81
|
+
...(typeof result.windowRef === "string"
|
|
82
|
+
? { windowRef: result.windowRef }
|
|
83
|
+
: {}),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
let stopping = Promise.resolve();
|
|
87
|
+
const stopListener = new StopListener((operationId) => {
|
|
88
|
+
void stopOperation(operationId).catch((error) => process.stderr.write(`${String(error)}\n`));
|
|
89
|
+
});
|
|
90
|
+
let presentationUpdating = false;
|
|
91
|
+
let presentationError;
|
|
92
|
+
const presentationPoll = setInterval(() => {
|
|
93
|
+
if (!stopListener.hasStarted || presentationUpdating)
|
|
94
|
+
return;
|
|
95
|
+
presentationUpdating = true;
|
|
96
|
+
void settings
|
|
97
|
+
.synchronize()
|
|
98
|
+
.then(async ({ settings: current }) => {
|
|
99
|
+
await stopListener.configure(current);
|
|
100
|
+
presentationError = undefined;
|
|
101
|
+
})
|
|
102
|
+
.catch((error) => {
|
|
103
|
+
const operation = stopListener.currentOperation;
|
|
104
|
+
if (operation)
|
|
105
|
+
void stopOperation(operation).catch(() => undefined);
|
|
106
|
+
const message = String(error);
|
|
107
|
+
if (message !== presentationError)
|
|
108
|
+
process.stderr.write(`PRESENTATION_SETTINGS_FAILED: ${message}\n`);
|
|
109
|
+
presentationError = message;
|
|
110
|
+
})
|
|
111
|
+
.finally(() => {
|
|
112
|
+
presentationUpdating = false;
|
|
113
|
+
});
|
|
114
|
+
}, 250);
|
|
115
|
+
presentationPoll.unref();
|
|
116
|
+
async function stopOperation(operationId) {
|
|
117
|
+
if (blocked.has(operationId))
|
|
118
|
+
return stopping;
|
|
119
|
+
blocked.add(operationId);
|
|
120
|
+
process.stdout.write(`${JSON.stringify({ event: "stopped", operationId })}\n`);
|
|
121
|
+
stopping = stopping.then(async () => {
|
|
122
|
+
try {
|
|
123
|
+
await stopListener.signal(operationId);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
for (const request of requests.values())
|
|
127
|
+
if (request.operationId === operationId &&
|
|
128
|
+
request.action !== "operation_end")
|
|
129
|
+
request.controller.abort(new Error("COMPUTER_USE_STOPPED"));
|
|
130
|
+
await awaitPointerCleanup();
|
|
131
|
+
await clearBrowserIndicators();
|
|
132
|
+
await retireDesktop();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return stopping;
|
|
136
|
+
}
|
|
137
|
+
function isObject(value) {
|
|
138
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
139
|
+
}
|
|
140
|
+
const inputPreparations = new Map();
|
|
141
|
+
async function acknowledgeInputPreparation(requestId, operationId, signal) {
|
|
142
|
+
signal.throwIfAborted();
|
|
143
|
+
const token = randomUUID();
|
|
144
|
+
await new Promise((resolve, reject) => {
|
|
145
|
+
const cleanup = () => {
|
|
146
|
+
inputPreparations.delete(requestId);
|
|
147
|
+
signal.removeEventListener("abort", abort);
|
|
148
|
+
input.removeListener("close", abort);
|
|
149
|
+
};
|
|
150
|
+
const abort = () => {
|
|
151
|
+
cleanup();
|
|
152
|
+
process.stdout.write(`${JSON.stringify({ event: "input_state", active: false })}\n`);
|
|
153
|
+
reject(signal.reason ?? new Error("COMPUTER_USE_STOPPED"));
|
|
154
|
+
};
|
|
155
|
+
inputPreparations.set(requestId, {
|
|
156
|
+
token,
|
|
157
|
+
accept: () => {
|
|
158
|
+
cleanup();
|
|
159
|
+
resolve();
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
163
|
+
input.once("close", abort);
|
|
164
|
+
process.stdout.write(`${JSON.stringify({ event: "input_prepare", requestId, operationId, token })}\n`);
|
|
165
|
+
if (signal.aborted)
|
|
166
|
+
abort();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function guardedInput(action, args, operationId, signal, requestId, quietScroll = false) {
|
|
170
|
+
if (quietScroll)
|
|
171
|
+
return executeAccessibility(action, args, signal, (normalized) => guardedInput(action, { ...normalized, _quietScroll: true }, operationId, signal, requestId));
|
|
172
|
+
const armed = await stopListener.arm(operationId, signal);
|
|
173
|
+
if (!armed.ok || typeof armed.cancelEvent !== "string")
|
|
174
|
+
return { ...armed, action };
|
|
175
|
+
const prepared = ["type_text", "keypress"].includes(action)
|
|
176
|
+
? await prepareTextInput(action, args, signal)
|
|
177
|
+
: args;
|
|
178
|
+
await acknowledgeInputPreparation(requestId, operationId, signal);
|
|
179
|
+
try {
|
|
180
|
+
signal.throwIfAborted();
|
|
181
|
+
if (prepared._quietScroll !== true) {
|
|
182
|
+
const control = await stopListener.acquire(operationId, signal);
|
|
183
|
+
if (!control.ok) {
|
|
184
|
+
process.stdout.write(`${JSON.stringify({ event: "input_state", active: false })}\n`);
|
|
185
|
+
return { ...control, action, cleanupConfirmed: true };
|
|
186
|
+
}
|
|
187
|
+
signal.throwIfAborted();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
process.stdout.write(`${JSON.stringify({ event: "input_state", active: false })}\n`);
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
let result;
|
|
195
|
+
try {
|
|
196
|
+
result = await executePointer(action, prepared, armed.cancelEvent, operationId, Number(armed.listenerProcessId), signal);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
void stopOperation(operationId).catch(() => undefined);
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
// Failure leaves the input lease pending; only the native cleanup ACK permits
|
|
203
|
+
// this notification, worker retirement and eventual desktop ownership release.
|
|
204
|
+
process.stdout.write(`${JSON.stringify({ event: "input_state", active: false })}\n`);
|
|
205
|
+
if (!result.ok &&
|
|
206
|
+
/^(COMPUTER_USE_STOPPED|EXCLUSIVE_CONTROL_LOST|STOP_LISTENER_UNAVAILABLE)/.test(String(result.error)))
|
|
207
|
+
await stopOperation(operationId);
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
async function dispatch(request, signal) {
|
|
211
|
+
signal.throwIfAborted();
|
|
212
|
+
const operationId = request.operationId ?? `worker-${process.pid}`;
|
|
213
|
+
if (request.action === "operation_end") {
|
|
214
|
+
await stopping;
|
|
215
|
+
await awaitPointerCleanup();
|
|
216
|
+
await stopListener.disarm(operationId);
|
|
217
|
+
await clearBrowserIndicators();
|
|
218
|
+
const result = await releaseDesktop(operationId);
|
|
219
|
+
if (result.ok)
|
|
220
|
+
lastTargets.delete(operationId);
|
|
221
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result })}\n`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (request.action === "indicator_preview") {
|
|
225
|
+
if (Object.keys(request.args).some((key) => key !== "show") ||
|
|
226
|
+
(request.args.show !== undefined &&
|
|
227
|
+
typeof request.args.show !== "boolean")) {
|
|
228
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result: { ok: false, action: request.action, error: "INVALID_ARGUMENTS: preview accepts boolean show only" } })}\n`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const current = await settings.synchronize();
|
|
232
|
+
const result = await stopListener.preview(current.settings, request.args.show !== false);
|
|
233
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result: { ...result, action: request.action } })}\n`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (["settings_get", "settings_update", "tools"].includes(request.action)) {
|
|
237
|
+
await stopListener.configure((await settings.synchronize()).settings);
|
|
238
|
+
const result = await settings.handle(request.action, request.args);
|
|
239
|
+
if (result.ok && settings.currentSettings)
|
|
240
|
+
await stopListener.configure(settings.currentSettings);
|
|
241
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result })}\n`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (request.args.windowRef !== undefined &&
|
|
245
|
+
(typeof request.args.windowRef !== "string" ||
|
|
246
|
+
!request.args.windowRef.trim() ||
|
|
247
|
+
isBrowserRequest(request.args) ||
|
|
248
|
+
request.args.scope === "desktop")) {
|
|
249
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result: { ok: false, action: request.action, error: "INVALID_ARGUMENTS: windowRef requires a nonempty native-window reference and cannot target desktop/browser channel" } })}\n`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const browserRefusal = isBrowserRequest(request.args)
|
|
253
|
+
? browserActionRefusal(request.action, request.args)
|
|
254
|
+
: undefined;
|
|
255
|
+
if (browserRefusal) {
|
|
256
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result: { ok: false, action: request.action, error: browserRefusal } })}\n`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const denial = await settings.authorize(request.action);
|
|
260
|
+
if (denial) {
|
|
261
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result: { ok: false, action: request.action, error: denial } })}\n`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const configuration = settings.currentSettings;
|
|
265
|
+
if (configuration)
|
|
266
|
+
await stopListener.configure(configuration);
|
|
267
|
+
if (configuration &&
|
|
268
|
+
process.platform === "win32" &&
|
|
269
|
+
settings.currentFileExists)
|
|
270
|
+
await configureBrowser({
|
|
271
|
+
browserApp: configuration.browserApp,
|
|
272
|
+
browserVisible: configuration.browserVisible,
|
|
273
|
+
browserSignIn: configuration.browserSignIn,
|
|
274
|
+
});
|
|
275
|
+
const suspendCapture = !isBrowserRequest(request.args) &&
|
|
276
|
+
["screenshot", "snapshot", "ocr", "find", "wait_for"].includes(request.action);
|
|
277
|
+
if (suspendCapture)
|
|
278
|
+
await stopListener.suspendCapture(true);
|
|
279
|
+
try {
|
|
280
|
+
if (request.action !== "health" && blocked.has(operationId)) {
|
|
281
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result: { ok: false, action: request.action, error: "COMPUTER_USE_STOPPED: wait for a new user operation" } })}\n`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (request.action !== "health" && process.platform === "win32") {
|
|
285
|
+
const ownership = await acquireDesktop(operationId, signal);
|
|
286
|
+
if (!ownership.ok) {
|
|
287
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result: { ...ownership, action: request.action } })}\n`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const previous = lastTargets.get(operationId);
|
|
292
|
+
if (previous &&
|
|
293
|
+
!isBrowserRequest(request.args) &&
|
|
294
|
+
request.args.scope !== "desktop" &&
|
|
295
|
+
![
|
|
296
|
+
"list_windows",
|
|
297
|
+
"health",
|
|
298
|
+
"operation_end",
|
|
299
|
+
"wait",
|
|
300
|
+
"launch_app",
|
|
301
|
+
"activate_window",
|
|
302
|
+
"move_window",
|
|
303
|
+
"close_window",
|
|
304
|
+
].includes(request.action) &&
|
|
305
|
+
![
|
|
306
|
+
"windowRef",
|
|
307
|
+
"windowTitle",
|
|
308
|
+
"processId",
|
|
309
|
+
"nativeWindowHandle",
|
|
310
|
+
"elementId",
|
|
311
|
+
].some((key) => request.args[key] !== undefined))
|
|
312
|
+
request.args = { ...request.args, ...previous };
|
|
313
|
+
if (request.action === "prepare_input") {
|
|
314
|
+
const result = await stopListener.arm(operationId, signal);
|
|
315
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result })}\n`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (["type_text", "keypress"].includes(request.action) &&
|
|
319
|
+
!isBrowserRequest(request.args)) {
|
|
320
|
+
const invalid = validateText(request.action, request.args);
|
|
321
|
+
if (invalid) {
|
|
322
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result: invalid })}\n`);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
let result;
|
|
326
|
+
if ((request.action === "type_text" &&
|
|
327
|
+
["auto", "background"].includes(String(request.args.method ?? "auto"))) ||
|
|
328
|
+
request.args.dispatch === "background") {
|
|
329
|
+
result = await executeText(request.action, request.args, "", operationId, undefined, signal);
|
|
330
|
+
if (result.inputRequired !== true) {
|
|
331
|
+
rememberTarget(operationId, result);
|
|
332
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result })}\n`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
result = await guardedInput(request.action, request.args, operationId, signal, request.id);
|
|
337
|
+
rememberTarget(operationId, result);
|
|
338
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result })}\n`);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (!isBrowserRequest(request.args) &&
|
|
342
|
+
(["click", "double_click", "move", "drag"].includes(request.action) ||
|
|
343
|
+
(request.action === "scroll" && !request.args.elementId))) {
|
|
344
|
+
const invalid = validatePointer(request.action, request.args);
|
|
345
|
+
if (invalid) {
|
|
346
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result: invalid })}\n`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
// Only the explicit needs-input result permits escalation/retry. No timeout,
|
|
350
|
+
// stale/disabled/covered-point error can trigger replay.
|
|
351
|
+
if ((request.args.dispatch === "background" ||
|
|
352
|
+
(request.action === "click" &&
|
|
353
|
+
request.args.holdMs === undefined &&
|
|
354
|
+
request.args.timeoutMs === undefined &&
|
|
355
|
+
request.args.elementId &&
|
|
356
|
+
(request.args.button ?? "left") === "left" &&
|
|
357
|
+
!request.args.activate &&
|
|
358
|
+
request.args.dispatch !== "foreground")) &&
|
|
359
|
+
["click", "double_click"].includes(request.action)) {
|
|
360
|
+
const quiet = await executePointer(request.action, request.args, "", operationId, undefined, signal);
|
|
361
|
+
if (!quiet.inputRequired || request.args.dispatch === "background") {
|
|
362
|
+
rememberTarget(operationId, quiet);
|
|
363
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result: quiet })}\n`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const result = await guardedInput(request.action, request.args, operationId, signal, request.id);
|
|
368
|
+
rememberTarget(operationId, result);
|
|
369
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result })}\n`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const response = {
|
|
373
|
+
id: request.id,
|
|
374
|
+
result: request.action === "health"
|
|
375
|
+
? {
|
|
376
|
+
ok: true,
|
|
377
|
+
action: "health",
|
|
378
|
+
processId: process.pid,
|
|
379
|
+
platform: process.platform,
|
|
380
|
+
nodeVersion: process.version,
|
|
381
|
+
control: await stopListener.status(),
|
|
382
|
+
settings: {
|
|
383
|
+
preset: settings.currentSettings?.toolPreset,
|
|
384
|
+
notices: settings.currentNotices,
|
|
385
|
+
source: settings.currentSource,
|
|
386
|
+
},
|
|
387
|
+
}
|
|
388
|
+
: isBrowserRequest(request.args)
|
|
389
|
+
? await browse(request.action, request.args, signal)
|
|
390
|
+
: windowTools.some((tool) => tool.action === request.action)
|
|
391
|
+
? await manageWindow(request.action, request.args, signal)
|
|
392
|
+
: ["wait", "wait_for"].includes(request.action)
|
|
393
|
+
? await waitDesktop(request.action, request.args, signal)
|
|
394
|
+
: request.action === "list_windows"
|
|
395
|
+
? {
|
|
396
|
+
...(await listWindows(request.args, signal)),
|
|
397
|
+
browserLane: await browserListing(),
|
|
398
|
+
}
|
|
399
|
+
: request.action === "ocr"
|
|
400
|
+
? await ocrDesktop(request.args, signal)
|
|
401
|
+
: request.action === "screenshot"
|
|
402
|
+
? await screenshot(request.args, signal)
|
|
403
|
+
: [
|
|
404
|
+
"snapshot",
|
|
405
|
+
"accessibility_tree",
|
|
406
|
+
"find",
|
|
407
|
+
"element_info",
|
|
408
|
+
"focus",
|
|
409
|
+
"invoke",
|
|
410
|
+
"set_value",
|
|
411
|
+
"scroll",
|
|
412
|
+
].includes(request.action)
|
|
413
|
+
? request.action === "scroll"
|
|
414
|
+
? await guardedInput(request.action, request.args, operationId, signal, request.id, true)
|
|
415
|
+
: await executeAccessibility(request.action, request.args, signal)
|
|
416
|
+
: {
|
|
417
|
+
ok: false,
|
|
418
|
+
action: request.action,
|
|
419
|
+
error: "UNKNOWN_ACTION",
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
if (!isBrowserRequest(request.args) &&
|
|
423
|
+
response.result.inputRequired === true &&
|
|
424
|
+
["invoke", "scroll"].includes(request.action)) {
|
|
425
|
+
const pointerAction = request.action === "invoke" ? "click" : "scroll";
|
|
426
|
+
const result = await guardedInput(pointerAction, { ...request.args, dispatch: "foreground" }, operationId, signal, request.id);
|
|
427
|
+
response.result = {
|
|
428
|
+
...result,
|
|
429
|
+
action: request.action,
|
|
430
|
+
method: result.ok ? `fallback:${String(result.method)}` : result.method,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (!isBrowserRequest(request.args) &&
|
|
434
|
+
request.action === "set_value" &&
|
|
435
|
+
response.result.inputRequired === true) {
|
|
436
|
+
const textArgs = {
|
|
437
|
+
text: request.args.value,
|
|
438
|
+
method: "clipboard",
|
|
439
|
+
focusMode: "preserve",
|
|
440
|
+
restoreClipboard: request.args.restoreClipboard ?? true,
|
|
441
|
+
_elementId: request.args.elementId,
|
|
442
|
+
_replaceAll: true,
|
|
443
|
+
};
|
|
444
|
+
for (const key of [
|
|
445
|
+
"windowRef",
|
|
446
|
+
"windowTitle",
|
|
447
|
+
"processId",
|
|
448
|
+
"nativeWindowHandle",
|
|
449
|
+
"activate",
|
|
450
|
+
])
|
|
451
|
+
if (request.args[key] !== undefined)
|
|
452
|
+
textArgs[key] = request.args[key];
|
|
453
|
+
const result = await guardedInput("type_text", textArgs, operationId, signal, request.id);
|
|
454
|
+
response.result = {
|
|
455
|
+
...result,
|
|
456
|
+
action: "set_value",
|
|
457
|
+
method: result.ok ? `fallback:${String(result.method)}` : result.method,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
rememberTarget(operationId, response.result);
|
|
461
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
462
|
+
}
|
|
463
|
+
finally {
|
|
464
|
+
if (suspendCapture)
|
|
465
|
+
await stopListener.suspendCapture(false);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
input.on("line", (line) => {
|
|
469
|
+
let parsed;
|
|
470
|
+
try {
|
|
471
|
+
parsed = JSON.parse(line);
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
process.stderr.write("WORKER_PROTOCOL_ERROR: invalid JSON\n");
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (isObject(parsed) &&
|
|
478
|
+
typeof parsed.inputAck === "string" &&
|
|
479
|
+
Number.isSafeInteger(parsed.requestId)) {
|
|
480
|
+
const preparation = inputPreparations.get(Number(parsed.requestId));
|
|
481
|
+
if (preparation && preparation.token === parsed.inputAck)
|
|
482
|
+
preparation.accept();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (isObject(parsed) && typeof parsed.stop === "string") {
|
|
486
|
+
void stopOperation(parsed.stop).catch((error) => process.stderr.write(`${String(error)}\n`));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (isObject(parsed) && "cancel" in parsed) {
|
|
490
|
+
if (typeof parsed.cancel === "number") {
|
|
491
|
+
const request = requests.get(parsed.cancel);
|
|
492
|
+
if (request)
|
|
493
|
+
void stopOperation(request.operationId).catch((error) => process.stderr.write(`${String(error)}\n`));
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (!isObject(parsed) || !Number.isSafeInteger(parsed.id)) {
|
|
498
|
+
process.stderr.write("WORKER_PROTOCOL_ERROR: request requires a numeric id\n");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (typeof parsed.action !== "string" ||
|
|
502
|
+
!isObject(parsed.args) ||
|
|
503
|
+
(parsed.operationId !== undefined &&
|
|
504
|
+
(typeof parsed.operationId !== "string" ||
|
|
505
|
+
parsed.operationId.length < 1 ||
|
|
506
|
+
parsed.operationId.length > 200))) {
|
|
507
|
+
process.stdout.write(`${JSON.stringify({ id: parsed.id, result: { ok: false, action: typeof parsed.action === "string" ? parsed.action : "invalid_request", error: "INVALID_REQUEST: action must be a string and args must be an object" } })}\n`);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const request = parsed;
|
|
511
|
+
if (requests.has(request.id)) {
|
|
512
|
+
process.stderr.write("WORKER_PROTOCOL_ERROR: duplicate request id\n");
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const controller = new AbortController();
|
|
516
|
+
requests.set(request.id, {
|
|
517
|
+
controller,
|
|
518
|
+
operationId: request.operationId ?? `worker-${process.pid}`,
|
|
519
|
+
action: request.action,
|
|
520
|
+
});
|
|
521
|
+
queue = queue
|
|
522
|
+
.then(() => runNativeOperation(request.operationId ?? `worker-${process.pid}`, () => dispatch(request, controller.signal)))
|
|
523
|
+
.catch((error) => {
|
|
524
|
+
process.stdout.write(`${JSON.stringify({ id: request.id, result: { ok: false, action: request.action, error: controller.signal.aborted ? "COMPUTER_USE_STOPPED" : String(error) } })}\n`);
|
|
525
|
+
})
|
|
526
|
+
.finally(() => {
|
|
527
|
+
requests.delete(request.id);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
input.on("close", () => {
|
|
531
|
+
clearInterval(presentationPoll);
|
|
532
|
+
// EOF is shutdown, including when a call deliberately disabled its deadline.
|
|
533
|
+
for (const request of requests.values())
|
|
534
|
+
if (request.action !== "operation_end")
|
|
535
|
+
void stopOperation(request.operationId).catch((error) => process.stderr.write(`${String(error)}\n`));
|
|
536
|
+
process.stdout.write(`${JSON.stringify({ event: "shutdown_state", phase: "started" })}\n`);
|
|
537
|
+
void queue
|
|
538
|
+
.then(async () => {
|
|
539
|
+
await stopping;
|
|
540
|
+
await disposeBrowser();
|
|
541
|
+
await awaitPointerCleanup();
|
|
542
|
+
await disposeDesktop();
|
|
543
|
+
await disposeOcr();
|
|
544
|
+
await stopListener.dispose();
|
|
545
|
+
await disposeOwnership();
|
|
546
|
+
process.stdout.write(`${JSON.stringify({ event: "shutdown_state", phase: "complete" })}\n`);
|
|
547
|
+
})
|
|
548
|
+
.catch((error) => {
|
|
549
|
+
// Keep native handles and ownership alive if safe exit cannot be proved.
|
|
550
|
+
process.stderr.write(`CLEANUP_UNCONFIRMED: ${String(error)}\n`);
|
|
551
|
+
process.stdout.write(`${JSON.stringify({ event: "shutdown_state", phase: "failed", error: String(error) })}\n`);
|
|
552
|
+
});
|
|
553
|
+
});
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Documentation index
|
|
2
|
+
|
|
3
|
+
Read the row matching the task; follow section links before reading whole documents.
|
|
4
|
+
|
|
5
|
+
| Task | Read |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| Requirements and scope | [Spec: solution](spec.md#solution), [user stories](spec.md#user-stories), [out of scope](spec.md#out-of-scope) |
|
|
8
|
+
| Modifier taps, holds and timing controls | [Input timing spec](spec.md#input-timing-and-modifier-taps), [delivery](implementation-plan.md#input-timing-delivery), [scoped acceptance](validation.md#input-timing-and-modifier-tap-acceptance) |
|
|
9
|
+
| Implementation contracts | [Spec: implementation decisions](spec.md#implementation-decisions) |
|
|
10
|
+
| Delivery, design review and feasibility | [Design revision](implementation-plan.md#design-revision), [plan: detailed milestones](implementation-plan.md#detailed-milestones), [feasibility gates](implementation-plan.md#feasibility-gates) |
|
|
11
|
+
| Test design and release validation | [Spec: testing decisions](spec.md#testing-decisions), [plan: coverage matrix](implementation-plan.md#coverage-matrix) |
|
|
12
|
+
| Pi acceptance, phased testing and agent recovery | [Current phases](implementation-plan.md#detailed-milestones), [baseline audit](validation.md#baseline-identity-and-schema-evidence), [guidance inventory](validation.md#model-visible-guidance-inventory), [scenario catalog](validation.md#alignment-scenario-catalog) |
|
|
13
|
+
| Release checks, capability coverage and pending acceptance | [0.1.1 preparation and release notes](validation.md#011-release-preparation), [verified 0.1.0 npm release](validation.md#npm-publication-receipt), [0.1.0 release preparation](validation.md#first-release-preparation), [current release audit](validation.md#final-release-audit), [capability coverage](validation.md#capability-coverage), [validation history](validation.md) |
|
|
14
|
+
| Pi interfaces and sources | [Pi extension research](pi-extension-research.md) (research context; current spec takes precedence over earlier proposals) |
|
|
15
|
+
| Domain terminology | [Glossary](../GLOSSARY.md) |
|
|
16
|
+
| Current Pi-first acceptance scope | [ADR 0004](adr/0004-pi-first-acceptance.md) |
|
|
17
|
+
| Windows default apps, existing windows and foreground | [Accepted policy](spec.md#default-apps-workspaces-and-foreground), [ADR 0005](adr/0005-default-apps-and-workspace-reuse.md), [delivery cases](implementation-plan.md#default-app-and-workspace-delivery), [current support](windows.md#default-apps-and-workspaces) |
|
|
18
|
+
| Historical reference-parity decision | [Superseded ADR 0001](adr/0001-preserve-reference-behavior.md) |
|
|
19
|
+
| Stronger native window identity design | [Lifetime plan](implementation-plan.md#window-lifetime-design), [ADR 0002](adr/0002-window-lifetime-references.md) |
|
|
20
|
+
| Publish or read issues | [Issue tracker](agents/issue-tracker.md), [triage labels](agents/triage-labels.md) |
|
|
21
|
+
| What works, remaining outcomes, next work and cleanup decisions | [Progress summary](tickets.md#current-progress), [delivery map](tickets.md#delivery-outcomes-and-ownership), [execution queue](tickets.md#execution-queue), [cleanup dispositions](tickets.md#cleanup-dispositions), [original obligations](tickets.md#original-issue-disposition) |
|
|
22
|
+
| Worker setup, protocol and test evidence | [Runtime and validation](runtime.md) |
|
|
23
|
+
| Exclusive control, human handback and emergency stopping | [Control contract](spec.md#exclusive-control), [delivery/test slices](implementation-plan.md#exclusive-control-delivery), [ADR 0003](adr/0003-exclusive-desktop-control.md), [current stopping implementation](stopping.md), [control acceptance](validation.md#control-interruption-acceptance) |
|
|
24
|
+
| Control indicators, preview and recording acceptance | [Indicators](indicators.md), [Snipping Tool receipt](validation.md#snipping-tool-recording-acceptance) |
|
|
25
|
+
| Desktop ownership and operation lifecycle | [Ownership](ownership.md) |
|
|
26
|
+
| Native window discovery and validation | [Windows discovery](windows.md), [accessibility observations](windows.md#accessibility-observations), [targeting acceptance reconciliation](validation.md#pi-targeting-and-accessibility-reconciliation), [OCR failures](validation.md#pi-ocr-failure-acceptance), [locked sessions](windows.md#locked-session-refusals) |
|
|
27
|
+
| Scoped browser/mixed agent acceptance | [Held-navigation cancellation](validation.md#scoped-held-navigation-cancellation), [Partial-observation recovery](validation.md#scoped-partial-observation-recovery), [mixed outcome evidence](validation.md#scoped-mixed-browsernative-outcome), [runner prerequisites](validation.md#reproduce-automated-checks), [ordinary workspace actor outcomes](validation.md#ordinary-pi-workspace-actor-preparation) |
|
|
28
|
+
| Scoped native document and visual agent acceptance | [Document workflow](validation.md#native-document-workflow), [handback continuation](validation.md#native-cancellation-and-handback-continuation), [opt-in visual runner](windows.md#scoped-visual-agent-check), [A04 evidence](validation.md#visual-ocr-association-preflight) |
|
|
29
|
+
| Lifetime-bound native targeting and stale-reference behavior | [Window references](windows.md#window-lifetime-references), [accepted design](implementation-plan.md#window-lifetime-design) |
|
|
30
|
+
| Native interop alternatives and tradeoffs | [Native interop research](native-interop-research.md) |
|
|
31
|
+
| Guarded desktop pointer and dedicated input validation | [Pointer actions](pointer.md), [single-click holds/deadlines](pointer.md#single-click-holds-and-deadlines), [popup/menu/cross-process drop and restoration acceptance](validation.md#scoped-pointer-and-restoration-acceptance) |
|
|
32
|
+
| Text, chords, keypress timing, clipboard preservation and validation | [Text entry](text.md), [keypress timing](text.md#keypress-holds-and-deadlines), [text pacing](text.md#sendinput-pacing-and-deadlines), [chord/editor-focus acceptance](validation.md#pi-chords-and-editor-focus-acceptance), [clipboard acceptance](validation.md#pi-clipboard-contention-and-privacy-acceptance) |
|
|
33
|
+
| Personal settings, commands and tool loading | [Settings](settings.md) |
|
|
34
|
+
| Browser reads/actions, dedicated profiles, idle cleanup and fixtures | [Browser](browser.md) |
|
|
35
|
+
| Domain-document consumer rules | [Domain docs](agents/domain.md) |
|
|
36
|
+
|
|
37
|
+
## Repository layout
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
README.md project entry point
|
|
41
|
+
AGENTS.md short contributor rules and routing
|
|
42
|
+
GLOSSARY.md domain terms
|
|
43
|
+
src/ flat TypeScript modules
|
|
44
|
+
tests/ tests and native smoke fixtures
|
|
45
|
+
assets/ packaged resources and runtime notices
|
|
46
|
+
docs/
|
|
47
|
+
README.md this index
|
|
48
|
+
spec.md authoritative requirements
|
|
49
|
+
implementation-plan.md delivery, feasibility and coverage
|
|
50
|
+
pi-extension-research.md cited Pi interface research
|
|
51
|
+
adr/ durable decision rationale
|
|
52
|
+
agents/ issue tracker, triage and domain configuration
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use the existing source, test and asset directories. Keep responsibility-named modules such as extension, launcher, worker, desktop, browser, settings and stop; split by demonstrated complexity rather than pre-creating a directory per module.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: superseded by ADR-0004
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Preserve reference behavior while implementing for Pi
|
|
6
|
+
|
|
7
|
+
Historical decision. [ADR 0004](0004-pi-first-acceptance.md) replaces mandatory DSH parity with Pi-first acceptance; the Pi capability and safety requirements remain in the current specification.
|
|
8
|
+
|
|
9
|
+
Implement fresh TypeScript code for native Pi tools and Windows/WSL2 hosts, using dsh-computer-use-win 0.5.3 as the behavioral baseline. Preserve its safeguards, browser post-action observations and experimental installed-Firefox compatibility rather than changing behavior to fit a simpler browser library; simplification remains welcome where these contracts are retained. [ADR 0003](0003-exclusive-desktop-control.md) supersedes shared physical-user-input behavior for exclusive-control periods; other reference contracts remain the baseline. This trades some implementation freedom for a faithful Pi version and supersedes the proposed managed-Firefox and observation changes.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Bind observed targets to a window lifetime
|
|
6
|
+
|
|
7
|
+
The user chose stronger window-lifetime tracking beyond DSH's HWND/PID checks; this records the reviewed implementation design, not delivered feature acceptance. Keep the reference authority beside the existing native UIA cache, using a unique private window property as a lifetime witness; expose one opaque `windowRef` target field and retain raw selectors as explicit compatibility selection. This avoids a second registry and asynchronous event-delivery assumptions, at the cost of window-metadata writes, UIPI restrictions, conservative invalidation after native-child restart and possible scalar-marker residue after abrupt native-child exit.
|
|
8
|
+
|
|
9
|
+
The owned-window probe confirmed marker isolation, removal and replacement behavior; it did not achieve identical-HWND reuse. [UIA RuntimeIds can be reused](https://learn.microsoft.com/en-us/windows/win32/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-getruntimeid), [WinEvent delivery can be asynchronous](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwineventhook), and [SetProp requires ownership hygiene and respects UIPI](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setpropw). Validation cannot atomically lock a foreign window through a later action; the contract rejects already-lost proof and prevents subsequent steps rather than promising to undo racing effects. Implementation and acceptance follow [the #28 plan](../implementation-plan.md#window-lifetime-design).
|
|
10
|
+
|
|
11
|
+
Unavailable binding leaves read metadata available without actionable native reference IDs. Existing desktop-image snapshots retain separate desktop provenance; stronger window identity does not change their image scope.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Use exclusive control for physical desktop automation
|
|
6
|
+
|
|
7
|
+
The user chose suppression of ordinary physical keyboard/mouse input while the agent controls the desktop, with a blue border and explicit stop or human handback. This departs from DSH's selectively handled stop keys and Pi's earlier interference-aborts behavior: accidental interaction should not disrupt an agent task. It superseded that part of [ADR 0001](0001-preserve-reference-behavior.md), trading simultaneous desktop use for a simpler control contract; full acceptance is pending and all Pi safeguards remain. [ADR 0004](0004-pi-first-acceptance.md) now governs the broader Pi-first acceptance scope.
|
|
8
|
+
|
|
9
|
+
Requirements live in the [spec](../spec.md#exclusive-control); feasibility, responsibility boundaries and acceptance live in the [plan](../implementation-plan.md#exclusive-control-delivery). [BlockInput](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-blockinput) restricts injection to its owning thread, while [keyboard hooks](https://learn.microsoft.com/en-us/windows/win32/winmsg/lowlevelkeyboardproc) can suppress events but can be removed on timeout. The candidate uses hooks in the existing independent listener, which allows separately tagged agent input. The native observer ignores physical input only when a downstream hook confirms it consumed the event and the retained listener/protection event remain valid; unconsumed or foreign injected input still invalidates the action. Tagged absolute pointer movement replaces the old expected-SetCursorPos queue.
|
|
10
|
+
|
|
11
|
+
On 2026-11-01, after the grouped physical check passed and a nonelevated BlockInput probe returned access denied, the user chose hooks with documented Windows limits for the first release. This avoids requiring additional privileges or isolation. Silent hook removal can allow physical events through and make physical stop keys unavailable; neither a live listener nor a stored hook handle proves otherwise. The first release accepts that specific platform limit, without claiming absolute input blocking. Detected protection loss must still stop automation and preserve uncertain-input safeguards. The decision does not waive remaining physical, failure, host or cleanup acceptance, or make #31 complete.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Judge release readiness by Pi functionality
|
|
6
|
+
|
|
7
|
+
On 2026-11-02 the user prioritized a working standalone Pi extension over copying DSH and discarded the remaining DSH manual comparisons. This supersedes ADR 0001's mandatory behavioral-parity gate: DSH remains design/source context, while the current Pi specification defines supported behavior and acceptance. DSH installation, credentials, host/model runs, side-by-side manual checks and comparative performance are no longer release prerequisites.
|
|
8
|
+
|
|
9
|
+
Keep the 25 implemented tool capabilities, supported browsers/hosts, preservation and cleanup safeguards, exclusive control and the accepted Windows hook limitation. Reuse existing Pi evidence; preserve historical comparisons as historical evidence, with reference-backed developer utilities optional. Close a Pi deliverable only when its remaining Pi criteria are supported; transfer any shared Pi obligations explicitly before retiring an alignment/diagnosis umbrella. Independent Pi correctness, failure handling and useful responsiveness still matter.
|