tagteam 0.3.0 → 0.4.2

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,7 +2,8 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import { execSync as execSync2 } from "child_process";
5
+ import { createRequire } from "module";
6
+ import { execSync as execSync3 } from "child_process";
6
7
  import chalk from "chalk";
7
8
 
8
9
  // src/config.ts
@@ -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
  }
@@ -569,6 +627,34 @@ function copyToClipboard(text) {
569
627
  }
570
628
  }
571
629
 
630
+ // src/gist.ts
631
+ import { execSync as execSync2 } from "child_process";
632
+ function createGist(content, filename) {
633
+ try {
634
+ execSync2("gh auth status", { stdio: "ignore" });
635
+ } catch {
636
+ try {
637
+ execSync2("which gh", { stdio: "ignore" });
638
+ } catch {
639
+ throw new Error(
640
+ "gh CLI not found. Install it from https://cli.github.com"
641
+ );
642
+ }
643
+ throw new Error(
644
+ "gh CLI is not authenticated. Run: gh auth login"
645
+ );
646
+ }
647
+ try {
648
+ const result = execSync2(
649
+ `gh gist create --private --filename "${filename}" -`,
650
+ { input: content, stdio: ["pipe", "pipe", "ignore"] }
651
+ );
652
+ return result.toString().trim();
653
+ } catch {
654
+ throw new Error("Failed to create gist.");
655
+ }
656
+ }
657
+
572
658
  // src/db/index.ts
573
659
  import Database from "better-sqlite3";
574
660
  import { join as join2 } from "path";
@@ -694,66 +780,279 @@ function deleteMessagesFromRound(sessionId, fromRound) {
694
780
  }
695
781
 
696
782
  // src/prompts.ts
697
- function otherAgent(agent, pair) {
698
- const other = pair[0] === agent ? pair[1] : pair[0];
699
- const desc = getAgent(other);
700
- return `${desc.displayName} (${desc.org})`;
783
+ var CONSENSUS_MARKER = "[CONSENSUS]";
784
+ function formatConversationHistory(messages) {
785
+ return messages.map((m) => {
786
+ let label;
787
+ if (m.role === "user") {
788
+ label = "User";
789
+ } else if (isValidAgentName(m.role)) {
790
+ label = getAgent(m.role).displayName;
791
+ } else if (m.agent && isValidAgentName(m.agent)) {
792
+ label = getAgent(m.agent).displayName;
793
+ } else {
794
+ label = m.role;
795
+ }
796
+ return `[${label}]: ${m.content}`;
797
+ }).join("\n\n");
701
798
  }
702
- function collaborationPrompt(agent, pair) {
703
- 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.`;
799
+ function basePrompt() {
800
+ return `You are one of two expert coding agents in a structured technical discussion.
801
+ You will independently analyze the problem, then engage in focused rounds of
802
+ critique and refinement with your peer.
803
+
804
+ Ground rules:
805
+ - You are evaluated on the ACCURACY and QUALITY of your final position, not
806
+ on agreement with your peer.
807
+ - When you change your position, you MUST name the specific argument that
808
+ changed your mind and explain why your previous reasoning was flawed.
809
+ Changing position without this justification is not acceptable.
810
+ - Each response must either: (a) introduce new evidence or a new argument,
811
+ (b) identify a specific logical flaw or unsupported claim in your peer's
812
+ reasoning, or (c) concede a point with explicit justification. Restating
813
+ or paraphrasing existing points is not acceptable.
814
+ - Your peer is a different AI model with different training. Their perspective
815
+ may reveal genuine blind spots in yours \u2014 and vice versa.`;
816
+ }
817
+ var ROLE_TEMPLATES = {
818
+ claude: `Your role: Architecture & Implementation Reviewer.
819
+
820
+ Focus your analysis on:
821
+ - Code structure, design patterns, and maintainability
822
+ - Multi-file coherence \u2014 how changes ripple across the codebase
823
+ - Production readiness \u2014 error handling, logging, edge cases in real usage
824
+ - Proposing complete, working implementations (not just pseudocode)
825
+
826
+ When you propose a solution, provide the actual implementation. When you
827
+ critique, point to specific structural issues and show what the fix looks
828
+ like. Your peer's role is {peerRole} \u2014 they will stress-test your proposals
829
+ from a different angle.`,
830
+ codex: `Your role: Correctness & Standards Reviewer.
831
+
832
+ Focus your analysis on:
833
+ - Algorithmic correctness \u2014 does the logic actually work for all inputs?
834
+ - Edge cases and failure modes \u2014 what breaks, what's untested?
835
+ - Standards compliance \u2014 does this follow language/framework conventions?
836
+ - Performance characteristics \u2014 time/space complexity, benchmarks
837
+
838
+ When you critique, provide specific test cases or inputs that demonstrate
839
+ the issue. When you propose alternatives, explain the correctness guarantees.
840
+ Your peer's role is {peerRole} \u2014 they will focus on different aspects of the
841
+ same problem.`,
842
+ gemini: `Your role: Strategic Context Analyst.
843
+
844
+ Focus your analysis on:
845
+ - Broad codebase context \u2014 how does this change fit the larger system?
846
+ - Current ecosystem conventions \u2014 what do the docs, community, and recent
847
+ releases recommend?
848
+ - Upstream and downstream effects \u2014 what will this break or enable elsewhere?
849
+ - Scope and planning \u2014 is this the right approach at the right level of
850
+ abstraction?
851
+
852
+ When you critique, ground your position in the broader context your peer may
853
+ be missing. When you propose alternatives, explain the architectural tradeoffs.
854
+ Your peer's role is {peerRole} \u2014 they will focus on different aspects of the
855
+ same problem.`
856
+ };
857
+ function rolePrompt(agent, pair) {
858
+ const template = ROLE_TEMPLATES[agent];
859
+ const peerRole = getPeerRoleDescription(agent, pair);
860
+ return template.replace("{peerRole}", peerRole);
704
861
  }
705
- function discussionPrompt(agent, conversationHistory, pair) {
706
- return `${collaborationPrompt(agent, pair)}
862
+ function collaborationSystemPrompt(agent, pair) {
863
+ return `${basePrompt()}
707
864
 
708
- Here is the conversation so far:
865
+ ${rolePrompt(agent, pair)}`;
866
+ }
867
+ function discussionRoundPrompt(agent, conversationContext, pair) {
868
+ return `${basePrompt()}
709
869
 
710
- ${conversationHistory}
870
+ ${rolePrompt(agent, pair)}
871
+
872
+ Here is the discussion so far:
873
+
874
+ ${conversationContext}
875
+
876
+ For this round:
877
+ 1. What is the strongest point in your peer's response?
878
+ 2. What is the weakest point, or what claim lacks supporting evidence?
879
+ 3. Has your position changed? State one of: HELD / PARTIALLY_CHANGED / CHANGED
880
+ \u2014 with explicit reasoning for why.
881
+ 4. If proposing code, show the specific implementation and explain tradeoffs
882
+ versus your peer's approach.
883
+ 5. Confidence in your current position: LOW | MEDIUM | HIGH
711
884
 
712
- Now provide your response for this discussion round. Build on what was said, highlight agreements, and address any disagreements constructively. Be concise.`;
885
+ CONFIDENCE: HIGH | MEDIUM | LOW
886
+
887
+ Keep it concise. Do not restate points already established.`;
713
888
  }
714
- function debatePrompt(agent, pair) {
715
- const other = otherAgent(agent, pair);
716
- 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.
889
+ function debateSystemPrompt(agent, pair) {
890
+ return `${basePrompt()}
891
+
892
+ ${rolePrompt(agent, pair)}
893
+
894
+ This is a structured discussion aimed at reaching a well-reasoned position
895
+ through genuine deliberation.
896
+
897
+ Additional rules for discussion mode:
898
+ - Structure your arguments: STATE your claim, provide EVIDENCE (code examples,
899
+ documentation, benchmarks), explain your REASONING connecting evidence to
900
+ claim, and note CAVEATS (when your claim doesn't hold).
901
+ - Express confidence: end your response with CONFIDENCE: HIGH | MEDIUM | LOW
902
+ and a one-line explanation of what would change your mind.
903
+ - Consensus signaling: when you believe you and your peer agree on all key
904
+ points AND your confidence is HIGH, end your response with ${CONSENSUS_MARKER} on
905
+ its own line. Only signal consensus when:
906
+ (a) You can state the shared position in one sentence
907
+ (b) You have HIGH confidence
908
+ (c) You are not just deferring \u2014 you genuinely agree with the reasoning`;
909
+ }
910
+ function debateRoundPrompt(agent, conversationContext, pair) {
911
+ return `${debateSystemPrompt(agent, pair)}
717
912
 
718
- Your goal is to reach consensus through constructive discussion. In each round:
719
- - Address specific points of agreement and disagreement
720
- - Refine your position based on valid arguments from ${other}
721
- - Be concise \u2014 don't repeat points already established
913
+ ${conversationContext}
722
914
 
723
- 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.`;
915
+ For this round:
916
+ 1. Address your peer's strongest argument directly \u2014 do you accept it? Why or
917
+ why not?
918
+ 2. If your peer identified a flaw in your reasoning, acknowledge it explicitly
919
+ or defend with new evidence.
920
+ 3. State your current position with EVIDENCE and REASONING.
921
+ 4. CONFIDENCE: HIGH | MEDIUM | LOW \u2014 what specific evidence would change
922
+ your remaining position?
923
+ 5. If consensus: state the shared position in one sentence, then ${CONSENSUS_MARKER}.
924
+
925
+ POSITION: HELD | PARTIALLY_CHANGED | CHANGED`;
724
926
  }
725
- function debateRoundPrompt(agent, conversationHistory, pair) {
726
- const other = otherAgent(agent, pair);
727
- return `${debatePrompt(agent, pair)}
927
+ function steelmanPrompt() {
928
+ return `You and your peer appear to largely agree after Round 1. Before confirming
929
+ consensus, steelman the opposing view:
728
930
 
729
- Here is the conversation so far:
931
+ - What is the strongest argument AGAINST your shared position?
932
+ - What context or edge case might make a different approach better?
933
+ - Is there a tradeoff you're both overlooking?
730
934
 
731
- ${conversationHistory}
935
+ If after considering the counterarguments you still hold your position, explain
936
+ why the counterarguments don't apply here. Then proceed with your normal round
937
+ response.
732
938
 
733
- Respond to the latest round. If you agree with ${other}'s position on all key points, end with [CONSENSUS]. Otherwise, continue the discussion.`;
939
+ `;
734
940
  }
735
- function directPrompt(conversationHistory) {
736
- return `You are being addressed directly in a multi-agent session. Here is the conversation so far:
941
+ function directPrompt(agent, conversationHistory) {
942
+ const profile = getAgentProfile(agent);
943
+ const focusAreas = profile.focus.map((f) => `- ${f}`).join("\n");
944
+ return `You are being addressed directly in a multi-agent session. The user wants YOUR
945
+ specific perspective.
737
946
 
947
+ Here is the conversation so far:
738
948
  ${conversationHistory}
739
949
 
740
- Respond to the user's latest message. Be concise.`;
950
+ Respond to the user's latest message. Focus on your area of expertise:
951
+ ${focusAreas}
952
+
953
+ Be concise and direct.`;
741
954
  }
742
- var CONSENSUS_MARKER = "[CONSENSUS]";
743
- function formatConversationHistory(messages) {
744
- return messages.map((m) => {
745
- let label;
746
- if (m.role === "user") {
747
- label = "User";
748
- } else if (isValidAgentName(m.role)) {
749
- label = getAgent(m.role).displayName;
750
- } else if (m.agent && isValidAgentName(m.agent)) {
751
- label = getAgent(m.agent).displayName;
752
- } else {
753
- label = m.role;
955
+
956
+ // src/discussion.ts
957
+ var CONFIDENCE_RE = /CONFIDENCE:\s*(HIGH|MEDIUM|LOW)/i;
958
+ var POSITION_RE = /POSITION:\s*(HELD|PARTIALLY_CHANGED|CHANGED)/i;
959
+ var CONSENSUS_RE = /\[CONSENSUS\]/;
960
+ function parseRoundAnalysis(agent, responseText) {
961
+ const confidenceMatch = responseText.match(CONFIDENCE_RE);
962
+ const positionMatch = responseText.match(POSITION_RE);
963
+ const signaledConsensus = CONSENSUS_RE.test(responseText);
964
+ const stripped = responseText.replace(CONFIDENCE_RE, "").replace(POSITION_RE, "").replace(CONSENSUS_RE, "").trim();
965
+ const hasNovelContent = stripped.length > 100;
966
+ return {
967
+ agent,
968
+ confidence: confidenceMatch?.[1]?.toUpperCase() ?? "MEDIUM",
969
+ positionChange: positionMatch?.[1]?.toUpperCase() ?? "HELD",
970
+ signaledConsensus,
971
+ hasNovelContent
972
+ };
973
+ }
974
+ function checkTermination(state, maxRounds) {
975
+ const { round, analyses } = state;
976
+ if (analyses.length > 0) {
977
+ const latest = analyses[analyses.length - 1];
978
+ if (latest && latest.length >= 2) {
979
+ const allConsensus = latest.every((a) => a.signaledConsensus);
980
+ const allHigh = latest.every((a) => a.confidence === "HIGH");
981
+ if (allConsensus && allHigh) {
982
+ return { terminated: true, reason: "mutual-consensus" };
983
+ }
754
984
  }
755
- return `[${label}]: ${m.content}`;
756
- }).join("\n\n");
985
+ }
986
+ if (analyses.length >= 2) {
987
+ const prev = analyses[analyses.length - 2];
988
+ const curr = analyses[analyses.length - 1];
989
+ if (prev && curr && prev.length >= 2 && curr.length >= 2) {
990
+ const prevStale = prev.every((a) => a.positionChange === "HELD" && !a.hasNovelContent);
991
+ const currStale = curr.every((a) => a.positionChange === "HELD" && !a.hasNovelContent);
992
+ if (prevStale && currStale) {
993
+ return { terminated: true, reason: "stale-no-progress" };
994
+ }
995
+ }
996
+ }
997
+ if (analyses.length >= 2) {
998
+ const prev = analyses[analyses.length - 2];
999
+ const curr = analyses[analyses.length - 1];
1000
+ if (prev && curr && prev.length >= 2 && curr.length >= 2) {
1001
+ const prevSwap = prev.every((a) => a.positionChange === "CHANGED");
1002
+ const currSwap = curr.every((a) => a.positionChange === "CHANGED");
1003
+ if (prevSwap && currSwap) {
1004
+ return { terminated: true, reason: "cyclic-swap" };
1005
+ }
1006
+ }
1007
+ }
1008
+ if (round >= maxRounds) {
1009
+ return { terminated: true, reason: "max-rounds" };
1010
+ }
1011
+ return { terminated: false };
1012
+ }
1013
+ function terminationMessage(reason) {
1014
+ switch (reason) {
1015
+ case "mutual-consensus":
1016
+ return "Consensus reached.";
1017
+ case "stale-no-progress":
1018
+ return "Discussion stalled \u2014 no new arguments. Showing final positions.";
1019
+ case "cyclic-swap":
1020
+ return "Agents are trading positions. Showing both perspectives.";
1021
+ case "max-rounds":
1022
+ return "Maximum rounds reached. Showing final positions.";
1023
+ }
1024
+ }
1025
+ function shouldInjectSteelman(state) {
1026
+ return state.round === 1 && state.analyses.length === 1;
1027
+ }
1028
+ function buildConversationContext(allMessages, analyses, currentRound, _pair) {
1029
+ if (currentRound <= 2) {
1030
+ return formatConversationHistory(allMessages);
1031
+ }
1032
+ const summaryParts = [];
1033
+ for (let i = 0; i < analyses.length - 1; i++) {
1034
+ const roundAnalyses = analyses[i];
1035
+ if (!roundAnalyses) continue;
1036
+ const roundSummary = roundAnalyses.map((a) => {
1037
+ return `${a.agent}: confidence=${a.confidence}, position=${a.positionChange}${a.signaledConsensus ? ", signaled consensus" : ""}`;
1038
+ }).join("; ");
1039
+ summaryParts.push(`Round ${i + 1}: ${roundSummary}`);
1040
+ }
1041
+ const latestMessages = allMessages.slice(-3);
1042
+ const summary = summaryParts.length > 0 ? `Previous rounds summary:
1043
+ ${summaryParts.join("\n")}
1044
+
1045
+ Latest exchange:
1046
+ ${formatConversationHistory(latestMessages)}` : formatConversationHistory(allMessages);
1047
+ return summary;
1048
+ }
1049
+ function analysisToMetadata(analysis) {
1050
+ return {
1051
+ confidence: analysis.confidence,
1052
+ positionChange: analysis.positionChange,
1053
+ signaledConsensus: analysis.signaledConsensus,
1054
+ hasNovelContent: analysis.hasNovelContent
1055
+ };
757
1056
  }
758
1057
 
759
1058
  // src/config-editor.tsx
@@ -940,10 +1239,10 @@ function parseInput(input) {
940
1239
  return { target: "both", prompt: input, discuss: false };
941
1240
  }
942
1241
  function RenderedMarkdown({ text }) {
943
- const rendered = marked.parse(text).trimEnd();
1242
+ const rendered = useMemo(() => marked.parse(text).trimEnd(), [text]);
944
1243
  return /* @__PURE__ */ jsx2(Text2, { children: rendered });
945
1244
  }
946
- function AgentResponseBlock({
1245
+ var AgentResponseBlock = React2.memo(function AgentResponseBlock2({
947
1246
  agent,
948
1247
  content,
949
1248
  error
@@ -965,7 +1264,7 @@ function AgentResponseBlock({
965
1264
  ] }),
966
1265
  /* @__PURE__ */ jsx2(Box2, { marginLeft: 1, children: /* @__PURE__ */ jsx2(RenderedMarkdown, { text: content }) })
967
1266
  ] });
968
- }
1267
+ });
969
1268
  function Header({ sessionId }) {
970
1269
  return /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
971
1270
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2500\u2500 " }),
@@ -992,7 +1291,7 @@ function QuickHelp() {
992
1291
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "/help for more" })
993
1292
  ] });
994
1293
  }
995
- function UserMessage({ content }) {
1294
+ var UserMessage = React2.memo(function UserMessage2({ content }) {
996
1295
  return /* @__PURE__ */ jsxs2(Box2, { marginLeft: 1, marginBottom: 1, children: [
997
1296
  /* @__PURE__ */ jsxs2(Text2, { bold: true, color: "white", children: [
998
1297
  "You:",
@@ -1000,7 +1299,7 @@ function UserMessage({ content }) {
1000
1299
  ] }),
1001
1300
  /* @__PURE__ */ jsx2(Text2, { children: content })
1002
1301
  ] });
1003
- }
1302
+ });
1004
1303
  function ThinkingIndicator({ agent }) {
1005
1304
  const descriptor = getAgent(agent);
1006
1305
  return /* @__PURE__ */ jsxs2(Box2, { marginLeft: 1, children: [
@@ -1077,6 +1376,7 @@ function App({
1077
1376
  }
1078
1377
  return 0;
1079
1378
  });
1379
+ const [committedCount, setCommittedCount] = useState2(showTranscript2?.length ?? 0);
1080
1380
  const sessionCreatedRef = useRef(!!existingSessionId);
1081
1381
  const ensureSession = (id) => {
1082
1382
  if (!sessionCreatedRef.current) {
@@ -1107,7 +1407,9 @@ function App({
1107
1407
  if (runningRoundRef.current !== null) {
1108
1408
  deleteMessagesFromRound(sessionId, runningRoundRef.current);
1109
1409
  const fromRound = runningRoundRef.current;
1110
- setMessages((prev) => prev.filter((m) => m.round < fromRound));
1410
+ const remaining = messages.filter((m) => m.round < fromRound);
1411
+ setMessages(remaining);
1412
+ setCommittedCount(remaining.length);
1111
1413
  runningRoundRef.current = null;
1112
1414
  }
1113
1415
  setThinkingAgents([]);
@@ -1116,7 +1418,7 @@ function App({
1116
1418
  setState("input");
1117
1419
  }
1118
1420
  });
1119
- const runAgents = async (currentMessages, round, target, promptOverride, isDebate = false) => {
1421
+ const runAgents = async (currentMessages, round, target, promptOverride, isDebate = false, systemPromptPerAgent) => {
1120
1422
  const activeAgents = Array.isArray(target) ? target : target === "both" ? [...pair] : [target];
1121
1423
  setThinkingAgents(activeAgents);
1122
1424
  const history = formatConversationHistory(
@@ -1138,15 +1440,16 @@ function App({
1138
1440
  return "Provide your response for this round.";
1139
1441
  };
1140
1442
  const agentSystemPrompt = (agent) => {
1443
+ if (systemPromptPerAgent?.[agent]) return systemPromptPerAgent[agent];
1141
1444
  if (isSingleAgent) {
1142
- return history ? directPrompt(history) : void 0;
1445
+ return history ? directPrompt(agent, history) : void 0;
1143
1446
  }
1144
1447
  if (isDebate) {
1145
- if (isFirstRound) return debatePrompt(agent, promptPair);
1448
+ if (isFirstRound) return debateSystemPrompt(agent, promptPair);
1146
1449
  return debateRoundPrompt(agent, history, promptPair);
1147
1450
  }
1148
- if (isFirstRound) return collaborationPrompt(agent, promptPair);
1149
- return discussionPrompt(agent, history, promptPair);
1451
+ if (isFirstRound) return collaborationSystemPrompt(agent, promptPair);
1452
+ return discussionRoundPrompt(agent, history, promptPair);
1150
1453
  };
1151
1454
  const ac = new AbortController();
1152
1455
  abortRef.current = ac;
@@ -1180,22 +1483,26 @@ function App({
1180
1483
  if (result.status === "fulfilled") {
1181
1484
  const resp = result.value;
1182
1485
  const msg = {
1486
+ id: `${round}-${agent}`,
1183
1487
  role: agent,
1184
1488
  content: resp.error || resp.text,
1185
1489
  round,
1186
1490
  error: !!resp.error
1187
1491
  };
1188
1492
  newMessages.push(msg);
1493
+ const metadata = isDebate && !resp.error ? analysisToMetadata(parseRoundAnalysis(agent, resp.text)) : void 0;
1189
1494
  insertMessage({
1190
1495
  sessionId,
1191
1496
  role: agent,
1192
1497
  content: resp.error || resp.text,
1193
1498
  round,
1194
- durationMs: resp.durationMs
1499
+ durationMs: resp.durationMs,
1500
+ metadata
1195
1501
  });
1196
1502
  } else {
1197
1503
  const errorMsg = result.reason?.message || "Failed to run";
1198
1504
  const msg = {
1505
+ id: `${round}-${agent}`,
1199
1506
  role: agent,
1200
1507
  content: errorMsg,
1201
1508
  round,
@@ -1225,6 +1532,7 @@ function App({
1225
1532
  updateSessionTitle(sessionId, title);
1226
1533
  }
1227
1534
  const userMsg = {
1535
+ id: `${currentRound}-user`,
1228
1536
  role: "user",
1229
1537
  content: rawInput,
1230
1538
  round: currentRound
@@ -1240,6 +1548,7 @@ function App({
1240
1548
  const newMessages = await runAgents(allMessages, currentRound, target);
1241
1549
  if (newMessages.length === 0) return;
1242
1550
  setMessages((prev) => [...prev, ...newMessages]);
1551
+ setCommittedCount((prev) => prev + 1 + newMessages.length);
1243
1552
  setRoundNum(currentRound + 1);
1244
1553
  runningRoundRef.current = null;
1245
1554
  touchSession(sessionId);
@@ -1247,6 +1556,7 @@ function App({
1247
1556
  };
1248
1557
  const runDiscussion = async (prompt, adHocPair) => {
1249
1558
  const discussionTarget = adHocPair ?? "both";
1559
+ const activePair = adHocPair ?? pair;
1250
1560
  setConsensusReached(false);
1251
1561
  let currentRound = roundNum;
1252
1562
  runningRoundRef.current = currentRound;
@@ -1256,6 +1566,7 @@ function App({
1256
1566
  updateSessionTitle(sessionId, title);
1257
1567
  }
1258
1568
  const userMsg = {
1569
+ id: `${currentRound}-user`,
1259
1570
  role: "user",
1260
1571
  content: `discuss ${prompt}`,
1261
1572
  round: currentRound
@@ -1268,26 +1579,63 @@ function App({
1268
1579
  round: currentRound
1269
1580
  });
1270
1581
  let allMessages = [...messages, userMsg];
1582
+ const discState = {
1583
+ round: 0,
1584
+ analyses: [],
1585
+ terminated: false
1586
+ };
1271
1587
  for (let disc = 1; disc <= config.discussion.max_rounds; disc++) {
1272
1588
  setDiscussionRound(disc);
1589
+ discState.round = disc;
1590
+ let perAgentPrompts;
1591
+ if (disc >= 2) {
1592
+ const context = buildConversationContext(
1593
+ allMessages.map((m) => ({
1594
+ role: m.role,
1595
+ agent: isValidAgentName(m.role) ? m.role : void 0,
1596
+ content: m.content
1597
+ })),
1598
+ discState.analyses,
1599
+ disc,
1600
+ activePair
1601
+ );
1602
+ perAgentPrompts = {};
1603
+ for (const agent of activePair) {
1604
+ let prompt_text = "";
1605
+ if (shouldInjectSteelman(discState)) {
1606
+ prompt_text += steelmanPrompt();
1607
+ }
1608
+ prompt_text += debateRoundPrompt(agent, context, activePair);
1609
+ perAgentPrompts[agent] = prompt_text;
1610
+ }
1611
+ }
1273
1612
  const newMessages = await runAgents(
1274
1613
  allMessages,
1275
1614
  currentRound,
1276
1615
  discussionTarget,
1277
1616
  disc === 1 ? prompt : "Provide your response for this round.",
1278
- true
1617
+ true,
1618
+ perAgentPrompts
1279
1619
  );
1280
1620
  if (newMessages.length === 0) break;
1281
1621
  setMessages((prev) => [...prev, ...newMessages]);
1622
+ setCommittedCount((prev) => prev + (disc === 1 ? 1 : 0) + newMessages.length);
1282
1623
  allMessages = [...allMessages, ...newMessages];
1283
1624
  currentRound++;
1284
- const activePair = adHocPair ?? pair;
1285
- const consensusFlags = activePair.map((agent) => {
1625
+ const roundAnalyses = activePair.map((agent) => {
1286
1626
  const msg = newMessages.find((m) => m.role === agent && !m.error);
1287
- return msg?.content.includes(CONSENSUS_MARKER) ?? false;
1288
- });
1289
- if (consensusFlags.every(Boolean)) {
1290
- setConsensusReached(true);
1627
+ if (!msg) return null;
1628
+ return parseRoundAnalysis(agent, msg.content);
1629
+ }).filter((a) => a !== null);
1630
+ discState.analyses.push(roundAnalyses);
1631
+ const termResult = checkTermination(discState, config.discussion.max_rounds);
1632
+ if (termResult.terminated && termResult.reason) {
1633
+ discState.terminated = true;
1634
+ discState.terminationReason = termResult.reason;
1635
+ setStatusMessage(terminationMessage(termResult.reason));
1636
+ if (termResult.reason === "mutual-consensus") {
1637
+ setConsensusReached(true);
1638
+ }
1291
1639
  break;
1292
1640
  }
1293
1641
  }
@@ -1311,6 +1659,7 @@ function App({
1311
1659
  "/config Edit configuration",
1312
1660
  "/new Start a new session",
1313
1661
  "/copy Copy conversation to clipboard",
1662
+ "/gist Create a private GitHub gist",
1314
1663
  "/exit Exit the app",
1315
1664
  "",
1316
1665
  "Esc Interrupt running agents"
@@ -1327,6 +1676,7 @@ function App({
1327
1676
  sessionCreatedRef.current = false;
1328
1677
  setSessionId(newId);
1329
1678
  setMessages([]);
1679
+ setCommittedCount(0);
1330
1680
  setRoundNum(0);
1331
1681
  setConsensusReached(false);
1332
1682
  setDiscussionRound(0);
@@ -1343,28 +1693,40 @@ function App({
1343
1693
  }
1344
1694
  return;
1345
1695
  }
1696
+ if (value === "/gist") {
1697
+ if (messages.length === 0) {
1698
+ setStatusMessage("Nothing to gist.");
1699
+ return;
1700
+ }
1701
+ try {
1702
+ const md = formatAsMarkdown(messages);
1703
+ const filename = `tagteam-${sessionId.slice(0, 7)}.md`;
1704
+ const url = createGist(md, filename);
1705
+ setStatusMessage(`Gist created: ${url}`);
1706
+ } catch (e) {
1707
+ setStatusMessage(e.message || "Failed to create gist.");
1708
+ }
1709
+ return;
1710
+ }
1346
1711
  setConsensusReached(false);
1347
1712
  setState("running");
1348
1713
  runRound(value);
1349
1714
  };
1715
+ const staticItems = useMemo(() => [
1716
+ { id: `header-${sessionId}`, role: "__header", content: "", round: -1 },
1717
+ ...messages.slice(0, committedCount)
1718
+ ], [sessionId, messages, committedCount]);
1350
1719
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1351
- /* @__PURE__ */ jsx2(Header, { sessionId }),
1720
+ /* @__PURE__ */ jsx2(Static, { items: staticItems, children: (item) => {
1721
+ if (item.role === "__header") return /* @__PURE__ */ jsx2(Header, { sessionId }, item.id);
1722
+ if (item.role === "user") return /* @__PURE__ */ jsx2(UserMessage, { content: item.content }, item.id);
1723
+ if (isValidAgentName(item.role)) return /* @__PURE__ */ jsx2(AgentResponseBlock, { agent: item.role, content: item.content, error: item.error }, item.id);
1724
+ return /* @__PURE__ */ jsx2(Box2, {}, item.id);
1725
+ } }),
1352
1726
  messages.length === 0 && state === "input" && /* @__PURE__ */ jsx2(QuickHelp, {}),
1353
- messages.map((msg, i) => {
1354
- if (msg.role === "user") {
1355
- return /* @__PURE__ */ jsx2(UserMessage, { content: msg.content }, i);
1356
- }
1357
- if (isValidAgentName(msg.role)) {
1358
- return /* @__PURE__ */ jsx2(
1359
- AgentResponseBlock,
1360
- {
1361
- agent: msg.role,
1362
- content: msg.content,
1363
- error: msg.error
1364
- },
1365
- i
1366
- );
1367
- }
1727
+ messages.slice(committedCount).map((msg) => {
1728
+ if (msg.role === "user") return /* @__PURE__ */ jsx2(UserMessage, { content: msg.content }, msg.id);
1729
+ if (isValidAgentName(msg.role)) return /* @__PURE__ */ jsx2(AgentResponseBlock, { agent: msg.role, content: msg.content, error: msg.error }, msg.id);
1368
1730
  return null;
1369
1731
  }),
1370
1732
  discussionRound > 0 && thinkingAgents.length > 0 && /* @__PURE__ */ jsx2(DiscussionStatus, { round: discussionRound, maxRounds: config.discussion.max_rounds }),
@@ -1410,6 +1772,7 @@ function showTranscriptMarkdown(sessionId) {
1410
1772
  function showTranscript(sessionId) {
1411
1773
  const dbMessages = getMessages(sessionId);
1412
1774
  const messages = dbMessages.map((m) => ({
1775
+ id: `${m.round}-${m.role}`,
1413
1776
  role: m.role,
1414
1777
  content: m.content,
1415
1778
  round: m.round
@@ -1417,12 +1780,12 @@ function showTranscript(sessionId) {
1417
1780
  const { unmount } = render2(
1418
1781
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1419
1782
  /* @__PURE__ */ jsx2(Header, { sessionId }),
1420
- messages.map((msg, i) => {
1783
+ messages.map((msg) => {
1421
1784
  if (msg.role === "user") {
1422
- return /* @__PURE__ */ jsx2(UserMessage, { content: msg.content }, i);
1785
+ return /* @__PURE__ */ jsx2(UserMessage, { content: msg.content }, msg.id);
1423
1786
  }
1424
1787
  if (isValidAgentName(msg.role)) {
1425
- return /* @__PURE__ */ jsx2(AgentResponseBlock, { agent: msg.role, content: msg.content }, i);
1788
+ return /* @__PURE__ */ jsx2(AgentResponseBlock, { agent: msg.role, content: msg.content }, msg.id);
1426
1789
  }
1427
1790
  return null;
1428
1791
  }),
@@ -1457,7 +1820,7 @@ process.on("SIGINT", () => {
1457
1820
  });
1458
1821
  function checkCli(name, installUrl) {
1459
1822
  try {
1460
- execSync2(`${name} --version`, { stdio: "ignore" });
1823
+ execSync3(`${name} --version`, { stdio: "ignore" });
1461
1824
  return true;
1462
1825
  } catch {
1463
1826
  console.error(
@@ -1493,7 +1856,7 @@ function resolveAgentModels(pair, opts, config) {
1493
1856
  };
1494
1857
  }
1495
1858
  var program = new Command();
1496
- program.name("tagteam").description("Tag Team - Orchestrate AI agents collaboratively").version("0.3.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) => {
1859
+ 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) => {
1497
1860
  const config = loadConfig();
1498
1861
  const pair = resolveAgentPair(opts, config);
1499
1862
  preflight(pair);
@@ -1533,6 +1896,7 @@ program.command("continue").description("Resume the most recent session").action
1533
1896
  }
1534
1897
  const dbMessages = getMessages(session.id);
1535
1898
  const transcript = dbMessages.map((m) => ({
1899
+ id: `${m.round}-${m.role}`,
1536
1900
  role: m.role,
1537
1901
  content: m.content,
1538
1902
  round: m.round
@@ -1583,6 +1947,7 @@ program.command("resume [id]").description("Resume a session by ID, or pick inte
1583
1947
  const session2 = sessions[num - 1];
1584
1948
  const dbMessages2 = getMessages(session2.id);
1585
1949
  const transcript2 = dbMessages2.map((m) => ({
1950
+ id: `${m.round}-${m.role}`,
1586
1951
  role: m.role,
1587
1952
  content: m.content,
1588
1953
  round: m.round
@@ -1606,6 +1971,7 @@ program.command("resume [id]").description("Resume a session by ID, or pick inte
1606
1971
  }
1607
1972
  const dbMessages = getMessages(session.id);
1608
1973
  const transcript = dbMessages.map((m) => ({
1974
+ id: `${m.round}-${m.role}`,
1609
1975
  role: m.role,
1610
1976
  content: m.content,
1611
1977
  round: m.round