@codemoot/cli 0.2.10 → 0.2.12

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CodeMoot Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -538,6 +538,7 @@ import {
538
538
  preflightTokenCheck
539
539
  } from "@codemoot/core";
540
540
  import chalk4 from "chalk";
541
+ import { writeFileSync } from "fs";
541
542
  async function debateStartCommand(topic, options) {
542
543
  let db;
543
544
  try {
@@ -560,7 +561,8 @@ async function debateStartCommand(topic, options) {
560
561
  status: "running",
561
562
  sessionIds: {},
562
563
  resumeStats: { attempted: 0, succeeded: 0, fallbacks: 0 },
563
- maxRounds: options.maxRounds ?? 5
564
+ maxRounds: options.maxRounds ?? 5,
565
+ defaultTimeout: options.timeout
564
566
  });
565
567
  const output = {
566
568
  debateId,
@@ -605,9 +607,10 @@ async function debateTurnCommand(debateId, prompt, options) {
605
607
  }
606
608
  const rawRound = options.round ?? criticRow.round + 1;
607
609
  const newRound = Number.isFinite(rawRound) && rawRound > 0 ? rawRound : criticRow.round + 1;
608
- const rawTimeout = options.timeout ?? 600;
609
- const timeout = (Number.isFinite(rawTimeout) && rawTimeout > 0 ? rawTimeout : 600) * 1e3;
610
610
  const proposerStateForLimit = store.loadState(debateId, "proposer");
611
+ const storedTimeout = proposerStateForLimit?.defaultTimeout;
612
+ const rawTimeout = options.timeout ?? storedTimeout ?? 600;
613
+ const timeout = (Number.isFinite(rawTimeout) && rawTimeout > 0 ? rawTimeout : 600) * 1e3;
611
614
  const rawMax = proposerStateForLimit?.maxRounds ?? 5;
612
615
  const maxRounds = Number.isFinite(rawMax) && rawMax > 0 ? rawMax : 5;
613
616
  if (newRound > maxRounds) {
@@ -626,10 +629,16 @@ async function debateTurnCommand(debateId, prompt, options) {
626
629
  status: "active"
627
630
  });
628
631
  }
632
+ if (options.output && existing.responseText) {
633
+ writeFileSync(options.output, existing.responseText, "utf-8");
634
+ console.error(chalk4.dim(`Full response written to ${options.output}`));
635
+ }
629
636
  const output = {
630
637
  debateId,
631
638
  round: newRound,
632
639
  response: existing.responseText?.slice(0, 2e3) ?? "",
640
+ responseTruncated: (existing.responseText?.length ?? 0) > 2e3,
641
+ responseFile: options.output ?? void 0,
633
642
  sessionId: existing.sessionId,
634
643
  resumed: false,
635
644
  cached: true,
@@ -671,10 +680,16 @@ async function debateTurnCommand(debateId, prompt, options) {
671
680
  if (!msgStore.markRunning(msgId)) {
672
681
  const recheckRow = msgStore.getByRound(debateId, newRound, "critic");
673
682
  if (recheckRow?.status === "completed") {
683
+ if (options.output && recheckRow.responseText) {
684
+ writeFileSync(options.output, recheckRow.responseText, "utf-8");
685
+ console.error(chalk4.dim(`Full response written to ${options.output}`));
686
+ }
674
687
  const output = {
675
688
  debateId,
676
689
  round: newRound,
677
690
  response: recheckRow.responseText?.slice(0, 2e3) ?? "",
691
+ responseTruncated: (recheckRow.responseText?.length ?? 0) > 2e3,
692
+ responseFile: options.output ?? void 0,
678
693
  sessionId: recheckRow.sessionId,
679
694
  resumed: false,
680
695
  cached: true,
@@ -702,14 +717,20 @@ async function debateTurnCommand(debateId, prompt, options) {
702
717
  const completedHistory = msgStore.getHistory(debateId).filter((m) => m.status === "completed");
703
718
  const maxContext = adapter.capabilities.maxContextTokens;
704
719
  const preflight = preflightTokenCheck(completedHistory, prompt, maxContext);
705
- if (preflight.shouldStop) {
706
- console.error(chalk4.yellow(` Token budget at ${Math.round(preflight.utilizationRatio * 100)}% (${preflight.totalTokensUsed}/${maxContext}). Consider completing this debate.`));
720
+ if (preflight.shouldStop && !options.force) {
721
+ console.error(chalk4.red(`Token budget at ${Math.round(preflight.utilizationRatio * 100)}% (${preflight.totalTokensUsed}/${maxContext}). Debate blocked \u2014 use --force to override.`));
722
+ db.close();
723
+ process.exit(1);
724
+ } else if (preflight.shouldStop && options.force) {
725
+ console.error(chalk4.yellow(` Token budget at ${Math.round(preflight.utilizationRatio * 100)}% (${preflight.totalTokensUsed}/${maxContext}) \u2014 forced past budget limit.`));
707
726
  } else if (preflight.shouldSummarize) {
708
727
  console.error(chalk4.yellow(` Token budget at ${Math.round(preflight.utilizationRatio * 100)}%. Older rounds will be summarized on resume failure.`));
709
728
  }
710
729
  const overflowCheck = sessionMgr.preCallOverflowCheck(unifiedSession.id);
711
730
  if (overflowCheck.rolled) {
712
731
  console.error(chalk4.yellow(` ${overflowCheck.message}`));
732
+ console.error(chalk4.yellow(" Warning: Session rolled over \u2014 previous conversation context has been lost."));
733
+ sessionMgr.updateThreadId(unifiedSession.id, null);
713
734
  existingSessionId = void 0;
714
735
  }
715
736
  try {
@@ -781,11 +802,16 @@ async function debateTurnCommand(debateId, prompt, options) {
781
802
  proposerState.resumeStats = stats;
782
803
  store.saveState(debateId, "proposer", proposerState);
783
804
  }
805
+ if (options.output) {
806
+ writeFileSync(options.output, result.text, "utf-8");
807
+ console.error(chalk4.dim(`Full response written to ${options.output}`));
808
+ }
784
809
  const output = {
785
810
  debateId,
786
811
  round: newRound,
787
812
  response: result.text.slice(0, 2e3),
788
813
  responseTruncated: result.text.length > 2e3,
814
+ responseFile: options.output ?? void 0,
789
815
  sessionId: result.sessionId,
790
816
  resumed,
791
817
  cached: false,
@@ -800,7 +826,7 @@ Round ${newRound} \u2014 Stance: ${verdict.stance}`));
800
826
  for (const line of previewLines) {
801
827
  console.error(chalk4.dim(` ${line.trim().slice(0, 120)}`));
802
828
  }
803
- console.error(chalk4.dim(`Duration: ${(result.durationMs / 1e3).toFixed(1)}s | Tokens: ${result.usage?.totalTokens ?? "?"} | Resumed: ${resumed}`));
829
+ console.error(chalk4.dim(`Duration: ${(result.durationMs / 1e3).toFixed(1)}s | Output: ${result.usage?.outputTokens ?? "?"} tokens | Input: ${result.usage?.inputTokens ?? "?"} (includes sandbox) | Resumed: ${resumed}`));
804
830
  console.log(JSON.stringify(output, null, 2));
805
831
  } catch (error) {
806
832
  msgStore.markFailed(msgId, error instanceof Error ? error.message : String(error));
@@ -813,6 +839,10 @@ Round ${newRound} \u2014 Stance: ${verdict.stance}`));
813
839
  process.exit(1);
814
840
  }
815
841
  }
842
+ async function debateNextCommand(debateId, options) {
843
+ const prompt = "Continue your analysis. Respond to the previous round's arguments and refine your position.";
844
+ return debateTurnCommand(debateId, prompt, options);
845
+ }
816
846
  async function debateStatusCommand(debateId) {
817
847
  let db;
818
848
  try {
@@ -893,7 +923,7 @@ async function debateListCommand(options) {
893
923
  process.exit(1);
894
924
  }
895
925
  }
896
- async function debateHistoryCommand(debateId) {
926
+ async function debateHistoryCommand(debateId, options = {}) {
897
927
  let db;
898
928
  try {
899
929
  const dbPath = getDbPath();
@@ -937,6 +967,29 @@ async function debateHistoryCommand(debateId) {
937
967
  completedAt: m.completedAt ? new Date(m.completedAt).toISOString() : null
938
968
  }))
939
969
  };
970
+ if (options.output) {
971
+ const fullOutput = {
972
+ ...output,
973
+ messages: history.map((m) => ({
974
+ round: m.round,
975
+ role: m.role,
976
+ bridge: m.bridge,
977
+ model: m.model,
978
+ status: m.status,
979
+ stance: m.stance,
980
+ confidence: m.confidence,
981
+ durationMs: m.durationMs,
982
+ sessionId: m.sessionId,
983
+ promptText: m.promptText,
984
+ responseText: m.responseText ?? null,
985
+ error: m.error,
986
+ createdAt: new Date(m.createdAt).toISOString(),
987
+ completedAt: m.completedAt ? new Date(m.completedAt).toISOString() : null
988
+ }))
989
+ };
990
+ writeFileSync(options.output, JSON.stringify(fullOutput, null, 2), "utf-8");
991
+ console.error(chalk4.dim(`Full history written to ${options.output}`));
992
+ }
940
993
  console.log(JSON.stringify(output, null, 2));
941
994
  db.close();
942
995
  } catch (error) {
@@ -1199,8 +1252,8 @@ Scan complete in ${(durationMs / 1e3).toFixed(1)}s`));
1199
1252
  durationMs
1200
1253
  });
1201
1254
  if (options.output) {
1202
- const { writeFileSync: writeFileSync4 } = await import("fs");
1203
- writeFileSync4(options.output, JSON.stringify(report, null, 2), "utf-8");
1255
+ const { writeFileSync: writeFileSync5 } = await import("fs");
1256
+ writeFileSync5(options.output, JSON.stringify(report, null, 2), "utf-8");
1204
1257
  console.error(chalk5.green(` Findings written to ${options.output}`));
1205
1258
  }
1206
1259
  const reportJson = JSON.stringify(report, null, 2);
@@ -1618,7 +1671,7 @@ async function eventsCommand(options) {
1618
1671
 
1619
1672
  // src/commands/fix.ts
1620
1673
  import { execFileSync as execFileSync2, execSync as execSync3 } from "child_process";
1621
- import { readFileSync as readFileSync2, writeFileSync } from "fs";
1674
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1622
1675
  import { resolve } from "path";
1623
1676
  import {
1624
1677
  DEFAULT_RULES,
@@ -1666,7 +1719,7 @@ function applyFix(fix, projectDir) {
1666
1719
  const trimmedOld = fix.oldCode.trim();
1667
1720
  if (content.includes(trimmedOld)) {
1668
1721
  const updated = content.replace(trimmedOld, fix.newCode.trim());
1669
- writeFileSync(filePath, updated, "utf-8");
1722
+ writeFileSync2(filePath, updated, "utf-8");
1670
1723
  return true;
1671
1724
  }
1672
1725
  return false;
@@ -1676,7 +1729,7 @@ function applyFix(fix, projectDir) {
1676
1729
  const newLines = fix.newCode.trim().split("\n");
1677
1730
  const oldLineCount = Math.max(1, newLines.length);
1678
1731
  lines.splice(lineIdx, oldLineCount, ...newLines);
1679
- writeFileSync(filePath, lines.join("\n"), "utf-8");
1732
+ writeFileSync2(filePath, lines.join("\n"), "utf-8");
1680
1733
  return true;
1681
1734
  }
1682
1735
  async function fixCommand(fileOrGlob, options) {
@@ -1777,11 +1830,26 @@ ${fixOutputContract}`,
1777
1830
  const timeoutMs = options.timeout * 1e3;
1778
1831
  const progress = createProgressCallbacks("fix-review");
1779
1832
  console.error(chalk9.dim(" GPT reviewing..."));
1780
- const reviewResult = await adapter.callWithResume(reviewPrompt, {
1781
- sessionId: threadId,
1782
- timeout: timeoutMs,
1783
- ...progress
1784
- });
1833
+ let reviewResult;
1834
+ try {
1835
+ reviewResult = await adapter.callWithResume(reviewPrompt, {
1836
+ sessionId: threadId,
1837
+ timeout: timeoutMs,
1838
+ ...progress
1839
+ });
1840
+ } catch (err) {
1841
+ if (threadId) {
1842
+ console.error(chalk9.yellow(" Clearing stale codex thread ID after failure."));
1843
+ sessionMgr.updateThreadId(session2.id, null);
1844
+ threadId = void 0;
1845
+ }
1846
+ throw err;
1847
+ }
1848
+ const resumed = threadId && reviewResult.sessionId === threadId;
1849
+ if (threadId && !resumed) {
1850
+ threadId = void 0;
1851
+ sessionMgr.updateThreadId(session2.id, null);
1852
+ }
1785
1853
  if (reviewResult.sessionId) {
1786
1854
  threadId = reviewResult.sessionId;
1787
1855
  sessionMgr.updateThreadId(session2.id, reviewResult.sessionId);
@@ -1999,7 +2067,7 @@ Initialized with '${presetName}' preset`));
1999
2067
  }
2000
2068
 
2001
2069
  // src/commands/install-skills.ts
2002
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
2070
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
2003
2071
  import { dirname, join as join5 } from "path";
2004
2072
  import chalk11 from "chalk";
2005
2073
  var SKILLS = [
@@ -2449,7 +2517,7 @@ async function installSkillsCommand(options) {
2449
2517
  continue;
2450
2518
  }
2451
2519
  mkdirSync2(dir, { recursive: true });
2452
- writeFileSync2(fullPath, skill.content, "utf-8");
2520
+ writeFileSync3(fullPath, skill.content, "utf-8");
2453
2521
  console.error(chalk11.green(` OK ${skill.path}`));
2454
2522
  installed++;
2455
2523
  }
@@ -2467,7 +2535,7 @@ async function installSkillsCommand(options) {
2467
2535
  const afterMarker = sectionEnd >= 0 ? existing.slice(sectionEnd + CLAUDE_MD_SECTION.trimEnd().length) : existing.slice(markerIdx + marker.length);
2468
2536
  const nextHeadingMatch = sectionEnd >= 0 ? null : afterMarker.match(/\n#{1,6} (?!CodeMoot)/);
2469
2537
  const after = nextHeadingMatch ? afterMarker.slice(nextHeadingMatch.index) : "";
2470
- writeFileSync2(claudeMdPath, before.trimEnd() + "\n" + CLAUDE_MD_SECTION + after, "utf-8");
2538
+ writeFileSync3(claudeMdPath, before.trimEnd() + "\n" + CLAUDE_MD_SECTION + after, "utf-8");
2471
2539
  console.error(chalk11.green(" OK CLAUDE.md (updated CodeMoot section)"));
2472
2540
  installed++;
2473
2541
  } else {
@@ -2475,12 +2543,12 @@ async function installSkillsCommand(options) {
2475
2543
  skipped++;
2476
2544
  }
2477
2545
  } else {
2478
- writeFileSync2(claudeMdPath, existing.trimEnd() + "\n" + CLAUDE_MD_SECTION, "utf-8");
2546
+ writeFileSync3(claudeMdPath, existing.trimEnd() + "\n" + CLAUDE_MD_SECTION, "utf-8");
2479
2547
  console.error(chalk11.green(" OK CLAUDE.md (appended CodeMoot section)"));
2480
2548
  installed++;
2481
2549
  }
2482
2550
  } else {
2483
- writeFileSync2(claudeMdPath, `# Project Instructions
2551
+ writeFileSync3(claudeMdPath, `# Project Instructions
2484
2552
  ${CLAUDE_MD_SECTION}`, "utf-8");
2485
2553
  console.error(chalk11.green(" OK CLAUDE.md (created with CodeMoot section)"));
2486
2554
  installed++;
@@ -2502,7 +2570,7 @@ ${CLAUDE_MD_SECTION}`, "utf-8");
2502
2570
  ...existing.hooks,
2503
2571
  PostToolUse: [...otherHooks, ...HOOKS_CONFIG.hooks.PostToolUse]
2504
2572
  };
2505
- writeFileSync2(settingsPath, JSON.stringify(existing, null, 2), "utf-8");
2573
+ writeFileSync3(settingsPath, JSON.stringify(existing, null, 2), "utf-8");
2506
2574
  console.error(chalk11.green(" OK .claude/settings.json (added post-commit hint hook)"));
2507
2575
  installed++;
2508
2576
  }
@@ -2513,7 +2581,7 @@ ${CLAUDE_MD_SECTION}`, "utf-8");
2513
2581
  }
2514
2582
  } else {
2515
2583
  mkdirSync2(settingsDir, { recursive: true });
2516
- writeFileSync2(settingsPath, JSON.stringify(HOOKS_CONFIG, null, 2), "utf-8");
2584
+ writeFileSync3(settingsPath, JSON.stringify(HOOKS_CONFIG, null, 2), "utf-8");
2517
2585
  console.error(chalk11.green(" OK .claude/settings.json (created with post-commit hook)"));
2518
2586
  installed++;
2519
2587
  }
@@ -2811,7 +2879,7 @@ async function jobsStatusCommand(jobId) {
2811
2879
  }
2812
2880
 
2813
2881
  // src/commands/plan.ts
2814
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2882
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
2815
2883
  import {
2816
2884
  ModelRegistry as ModelRegistry4,
2817
2885
  Orchestrator,
@@ -2928,7 +2996,7 @@ async function planGenerateCommand(task, options) {
2928
2996
  maxRounds: options.rounds
2929
2997
  });
2930
2998
  if (options.output) {
2931
- writeFileSync3(options.output, result.finalOutput, "utf-8");
2999
+ writeFileSync4(options.output, result.finalOutput, "utf-8");
2932
3000
  console.error(chalk15.green(`Plan saved to ${options.output}`));
2933
3001
  }
2934
3002
  printSessionSummary(result);
@@ -3079,7 +3147,7 @@ Verdict: ${verdict.toUpperCase()} (${score ?? "?"}/10)`));
3079
3147
  };
3080
3148
  console.log(JSON.stringify(output, null, 2));
3081
3149
  if (options.output) {
3082
- writeFileSync3(options.output, JSON.stringify(output, null, 2), "utf-8");
3150
+ writeFileSync4(options.output, JSON.stringify(output, null, 2), "utf-8");
3083
3151
  console.error(chalk15.green(`Review saved to ${options.output}`));
3084
3152
  }
3085
3153
  db.close();
@@ -3297,11 +3365,22 @@ ${fileContents}`,
3297
3365
  }
3298
3366
  const timeoutMs = (options.timeout ?? 600) * 1e3;
3299
3367
  const progress = createProgressCallbacks("review");
3300
- const result = await adapter.callWithResume(prompt, {
3301
- sessionId: sessionThreadId,
3302
- timeout: timeoutMs,
3303
- ...progress
3304
- });
3368
+ let result;
3369
+ try {
3370
+ result = await adapter.callWithResume(prompt, {
3371
+ sessionId: sessionThreadId,
3372
+ timeout: timeoutMs,
3373
+ ...progress
3374
+ });
3375
+ } catch (err) {
3376
+ if (sessionThreadId) {
3377
+ sessionMgr.updateThreadId(session2.id, null);
3378
+ }
3379
+ throw err;
3380
+ }
3381
+ if (sessionThreadId && result.sessionId !== sessionThreadId) {
3382
+ sessionMgr.updateThreadId(session2.id, null);
3383
+ }
3305
3384
  if (result.sessionId) {
3306
3385
  sessionMgr.updateThreadId(session2.id, result.sessionId);
3307
3386
  }
@@ -3985,11 +4064,17 @@ var plan = program.command("plan").description("Plan generation and review \u201
3985
4064
  plan.command("generate").description("Generate a plan using architect + reviewer loop").argument("<task>", "Task to plan").option("--rounds <n>", "Max plan-review rounds", (v) => Number.parseInt(v, 10), 3).option("--output <file>", "Save plan to file").action(planGenerateCommand);
3986
4065
  plan.command("review").description("Send a host-authored plan to codex for review").argument("<plan-file>", "Plan file to review (use - for stdin)").option("--build <id>", "Link review to a build ID").option("--phase <id>", 'Phase identifier (e.g. "1", "setup")').option("--timeout <seconds>", "Review timeout", (v) => Number.parseInt(v, 10), 300).option("--output <file>", "Save review result to file").action(planReviewCommand);
3987
4066
  var debate = program.command("debate").description("Multi-model debate with session persistence");
3988
- debate.command("start").description("Start a new debate").argument("<topic>", "Debate topic or question").option("--max-rounds <n>", "Max debate rounds", (v) => Number.parseInt(v, 10), 5).action(debateStartCommand);
3989
- debate.command("turn").description("Send a prompt to GPT and get critique (with session resume)").argument("<debate-id>", "Debate ID from start command").argument("<prompt>", "Prompt to send to GPT").option("--round <n>", "Round number", (v) => Number.parseInt(v, 10)).option("--timeout <seconds>", "Timeout in seconds", (v) => Number.parseInt(v, 10), 600).action(debateTurnCommand);
4067
+ debate.command("start").description("Start a new debate").argument("<topic>", "Debate topic or question").option("--max-rounds <n>", "Max debate rounds", (v) => Number.parseInt(v, 10), 5).option("--timeout <seconds>", "Default timeout for debate turns in seconds", (v) => {
4068
+ if (!/^\d+$/.test(v)) throw new InvalidArgumentError("Timeout must be a positive integer");
4069
+ const n = Number.parseInt(v, 10);
4070
+ if (n <= 0) throw new InvalidArgumentError("Timeout must be a positive integer");
4071
+ return n;
4072
+ }).action(debateStartCommand);
4073
+ debate.command("turn").description("Send a prompt to GPT and get critique (with session resume)").argument("<debate-id>", "Debate ID from start command").argument("<prompt>", "Prompt to send to GPT").option("--round <n>", "Round number", (v) => Number.parseInt(v, 10)).option("--timeout <seconds>", "Timeout in seconds", (v) => Number.parseInt(v, 10)).option("--output <file>", "Write full untruncated response to file").option("--force", "Continue past token budget limit", false).action(debateTurnCommand);
4074
+ debate.command("next").description("Continue debate with auto-generated prompt").argument("<debate-id>", "Debate ID").option("--timeout <seconds>", "Timeout in seconds", (v) => Number.parseInt(v, 10)).option("--output <file>", "Write full untruncated response to file").option("--force", "Continue past token budget limit", false).action(debateNextCommand);
3990
4075
  debate.command("status").description("Show debate status and session info").argument("<debate-id>", "Debate ID").action(debateStatusCommand);
3991
4076
  debate.command("list").description("List all debates").option("--status <status>", "Filter by status (active|completed|stale)").option("--limit <n>", "Max results", (v) => Number.parseInt(v, 10), 20).action(debateListCommand);
3992
- debate.command("history").description("Show full message history with token budget").argument("<debate-id>", "Debate ID").action(debateHistoryCommand);
4077
+ debate.command("history").description("Show full message history with token budget").argument("<debate-id>", "Debate ID").option("--output <file>", "Write full untruncated history to file").action(debateHistoryCommand);
3993
4078
  debate.command("complete").description("Mark a debate as completed").argument("<debate-id>", "Debate ID").action(debateCompleteCommand);
3994
4079
  var build = program.command("build").description("Automated build loop: debate \u2192 plan \u2192 implement \u2192 review \u2192 fix");
3995
4080
  build.command("start").description("Start a new build session").argument("<task>", "Task description").option("--max-rounds <n>", "Max debate rounds", (v) => Number.parseInt(v, 10), 5).option("--allow-dirty", "Allow starting with dirty working tree (auto-stashes)").action(buildStartCommand);