pi2web 0.2.1 → 0.2.8
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 +53 -6
- package/dist/activity.js +178 -0
- package/dist/agent-feed.js +133 -0
- package/dist/agent-templates.js +144 -0
- package/dist/agents.js +671 -152
- package/dist/auth.js +3 -1
- package/dist/collab/baseline.js +84 -0
- package/dist/collab/dispatcher.js +45 -10
- package/dist/collab/hub.js +1392 -384
- package/dist/collab/pi-extension.js +322 -65
- package/dist/collab/review-flow.js +197 -48
- package/dist/collab/routes.js +94 -23
- package/dist/collab/schemas.js +69 -13
- package/dist/collab/scoring-flow.js +145 -39
- package/dist/collab/similarity.js +15 -0
- package/dist/collab/store.js +467 -112
- package/dist/collab/types.js +94 -10
- package/dist/collab/validate.js +62 -25
- package/dist/drafts.js +26 -6
- package/dist/http/agent-routes.js +222 -0
- package/dist/http/login-limiter.js +53 -0
- package/dist/http/mail-settings-routes.js +53 -0
- package/dist/http/system-prompt-routes.js +52 -0
- package/dist/http/utils.js +47 -0
- package/dist/http/web-assets.js +64 -0
- package/dist/http/workspace-routes.js +58 -0
- package/dist/mail-notifier.js +63 -17
- package/dist/main.js +61 -16
- package/dist/mcp.js +24 -5
- package/dist/metadata-store.js +203 -56
- package/dist/paths.js +10 -0
- package/dist/sdk-backend.js +249 -45
- package/dist/server.js +488 -614
- package/dist/session-namer.js +66 -17
- package/dist/session-performance.js +137 -0
- package/dist/sotto-stt.js +217 -0
- package/dist/terminals.js +94 -27
- package/dist/timeline.js +185 -0
- package/dist/voice.js +153 -35
- package/dist/workspaces.js +91 -26
- package/dist/ws/agent-socket.js +256 -0
- package/dist/ws/terminal-socket.js +46 -0
- package/npm-shrinkwrap.json +3685 -0
- package/package.json +11 -1
- package/packages/protocol/openapi.yaml +24 -2
- package/packages/protocol/websocket.schema.json +58 -15
- package/web/activity-view.js +121 -0
- package/web/app.js +3647 -756
- package/web/backend-state.js +59 -27
- package/web/collab.js +684 -257
- package/web/hid-keyboard.js +173 -140
- package/web/index.html +7 -2
- package/web/styles.css +21 -2
- package/web/voice-pcm.js +36 -0
package/dist/auth.js
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createReadStream } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
const run = promisify(execFile);
|
|
7
|
+
/** Anchors every round to an immutable code state so round N+1 verdicts are not made against round N's memory. */
|
|
8
|
+
export const gitBaseline = async ({ cwd, subject }) => {
|
|
9
|
+
const git = async (args) => (await run('git', args, { cwd })).stdout;
|
|
10
|
+
try {
|
|
11
|
+
const commit = (await git(['rev-parse', 'HEAD'])).trim();
|
|
12
|
+
let dirtyHash;
|
|
13
|
+
const status = await git(['status', '--porcelain', '-z']);
|
|
14
|
+
if (status) {
|
|
15
|
+
const untracked = (await git(['ls-files', '--others', '--exclude-standard', '-z']))
|
|
16
|
+
.split('\0')
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.sort();
|
|
19
|
+
// Hashed as a stream, never buffered. A working tree diff can be tens of megabytes, and a diff dropped for
|
|
20
|
+
// exceeding a buffer would leave a hash of nothing but file names - identical before and after an edit to
|
|
21
|
+
// those same files, which is exactly what `assertBaselineCurrent()` exists to catch.
|
|
22
|
+
const hash = createHash('sha256').update(status);
|
|
23
|
+
await hashStream(hash, spawn('git', ['diff', 'HEAD', '--binary'], { cwd, stdio: ['ignore', 'pipe', 'ignore'] }));
|
|
24
|
+
for (const relative of untracked) {
|
|
25
|
+
hash.update(relative).update('\0');
|
|
26
|
+
// A file removed during capture is already represented by status.
|
|
27
|
+
await hashStream(hash, createReadStream(path.join(cwd, relative))).catch(() => { });
|
|
28
|
+
}
|
|
29
|
+
dirtyHash = `sha256:${hash.digest('hex')}`;
|
|
30
|
+
}
|
|
31
|
+
// A commit range such as `main...feature` is only a name: either endpoint can move while HEAD and the
|
|
32
|
+
// working tree stay identical, which would silently change the code under review. Resolve both endpoints
|
|
33
|
+
// so the pinned identity covers the range, not just its spelling.
|
|
34
|
+
let rangeResolved;
|
|
35
|
+
if (subject.type === 'commit_range') {
|
|
36
|
+
const expression = subject.value.trim();
|
|
37
|
+
const endpoints = expression.includes('...')
|
|
38
|
+
? expression.split('...')
|
|
39
|
+
: expression.includes('..')
|
|
40
|
+
? expression.split('..')
|
|
41
|
+
: [expression];
|
|
42
|
+
const resolved = [];
|
|
43
|
+
for (const endpoint of endpoints) {
|
|
44
|
+
const name = endpoint.trim();
|
|
45
|
+
resolved.push(name
|
|
46
|
+
? (await git(['rev-parse', '--verify', `${name}^{commit}`]).catch(() => '')).trim() ||
|
|
47
|
+
`unresolved:${name}`
|
|
48
|
+
: 'HEAD');
|
|
49
|
+
}
|
|
50
|
+
rangeResolved = resolved.join('...');
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
vcs: 'git',
|
|
54
|
+
commit,
|
|
55
|
+
range: subject.type === 'commit_range' ? subject.value : undefined,
|
|
56
|
+
rangeResolved,
|
|
57
|
+
dirtyHash,
|
|
58
|
+
paths: subject.type === 'paths'
|
|
59
|
+
? subject.value
|
|
60
|
+
.split(/[\n,]/)
|
|
61
|
+
.map(value => value.trim())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
: []
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Not a git checkout (or git is missing). The hub still works; it just cannot prove code identity.
|
|
68
|
+
return { vcs: 'none', paths: [] };
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/** Feeds one stream into a hash, rejecting on failure so a partial read can never pass as "nothing changed". */
|
|
72
|
+
function hashStream(hash, source) {
|
|
73
|
+
const process = 'stdout' in source ? source : undefined, stream = (process ? process.stdout : source);
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
if (!stream)
|
|
76
|
+
return reject(new Error('Stream is not readable'));
|
|
77
|
+
stream.on('data', chunk => hash.update(chunk));
|
|
78
|
+
stream.on('error', reject);
|
|
79
|
+
if (!process)
|
|
80
|
+
return void stream.on('end', resolve);
|
|
81
|
+
process.on('error', reject);
|
|
82
|
+
process.on('close', code => (code === 0 ? resolve() : reject(new Error(`git exited with ${code}`))));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -86,7 +86,10 @@ export class CollabDispatcher {
|
|
|
86
86
|
const existing = this.pending.get(participantId);
|
|
87
87
|
if (existing)
|
|
88
88
|
clearTimeout(existing);
|
|
89
|
-
const timer = setTimeout(() => {
|
|
89
|
+
const timer = setTimeout(() => {
|
|
90
|
+
this.pending.delete(participantId);
|
|
91
|
+
void this.enqueue(sessionId, participantId, task);
|
|
92
|
+
}, this.deps.delayMs ?? 150);
|
|
90
93
|
timer.unref?.();
|
|
91
94
|
this.pending.set(participantId, timer);
|
|
92
95
|
}
|
|
@@ -94,8 +97,10 @@ export class CollabDispatcher {
|
|
|
94
97
|
enqueue(sessionId, participantId, task) {
|
|
95
98
|
const previous = this.inFlight.get(participantId) ?? Promise.resolve();
|
|
96
99
|
const job = previous.catch(() => { }).then(() => this.wake(sessionId, participantId, task));
|
|
97
|
-
const tracked = job.finally(() => {
|
|
98
|
-
this.inFlight.
|
|
100
|
+
const tracked = job.finally(() => {
|
|
101
|
+
if (this.inFlight.get(participantId) === tracked)
|
|
102
|
+
this.inFlight.delete(participantId);
|
|
103
|
+
});
|
|
99
104
|
this.inFlight.set(participantId, tracked);
|
|
100
105
|
return tracked;
|
|
101
106
|
}
|
|
@@ -108,14 +113,21 @@ export class CollabDispatcher {
|
|
|
108
113
|
return;
|
|
109
114
|
}
|
|
110
115
|
const agentId = participant.agentId;
|
|
111
|
-
if (!agentId) {
|
|
116
|
+
if (!agentId) {
|
|
117
|
+
// only a pre-managed-only row can get here
|
|
112
118
|
this.hub.logDispatch(sessionId, 'dispatch_failed', { participantId, task, reason: 'NO_AGENT_BOUND' });
|
|
113
119
|
return;
|
|
114
120
|
}
|
|
115
121
|
if (participant.state !== 'active')
|
|
116
122
|
return; // left or out of budget: a human has to act
|
|
117
|
-
if (this.deps.isCollabAgent?.(agentId) === false) {
|
|
118
|
-
|
|
123
|
+
if (this.deps.isCollabAgent?.(agentId) === false) {
|
|
124
|
+
// undefined: the check is not wired up
|
|
125
|
+
this.hub.logDispatch(sessionId, 'dispatch_failed', {
|
|
126
|
+
participantId,
|
|
127
|
+
agentId,
|
|
128
|
+
task,
|
|
129
|
+
reason: 'COLLAB_PROFILE_REQUIRED'
|
|
130
|
+
});
|
|
119
131
|
this.deps.log?.(`Collab dispatch skipped for ${participant.displayName}: the Agent is not profile=collab.`);
|
|
120
132
|
return;
|
|
121
133
|
}
|
|
@@ -137,7 +149,14 @@ export class CollabDispatcher {
|
|
|
137
149
|
if (!terminal && this.hub.currentTaskFor(participantId) === 'wait') {
|
|
138
150
|
this.hub.completeDelivery(participantId, task);
|
|
139
151
|
this.delivered.delete(participantId);
|
|
140
|
-
this.hub.logDispatch(sessionId, 'dispatch_skipped', {
|
|
152
|
+
this.hub.logDispatch(sessionId, 'dispatch_skipped', {
|
|
153
|
+
participantId,
|
|
154
|
+
agentId,
|
|
155
|
+
task,
|
|
156
|
+
reason: 'NOTHING_OWED',
|
|
157
|
+
phase: session.phase,
|
|
158
|
+
round: session.round
|
|
159
|
+
});
|
|
141
160
|
return;
|
|
142
161
|
}
|
|
143
162
|
// The bound agent is part of the identity of a delivery: after a rebind the *new* agent has received
|
|
@@ -152,7 +171,14 @@ export class CollabDispatcher {
|
|
|
152
171
|
try {
|
|
153
172
|
await this.deps.command(agentId, kind, toolBriefing({ session, participant, task }));
|
|
154
173
|
this.delivered.set(participantId, key);
|
|
155
|
-
this.hub.logDispatch(sessionId, 'agent_dispatched', {
|
|
174
|
+
this.hub.logDispatch(sessionId, 'agent_dispatched', {
|
|
175
|
+
participantId,
|
|
176
|
+
agentId,
|
|
177
|
+
task,
|
|
178
|
+
kind,
|
|
179
|
+
phase: session.phase,
|
|
180
|
+
round: session.round
|
|
181
|
+
});
|
|
156
182
|
// Normal work is acknowledged by collab_get_task, not here: followUp() returning means queued, not run.
|
|
157
183
|
// A closing note has no tool collection step, so successful completion remains its acknowledgement.
|
|
158
184
|
if (terminal) {
|
|
@@ -161,7 +187,12 @@ export class CollabDispatcher {
|
|
|
161
187
|
}
|
|
162
188
|
}
|
|
163
189
|
catch (error) {
|
|
164
|
-
this.hub.logDispatch(sessionId, 'dispatch_failed', {
|
|
190
|
+
this.hub.logDispatch(sessionId, 'dispatch_failed', {
|
|
191
|
+
participantId,
|
|
192
|
+
agentId,
|
|
193
|
+
task,
|
|
194
|
+
reason: error.message
|
|
195
|
+
});
|
|
165
196
|
this.deps.log?.(`Collab dispatch to agent ${agentId} failed: ${error.message}`);
|
|
166
197
|
// The note did NOT arrive, so the item stays unacked: a restart retries it inside
|
|
167
198
|
// CLOSING_NOTE_RETRY_WINDOW_MS.
|
|
@@ -179,7 +210,11 @@ export function toolBriefing(input) {
|
|
|
179
210
|
`role: ${input.participant.role}; action: ${input.task}; working directory: ${input.session.cwd}`,
|
|
180
211
|
`user request: ${input.session.subject.value}`,
|
|
181
212
|
...(input.session.subject.notes ? [`user notes: ${input.session.subject.notes}`] : []),
|
|
182
|
-
...(input.task === 'implement'
|
|
213
|
+
...(input.task === 'implement'
|
|
214
|
+
? []
|
|
215
|
+
: [
|
|
216
|
+
'This is a non-mutating task. You may use bash to inspect (for example git diff/log), but do not create, delete, rename, or modify anything under the working directory, including redirected output, scratch files, generated files, caches, or formatter changes.'
|
|
217
|
+
]),
|
|
183
218
|
'Call collab_get_task first. It repeats the request, supplies the pinned baseline and activates the exact collab_submit_* tool; use that tool to record the complete result.',
|
|
184
219
|
'Do not use curl, construct URLs, handle credentials, contact other agents, sleep, or poll. End your turn after submitting.'
|
|
185
220
|
].join('\n');
|