debaitable 0.0.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.
Files changed (3) hide show
  1. package/README.md +62 -0
  2. package/dist/main.js +2212 -0
  3. package/package.json +34 -0
package/dist/main.js ADDED
@@ -0,0 +1,2212 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/tui-ui/app.ts
4
+ import blessed from "blessed";
5
+ import { chmod, readFile, writeFile as writeFile2 } from "fs/promises";
6
+ import path2 from "path";
7
+
8
+ // src/ai/schemas.ts
9
+ import { z } from "zod";
10
+ var LlmRequestSchema = z.object({
11
+ system: z.string().min(1),
12
+ prompt: z.string().min(1),
13
+ schema: z.unknown()
14
+ });
15
+ var LlmResponseSchema = z.object({
16
+ output: z.unknown(),
17
+ raw: z.string().min(1),
18
+ model: z.string().min(1)
19
+ });
20
+
21
+ // src/core/constants.ts
22
+ var DEFAULT_VISIBILITY = "private";
23
+ var DEFAULT_STATUS = "queued";
24
+
25
+ // src/core/errors.ts
26
+ var NotFoundError = class extends Error {
27
+ constructor(message) {
28
+ super(message);
29
+ this.name = "NotFoundError";
30
+ }
31
+ };
32
+ var BadRequestError = class extends Error {
33
+ constructor(message) {
34
+ super(message);
35
+ this.name = "BadRequestError";
36
+ }
37
+ };
38
+
39
+ // src/core/roles.ts
40
+ var roleDefinitions = [
41
+ {
42
+ key: "strategist",
43
+ name: "Strategist",
44
+ focus: "Long-term value and strategic alignment"
45
+ },
46
+ {
47
+ key: "skeptic",
48
+ name: "Skeptic",
49
+ focus: "Challenges assumptions and surfaces weaknesses"
50
+ },
51
+ {
52
+ key: "risk_analyst",
53
+ name: "Risk Analyst",
54
+ focus: "Failure modes, edge cases, and mitigation"
55
+ },
56
+ {
57
+ key: "execution_planner",
58
+ name: "Execution Planner",
59
+ focus: "Steps, dependencies, and timelines"
60
+ },
61
+ {
62
+ key: "cost_roi",
63
+ name: "Cost / ROI",
64
+ focus: "Budget constraints and return on investment"
65
+ }
66
+ ];
67
+
68
+ // src/core/sanitize.ts
69
+ var normalizeText = (value) => value.replace(/\r\n/g, "\n").trim();
70
+ var normalizeList = (values) => values.map(normalizeText).filter((value) => value.length > 0);
71
+ var sanitizeDecisionInput = (input) => ({
72
+ title: normalizeText(input.title),
73
+ context: normalizeText(input.context),
74
+ goals: normalizeList(input.goals),
75
+ constraints: normalizeList(input.constraints),
76
+ decisionType: input.decisionType
77
+ });
78
+
79
+ // src/core/schemas.ts
80
+ import { z as z2 } from "zod";
81
+ var DecisionTypeSchema = z2.enum([
82
+ "product",
83
+ "engineering",
84
+ "hiring",
85
+ "growth",
86
+ "general"
87
+ ]);
88
+ var DecisionStatusSchema = z2.enum([
89
+ "queued",
90
+ "running",
91
+ "succeeded",
92
+ "failed"
93
+ ]);
94
+ var VisibilitySchema = z2.enum(["private", "unlisted", "public"]);
95
+ var RoleKeySchema = z2.enum([
96
+ "strategist",
97
+ "skeptic",
98
+ "risk_analyst",
99
+ "execution_planner",
100
+ "cost_roi"
101
+ ]);
102
+ var DecisionInputSchema = z2.object({
103
+ title: z2.string().min(1),
104
+ context: z2.string().min(1),
105
+ goals: z2.array(z2.string().min(1)),
106
+ constraints: z2.array(z2.string().min(1)),
107
+ decisionType: DecisionTypeSchema
108
+ });
109
+ var RoleDefinitionSchema = z2.object({
110
+ key: RoleKeySchema,
111
+ name: z2.string().min(1),
112
+ focus: z2.string().min(1)
113
+ });
114
+ var DebateRoundSchema = z2.object({
115
+ roundIndex: z2.number().int().nonnegative(),
116
+ roleKey: RoleKeySchema,
117
+ output: z2.string().min(1)
118
+ });
119
+ var ExecutiveDecisionSchema = z2.object({
120
+ decision: z2.enum(["go", "iterate", "stop", "yes", "no", "conditional"]),
121
+ why: z2.array(z2.string().min(1)),
122
+ topRisks: z2.array(z2.string().min(1)),
123
+ topActions: z2.array(z2.string().min(1)),
124
+ stopGoCriteria: z2.string().min(1)
125
+ });
126
+ var DecisionRecordSchema = z2.object({
127
+ summary: z2.string().min(1),
128
+ rationale: z2.string().min(1),
129
+ tradeoffs: z2.array(z2.string().min(1)),
130
+ risks: z2.array(z2.string().min(1)),
131
+ actions: z2.array(z2.string().min(1)),
132
+ confidence: z2.number().min(0).max(1),
133
+ minorityReport: z2.string().min(1),
134
+ executiveDecision: ExecutiveDecisionSchema
135
+ });
136
+ var DecisionSchema = z2.object({
137
+ id: z2.string().min(1),
138
+ title: z2.string().min(1),
139
+ context: z2.string().min(1),
140
+ goals: z2.array(z2.string().min(1)),
141
+ constraints: z2.array(z2.string().min(1)),
142
+ decisionType: DecisionTypeSchema,
143
+ status: DecisionStatusSchema,
144
+ visibility: VisibilitySchema
145
+ });
146
+ var DecisionRunSchema = z2.object({
147
+ runId: z2.string().min(1),
148
+ decisionId: z2.string().min(1),
149
+ status: DecisionStatusSchema
150
+ });
151
+
152
+ // src/core/validate.ts
153
+ var parseDecisionRecord = (value) => DecisionRecordSchema.parse(value);
154
+
155
+ // src/ai/openai-provider.ts
156
+ var DEFAULT_BASE_URL = "https://api.openai.com/v1/responses";
157
+ var isZodSchema = (schema) => Boolean(schema && typeof schema.parse === "function");
158
+ var parseJson = (value) => {
159
+ try {
160
+ return JSON.parse(value);
161
+ } catch (error) {
162
+ throw new BadRequestError("Model output was not valid JSON");
163
+ }
164
+ };
165
+ var getOutputText = (payload) => {
166
+ if (!payload || typeof payload !== "object") {
167
+ throw new Error("OpenAI response payload missing");
168
+ }
169
+ const data = payload;
170
+ if (Array.isArray(data.output_text) && data.output_text.length > 0) {
171
+ return data.output_text.join("");
172
+ }
173
+ if (Array.isArray(data.output)) {
174
+ const parts = [];
175
+ for (const item of data.output) {
176
+ if (item.type !== "message" || !Array.isArray(item.content)) {
177
+ continue;
178
+ }
179
+ for (const content of item.content) {
180
+ if (content.type === "output_text" && content.text) {
181
+ parts.push(content.text);
182
+ }
183
+ }
184
+ }
185
+ if (parts.length > 0) {
186
+ return parts.join("");
187
+ }
188
+ }
189
+ if (typeof data.text === "string" && data.text.length > 0) {
190
+ return data.text;
191
+ }
192
+ throw new Error("OpenAI response missing output text");
193
+ };
194
+ var createOpenAiProvider = (options = {}) => {
195
+ const apiKey = options.apiKey ?? process.env.OPENAI_API_KEY;
196
+ if (!apiKey) {
197
+ throw new Error("OPENAI_API_KEY is required");
198
+ }
199
+ const model = options.model ?? process.env.OPENAI_MODEL ?? "gpt-5";
200
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
201
+ const timeoutMs = options.timeoutMs ?? 6e4;
202
+ return {
203
+ async generate(request) {
204
+ const controller = new AbortController();
205
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
206
+ try {
207
+ const response = await fetch(baseUrl, {
208
+ method: "POST",
209
+ headers: {
210
+ "Content-Type": "application/json",
211
+ Authorization: `Bearer ${apiKey}`
212
+ },
213
+ body: JSON.stringify({
214
+ model,
215
+ input: [
216
+ { role: "system", content: request.system },
217
+ { role: "user", content: request.prompt }
218
+ ]
219
+ }),
220
+ signal: controller.signal
221
+ });
222
+ if (!response.ok) {
223
+ const message = await response.text();
224
+ throw new Error(`OpenAI request failed: ${response.status} ${message}`);
225
+ }
226
+ const payload = await response.json();
227
+ const text = getOutputText(payload);
228
+ const json = parseJson(text);
229
+ const output = isZodSchema(request.schema) ? request.schema.parse(json) : json;
230
+ return {
231
+ output,
232
+ raw: text,
233
+ model
234
+ };
235
+ } finally {
236
+ clearTimeout(timer);
237
+ }
238
+ }
239
+ };
240
+ };
241
+
242
+ // src/ai/heuristic-provider.ts
243
+ var parseInput = (prompt) => {
244
+ const marker = "Decision input:\n";
245
+ const start = prompt.indexOf(marker);
246
+ if (start < 0) {
247
+ return null;
248
+ }
249
+ const after = prompt.slice(start + marker.length);
250
+ const end = after.indexOf("\n\n");
251
+ const jsonText = (end >= 0 ? after.slice(0, end) : after).trim();
252
+ try {
253
+ return JSON.parse(jsonText);
254
+ } catch {
255
+ return null;
256
+ }
257
+ };
258
+ var parseRoleKey = (prompt) => {
259
+ const match = prompt.match(/"roleKey":"([a-z_]+)"/);
260
+ if (!match) {
261
+ return "strategist";
262
+ }
263
+ return match[1];
264
+ };
265
+ var inferRound = (prompt) => {
266
+ if (prompt.includes("Task: Provide an independent proposal.")) {
267
+ return "proposal";
268
+ }
269
+ if (prompt.includes("Task: Provide critiques and rebuttals")) {
270
+ return "critique";
271
+ }
272
+ if (prompt.includes("Task: Converge and vote.")) {
273
+ return "convergence";
274
+ }
275
+ return "record";
276
+ };
277
+ var pickVote = (roleKey) => {
278
+ if (roleKey === "skeptic") {
279
+ return "conditional";
280
+ }
281
+ if (roleKey === "risk_analyst") {
282
+ return "conditional";
283
+ }
284
+ return "support";
285
+ };
286
+ var isBinaryQuestion = (context) => /^(should|is|are|can|could|do|does|did|will|would)\b/i.test(context.trim());
287
+ var toResponse = (output) => ({
288
+ output,
289
+ raw: JSON.stringify(output),
290
+ model: "heuristic-v1"
291
+ });
292
+ var HeuristicDebateProvider = class {
293
+ async generate(request) {
294
+ const input = parseInput(request.prompt);
295
+ const roleKey = parseRoleKey(request.prompt);
296
+ const round = inferRound(request.prompt);
297
+ if (!input) {
298
+ throw new Error("HeuristicDebateProvider: missing decision input in prompt");
299
+ }
300
+ if (round === "proposal") {
301
+ const output2 = {
302
+ roleKey,
303
+ summary: `${input.title}: ${roleKey} perspective for ${input.decisionType} analysis.`,
304
+ recommendation: `Prioritize an evidence-backed approach focused on ${input.goals[0] ?? "the core goal"}.`,
305
+ rationale: `This balances objective achievement with the key constraint: ${input.constraints[0] ?? "practical limits"}.`,
306
+ risks: [
307
+ `Key assumptions may be wrong and weaken ${input.goals[0] ?? "the target outcome"}.`,
308
+ "Overconfidence can reduce fairness and decision quality."
309
+ ],
310
+ assumptions: [
311
+ "Available evidence is relevant and reasonably reliable.",
312
+ "Constraints and stakeholder impacts are represented fairly."
313
+ ],
314
+ actions: [
315
+ "Define explicit criteria for a strong vs weak recommendation.",
316
+ "Test key assumptions against available evidence.",
317
+ "Document risks and mitigation before finalizing."
318
+ ]
319
+ };
320
+ return toResponse(output2);
321
+ }
322
+ if (round === "critique") {
323
+ const output2 = {
324
+ roleKey,
325
+ critiques: [
326
+ "Current proposals under-specify rollback and failure thresholds.",
327
+ "Dependencies and ownership boundaries are not explicit enough."
328
+ ],
329
+ rebuttals: [
330
+ "A constrained pilot can validate assumptions before full commitment.",
331
+ "A staged plan can reduce downside while preserving speed."
332
+ ],
333
+ openQuestions: [
334
+ "What metric threshold triggers expansion versus rollback?",
335
+ "Which team owns cross-functional coordination and deadline risk?"
336
+ ]
337
+ };
338
+ return toResponse(output2);
339
+ }
340
+ if (round === "convergence") {
341
+ const vote = pickVote(roleKey);
342
+ const output2 = {
343
+ roleKey,
344
+ vote,
345
+ reasons: [
346
+ `Recommendation aligns with ${input.goals[0] ?? "the primary objective"}.`,
347
+ `Plan acknowledges constraint: ${input.constraints[0] ?? "delivery risk"}.`
348
+ ],
349
+ conditions: vote === "support" ? [] : [
350
+ "Set clear stop/continue criteria before rollout.",
351
+ "Assign accountable owner for risk and dependency tracking."
352
+ ]
353
+ };
354
+ return toResponse(output2);
355
+ }
356
+ const output = {
357
+ summary: `Conclude a defensible recommendation for ${input.title}.`,
358
+ rationale: "Role debate converges on evidence quality, risk handling, and practical feasibility.",
359
+ tradeoffs: [
360
+ "Stronger confidence from stricter evidence thresholds vs faster decision speed with less evidence.",
361
+ "Broader inclusion of edge cases improves safety but increases analysis complexity."
362
+ ],
363
+ risks: [
364
+ "Biased assumptions can distort conclusions.",
365
+ "Insufficient evidence quality can produce unreliable recommendations."
366
+ ],
367
+ actions: [
368
+ "Set explicit decision criteria and acceptable uncertainty bounds.",
369
+ "Validate top assumptions with disconfirming evidence checks.",
370
+ "Finalize recommendation with documented tradeoffs and risks."
371
+ ],
372
+ confidence: 0.68,
373
+ minorityReport: "Conditional voters requested clearer evidence thresholds and stronger handling of edge cases.",
374
+ executiveDecision: {
375
+ decision: isBinaryQuestion(input.context) ? "conditional" : "iterate",
376
+ why: [
377
+ "The recommendation aligns with stated goals and constraints.",
378
+ "Convergence includes conditional support that can be resolved with clearer criteria."
379
+ ],
380
+ topRisks: [
381
+ "Unclear evidence standards can skew the final answer.",
382
+ "Missing edge-case analysis can create fairness or safety blind spots.",
383
+ "Weak uncertainty handling can overstate confidence."
384
+ ],
385
+ topActions: [
386
+ "Define clear recommendation criteria before deciding.",
387
+ "Pressure-test claims against counterarguments and edge cases.",
388
+ "Record uncertainty bounds and confidence rationale.",
389
+ "Update recommendation if critical evidence changes.",
390
+ "Document unresolved questions for follow-up."
391
+ ],
392
+ stopGoCriteria: "Choose the positive path only if evidence quality and risk thresholds are met; otherwise choose the safer alternative or refine."
393
+ }
394
+ };
395
+ return toResponse(output);
396
+ }
397
+ };
398
+
399
+ // src/jobs/schemas.ts
400
+ import { z as z3 } from "zod";
401
+ var DecisionJobPayloadSchema = z3.object({
402
+ decisionId: z3.string().min(1),
403
+ runId: z3.string().min(1)
404
+ });
405
+
406
+ // src/jobs/memory-queue.ts
407
+ var MemoryDecisionQueue = class {
408
+ payloads = [];
409
+ async enqueueDecision(payload) {
410
+ this.payloads = [...this.payloads, payload];
411
+ }
412
+ getPending() {
413
+ return this.payloads.map((payload) => ({ ...payload }));
414
+ }
415
+ clear() {
416
+ this.payloads = [];
417
+ }
418
+ };
419
+
420
+ // src/orchestration/constants.ts
421
+ var PROMPT_VERSION = "2026-02-07";
422
+ var OUTPUT_TEXT_LIMIT = 700;
423
+
424
+ // src/orchestration/compact.ts
425
+ var compactProposal = (output) => ({
426
+ rk: output.roleKey,
427
+ s: output.summary,
428
+ rec: output.recommendation,
429
+ rat: output.rationale,
430
+ r: output.risks,
431
+ a: output.assumptions,
432
+ act: output.actions
433
+ });
434
+ var compactCritique = (output) => ({
435
+ rk: output.roleKey,
436
+ c: output.critiques,
437
+ rb: output.rebuttals,
438
+ q: output.openQuestions
439
+ });
440
+ var compactConvergence = (output) => ({
441
+ rk: output.roleKey,
442
+ v: output.vote,
443
+ rs: output.reasons,
444
+ c: output.conditions
445
+ });
446
+ var formatCompactProposals = (outputs) => JSON.stringify(outputs.map(compactProposal));
447
+ var formatCompactCritiques = (outputs) => JSON.stringify(outputs.map(compactCritique));
448
+ var formatCompactConvergence = (outputs) => JSON.stringify(outputs.map(compactConvergence));
449
+
450
+ // src/orchestration/guards.ts
451
+ var assertValidRoles = (roles) => {
452
+ if (roles.length === 0) {
453
+ throw new BadRequestError("At least one role is required");
454
+ }
455
+ const seen = /* @__PURE__ */ new Set();
456
+ for (const role of roles) {
457
+ if (seen.has(role.key)) {
458
+ throw new BadRequestError(`Duplicate role key: ${role.key}`);
459
+ }
460
+ seen.add(role.key);
461
+ }
462
+ };
463
+
464
+ // src/orchestration/normalize.ts
465
+ var normalizeText2 = (value, limit = OUTPUT_TEXT_LIMIT) => value.replace(/\s+/g, " ").replace(/\u0000/g, "").trim().slice(0, limit);
466
+ var dedupe = (values) => {
467
+ const seen = /* @__PURE__ */ new Set();
468
+ const result = [];
469
+ for (const value of values) {
470
+ const key = value.toLowerCase();
471
+ if (!seen.has(key)) {
472
+ seen.add(key);
473
+ result.push(value);
474
+ }
475
+ }
476
+ return result;
477
+ };
478
+ var normalizeList2 = (values, maxItems, limit = 240) => dedupe(values.map((value) => normalizeText2(value, limit)).filter((value) => value.length > 0)).slice(
479
+ 0,
480
+ maxItems
481
+ );
482
+ var normalizeProposalOutput = (output) => ({
483
+ roleKey: output.roleKey,
484
+ summary: normalizeText2(output.summary, 260),
485
+ recommendation: normalizeText2(output.recommendation, 260),
486
+ rationale: normalizeText2(output.rationale, 320),
487
+ risks: normalizeList2(output.risks, 6),
488
+ assumptions: normalizeList2(output.assumptions, 6),
489
+ actions: normalizeList2(output.actions, 8)
490
+ });
491
+ var normalizeCritiqueOutput = (output) => ({
492
+ roleKey: output.roleKey,
493
+ critiques: normalizeList2(output.critiques, 8),
494
+ rebuttals: normalizeList2(output.rebuttals, 8),
495
+ openQuestions: normalizeList2(output.openQuestions, 8)
496
+ });
497
+ var normalizeConvergenceOutput = (output) => ({
498
+ roleKey: output.roleKey,
499
+ vote: output.vote,
500
+ reasons: normalizeList2(output.reasons, 5),
501
+ conditions: normalizeList2(output.conditions, 5)
502
+ });
503
+ var normalizeDecisionRecordOutput = (output) => ({
504
+ summary: normalizeText2(output.summary, 700),
505
+ rationale: normalizeText2(output.rationale, 850),
506
+ tradeoffs: normalizeList2(output.tradeoffs, 6),
507
+ risks: normalizeList2(output.risks, 8),
508
+ actions: normalizeList2(output.actions, 8),
509
+ confidence: Math.max(0, Math.min(1, Number(output.confidence.toFixed(2)))),
510
+ minorityReport: normalizeText2(output.minorityReport, 500),
511
+ executiveDecision: {
512
+ decision: output.executiveDecision.decision,
513
+ why: normalizeList2(output.executiveDecision.why, 3, 220),
514
+ topRisks: normalizeList2(output.executiveDecision.topRisks, 3, 220),
515
+ topActions: normalizeList2(output.executiveDecision.topActions, 5, 220),
516
+ stopGoCriteria: normalizeText2(output.executiveDecision.stopGoCriteria, 300)
517
+ }
518
+ });
519
+
520
+ // src/orchestration/quality.ts
521
+ var PLACEHOLDER_EXACT = /* @__PURE__ */ new Set([
522
+ "final summary",
523
+ "final rationale",
524
+ "tradeoff",
525
+ "risk",
526
+ "action",
527
+ "no minority report"
528
+ ]);
529
+ var hasPlaceholder = (value) => PLACEHOLDER_EXACT.has(value.trim().toLowerCase());
530
+ var assertListNonEmpty = (label, values, min) => {
531
+ if (values.length < min) {
532
+ throw new BadRequestError(`Decision record quality: ${label} requires at least ${min} items`);
533
+ }
534
+ };
535
+ var assertNoMalformedTokens = (value, field) => {
536
+ const malformedPatterns = [/\bRun\d+\/\d+\b/, /\btheflag\b/i, /\bRun\d+/];
537
+ if (malformedPatterns.some((pattern) => pattern.test(value))) {
538
+ throw new BadRequestError(`Decision record quality: malformed content in ${field}`);
539
+ }
540
+ };
541
+ var assertValue = (field, value) => {
542
+ if (!value.trim()) {
543
+ throw new BadRequestError(`Decision record quality: ${field} cannot be empty`);
544
+ }
545
+ if (hasPlaceholder(value)) {
546
+ throw new BadRequestError(`Decision record quality: ${field} is placeholder text`);
547
+ }
548
+ assertNoMalformedTokens(value, field);
549
+ };
550
+ var assertDecisionRecordQuality = (record) => {
551
+ assertValue("summary", record.summary);
552
+ assertValue("rationale", record.rationale);
553
+ assertValue("minorityReport", record.minorityReport);
554
+ assertValue("executiveDecision.stopGoCriteria", record.executiveDecision.stopGoCriteria);
555
+ assertListNonEmpty("tradeoffs", record.tradeoffs, 2);
556
+ assertListNonEmpty("risks", record.risks, 2);
557
+ assertListNonEmpty("actions", record.actions, 2);
558
+ assertListNonEmpty("executiveDecision.why", record.executiveDecision.why, 1);
559
+ assertListNonEmpty("executiveDecision.topRisks", record.executiveDecision.topRisks, 1);
560
+ assertListNonEmpty("executiveDecision.topActions", record.executiveDecision.topActions, 1);
561
+ for (const value of record.tradeoffs) {
562
+ assertValue("tradeoffs", value);
563
+ }
564
+ for (const value of record.risks) {
565
+ assertValue("risks", value);
566
+ }
567
+ for (const value of record.actions) {
568
+ assertValue("actions", value);
569
+ }
570
+ for (const value of record.executiveDecision.why) {
571
+ assertValue("executiveDecision.why", value);
572
+ }
573
+ for (const value of record.executiveDecision.topRisks) {
574
+ assertValue("executiveDecision.topRisks", value);
575
+ }
576
+ for (const value of record.executiveDecision.topActions) {
577
+ assertValue("executiveDecision.topActions", value);
578
+ }
579
+ };
580
+
581
+ // src/orchestration/votes.ts
582
+ var tallyVotes = (outputs) => outputs.reduce(
583
+ (tally, output) => ({
584
+ ...tally,
585
+ [output.vote]: tally[output.vote] + 1
586
+ }),
587
+ { support: 0, conditional: 0, oppose: 0 }
588
+ );
589
+ var formatVoteTally = (tally) => JSON.stringify(tally, null, 2);
590
+
591
+ // src/orchestration/prompts.ts
592
+ var formatDecisionInput = (input) => JSON.stringify(
593
+ {
594
+ title: input.title,
595
+ context: input.context,
596
+ goals: input.goals,
597
+ constraints: input.constraints,
598
+ decisionType: input.decisionType
599
+ },
600
+ null,
601
+ 2
602
+ );
603
+ var formatProposalList = (outputs) => formatCompactProposals(outputs);
604
+ var formatCritiqueList = (outputs) => formatCompactCritiques(outputs);
605
+ var formatConvergenceList = (outputs) => formatCompactConvergence(outputs);
606
+ var baseInstructions = [
607
+ `Prompt version: ${PROMPT_VERSION}.`,
608
+ "Return JSON only.",
609
+ "Use double quotes for all keys and string values.",
610
+ "Do not include code fences or commentary.",
611
+ "Arrays must contain only strings, not objects.",
612
+ "Keep writing concise and non-repetitive.",
613
+ "Use plain, direct language with short sentences.",
614
+ "Avoid unexplained acronyms. If you must use one, define it once.",
615
+ "Prioritize clear reasoning over buzzwords.",
616
+ "Keep claims tied to the user's subject. Do not introduce unrelated domains.",
617
+ "Avoid placeholder text and malformed tokens."
618
+ ].join(" ");
619
+ var compactKeyHint = "Compact context keys: rk=roleKey, s=summary, rec=recommendation, rat=rationale, r=risks, a=assumptions, act=actions, c=critiques or conditions, rb=rebuttals, q=openQuestions, v=vote, rs=reasons.";
620
+ var proposalSchemaHint = (roleKey) => [
621
+ '{"roleKey":"',
622
+ roleKey,
623
+ '","summary":"...","recommendation":"...","rationale":"...",',
624
+ '"risks":["..."],"assumptions":["..."],"actions":["..."]}'
625
+ ].join("");
626
+ var critiqueSchemaHint = (roleKey) => [
627
+ '{"roleKey":"',
628
+ roleKey,
629
+ '","critiques":["..."],"rebuttals":["..."],"openQuestions":["..."]}'
630
+ ].join("");
631
+ var convergenceSchemaHint = (roleKey) => [
632
+ '{"roleKey":"',
633
+ roleKey,
634
+ '","vote":"support|conditional|oppose","reasons":["..."],',
635
+ '"conditions":["..."]}'
636
+ ].join("");
637
+ var decisionRecordSchemaHint = [
638
+ '{"summary":"...","rationale":"...","tradeoffs":["..."],',
639
+ '"risks":["..."],"actions":["..."],"confidence":0.0,',
640
+ '"minorityReport":"...",',
641
+ '"executiveDecision":{"decision":"go|iterate|stop|yes|no|conditional","why":["..."],',
642
+ '"topRisks":["..."],"topActions":["..."],"stopGoCriteria":"..."}}'
643
+ ].join("");
644
+ var buildProposalPrompt = (role, input) => ({
645
+ system: [
646
+ "You are a decision role in a structured debate.",
647
+ `Role: ${role.name}.`,
648
+ `Focus: ${role.focus}.`,
649
+ baseInstructions
650
+ ].join(" "),
651
+ prompt: [
652
+ "Decision input:",
653
+ formatDecisionInput(input),
654
+ "",
655
+ "Task: Provide an independent proposal.",
656
+ "Limits: summary <= 260 chars, recommendation <= 260 chars, rationale <= 320 chars, risks/assumptions <= 6 each, actions <= 8.",
657
+ "Output JSON schema:",
658
+ proposalSchemaHint(role.key)
659
+ ].join("\n")
660
+ });
661
+ var buildCritiquePrompt = (role, input, proposals) => ({
662
+ system: [
663
+ "You are a decision role in a structured debate.",
664
+ `Role: ${role.name}.`,
665
+ `Focus: ${role.focus}.`,
666
+ baseInstructions
667
+ ].join(" "),
668
+ prompt: [
669
+ "Decision input:",
670
+ formatDecisionInput(input),
671
+ "",
672
+ "Round 1 proposals:",
673
+ formatProposalList(proposals),
674
+ compactKeyHint,
675
+ "",
676
+ "Task: Provide critiques and rebuttals from your perspective.",
677
+ "Limits: critiques/rebuttals/openQuestions <= 8 items each, each item <= 240 chars.",
678
+ "Output JSON schema:",
679
+ critiqueSchemaHint(role.key)
680
+ ].join("\n")
681
+ });
682
+ var buildConvergencePrompt = (role, input, proposals, critiques) => ({
683
+ system: [
684
+ "You are a decision role in a structured debate.",
685
+ `Role: ${role.name}.`,
686
+ `Focus: ${role.focus}.`,
687
+ baseInstructions
688
+ ].join(" "),
689
+ prompt: [
690
+ "Decision input:",
691
+ formatDecisionInput(input),
692
+ "",
693
+ "Round 1 proposals:",
694
+ formatProposalList(proposals),
695
+ compactKeyHint,
696
+ "",
697
+ "Round 2 critiques:",
698
+ formatCritiqueList(critiques),
699
+ compactKeyHint,
700
+ "",
701
+ "Task: Converge and vote.",
702
+ "Limits: reasons/conditions <= 5 items each, each item <= 240 chars.",
703
+ "Output JSON schema:",
704
+ convergenceSchemaHint(role.key)
705
+ ].join("\n")
706
+ });
707
+ var buildDecisionRecordPrompt = (input, proposals, critiques, convergence) => ({
708
+ system: [
709
+ "You are an aggregator that produces a final Decision Record.",
710
+ baseInstructions
711
+ ].join(" "),
712
+ prompt: [
713
+ "Decision input:",
714
+ formatDecisionInput(input),
715
+ "",
716
+ "Round 1 proposals:",
717
+ formatProposalList(proposals),
718
+ compactKeyHint,
719
+ "",
720
+ "Round 2 critiques:",
721
+ formatCritiqueList(critiques),
722
+ compactKeyHint,
723
+ "",
724
+ "Round 3 convergence:",
725
+ formatConvergenceList(convergence),
726
+ compactKeyHint,
727
+ "",
728
+ "Vote tally:",
729
+ formatVoteTally(tallyVotes(convergence)),
730
+ "",
731
+ "Task: Produce the final Decision Record with rationale, tradeoffs, risks, actions, confidence (0-1), minority report, and executiveDecision.",
732
+ "Reasoning style: explain the recommendation in a simple chain: evidence -> implications -> decision.",
733
+ "Relevance rule: stay strictly on the user subject and wording. Do not invent unrelated business mechanics.",
734
+ "For non-business topics, avoid terms like KPI, SLA, rollout, pilot, canary, funnel, activation unless explicitly present in input.",
735
+ "Limits: summary <= 420 chars, rationale <= 520 chars, minorityReport <= 360 chars.",
736
+ "Limits: tradeoffs <= 6, risks <= 8, actions <= 8.",
737
+ "Executive block limits: why <= 3, topRisks <= 3, topActions <= 5, stopGoCriteria <= 300 chars.",
738
+ "Use yes/no/conditional for binary or policy questions; use go/iterate/stop for execution/rollout decisions.",
739
+ "No duplicate list items. Prefer concise bullets over long paragraphs.",
740
+ "Output JSON schema:",
741
+ decisionRecordSchemaHint
742
+ ].join("\n")
743
+ });
744
+
745
+ // src/orchestration/schemas.ts
746
+ import { z as z4 } from "zod";
747
+ var ProposalOutputSchema = z4.object({
748
+ roleKey: RoleKeySchema,
749
+ summary: z4.string().min(1),
750
+ recommendation: z4.string().min(1),
751
+ rationale: z4.string().min(1),
752
+ risks: z4.array(z4.string().min(1)),
753
+ assumptions: z4.array(z4.string().min(1)),
754
+ actions: z4.array(z4.string().min(1))
755
+ });
756
+ var CritiqueOutputSchema = z4.object({
757
+ roleKey: RoleKeySchema,
758
+ critiques: z4.array(z4.string().min(1)),
759
+ rebuttals: z4.array(z4.string().min(1)),
760
+ openQuestions: z4.array(z4.string().min(1))
761
+ });
762
+ var VoteSchema = z4.enum(["support", "conditional", "oppose"]);
763
+ var ConvergenceOutputSchema = z4.object({
764
+ roleKey: RoleKeySchema,
765
+ vote: VoteSchema,
766
+ reasons: z4.array(z4.string().min(1)),
767
+ conditions: z4.array(z4.string().min(1))
768
+ });
769
+
770
+ // src/orchestration/serialize.ts
771
+ var serializeProposalOutput = (output) => JSON.stringify({
772
+ roleKey: output.roleKey,
773
+ summary: output.summary,
774
+ recommendation: output.recommendation,
775
+ rationale: output.rationale,
776
+ risks: output.risks,
777
+ assumptions: output.assumptions,
778
+ actions: output.actions
779
+ });
780
+ var serializeCritiqueOutput = (output) => JSON.stringify({
781
+ roleKey: output.roleKey,
782
+ critiques: output.critiques,
783
+ rebuttals: output.rebuttals,
784
+ openQuestions: output.openQuestions
785
+ });
786
+ var serializeConvergenceOutput = (output) => JSON.stringify({
787
+ roleKey: output.roleKey,
788
+ vote: output.vote,
789
+ reasons: output.reasons,
790
+ conditions: output.conditions
791
+ });
792
+
793
+ // src/orchestration/validate.ts
794
+ var parseProposalOutput = (value) => ProposalOutputSchema.parse(value);
795
+ var parseCritiqueOutput = (value) => CritiqueOutputSchema.parse(value);
796
+ var parseConvergenceOutput = (value) => ConvergenceOutputSchema.parse(value);
797
+
798
+ // src/orchestration/synthesize.ts
799
+ var dedupe2 = (values) => {
800
+ const seen = /* @__PURE__ */ new Set();
801
+ const result = [];
802
+ for (const value of values) {
803
+ const next = value.trim();
804
+ if (!next) {
805
+ continue;
806
+ }
807
+ const key = next.toLowerCase();
808
+ if (seen.has(key)) {
809
+ continue;
810
+ }
811
+ seen.add(key);
812
+ result.push(next);
813
+ }
814
+ return result;
815
+ };
816
+ var take = (values, max) => values.slice(0, max);
817
+ var fallbackSummary = (input) => `Decision on ${input.title} for ${input.decisionType} priorities.`;
818
+ var buildRationale = (convergence, proposals) => {
819
+ const voteReasons = convergence.flatMap((item) => item.reasons);
820
+ const proposalRationales = proposals.map((item) => item.rationale);
821
+ const reasons = take(dedupe2([...voteReasons, ...proposalRationales]), 3);
822
+ if (reasons.length === 0) {
823
+ return "Rationale assembled from role recommendations and convergence votes.";
824
+ }
825
+ return reasons.join(" ");
826
+ };
827
+ var buildMinorityReport = (convergence, critiques) => {
828
+ const dissenters = convergence.filter((item) => item.vote !== "support");
829
+ const dissentReasons = dissenters.flatMap((item) => item.reasons);
830
+ const conditions = dissenters.flatMap((item) => item.conditions);
831
+ const openQuestions = critiques.flatMap((item) => item.openQuestions);
832
+ const lines = take(dedupe2([...dissentReasons, ...conditions, ...openQuestions]), 3);
833
+ if (lines.length === 0) {
834
+ return "No substantial minority objections were raised in convergence.";
835
+ }
836
+ return lines.join(" ");
837
+ };
838
+ var scoreConfidence = (convergence) => {
839
+ if (convergence.length === 0) {
840
+ return 0.4;
841
+ }
842
+ const weight = { support: 1, conditional: 0.6, oppose: 0.2 };
843
+ const total = convergence.reduce((sum, item) => sum + weight[item.vote], 0);
844
+ const score = total / convergence.length;
845
+ return Math.max(0, Math.min(1, Number(score.toFixed(2))));
846
+ };
847
+ var isBinaryQuestion2 = (context) => {
848
+ const normalized = context.trim().toLowerCase();
849
+ return /^(should|is|are|can|could|do|does|did|will|would)\b/.test(normalized);
850
+ };
851
+ var synthesizeDecisionRecord = (input, proposals, critiques, convergence) => {
852
+ const tally = tallyVotes(convergence);
853
+ const recommendationLines = proposals.map((item) => item.recommendation);
854
+ const summaryCandidates = take(dedupe2(recommendationLines), 2);
855
+ const summary = summaryCandidates.length > 0 ? summaryCandidates.join(" ") : fallbackSummary(input);
856
+ const tradeoffs = take(
857
+ dedupe2([
858
+ ...critiques.flatMap((item) => item.critiques),
859
+ `Vote split: support=${tally.support}, conditional=${tally.conditional}, oppose=${tally.oppose}`
860
+ ]),
861
+ 5
862
+ );
863
+ const risks = take(
864
+ dedupe2([
865
+ ...proposals.flatMap((item) => item.risks),
866
+ ...critiques.flatMap((item) => item.openQuestions)
867
+ ]),
868
+ 5
869
+ );
870
+ const actions = take(dedupe2(proposals.flatMap((item) => item.actions)), 6);
871
+ const binaryQuestion = isBinaryQuestion2(input.context);
872
+ const executiveDecision = tally.support >= Math.max(tally.conditional, tally.oppose) ? binaryQuestion ? "yes" : "go" : tally.oppose > tally.support ? binaryQuestion ? "no" : "stop" : binaryQuestion ? "conditional" : "iterate";
873
+ return {
874
+ summary,
875
+ rationale: buildRationale(convergence, proposals),
876
+ tradeoffs: tradeoffs.length > 0 ? tradeoffs : ["Tradeoffs remain between speed of delivery and downside risk."],
877
+ risks: risks.length > 0 ? risks : ["Risk assumptions require validation with real implementation constraints."],
878
+ actions: actions.length > 0 ? actions : ["Assign an owner and timeline for the next validation step."],
879
+ confidence: scoreConfidence(convergence),
880
+ minorityReport: buildMinorityReport(convergence, critiques),
881
+ executiveDecision: {
882
+ decision: executiveDecision,
883
+ why: take(
884
+ dedupe2([
885
+ ...convergence.flatMap((item) => item.reasons),
886
+ ...proposals.map((item) => item.recommendation)
887
+ ]),
888
+ 3
889
+ ),
890
+ topRisks: take(
891
+ dedupe2([
892
+ ...proposals.flatMap((item) => item.risks),
893
+ ...critiques.flatMap((item) => item.openQuestions)
894
+ ]),
895
+ 3
896
+ ),
897
+ topActions: take(dedupe2(proposals.flatMap((item) => item.actions)), 5),
898
+ stopGoCriteria: "Choose the positive path only if evidence quality, risk controls, and fairness thresholds are met; otherwise choose the safer alternative or refine the plan."
899
+ }
900
+ };
901
+ };
902
+
903
+ // src/orchestration/run.ts
904
+ var ensureRoleKey = (expected, actual) => {
905
+ if (expected !== actual) {
906
+ throw new BadRequestError(
907
+ `Role key mismatch: expected ${expected}, got ${actual}`
908
+ );
909
+ }
910
+ };
911
+ var BUSINESS_JARGON = [
912
+ "kpi",
913
+ "sla",
914
+ "rollout",
915
+ "pilot",
916
+ "canary",
917
+ "funnel",
918
+ "activation",
919
+ "sponsor rev",
920
+ "market share",
921
+ "go to market"
922
+ ];
923
+ var countMatches = (text, terms) => terms.reduce((count, term) => text.includes(term) ? count + 1 : count, 0);
924
+ var isLikelyOffTopic = (input, output) => {
925
+ if (input.decisionType !== "general") {
926
+ return false;
927
+ }
928
+ const subjectText = `${input.title} ${input.context} ${input.goals.join(" ")} ${input.constraints.join(" ")}`.toLowerCase();
929
+ const outputText = [
930
+ output.summary,
931
+ output.rationale,
932
+ output.executiveDecision.stopGoCriteria,
933
+ ...output.executiveDecision.why,
934
+ ...output.actions
935
+ ].join(" ").toLowerCase();
936
+ const outputJargon = countMatches(outputText, BUSINESS_JARGON);
937
+ const subjectJargon = countMatches(subjectText, BUSINESS_JARGON);
938
+ return outputJargon >= 2 && subjectJargon === 0;
939
+ };
940
+ var runProposals = async ({ input, roles, provider }) => Promise.all(
941
+ roles.map(async (role) => {
942
+ const { system, prompt } = buildProposalPrompt(role, input);
943
+ const response = await provider.generate({
944
+ system,
945
+ prompt,
946
+ schema: ProposalOutputSchema
947
+ });
948
+ const output = normalizeProposalOutput(parseProposalOutput(response.output));
949
+ ensureRoleKey(role.key, output.roleKey);
950
+ return { ...response, output };
951
+ })
952
+ );
953
+ var runCritiques = async ({ input, roles, provider }, proposals) => Promise.all(
954
+ roles.map(async (role) => {
955
+ const { system, prompt } = buildCritiquePrompt(role, input, proposals);
956
+ const response = await provider.generate({
957
+ system,
958
+ prompt,
959
+ schema: CritiqueOutputSchema
960
+ });
961
+ const output = normalizeCritiqueOutput(parseCritiqueOutput(response.output));
962
+ ensureRoleKey(role.key, output.roleKey);
963
+ return { ...response, output };
964
+ })
965
+ );
966
+ var runConvergence = async ({ input, roles, provider }, proposals, critiques) => Promise.all(
967
+ roles.map(async (role) => {
968
+ const { system, prompt } = buildConvergencePrompt(
969
+ role,
970
+ input,
971
+ proposals,
972
+ critiques
973
+ );
974
+ const response = await provider.generate({
975
+ system,
976
+ prompt,
977
+ schema: ConvergenceOutputSchema
978
+ });
979
+ const output = normalizeConvergenceOutput(
980
+ parseConvergenceOutput(response.output)
981
+ );
982
+ ensureRoleKey(role.key, output.roleKey);
983
+ return { ...response, output };
984
+ })
985
+ );
986
+ var runDecisionRecord = async (provider, input, proposals, critiques, convergence) => {
987
+ const fallback = normalizeDecisionRecordOutput(
988
+ synthesizeDecisionRecord(input, proposals, critiques, convergence)
989
+ );
990
+ assertDecisionRecordQuality(fallback);
991
+ try {
992
+ const { system, prompt } = buildDecisionRecordPrompt(
993
+ input,
994
+ proposals,
995
+ critiques,
996
+ convergence
997
+ );
998
+ const response = await provider.generate({
999
+ system,
1000
+ prompt,
1001
+ schema: DecisionRecordSchema
1002
+ });
1003
+ const output = normalizeDecisionRecordOutput(
1004
+ parseDecisionRecord(response.output)
1005
+ );
1006
+ assertDecisionRecordQuality(output);
1007
+ if (isLikelyOffTopic(input, output)) {
1008
+ throw new BadRequestError("Decision record relevance: output drifted from the input subject");
1009
+ }
1010
+ return { ...response, output };
1011
+ } catch {
1012
+ return {
1013
+ output: fallback,
1014
+ raw: JSON.stringify(fallback),
1015
+ model: "deterministic-synth"
1016
+ };
1017
+ }
1018
+ };
1019
+ var buildDebateRounds = (proposals, critiques, convergence) => {
1020
+ const proposalRounds = proposals.map((proposal) => ({
1021
+ roundIndex: 1,
1022
+ roleKey: proposal.output.roleKey,
1023
+ output: serializeProposalOutput(proposal.output)
1024
+ }));
1025
+ const critiqueRounds = critiques.map((critique) => ({
1026
+ roundIndex: 2,
1027
+ roleKey: critique.output.roleKey,
1028
+ output: serializeCritiqueOutput(critique.output)
1029
+ }));
1030
+ const convergenceRounds = convergence.map((converged) => ({
1031
+ roundIndex: 3,
1032
+ roleKey: converged.output.roleKey,
1033
+ output: serializeConvergenceOutput(converged.output)
1034
+ }));
1035
+ return [...proposalRounds, ...critiqueRounds, ...convergenceRounds];
1036
+ };
1037
+ var runDebate = async ({
1038
+ input,
1039
+ roles,
1040
+ provider
1041
+ }) => {
1042
+ assertValidRoles(roles);
1043
+ const sanitizedInput = sanitizeDecisionInput(input);
1044
+ const proposals = await runProposals({
1045
+ input: sanitizedInput,
1046
+ roles,
1047
+ provider
1048
+ });
1049
+ const proposalOutputs = proposals.map((proposal) => proposal.output);
1050
+ const critiques = await runCritiques(
1051
+ { input: sanitizedInput, roles, provider },
1052
+ proposalOutputs
1053
+ );
1054
+ const critiqueOutputs = critiques.map((critique) => critique.output);
1055
+ const convergence = await runConvergence(
1056
+ { input: sanitizedInput, roles, provider },
1057
+ proposalOutputs,
1058
+ critiqueOutputs
1059
+ );
1060
+ const convergenceOutputs = convergence.map((result) => result.output);
1061
+ const decisionRecord = await runDecisionRecord(
1062
+ provider,
1063
+ sanitizedInput,
1064
+ proposalOutputs,
1065
+ critiqueOutputs,
1066
+ convergenceOutputs
1067
+ );
1068
+ const rounds = buildDebateRounds(proposals, critiques, convergence);
1069
+ return {
1070
+ input: sanitizedInput,
1071
+ proposals,
1072
+ critiques,
1073
+ convergence,
1074
+ decisionRecord,
1075
+ rounds
1076
+ };
1077
+ };
1078
+
1079
+ // src/jobs/run-decision.ts
1080
+ var toDecisionInput = (decision) => ({
1081
+ title: decision.title,
1082
+ context: decision.context,
1083
+ goals: decision.goals,
1084
+ constraints: decision.constraints,
1085
+ decisionType: decision.decisionType
1086
+ });
1087
+ var runDecisionJob = async (payload, context) => {
1088
+ const decision = await context.store.getDecision(payload.decisionId);
1089
+ if (!decision) {
1090
+ throw new NotFoundError(`Decision not found: ${payload.decisionId}`);
1091
+ }
1092
+ await context.store.saveDecisionRun({
1093
+ runId: payload.runId,
1094
+ decisionId: decision.id,
1095
+ status: "running"
1096
+ });
1097
+ await context.store.updateDecision(decision.id, {
1098
+ status: "running"
1099
+ });
1100
+ try {
1101
+ const run = await runDebate({
1102
+ input: toDecisionInput(decision),
1103
+ roles: context.roles,
1104
+ provider: context.provider
1105
+ });
1106
+ await context.store.saveDebateRounds(decision.id, run.rounds);
1107
+ await context.store.saveDecisionRecord(decision.id, run.decisionRecord.output);
1108
+ await context.store.saveDecisionRun({
1109
+ runId: payload.runId,
1110
+ decisionId: decision.id,
1111
+ status: "succeeded"
1112
+ });
1113
+ await context.store.updateDecision(decision.id, {
1114
+ status: "succeeded"
1115
+ });
1116
+ } catch (error) {
1117
+ await context.store.saveDecisionRun({
1118
+ runId: payload.runId,
1119
+ decisionId: decision.id,
1120
+ status: "failed"
1121
+ });
1122
+ await context.store.updateDecision(decision.id, {
1123
+ status: "failed"
1124
+ });
1125
+ throw error;
1126
+ }
1127
+ };
1128
+
1129
+ // src/persistence/memory-store.ts
1130
+ var cloneDecision = (decision) => ({
1131
+ ...decision,
1132
+ goals: [...decision.goals],
1133
+ constraints: [...decision.constraints]
1134
+ });
1135
+ var cloneRounds = (rounds) => rounds.map((round) => ({ ...round }));
1136
+ var cloneRun = (run) => ({ ...run });
1137
+ var MemoryDecisionStore = class {
1138
+ state;
1139
+ constructor() {
1140
+ this.state = {
1141
+ decisions: /* @__PURE__ */ new Map(),
1142
+ records: /* @__PURE__ */ new Map(),
1143
+ rounds: /* @__PURE__ */ new Map(),
1144
+ runs: /* @__PURE__ */ new Map(),
1145
+ runsByDecision: /* @__PURE__ */ new Map(),
1146
+ counter: 0
1147
+ };
1148
+ }
1149
+ async createDecision(input) {
1150
+ const id = this.nextId();
1151
+ const decision = {
1152
+ id,
1153
+ title: input.title,
1154
+ context: input.context,
1155
+ goals: [...input.goals],
1156
+ constraints: [...input.constraints],
1157
+ decisionType: input.decisionType,
1158
+ status: DEFAULT_STATUS,
1159
+ visibility: input.visibility ?? DEFAULT_VISIBILITY
1160
+ };
1161
+ this.state.decisions.set(id, decision);
1162
+ return cloneDecision(decision);
1163
+ }
1164
+ async updateDecision(id, update) {
1165
+ const current = this.state.decisions.get(id);
1166
+ if (!current) {
1167
+ throw new NotFoundError(`Decision not found: ${id}`);
1168
+ }
1169
+ const next = {
1170
+ ...current,
1171
+ status: update.status ?? current.status,
1172
+ visibility: update.visibility ?? current.visibility
1173
+ };
1174
+ this.state.decisions.set(id, next);
1175
+ return cloneDecision(next);
1176
+ }
1177
+ async saveDecisionRecord(id, record) {
1178
+ if (!this.state.decisions.has(id)) {
1179
+ throw new NotFoundError(`Decision not found: ${id}`);
1180
+ }
1181
+ this.state.records.set(id, { ...record });
1182
+ }
1183
+ async saveDebateRounds(id, rounds) {
1184
+ if (!this.state.decisions.has(id)) {
1185
+ throw new NotFoundError(`Decision not found: ${id}`);
1186
+ }
1187
+ this.state.rounds.set(id, cloneRounds(rounds));
1188
+ }
1189
+ async getDecision(id) {
1190
+ const decision = this.state.decisions.get(id);
1191
+ return decision ? cloneDecision(decision) : null;
1192
+ }
1193
+ async getDecisionRecord(id) {
1194
+ const record = this.state.records.get(id);
1195
+ return record ? { ...record } : null;
1196
+ }
1197
+ async getDebateRounds(id) {
1198
+ return cloneRounds(this.state.rounds.get(id) ?? []);
1199
+ }
1200
+ async saveDecisionRun(run) {
1201
+ if (!this.state.decisions.has(run.decisionId)) {
1202
+ throw new NotFoundError(`Decision not found: ${run.decisionId}`);
1203
+ }
1204
+ this.state.runs.set(run.runId, { ...run });
1205
+ const existing = this.state.runsByDecision.get(run.decisionId) ?? [];
1206
+ const next = existing.includes(run.runId) ? existing : [...existing, run.runId];
1207
+ this.state.runsByDecision.set(run.decisionId, next);
1208
+ }
1209
+ async getDecisionRun(runId) {
1210
+ const run = this.state.runs.get(runId);
1211
+ return run ? cloneRun(run) : null;
1212
+ }
1213
+ async listDecisionRuns(decisionId) {
1214
+ const runIds = this.state.runsByDecision.get(decisionId) ?? [];
1215
+ return runIds.map((runId) => this.state.runs.get(runId)).filter((run) => Boolean(run)).map(cloneRun);
1216
+ }
1217
+ nextId() {
1218
+ this.state.counter += 1;
1219
+ return `decision_${this.state.counter}`;
1220
+ }
1221
+ };
1222
+
1223
+ // src/cli/tui-ui/input-parser.ts
1224
+ var defaultGoal = "Produce a clear, defensible answer with transparent tradeoffs.";
1225
+ var defaultConstraint = "Use balanced reasoning and explicit assumptions.";
1226
+ var parseList = (raw) => raw.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
1227
+ var normalizePhrase = (value) => value.replace(/[?!.,;:]+$/g, "").replace(/\s+/g, " ").trim();
1228
+ var toWords = (value) => value.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((part) => part.length > 0);
1229
+ var inferDecisionType = (value) => {
1230
+ const words = new Set(toWords(value));
1231
+ if (words.has("engineer") || words.has("engineering") || words.has("infra") || words.has("architecture") || words.has("technical")) {
1232
+ return "engineering";
1233
+ }
1234
+ if (words.has("hire") || words.has("hiring") || words.has("recruit") || words.has("candidate") || words.has("team")) {
1235
+ return "hiring";
1236
+ }
1237
+ if (words.has("growth") || words.has("acquisition") || words.has("marketing") || words.has("funnel") || words.has("activation")) {
1238
+ return "growth";
1239
+ }
1240
+ if (words.has("product") || words.has("roadmap") || words.has("feature") || words.has("launch") || words.has("ux")) {
1241
+ return "product";
1242
+ }
1243
+ return "general";
1244
+ };
1245
+ var buildTitleFromSituation = (value) => {
1246
+ const firstSentence = value.split(/[.!?]/)[0]?.trim() ?? value.trim();
1247
+ if (firstSentence.length <= 90) {
1248
+ return firstSentence;
1249
+ }
1250
+ return `${firstSentence.slice(0, 87).trim()}...`;
1251
+ };
1252
+ var extractQuotedList = (source, keyword) => {
1253
+ const regex = new RegExp(`${keyword}\\s*:\\s*([^.;\\n]+)`, "i");
1254
+ const match = source.match(regex);
1255
+ if (!match) {
1256
+ return [];
1257
+ }
1258
+ return parseList(match[1]).map(normalizePhrase).filter((part) => part.length > 0);
1259
+ };
1260
+ var inferConstraints = (value) => {
1261
+ const explicit = extractQuotedList(value, "constraints?");
1262
+ if (explicit.length > 0) {
1263
+ return explicit;
1264
+ }
1265
+ const fragments = [];
1266
+ const patterns = [
1267
+ /within\s+[^,.;]+/gi,
1268
+ /by\s+[^,.;]+/gi,
1269
+ /no\s+[^,.;]+/gi,
1270
+ /without\s+[^,.;]+/gi,
1271
+ /must\s+[^,.;]+/gi,
1272
+ /cannot\s+[^,.;]+/gi,
1273
+ /can\'t\s+[^,.;]+/gi
1274
+ ];
1275
+ for (const pattern of patterns) {
1276
+ const matches = value.match(pattern) ?? [];
1277
+ for (const match of matches) {
1278
+ const next = normalizePhrase(match);
1279
+ if (next.length > 0) {
1280
+ fragments.push(next);
1281
+ }
1282
+ }
1283
+ }
1284
+ if (fragments.length > 0) {
1285
+ const unique = [...new Set(fragments.map((item) => item.toLowerCase()))];
1286
+ const compact = unique.filter(
1287
+ (item) => !unique.some((other) => other !== item && item.includes(other))
1288
+ );
1289
+ return compact.map((item) => item[0].toUpperCase() + item.slice(1)).slice(0, 3);
1290
+ }
1291
+ const bits = value.split(/[.;]/).map((part) => part.trim()).filter((part) => part.length > 0);
1292
+ const keywords = ["within", "by", "deadline", "budget", "no ", "cannot", "can't", "must"];
1293
+ const inferred = bits.filter(
1294
+ (part) => keywords.some((keyword) => part.toLowerCase().includes(keyword))
1295
+ );
1296
+ return inferred.length > 0 ? inferred.map(normalizePhrase).filter((part) => part.length > 0).slice(0, 3) : [defaultConstraint];
1297
+ };
1298
+ var inferGoals = (value) => {
1299
+ const explicit = extractQuotedList(value, "goals?");
1300
+ if (explicit.length > 0) {
1301
+ return explicit;
1302
+ }
1303
+ const lower = value.toLowerCase();
1304
+ const markers = [" to ", " so that ", " in order to "];
1305
+ for (const marker of markers) {
1306
+ const index = lower.indexOf(marker);
1307
+ if (index >= 0) {
1308
+ const goal = value.slice(index + marker.length).replace(/\b(within|by|with no|without|no|must|cannot|can't)\b[\s\S]*$/i, "");
1309
+ const normalizedGoal = normalizePhrase(goal);
1310
+ if (normalizedGoal.length > 0) {
1311
+ return [normalizedGoal.slice(0, 180)];
1312
+ }
1313
+ }
1314
+ }
1315
+ return [defaultGoal];
1316
+ };
1317
+ var buildInputFromSituation = (situation) => ({
1318
+ title: buildTitleFromSituation(situation),
1319
+ context: situation.trim(),
1320
+ goals: inferGoals(situation),
1321
+ constraints: inferConstraints(situation),
1322
+ decisionType: inferDecisionType(situation)
1323
+ });
1324
+
1325
+ // src/cli/tui-ui/runner.ts
1326
+ import { mkdir, writeFile } from "fs/promises";
1327
+ import path from "path";
1328
+
1329
+ // src/api/handlers.ts
1330
+ import { z as z6 } from "zod";
1331
+
1332
+ // src/api/schemas.ts
1333
+ import { z as z5 } from "zod";
1334
+ var CreateDecisionRequestSchema = z5.object({
1335
+ input: DecisionInputSchema,
1336
+ visibility: VisibilitySchema.optional()
1337
+ });
1338
+ var CreateDecisionResponseSchema = z5.object({
1339
+ decisionId: z5.string().min(1),
1340
+ runId: z5.string().min(1),
1341
+ status: DecisionStatusSchema
1342
+ });
1343
+ var GetDecisionResponseSchema = z5.object({
1344
+ decision: DecisionSchema,
1345
+ record: DecisionRecordSchema.nullable(),
1346
+ rounds: z5.array(DebateRoundSchema),
1347
+ runs: z5.array(DecisionRunSchema)
1348
+ });
1349
+ var ErrorResponseSchema = z5.object({
1350
+ error: z5.string().min(1),
1351
+ code: z5.enum(["bad_request", "not_found", "internal_error"])
1352
+ });
1353
+
1354
+ // src/api/validate.ts
1355
+ var parseCreateDecisionRequest = (value) => CreateDecisionRequestSchema.parse(value);
1356
+
1357
+ // src/api/service.ts
1358
+ var createDecision = async (request, context) => {
1359
+ const parsed = parseCreateDecisionRequest(request);
1360
+ const input = sanitizeDecisionInput(parsed.input);
1361
+ const decision = await context.store.createDecision({
1362
+ ...input,
1363
+ visibility: parsed.visibility ?? DEFAULT_VISIBILITY
1364
+ });
1365
+ const runId = context.generateRunId();
1366
+ await context.store.saveDecisionRun({
1367
+ runId,
1368
+ decisionId: decision.id,
1369
+ status: "queued"
1370
+ });
1371
+ await context.queue.enqueueDecision({
1372
+ decisionId: decision.id,
1373
+ runId
1374
+ });
1375
+ return {
1376
+ decisionId: decision.id,
1377
+ runId,
1378
+ status: decision.status
1379
+ };
1380
+ };
1381
+ var getDecision = async (decisionId, context) => {
1382
+ const decision = await context.store.getDecision(decisionId);
1383
+ if (!decision) {
1384
+ throw new NotFoundError(`Decision not found: ${decisionId}`);
1385
+ }
1386
+ const record = await context.store.getDecisionRecord(decisionId);
1387
+ const rounds = await context.store.getDebateRounds(decisionId);
1388
+ const runs = await context.store.listDecisionRuns(decisionId);
1389
+ return {
1390
+ decision,
1391
+ record,
1392
+ rounds,
1393
+ runs
1394
+ };
1395
+ };
1396
+
1397
+ // src/cli/tui-ui/runner.ts
1398
+ var parseRoundOutput = (raw) => {
1399
+ try {
1400
+ return JSON.parse(raw);
1401
+ } catch {
1402
+ return raw;
1403
+ }
1404
+ };
1405
+ var saveArtifact = async (input, decisionId, result) => {
1406
+ const artifact = {
1407
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1408
+ decisionId,
1409
+ input,
1410
+ status: result.decision.status,
1411
+ rounds: result.rounds.map((round) => ({
1412
+ roundIndex: round.roundIndex,
1413
+ roleKey: round.roleKey,
1414
+ output: parseRoundOutput(round.output)
1415
+ })),
1416
+ record: result.record,
1417
+ runs: result.runs
1418
+ };
1419
+ const dir = path.resolve(process.cwd(), "artifacts");
1420
+ await mkdir(dir, { recursive: true });
1421
+ const stamp = artifact.generatedAt.replace(/[:.]/g, "-");
1422
+ const filePath = path.join(dir, `${decisionId}-${stamp}.json`);
1423
+ await writeFile(filePath, JSON.stringify(artifact, null, 2), "utf8");
1424
+ return filePath;
1425
+ };
1426
+ var runDecisionPipeline = async (input, session, onProgress) => {
1427
+ const apiContext = {
1428
+ store: session.store,
1429
+ queue: session.queue,
1430
+ generateRunId: () => {
1431
+ session.runCounter += 1;
1432
+ return `run_${session.runCounter}`;
1433
+ }
1434
+ };
1435
+ onProgress("Creating decision...");
1436
+ const created = await createDecision({ input }, apiContext);
1437
+ const pending = session.queue.getPending();
1438
+ session.queue.clear();
1439
+ onProgress(`Queued ${pending.length} job(s).`);
1440
+ for (const payload of pending) {
1441
+ onProgress(`Running ${payload.runId}...`);
1442
+ await runDecisionJob(payload, {
1443
+ provider: session.provider,
1444
+ store: session.store,
1445
+ roles: session.roles
1446
+ });
1447
+ onProgress(`${payload.runId} completed.`);
1448
+ }
1449
+ const result = await getDecision(created.decisionId, apiContext);
1450
+ const artifactPath = await saveArtifact(input, created.decisionId, result);
1451
+ onProgress(`Artifact saved: ${artifactPath}`);
1452
+ return {
1453
+ decisionId: created.decisionId,
1454
+ result,
1455
+ artifactPath
1456
+ };
1457
+ };
1458
+
1459
+ // src/cli/tui-ui/state.ts
1460
+ var createInitialState = (mode) => ({
1461
+ mode,
1462
+ runState: "idle",
1463
+ showAudit: false,
1464
+ showDetails: false,
1465
+ logs: ["Ready. Type a prompt and press Enter to run"],
1466
+ currentInput: null,
1467
+ currentResult: null,
1468
+ history: [],
1469
+ selectedHistoryIndex: 0,
1470
+ statusMessage: "Ready",
1471
+ commandHint: "Enter run i focus prompt e guided edit a audit d details m model r rerun [ ] history tab focus q quit"
1472
+ });
1473
+
1474
+ // src/cli/tui-ui/app.ts
1475
+ var BRAND_ASCII = [
1476
+ " ____ _____ ____ _ ___ _____ _ ____ _ _____ ",
1477
+ "| _ \\| ____| __ ) / \\ |_ _|_ _|/ \\ | __ )| | | ____|",
1478
+ "| | | | _| | _ \\ / _ \\ | | | | / _ \\ | _ \\| | | _| ",
1479
+ "| |_| | |___| |_) / ___ \\ | | | |/ ___ \\| |_) | |___| |___ ",
1480
+ "|____/|_____|____/_/ \\_\\___| |_/_/ \\_\\____/|_____|_____|"
1481
+ ].join("\n");
1482
+ var THEME = {
1483
+ bg: "#16142a",
1484
+ panelBg: "#211a3f",
1485
+ panelBorder: "#8a73ff",
1486
+ focusBorder: "#ffffff",
1487
+ brandFg: "#b8a7ff",
1488
+ primaryText: "#efeaff",
1489
+ secondaryText: "#cfc5f4",
1490
+ selectedBg: "#ffffff",
1491
+ selectedFg: "#18142a"
1492
+ };
1493
+ var trimForLine = (value, max = 80) => value.length <= max ? value : `${value.slice(0, max - 3)}...`;
1494
+ var CONTEXT_TIP = "Include context, goals, constraints, budget, timeline, and must-not-fail risks.";
1495
+ var ENV_FILE_PATH = path2.resolve(process.cwd(), ".env");
1496
+ var SESSION_FILE_PATH = path2.resolve(process.cwd(), ".debaitable-session.json");
1497
+ var upsertEnvLine = (source, key, value) => {
1498
+ const safeValue = value.replace(/\r?\n/g, "").trim();
1499
+ const lines = source.length > 0 ? source.split(/\r?\n/) : [];
1500
+ const prefix = `${key}=`;
1501
+ let found = false;
1502
+ const updated = lines.map((line) => {
1503
+ if (line.startsWith(prefix)) {
1504
+ found = true;
1505
+ return `${prefix}${safeValue}`;
1506
+ }
1507
+ return line;
1508
+ });
1509
+ if (!found) {
1510
+ updated.push(`${prefix}${safeValue}`);
1511
+ }
1512
+ return `${updated.filter((line) => line.length > 0).join("\n")}
1513
+ `;
1514
+ };
1515
+ var hydrateEnvFromFile = async () => {
1516
+ let source = "";
1517
+ try {
1518
+ source = await readFile(ENV_FILE_PATH, "utf8");
1519
+ } catch (error) {
1520
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
1521
+ throw error;
1522
+ }
1523
+ return;
1524
+ }
1525
+ for (const rawLine of source.split(/\r?\n/)) {
1526
+ const line = rawLine.trim();
1527
+ if (!line || line.startsWith("#")) {
1528
+ continue;
1529
+ }
1530
+ const equalIndex = line.indexOf("=");
1531
+ if (equalIndex <= 0) {
1532
+ continue;
1533
+ }
1534
+ const key = line.slice(0, equalIndex).trim();
1535
+ let value = line.slice(equalIndex + 1).trim();
1536
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1537
+ value = value.slice(1, -1);
1538
+ }
1539
+ if (!process.env[key]) {
1540
+ process.env[key] = value;
1541
+ }
1542
+ }
1543
+ };
1544
+ var persistOpenAiEnv = async (apiKey) => {
1545
+ let current = "";
1546
+ try {
1547
+ current = await readFile(ENV_FILE_PATH, "utf8");
1548
+ } catch (error) {
1549
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
1550
+ throw error;
1551
+ }
1552
+ }
1553
+ let next = upsertEnvLine(current, "OPENAI_API_KEY", apiKey);
1554
+ next = upsertEnvLine(next, "OPENAI_MODEL", process.env.OPENAI_MODEL?.trim() || "gpt-5");
1555
+ await writeFile2(ENV_FILE_PATH, next, "utf8");
1556
+ await chmod(ENV_FILE_PATH, 384);
1557
+ };
1558
+ var loadPersistedHistory = async () => {
1559
+ let source = "";
1560
+ try {
1561
+ source = await readFile(SESSION_FILE_PATH, "utf8");
1562
+ } catch (error) {
1563
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
1564
+ throw error;
1565
+ }
1566
+ return [];
1567
+ }
1568
+ try {
1569
+ const parsed = JSON.parse(source);
1570
+ if (!Array.isArray(parsed)) {
1571
+ return [];
1572
+ }
1573
+ return parsed;
1574
+ } catch {
1575
+ return [];
1576
+ }
1577
+ };
1578
+ var persistHistory = async (history) => {
1579
+ await writeFile2(SESSION_FILE_PATH, `${JSON.stringify(history, null, 2)}
1580
+ `, "utf8");
1581
+ };
1582
+ var ensureOpenAiApiKey = async () => {
1583
+ if (process.env.OPENAI_API_KEY?.trim()) {
1584
+ return;
1585
+ }
1586
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1587
+ return;
1588
+ }
1589
+ const apiKey = (await promptHiddenInput(
1590
+ "OPENAI_API_KEY not found. Paste key to enable OpenAI mode (or press Enter to skip): "
1591
+ )).trim();
1592
+ if (!apiKey) {
1593
+ return;
1594
+ }
1595
+ process.env.OPENAI_API_KEY = apiKey;
1596
+ process.env.OPENAI_MODEL = process.env.OPENAI_MODEL?.trim() || "gpt-5";
1597
+ await persistOpenAiEnv(apiKey);
1598
+ console.log("Saved OPENAI_API_KEY to .env");
1599
+ };
1600
+ var promptHiddenInput = async (label) => {
1601
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1602
+ return "";
1603
+ }
1604
+ process.stdout.write(label);
1605
+ const stdin = process.stdin;
1606
+ const wasRaw = stdin.isRaw;
1607
+ stdin.setEncoding("utf8");
1608
+ if (typeof stdin.setRawMode === "function") {
1609
+ stdin.setRawMode(true);
1610
+ }
1611
+ stdin.resume();
1612
+ return new Promise((resolve, reject) => {
1613
+ let value = "";
1614
+ const cleanup = () => {
1615
+ stdin.off("data", onData);
1616
+ if (typeof stdin.setRawMode === "function") {
1617
+ stdin.setRawMode(Boolean(wasRaw));
1618
+ }
1619
+ stdin.pause();
1620
+ process.stdout.write("\n");
1621
+ };
1622
+ const onData = (chunk) => {
1623
+ if (chunk === "") {
1624
+ cleanup();
1625
+ reject(new Error("Input cancelled"));
1626
+ return;
1627
+ }
1628
+ if (chunk === "\r" || chunk === "\n") {
1629
+ cleanup();
1630
+ resolve(value);
1631
+ return;
1632
+ }
1633
+ if (chunk === "\x7F" || chunk === "\b") {
1634
+ if (value.length > 0) {
1635
+ value = value.slice(0, -1);
1636
+ }
1637
+ return;
1638
+ }
1639
+ if (chunk.startsWith("\x1B")) {
1640
+ return;
1641
+ }
1642
+ value += chunk;
1643
+ process.stdout.write("*");
1644
+ };
1645
+ stdin.on("data", onData);
1646
+ });
1647
+ };
1648
+ var renderResult = (state) => {
1649
+ if (!state.currentResult?.record) {
1650
+ return "No result yet. Run a decision to see output.";
1651
+ }
1652
+ const record = state.currentResult.record;
1653
+ const lines = [];
1654
+ lines.push("{bold}Final Decision{/bold}");
1655
+ lines.push(`Decision: ${record.executiveDecision.decision.toUpperCase()}`);
1656
+ lines.push(`Confidence: ${record.confidence}`);
1657
+ lines.push(`Summary: ${record.summary}`);
1658
+ lines.push("");
1659
+ lines.push(`Decision Criteria: ${record.executiveDecision.stopGoCriteria}`);
1660
+ lines.push("Reasoning:");
1661
+ let reasoningIndex = 1;
1662
+ for (const item of record.executiveDecision.why) {
1663
+ lines.push(`${reasoningIndex}. ${item}`);
1664
+ reasoningIndex += 1;
1665
+ }
1666
+ lines.push("Top Actions:");
1667
+ for (const item of record.executiveDecision.topActions.slice(0, 4)) {
1668
+ lines.push(`- ${item}`);
1669
+ }
1670
+ if (state.showDetails) {
1671
+ lines.push("");
1672
+ lines.push("{bold}Details{/bold}");
1673
+ lines.push(`Rationale: ${record.rationale}`);
1674
+ lines.push("Top Risks:");
1675
+ for (const item of record.executiveDecision.topRisks) {
1676
+ lines.push(`- ${item}`);
1677
+ }
1678
+ lines.push("Tradeoffs:");
1679
+ for (const item of record.tradeoffs) {
1680
+ lines.push(`- ${item}`);
1681
+ }
1682
+ lines.push("Risks:");
1683
+ for (const item of record.risks) {
1684
+ lines.push(`- ${item}`);
1685
+ }
1686
+ lines.push("Actions:");
1687
+ for (const item of record.actions) {
1688
+ lines.push(`- ${item}`);
1689
+ }
1690
+ lines.push(`Minority: ${record.minorityReport}`);
1691
+ }
1692
+ if (state.showAudit) {
1693
+ lines.push("");
1694
+ lines.push("{bold}Audit Timeline{/bold}");
1695
+ for (const round of state.currentResult.rounds) {
1696
+ lines.push(`- Round ${round.roundIndex} | ${round.roleKey}`);
1697
+ }
1698
+ } else {
1699
+ lines.push("{gray-fg}[a] Show audit timeline{/gray-fg}");
1700
+ }
1701
+ return lines.join("\n");
1702
+ };
1703
+ var renderHistoryLabel = (item) => `${item.decision.toUpperCase()} | ${trimForLine(item.title, 40)}`;
1704
+ var DecisionTuiApp = class {
1705
+ screen;
1706
+ inputBox;
1707
+ tipBox;
1708
+ outputBox;
1709
+ historyBox;
1710
+ helpModal;
1711
+ footer;
1712
+ loadingTimer = null;
1713
+ loadingFrame = 0;
1714
+ suppressFocusHighlight = false;
1715
+ state;
1716
+ session;
1717
+ constructor(history) {
1718
+ const defaultMode = process.env.OPENAI_API_KEY ? "openai" : "heuristic";
1719
+ this.state = createInitialState(defaultMode);
1720
+ this.state.history = history;
1721
+ this.session = {
1722
+ store: new MemoryDecisionStore(),
1723
+ queue: new MemoryDecisionQueue(),
1724
+ provider: this.makeProvider(defaultMode),
1725
+ roles: roleDefinitions,
1726
+ runCounter: 0
1727
+ };
1728
+ this.screen = blessed.screen({
1729
+ smartCSR: true,
1730
+ title: "DebAItable TUI",
1731
+ dockBorders: true,
1732
+ fullUnicode: true
1733
+ });
1734
+ blessed.box({
1735
+ parent: this.screen,
1736
+ top: 0,
1737
+ left: 0,
1738
+ width: "100%",
1739
+ height: 7,
1740
+ tags: true,
1741
+ style: { fg: THEME.brandFg, bg: THEME.panelBg },
1742
+ content: `${BRAND_ASCII}
1743
+ Make specialized, trained LLMs debate to achieve a refined consensus.`
1744
+ });
1745
+ this.inputBox = blessed.textarea({
1746
+ parent: this.screen,
1747
+ top: 7,
1748
+ left: 0,
1749
+ width: "34%",
1750
+ height: 6,
1751
+ inputOnFocus: true,
1752
+ keys: true,
1753
+ mouse: true,
1754
+ label: " Prompt ",
1755
+ border: "line",
1756
+ style: { border: { fg: THEME.panelBorder }, fg: THEME.primaryText, bg: THEME.bg },
1757
+ value: "",
1758
+ scrollable: true,
1759
+ alwaysScroll: true,
1760
+ scrollbar: { ch: " " },
1761
+ wrap: true
1762
+ });
1763
+ this.inputBox.key(["enter"], () => {
1764
+ void this.runCurrentInput();
1765
+ return false;
1766
+ });
1767
+ this.tipBox = blessed.box({
1768
+ parent: this.screen,
1769
+ top: 13,
1770
+ left: 0,
1771
+ width: "34%",
1772
+ height: 4,
1773
+ label: " Context Tip ",
1774
+ tags: true,
1775
+ border: "line",
1776
+ style: { border: { fg: THEME.panelBorder }, fg: THEME.primaryText, bg: THEME.bg },
1777
+ content: CONTEXT_TIP
1778
+ });
1779
+ this.historyBox = blessed.list({
1780
+ parent: this.screen,
1781
+ top: 17,
1782
+ left: 0,
1783
+ width: "34%",
1784
+ bottom: 5,
1785
+ label: " Session History ",
1786
+ border: "line",
1787
+ keys: false,
1788
+ vi: false,
1789
+ mouse: true,
1790
+ style: {
1791
+ border: { fg: THEME.panelBorder },
1792
+ fg: THEME.primaryText,
1793
+ bg: THEME.bg,
1794
+ selected: { bg: THEME.selectedBg, fg: THEME.selectedFg }
1795
+ },
1796
+ items: ["No decisions yet"]
1797
+ });
1798
+ this.historyBox.on("click", () => {
1799
+ this.focusHistory();
1800
+ });
1801
+ this.historyBox.on("select", (_item, index) => {
1802
+ this.state.selectedHistoryIndex = Number(index);
1803
+ this.previewHistorySelection();
1804
+ this.render();
1805
+ });
1806
+ this.outputBox = blessed.box({
1807
+ parent: this.screen,
1808
+ top: 7,
1809
+ left: "34%",
1810
+ width: "66%",
1811
+ bottom: 5,
1812
+ label: " Output ",
1813
+ border: "line",
1814
+ style: { border: { fg: THEME.panelBorder }, fg: THEME.primaryText, bg: THEME.bg },
1815
+ scrollable: true,
1816
+ alwaysScroll: true,
1817
+ tags: true,
1818
+ keys: true,
1819
+ vi: true,
1820
+ mouse: true,
1821
+ content: renderResult(this.state)
1822
+ });
1823
+ this.outputBox.on("click", () => {
1824
+ this.focusOutput();
1825
+ });
1826
+ this.helpModal = blessed.box({
1827
+ parent: this.screen,
1828
+ width: "58%",
1829
+ height: 10,
1830
+ top: "center",
1831
+ left: "center",
1832
+ label: " Help ",
1833
+ border: "line",
1834
+ tags: true,
1835
+ hidden: true,
1836
+ style: {
1837
+ border: { fg: THEME.focusBorder },
1838
+ fg: THEME.primaryText,
1839
+ bg: THEME.panelBg
1840
+ },
1841
+ content: [
1842
+ " Core controls",
1843
+ " [Enter] Run decision",
1844
+ " [Arrows] Move between prompt, history, and output",
1845
+ " [A] Toggle audit timeline",
1846
+ " [M] Toggle model (if OPENAI_API_KEY exists)",
1847
+ " [Q] Quit [Ctrl+C] Force quit",
1848
+ " [?] Toggle this help"
1849
+ ].join("\n")
1850
+ });
1851
+ this.helpModal.on("click", () => {
1852
+ this.helpModal.hide();
1853
+ this.render();
1854
+ });
1855
+ this.footer = blessed.box({
1856
+ parent: this.screen,
1857
+ bottom: 0,
1858
+ left: 0,
1859
+ width: "100%",
1860
+ height: 4,
1861
+ tags: true,
1862
+ style: { fg: THEME.secondaryText, bg: THEME.panelBg },
1863
+ content: this.renderFooterContent()
1864
+ });
1865
+ this.bindKeys();
1866
+ this.render();
1867
+ this.focusInput();
1868
+ }
1869
+ makeProvider(mode) {
1870
+ return mode === "openai" ? createOpenAiProvider() : new HeuristicDebateProvider();
1871
+ }
1872
+ isTypingInPrompt() {
1873
+ return this.screen.focused === this.inputBox;
1874
+ }
1875
+ stopPromptEditing() {
1876
+ ;
1877
+ this.inputBox.cancel?.();
1878
+ }
1879
+ focusHistory() {
1880
+ this.stopPromptEditing();
1881
+ this.suppressFocusHighlight = false;
1882
+ this.historyBox.focus();
1883
+ this.updateFocusStyles();
1884
+ this.screen.render();
1885
+ }
1886
+ focusOutput() {
1887
+ this.stopPromptEditing();
1888
+ this.suppressFocusHighlight = false;
1889
+ this.outputBox.focus();
1890
+ this.updateFocusStyles();
1891
+ this.screen.render();
1892
+ }
1893
+ updateFocusStyles() {
1894
+ if (this.suppressFocusHighlight) {
1895
+ ;
1896
+ this.inputBox.style.border = {
1897
+ fg: THEME.panelBorder
1898
+ };
1899
+ this.historyBox.style.border = {
1900
+ fg: THEME.panelBorder
1901
+ };
1902
+ this.outputBox.style.border = {
1903
+ fg: THEME.panelBorder
1904
+ };
1905
+ return;
1906
+ }
1907
+ const focused = this.screen.focused;
1908
+ this.inputBox.style.border = {
1909
+ fg: focused === this.inputBox ? THEME.focusBorder : THEME.panelBorder
1910
+ };
1911
+ this.historyBox.style.border = {
1912
+ fg: focused === this.historyBox ? THEME.focusBorder : THEME.panelBorder
1913
+ };
1914
+ this.outputBox.style.border = {
1915
+ fg: focused === this.outputBox ? THEME.focusBorder : THEME.panelBorder
1916
+ };
1917
+ }
1918
+ renderFooterContent() {
1919
+ const mode = this.state.mode.toUpperCase();
1920
+ return [
1921
+ ` Status: ${this.state.statusMessage} | Mode: ${mode}`,
1922
+ " [?] Help [Q] Quit"
1923
+ ].join("\n");
1924
+ }
1925
+ setStatus(value) {
1926
+ this.state.statusMessage = value;
1927
+ this.footer.setContent(this.renderFooterContent());
1928
+ }
1929
+ focusInput() {
1930
+ this.suppressFocusHighlight = false;
1931
+ if (this.screen.focused !== this.inputBox) {
1932
+ this.inputBox.focus();
1933
+ }
1934
+ this.updateFocusStyles();
1935
+ this.screen.render();
1936
+ }
1937
+ getHistorySelectionIndex() {
1938
+ const rawSelected = this.historyBox.selected;
1939
+ if (typeof rawSelected === "number" && rawSelected >= 0) {
1940
+ return Math.min(rawSelected, Math.max(this.state.history.length - 1, 0));
1941
+ }
1942
+ return this.state.selectedHistoryIndex;
1943
+ }
1944
+ previewHistorySelection() {
1945
+ const item = this.state.history[this.state.selectedHistoryIndex];
1946
+ if (!item) {
1947
+ return;
1948
+ }
1949
+ this.state.currentResult = item.result;
1950
+ this.state.currentInput = item.input;
1951
+ }
1952
+ log(message) {
1953
+ this.state.logs = [...this.state.logs.slice(-50), message];
1954
+ this.setStatus(message);
1955
+ }
1956
+ updateHistory() {
1957
+ if (this.state.history.length === 0) {
1958
+ this.historyBox.setItems(["No decisions yet"]);
1959
+ this.state.selectedHistoryIndex = 0;
1960
+ return;
1961
+ }
1962
+ this.historyBox.setItems(this.state.history.map(renderHistoryLabel));
1963
+ const safeIndex = Math.min(this.state.selectedHistoryIndex, this.state.history.length - 1);
1964
+ this.state.selectedHistoryIndex = Math.max(safeIndex, 0);
1965
+ this.historyBox.select(this.state.selectedHistoryIndex);
1966
+ }
1967
+ render() {
1968
+ this.tipBox.setContent(CONTEXT_TIP);
1969
+ if (this.state.runState === "running") {
1970
+ this.outputBox.setContent(this.renderLoadingOutput());
1971
+ } else {
1972
+ this.outputBox.setContent(renderResult(this.state));
1973
+ }
1974
+ this.footer.setContent(this.renderFooterContent());
1975
+ this.updateHistory();
1976
+ this.updateFocusStyles();
1977
+ this.screen.render();
1978
+ }
1979
+ renderLoadingOutput() {
1980
+ const dots = ".".repeat(this.loadingFrame % 4 + 1).padEnd(4, " ");
1981
+ const spinner = ["|", "/", "-", "\\"][this.loadingFrame % 4];
1982
+ return [`{bold}${spinner} Running debate ${dots}{/bold}`].join("\n");
1983
+ }
1984
+ startLoadingAnimation() {
1985
+ if (this.loadingTimer) {
1986
+ return;
1987
+ }
1988
+ this.loadingTimer = setInterval(() => {
1989
+ if (this.state.runState !== "running") {
1990
+ return;
1991
+ }
1992
+ this.loadingFrame += 1;
1993
+ this.outputBox.setContent(this.renderLoadingOutput());
1994
+ this.screen.render();
1995
+ }, 180);
1996
+ }
1997
+ stopLoadingAnimation() {
1998
+ if (!this.loadingTimer) {
1999
+ return;
2000
+ }
2001
+ clearInterval(this.loadingTimer);
2002
+ this.loadingTimer = null;
2003
+ this.loadingFrame = 0;
2004
+ }
2005
+ async runCurrentInput() {
2006
+ if (this.state.runState === "running") {
2007
+ return;
2008
+ }
2009
+ const prompt = this.inputBox.getValue().trim();
2010
+ if (!prompt) {
2011
+ this.setStatus("Type a prompt first.");
2012
+ this.render();
2013
+ return;
2014
+ }
2015
+ const input = buildInputFromSituation(prompt);
2016
+ this.state.currentInput = input;
2017
+ this.state.runState = "running";
2018
+ this.startLoadingAnimation();
2019
+ this.log(`Running decision in ${this.state.mode.toUpperCase()} mode...`);
2020
+ this.render();
2021
+ try {
2022
+ const run = await runDecisionPipeline(input, this.session, (message) => this.log(message));
2023
+ const recordDecision = run.result.record?.executiveDecision.decision ?? "unknown";
2024
+ this.state.currentResult = run.result;
2025
+ this.state.history = [
2026
+ {
2027
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2028
+ decisionId: run.decisionId,
2029
+ title: input.title,
2030
+ decision: recordDecision,
2031
+ artifactPath: run.artifactPath,
2032
+ input,
2033
+ result: run.result
2034
+ },
2035
+ ...this.state.history
2036
+ ].slice(0, 50);
2037
+ await persistHistory(this.state.history);
2038
+ this.state.selectedHistoryIndex = 0;
2039
+ this.state.runState = "done";
2040
+ this.setStatus(`Done. ${recordDecision.toUpperCase()} | ${run.artifactPath}`);
2041
+ } catch (error) {
2042
+ this.state.runState = "error";
2043
+ const message = error instanceof Error ? error.message : String(error);
2044
+ this.log(`Error: ${message}`);
2045
+ this.setStatus(`Error: ${message}`);
2046
+ } finally {
2047
+ this.stopLoadingAnimation();
2048
+ }
2049
+ this.render();
2050
+ this.focusInput();
2051
+ }
2052
+ rerunSelectedWithEdits() {
2053
+ this.state.selectedHistoryIndex = this.getHistorySelectionIndex();
2054
+ const item = this.state.history[this.state.selectedHistoryIndex];
2055
+ if (!item) {
2056
+ this.setStatus("No history item selected.");
2057
+ this.render();
2058
+ return;
2059
+ }
2060
+ this.inputBox.setValue(item.input.context);
2061
+ this.state.currentInput = item.input;
2062
+ this.setStatus("Loaded previous prompt. Edit and press Enter to rerun.");
2063
+ this.render();
2064
+ this.focusInput();
2065
+ }
2066
+ bindKeys() {
2067
+ this.screen.key(["?"], () => {
2068
+ this.helpModal.hidden = !this.helpModal.hidden;
2069
+ if (this.helpModal.hidden) {
2070
+ this.focusInput();
2071
+ }
2072
+ this.screen.render();
2073
+ });
2074
+ this.screen.key(["escape"], () => {
2075
+ if (!this.helpModal.hidden) {
2076
+ this.helpModal.hide();
2077
+ this.render();
2078
+ return;
2079
+ }
2080
+ this.stopPromptEditing();
2081
+ this.suppressFocusHighlight = true;
2082
+ this.updateFocusStyles();
2083
+ this.screen.render();
2084
+ });
2085
+ this.screen.on("mouse", (data) => {
2086
+ if (this.helpModal.hidden || data.action !== "mousedown") {
2087
+ return;
2088
+ }
2089
+ const lpos = this.helpModal.lpos;
2090
+ if (!lpos) {
2091
+ return;
2092
+ }
2093
+ const outsideX = data.x < lpos.xi || data.x > lpos.xl;
2094
+ const outsideY = data.y < lpos.yi || data.y > lpos.yl;
2095
+ if (outsideX || outsideY) {
2096
+ this.helpModal.hide();
2097
+ this.render();
2098
+ }
2099
+ });
2100
+ this.screen.key(["q"], () => {
2101
+ if (this.isTypingInPrompt()) {
2102
+ return;
2103
+ }
2104
+ this.screen.destroy();
2105
+ process.exit(0);
2106
+ });
2107
+ this.screen.key(["enter"], () => {
2108
+ if (!this.helpModal.hidden || !this.isTypingInPrompt()) {
2109
+ return;
2110
+ }
2111
+ void this.runCurrentInput();
2112
+ });
2113
+ this.screen.key(["C-c"], () => {
2114
+ this.screen.destroy();
2115
+ process.exit(0);
2116
+ });
2117
+ this.inputBox.key(["down"], () => {
2118
+ this.focusHistory();
2119
+ return false;
2120
+ });
2121
+ this.inputBox.key(["right"], () => {
2122
+ this.focusOutput();
2123
+ return false;
2124
+ });
2125
+ this.screen.key(["up"], () => {
2126
+ if (this.screen.focused !== this.historyBox) {
2127
+ return;
2128
+ }
2129
+ if (this.state.history.length === 0) {
2130
+ return;
2131
+ }
2132
+ const selectedIndex = this.getHistorySelectionIndex();
2133
+ if (selectedIndex > 0) {
2134
+ this.state.selectedHistoryIndex = selectedIndex - 1;
2135
+ this.previewHistorySelection();
2136
+ this.render();
2137
+ return;
2138
+ }
2139
+ this.focusInput();
2140
+ });
2141
+ this.screen.key(["down"], () => {
2142
+ if (this.screen.focused !== this.historyBox) {
2143
+ return;
2144
+ }
2145
+ if (this.state.history.length === 0) {
2146
+ return;
2147
+ }
2148
+ const selectedIndex = this.getHistorySelectionIndex();
2149
+ const nextIndex = Math.min(this.state.history.length - 1, selectedIndex + 1);
2150
+ if (nextIndex !== selectedIndex) {
2151
+ this.state.selectedHistoryIndex = nextIndex;
2152
+ this.previewHistorySelection();
2153
+ this.render();
2154
+ }
2155
+ });
2156
+ this.screen.key(["right"], () => {
2157
+ if (this.screen.focused !== this.historyBox) {
2158
+ return;
2159
+ }
2160
+ this.focusOutput();
2161
+ });
2162
+ this.screen.key(["enter"], () => {
2163
+ if (this.screen.focused !== this.historyBox) {
2164
+ return;
2165
+ }
2166
+ this.rerunSelectedWithEdits();
2167
+ });
2168
+ this.outputBox.key(["left"], () => {
2169
+ this.focusHistory();
2170
+ return false;
2171
+ });
2172
+ this.screen.key(["a"], () => {
2173
+ if (this.isTypingInPrompt()) {
2174
+ return;
2175
+ }
2176
+ this.state.showAudit = !this.state.showAudit;
2177
+ this.setStatus(`Audit ${this.state.showAudit ? "shown" : "hidden"}.`);
2178
+ this.render();
2179
+ });
2180
+ this.screen.key(["m"], () => {
2181
+ if (this.isTypingInPrompt()) {
2182
+ return;
2183
+ }
2184
+ if (!process.env.OPENAI_API_KEY) {
2185
+ this.setStatus("OPENAI_API_KEY missing. Heuristic mode only.");
2186
+ this.render();
2187
+ return;
2188
+ }
2189
+ const next = this.state.mode === "openai" ? "heuristic" : "openai";
2190
+ this.state.mode = next;
2191
+ this.session.provider = this.makeProvider(next);
2192
+ this.setStatus(`Mode switched to ${next.toUpperCase()}.`);
2193
+ this.render();
2194
+ });
2195
+ }
2196
+ run() {
2197
+ this.render();
2198
+ }
2199
+ };
2200
+ var runTui = async () => {
2201
+ await hydrateEnvFromFile();
2202
+ await ensureOpenAiApiKey();
2203
+ const history = await loadPersistedHistory();
2204
+ const app = new DecisionTuiApp(history);
2205
+ app.run();
2206
+ };
2207
+
2208
+ // src/cli/main.ts
2209
+ runTui().catch((error) => {
2210
+ console.error("CLI failed", error);
2211
+ process.exitCode = 1;
2212
+ });