@prplkane/roundtable 0.0.0-stage → 0.6.4

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 (70) hide show
  1. package/CHANGELOG.md +529 -0
  2. package/LICENSE +21 -0
  3. package/README.md +189 -3
  4. package/SECURITY.md +299 -0
  5. package/docs/workflow.md +1344 -0
  6. package/package.json +52 -6
  7. package/scripts/agent.mjs +40 -0
  8. package/scripts/export.mjs +20 -0
  9. package/scripts/mcp.mjs +175 -0
  10. package/scripts/run-debate.mjs +70 -0
  11. package/scripts/server.mjs +28 -0
  12. package/scripts/setup.mjs +182 -0
  13. package/skills/roundtable/SKILL.md +57 -0
  14. package/src/adapters/agy.js +7 -0
  15. package/src/adapters/catalogue.js +49 -0
  16. package/src/adapters/claude.js +6 -0
  17. package/src/adapters/codex.js +6 -0
  18. package/src/adapters/common.js +304 -0
  19. package/src/adapters/config.js +432 -0
  20. package/src/adapters/endpoint.js +178 -0
  21. package/src/adapters/errors.js +69 -0
  22. package/src/adapters/index.js +11 -0
  23. package/src/adapters/models.js +106 -0
  24. package/src/adapters/quota.js +305 -0
  25. package/src/adapters/spawn.js +178 -0
  26. package/src/agent-client.js +108 -0
  27. package/src/cli.js +44 -0
  28. package/src/contract.js +256 -0
  29. package/src/diagnostics.js +133 -0
  30. package/src/doctor.js +101 -0
  31. package/src/engine.js +902 -0
  32. package/src/estimate.js +331 -0
  33. package/src/export.js +307 -0
  34. package/src/fake.js +75 -0
  35. package/src/folder-picker.js +46 -0
  36. package/src/history.js +220 -0
  37. package/src/local-http.js +39 -0
  38. package/src/local-server.js +110 -0
  39. package/src/notes.js +16 -0
  40. package/src/personas.js +49 -0
  41. package/src/preflight.js +126 -0
  42. package/src/progress.js +30 -0
  43. package/src/prompts.js +278 -0
  44. package/src/research.js +113 -0
  45. package/src/review-view.js +157 -0
  46. package/src/seats.js +272 -0
  47. package/src/server.js +928 -0
  48. package/src/settings.js +27 -0
  49. package/src/setup-toml.js +124 -0
  50. package/src/snapshot.js +93 -0
  51. package/src/store.js +303 -0
  52. package/src/terminal.js +77 -0
  53. package/src/terms.js +52 -0
  54. package/src/ui/app.js +1967 -0
  55. package/src/ui/avatars.js +132 -0
  56. package/src/ui/ceremony.js +87 -0
  57. package/src/ui/design/app.css +714 -0
  58. package/src/ui/design/preset-dusk.css +91 -0
  59. package/src/ui/design/tokens.css +129 -0
  60. package/src/ui/diagnostics.js +61 -0
  61. package/src/ui/help.js +122 -0
  62. package/src/ui/icons.js +64 -0
  63. package/src/ui/index.html +272 -0
  64. package/src/ui/loader.js +73 -0
  65. package/src/ui/logo.svg +4 -0
  66. package/src/ui/scan.js +57 -0
  67. package/src/ui/scribe.js +64 -0
  68. package/src/ui/sound.js +80 -0
  69. package/src/validate.js +112 -0
  70. package/src/version.js +5 -0
@@ -0,0 +1,112 @@
1
+ // Bounds for everything the HTTP API accepts. The server is local, but a browser page from
2
+ // anywhere can try to reach it, so every field is typed and bounded here before the engine,
3
+ // the seats module or the exporter sees it. Pure, so it is unit tested.
4
+
5
+ import { researchSettings } from './research.js';
6
+
7
+ export const LIMITS = {
8
+ decision: 2000, option: 200, options: [2, 12], field: 4000, exportTo: 1024, seats: [2, 8], message: 4000,
9
+ critiqueRounds: [0, 8], autoContinueMs: [100, 600_000], questionWaitMs: [100, 600_000], wallClockMs: [60_000, 10_800_000], callTimeoutMs: [30_000, 1_800_000], endpoint: 500,
10
+ seatField: 200, mood: 400, answers: 8, questionId: 8, answer: 1200,
11
+ waitSeconds: [1, 30], port: [1, 65535],
12
+ };
13
+ export const BODY_LIMIT = 256 * 1024;
14
+ const NOTE_FIELDS = ['constraints', 'reversibility', 'known', 'assumed', 'deadline'];
15
+
16
+ const fail = (field, why) => { throw Object.assign(new Error(`${field}: ${why}`), { status: 400 }); };
17
+ const str = (v, field, max, { required = false } = {}) => {
18
+ if (v == null || v === '') { if (required) fail(field, 'required'); return ''; }
19
+ if (typeof v !== 'string') fail(field, 'must be text');
20
+ if (v.length > max) fail(field, `at most ${max} characters`);
21
+ return v;
22
+ };
23
+ const int = (v, field, [lo, hi]) => {
24
+ if (!Number.isInteger(v) || v < lo || v > hi) fail(field, `a whole number from ${lo} to ${hi}`);
25
+ return v;
26
+ };
27
+ const amount = (v, field) => {
28
+ if (v == null || v === '') return undefined;
29
+ if (typeof v !== 'number' || !Number.isFinite(v) || v < 0) fail(field, 'a finite number of 0 or more');
30
+ return v;
31
+ };
32
+
33
+ // The one normalisation every option comparison uses (S7 builds on it): NFKC, lower case,
34
+ // whitespace collapsed. Nothing is stripped, so non-Latin labels stay distinct.
35
+ export const normLabel = (s) => String(s).normalize('NFKC').toLowerCase().replace(/\s+/g, ' ').trim();
36
+
37
+ // POST /api/debates. adapters: the adapter table (a vendor whose CLI reports no cost cannot take
38
+ // a dollar cap). Returns the body with only known keys, every value checked.
39
+ export function validateDebateRequest(body, { adapters }) {
40
+ if (!body || typeof body !== 'object' || Array.isArray(body)) fail('body', 'a JSON object');
41
+ const fn = body.fieldNotes;
42
+ if (!fn || typeof fn !== 'object') fail('fieldNotes', 'required');
43
+ const decision = str(fn.decision, 'decision', LIMITS.decision, { required: true }).trim();
44
+ if (!decision) fail('decision', 'required');
45
+ if (!Array.isArray(fn.options)) fail('options', 'a list');
46
+ const options = fn.options.map((o, i) => str(o, `option ${i + 1}`, LIMITS.option).trim()).filter(Boolean);
47
+ if (options.length < LIMITS.options[0] || options.length > LIMITS.options[1]) fail('options', `${LIMITS.options[0]} to ${LIMITS.options[1]} options`);
48
+ const seen = new Set();
49
+ for (const o of options) { const k = normLabel(o); if (seen.has(k)) fail('options', `"${o}" appears twice`); seen.add(k); }
50
+ const fieldNotes = { decision, options };
51
+ for (const k of NOTE_FIELDS) fieldNotes[k] = str(fn[k], k, LIMITS.field);
52
+ const exportTo = str(fn.exportTo, 'exportTo', LIMITS.exportTo).trim();
53
+ if (exportTo) fieldNotes.exportTo = exportTo;
54
+
55
+ if (!Array.isArray(body.seats)) fail('seats', 'a list');
56
+ if (body.seats.length < LIMITS.seats[0] || body.seats.length > LIMITS.seats[1]) fail('seats', `${LIMITS.seats[0]} to ${LIMITS.seats[1]} seats`);
57
+ for (const [i, s] of body.seats.entries()) {
58
+ if (!s || typeof s !== 'object') fail(`seat ${i + 1}`, 'an object');
59
+ for (const k of ['vendor', 'lens', 'model', 'effort', 'account', 'colour', 'keyEnv']) str(s[k], `seat ${i + 1} ${k}`, LIMITS.seatField);
60
+ str(s.endpoint, `seat ${i + 1} endpoint`, LIMITS.endpoint);
61
+ if (s.mood != null && typeof s.mood !== 'string') str(s.mood.text, `seat ${i + 1} mood`, LIMITS.mood);
62
+ }
63
+ if (body.chairman != null) { for (const k of ['vendor', 'model', 'effort', 'account', 'keyEnv']) str(body.chairman[k], `chairman ${k}`, LIMITS.seatField); str(body.chairman.endpoint, 'chairman endpoint', LIMITS.endpoint); }
64
+
65
+ const o = body.options || {};
66
+ const options_ = {};
67
+ if (o.research != null) options_.research = researchSettings(o.research);
68
+ if (o.critiqueRounds != null) options_.critiqueRounds = int(o.critiqueRounds, 'critiqueRounds', LIMITS.critiqueRounds);
69
+ for (const k of ['autoContinueMs', 'questionWaitMs', 'wallClockMs', 'callTimeoutMs']) if (o[k] != null) options_[k] = int(o[k], k, LIMITS[k]);
70
+ if (o.memory != null) { if (!['stateless', 'resume'].includes(o.memory)) fail('memory', 'stateless or resume'); options_.memory = o.memory; }
71
+ // an endpoint seat has no thread at the vendor to continue, so `resume` prompts would reach it
72
+ // with no context: refuse the table rather than run it on half-prompts
73
+ if (options_.memory === 'resume' && [...body.seats, body.chairman].some((s) => s && adapters[s.vendor]?.kind === 'api')) fail('memory', 'resume needs a session to continue, and an API endpoint seat has none; use stateless');
74
+ if (o.strictTools != null) { if (o.strictTools !== true) fail('strictTools', 'the debate tool guard cannot be disabled'); options_.strictTools = true; }
75
+ if (o.allowLimitedIsolation != null) { if (typeof o.allowLimitedIsolation !== 'boolean') fail('allowLimitedIsolation', 'true or false'); options_.allowLimitedIsolation = o.allowLimitedIsolation; }
76
+ if (o.acknowledgeTermsRisk != null) { if (typeof o.acknowledgeTermsRisk !== 'boolean') fail('acknowledgeTermsRisk', 'true or false'); options_.acknowledgeTermsRisk = o.acknowledgeTermsRisk; }
77
+ if (o.unattended != null) { if (typeof o.unattended !== 'boolean') fail('unattended', 'true or false'); options_.unattended = o.unattended; }
78
+ if (o.budget != null) {
79
+ if (typeof o.budget !== 'object' || Array.isArray(o.budget)) fail('budget', 'an object');
80
+ options_.budget = {};
81
+ for (const [vendor, cap] of Object.entries(o.budget)) {
82
+ if (!adapters[vendor]) fail('budget', `unknown vendor ${vendor}`);
83
+ const tokens = amount(cap?.tokens, `${vendor} token cap`), usd = amount(cap?.usd, `${vendor} dollar cap`);
84
+ if (usd > 0 && !adapters[vendor].paths?.cost) fail('budget', `${adapters[vendor].label} does not report cost, so it cannot take a dollar cap`);
85
+ options_.budget[vendor] = { ...(tokens != null ? { tokens } : {}), ...(usd != null ? { usd } : {}) };
86
+ }
87
+ }
88
+ return { fieldNotes, seats: body.seats, chairman: body.chairman ?? null, options: options_ };
89
+ }
90
+
91
+ // continue / stop / resume / accept: an optional message.
92
+ export function validateMessage(body) {
93
+ if (!body || typeof body !== 'object') return { message: '' };
94
+ const out = { ...body, message: str(body.message, 'message', LIMITS.message) };
95
+ if (body.source != null && !['human', 'agent'].includes(body.source)) fail('source', 'human or agent');
96
+ out.source = body.source ?? 'human';
97
+ if (body.round != null) out.round = int(body.round, 'round', LIMITS.critiqueRounds);
98
+ if (body.answers != null) {
99
+ if (!Array.isArray(body.answers) || body.answers.length > LIMITS.answers) fail('answers', `a list of at most ${LIMITS.answers} answers`);
100
+ if (body.round == null) fail('round', 'required with answers');
101
+ out.answers = body.answers.map((a) => ({ id: str(a?.id, 'question id', LIMITS.questionId, { required: true }), text: str(a?.text, 'answer', LIMITS.answer) }));
102
+ }
103
+ return out;
104
+ }
105
+
106
+ // accept: an option id from this debate's options, or 'none' (decline every option), and a note.
107
+ export function validateAccept(body, options) {
108
+ const option = body?.option;
109
+ const ids = (options || []).map((_, i) => `opt-${i + 1}`);
110
+ if (option !== 'none' && !ids.includes(option)) fail('option', `one of ${[...ids, 'none'].join(', ')}`);
111
+ return { option, note: str(body?.note, 'note', LIMITS.message).trim() };
112
+ }
package/src/version.js ADDED
@@ -0,0 +1,5 @@
1
+ // The version is written once, in package.json. The board footer, diagnostics and the Codex
2
+ // app-server handshake read it from here, so a release bumps one field and no copy goes stale.
3
+ import { readFileSync } from 'node:fs';
4
+
5
+ export const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;