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.
- package/README.md +209 -18
- package/dist/activity.js +178 -0
- package/dist/agent-feed.js +133 -0
- package/dist/agent-templates.js +144 -0
- package/dist/agents.js +575 -97
- package/dist/auth.js +85 -20
- package/dist/collab/baseline.js +84 -0
- package/dist/collab/dispatcher.js +221 -0
- package/dist/collab/hub.js +2888 -0
- package/dist/collab/pi-extension.js +494 -0
- package/dist/collab/review-flow.js +459 -0
- package/dist/collab/routes.js +285 -0
- package/dist/collab/schemas.js +281 -0
- package/dist/collab/scoring-flow.js +336 -0
- package/dist/collab/similarity.js +15 -0
- package/dist/collab/store.js +918 -0
- package/dist/collab/types.js +137 -0
- package/dist/collab/validate.js +185 -0
- package/dist/drafts.js +50 -0
- package/dist/http/agent-routes.js +196 -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 +43 -0
- package/dist/http/web-assets.js +63 -0
- package/dist/http/workspace-routes.js +58 -0
- package/dist/mail-notifier.js +95 -21
- package/dist/main.js +141 -17
- package/dist/mcp.js +132 -0
- package/dist/metadata-store.js +199 -56
- package/dist/paths.js +10 -0
- package/dist/sdk-backend.js +288 -49
- package/dist/server.js +558 -358
- package/dist/session-namer.js +163 -0
- package/dist/terminals.js +94 -27
- package/dist/timeline.js +185 -0
- package/dist/voice.js +196 -40
- 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 +18 -6
- package/packages/edge-tts-server/src/remote_pi_edge_tts/__pycache__/__init__.cpython-312.pyc +0 -0
- package/packages/edge-tts-server/src/remote_pi_edge_tts/__pycache__/__main__.cpython-312.pyc +0 -0
- package/packages/protocol/openapi.yaml +8 -2
- package/packages/protocol/websocket.schema.json +94 -10
- package/web/activity-view.js +121 -0
- package/web/app.js +3770 -404
- package/web/backend-state.js +269 -0
- package/web/collab.html +95 -0
- package/web/collab.js +1080 -0
- package/web/hid-keyboard.js +453 -0
- package/web/index.html +14 -9
- package/web/styles.css +73 -0
- package/ANDROID_APP_FEATURES.md +0 -256
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { StringEnum } from '@earendil-works/pi-ai';
|
|
3
|
+
import { Type } from 'typebox';
|
|
4
|
+
export const COLLAB_ACTIONS = [
|
|
5
|
+
'ready',
|
|
6
|
+
'findings',
|
|
7
|
+
'issue_votes',
|
|
8
|
+
'merge_votes',
|
|
9
|
+
'issue_discussions',
|
|
10
|
+
'nominations',
|
|
11
|
+
'votes',
|
|
12
|
+
'scores',
|
|
13
|
+
'debate_argument',
|
|
14
|
+
'debate_arguments',
|
|
15
|
+
'escalate',
|
|
16
|
+
'withdraw'
|
|
17
|
+
];
|
|
18
|
+
const evidence = Type.Object({
|
|
19
|
+
path: Type.String({ minLength: 1, description: 'Workspace-relative evidence file' }),
|
|
20
|
+
startLine: Type.Optional(Type.Number({ minimum: 1 })),
|
|
21
|
+
endLine: Type.Optional(Type.Number({ minimum: 1 })),
|
|
22
|
+
excerpt: Type.Optional(Type.String({ maxLength: 2000 }))
|
|
23
|
+
}, { additionalProperties: false });
|
|
24
|
+
const location = Type.Object({
|
|
25
|
+
path: Type.String({ minLength: 1, description: 'Workspace-relative file containing the finding' }),
|
|
26
|
+
startLine: Type.Optional(Type.Number({ minimum: 1 })),
|
|
27
|
+
endLine: Type.Optional(Type.Number({ minimum: 1 }))
|
|
28
|
+
}, { additionalProperties: false });
|
|
29
|
+
const finding = Type.Object({
|
|
30
|
+
externalId: Type.Optional(Type.String({ maxLength: 100 })),
|
|
31
|
+
title: Type.String({ minLength: 8, maxLength: 200 }),
|
|
32
|
+
severity: StringEnum(['blocker', 'critical', 'major', 'minor', 'nit']),
|
|
33
|
+
category: StringEnum([
|
|
34
|
+
'security',
|
|
35
|
+
'correctness',
|
|
36
|
+
'performance',
|
|
37
|
+
'maintainability',
|
|
38
|
+
'style',
|
|
39
|
+
'test',
|
|
40
|
+
'docs',
|
|
41
|
+
'process'
|
|
42
|
+
]),
|
|
43
|
+
requiredAction: Type.Optional(StringEnum(['must_fix', 'should_fix', 'discuss', 'fyi'])),
|
|
44
|
+
confidence: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
|
|
45
|
+
location,
|
|
46
|
+
evidence: Type.String({ minLength: 10, maxLength: 4000 }),
|
|
47
|
+
impact: Type.Optional(Type.String({ maxLength: 2000 })),
|
|
48
|
+
suggestion: Type.Optional(Type.String({ maxLength: 4000 }))
|
|
49
|
+
}, { additionalProperties: false });
|
|
50
|
+
const TOOL_FOR_TASK = {
|
|
51
|
+
implement: 'collab_submit_ready',
|
|
52
|
+
file_findings: 'collab_submit_findings',
|
|
53
|
+
file_findings_optional: 'collab_submit_findings',
|
|
54
|
+
validate_issues: 'collab_submit_issue_votes',
|
|
55
|
+
vote_on_merges: 'collab_submit_merge_votes',
|
|
56
|
+
defend_approved_issues: 'collab_submit_issue_discussions',
|
|
57
|
+
reconsider_issue_votes: 'collab_submit_issue_votes',
|
|
58
|
+
nominate_criteria: 'collab_submit_nominations',
|
|
59
|
+
vote_on_criteria: 'collab_submit_votes',
|
|
60
|
+
score_rubric: 'collab_submit_scores',
|
|
61
|
+
rescore_contested: 'collab_submit_scores',
|
|
62
|
+
debate_contested_scores: 'collab_submit_debate_arguments'
|
|
63
|
+
};
|
|
64
|
+
const TASK_FOR_ACTION = {
|
|
65
|
+
ready: ['implement'],
|
|
66
|
+
findings: ['file_findings', 'file_findings_optional'],
|
|
67
|
+
issue_votes: ['validate_issues', 'reconsider_issue_votes'],
|
|
68
|
+
merge_votes: ['vote_on_merges'],
|
|
69
|
+
issue_discussions: ['defend_approved_issues'],
|
|
70
|
+
nominations: ['nominate_criteria'],
|
|
71
|
+
votes: ['vote_on_criteria'],
|
|
72
|
+
scores: ['score_rubric', 'rescore_contested'],
|
|
73
|
+
debate_arguments: ['debate_contested_scores']
|
|
74
|
+
};
|
|
75
|
+
const SUBMIT_TOOLS = [...new Set(Object.values(TOOL_FOR_TASK))];
|
|
76
|
+
const OPTIONAL_TOOLS = ['collab_escalate', 'collab_withdraw_issue'];
|
|
77
|
+
/** Last-resort scrub for text the hub did not write for the tool flow (mostly error strings). */
|
|
78
|
+
const scrub = (text) => text
|
|
79
|
+
.replace(/(?:POST|GET|PUT|DELETE)s?\s+(?:to\s+)?\S*\/api\/v1\/\S+/gi, 'use the active collaboration submission tool')
|
|
80
|
+
.replace(/\S*\/api\/v1\/\S+/gi, 'the collaboration tool')
|
|
81
|
+
.replace(/[0-9a-f]{8}-[0-9a-f-]{16,}/gi, '[internal reference]');
|
|
82
|
+
/** Inline-only Pi extension. It is loaded solely for profile=collab sessions. */
|
|
83
|
+
export function createCollabExtension(agentId, bridge) {
|
|
84
|
+
return function collabExtension(pi) {
|
|
85
|
+
let aliases = new Map(), lastTask, baseTools;
|
|
86
|
+
const reference = (id, kind) => {
|
|
87
|
+
if (!id)
|
|
88
|
+
return id;
|
|
89
|
+
const existing = aliases.get(id);
|
|
90
|
+
if (existing)
|
|
91
|
+
return existing;
|
|
92
|
+
const prefix = kind === 'issue'
|
|
93
|
+
? 'issue'
|
|
94
|
+
: kind === 'criterion'
|
|
95
|
+
? 'criterion'
|
|
96
|
+
: kind === 'proposal'
|
|
97
|
+
? 'proposal'
|
|
98
|
+
: kind === 'debate'
|
|
99
|
+
? 'debate'
|
|
100
|
+
: kind === 'baseline'
|
|
101
|
+
? 'base'
|
|
102
|
+
: kind === 'participant'
|
|
103
|
+
? 'reviewer'
|
|
104
|
+
: 'ref';
|
|
105
|
+
const value = `${prefix}-${[...aliases.values()].filter(entry => entry.startsWith(`${prefix}-`)).length + 1}`;
|
|
106
|
+
aliases.set(id, value);
|
|
107
|
+
return value;
|
|
108
|
+
};
|
|
109
|
+
const redact = (value) => {
|
|
110
|
+
if (Array.isArray(value))
|
|
111
|
+
return value.map(redact);
|
|
112
|
+
if (!value || typeof value !== 'object')
|
|
113
|
+
return typeof value === 'string' && aliases.has(value) ? aliases.get(value) : value;
|
|
114
|
+
const output = {};
|
|
115
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
116
|
+
if (name === 'sessionId')
|
|
117
|
+
continue;
|
|
118
|
+
const ids = typeof entry === 'string'
|
|
119
|
+
? [entry]
|
|
120
|
+
: Array.isArray(entry) && entry.every(item => typeof item === 'string')
|
|
121
|
+
? entry
|
|
122
|
+
: undefined;
|
|
123
|
+
if (ids && /(Id|Ids)$/.test(name)) {
|
|
124
|
+
const kind = /issue/i.test(name)
|
|
125
|
+
? 'issue'
|
|
126
|
+
: /criterion/i.test(name)
|
|
127
|
+
? 'criterion'
|
|
128
|
+
: /proposal/i.test(name)
|
|
129
|
+
? 'proposal'
|
|
130
|
+
: /debate/i.test(name)
|
|
131
|
+
? 'debate'
|
|
132
|
+
: /baseline/i.test(name)
|
|
133
|
+
? 'baseline'
|
|
134
|
+
: /participant|author|reporter|target|raisedBy/i.test(name)
|
|
135
|
+
? 'participant'
|
|
136
|
+
: 'ref';
|
|
137
|
+
output[name.replace(/Id(s)?$/, 'Ref$1')] =
|
|
138
|
+
typeof entry === 'string' ? reference(entry, kind) : ids.map(item => reference(item, kind));
|
|
139
|
+
}
|
|
140
|
+
else
|
|
141
|
+
output[name] = redact(entry);
|
|
142
|
+
}
|
|
143
|
+
return output;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Aliases are assigned before the walk, not during it, for two reasons. Seat refs must line up with the panel
|
|
147
|
+
* (so "reviewer-2" means the same agent in every turn and in every argument the model writes), and any id the
|
|
148
|
+
* walk does not recognise as an id-shaped key - `progress.waitingOn` is a plain string array - would otherwise
|
|
149
|
+
* reach the model as a raw hub uuid. Findings borrow the board's own numbering so a ref survives a new turn.
|
|
150
|
+
*/
|
|
151
|
+
const seed = (task) => {
|
|
152
|
+
const counters = new Map();
|
|
153
|
+
const claim = (id, alias) => {
|
|
154
|
+
if (typeof id === 'string' && id && !aliases.has(id))
|
|
155
|
+
aliases.set(id, alias);
|
|
156
|
+
};
|
|
157
|
+
for (const seat of Array.isArray(task.panel) ? task.panel : []) {
|
|
158
|
+
const role = typeof seat.role === 'string' ? seat.role : 'participant', next = (counters.get(role) ?? 0) + 1;
|
|
159
|
+
counters.set(role, next);
|
|
160
|
+
claim(seat.participantId, `${role}-${next}`);
|
|
161
|
+
}
|
|
162
|
+
const walk = (value) => {
|
|
163
|
+
if (Array.isArray(value))
|
|
164
|
+
return value.forEach(walk);
|
|
165
|
+
if (!value || typeof value !== 'object')
|
|
166
|
+
return;
|
|
167
|
+
const entry = value;
|
|
168
|
+
if (typeof entry.issueId === 'string' && typeof entry.number === 'number')
|
|
169
|
+
claim(entry.issueId, `issue-${entry.number}`);
|
|
170
|
+
for (const nested of Object.values(entry))
|
|
171
|
+
walk(nested);
|
|
172
|
+
};
|
|
173
|
+
walk(task);
|
|
174
|
+
};
|
|
175
|
+
/** Hub errors name findings by hub id; the model only knows aliases, so map before falling back to scrubbing. */
|
|
176
|
+
const explain = (text) => {
|
|
177
|
+
let output = text;
|
|
178
|
+
for (const [id, alias] of aliases)
|
|
179
|
+
output = output.split(id).join(alias);
|
|
180
|
+
return scrub(output);
|
|
181
|
+
};
|
|
182
|
+
const restore = (value) => {
|
|
183
|
+
if (Array.isArray(value))
|
|
184
|
+
return value.map(restore);
|
|
185
|
+
if (!value || typeof value !== 'object') {
|
|
186
|
+
if (typeof value !== 'string')
|
|
187
|
+
return value;
|
|
188
|
+
for (const [id, alias] of aliases)
|
|
189
|
+
if (alias === value)
|
|
190
|
+
return id;
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
// Only alias-carrying keys are translated back to hub ids. `codeRef` is a semantic field whose value is an
|
|
194
|
+
// object (commit/dirtyHash), so renaming it to `codeId` would make the hub reject a valid ready submission.
|
|
195
|
+
const isAliasKey = (key, entry) => /Ref(s)?$/.test(key) &&
|
|
196
|
+
(typeof entry === 'string' ||
|
|
197
|
+
(Array.isArray(entry) && entry.every(item => typeof item === 'string')));
|
|
198
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
199
|
+
isAliasKey(key, entry) ? key.replace(/Ref(s)?$/, 'Id$1') : key,
|
|
200
|
+
restore(entry)
|
|
201
|
+
]));
|
|
202
|
+
};
|
|
203
|
+
const taskText = (task) => {
|
|
204
|
+
const safe = redact(task), context = { ...safe };
|
|
205
|
+
delete context.task;
|
|
206
|
+
delete context.instructions;
|
|
207
|
+
const lines = ['Local code collaboration task.'];
|
|
208
|
+
if (safe.task)
|
|
209
|
+
lines.push(`Current action: ${safe.task}.`);
|
|
210
|
+
if (safe.phase)
|
|
211
|
+
lines.push(`Phase: ${safe.phase}${safe.round !== undefined ? `, round ${safe.round}` : ''}.`);
|
|
212
|
+
const subject = safe.subject;
|
|
213
|
+
if (typeof subject?.value === 'string')
|
|
214
|
+
lines.push(`User request: ${subject.value}`);
|
|
215
|
+
if (typeof subject?.notes === 'string')
|
|
216
|
+
lines.push(`User notes: ${subject.notes}`);
|
|
217
|
+
if (safe.task !== 'implement')
|
|
218
|
+
lines.push('Workspace constraint: this task is non-mutating. Do not create, delete, rename, or modify files under the working directory, including scratch files, redirected command output, generated files, caches, or formatter changes. Send inspection output to stdout; use an OS temporary directory outside the workspace only when unavoidable.');
|
|
219
|
+
if (typeof safe.instructions === 'string')
|
|
220
|
+
lines.push(`Instructions: ${scrub(safe.instructions)}`);
|
|
221
|
+
if (Object.keys(context).length)
|
|
222
|
+
lines.push(`Task context:\n${JSON.stringify(context, null, 2)}`);
|
|
223
|
+
const submit = TOOL_FOR_TASK[String(safe.task ?? '')];
|
|
224
|
+
lines.push(submit
|
|
225
|
+
? `Complete every required Ref with ${submit}, then end the turn.`
|
|
226
|
+
: 'No submission is required; end the turn.');
|
|
227
|
+
return lines.join('\n');
|
|
228
|
+
};
|
|
229
|
+
const currentTask = () => String(lastTask?.task ?? '');
|
|
230
|
+
const normalize = (action, payload) => {
|
|
231
|
+
const body = restore(payload);
|
|
232
|
+
if (action === 'findings') {
|
|
233
|
+
if (!body.baselineId &&
|
|
234
|
+
lastTask?.baseline &&
|
|
235
|
+
typeof lastTask.baseline.baselineId === 'string')
|
|
236
|
+
body.baselineId = lastTask.baseline.baselineId;
|
|
237
|
+
body.reviewComplete = true;
|
|
238
|
+
}
|
|
239
|
+
if (action === 'nominations')
|
|
240
|
+
body.nominationsComplete = true;
|
|
241
|
+
if (action !== 'withdraw')
|
|
242
|
+
body.clientRequestId = randomUUID();
|
|
243
|
+
return body;
|
|
244
|
+
};
|
|
245
|
+
const rememberBaseTools = () => {
|
|
246
|
+
if (!baseTools)
|
|
247
|
+
baseTools = pi
|
|
248
|
+
.getActiveTools()
|
|
249
|
+
.filter(name => !SUBMIT_TOOLS.includes(name) && !OPTIONAL_TOOLS.includes(name) && name !== 'collab_get_task');
|
|
250
|
+
};
|
|
251
|
+
const activate = (task) => {
|
|
252
|
+
rememberBaseTools();
|
|
253
|
+
const taskName = String(task?.task ?? '');
|
|
254
|
+
// Tool names are not an integrity boundary: bash can write just as readily as edit/write, while edit/write
|
|
255
|
+
// may still be useful for scratch work outside the reviewed checkout. Keep the normal tools available,
|
|
256
|
+
// state the non-mutation contract explicitly, and let the baseline comparison enforce workspace identity.
|
|
257
|
+
const submit = TOOL_FOR_TASK[taskName], optional = taskName && taskName !== 'wait'
|
|
258
|
+
? [
|
|
259
|
+
'collab_escalate',
|
|
260
|
+
...(taskName.includes('issue') || taskName === 'validate_issues'
|
|
261
|
+
? ['collab_withdraw_issue']
|
|
262
|
+
: [])
|
|
263
|
+
]
|
|
264
|
+
: [];
|
|
265
|
+
pi.setActiveTools([
|
|
266
|
+
...new Set([...baseTools, 'collab_get_task', ...(submit ? [submit] : []), ...optional])
|
|
267
|
+
]);
|
|
268
|
+
};
|
|
269
|
+
const assertTask = (action) => {
|
|
270
|
+
const allowed = TASK_FOR_ACTION[action];
|
|
271
|
+
if (allowed && !allowed.includes(currentTask()))
|
|
272
|
+
throw new Error(`The active task is ${currentTask() || 'unknown'}, which cannot submit ${action}. Call collab_get_task again.`);
|
|
273
|
+
};
|
|
274
|
+
const acceptedText = (result) => `Collaboration submission accepted.${result && typeof result === 'object' ? ` ${JSON.stringify(redact(result))}` : ''}`;
|
|
275
|
+
const registerSubmit = (definition) => pi.registerTool({
|
|
276
|
+
name: definition.name,
|
|
277
|
+
label: definition.label,
|
|
278
|
+
description: definition.description,
|
|
279
|
+
promptSnippet: definition.description,
|
|
280
|
+
promptGuidelines: [
|
|
281
|
+
`Use ${definition.name} only after collab_get_task requests it, and include every required Ref exactly once.`
|
|
282
|
+
],
|
|
283
|
+
parameters: definition.parameters,
|
|
284
|
+
async execute(_id, input) {
|
|
285
|
+
try {
|
|
286
|
+
assertTask(definition.action);
|
|
287
|
+
const result = await bridge.submit(agentId, definition.action, normalize(definition.action, input));
|
|
288
|
+
return {
|
|
289
|
+
content: [{ type: 'text', text: acceptedText(result) }],
|
|
290
|
+
details: { result: redact(result) },
|
|
291
|
+
terminate: definition.terminate ?? true
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
const fields = error.fieldErrors;
|
|
296
|
+
throw new Error(explain(`${error.message ?? 'Submission failed'}${fields ? ` ${JSON.stringify(fields)}` : ''}`));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
pi.on('session_start', () => {
|
|
301
|
+
baseTools = undefined;
|
|
302
|
+
lastTask = undefined;
|
|
303
|
+
aliases = new Map();
|
|
304
|
+
activate();
|
|
305
|
+
});
|
|
306
|
+
pi.on('before_agent_start', event => {
|
|
307
|
+
lastTask = undefined;
|
|
308
|
+
aliases = new Map();
|
|
309
|
+
activate();
|
|
310
|
+
return {
|
|
311
|
+
systemPrompt: `${event.systemPrompt}\n\n## Local collaboration\nThis is a local software-development workflow. Call collab_get_task first after every collaboration wake-up. Use only the collaboration submission tool activated for that task. Review and scoring are strictly non-mutating: bash may inspect with commands such as git diff/log, but must not write anywhere under the working directory—not through redirection, tee, file-management commands, formatters, generators, package installation, build artifacts, caches, or scratch files. Send command output to stdout, or if temporary storage is unavoidable use the OS temporary directory outside the workspace. Only an implement task may modify the workspace. The tools own transport and credentials: never use curl, invent endpoints or identifiers, sleep, or poll.`
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
pi.registerTool({
|
|
315
|
+
name: 'collab_get_task',
|
|
316
|
+
label: 'Get Collaboration Task',
|
|
317
|
+
description: 'Get the complete current collaboration assignment and activate its exact typed submission tool.',
|
|
318
|
+
promptSnippet: 'Fetch the current collaboration assignment before doing any collaboration work',
|
|
319
|
+
promptGuidelines: ['Call collab_get_task first after every pi2web collaboration wake-up.'],
|
|
320
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
321
|
+
async execute() {
|
|
322
|
+
aliases = new Map();
|
|
323
|
+
lastTask = await bridge.getTask(agentId);
|
|
324
|
+
seed(lastTask);
|
|
325
|
+
activate(lastTask);
|
|
326
|
+
return { content: [{ type: 'text', text: taskText(lastTask) }], details: { task: redact(lastTask) } };
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
registerSubmit({
|
|
330
|
+
name: 'collab_submit_ready',
|
|
331
|
+
label: 'Submit Implementation',
|
|
332
|
+
action: 'ready',
|
|
333
|
+
description: 'Declare the assigned implementation complete with an auditable change summary.',
|
|
334
|
+
parameters: Type.Object({
|
|
335
|
+
summary: Type.String({ minLength: 10, maxLength: 4000 }),
|
|
336
|
+
changes: Type.Array(Type.Object({ path: Type.String({ minLength: 1 }), summary: Type.String({ minLength: 5 }) }, { additionalProperties: false }), { maxItems: 100 }),
|
|
337
|
+
codeRef: Type.Optional(Type.Object({ commit: Type.Optional(Type.String()), dirtyHash: Type.Optional(Type.String()) }, { additionalProperties: false }))
|
|
338
|
+
}, { additionalProperties: false })
|
|
339
|
+
});
|
|
340
|
+
registerSubmit({
|
|
341
|
+
name: 'collab_submit_findings',
|
|
342
|
+
label: 'Submit Review Findings',
|
|
343
|
+
action: 'findings',
|
|
344
|
+
description: 'Submit the complete blind review, including every required recheck and all new evidence-backed findings.',
|
|
345
|
+
parameters: Type.Object({
|
|
346
|
+
findings: Type.Array(finding, { maxItems: 50 }),
|
|
347
|
+
rechecks: Type.Optional(Type.Array(Type.Object({
|
|
348
|
+
issueRef: Type.String(),
|
|
349
|
+
outcome: StringEnum(['resolved', 'still_present']),
|
|
350
|
+
rationale: Type.String({ minLength: 10, maxLength: 4000 })
|
|
351
|
+
}, { additionalProperties: false }), { maxItems: 100 }))
|
|
352
|
+
}, { additionalProperties: false })
|
|
353
|
+
});
|
|
354
|
+
registerSubmit({
|
|
355
|
+
name: 'collab_submit_issue_votes',
|
|
356
|
+
label: 'Submit Issue Votes',
|
|
357
|
+
action: 'issue_votes',
|
|
358
|
+
description: 'Vote on every required issue and optionally propose duplicate groups.',
|
|
359
|
+
parameters: Type.Object({
|
|
360
|
+
votes: Type.Array(Type.Object({
|
|
361
|
+
issueRef: Type.String(),
|
|
362
|
+
stance: StringEnum(['approve', 'reject']),
|
|
363
|
+
rationale: Type.Optional(Type.String({ maxLength: 4000 }))
|
|
364
|
+
}, { additionalProperties: false }), {
|
|
365
|
+
maxItems: 200,
|
|
366
|
+
description: 'One vote per Ref in yourRequiredIssueIds; an empty array is only accepted when you owe none'
|
|
367
|
+
}),
|
|
368
|
+
mergeProposals: Type.Optional(Type.Array(Type.Object({
|
|
369
|
+
issueRefs: Type.Array(Type.String(), { minItems: 2, maxItems: 20 }),
|
|
370
|
+
rationale: Type.String({ minLength: 10, maxLength: 2000 })
|
|
371
|
+
}, { additionalProperties: false }), { maxItems: 50 }))
|
|
372
|
+
}, { additionalProperties: false })
|
|
373
|
+
});
|
|
374
|
+
registerSubmit({
|
|
375
|
+
name: 'collab_submit_merge_votes',
|
|
376
|
+
label: 'Submit Merge Votes',
|
|
377
|
+
action: 'merge_votes',
|
|
378
|
+
description: 'Vote on every required duplicate-merge proposal.',
|
|
379
|
+
parameters: Type.Object({
|
|
380
|
+
votes: Type.Array(Type.Object({
|
|
381
|
+
proposalRef: Type.String(),
|
|
382
|
+
stance: StringEnum(['approve', 'reject']),
|
|
383
|
+
rationale: Type.Optional(Type.String({ maxLength: 4000 }))
|
|
384
|
+
}, { additionalProperties: false }), { minItems: 1, maxItems: 100 })
|
|
385
|
+
}, { additionalProperties: false })
|
|
386
|
+
});
|
|
387
|
+
registerSubmit({
|
|
388
|
+
name: 'collab_submit_issue_discussions',
|
|
389
|
+
label: 'Submit Issue Arguments',
|
|
390
|
+
action: 'issue_discussions',
|
|
391
|
+
description: 'Defend every required finding with evidence, and withdraw the ones you no longer stand by.',
|
|
392
|
+
parameters: Type.Object({
|
|
393
|
+
discussions: Type.Optional(Type.Array(Type.Object({
|
|
394
|
+
issueRef: Type.String(),
|
|
395
|
+
argument: Type.String({ minLength: 20, maxLength: 4000 }),
|
|
396
|
+
respondingTo: Type.Optional(Type.String())
|
|
397
|
+
}, { additionalProperties: false }), { maxItems: 200 })),
|
|
398
|
+
withdrawals: Type.Optional(Type.Array(Type.Object({
|
|
399
|
+
issueRef: Type.String(),
|
|
400
|
+
rationale: Type.String({
|
|
401
|
+
minLength: 10,
|
|
402
|
+
maxLength: 4000,
|
|
403
|
+
description: 'Why you no longer stand by your own finding'
|
|
404
|
+
})
|
|
405
|
+
}, { additionalProperties: false }), { maxItems: 200 }))
|
|
406
|
+
}, { additionalProperties: false })
|
|
407
|
+
});
|
|
408
|
+
registerSubmit({
|
|
409
|
+
name: 'collab_submit_nominations',
|
|
410
|
+
label: 'Submit Criteria Nominations',
|
|
411
|
+
action: 'nominations',
|
|
412
|
+
description: 'Submit the complete independent set of proposed scoring criteria.',
|
|
413
|
+
parameters: Type.Object({
|
|
414
|
+
nominations: Type.Array(Type.Object({
|
|
415
|
+
externalId: Type.Optional(Type.String()),
|
|
416
|
+
name: Type.String({ minLength: 2, maxLength: 80 }),
|
|
417
|
+
definition: Type.String({ minLength: 20, maxLength: 2000 }),
|
|
418
|
+
weightSuggestion: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
|
|
419
|
+
anchors: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
420
|
+
rationale: Type.Optional(Type.String({ maxLength: 2000 }))
|
|
421
|
+
}, { additionalProperties: false }), { maxItems: 20 })
|
|
422
|
+
}, { additionalProperties: false })
|
|
423
|
+
});
|
|
424
|
+
registerSubmit({
|
|
425
|
+
name: 'collab_submit_votes',
|
|
426
|
+
label: 'Submit Criteria Votes',
|
|
427
|
+
action: 'votes',
|
|
428
|
+
description: 'Vote exactly once on every required rubric candidate.',
|
|
429
|
+
parameters: Type.Object({
|
|
430
|
+
votes: Type.Array(Type.Object({
|
|
431
|
+
criterionRef: Type.String(),
|
|
432
|
+
stance: StringEnum(['approve', 'reject', 'abstain']),
|
|
433
|
+
weight: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
|
|
434
|
+
amendment: Type.Optional(Type.String({ maxLength: 1000 })),
|
|
435
|
+
rationale: Type.Optional(Type.String({ maxLength: 2000 }))
|
|
436
|
+
}, { additionalProperties: false }), { minItems: 1, maxItems: 40 })
|
|
437
|
+
}, { additionalProperties: false })
|
|
438
|
+
});
|
|
439
|
+
registerSubmit({
|
|
440
|
+
name: 'collab_submit_scores',
|
|
441
|
+
label: 'Submit Rubric Scores',
|
|
442
|
+
action: 'scores',
|
|
443
|
+
description: 'Score exactly once every required criterion with rationale and file evidence.',
|
|
444
|
+
parameters: Type.Object({
|
|
445
|
+
scores: Type.Array(Type.Object({
|
|
446
|
+
criterionRef: Type.String(),
|
|
447
|
+
score: Type.Number(),
|
|
448
|
+
rationale: Type.String({ minLength: 20, maxLength: 4000 }),
|
|
449
|
+
evidence: Type.Array(evidence, { minItems: 1, maxItems: 20 }),
|
|
450
|
+
confidence: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
|
|
451
|
+
changeReason: Type.Optional(Type.String({ maxLength: 2000 }))
|
|
452
|
+
}, { additionalProperties: false }), { minItems: 1, maxItems: 40 })
|
|
453
|
+
}, { additionalProperties: false })
|
|
454
|
+
});
|
|
455
|
+
registerSubmit({
|
|
456
|
+
name: 'collab_submit_debate_arguments',
|
|
457
|
+
label: 'Submit Score Debate',
|
|
458
|
+
action: 'debate_arguments',
|
|
459
|
+
description: 'Submit one evidence-backed position for every required contested-criterion debate.',
|
|
460
|
+
parameters: Type.Object({
|
|
461
|
+
arguments: Type.Array(Type.Object({
|
|
462
|
+
debateRef: Type.String(),
|
|
463
|
+
stance: StringEnum(['raise', 'lower', 'hold']),
|
|
464
|
+
argument: Type.String({ minLength: 20, maxLength: 4000 }),
|
|
465
|
+
evidence: Type.Optional(Type.Array(evidence, { maxItems: 20 })),
|
|
466
|
+
respondingTo: Type.Optional(Type.String())
|
|
467
|
+
}, { additionalProperties: false }), { minItems: 1, maxItems: 40 })
|
|
468
|
+
}, { additionalProperties: false })
|
|
469
|
+
});
|
|
470
|
+
registerSubmit({
|
|
471
|
+
name: 'collab_escalate',
|
|
472
|
+
label: 'Escalate Collaboration',
|
|
473
|
+
action: 'escalate',
|
|
474
|
+
description: 'Escalate a genuine blocker or unresolved judgment to the human operator.',
|
|
475
|
+
terminate: false,
|
|
476
|
+
parameters: Type.Object({
|
|
477
|
+
kind: StringEnum(['issue_dispute', 'rubric_dispute', 'score_dispute', 'other']),
|
|
478
|
+
refRef: Type.Optional(Type.String()),
|
|
479
|
+
summary: Type.String({ minLength: 20, maxLength: 2000 }),
|
|
480
|
+
question: Type.String({ minLength: 10, maxLength: 1000 }),
|
|
481
|
+
options: Type.Optional(Type.Array(Type.String(), { maxItems: 10 })),
|
|
482
|
+
urgency: Type.Optional(StringEnum(['low', 'normal', 'high']))
|
|
483
|
+
}, { additionalProperties: false })
|
|
484
|
+
});
|
|
485
|
+
registerSubmit({
|
|
486
|
+
name: 'collab_withdraw_issue',
|
|
487
|
+
label: 'Withdraw Issue',
|
|
488
|
+
action: 'withdraw',
|
|
489
|
+
description: 'Withdraw one of your own findings when you no longer stand by it. Does not end your turn.',
|
|
490
|
+
terminate: false,
|
|
491
|
+
parameters: Type.Object({ issueRef: Type.String() }, { additionalProperties: false })
|
|
492
|
+
});
|
|
493
|
+
};
|
|
494
|
+
}
|