tagteam 0.2.0 → 0.4.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.
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
+ import { createRequire } from "module";
5
6
  import { execSync as execSync2 } from "child_process";
6
7
  import chalk from "chalk";
7
8
 
@@ -22,7 +23,7 @@ var DEFAULT_CONFIG = {
22
23
  model: "gemini-2.5-pro"
23
24
  },
24
25
  discussion: {
25
- max_rounds: 10
26
+ max_rounds: 5
26
27
  }
27
28
  };
28
29
  function getConfigDir() {
@@ -111,8 +112,8 @@ function setConfigValue(key, value) {
111
112
  }
112
113
 
113
114
  // src/ui.tsx
114
- import { useState as useState2, useEffect, useCallback, useRef } from "react";
115
- import { render as render2, Box as Box2, Text as Text2, useApp as useApp2, useInput as useInput2 } from "ink";
115
+ import React2, { useState as useState2, useEffect, useCallback, useRef, useMemo } from "react";
116
+ import { render as render2, Box as Box2, Text as Text2, useApp as useApp2, useInput as useInput2, Static } from "ink";
116
117
  import TextInput2 from "ink-text-input";
117
118
  import Spinner from "ink-spinner";
118
119
  import { marked } from "marked";
@@ -458,6 +459,16 @@ var AGENTS = {
458
459
  cliBinary: "claude",
459
460
  installUrl: "https://docs.anthropic.com/en/docs/claude-code",
460
461
  org: "Anthropic",
462
+ profile: {
463
+ strength: "architecture-implementation",
464
+ role: "The Builder",
465
+ focus: [
466
+ "multi-file coherence and refactoring",
467
+ "production-quality implementation",
468
+ "design patterns and maintainability",
469
+ "comprehensive working solutions"
470
+ ]
471
+ },
461
472
  run: runClaude
462
473
  },
463
474
  codex: {
@@ -467,6 +478,16 @@ var AGENTS = {
467
478
  cliBinary: "codex",
468
479
  installUrl: "https://github.com/openai/codex",
469
480
  org: "OpenAI",
481
+ profile: {
482
+ strength: "correctness-verification",
483
+ role: "The Verifier",
484
+ focus: [
485
+ "algorithmic correctness and edge cases",
486
+ "test coverage and failure modes",
487
+ "standards compliance and best practices",
488
+ "performance characteristics and benchmarks"
489
+ ]
490
+ },
470
491
  run: runCodex
471
492
  },
472
493
  gemini: {
@@ -476,12 +497,49 @@ var AGENTS = {
476
497
  cliBinary: "gemini",
477
498
  installUrl: "https://github.com/google-gemini/gemini-cli",
478
499
  org: "Google",
500
+ profile: {
501
+ strength: "context-strategy",
502
+ role: "The Strategist",
503
+ focus: [
504
+ "broad codebase context and upstream/downstream effects",
505
+ "current ecosystem conventions and documentation",
506
+ "architectural fit and scope assessment",
507
+ "planning, decomposition, and tradeoff analysis"
508
+ ]
509
+ },
479
510
  run: runGemini
480
511
  }
481
512
  };
513
+ var PEER_ROLES = {
514
+ "claude,codex": {
515
+ claude: "Correctness & Standards \u2014 they verify edge cases, test coverage, and standards compliance",
516
+ codex: "Architecture & Implementation \u2014 they propose complete solutions and assess structural coherence",
517
+ gemini: ""
518
+ // not in this pair
519
+ },
520
+ "claude,gemini": {
521
+ claude: "Strategic Context \u2014 they assess broad codebase fit, ecosystem conventions, and architectural tradeoffs",
522
+ gemini: "Architecture & Implementation \u2014 they propose complete solutions and assess structural coherence",
523
+ codex: ""
524
+ // not in this pair
525
+ },
526
+ "codex,gemini": {
527
+ codex: "Strategic Context \u2014 they assess broad codebase fit, ecosystem conventions, and architectural tradeoffs",
528
+ gemini: "Correctness & Standards \u2014 they verify edge cases, test coverage, and standards compliance",
529
+ claude: ""
530
+ // not in this pair
531
+ }
532
+ };
482
533
  function getAgent(name) {
483
534
  return AGENTS[name];
484
535
  }
536
+ function getAgentProfile(name) {
537
+ return AGENTS[name].profile;
538
+ }
539
+ function getPeerRoleDescription(agent, pair) {
540
+ const key = [...pair].sort().join(",");
541
+ return PEER_ROLES[key]?.[agent] ?? "";
542
+ }
485
543
  function getAllAgentNames() {
486
544
  return Object.keys(AGENTS);
487
545
  }
@@ -655,6 +713,16 @@ function touchSession(id) {
655
713
  id
656
714
  );
657
715
  }
716
+ function deleteSession(id) {
717
+ const db2 = getDb();
718
+ db2.prepare(`DELETE FROM messages WHERE session_id = ?`).run(id);
719
+ db2.prepare(`DELETE FROM sessions WHERE id = ?`).run(id);
720
+ }
721
+ function deleteAllSessions() {
722
+ const db2 = getDb();
723
+ db2.prepare(`DELETE FROM messages`).run();
724
+ db2.prepare(`DELETE FROM sessions`).run();
725
+ }
658
726
 
659
727
  // src/db/messages.ts
660
728
  function insertMessage(params) {
@@ -684,44 +752,6 @@ function deleteMessagesFromRound(sessionId, fromRound) {
684
752
  }
685
753
 
686
754
  // src/prompts.ts
687
- function otherAgent(agent, pair) {
688
- const other = pair[0] === agent ? pair[1] : pair[0];
689
- const desc = getAgent(other);
690
- return `${desc.displayName} (${desc.org})`;
691
- }
692
- function collaborationPrompt(agent, pair) {
693
- return `You are in a collaborative session with ${otherAgent(agent, pair)}. You'll both respond to the user's prompt independently, then see each other's responses. In discussion rounds: highlight where you agree, constructively address disagreements, and build on each other's ideas. Be concise - avoid repeating what was already said.`;
694
- }
695
- function discussionPrompt(agent, conversationHistory, pair) {
696
- return `${collaborationPrompt(agent, pair)}
697
-
698
- Here is the conversation so far:
699
-
700
- ${conversationHistory}
701
-
702
- Now provide your response for this discussion round. Build on what was said, highlight agreements, and address any disagreements constructively. Be concise.`;
703
- }
704
- function debatePrompt(agent, pair) {
705
- const other = otherAgent(agent, pair);
706
- return `You are in a structured debate with ${other}. You'll both respond to the user's prompt, then see each other's responses and discuss.
707
-
708
- Your goal is to reach consensus through constructive discussion. In each round:
709
- - Address specific points of agreement and disagreement
710
- - Refine your position based on valid arguments from ${other}
711
- - Be concise \u2014 don't repeat points already established
712
-
713
- When you believe you and ${other} have reached substantial agreement on the key points, end your response with [CONSENSUS] on its own line. Only do this when you genuinely agree \u2014 don't force premature consensus.`;
714
- }
715
- function debateRoundPrompt(agent, conversationHistory, pair) {
716
- const other = otherAgent(agent, pair);
717
- return `${debatePrompt(agent, pair)}
718
-
719
- Here is the conversation so far:
720
-
721
- ${conversationHistory}
722
-
723
- Respond to the latest round. If you agree with ${other}'s position on all key points, end with [CONSENSUS]. Otherwise, continue the discussion.`;
724
- }
725
755
  var CONSENSUS_MARKER = "[CONSENSUS]";
726
756
  function formatConversationHistory(messages) {
727
757
  return messages.map((m) => {
@@ -738,6 +768,264 @@ function formatConversationHistory(messages) {
738
768
  return `[${label}]: ${m.content}`;
739
769
  }).join("\n\n");
740
770
  }
771
+ function basePrompt() {
772
+ return `You are one of two expert coding agents in a structured technical discussion.
773
+ You will independently analyze the problem, then engage in focused rounds of
774
+ critique and refinement with your peer.
775
+
776
+ Ground rules:
777
+ - You are evaluated on the ACCURACY and QUALITY of your final position, not
778
+ on agreement with your peer.
779
+ - When you change your position, you MUST name the specific argument that
780
+ changed your mind and explain why your previous reasoning was flawed.
781
+ Changing position without this justification is not acceptable.
782
+ - Each response must either: (a) introduce new evidence or a new argument,
783
+ (b) identify a specific logical flaw or unsupported claim in your peer's
784
+ reasoning, or (c) concede a point with explicit justification. Restating
785
+ or paraphrasing existing points is not acceptable.
786
+ - Your peer is a different AI model with different training. Their perspective
787
+ may reveal genuine blind spots in yours \u2014 and vice versa.`;
788
+ }
789
+ var ROLE_TEMPLATES = {
790
+ claude: `Your role: Architecture & Implementation Reviewer.
791
+
792
+ Focus your analysis on:
793
+ - Code structure, design patterns, and maintainability
794
+ - Multi-file coherence \u2014 how changes ripple across the codebase
795
+ - Production readiness \u2014 error handling, logging, edge cases in real usage
796
+ - Proposing complete, working implementations (not just pseudocode)
797
+
798
+ When you propose a solution, provide the actual implementation. When you
799
+ critique, point to specific structural issues and show what the fix looks
800
+ like. Your peer's role is {peerRole} \u2014 they will stress-test your proposals
801
+ from a different angle.`,
802
+ codex: `Your role: Correctness & Standards Reviewer.
803
+
804
+ Focus your analysis on:
805
+ - Algorithmic correctness \u2014 does the logic actually work for all inputs?
806
+ - Edge cases and failure modes \u2014 what breaks, what's untested?
807
+ - Standards compliance \u2014 does this follow language/framework conventions?
808
+ - Performance characteristics \u2014 time/space complexity, benchmarks
809
+
810
+ When you critique, provide specific test cases or inputs that demonstrate
811
+ the issue. When you propose alternatives, explain the correctness guarantees.
812
+ Your peer's role is {peerRole} \u2014 they will focus on different aspects of the
813
+ same problem.`,
814
+ gemini: `Your role: Strategic Context Analyst.
815
+
816
+ Focus your analysis on:
817
+ - Broad codebase context \u2014 how does this change fit the larger system?
818
+ - Current ecosystem conventions \u2014 what do the docs, community, and recent
819
+ releases recommend?
820
+ - Upstream and downstream effects \u2014 what will this break or enable elsewhere?
821
+ - Scope and planning \u2014 is this the right approach at the right level of
822
+ abstraction?
823
+
824
+ When you critique, ground your position in the broader context your peer may
825
+ be missing. When you propose alternatives, explain the architectural tradeoffs.
826
+ Your peer's role is {peerRole} \u2014 they will focus on different aspects of the
827
+ same problem.`
828
+ };
829
+ function rolePrompt(agent, pair) {
830
+ const template = ROLE_TEMPLATES[agent];
831
+ const peerRole = getPeerRoleDescription(agent, pair);
832
+ return template.replace("{peerRole}", peerRole);
833
+ }
834
+ function collaborationSystemPrompt(agent, pair) {
835
+ return `${basePrompt()}
836
+
837
+ ${rolePrompt(agent, pair)}`;
838
+ }
839
+ function discussionRoundPrompt(agent, conversationContext, pair) {
840
+ return `${basePrompt()}
841
+
842
+ ${rolePrompt(agent, pair)}
843
+
844
+ Here is the discussion so far:
845
+
846
+ ${conversationContext}
847
+
848
+ For this round:
849
+ 1. What is the strongest point in your peer's response?
850
+ 2. What is the weakest point, or what claim lacks supporting evidence?
851
+ 3. Has your position changed? State one of: HELD / PARTIALLY_CHANGED / CHANGED
852
+ \u2014 with explicit reasoning for why.
853
+ 4. If proposing code, show the specific implementation and explain tradeoffs
854
+ versus your peer's approach.
855
+ 5. Confidence in your current position: LOW | MEDIUM | HIGH
856
+
857
+ CONFIDENCE: HIGH | MEDIUM | LOW
858
+
859
+ Keep it concise. Do not restate points already established.`;
860
+ }
861
+ function debateSystemPrompt(agent, pair) {
862
+ return `${basePrompt()}
863
+
864
+ ${rolePrompt(agent, pair)}
865
+
866
+ This is a structured discussion aimed at reaching a well-reasoned position
867
+ through genuine deliberation.
868
+
869
+ Additional rules for discussion mode:
870
+ - Structure your arguments: STATE your claim, provide EVIDENCE (code examples,
871
+ documentation, benchmarks), explain your REASONING connecting evidence to
872
+ claim, and note CAVEATS (when your claim doesn't hold).
873
+ - Express confidence: end your response with CONFIDENCE: HIGH | MEDIUM | LOW
874
+ and a one-line explanation of what would change your mind.
875
+ - Consensus signaling: when you believe you and your peer agree on all key
876
+ points AND your confidence is HIGH, end your response with ${CONSENSUS_MARKER} on
877
+ its own line. Only signal consensus when:
878
+ (a) You can state the shared position in one sentence
879
+ (b) You have HIGH confidence
880
+ (c) You are not just deferring \u2014 you genuinely agree with the reasoning`;
881
+ }
882
+ function debateRoundPrompt(agent, conversationContext, pair) {
883
+ return `${debateSystemPrompt(agent, pair)}
884
+
885
+ ${conversationContext}
886
+
887
+ For this round:
888
+ 1. Address your peer's strongest argument directly \u2014 do you accept it? Why or
889
+ why not?
890
+ 2. If your peer identified a flaw in your reasoning, acknowledge it explicitly
891
+ or defend with new evidence.
892
+ 3. State your current position with EVIDENCE and REASONING.
893
+ 4. CONFIDENCE: HIGH | MEDIUM | LOW \u2014 what specific evidence would change
894
+ your remaining position?
895
+ 5. If consensus: state the shared position in one sentence, then ${CONSENSUS_MARKER}.
896
+
897
+ POSITION: HELD | PARTIALLY_CHANGED | CHANGED`;
898
+ }
899
+ function steelmanPrompt() {
900
+ return `You and your peer appear to largely agree after Round 1. Before confirming
901
+ consensus, steelman the opposing view:
902
+
903
+ - What is the strongest argument AGAINST your shared position?
904
+ - What context or edge case might make a different approach better?
905
+ - Is there a tradeoff you're both overlooking?
906
+
907
+ If after considering the counterarguments you still hold your position, explain
908
+ why the counterarguments don't apply here. Then proceed with your normal round
909
+ response.
910
+
911
+ `;
912
+ }
913
+ function directPrompt(agent, conversationHistory) {
914
+ const profile = getAgentProfile(agent);
915
+ const focusAreas = profile.focus.map((f) => `- ${f}`).join("\n");
916
+ return `You are being addressed directly in a multi-agent session. The user wants YOUR
917
+ specific perspective.
918
+
919
+ Here is the conversation so far:
920
+ ${conversationHistory}
921
+
922
+ Respond to the user's latest message. Focus on your area of expertise:
923
+ ${focusAreas}
924
+
925
+ Be concise and direct.`;
926
+ }
927
+
928
+ // src/discussion.ts
929
+ var CONFIDENCE_RE = /CONFIDENCE:\s*(HIGH|MEDIUM|LOW)/i;
930
+ var POSITION_RE = /POSITION:\s*(HELD|PARTIALLY_CHANGED|CHANGED)/i;
931
+ var CONSENSUS_RE = /\[CONSENSUS\]/;
932
+ function parseRoundAnalysis(agent, responseText) {
933
+ const confidenceMatch = responseText.match(CONFIDENCE_RE);
934
+ const positionMatch = responseText.match(POSITION_RE);
935
+ const signaledConsensus = CONSENSUS_RE.test(responseText);
936
+ const stripped = responseText.replace(CONFIDENCE_RE, "").replace(POSITION_RE, "").replace(CONSENSUS_RE, "").trim();
937
+ const hasNovelContent = stripped.length > 100;
938
+ return {
939
+ agent,
940
+ confidence: confidenceMatch?.[1]?.toUpperCase() ?? "MEDIUM",
941
+ positionChange: positionMatch?.[1]?.toUpperCase() ?? "HELD",
942
+ signaledConsensus,
943
+ hasNovelContent
944
+ };
945
+ }
946
+ function checkTermination(state, maxRounds) {
947
+ const { round, analyses } = state;
948
+ if (analyses.length > 0) {
949
+ const latest = analyses[analyses.length - 1];
950
+ if (latest && latest.length >= 2) {
951
+ const allConsensus = latest.every((a) => a.signaledConsensus);
952
+ const allHigh = latest.every((a) => a.confidence === "HIGH");
953
+ if (allConsensus && allHigh) {
954
+ return { terminated: true, reason: "mutual-consensus" };
955
+ }
956
+ }
957
+ }
958
+ if (analyses.length >= 2) {
959
+ const prev = analyses[analyses.length - 2];
960
+ const curr = analyses[analyses.length - 1];
961
+ if (prev && curr && prev.length >= 2 && curr.length >= 2) {
962
+ const prevStale = prev.every((a) => a.positionChange === "HELD" && !a.hasNovelContent);
963
+ const currStale = curr.every((a) => a.positionChange === "HELD" && !a.hasNovelContent);
964
+ if (prevStale && currStale) {
965
+ return { terminated: true, reason: "stale-no-progress" };
966
+ }
967
+ }
968
+ }
969
+ if (analyses.length >= 2) {
970
+ const prev = analyses[analyses.length - 2];
971
+ const curr = analyses[analyses.length - 1];
972
+ if (prev && curr && prev.length >= 2 && curr.length >= 2) {
973
+ const prevSwap = prev.every((a) => a.positionChange === "CHANGED");
974
+ const currSwap = curr.every((a) => a.positionChange === "CHANGED");
975
+ if (prevSwap && currSwap) {
976
+ return { terminated: true, reason: "cyclic-swap" };
977
+ }
978
+ }
979
+ }
980
+ if (round >= maxRounds) {
981
+ return { terminated: true, reason: "max-rounds" };
982
+ }
983
+ return { terminated: false };
984
+ }
985
+ function terminationMessage(reason) {
986
+ switch (reason) {
987
+ case "mutual-consensus":
988
+ return "Consensus reached.";
989
+ case "stale-no-progress":
990
+ return "Discussion stalled \u2014 no new arguments. Showing final positions.";
991
+ case "cyclic-swap":
992
+ return "Agents are trading positions. Showing both perspectives.";
993
+ case "max-rounds":
994
+ return "Maximum rounds reached. Showing final positions.";
995
+ }
996
+ }
997
+ function shouldInjectSteelman(state) {
998
+ return state.round === 1 && state.analyses.length === 1;
999
+ }
1000
+ function buildConversationContext(allMessages, analyses, currentRound, _pair) {
1001
+ if (currentRound <= 2) {
1002
+ return formatConversationHistory(allMessages);
1003
+ }
1004
+ const summaryParts = [];
1005
+ for (let i = 0; i < analyses.length - 1; i++) {
1006
+ const roundAnalyses = analyses[i];
1007
+ if (!roundAnalyses) continue;
1008
+ const roundSummary = roundAnalyses.map((a) => {
1009
+ return `${a.agent}: confidence=${a.confidence}, position=${a.positionChange}${a.signaledConsensus ? ", signaled consensus" : ""}`;
1010
+ }).join("; ");
1011
+ summaryParts.push(`Round ${i + 1}: ${roundSummary}`);
1012
+ }
1013
+ const latestMessages = allMessages.slice(-3);
1014
+ const summary = summaryParts.length > 0 ? `Previous rounds summary:
1015
+ ${summaryParts.join("\n")}
1016
+
1017
+ Latest exchange:
1018
+ ${formatConversationHistory(latestMessages)}` : formatConversationHistory(allMessages);
1019
+ return summary;
1020
+ }
1021
+ function analysisToMetadata(analysis) {
1022
+ return {
1023
+ confidence: analysis.confidence,
1024
+ positionChange: analysis.positionChange,
1025
+ signaledConsensus: analysis.signaledConsensus,
1026
+ hasNovelContent: analysis.hasNovelContent
1027
+ };
1028
+ }
741
1029
 
742
1030
  // src/config-editor.tsx
743
1031
  import { useState } from "react";
@@ -887,12 +1175,35 @@ function startConfigEditor() {
887
1175
  // src/ui.tsx
888
1176
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
889
1177
  marked.use(markedTerminal());
890
- function parseInput(input, pair) {
1178
+ var PAIR_SEPARATORS = /^(\w+)\s*(?:and|&|\/|,)\s*(\w+)[\s,]\s*/i;
1179
+ function tryParsePair(text) {
1180
+ const match = text.toLowerCase().match(PAIR_SEPARATORS);
1181
+ if (!match) return null;
1182
+ const [fullMatch, first, second] = match;
1183
+ if (isValidAgentName(first) && isValidAgentName(second) && first !== second) {
1184
+ return { pair: [first, second], rest: text.slice(fullMatch.length).trim() };
1185
+ }
1186
+ return null;
1187
+ }
1188
+ function parseInput(input) {
891
1189
  const lower = input.toLowerCase();
892
1190
  if (lower.startsWith("discuss ")) {
893
- return { target: "both", prompt: input.slice(8).trim(), discuss: true };
1191
+ const rest = input.slice(8).trim();
1192
+ const adHoc2 = tryParsePair(rest);
1193
+ if (adHoc2) {
1194
+ return { target: adHoc2.pair, prompt: adHoc2.rest, discuss: true };
1195
+ }
1196
+ return { target: "both", prompt: rest, discuss: true };
894
1197
  }
895
- for (const agent of pair) {
1198
+ const adHoc = tryParsePair(input);
1199
+ if (adHoc) {
1200
+ const adHocLower = adHoc.rest.toLowerCase();
1201
+ if (adHocLower.startsWith("discuss ")) {
1202
+ return { target: adHoc.pair, prompt: adHoc.rest.slice(8).trim(), discuss: true };
1203
+ }
1204
+ return { target: adHoc.pair, prompt: adHoc.rest, discuss: false };
1205
+ }
1206
+ for (const agent of getAllAgentNames()) {
896
1207
  if (lower.startsWith(`${agent} `) || lower.startsWith(`${agent}, `)) {
897
1208
  return { target: agent, prompt: input.slice(input.indexOf(" ") + 1).trim(), discuss: false };
898
1209
  }
@@ -900,10 +1211,10 @@ function parseInput(input, pair) {
900
1211
  return { target: "both", prompt: input, discuss: false };
901
1212
  }
902
1213
  function RenderedMarkdown({ text }) {
903
- const rendered = marked.parse(text).trimEnd();
1214
+ const rendered = useMemo(() => marked.parse(text).trimEnd(), [text]);
904
1215
  return /* @__PURE__ */ jsx2(Text2, { children: rendered });
905
1216
  }
906
- function AgentResponseBlock({
1217
+ var AgentResponseBlock = React2.memo(function AgentResponseBlock2({
907
1218
  agent,
908
1219
  content,
909
1220
  error
@@ -925,7 +1236,7 @@ function AgentResponseBlock({
925
1236
  ] }),
926
1237
  /* @__PURE__ */ jsx2(Box2, { marginLeft: 1, children: /* @__PURE__ */ jsx2(RenderedMarkdown, { text: content }) })
927
1238
  ] });
928
- }
1239
+ });
929
1240
  function Header({ sessionId }) {
930
1241
  return /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
931
1242
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2500\u2500 " }),
@@ -935,7 +1246,24 @@ function Header({ sessionId }) {
935
1246
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " " + "\u2500".repeat(35) })
936
1247
  ] });
937
1248
  }
938
- function UserMessage({ content }) {
1249
+ function QuickHelp() {
1250
+ const col = 38;
1251
+ const examples = [
1252
+ ["ask anything", "sends to both agents"],
1253
+ ["gemini explain this", "sends to one agent"],
1254
+ ["discuss best approach", "multi-round debate"],
1255
+ ["gemini and codex discuss review app", "pick agents + debate"]
1256
+ ];
1257
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
1258
+ examples.map(([cmd, desc]) => /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1259
+ cmd.padEnd(col),
1260
+ "\u2192 ",
1261
+ desc
1262
+ ] }, cmd)),
1263
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "/help for more" })
1264
+ ] });
1265
+ }
1266
+ var UserMessage = React2.memo(function UserMessage2({ content }) {
939
1267
  return /* @__PURE__ */ jsxs2(Box2, { marginLeft: 1, marginBottom: 1, children: [
940
1268
  /* @__PURE__ */ jsxs2(Text2, { bold: true, color: "white", children: [
941
1269
  "You:",
@@ -943,7 +1271,7 @@ function UserMessage({ content }) {
943
1271
  ] }),
944
1272
  /* @__PURE__ */ jsx2(Text2, { children: content })
945
1273
  ] });
946
- }
1274
+ });
947
1275
  function ThinkingIndicator({ agent }) {
948
1276
  const descriptor = getAgent(agent);
949
1277
  return /* @__PURE__ */ jsxs2(Box2, { marginLeft: 1, children: [
@@ -1020,11 +1348,14 @@ function App({
1020
1348
  }
1021
1349
  return 0;
1022
1350
  });
1023
- useEffect(() => {
1024
- if (!existingSessionId) {
1025
- createSession(sessionId, process.cwd());
1351
+ const [committedCount, setCommittedCount] = useState2(showTranscript2?.length ?? 0);
1352
+ const sessionCreatedRef = useRef(!!existingSessionId);
1353
+ const ensureSession = (id) => {
1354
+ if (!sessionCreatedRef.current) {
1355
+ createSession(id, process.cwd());
1356
+ sessionCreatedRef.current = true;
1026
1357
  }
1027
- }, []);
1358
+ };
1028
1359
  useEffect(() => {
1029
1360
  if (initialPrompt && state === "running") {
1030
1361
  if (initialDiscuss) {
@@ -1048,7 +1379,9 @@ function App({
1048
1379
  if (runningRoundRef.current !== null) {
1049
1380
  deleteMessagesFromRound(sessionId, runningRoundRef.current);
1050
1381
  const fromRound = runningRoundRef.current;
1051
- setMessages((prev) => prev.filter((m) => m.round < fromRound));
1382
+ const remaining = messages.filter((m) => m.round < fromRound);
1383
+ setMessages(remaining);
1384
+ setCommittedCount(remaining.length);
1052
1385
  runningRoundRef.current = null;
1053
1386
  }
1054
1387
  setThinkingAgents([]);
@@ -1057,8 +1390,8 @@ function App({
1057
1390
  setState("input");
1058
1391
  }
1059
1392
  });
1060
- const runAgents = async (currentMessages, round, target, promptOverride, isDebate = false) => {
1061
- const activeAgents = target === "both" ? [...pair] : [target];
1393
+ const runAgents = async (currentMessages, round, target, promptOverride, isDebate = false, systemPromptPerAgent) => {
1394
+ const activeAgents = Array.isArray(target) ? target : target === "both" ? [...pair] : [target];
1062
1395
  setThinkingAgents(activeAgents);
1063
1396
  const history = formatConversationHistory(
1064
1397
  currentMessages.map((m) => ({
@@ -1069,20 +1402,26 @@ function App({
1069
1402
  );
1070
1403
  const cwd = process.cwd();
1071
1404
  const isFirstRound = round === 0 && !existingSessionId;
1405
+ const userPrompt = currentMessages[currentMessages.length - 1]?.content || "";
1406
+ const isSingleAgent = !Array.isArray(target) && target !== "both";
1407
+ const promptPair = Array.isArray(target) ? target : pair;
1072
1408
  const agentPrompt = (_agent) => {
1073
1409
  if (promptOverride) return promptOverride;
1074
- if (isFirstRound && target === "both") {
1075
- return currentMessages[currentMessages.length - 1]?.content || "";
1076
- }
1410
+ if (isSingleAgent) return userPrompt;
1411
+ if (isFirstRound) return userPrompt;
1077
1412
  return "Provide your response for this round.";
1078
1413
  };
1079
1414
  const agentSystemPrompt = (agent) => {
1415
+ if (systemPromptPerAgent?.[agent]) return systemPromptPerAgent[agent];
1416
+ if (isSingleAgent) {
1417
+ return history ? directPrompt(agent, history) : void 0;
1418
+ }
1080
1419
  if (isDebate) {
1081
- if (isFirstRound) return debatePrompt(agent, pair);
1082
- return debateRoundPrompt(agent, history, pair);
1420
+ if (isFirstRound) return debateSystemPrompt(agent, promptPair);
1421
+ return debateRoundPrompt(agent, history, promptPair);
1083
1422
  }
1084
- if (isFirstRound && target === "both") return collaborationPrompt(agent, pair);
1085
- return discussionPrompt(agent, history, pair);
1423
+ if (isFirstRound) return collaborationSystemPrompt(agent, promptPair);
1424
+ return discussionRoundPrompt(agent, history, promptPair);
1086
1425
  };
1087
1426
  const ac = new AbortController();
1088
1427
  abortRef.current = ac;
@@ -1116,22 +1455,26 @@ function App({
1116
1455
  if (result.status === "fulfilled") {
1117
1456
  const resp = result.value;
1118
1457
  const msg = {
1458
+ id: `${round}-${agent}`,
1119
1459
  role: agent,
1120
1460
  content: resp.error || resp.text,
1121
1461
  round,
1122
1462
  error: !!resp.error
1123
1463
  };
1124
1464
  newMessages.push(msg);
1465
+ const metadata = isDebate && !resp.error ? analysisToMetadata(parseRoundAnalysis(agent, resp.text)) : void 0;
1125
1466
  insertMessage({
1126
1467
  sessionId,
1127
1468
  role: agent,
1128
1469
  content: resp.error || resp.text,
1129
1470
  round,
1130
- durationMs: resp.durationMs
1471
+ durationMs: resp.durationMs,
1472
+ metadata
1131
1473
  });
1132
1474
  } else {
1133
1475
  const errorMsg = result.reason?.message || "Failed to run";
1134
1476
  const msg = {
1477
+ id: `${round}-${agent}`,
1135
1478
  role: agent,
1136
1479
  content: errorMsg,
1137
1480
  round,
@@ -1149,13 +1492,19 @@ function App({
1149
1492
  return newMessages;
1150
1493
  };
1151
1494
  const runRound = async (rawInput) => {
1152
- const { target, prompt, discuss } = parseInput(rawInput, pair);
1495
+ const { target, prompt, discuss } = parseInput(rawInput);
1153
1496
  if (discuss) {
1154
- return runDiscussion(prompt);
1497
+ return runDiscussion(prompt, Array.isArray(target) ? target : void 0);
1155
1498
  }
1156
1499
  const currentRound = roundNum;
1157
1500
  runningRoundRef.current = currentRound;
1501
+ ensureSession(sessionId);
1502
+ if (currentRound === 0) {
1503
+ const title = prompt.length > 60 ? prompt.slice(0, 57) + "..." : prompt;
1504
+ updateSessionTitle(sessionId, title);
1505
+ }
1158
1506
  const userMsg = {
1507
+ id: `${currentRound}-user`,
1159
1508
  role: "user",
1160
1509
  content: rawInput,
1161
1510
  round: currentRound
@@ -1171,20 +1520,25 @@ function App({
1171
1520
  const newMessages = await runAgents(allMessages, currentRound, target);
1172
1521
  if (newMessages.length === 0) return;
1173
1522
  setMessages((prev) => [...prev, ...newMessages]);
1523
+ setCommittedCount((prev) => prev + 1 + newMessages.length);
1174
1524
  setRoundNum(currentRound + 1);
1175
1525
  runningRoundRef.current = null;
1176
- if (currentRound === 0 && !existingSessionId) {
1177
- const title = prompt.length > 60 ? prompt.slice(0, 57) + "..." : prompt;
1178
- updateSessionTitle(sessionId, title);
1179
- }
1180
1526
  touchSession(sessionId);
1181
1527
  setState("input");
1182
1528
  };
1183
- const runDiscussion = async (prompt) => {
1529
+ const runDiscussion = async (prompt, adHocPair) => {
1530
+ const discussionTarget = adHocPair ?? "both";
1531
+ const activePair = adHocPair ?? pair;
1184
1532
  setConsensusReached(false);
1185
1533
  let currentRound = roundNum;
1186
1534
  runningRoundRef.current = currentRound;
1535
+ ensureSession(sessionId);
1536
+ if (currentRound === 0) {
1537
+ const title = prompt.length > 60 ? prompt.slice(0, 57) + "..." : prompt;
1538
+ updateSessionTitle(sessionId, title);
1539
+ }
1187
1540
  const userMsg = {
1541
+ id: `${currentRound}-user`,
1188
1542
  role: "user",
1189
1543
  content: `discuss ${prompt}`,
1190
1544
  round: currentRound
@@ -1197,29 +1551,63 @@ function App({
1197
1551
  round: currentRound
1198
1552
  });
1199
1553
  let allMessages = [...messages, userMsg];
1200
- if (currentRound === 0 && !existingSessionId) {
1201
- const title = prompt.length > 60 ? prompt.slice(0, 57) + "..." : prompt;
1202
- updateSessionTitle(sessionId, title);
1203
- }
1554
+ const discState = {
1555
+ round: 0,
1556
+ analyses: [],
1557
+ terminated: false
1558
+ };
1204
1559
  for (let disc = 1; disc <= config.discussion.max_rounds; disc++) {
1205
1560
  setDiscussionRound(disc);
1561
+ discState.round = disc;
1562
+ let perAgentPrompts;
1563
+ if (disc >= 2) {
1564
+ const context = buildConversationContext(
1565
+ allMessages.map((m) => ({
1566
+ role: m.role,
1567
+ agent: isValidAgentName(m.role) ? m.role : void 0,
1568
+ content: m.content
1569
+ })),
1570
+ discState.analyses,
1571
+ disc,
1572
+ activePair
1573
+ );
1574
+ perAgentPrompts = {};
1575
+ for (const agent of activePair) {
1576
+ let prompt_text = "";
1577
+ if (shouldInjectSteelman(discState)) {
1578
+ prompt_text += steelmanPrompt();
1579
+ }
1580
+ prompt_text += debateRoundPrompt(agent, context, activePair);
1581
+ perAgentPrompts[agent] = prompt_text;
1582
+ }
1583
+ }
1206
1584
  const newMessages = await runAgents(
1207
1585
  allMessages,
1208
1586
  currentRound,
1209
- "both",
1587
+ discussionTarget,
1210
1588
  disc === 1 ? prompt : "Provide your response for this round.",
1211
- true
1589
+ true,
1590
+ perAgentPrompts
1212
1591
  );
1213
1592
  if (newMessages.length === 0) break;
1214
1593
  setMessages((prev) => [...prev, ...newMessages]);
1594
+ setCommittedCount((prev) => prev + (disc === 1 ? 1 : 0) + newMessages.length);
1215
1595
  allMessages = [...allMessages, ...newMessages];
1216
1596
  currentRound++;
1217
- const consensusFlags = pair.map((agent) => {
1597
+ const roundAnalyses = activePair.map((agent) => {
1218
1598
  const msg = newMessages.find((m) => m.role === agent && !m.error);
1219
- return msg?.content.includes(CONSENSUS_MARKER) ?? false;
1220
- });
1221
- if (consensusFlags.every(Boolean)) {
1222
- setConsensusReached(true);
1599
+ if (!msg) return null;
1600
+ return parseRoundAnalysis(agent, msg.content);
1601
+ }).filter((a) => a !== null);
1602
+ discState.analyses.push(roundAnalyses);
1603
+ const termResult = checkTermination(discState, config.discussion.max_rounds);
1604
+ if (termResult.terminated && termResult.reason) {
1605
+ discState.terminated = true;
1606
+ discState.terminationReason = termResult.reason;
1607
+ setStatusMessage(terminationMessage(termResult.reason));
1608
+ if (termResult.reason === "mutual-consensus") {
1609
+ setConsensusReached(true);
1610
+ }
1223
1611
  break;
1224
1612
  }
1225
1613
  }
@@ -1256,9 +1644,10 @@ function App({
1256
1644
  }
1257
1645
  if (value === "/new") {
1258
1646
  const newId = nanoid(12);
1259
- createSession(newId, process.cwd());
1647
+ sessionCreatedRef.current = false;
1260
1648
  setSessionId(newId);
1261
1649
  setMessages([]);
1650
+ setCommittedCount(0);
1262
1651
  setRoundNum(0);
1263
1652
  setConsensusReached(false);
1264
1653
  setDiscussionRound(0);
@@ -1279,23 +1668,21 @@ function App({
1279
1668
  setState("running");
1280
1669
  runRound(value);
1281
1670
  };
1671
+ const staticItems = useMemo(() => [
1672
+ { id: `header-${sessionId}`, role: "__header", content: "", round: -1 },
1673
+ ...messages.slice(0, committedCount)
1674
+ ], [sessionId, messages, committedCount]);
1282
1675
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1283
- /* @__PURE__ */ jsx2(Header, { sessionId }),
1284
- messages.map((msg, i) => {
1285
- if (msg.role === "user") {
1286
- return /* @__PURE__ */ jsx2(UserMessage, { content: msg.content }, i);
1287
- }
1288
- if (isValidAgentName(msg.role)) {
1289
- return /* @__PURE__ */ jsx2(
1290
- AgentResponseBlock,
1291
- {
1292
- agent: msg.role,
1293
- content: msg.content,
1294
- error: msg.error
1295
- },
1296
- i
1297
- );
1298
- }
1676
+ /* @__PURE__ */ jsx2(Static, { items: staticItems, children: (item) => {
1677
+ if (item.role === "__header") return /* @__PURE__ */ jsx2(Header, { sessionId }, item.id);
1678
+ if (item.role === "user") return /* @__PURE__ */ jsx2(UserMessage, { content: item.content }, item.id);
1679
+ if (isValidAgentName(item.role)) return /* @__PURE__ */ jsx2(AgentResponseBlock, { agent: item.role, content: item.content, error: item.error }, item.id);
1680
+ return /* @__PURE__ */ jsx2(Box2, {}, item.id);
1681
+ } }),
1682
+ messages.length === 0 && state === "input" && /* @__PURE__ */ jsx2(QuickHelp, {}),
1683
+ messages.slice(committedCount).map((msg) => {
1684
+ if (msg.role === "user") return /* @__PURE__ */ jsx2(UserMessage, { content: msg.content }, msg.id);
1685
+ if (isValidAgentName(msg.role)) return /* @__PURE__ */ jsx2(AgentResponseBlock, { agent: msg.role, content: msg.content, error: msg.error }, msg.id);
1299
1686
  return null;
1300
1687
  }),
1301
1688
  discussionRound > 0 && thinkingAgents.length > 0 && /* @__PURE__ */ jsx2(DiscussionStatus, { round: discussionRound, maxRounds: config.discussion.max_rounds }),
@@ -1341,6 +1728,7 @@ function showTranscriptMarkdown(sessionId) {
1341
1728
  function showTranscript(sessionId) {
1342
1729
  const dbMessages = getMessages(sessionId);
1343
1730
  const messages = dbMessages.map((m) => ({
1731
+ id: `${m.round}-${m.role}`,
1344
1732
  role: m.role,
1345
1733
  content: m.content,
1346
1734
  round: m.round
@@ -1348,12 +1736,12 @@ function showTranscript(sessionId) {
1348
1736
  const { unmount } = render2(
1349
1737
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1350
1738
  /* @__PURE__ */ jsx2(Header, { sessionId }),
1351
- messages.map((msg, i) => {
1739
+ messages.map((msg) => {
1352
1740
  if (msg.role === "user") {
1353
- return /* @__PURE__ */ jsx2(UserMessage, { content: msg.content }, i);
1741
+ return /* @__PURE__ */ jsx2(UserMessage, { content: msg.content }, msg.id);
1354
1742
  }
1355
1743
  if (isValidAgentName(msg.role)) {
1356
- return /* @__PURE__ */ jsx2(AgentResponseBlock, { agent: msg.role, content: msg.content }, i);
1744
+ return /* @__PURE__ */ jsx2(AgentResponseBlock, { agent: msg.role, content: msg.content }, msg.id);
1357
1745
  }
1358
1746
  return null;
1359
1747
  }),
@@ -1424,7 +1812,7 @@ function resolveAgentModels(pair, opts, config) {
1424
1812
  };
1425
1813
  }
1426
1814
  var program = new Command();
1427
- program.name("tagteam").description("Tag Team - Orchestrate AI agents collaboratively").version("0.2.0").option("--agents <pair>", "Agent pair to use (comma-separated, e.g. claude,gemini)").option("--claude-model <model>", "Claude model to use").option("--codex-model <model>", "Codex model to use").option("--gemini-model <model>", "Gemini model to use").argument("[prompt...]", "Prompt to send to both agents").action(async (promptParts, opts) => {
1815
+ program.name("tagteam").description("Tag Team - Orchestrate AI agents collaboratively").version(createRequire(import.meta.url)("../package.json").version).option("--agents <pair>", "Agent pair to use (comma-separated, e.g. claude,gemini)").option("--claude-model <model>", "Claude model to use").option("--codex-model <model>", "Codex model to use").option("--gemini-model <model>", "Gemini model to use").argument("[prompt...]", "Prompt to send to both agents").action(async (promptParts, opts) => {
1428
1816
  const config = loadConfig();
1429
1817
  const pair = resolveAgentPair(opts, config);
1430
1818
  preflight(pair);
@@ -1464,6 +1852,7 @@ program.command("continue").description("Resume the most recent session").action
1464
1852
  }
1465
1853
  const dbMessages = getMessages(session.id);
1466
1854
  const transcript = dbMessages.map((m) => ({
1855
+ id: `${m.round}-${m.role}`,
1467
1856
  role: m.role,
1468
1857
  content: m.content,
1469
1858
  round: m.round
@@ -1514,6 +1903,7 @@ program.command("resume [id]").description("Resume a session by ID, or pick inte
1514
1903
  const session2 = sessions[num - 1];
1515
1904
  const dbMessages2 = getMessages(session2.id);
1516
1905
  const transcript2 = dbMessages2.map((m) => ({
1906
+ id: `${m.round}-${m.role}`,
1517
1907
  role: m.role,
1518
1908
  content: m.content,
1519
1909
  round: m.round
@@ -1537,6 +1927,7 @@ program.command("resume [id]").description("Resume a session by ID, or pick inte
1537
1927
  }
1538
1928
  const dbMessages = getMessages(session.id);
1539
1929
  const transcript = dbMessages.map((m) => ({
1930
+ id: `${m.round}-${m.role}`,
1540
1931
  role: m.role,
1541
1932
  content: m.content,
1542
1933
  round: m.round
@@ -1550,13 +1941,51 @@ program.command("resume [id]").description("Resume a session by ID, or pick inte
1550
1941
  });
1551
1942
  await instance.waitUntilExit();
1552
1943
  });
1553
- program.command("history").description("List recent sessions").option("-n, --limit <n>", "Number of sessions to show", parseInt, 20).action((opts) => {
1944
+ var historyCmd = program.command("history").description("List, remove, or clear sessions").option("-n, --limit <n>", "Number of sessions to show", parseInt, 20).action((opts) => {
1554
1945
  const sessions = listSessions(opts.limit);
1555
1946
  console.log(chalk.bold("\n Recent sessions:\n"));
1556
1947
  showSessionList(sessions);
1557
1948
  console.log();
1558
1949
  closeDb();
1559
1950
  });
1951
+ historyCmd.command("rm <id>").description("Delete a session by ID or prefix").action((id) => {
1952
+ const session = getSession(id) || getSessionByPrefix(id);
1953
+ if (!session) {
1954
+ console.log(chalk.red(` Session not found: ${id}`));
1955
+ process.exit(1);
1956
+ }
1957
+ deleteSession(session.id);
1958
+ const sid = session.id.slice(0, 7);
1959
+ const title = session.title || "(untitled)";
1960
+ console.log(chalk.green(` Deleted session ${sid} \u2014 ${title}`));
1961
+ closeDb();
1962
+ });
1963
+ historyCmd.command("clear").description("Delete all sessions").action(async () => {
1964
+ const sessions = listSessions();
1965
+ if (sessions.length === 0) {
1966
+ console.log(chalk.yellow(" No sessions to delete."));
1967
+ closeDb();
1968
+ return;
1969
+ }
1970
+ const readline = await import("readline");
1971
+ const rl = readline.createInterface({
1972
+ input: process.stdin,
1973
+ output: process.stdout
1974
+ });
1975
+ rl.question(
1976
+ chalk.yellow(` Delete all ${sessions.length} session(s)? [y/N] `),
1977
+ (answer) => {
1978
+ rl.close();
1979
+ if (answer.trim().toLowerCase() === "y") {
1980
+ deleteAllSessions();
1981
+ console.log(chalk.green(` Deleted ${sessions.length} session(s).`));
1982
+ } else {
1983
+ console.log(" Cancelled.");
1984
+ }
1985
+ closeDb();
1986
+ }
1987
+ );
1988
+ });
1560
1989
  program.command("show <id>").description("Print full transcript for a session").option("-m, --markdown", "Output as GitHub-compatible markdown").action((id, opts) => {
1561
1990
  const session = getSession(id) || getSessionByPrefix(id);
1562
1991
  if (!session) {