pi2web 0.2.0 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +209 -18
  2. package/dist/activity.js +178 -0
  3. package/dist/agent-feed.js +133 -0
  4. package/dist/agent-templates.js +144 -0
  5. package/dist/agents.js +575 -97
  6. package/dist/auth.js +85 -20
  7. package/dist/collab/baseline.js +84 -0
  8. package/dist/collab/dispatcher.js +221 -0
  9. package/dist/collab/hub.js +2888 -0
  10. package/dist/collab/pi-extension.js +494 -0
  11. package/dist/collab/review-flow.js +459 -0
  12. package/dist/collab/routes.js +285 -0
  13. package/dist/collab/schemas.js +281 -0
  14. package/dist/collab/scoring-flow.js +336 -0
  15. package/dist/collab/similarity.js +15 -0
  16. package/dist/collab/store.js +918 -0
  17. package/dist/collab/types.js +137 -0
  18. package/dist/collab/validate.js +185 -0
  19. package/dist/drafts.js +50 -0
  20. package/dist/http/agent-routes.js +196 -0
  21. package/dist/http/login-limiter.js +53 -0
  22. package/dist/http/mail-settings-routes.js +53 -0
  23. package/dist/http/system-prompt-routes.js +52 -0
  24. package/dist/http/utils.js +43 -0
  25. package/dist/http/web-assets.js +63 -0
  26. package/dist/http/workspace-routes.js +58 -0
  27. package/dist/mail-notifier.js +95 -21
  28. package/dist/main.js +141 -17
  29. package/dist/mcp.js +132 -0
  30. package/dist/metadata-store.js +199 -56
  31. package/dist/paths.js +10 -0
  32. package/dist/sdk-backend.js +288 -49
  33. package/dist/server.js +558 -358
  34. package/dist/session-namer.js +163 -0
  35. package/dist/terminals.js +94 -27
  36. package/dist/timeline.js +185 -0
  37. package/dist/voice.js +196 -40
  38. package/dist/workspaces.js +91 -26
  39. package/dist/ws/agent-socket.js +256 -0
  40. package/dist/ws/terminal-socket.js +46 -0
  41. package/npm-shrinkwrap.json +3685 -0
  42. package/package.json +18 -6
  43. package/packages/edge-tts-server/src/remote_pi_edge_tts/__pycache__/__init__.cpython-312.pyc +0 -0
  44. package/packages/edge-tts-server/src/remote_pi_edge_tts/__pycache__/__main__.cpython-312.pyc +0 -0
  45. package/packages/protocol/openapi.yaml +8 -2
  46. package/packages/protocol/websocket.schema.json +94 -10
  47. package/web/activity-view.js +121 -0
  48. package/web/app.js +3770 -404
  49. package/web/backend-state.js +269 -0
  50. package/web/collab.html +95 -0
  51. package/web/collab.js +1080 -0
  52. package/web/hid-keyboard.js +453 -0
  53. package/web/index.html +14 -9
  54. package/web/styles.css +73 -0
  55. package/ANDROID_APP_FEATURES.md +0 -256
@@ -0,0 +1,285 @@
1
+ import { ValidationError } from './validate.js';
2
+ import { COLLAB_ERRORS } from './types.js';
3
+ const ok = (payload, status = 200) => ({ status, payload: { data: payload } });
4
+ const forbidden = (message) => ({
5
+ status: 403,
6
+ payload: { error: { code: COLLAB_ERRORS.forbidden, message } }
7
+ });
8
+ /**
9
+ * HTTP surface for the collaboration hub. Two credentials are accepted:
10
+ * the pairing code (a human, full control) and a participant token (one agent, its own session only).
11
+ * Session creation, participant registration, forced advances, and rulings are human-only by design.
12
+ */
13
+ export class CollabRouter {
14
+ hub;
15
+ deps;
16
+ constructor(hub, deps) {
17
+ this.hub = hub;
18
+ this.deps = deps;
19
+ }
20
+ async handle(input) {
21
+ const { method, url, parts, token } = input;
22
+ if (parts[0] !== 'api' || parts[1] !== 'v1' || parts[2] !== 'collab')
23
+ return undefined;
24
+ const isHuman = token ? await this.deps.verifyHuman(token) : this.deps.noAuth;
25
+ let participant;
26
+ if (!isHuman) {
27
+ try {
28
+ participant = this.hub.authenticate(token);
29
+ }
30
+ catch {
31
+ return {
32
+ status: 401,
33
+ payload: {
34
+ error: { code: 'UNAUTHORIZED', message: 'A pairing code or participant token is required' }
35
+ },
36
+ authFailed: true
37
+ };
38
+ }
39
+ }
40
+ try {
41
+ return await this.route({ method, url, parts, body: input.body, isHuman, participant });
42
+ }
43
+ catch (error) {
44
+ return this.errorResult(error);
45
+ }
46
+ }
47
+ async route(ctx) {
48
+ const { method, url, parts, isHuman, participant } = ctx;
49
+ const human = (action) => {
50
+ if (!isHuman)
51
+ throw Object.assign(new Error(`${action} is reserved for humans`), {
52
+ code: COLLAB_ERRORS.humanOnly,
53
+ httpStatus: 403
54
+ });
55
+ };
56
+ const asParticipant = () => {
57
+ if (!participant)
58
+ throw Object.assign(new Error('This endpoint requires a participant token'), {
59
+ code: COLLAB_ERRORS.forbidden,
60
+ httpStatus: 403
61
+ });
62
+ return participant;
63
+ };
64
+ const scoped = (sessionId) => {
65
+ if (participant && participant.sessionId !== sessionId)
66
+ throw Object.assign(new Error('Collaboration session not found'), {
67
+ code: COLLAB_ERRORS.sessionNotFound,
68
+ httpStatus: 404
69
+ });
70
+ };
71
+ const collabAgent = (agentId) => {
72
+ if (typeof agentId !== 'string' || !this.deps.isCollabAgent(agentId))
73
+ throw Object.assign(new Error('A collaboration seat requires an existing, dedicated collaboration Agent (profile=collab). Create one from the collaboration board, then register or rebind this seat to it.'), { code: COLLAB_ERRORS.forbidden, httpStatus: 403 });
74
+ };
75
+ const number = (name, fallback) => {
76
+ const raw = url.searchParams.get(name);
77
+ const value = raw === null ? fallback : Number(raw);
78
+ return Number.isFinite(value) ? value : fallback;
79
+ };
80
+ // /api/v1/collab/escalations[/{id}/resolve]
81
+ if (parts[3] === 'escalations') {
82
+ if (method === 'GET' && parts.length === 4) {
83
+ human('Listing escalations');
84
+ return ok(this.hub.listEscalations({
85
+ status: url.searchParams.get('status') ?? undefined,
86
+ sessionId: url.searchParams.get('sessionId') ?? undefined
87
+ }));
88
+ }
89
+ if (method === 'GET' && parts.length === 5) {
90
+ human('Reading an escalation');
91
+ return ok(this.hub.getEscalation(parts[4]));
92
+ }
93
+ if (method === 'POST' && parts[5] === 'resolve') {
94
+ human('Ruling on an escalation');
95
+ return ok(await this.hub.resolveEscalation(parts[4], await ctx.body()));
96
+ }
97
+ return undefined;
98
+ }
99
+ if (parts[3] !== 'sessions')
100
+ return undefined;
101
+ if (method === 'POST' && parts.length === 4) {
102
+ human('Creating a collaboration session');
103
+ return ok(await this.hub.createSession(await ctx.body(), this.deps.resolveCwd), 201);
104
+ }
105
+ if (method === 'GET' && parts.length === 4) {
106
+ if (isHuman)
107
+ return ok(this.hub.listSessions({
108
+ status: url.searchParams.get('status') ?? undefined,
109
+ kind: url.searchParams.get('kind') ?? undefined,
110
+ limit: number('limit', 50),
111
+ offset: number('offset', 0)
112
+ }));
113
+ return ok([this.hub.getSession(asParticipant().sessionId)]);
114
+ }
115
+ const sessionId = parts[4];
116
+ if (!sessionId)
117
+ return undefined;
118
+ scoped(sessionId);
119
+ const tail = parts[5], sub = parts[6];
120
+ // Only these tails have sub-resources. Without this, `POST .../advance/anything` would be treated as an
121
+ // advance, and any future sub-path would be silently answered by the flat handler above it.
122
+ if (sub && !['participants', 'issues', 'debates'].includes(tail ?? ''))
123
+ return undefined;
124
+ if (method === 'GET' && !tail)
125
+ return ok({
126
+ ...this.hub.getSession(sessionId),
127
+ progress: this.hub.progress(sessionId),
128
+ participants: isHuman
129
+ ? this.hub.participantsForHuman(sessionId)
130
+ : this.hub.store.listParticipants(sessionId)
131
+ });
132
+ if (tail === 'participants') {
133
+ // Sub-resources first: an unguarded register branch would swallow .../participants/{id}/binding and /budget
134
+ // and answer them with "role is required", which is how both repair paths became unreachable over HTTP.
135
+ // .../participants/{participantId}/binding — repairs a seat that was registered with the wrong binding.
136
+ if (method === 'POST' && sub && parts[7] === 'binding') {
137
+ human('Rebinding a participant');
138
+ const body = await ctx.body();
139
+ if (typeof body?.agentId === 'string')
140
+ collabAgent(body.agentId);
141
+ const { participant: bound, token } = this.hub.rebindParticipant(sessionId, sub, body);
142
+ return ok({ participant: bound, participantToken: token });
143
+ }
144
+ // .../participants/{participantId}/budget — un-blocks a seat that spent its budget, at any time.
145
+ if (method === 'POST' && sub && parts[7] === 'budget') {
146
+ human('Raising a token budget');
147
+ return ok(this.hub.raiseParticipantBudget(sessionId, sub, await ctx.body()));
148
+ }
149
+ // An unknown sub-resource must 404, not fall through to "register a participant".
150
+ if (sub)
151
+ return undefined;
152
+ if (method === 'POST') {
153
+ human('Registering a participant');
154
+ const body = await ctx.body();
155
+ if (typeof body?.agentId === 'string')
156
+ collabAgent(body.agentId);
157
+ const { participant: created, token } = this.hub.addParticipant(sessionId, body);
158
+ // The plaintext token is returned exactly once; only its hash is stored.
159
+ return ok({ participant: created, participantToken: token, briefing: this.hub.digest(created) }, 201);
160
+ }
161
+ if (method === 'GET')
162
+ return ok(isHuman
163
+ ? this.hub.participantsForHuman(sessionId)
164
+ : this.hub.store.listParticipants(sessionId).map(entry => ({ ...entry })));
165
+ }
166
+ if (method === 'POST' && tail === 'advance') {
167
+ human('Advancing a phase');
168
+ return ok(await this.hub.advance(sessionId, await ctx.body()));
169
+ }
170
+ if (method === 'POST' && tail === 'retry-waiting') {
171
+ human('Retrying waiting Agents');
172
+ return ok(this.hub.retryWaiting(sessionId, await ctx.body()));
173
+ }
174
+ if (method === 'POST' && tail === 'policy') {
175
+ human('Changing the policy');
176
+ return ok(this.hub.updatePolicy(sessionId, await ctx.body()));
177
+ }
178
+ if (method === 'POST' && tail === 'open-round') {
179
+ human('Opening a round');
180
+ return ok(await this.hub.openRound(sessionId));
181
+ }
182
+ if (method === 'POST' && tail === 'recheck') {
183
+ human('Reopening a finished review');
184
+ return ok(await this.hub.startRecheck(sessionId, await ctx.body()));
185
+ }
186
+ if (method === 'GET' && tail === 'events')
187
+ return ok(url.searchParams.has('tail')
188
+ ? this.hub.recentEvents(sessionId, number('tail', 200), participant)
189
+ : this.hub.events(sessionId, number('since', 0), number('limit', 500), participant));
190
+ if (method === 'GET' && tail === 'digest')
191
+ return ok(this.hub.digest(asParticipant()));
192
+ if (method === 'GET' && tail === 'issues' && !sub)
193
+ return ok(isHuman ? this.hub.store.listIssues(sessionId) : this.hub.listIssues(asParticipant()));
194
+ if (method === 'GET' && tail === 'issues' && sub)
195
+ return ok(this.hub.issueDetail(sessionId, sub, participant));
196
+ if (method === 'POST' && tail === 'issues' && parts[7] === 'withdraw')
197
+ return ok(await this.hub.withdrawIssue(asParticipant(), sub));
198
+ if (method === 'GET' && tail === 'review-consensus')
199
+ return ok(this.hub.reviewConsensus(sessionId, participant));
200
+ // Round history is a close-out view over every wave's findings; a participant token reading it during a live
201
+ // round would see the other reviewers' sealed issues, so it stays human-only like /report.
202
+ if (method === 'GET' && tail === 'review-rounds') {
203
+ human('Reading the round history');
204
+ return ok(this.hub.reviewRounds(sessionId));
205
+ }
206
+ if (method === 'POST' && tail === 'issue-votes')
207
+ return ok(await this.hub.submitIssueVotes(asParticipant(), await ctx.body()));
208
+ if (method === 'POST' && tail === 'merge-votes')
209
+ return ok(await this.hub.submitMergeVotes(asParticipant(), await ctx.body()));
210
+ if (method === 'POST' && tail === 'issue-discussions')
211
+ return ok(await this.hub.submitIssueDiscussions(asParticipant(), await ctx.body()));
212
+ if (method === 'POST' && tail === 'nominations')
213
+ return ok(await this.hub.submitNominations(asParticipant(), await ctx.body()), 201);
214
+ if (method === 'GET' && tail === 'criteria')
215
+ return ok(this.hub.criteria(sessionId, participant));
216
+ if (method === 'POST' && tail === 'votes')
217
+ return ok(await this.hub.submitVotes(asParticipant(), await ctx.body()));
218
+ if (method === 'GET' && tail === 'votes')
219
+ return ok(this.hub.votes(sessionId, participant));
220
+ if (method === 'POST' && tail === 'scores')
221
+ return ok(await this.hub.submitScores(asParticipant(), await ctx.body()));
222
+ if (method === 'GET' && tail === 'analysis')
223
+ return ok(this.hub.analysis(sessionId, participant));
224
+ if (method === 'GET' && tail === 'debates')
225
+ return ok(this.hub.store.listDebates(sessionId));
226
+ if (method === 'POST' && tail === 'debates' && parts[7] === 'arguments')
227
+ return ok(await this.hub.submitDebateArgument(asParticipant(), sub, await ctx.body()), 201);
228
+ if (method === 'POST' && tail === 'finalize') {
229
+ human('Finalizing a scoring session');
230
+ return ok(await this.hub.finalizeScoring(sessionId, await ctx.body()));
231
+ }
232
+ if (method === 'POST' && tail === 'ready')
233
+ return ok(await this.hub.markImplementationReady(asParticipant(), await ctx.body()));
234
+ if (method === 'POST' && tail === 'findings')
235
+ return ok(await this.hub.submitFindings(asParticipant(), await ctx.body()), 201);
236
+ if (method === 'POST' && tail === 'escalations')
237
+ return ok(await this.hub.raiseEscalation(asParticipant(), await ctx.body()), 202);
238
+ // The report is the human close-out view: it lists every issue and escalation, which would defeat blind
239
+ // collection if a participant token could read it while the round is still open.
240
+ if (method === 'GET' && tail === 'report') {
241
+ human('Reading the session report');
242
+ const reportSession = this.hub.getSession(sessionId);
243
+ return ok({
244
+ session: reportSession,
245
+ progress: this.hub.progress(sessionId),
246
+ issues: this.hub.store.listIssues(sessionId),
247
+ reviewSummary: reportSession.kind === 'review' ? this.hub.reviewSummary(sessionId) : undefined,
248
+ escalations: this.hub.listEscalations({ sessionId })
249
+ });
250
+ }
251
+ return undefined;
252
+ }
253
+ /** Every failure carries a stable code so an agent can branch on it instead of parsing prose. */
254
+ errorResult(error) {
255
+ if (error instanceof ValidationError)
256
+ return {
257
+ status: 422,
258
+ payload: { error: { code: error.code, message: error.message, fieldErrors: error.fieldErrors } }
259
+ };
260
+ const value = error, code = value?.code;
261
+ const status = Number.isInteger(value?.httpStatus)
262
+ ? value.httpStatus
263
+ : code === COLLAB_ERRORS.conflict ||
264
+ code === COLLAB_ERRORS.staleBaseline ||
265
+ code === COLLAB_ERRORS.wrongPhase ||
266
+ code === COLLAB_ERRORS.humanRulingFinal
267
+ ? 409
268
+ : code === COLLAB_ERRORS.budgetExhausted
269
+ ? 429
270
+ : code?.endsWith('NOT_FOUND')
271
+ ? 404
272
+ : 400;
273
+ const extra = code === COLLAB_ERRORS.staleBaseline && value.currentBaseline
274
+ ? { currentBaseline: value.currentBaseline }
275
+ : {};
276
+ if (status === 403 && !code)
277
+ return forbidden(String(value?.message ?? 'Forbidden'));
278
+ return {
279
+ status,
280
+ payload: {
281
+ error: { code: code ?? 'BAD_REQUEST', message: String(value?.message ?? 'Request failed'), ...extra }
282
+ }
283
+ };
284
+ }
285
+ }
@@ -0,0 +1,281 @@
1
+ import { CATEGORIES, DEBATE_STANCES, ESCALATION_KINDS, REGISTRABLE_ROLES, REQUIRED_ACTIONS, SEVERITIES, VOTE_STANCES } from './types.js';
2
+ import { anyJson, arr, bool, num, obj, oneOf, optional, record, refine, str, withDefault } from './validate.js';
3
+ /**
4
+ * Request schemas for every collaboration endpoint. They are deliberately strict:
5
+ * a rejected payload comes back with per-field codes so an agent can repair and retry
6
+ * instead of guessing what the hub wanted.
7
+ */
8
+ const clientRequestId = () => str({ min: 8, max: 200 });
9
+ const usage = () => optional(obj({
10
+ inputTokens: optional(num({ min: 0, max: 100_000_000, integer: true })),
11
+ outputTokens: optional(num({ min: 0, max: 100_000_000, integer: true })),
12
+ totalTokens: optional(num({ min: 0, max: 100_000_000, integer: true }))
13
+ }));
14
+ const codeLocation = () => obj({
15
+ path: str({ min: 1, max: 400 }),
16
+ startLine: optional(num({ integer: true, min: 1 })),
17
+ endLine: optional(num({ integer: true, min: 1 }))
18
+ });
19
+ /** Evidence must point at a file: an unanchored claim is indistinguishable from a hallucination. */
20
+ const evidenceItem = () => obj({
21
+ path: str({ min: 1, max: 400 }),
22
+ startLine: optional(num({ integer: true, min: 1 })),
23
+ endLine: optional(num({ integer: true, min: 1 })),
24
+ excerpt: optional(str({ max: 2000 }))
25
+ });
26
+ export const findingSchema = obj({
27
+ externalId: optional(str({ max: 100 })),
28
+ title: str({ min: 8, max: 200 }),
29
+ severity: oneOf(SEVERITIES),
30
+ category: oneOf(CATEGORIES),
31
+ requiredAction: withDefault(oneOf(REQUIRED_ACTIONS), () => 'should_fix'),
32
+ confidence: optional(num({ min: 0, max: 1 })),
33
+ location: codeLocation(),
34
+ evidence: str({ min: 10, max: 4000 }),
35
+ impact: optional(str({ max: 2000 })),
36
+ suggestion: optional(str({ max: 4000 })),
37
+ targetParticipantId: optional(str({ max: 100 }))
38
+ });
39
+ export const findingsRequest = obj({
40
+ clientRequestId: clientRequestId(),
41
+ baselineId: str({ min: 1, max: 100 }),
42
+ findings: arr(findingSchema, { max: 50 }),
43
+ /** On a later review round, the original reporter verifies each previously confirmed issue. */
44
+ rechecks: withDefault(arr(obj({
45
+ issueId: str({ min: 1, max: 100 }),
46
+ outcome: oneOf(['resolved', 'still_present']),
47
+ rationale: str({ min: 10, max: 4000 })
48
+ }), { max: 100 }), () => []),
49
+ reviewComplete: withDefault(bool(), () => false),
50
+ usage: usage()
51
+ });
52
+ /** Batched after blind collection: one request validates every issue this reviewer did not report. */
53
+ export const issueVotesRequest = obj({
54
+ clientRequestId: clientRequestId(),
55
+ votes: arr(obj({
56
+ issueId: str({ min: 1, max: 100 }),
57
+ stance: oneOf(['approve', 'reject']),
58
+ rationale: optional(str({ max: 4000 }))
59
+ }), { max: 200 }),
60
+ mergeProposals: withDefault(arr(obj({ issueIds: arr(str({ min: 1, max: 100 }), { min: 2, max: 20 }), rationale: str({ min: 10, max: 2000 }) }), { max: 50 }), () => []),
61
+ complete: withDefault(bool(), () => true),
62
+ usage: usage()
63
+ });
64
+ export const mergeVotesRequest = obj({
65
+ clientRequestId: clientRequestId(),
66
+ votes: arr(obj({
67
+ proposalId: str({ min: 1, max: 100 }),
68
+ stance: oneOf(['approve', 'reject']),
69
+ rationale: optional(str({ max: 4000 }))
70
+ }), { max: 100 }),
71
+ complete: withDefault(bool(), () => true),
72
+ usage: usage()
73
+ });
74
+ export const issueDiscussionsRequest = obj({
75
+ clientRequestId: clientRequestId(),
76
+ discussions: withDefault(arr(obj({
77
+ issueId: str({ min: 1, max: 100 }),
78
+ argument: str({ min: 20, max: 4000 }),
79
+ respondingTo: optional(str({ max: 100 }))
80
+ }), { max: 200 }), () => []),
81
+ /** The reporter's way out of a finding it no longer stands by, in the same call that defends the rest. */
82
+ withdrawals: withDefault(arr(obj({ issueId: str({ min: 1, max: 100 }), rationale: str({ min: 10, max: 4000 }) }), { max: 200 }), () => []),
83
+ complete: withDefault(bool(), () => true),
84
+ usage: usage()
85
+ });
86
+ export const escalationRequest = obj({
87
+ clientRequestId: clientRequestId(),
88
+ kind: oneOf(ESCALATION_KINDS),
89
+ refId: optional(str({ max: 100 })),
90
+ summary: str({ min: 20, max: 2000 }),
91
+ positions: withDefault(arr(obj({
92
+ participantId: str({ min: 1, max: 100 }),
93
+ stance: str({ max: 200 }),
94
+ rationale: str({ min: 10, max: 2000 })
95
+ }), { max: 20 }), () => []),
96
+ question: str({ min: 10, max: 1000 }),
97
+ options: withDefault(arr(str({ min: 1, max: 200 }), { max: 10 }), () => []),
98
+ urgency: withDefault(oneOf(['low', 'normal', 'high']), () => 'normal'),
99
+ usage: usage()
100
+ });
101
+ /**
102
+ * A scale is only usable as a whole: `{min:10,max:1,step:1}` passes every per-field range check and then
103
+ * produces a session in which every possible score fails validation. The span must be positive and
104
+ * at least one step wide.
105
+ */
106
+ const scoringScale = refine(obj({ min: num({ min: 0, max: 1000 }), max: num({ min: 1, max: 1000 }), step: num({ min: 0.01, max: 100 }) }), scale => {
107
+ const errors = [];
108
+ if (scale.max <= scale.min)
109
+ errors.push({
110
+ path: 'max',
111
+ code: 'OUT_OF_ORDER',
112
+ message: `Expected max (${scale.max}) to be greater than min (${scale.min})`,
113
+ expected: `> ${scale.min}`
114
+ });
115
+ else if (scale.step > scale.max - scale.min)
116
+ errors.push({
117
+ path: 'step',
118
+ code: 'TOO_LARGE',
119
+ message: `Expected a step that fits inside the ${scale.min}-${scale.max} range`,
120
+ expected: `<= ${scale.max - scale.min}`
121
+ });
122
+ return errors;
123
+ });
124
+ const scoringPolicyPatch = refine(obj({
125
+ minCriteria: optional(num({ integer: true, min: 1, max: 20 })),
126
+ maxCriteria: optional(num({ integer: true, min: 1, max: 20 })),
127
+ approvalThreshold: optional(num({ min: 0.5, max: 1 })),
128
+ maxVotingRounds: optional(num({ integer: true, min: 1, max: 10 })),
129
+ scale: optional(scoringScale),
130
+ convergenceRange: optional(num({ min: 0, max: 1000 })),
131
+ maxDebateRounds: optional(num({ integer: true, min: 0, max: 10 })),
132
+ blindScoring: optional(bool())
133
+ }), patch => patch.minCriteria !== undefined &&
134
+ patch.maxCriteria !== undefined &&
135
+ patch.minCriteria > patch.maxCriteria
136
+ ? [
137
+ {
138
+ path: 'maxCriteria',
139
+ code: 'OUT_OF_ORDER',
140
+ message: `Expected maxCriteria (${patch.maxCriteria}) to be at least minCriteria (${patch.minCriteria})`,
141
+ expected: `>= ${patch.minCriteria}`
142
+ }
143
+ ]
144
+ : undefined);
145
+ export const policyPatch = obj({
146
+ maxIssueRounds: optional(num({ integer: true, min: 1, max: 20 })),
147
+ maxTotalRounds: optional(num({ integer: true, min: 1, max: 50 })),
148
+ overdueWarningSec: optional(num({ integer: true, min: 60, max: 86_400 })),
149
+ autoEscalateOnDeadlock: optional(bool()),
150
+ blindFindings: optional(bool()),
151
+ consensusReview: optional(bool()),
152
+ maxConsensusRounds: optional(num({ integer: true, min: 1, max: 10 })),
153
+ implementationFirst: optional(bool()),
154
+ tokenBudgetPerParticipant: optional(num({ integer: true, min: 1000, max: 100_000_000 })),
155
+ scoring: optional(scoringPolicyPatch)
156
+ });
157
+ /** The implementer's "I am done, call the reviewers" signal. It is what turns build-then-review into one loop. */
158
+ export const readyRequest = obj({
159
+ clientRequestId: clientRequestId(),
160
+ summary: str({ min: 10, max: 4000 }),
161
+ changes: withDefault(arr(obj({ path: str({ min: 1, max: 400 }), summary: str({ min: 5, max: 1000 }) }), { max: 100 }), () => []),
162
+ codeRef: optional(obj({ commit: optional(str({ max: 100 })), dirtyHash: optional(str({ max: 200 })) })),
163
+ usage: usage()
164
+ });
165
+ /** Human-only restart of a finished review: review current code, or let selected developers fix first. */
166
+ export const recheckRequest = obj({
167
+ mode: oneOf(['review_only', 'fix_then_review']),
168
+ implementerParticipantIds: withDefault(arr(str({ min: 1, max: 100 }), { max: 20 }), () => [])
169
+ });
170
+ export const createSessionRequest = obj({
171
+ kind: oneOf(['review', 'scoring']),
172
+ title: str({ min: 3, max: 200 }),
173
+ workspaceId: str({ min: 1, max: 200 }),
174
+ relativeCwd: withDefault(str({ max: 400 }), () => '.'),
175
+ subject: obj({
176
+ type: oneOf(['diff', 'paths', 'commit_range', 'free']),
177
+ value: str({ min: 1, max: 2000 }),
178
+ notes: optional(str({ max: 4000 }))
179
+ }),
180
+ policy: optional(policyPatch)
181
+ });
182
+ export const createParticipantRequest = obj({
183
+ /** `human` is not registrable: a human uses the pairing code, not a seat the panel never waits for. */
184
+ role: oneOf(REGISTRABLE_ROLES),
185
+ displayName: str({ min: 1, max: 100 }),
186
+ model: optional(str({ max: 200 })),
187
+ /** The local pi2web agent that fills this seat. The hub wakes it; nothing polls the hub. */
188
+ agentId: str({ min: 1, max: 200 }),
189
+ tokenBudget: optional(num({ integer: true, min: 100, max: 100_000_000 }))
190
+ });
191
+ export const rebindParticipantRequest = obj({
192
+ /** Hands the seat to another local agent: the bound one died, or was picked by mistake. */
193
+ agentId: str({ min: 1, max: 200 }),
194
+ /** Filled by the human UI from the selected Agent's actual capabilities; never typed by the operator. */
195
+ model: optional(str({ max: 200 }))
196
+ });
197
+ /** Raising a spent budget is its own operation: an escalation ruling can only be used once. */
198
+ export const participantBudgetRequest = obj({
199
+ tokenBudget: num({ integer: true, min: 100, max: 100_000_000 })
200
+ });
201
+ export const advanceRequest = obj({
202
+ force: withDefault(bool(), () => false),
203
+ reason: withDefault(str({ max: 2000 }), () => '')
204
+ });
205
+ export const retryWaitingRequest = obj({
206
+ /** Optional UI-selected subset. Busy Agents are always skipped by the hub. */
207
+ participantIds: optional(arr(str({ min: 1, max: 100 }), { max: 100 }))
208
+ });
209
+ export const resolveEscalationRequest = obj({
210
+ decision: str({ min: 1, max: 2000 }),
211
+ rationale: str({ min: 10, max: 4000 }),
212
+ /** Optional ruling applied to the referenced issue. `reopen` hands it back to the responder. */
213
+ issueDecision: optional(oneOf(['resolved', 'wontfix', 'closed', 'reopen'])),
214
+ extra: optional(anyJson(16 * 1024))
215
+ });
216
+ // ---- scoring session payloads ----
217
+ export const nominationsRequest = obj({
218
+ clientRequestId: clientRequestId(),
219
+ nominations: arr(obj({
220
+ externalId: optional(str({ max: 100 })),
221
+ name: str({ min: 2, max: 80 }),
222
+ definition: str({ min: 20, max: 2000 }),
223
+ weightSuggestion: optional(num({ min: 0, max: 1 })),
224
+ anchors: optional(record(str({ min: 1, max: 400 }), { maxKeys: 20, maxKeyLength: 40 })),
225
+ rationale: optional(str({ max: 2000 }))
226
+ }), { max: 20 }),
227
+ nominationsComplete: withDefault(bool(), () => false),
228
+ usage: usage()
229
+ });
230
+ export const votesRequest = obj({
231
+ clientRequestId: clientRequestId(),
232
+ votes: arr(obj({
233
+ criterionId: str({ min: 1, max: 100 }),
234
+ stance: oneOf(VOTE_STANCES),
235
+ weight: optional(num({ min: 0, max: 1 })),
236
+ amendment: optional(str({ max: 1000 })),
237
+ rationale: optional(str({ max: 2000 }))
238
+ }), { min: 1, max: 40 }),
239
+ usage: usage()
240
+ });
241
+ /** Evidence is mandatory and each entry must point at a file: an unanchored score is a guess. */
242
+ export const scoresRequest = obj({
243
+ clientRequestId: clientRequestId(),
244
+ scores: arr(obj({
245
+ criterionId: str({ min: 1, max: 100 }),
246
+ score: num({ min: 0, max: 1000 }),
247
+ rationale: str({ min: 20, max: 4000 }),
248
+ evidence: arr(evidenceItem(), { min: 1, max: 20 }),
249
+ confidence: optional(num({ min: 0, max: 1 })),
250
+ changeReason: optional(str({ max: 2000 }))
251
+ }), { min: 1, max: 40 }),
252
+ usage: usage()
253
+ });
254
+ export const debateArgumentRequest = obj({
255
+ clientRequestId: clientRequestId(),
256
+ stance: oneOf(DEBATE_STANCES),
257
+ argument: str({ min: 20, max: 4000 }),
258
+ evidence: withDefault(arr(evidenceItem(), { max: 20 }), () => []),
259
+ respondingTo: optional(str({ max: 100 })),
260
+ usage: usage()
261
+ });
262
+ /** The Pi tool submits every debate position owed by one reviewer as one logical operation. */
263
+ export const debateArgumentsRequest = obj({
264
+ clientRequestId: clientRequestId(),
265
+ arguments: arr(obj({
266
+ debateId: str({ min: 1, max: 100 }),
267
+ stance: oneOf(DEBATE_STANCES),
268
+ argument: str({ min: 20, max: 4000 }),
269
+ evidence: withDefault(arr(evidenceItem(), { max: 20 }), () => []),
270
+ respondingTo: optional(str({ max: 100 }))
271
+ }), { min: 1, max: 40 }),
272
+ usage: usage()
273
+ });
274
+ export const finalizeRequest = obj({
275
+ /** Human overrides for criteria the panel could not settle, keyed by criterionId. */
276
+ rulings: withDefault(arr(obj({
277
+ criterionId: str({ min: 1, max: 100 }),
278
+ score: num({ min: 0, max: 1000 }),
279
+ rationale: str({ min: 10, max: 2000 })
280
+ }), { max: 40 }), () => [])
281
+ });