jinzd-ai-cli 0.4.155 → 0.4.156

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/{batch-LS3IJVBK.js → batch-DBOCPVH5.js} +2 -2
  2. package/dist/{chat-index-IF4EINLQ.js → chat-index-2I7ZHRE5.js} +2 -2
  3. package/dist/{chunk-D6GJTJQH.js → chunk-3DCAQKVZ.js} +1 -1
  4. package/dist/{chunk-NFRTSL3N.js → chunk-3SQMKA4I.js} +1 -1
  5. package/dist/{chunk-B3LFGPU2.js → chunk-3WLEDKOW.js} +1 -1
  6. package/dist/{chunk-JXSWY54M.js → chunk-7OCOFVQP.js} +1 -1
  7. package/dist/{chunk-JOJRBV2K.js → chunk-AACNCJMD.js} +1 -1
  8. package/dist/{chunk-CIZQZ7CC.js → chunk-GQ647SF3.js} +2 -2
  9. package/dist/chunk-NZ4X6GUC.js +230 -0
  10. package/dist/{chunk-O6MLS5QO.js → chunk-OJL3PY36.js} +0 -226
  11. package/dist/{hub-ZILVZWI2.js → chunk-Q3ZUDA6S.js} +6 -249
  12. package/dist/{persist-3EBOLHFZ.js → chunk-RUJQ5OUB.js} +1 -2
  13. package/dist/{chunk-IBBYW6PM.js → chunk-UAPNBQLU.js} +1 -1
  14. package/dist/{chunk-E5ICQT3P.js → chunk-Z2UJDFJK.js} +4 -4
  15. package/dist/{ci-34ZQH43L.js → ci-AWA6KC3X.js} +3 -3
  16. package/dist/{constants-DQ5VJOGS.js → constants-QDTWBWWC.js} +1 -1
  17. package/dist/{doctor-cli-TSCI4ORL.js → doctor-cli-4MTG6SFH.js} +6 -6
  18. package/dist/electron-server.js +740 -44
  19. package/dist/hub-QRDXG527.js +260 -0
  20. package/dist/{hub-server-OH7AYQIW.js → hub-server-GSTG5MNE.js} +4 -2
  21. package/dist/index.js +40 -40
  22. package/dist/persist-UI6WRBGB.js +12 -0
  23. package/dist/{run-tests-5KWCHBQS.js → run-tests-CNURD2ST.js} +2 -2
  24. package/dist/{run-tests-5CJRMOMI.js → run-tests-NPWSCWP5.js} +1 -1
  25. package/dist/{server-DVIP7NLW.js → server-HFG2SM3Y.js} +6 -6
  26. package/dist/{server-35OQV62B.js → server-RRUCZMMM.js} +124 -32
  27. package/dist/{task-orchestrator-AXSS7ROD.js → task-orchestrator-GF6ZMNUK.js} +6 -6
  28. package/dist/web/client/app.js +138 -0
  29. package/dist/web/client/index.html +28 -0
  30. package/package.json +1 -1
  31. package/dist/{chunk-U5MY24UZ.js → chunk-MM3F43H6.js} +3 -3
@@ -36,7 +36,7 @@ import {
36
36
  VERSION,
37
37
  buildUserIdentityPrompt,
38
38
  runTestsTool
39
- } from "./chunk-JXSWY54M.js";
39
+ } from "./chunk-7OCOFVQP.js";
40
40
  import {
41
41
  hasSemanticIndex,
42
42
  semanticSearch
@@ -57,7 +57,7 @@ import "./chunk-3RG5ZIWI.js";
57
57
  import express from "express";
58
58
  import { createServer } from "http";
59
59
  import { WebSocketServer } from "ws";
60
- import { join as join16, dirname as dirname6, resolve as resolve6, relative as relative3, sep as sep3 } from "path";
60
+ import { join as join17, dirname as dirname6, resolve as resolve6, relative as relative3, sep as sep3 } from "path";
61
61
  import { existsSync as existsSync23, readFileSync as readFileSync16, readdirSync as readdirSync11, statSync as statSync9, realpathSync } from "fs";
62
62
  import { networkInterfaces } from "os";
63
63
 
@@ -10662,7 +10662,7 @@ function autoTrimSessionIfNeeded(session, sizeLimit = SESSION_SIZE_LIMIT) {
10662
10662
 
10663
10663
  // src/web/session-handler.ts
10664
10664
  import { existsSync as existsSync21, readFileSync as readFileSync14, appendFileSync as appendFileSync3, writeFileSync as writeFileSync9, mkdirSync as mkdirSync10, readdirSync as readdirSync9, statSync as statSync8, createWriteStream, unlinkSync as unlinkSync4 } from "fs";
10665
- import { join as join14, resolve as resolve5, dirname as dirname5 } from "path";
10665
+ import { join as join15, resolve as resolve5, dirname as dirname5 } from "path";
10666
10666
  import { execSync as execSync3 } from "child_process";
10667
10667
 
10668
10668
  // src/tools/git-context.ts
@@ -10755,6 +10755,619 @@ function formatGitContextForPrompt(ctx) {
10755
10755
  return lines.join("\n");
10756
10756
  }
10757
10757
 
10758
+ // src/hub/convergence.ts
10759
+ function convergenceThreshold(total) {
10760
+ if (total <= 0) return Infinity;
10761
+ return Math.ceil(total * 2 / 3);
10762
+ }
10763
+ function isConverged(convergedCount, total) {
10764
+ if (convergedCount <= 0) return false;
10765
+ return convergedCount >= convergenceThreshold(total);
10766
+ }
10767
+ var CONVERGED_MARKER = /\[CONVERGED\]/i;
10768
+ function hasConvergedMarker(content) {
10769
+ return CONVERGED_MARKER.test(content);
10770
+ }
10771
+ function stripConvergedMarker(content) {
10772
+ return content.replace(/\s*\[CONVERGED\]\s*/gi, " ").trim();
10773
+ }
10774
+
10775
+ // src/hub/agent.ts
10776
+ var PASS_MARKER = "[PASS]";
10777
+ function parseTurn(raw) {
10778
+ const trimmed = raw.trim();
10779
+ const upper = trimmed.toUpperCase();
10780
+ if (upper.startsWith(PASS_MARKER) || upper === PASS_MARKER) {
10781
+ return { content: "", passed: true, converged: false };
10782
+ }
10783
+ const converged = hasConvergedMarker(trimmed);
10784
+ return { content: converged ? stripConvergedMarker(trimmed) : trimmed, passed: false, converged };
10785
+ }
10786
+ var HubAgent = class {
10787
+ role;
10788
+ providers;
10789
+ defaultProvider;
10790
+ defaultModel;
10791
+ /** External context documents injected into system prompt */
10792
+ context;
10793
+ constructor(role, providers, defaultProvider, defaultModel, context) {
10794
+ this.role = role;
10795
+ this.providers = providers;
10796
+ this.defaultProvider = defaultProvider;
10797
+ this.defaultModel = defaultModel;
10798
+ this.context = context;
10799
+ }
10800
+ get providerId() {
10801
+ return this.role.provider ?? this.defaultProvider;
10802
+ }
10803
+ get modelId() {
10804
+ return this.role.model ?? this.defaultModel;
10805
+ }
10806
+ /**
10807
+ * Generate this agent's response given the full discussion history.
10808
+ *
10809
+ * Returns a DiscussionMessage, with `passed: true` if the agent outputs [PASS].
10810
+ */
10811
+ async speak(topic, history, round, maxRounds, opts) {
10812
+ const provider = this.providers.get(this.providerId);
10813
+ if (!provider) {
10814
+ throw new Error(`Provider "${this.providerId}" not available for agent "${this.role.id}"`);
10815
+ }
10816
+ const systemPrompt = this.buildSystemPrompt(topic, round, maxRounds, opts?.voteConverge);
10817
+ const messages = this.buildMessages(history);
10818
+ const response = await provider.chat({
10819
+ messages,
10820
+ model: this.modelId,
10821
+ systemPrompt,
10822
+ stream: false,
10823
+ temperature: 0.7,
10824
+ maxTokens: 4096
10825
+ });
10826
+ const { content, passed, converged } = parseTurn(response.content);
10827
+ return {
10828
+ speaker: this.role.id,
10829
+ speakerName: this.role.name,
10830
+ content,
10831
+ timestamp: /* @__PURE__ */ new Date(),
10832
+ passed,
10833
+ converged
10834
+ };
10835
+ }
10836
+ /**
10837
+ * Streaming version of speak() — yields tokens as they arrive.
10838
+ * Falls back to non-streaming speak() if the provider doesn't support chatStream.
10839
+ */
10840
+ async speakStream(topic, history, round, maxRounds, onToken, opts) {
10841
+ const provider = this.providers.get(this.providerId);
10842
+ if (!provider) {
10843
+ throw new Error(`Provider "${this.providerId}" not available for agent "${this.role.id}"`);
10844
+ }
10845
+ if (!provider.chatStream) {
10846
+ return this.speak(topic, history, round, maxRounds, opts);
10847
+ }
10848
+ const systemPrompt = this.buildSystemPrompt(topic, round, maxRounds, opts?.voteConverge);
10849
+ const messages = this.buildMessages(history);
10850
+ let raw = "";
10851
+ const stream = provider.chatStream({
10852
+ messages,
10853
+ model: this.modelId,
10854
+ systemPrompt,
10855
+ stream: true,
10856
+ temperature: 0.7,
10857
+ maxTokens: 4096
10858
+ });
10859
+ for await (const chunk of stream) {
10860
+ if (chunk.delta) {
10861
+ raw += chunk.delta;
10862
+ onToken?.(chunk.delta);
10863
+ }
10864
+ }
10865
+ const { content, passed, converged } = parseTurn(raw);
10866
+ return {
10867
+ speaker: this.role.id,
10868
+ speakerName: this.role.name,
10869
+ content,
10870
+ timestamp: /* @__PURE__ */ new Date(),
10871
+ passed,
10872
+ converged
10873
+ };
10874
+ }
10875
+ /**
10876
+ * Generate a summary of the entire discussion from this agent's perspective.
10877
+ */
10878
+ async summarize(topic, history) {
10879
+ const provider = this.providers.get(this.providerId);
10880
+ if (!provider) {
10881
+ throw new Error(`Provider "${this.providerId}" not available`);
10882
+ }
10883
+ const messages = [
10884
+ {
10885
+ role: "user",
10886
+ content: this.buildSummaryPrompt(topic, history),
10887
+ timestamp: /* @__PURE__ */ new Date()
10888
+ }
10889
+ ];
10890
+ const response = await provider.chat({
10891
+ messages,
10892
+ model: this.modelId,
10893
+ stream: false,
10894
+ temperature: 0.3,
10895
+ maxTokens: 4096
10896
+ });
10897
+ return response.content.trim();
10898
+ }
10899
+ // ── Private ──────────────────────────────────────────────────────
10900
+ buildSystemPrompt(topic, round, maxRounds, voteConverge) {
10901
+ const contextSection = this.context ? `
10902
+
10903
+ ## Reference Documents
10904
+ ${this.context}` : "";
10905
+ const convergeRule = voteConverge ? `
10906
+ - If you believe the group has reached a sufficient conclusion and further rounds would add little, append the marker [CONVERGED] at the very end of your message (you may still make your substantive point above it). When a 2/3 majority converges, the discussion ends.` : "";
10907
+ return `# Multi-Agent Discussion \u2014 Role: ${this.role.name}
10908
+
10909
+ ${this.role.persona}
10910
+
10911
+ ## Discussion Rules
10912
+ - You are participating in a multi-agent discussion about the topic below.
10913
+ - You can see what other participants have said. Build on their ideas, challenge them, or add your own perspective.
10914
+ - Stay in character as ${this.role.name}. Respond from your role's expertise and viewpoint.
10915
+ - Keep responses concise and focused (2-6 paragraphs). Do not repeat what others have already said.
10916
+ - If you have nothing meaningful to add (others have covered your points), respond with exactly: [PASS]${convergeRule}
10917
+ - This is round ${round} of ${maxRounds}. ${round >= maxRounds - 1 ? "This is one of the final rounds \u2014 try to converge on conclusions." : ""}
10918
+ - Use the language that the topic is written in (if the topic is in Chinese, respond in Chinese).
10919
+
10920
+ ## Topic
10921
+ ${topic}${contextSection}`;
10922
+ }
10923
+ buildMessages(history) {
10924
+ const messages = [];
10925
+ for (const msg of history) {
10926
+ if (msg.passed) continue;
10927
+ if (msg.speaker === this.role.id) {
10928
+ messages.push({
10929
+ role: "assistant",
10930
+ content: msg.content,
10931
+ timestamp: msg.timestamp
10932
+ });
10933
+ } else {
10934
+ const prefix = `[${msg.speakerName} (${msg.speaker})]:`;
10935
+ messages.push({
10936
+ role: "user",
10937
+ content: `${prefix}
10938
+ ${msg.content}`,
10939
+ timestamp: msg.timestamp
10940
+ });
10941
+ }
10942
+ }
10943
+ if (messages.length === 0) {
10944
+ messages.push({
10945
+ role: "user",
10946
+ content: "Please share your initial thoughts on the topic described in the system prompt. You are the first to speak.",
10947
+ timestamp: /* @__PURE__ */ new Date()
10948
+ });
10949
+ } else {
10950
+ messages.push({
10951
+ role: "user",
10952
+ content: "It is now your turn to respond. Consider what the other participants have said and share your perspective. If you have nothing to add, respond with [PASS].",
10953
+ timestamp: /* @__PURE__ */ new Date()
10954
+ });
10955
+ }
10956
+ return messages;
10957
+ }
10958
+ buildSummaryPrompt(topic, history) {
10959
+ const transcript = history.filter((m) => !m.passed).map((m) => `**${m.speakerName}** (${m.speaker}):
10960
+ ${m.content}`).join("\n\n---\n\n");
10961
+ return `Please synthesize the following multi-agent discussion into a clear, structured summary.
10962
+
10963
+ ## Discussion Topic
10964
+ ${topic}
10965
+
10966
+ ## Full Transcript
10967
+ ${transcript}
10968
+
10969
+ ## Instructions
10970
+ Produce a structured, decision-oriented synthesis with these exact sections:
10971
+
10972
+ 1. **Decision / Recommendation** \u2014 the single clearest recommendation the discussion points to. If the group genuinely did not converge, say so and give the leading option plus what would settle it.
10973
+ 2. **Key Points of Agreement** \u2014 what participants agreed on.
10974
+ 3. **Points of Debate** \u2014 where they disagreed, with the competing perspectives.
10975
+ 4. **Action Items** \u2014 concrete next steps as a checklist. For each, suggest which role/expertise should own it, e.g. "- [ ] (\u67B6\u6784\u5E08) \u8BC4\u4F30\u670D\u52A1\u62C6\u5206\u8FB9\u754C".
10976
+ 5. **Risks & Open Questions** \u2014 unresolved issues and what to watch.
10977
+
10978
+ Keep it tight (within ~600 words). Use the same language as the discussion.`;
10979
+ }
10980
+ };
10981
+
10982
+ // src/hub/discuss.ts
10983
+ var DiscussionOrchestrator = class {
10984
+ agents = [];
10985
+ state;
10986
+ aborted = false;
10987
+ humanSteer;
10988
+ voteConverge;
10989
+ /** Callback for rendering events */
10990
+ onEvent;
10991
+ /**
10992
+ * P2: human-in-the-loop. When `config.humanSteer` is on, the orchestrator
10993
+ * awaits this between rounds so a person can inject guidance or stop. The
10994
+ * CLI layer supplies the actual prompt; the orchestrator stays UI-agnostic.
10995
+ */
10996
+ onRoundReview;
10997
+ constructor(config, providers) {
10998
+ this.agents = config.roles.map(
10999
+ (role) => new HubAgent(role, providers, config.defaultProvider, config.defaultModel, config.context)
11000
+ );
11001
+ this.humanSteer = config.humanSteer ?? false;
11002
+ this.voteConverge = config.voteConverge ?? false;
11003
+ this.state = {
11004
+ topic: "",
11005
+ messages: [],
11006
+ round: 0,
11007
+ maxRounds: config.maxRounds ?? 10,
11008
+ finished: false
11009
+ };
11010
+ }
11011
+ /** Get all agents */
11012
+ getAgents() {
11013
+ return this.agents;
11014
+ }
11015
+ /** Current discussion state (used to persist even after an error). */
11016
+ getState() {
11017
+ return this.state;
11018
+ }
11019
+ /** Signal the orchestrator to stop after current turn */
11020
+ abort() {
11021
+ this.aborted = true;
11022
+ }
11023
+ /**
11024
+ * Run the full discussion on a given topic.
11025
+ * Returns the final DiscussionState including summary.
11026
+ */
11027
+ async run(topic) {
11028
+ this.state.topic = topic;
11029
+ this.state.round = 0;
11030
+ this.state.finished = false;
11031
+ this.aborted = false;
11032
+ try {
11033
+ for (let round = 1; round <= this.state.maxRounds; round++) {
11034
+ if (this.aborted) {
11035
+ this.emit({ type: "discussion_end", reason: "user_interrupt" });
11036
+ break;
11037
+ }
11038
+ this.state.round = round;
11039
+ this.emit({ type: "round_start", round, maxRounds: this.state.maxRounds });
11040
+ let allPassed = true;
11041
+ let convergedCount = 0;
11042
+ for (const agent of this.agents) {
11043
+ if (this.aborted) break;
11044
+ this.emit({ type: "agent_speaking", roleId: agent.role.id, roleName: agent.role.name });
11045
+ try {
11046
+ const message = await agent.speakStream(
11047
+ topic,
11048
+ this.state.messages,
11049
+ round,
11050
+ this.state.maxRounds,
11051
+ (token) => this.emit({ type: "agent_token", roleId: agent.role.id, token }),
11052
+ { voteConverge: this.voteConverge }
11053
+ );
11054
+ this.state.messages.push(message);
11055
+ if (message.converged) convergedCount++;
11056
+ if (message.passed) {
11057
+ this.emit({ type: "agent_passed", roleId: agent.role.id });
11058
+ this.emit({ type: "agent_spoke", roleId: agent.role.id, message });
11059
+ } else {
11060
+ allPassed = false;
11061
+ this.emit({ type: "agent_spoke", roleId: agent.role.id, message });
11062
+ }
11063
+ } catch (err) {
11064
+ const errMsg = err instanceof Error ? err.message : String(err);
11065
+ this.emit({ type: "error", roleId: agent.role.id, message: errMsg });
11066
+ this.state.messages.push({
11067
+ speaker: "system",
11068
+ speakerName: "System",
11069
+ content: `[${agent.role.name} encountered an error: ${errMsg}]`,
11070
+ timestamp: /* @__PURE__ */ new Date()
11071
+ });
11072
+ }
11073
+ }
11074
+ this.emit({ type: "round_end", round });
11075
+ if (allPassed && round > 1) {
11076
+ this.emit({ type: "discussion_end", reason: "consensus" });
11077
+ break;
11078
+ }
11079
+ if (this.voteConverge && convergedCount > 0) {
11080
+ this.emit({ type: "converge_vote", converged: convergedCount, total: this.agents.length });
11081
+ if (isConverged(convergedCount, this.agents.length)) {
11082
+ this.emit({ type: "discussion_end", reason: "vote_converged" });
11083
+ break;
11084
+ }
11085
+ }
11086
+ if (round === this.state.maxRounds) {
11087
+ this.emit({ type: "discussion_end", reason: "max_rounds", maxRounds: this.state.maxRounds });
11088
+ break;
11089
+ }
11090
+ if (this.humanSteer && this.onRoundReview && !this.aborted) {
11091
+ const steer = await this.onRoundReview({ round, maxRounds: this.state.maxRounds, state: this.state });
11092
+ if (steer.action === "stop") {
11093
+ this.emit({ type: "discussion_end", reason: "human_stop" });
11094
+ break;
11095
+ }
11096
+ const guidance = steer.message?.trim();
11097
+ if (guidance) {
11098
+ this.state.messages.push({
11099
+ speaker: "human",
11100
+ speakerName: "\u4E3B\u6301\u4EBA (You)",
11101
+ content: guidance,
11102
+ timestamp: /* @__PURE__ */ new Date()
11103
+ });
11104
+ this.emit({ type: "human_steer", message: guidance });
11105
+ }
11106
+ }
11107
+ }
11108
+ } catch (err) {
11109
+ const errMsg = err instanceof Error ? err.message : String(err);
11110
+ this.emit({ type: "error", message: errMsg });
11111
+ }
11112
+ await this.generateSummary();
11113
+ this.state.finished = true;
11114
+ return this.state;
11115
+ }
11116
+ // ── Private ──────────────────────────────────────────────────────
11117
+ async generateSummary() {
11118
+ if (this.state.messages.filter((m) => !m.passed && m.speaker !== "system").length === 0) {
11119
+ this.state.summary = "(No substantive discussion occurred.)";
11120
+ this.emit({ type: "summary", content: this.state.summary });
11121
+ return;
11122
+ }
11123
+ const summarizer = this.agents[0];
11124
+ if (!summarizer) {
11125
+ this.state.summary = "(No agents available to summarize.)";
11126
+ this.emit({ type: "summary", content: this.state.summary });
11127
+ return;
11128
+ }
11129
+ try {
11130
+ this.state.summary = await summarizer.summarize(this.state.topic, this.state.messages);
11131
+ this.emit({ type: "summary", content: this.state.summary });
11132
+ } catch (err) {
11133
+ const errMsg = err instanceof Error ? err.message : String(err);
11134
+ this.state.summary = `(Summary generation failed: ${errMsg})`;
11135
+ this.emit({ type: "summary", content: this.state.summary });
11136
+ }
11137
+ }
11138
+ emit(event) {
11139
+ this.onEvent?.(event);
11140
+ }
11141
+ };
11142
+
11143
+ // src/hub/presets.ts
11144
+ var PRESETS = [
11145
+ {
11146
+ id: "tech-review",
11147
+ name: "Tech Review Panel",
11148
+ description: "\u67B6\u6784\u5E08 + \u5F00\u53D1\u8005 + \u5B89\u5168\u4E13\u5BB6 \u8BA8\u8BBA\u6280\u672F\u65B9\u6848",
11149
+ roles: [
11150
+ {
11151
+ id: "architect",
11152
+ name: "\u67B6\u6784\u5E08",
11153
+ persona: `\u4F60\u662F\u4E00\u4F4D\u8D44\u6DF1\u8F6F\u4EF6\u67B6\u6784\u5E08\uFF0C\u6709 15 \u5E74\u4EE5\u4E0A\u7684\u7CFB\u7EDF\u8BBE\u8BA1\u7ECF\u9A8C\u3002
11154
+ \u4F60\u7684\u4E13\u957F\uFF1A\u7CFB\u7EDF\u67B6\u6784\u3001\u53EF\u6269\u5C55\u6027\u3001\u6280\u672F\u9009\u578B\u3001\u8BBE\u8BA1\u6A21\u5F0F\u3002
11155
+ \u4F60\u5173\u6CE8\u7684\u7EF4\u5EA6\uFF1A\u7CFB\u7EDF\u6574\u4F53\u7ED3\u6784\u3001\u6A21\u5757\u89E3\u8026\u3001\u957F\u671F\u53EF\u7EF4\u62A4\u6027\u3001\u6280\u672F\u503A\u52A1\u3002
11156
+ \u98CE\u683C\uFF1A\u5168\u5C40\u89C6\u89D2\uFF0C\u5584\u4E8E\u6743\u8861 trade-off\uFF0C\u7528\u56FE\u8868\u548C\u7C7B\u6BD4\u89E3\u91CA\u590D\u6742\u6982\u5FF5\u3002`,
11157
+ color: "cyan"
11158
+ },
11159
+ {
11160
+ id: "developer",
11161
+ name: "\u5168\u6808\u5F00\u53D1\u8005",
11162
+ persona: `\u4F60\u662F\u4E00\u4F4D\u7ECF\u9A8C\u4E30\u5BCC\u7684\u5168\u6808\u5F00\u53D1\u8005\uFF0C\u7CBE\u901A\u524D\u7AEF\u548C\u540E\u7AEF\u6280\u672F\u6808\u3002
11163
+ \u4F60\u7684\u4E13\u957F\uFF1A\u4EE3\u7801\u5B9E\u73B0\u3001\u6027\u80FD\u4F18\u5316\u3001API \u8BBE\u8BA1\u3001\u5F00\u53D1\u6548\u7387\u3002
11164
+ \u4F60\u5173\u6CE8\u7684\u7EF4\u5EA6\uFF1A\u4EE3\u7801\u53EF\u8BFB\u6027\u3001\u5B9E\u73B0\u590D\u6742\u5EA6\u3001DX(\u5F00\u53D1\u8005\u4F53\u9A8C)\u3001\u5177\u4F53\u6280\u672F\u7EC6\u8282\u3002
11165
+ \u98CE\u683C\uFF1A\u52A1\u5B9E\u3001\u6CE8\u91CD\u7EC6\u8282\uFF0C\u559C\u6B22\u7ED9\u51FA\u5177\u4F53\u7684\u4EE3\u7801\u793A\u4F8B\u548C\u5B9E\u73B0\u5EFA\u8BAE\u3002`,
11166
+ color: "green"
11167
+ },
11168
+ {
11169
+ id: "security",
11170
+ name: "\u5B89\u5168\u4E13\u5BB6",
11171
+ persona: `\u4F60\u662F\u4E00\u4F4D\u7F51\u7EDC\u5B89\u5168\u4E13\u5BB6\uFF0C\u4E13\u6CE8\u4E8E\u5E94\u7528\u5B89\u5168\u548C\u5B89\u5168\u67B6\u6784\u8BBE\u8BA1\u3002
11172
+ \u4F60\u7684\u4E13\u957F\uFF1A\u5A01\u80C1\u5EFA\u6A21\u3001\u6F0F\u6D1E\u5206\u6790\u3001\u5B89\u5168\u6700\u4F73\u5B9E\u8DF5\u3001\u5408\u89C4\u8981\u6C42\u3002
11173
+ \u4F60\u5173\u6CE8\u7684\u7EF4\u5EA6\uFF1A\u653B\u51FB\u9762\u3001\u6570\u636E\u4FDD\u62A4\u3001\u8BA4\u8BC1\u6388\u6743\u3001\u5B89\u5168\u7F16\u7801\u5B9E\u8DF5\u3002
11174
+ \u98CE\u683C\uFF1A\u8C28\u614E\u3001\u4E25\u8C28\uFF0C\u5584\u4E8E\u53D1\u73B0\u6F5C\u5728\u98CE\u9669\uFF0C\u540C\u65F6\u4E5F\u4F1A\u63D0\u51FA\u5B9E\u7528\u7684\u5B89\u5168\u65B9\u6848\u800C\u4E0D\u662F\u4E00\u5473\u5426\u5B9A\u3002`,
11175
+ color: "red"
11176
+ }
11177
+ ]
11178
+ },
11179
+ {
11180
+ id: "brainstorm",
11181
+ name: "Brainstorm Team",
11182
+ description: "\u521B\u610F\u8005 + \u5206\u6790\u5E08 + \u6267\u884C\u8005 \u5934\u8111\u98CE\u66B4",
11183
+ roles: [
11184
+ {
11185
+ id: "creative",
11186
+ name: "\u521B\u610F\u8005",
11187
+ persona: `\u4F60\u662F\u4E00\u4F4D\u5145\u6EE1\u521B\u610F\u7684\u4EA7\u54C1\u601D\u8003\u8005\uFF0C\u5584\u4E8E\u8DF3\u51FA\u6846\u67B6\u601D\u8003\u3002
11188
+ \u4F60\u7684\u4E13\u957F\uFF1A\u521B\u65B0\u601D\u7EF4\u3001\u7528\u6237\u4F53\u9A8C\u3001\u4EA7\u54C1\u613F\u666F\u3001\u8BBE\u8BA1\u601D\u7EF4\u3002
11189
+ \u4F60\u5173\u6CE8\u7684\u7EF4\u5EA6\uFF1A\u7528\u6237\u9700\u6C42\u3001\u521B\u65B0\u6027\u3001\u5DEE\u5F02\u5316\u3001\u60C5\u611F\u4EF7\u503C\u3002
11190
+ \u98CE\u683C\uFF1A\u53D1\u6563\u6027\u601D\u7EF4\uFF0C\u5927\u80C6\u63D0\u51FA\u65B0\u60F3\u6CD5\uFF0C\u4E0D\u6015\u5929\u9A6C\u884C\u7A7A\u3002\u5584\u4E8E\u7528\u6545\u4E8B\u548C\u573A\u666F\u6765\u63CF\u8FF0\u6784\u60F3\u3002`,
11191
+ color: "magenta"
11192
+ },
11193
+ {
11194
+ id: "analyst",
11195
+ name: "\u5206\u6790\u5E08",
11196
+ persona: `\u4F60\u662F\u4E00\u4F4D\u7406\u6027\u7684\u6570\u636E\u5206\u6790\u5E08\uFF0C\u5584\u4E8E\u7528\u6570\u636E\u548C\u903B\u8F91\u8BC4\u4F30\u65B9\u6848\u3002
11197
+ \u4F60\u7684\u4E13\u957F\uFF1A\u6570\u636E\u5206\u6790\u3001\u5E02\u573A\u7814\u7A76\u3001\u53EF\u884C\u6027\u8BC4\u4F30\u3001ROI \u5206\u6790\u3002
11198
+ \u4F60\u5173\u6CE8\u7684\u7EF4\u5EA6\uFF1A\u6570\u636E\u652F\u6491\u3001\u6210\u672C\u6536\u76CA\u3001\u98CE\u9669\u8BC4\u4F30\u3001\u5E02\u573A\u53EF\u884C\u6027\u3002
11199
+ \u98CE\u683C\uFF1A\u4E25\u8C28\u5BA2\u89C2\uFF0C\u5584\u4E8E\u63D0\u51FA\u5173\u952E\u95EE\u9898\uFF0C\u7528\u6570\u636E\u8BF4\u8BDD\u3002\u4E0D\u8F7B\u6613\u5426\u5B9A\u4F46\u4F1A\u6307\u51FA\u903B\u8F91\u6F0F\u6D1E\u3002`,
11200
+ color: "yellow"
11201
+ },
11202
+ {
11203
+ id: "executor",
11204
+ name: "\u6267\u884C\u8005",
11205
+ persona: `\u4F60\u662F\u4E00\u4F4D\u9AD8\u6548\u7684\u9879\u76EE\u6267\u884C\u8005\uFF0C\u5584\u4E8E\u5C06\u60F3\u6CD5\u8F6C\u5316\u4E3A\u53EF\u6267\u884C\u7684\u8BA1\u5212\u3002
11206
+ \u4F60\u7684\u4E13\u957F\uFF1A\u9879\u76EE\u7BA1\u7406\u3001\u8D44\u6E90\u89C4\u5212\u3001\u98CE\u9669\u7BA1\u7406\u3001\u654F\u6377\u65B9\u6CD5\u3002
11207
+ \u4F60\u5173\u6CE8\u7684\u7EF4\u5EA6\uFF1A\u53EF\u6267\u884C\u6027\u3001\u65F6\u95F4\u7EBF\u3001\u8D44\u6E90\u9700\u6C42\u3001MVP \u7B56\u7565\u3002
11208
+ \u98CE\u683C\uFF1A\u5B9E\u9645\u3001\u9AD8\u6548\uFF0C\u5584\u4E8E\u62C6\u89E3\u4EFB\u52A1\uFF0C\u63D0\u51FA\u5177\u4F53\u7684\u5B9E\u65BD\u6B65\u9AA4\u548C\u91CC\u7A0B\u7891\u3002`,
11209
+ color: "green"
11210
+ }
11211
+ ]
11212
+ },
11213
+ {
11214
+ id: "code-review",
11215
+ name: "Code Review Panel",
11216
+ description: "\u4EE3\u7801\u8D28\u91CF + \u6027\u80FD + \u53EF\u6D4B\u8BD5\u6027 \u591A\u89D2\u5EA6\u5BA1\u67E5",
11217
+ roles: [
11218
+ {
11219
+ id: "quality",
11220
+ name: "\u4EE3\u7801\u8D28\u91CF\u5BA1\u67E5\u5458",
11221
+ persona: `\u4F60\u662F\u4E00\u4F4D\u4EE3\u7801\u8D28\u91CF\u4E13\u5BB6\uFF0C\u4E13\u6CE8\u4E8E\u53EF\u8BFB\u6027\u3001\u53EF\u7EF4\u62A4\u6027\u548C\u6700\u4F73\u5B9E\u8DF5\u3002
11222
+ \u4F60\u7684\u4E13\u957F\uFF1AClean Code\u3001\u8BBE\u8BA1\u6A21\u5F0F\u3001SOLID \u539F\u5219\u3001\u91CD\u6784\u6280\u672F\u3002
11223
+ \u4F60\u5173\u6CE8\u7684\u7EF4\u5EA6\uFF1A\u547D\u540D\u89C4\u8303\u3001\u51FD\u6570\u957F\u5EA6\u3001\u8026\u5408\u5EA6\u3001\u4EE3\u7801\u590D\u6742\u5EA6\u3001\u6CE8\u91CA\u8D28\u91CF\u3002
11224
+ \u98CE\u683C\uFF1A\u5EFA\u8BBE\u6027\u6279\u8BC4\uFF0C\u603B\u662F\u7ED9\u51FA\u6539\u8FDB\u5EFA\u8BAE\u800C\u4E0D\u4EC5\u662F\u6307\u51FA\u95EE\u9898\u3002`,
11225
+ color: "cyan"
11226
+ },
11227
+ {
11228
+ id: "perf",
11229
+ name: "\u6027\u80FD\u5DE5\u7A0B\u5E08",
11230
+ persona: `\u4F60\u662F\u4E00\u4F4D\u6027\u80FD\u4F18\u5316\u4E13\u5BB6\uFF0C\u5584\u4E8E\u53D1\u73B0\u548C\u89E3\u51B3\u6027\u80FD\u74F6\u9888\u3002
11231
+ \u4F60\u7684\u4E13\u957F\uFF1A\u7B97\u6CD5\u590D\u6742\u5EA6\u3001\u5185\u5B58\u7BA1\u7406\u3001\u5E76\u53D1\u4F18\u5316\u3001\u7F13\u5B58\u7B56\u7565\u3002
11232
+ \u4F60\u5173\u6CE8\u7684\u7EF4\u5EA6\uFF1A\u65F6\u95F4\u590D\u6742\u5EA6\u3001\u7A7A\u95F4\u590D\u6742\u5EA6\u3001IO \u74F6\u9888\u3001\u70ED\u70B9\u8DEF\u5F84\u3002
11233
+ \u98CE\u683C\uFF1A\u6570\u636E\u9A71\u52A8\uFF0C\u559C\u6B22\u7528\u57FA\u51C6\u6D4B\u8BD5\u548C\u5927 O \u5206\u6790\u6765\u8BBA\u8BC1\u89C2\u70B9\u3002`,
11234
+ color: "yellow"
11235
+ },
11236
+ {
11237
+ id: "testing",
11238
+ name: "\u6D4B\u8BD5\u67B6\u6784\u5E08",
11239
+ persona: `\u4F60\u662F\u4E00\u4F4D\u6D4B\u8BD5\u67B6\u6784\u5E08\uFF0C\u4E13\u6CE8\u4E8E\u4EE3\u7801\u7684\u53EF\u6D4B\u8BD5\u6027\u548C\u6D4B\u8BD5\u7B56\u7565\u3002
11240
+ \u4F60\u7684\u4E13\u957F\uFF1A\u5355\u5143\u6D4B\u8BD5\u3001\u96C6\u6210\u6D4B\u8BD5\u3001Mock \u7B56\u7565\u3001TDD\u3001\u6D4B\u8BD5\u8986\u76D6\u7387\u3002
11241
+ \u4F60\u5173\u6CE8\u7684\u7EF4\u5EA6\uFF1A\u53EF\u6D4B\u8BD5\u6027\u3001\u8FB9\u754C\u6761\u4EF6\u3001\u9519\u8BEF\u5904\u7406\u8DEF\u5F84\u3001\u6D4B\u8BD5\u91D1\u5B57\u5854\u3002
11242
+ \u98CE\u683C\uFF1A\u5173\u6CE8\u8FB9\u754C\u60C5\u51B5\u548C\u5F02\u5E38\u8DEF\u5F84\uFF0C\u5584\u4E8E\u63D0\u51FA"\u5982\u679C...\u600E\u4E48\u529E"\u7684\u95EE\u9898\u3002`,
11243
+ color: "green"
11244
+ }
11245
+ ]
11246
+ },
11247
+ {
11248
+ id: "debate",
11249
+ name: "Debate (Pro vs Con)",
11250
+ description: "\u6B63\u65B9 + \u53CD\u65B9 + \u4E3B\u6301\u4EBA \u8FA9\u8BBA\u6A21\u5F0F",
11251
+ roles: [
11252
+ {
11253
+ id: "pro",
11254
+ name: "\u6B63\u65B9",
11255
+ persona: `\u4F60\u662F\u8FA9\u8BBA\u4E2D\u7684\u6B63\u65B9\uFF0C\u4F60\u7684\u4EFB\u52A1\u662F\u8BBA\u8BC1\u8BA8\u8BBA\u4E3B\u9898\u7684\u6B63\u9762\u4EF7\u503C\u3002
11256
+ \u4F60\u5FC5\u987B\uFF1A\u4E3A\u4E3B\u9898\u8FA9\u62A4\uFF0C\u627E\u5230\u652F\u6301\u8BBA\u636E\uFF0C\u56DE\u5E94\u53CD\u65B9\u7684\u8D28\u7591\u3002
11257
+ \u98CE\u683C\uFF1A\u903B\u8F91\u4E25\u5BC6\uFF0C\u5584\u4E8E\u5F15\u7528\u6848\u4F8B\u548C\u6570\u636E\u3002\u5373\u4F7F\u9762\u5BF9\u5F3A\u6709\u529B\u7684\u53CD\u9A73\u4E5F\u8981\u627E\u5230\u65B0\u7684\u8BBA\u8BC1\u89D2\u5EA6\u3002
11258
+ \u6CE8\u610F\uFF1A\u4F60\u53EF\u4EE5\u627F\u8BA4\u5BF9\u65B9\u7684\u90E8\u5206\u89C2\u70B9\uFF0C\u4F46\u8981\u6307\u51FA\u8FD9\u4E0D\u5F71\u54CD\u4F60\u7684\u6838\u5FC3\u8BBA\u70B9\u3002`,
11259
+ color: "green"
11260
+ },
11261
+ {
11262
+ id: "con",
11263
+ name: "\u53CD\u65B9",
11264
+ persona: `\u4F60\u662F\u8FA9\u8BBA\u4E2D\u7684\u53CD\u65B9\uFF0C\u4F60\u7684\u4EFB\u52A1\u662F\u627E\u51FA\u8BA8\u8BBA\u4E3B\u9898\u7684\u95EE\u9898\u548C\u98CE\u9669\u3002
11265
+ \u4F60\u5FC5\u987B\uFF1A\u63D0\u51FA\u8D28\u7591\uFF0C\u53D1\u73B0\u6F0F\u6D1E\uFF0C\u6307\u51FA\u6F5C\u5728\u98CE\u9669\u548C\u66FF\u4EE3\u65B9\u6848\u3002
11266
+ \u98CE\u683C\uFF1A\u7280\u5229\u3001\u5584\u4E8E\u53CD\u95EE\uFF0C\u7528\u53CD\u9762\u6848\u4F8B\u548C\u903B\u8F91\u63A8\u7406\u6765\u8BBA\u8BC1\u3002
11267
+ \u6CE8\u610F\uFF1A\u4F60\u4E0D\u662F\u4E3A\u4E86\u5426\u5B9A\u800C\u5426\u5B9A\uFF0C\u800C\u662F\u901A\u8FC7\u8D28\u7591\u6765\u5E2E\u52A9\u5168\u9762\u7406\u89E3\u95EE\u9898\u3002`,
11268
+ color: "red"
11269
+ },
11270
+ {
11271
+ id: "moderator",
11272
+ name: "\u4E3B\u6301\u4EBA",
11273
+ persona: `\u4F60\u662F\u8FA9\u8BBA\u7684\u4E3B\u6301\u4EBA\uFF0C\u4F60\u7684\u4EFB\u52A1\u662F\u5F15\u5BFC\u8BA8\u8BBA\u3001\u603B\u7ED3\u89C2\u70B9\u3001\u63D0\u51FA\u65B0\u7684\u8BA8\u8BBA\u65B9\u5411\u3002
11274
+ \u4F60\u5FC5\u987B\uFF1A\u4FDD\u6301\u4E2D\u7ACB\uFF0C\u603B\u7ED3\u53CC\u65B9\u8981\u70B9\uFF0C\u5728\u8BA8\u8BBA\u9677\u5165\u50F5\u5C40\u65F6\u63D0\u51FA\u65B0\u89D2\u5EA6\u3002
11275
+ \u98CE\u683C\uFF1A\u516C\u6B63\u3001\u5584\u4E8E\u5F52\u7EB3\uFF0C\u6BCF\u6B21\u53D1\u8A00\u5148\u7B80\u77ED\u603B\u7ED3\u53CC\u65B9\u89C2\u70B9\uFF0C\u518D\u5F15\u5BFC\u4E0B\u4E00\u4E2A\u8BA8\u8BBA\u65B9\u5411\u3002
11276
+ \u6CE8\u610F\uFF1A\u4E0D\u8981\u8FC7\u591A\u53D1\u8868\u4E2A\u4EBA\u89C2\u70B9\uFF0C\u91CD\u70B9\u662F\u63A8\u8FDB\u8BA8\u8BBA\u8D28\u91CF\u3002`,
11277
+ color: "cyan"
11278
+ }
11279
+ ]
11280
+ }
11281
+ ];
11282
+ function getPreset(id) {
11283
+ return PRESETS.find((p) => p.id === id);
11284
+ }
11285
+
11286
+ // src/hub/resolve-providers.ts
11287
+ function resolveRoleProviders(roles, lookup, defaultProvider, defaultModel, available, mix) {
11288
+ const warnings = [];
11289
+ let pool = null;
11290
+ if (mix !== void 0 && mix !== false) {
11291
+ if (typeof mix === "string" && mix.trim().length > 0) {
11292
+ const requested = mix.split(",").map((s) => s.trim()).filter(Boolean);
11293
+ pool = [];
11294
+ for (const id of requested) {
11295
+ if (lookup.has(id)) pool.push(id);
11296
+ else warnings.push(`--mix: provider "${id}" not configured \u2014 skipped.`);
11297
+ }
11298
+ } else {
11299
+ pool = [...available];
11300
+ }
11301
+ if (pool.length === 0) {
11302
+ warnings.push(`--mix: no usable providers \u2014 all roles fall back to "${defaultProvider}".`);
11303
+ pool = [defaultProvider];
11304
+ }
11305
+ }
11306
+ const providerDefaultModel = (pid) => pid === defaultProvider ? defaultModel : lookup.defaultModelFor(pid) ?? defaultModel;
11307
+ const assignments = [];
11308
+ const outRoles = roles.map((role, i) => {
11309
+ const desired = pool ? pool[i % pool.length] : role.provider;
11310
+ let providerId;
11311
+ let fellBack = false;
11312
+ if (desired && lookup.has(desired)) {
11313
+ providerId = desired;
11314
+ } else {
11315
+ if (desired && !lookup.has(desired)) {
11316
+ warnings.push(`role "${role.id}": provider "${desired}" not available \u2014 using "${defaultProvider}".`);
11317
+ fellBack = true;
11318
+ }
11319
+ providerId = defaultProvider;
11320
+ }
11321
+ const explicitProvider = role.provider ?? defaultProvider;
11322
+ const modelId = !pool && role.model && explicitProvider === providerId ? role.model : providerDefaultModel(providerId);
11323
+ assignments.push({ roleId: role.id, roleName: role.name, providerId, modelId, fellBack });
11324
+ return { ...role, provider: providerId, model: modelId };
11325
+ });
11326
+ return { roles: outRoles, assignments, warnings };
11327
+ }
11328
+
11329
+ // src/hub/persist.ts
11330
+ import { join as join14 } from "path";
11331
+ function discussionToMessages(state2) {
11332
+ const out = [];
11333
+ const t0 = state2.messages[0]?.timestamp ?? /* @__PURE__ */ new Date();
11334
+ out.push({ role: "user", content: `\u{1F3DB} Topic: ${state2.topic}`, timestamp: t0 });
11335
+ for (const m of state2.messages) {
11336
+ if (m.speaker === "system") {
11337
+ out.push({ role: "system", content: m.content, timestamp: m.timestamp });
11338
+ continue;
11339
+ }
11340
+ if (m.speaker === "human") {
11341
+ out.push({ role: "user", content: `\u{1F9ED} ${m.speakerName}: ${m.content}`, timestamp: m.timestamp });
11342
+ continue;
11343
+ }
11344
+ if (m.passed || !m.content.trim()) continue;
11345
+ const tag = m.converged ? " \u2713converged" : "";
11346
+ out.push({
11347
+ role: "assistant",
11348
+ content: `**${m.speakerName}** (${m.speaker})${tag}
11349
+
11350
+ ${m.content}`,
11351
+ timestamp: m.timestamp
11352
+ });
11353
+ }
11354
+ if (state2.summary && state2.summary.trim()) {
11355
+ out.push({ role: "assistant", content: `\u{1F4CB} **Summary**
11356
+
11357
+ ${state2.summary}`, timestamp: /* @__PURE__ */ new Date() });
11358
+ }
11359
+ return out;
11360
+ }
11361
+ async function persistDiscussion(state2, config, defaultProvider, defaultModel) {
11362
+ const sm = new SessionManager(config);
11363
+ const session = sm.createSession(defaultProvider, defaultModel);
11364
+ session.messages = discussionToMessages(state2);
11365
+ session.title = `[Hub] ${state2.topic.slice(0, 48)}`.replace(/\n/g, " ");
11366
+ session.titleAiGenerated = true;
11367
+ await sm.save();
11368
+ return { id: session.id, path: join14(config.getHistoryDir(), `${session.id}.json`) };
11369
+ }
11370
+
10758
11371
  // src/web/session-handler.ts
10759
11372
  var FREE_ROUND_TOOLS = /* @__PURE__ */ new Set(["write_todos"]);
10760
11373
  var MAX_CONSECUTIVE_FREE_ROUNDS = 5;
@@ -10785,6 +11398,8 @@ var SessionHandler = class _SessionHandler {
10785
11398
  abortController = null;
10786
11399
  userInterjection = null;
10787
11400
  processing = false;
11401
+ /** P4: active hub discussion orchestrator (web room), if any. */
11402
+ hubOrchestrator = null;
10788
11403
  /** Pending ask_user promises */
10789
11404
  pendingAskUser = /* @__PURE__ */ new Map();
10790
11405
  /** Pending auto-pause promises */
@@ -10923,6 +11538,11 @@ var SessionHandler = class _SessionHandler {
10923
11538
  this.abortController.abort();
10924
11539
  }
10925
11540
  return;
11541
+ case "hub_start":
11542
+ return this.handleHubStart(msg);
11543
+ case "hub_abort":
11544
+ this.hubOrchestrator?.abort();
11545
+ return;
10926
11546
  case "interjection":
10927
11547
  this.userInterjection = msg.content;
10928
11548
  return;
@@ -11013,6 +11633,82 @@ var SessionHandler = class _SessionHandler {
11013
11633
  }
11014
11634
  }
11015
11635
  // ── Chat handling ────────────────────────────────────────────────
11636
+ /**
11637
+ * P4: run a multi-agent hub discussion in the browser room. Bridges the
11638
+ * UI-agnostic DiscussionOrchestrator (onEvent) to WebSocket hub_event
11639
+ * messages, then persists the result (P3) so it can be replayed.
11640
+ */
11641
+ async handleHubStart(msg) {
11642
+ if (this.processing || this.hubOrchestrator) {
11643
+ this.send({ type: "error", message: "Already running a request. Abort first." });
11644
+ return;
11645
+ }
11646
+ const topic = (msg.topic ?? "").trim();
11647
+ if (!topic) {
11648
+ this.send({ type: "error", message: "Hub topic is required." });
11649
+ return;
11650
+ }
11651
+ const preset = getPreset(msg.preset ?? "brainstorm");
11652
+ if (!preset) {
11653
+ this.send({ type: "error", message: `Unknown preset "${msg.preset}".` });
11654
+ return;
11655
+ }
11656
+ const defaultProvider = this.currentProvider;
11657
+ const defaultModel = this.currentModel;
11658
+ const resolution = resolveRoleProviders(
11659
+ preset.roles,
11660
+ {
11661
+ has: (id) => this.providers.has(id),
11662
+ defaultModelFor: (id) => this.providers.has(id) ? this.providers.get(id).info.defaultModel : void 0
11663
+ },
11664
+ defaultProvider,
11665
+ defaultModel,
11666
+ this.providers.listAvailable().map((p) => p.info.id),
11667
+ msg.mix
11668
+ );
11669
+ const roles = resolution.roles;
11670
+ for (const w of resolution.warnings) this.send({ type: "info", message: w });
11671
+ const config = {
11672
+ mode: "discuss",
11673
+ roles,
11674
+ defaultProvider,
11675
+ defaultModel,
11676
+ maxRounds: msg.maxRounds && msg.maxRounds > 0 ? Math.min(msg.maxRounds, 20) : 6,
11677
+ voteConverge: msg.vote === true
11678
+ };
11679
+ this.send({
11680
+ type: "hub_started",
11681
+ topic,
11682
+ maxRounds: config.maxRounds,
11683
+ roles: roles.map((r) => ({ id: r.id, name: r.name, color: r.color, provider: r.provider, model: r.model }))
11684
+ });
11685
+ const orchestrator = new DiscussionOrchestrator(config, this.providers);
11686
+ this.hubOrchestrator = orchestrator;
11687
+ orchestrator.onEvent = (event) => this.send({ type: "hub_event", event });
11688
+ this.processing = true;
11689
+ try {
11690
+ const state2 = await orchestrator.run(topic);
11691
+ let sessionId;
11692
+ let saved = false;
11693
+ if (state2.messages.length > 0) {
11694
+ try {
11695
+ const res = await persistDiscussion(state2, this.config, defaultProvider, defaultModel);
11696
+ sessionId = res.id;
11697
+ saved = true;
11698
+ } catch (err) {
11699
+ this.send({ type: "info", message: `Could not save discussion: ${err.message}` });
11700
+ }
11701
+ }
11702
+ this.send({ type: "hub_done", sessionId, saved });
11703
+ if (saved) this.sendSessionList();
11704
+ } catch (err) {
11705
+ this.send({ type: "error", message: `Hub error: ${err.message}` });
11706
+ this.send({ type: "hub_done", saved: false });
11707
+ } finally {
11708
+ this.processing = false;
11709
+ this.hubOrchestrator = null;
11710
+ }
11711
+ }
11016
11712
  async handleChat(content, images) {
11017
11713
  if (this.processing) {
11018
11714
  this.send({ type: "error", message: "Already processing a request. Use abort first." });
@@ -12715,7 +13411,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
12715
13411
  case "test": {
12716
13412
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
12717
13413
  try {
12718
- const { executeTests } = await import("./run-tests-5CJRMOMI.js");
13414
+ const { executeTests } = await import("./run-tests-NPWSCWP5.js");
12719
13415
  const argStr = args.join(" ").trim();
12720
13416
  let testArgs = {};
12721
13417
  if (argStr) {
@@ -12732,7 +13428,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
12732
13428
  // ── /init ───────────────────────────────────────────────────────
12733
13429
  case "init": {
12734
13430
  const cwd = process.cwd();
12735
- const targetPath = join14(cwd, "AICLI.md");
13431
+ const targetPath = join15(cwd, "AICLI.md");
12736
13432
  const force = args.includes("--force");
12737
13433
  if (existsSync21(targetPath) && !force) {
12738
13434
  this.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
@@ -12777,9 +13473,9 @@ Use /context reload to load it.` });
12777
13473
  }
12778
13474
  lines.push(` ${exists ? "\u2713" : "\u2013"} ${label.padEnd(14)} ${exists ? filePath + extra : "(not found)"}`);
12779
13475
  };
12780
- checkFile("config.json", join14(configDir, "config.json"));
12781
- checkFile("memory.md", join14(configDir, MEMORY_FILE_NAME));
12782
- checkFile("dev-state.md", join14(configDir, "dev-state.md"));
13476
+ checkFile("config.json", join15(configDir, "config.json"));
13477
+ checkFile("memory.md", join15(configDir, MEMORY_FILE_NAME));
13478
+ checkFile("dev-state.md", join15(configDir, "dev-state.md"));
12783
13479
  lines.push("");
12784
13480
  if (this.mcpManager) {
12785
13481
  lines.push("**MCP Servers:**");
@@ -12886,7 +13582,7 @@ ${this.config.toFormattedJSON()}
12886
13582
  const layers = ["\u{1F4DA} **Context Layers:**", ""];
12887
13583
  const checkLayer = (label, dir) => {
12888
13584
  for (const name2 of CONTEXT_FILE_CANDIDATES) {
12889
- const fullPath = join14(dir, name2);
13585
+ const fullPath = join15(dir, name2);
12890
13586
  if (existsSync21(fullPath)) {
12891
13587
  try {
12892
13588
  const size = statSync8(fullPath).size;
@@ -12988,7 +13684,7 @@ It will be included in AI context for subsequent messages.` });
12988
13684
  // ── /commands ───────────────────────────────────────────────────
12989
13685
  case "commands": {
12990
13686
  const configDir = this.config.getConfigDir();
12991
- const commandsDir = join14(configDir, CUSTOM_COMMANDS_DIR_NAME);
13687
+ const commandsDir = join15(configDir, CUSTOM_COMMANDS_DIR_NAME);
12992
13688
  if (!existsSync21(commandsDir)) {
12993
13689
  this.send({ type: "info", message: `No custom commands directory.
12994
13690
  Create: ${commandsDir}/ with .md files.` });
@@ -13015,7 +13711,7 @@ Add .md files to create commands.` });
13015
13711
  // ── /plugins ────────────────────────────────────────────────────
13016
13712
  case "plugins": {
13017
13713
  const configDir = this.config.getConfigDir();
13018
- const pluginsDir = join14(configDir, PLUGINS_DIR_NAME);
13714
+ const pluginsDir = join15(configDir, PLUGINS_DIR_NAME);
13019
13715
  const pluginTools = this.toolRegistry.listPluginTools();
13020
13716
  const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
13021
13717
  if (pluginTools.length === 0) {
@@ -13189,7 +13885,7 @@ Add .md files to create commands.` });
13189
13885
  }
13190
13886
  memoryShow() {
13191
13887
  const configDir = this.config.getConfigDir();
13192
- const memPath = join14(configDir, MEMORY_FILE_NAME);
13888
+ const memPath = join15(configDir, MEMORY_FILE_NAME);
13193
13889
  let content = "";
13194
13890
  try {
13195
13891
  if (existsSync21(memPath)) {
@@ -13207,7 +13903,7 @@ Add .md files to create commands.` });
13207
13903
  }
13208
13904
  memoryAdd(text) {
13209
13905
  const configDir = this.config.getConfigDir();
13210
- const memPath = join14(configDir, MEMORY_FILE_NAME);
13906
+ const memPath = join15(configDir, MEMORY_FILE_NAME);
13211
13907
  try {
13212
13908
  mkdirSync10(configDir, { recursive: true });
13213
13909
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
@@ -13221,7 +13917,7 @@ Add .md files to create commands.` });
13221
13917
  }
13222
13918
  memoryClear() {
13223
13919
  const configDir = this.config.getConfigDir();
13224
- const memPath = join14(configDir, MEMORY_FILE_NAME);
13920
+ const memPath = join15(configDir, MEMORY_FILE_NAME);
13225
13921
  try {
13226
13922
  writeFileSync9(memPath, "", "utf-8");
13227
13923
  this.send({ type: "info", message: "\u{1F5D1}\uFE0F Persistent memory cleared." });
@@ -13430,7 +14126,7 @@ Add .md files to create commands.` });
13430
14126
  */
13431
14127
  findContextFile(dir) {
13432
14128
  for (const name of CONTEXT_FILE_CANDIDATES) {
13433
- const fullPath = join14(dir, name);
14129
+ const fullPath = join15(dir, name);
13434
14130
  try {
13435
14131
  if (existsSync21(fullPath)) {
13436
14132
  const content = readFileSync14(fullPath, "utf-8").trim();
@@ -13491,11 +14187,11 @@ Add .md files to create commands.` });
13491
14187
  const sorted = filtered.sort((a, b) => {
13492
14188
  let aIsDir = false, bIsDir = false;
13493
14189
  try {
13494
- aIsDir = statSync8(join14(d, a)).isDirectory();
14190
+ aIsDir = statSync8(join15(d, a)).isDirectory();
13495
14191
  } catch {
13496
14192
  }
13497
14193
  try {
13498
- bIsDir = statSync8(join14(d, b)).isDirectory();
14194
+ bIsDir = statSync8(join15(d, b)).isDirectory();
13499
14195
  } catch {
13500
14196
  }
13501
14197
  if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
@@ -13503,7 +14199,7 @@ Add .md files to create commands.` });
13503
14199
  });
13504
14200
  for (let i = 0; i < sorted.length && count < maxEntries; i++) {
13505
14201
  const name = sorted[i];
13506
- const fullPath = join14(d, name);
14202
+ const fullPath = join15(d, name);
13507
14203
  const isLast = i === sorted.length - 1;
13508
14204
  let isDir;
13509
14205
  try {
@@ -13522,7 +14218,7 @@ Add .md files to create commands.` });
13522
14218
  }
13523
14219
  scanProject(cwd) {
13524
14220
  const info = { type: "unknown", language: "unknown", configFiles: [], directoryStructure: "" };
13525
- const check = (file) => existsSync21(join14(cwd, file));
14221
+ const check = (file) => existsSync21(join15(cwd, file));
13526
14222
  const configCandidates = [
13527
14223
  "package.json",
13528
14224
  "tsconfig.json",
@@ -13549,7 +14245,7 @@ Add .md files to create commands.` });
13549
14245
  info.type = "node";
13550
14246
  info.language = check("tsconfig.json") ? "TypeScript" : "JavaScript";
13551
14247
  try {
13552
- const pkg = JSON.parse(readFileSync14(join14(cwd, "package.json"), "utf-8"));
14248
+ const pkg = JSON.parse(readFileSync14(join15(cwd, "package.json"), "utf-8"));
13553
14249
  const scripts = pkg.scripts ?? {};
13554
14250
  info.buildCommand = scripts.build ? "npm run build" : void 0;
13555
14251
  info.testCommand = scripts.test ? "npm test" : void 0;
@@ -13700,7 +14396,7 @@ async function setupProxy(configProxy) {
13700
14396
 
13701
14397
  // src/web/auth.ts
13702
14398
  import { existsSync as existsSync22, readFileSync as readFileSync15, writeFileSync as writeFileSync10, mkdirSync as mkdirSync11, readdirSync as readdirSync10, copyFileSync, renameSync as renameSync3, unlinkSync as unlinkSync5 } from "fs";
13703
- import { join as join15 } from "path";
14399
+ import { join as join16 } from "path";
13704
14400
  import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
13705
14401
  var USERS_FILE = "users.json";
13706
14402
  var TOKEN_EXPIRY_HOURS = 24;
@@ -13715,7 +14411,7 @@ var AuthManager = class {
13715
14411
  db;
13716
14412
  constructor(baseDir) {
13717
14413
  this.baseDir = baseDir;
13718
- this.usersFile = join15(baseDir, USERS_FILE);
14414
+ this.usersFile = join16(baseDir, USERS_FILE);
13719
14415
  this.db = this.loadOrCreate();
13720
14416
  }
13721
14417
  // ── Public API ─────────────────────────────────────────────────
@@ -13744,9 +14440,9 @@ var AuthManager = class {
13744
14440
  }
13745
14441
  const salt = randomBytes(16).toString("hex");
13746
14442
  const passwordHash = this.hashPassword(password, salt);
13747
- const dataDir = join15(USERS_DIR, username);
13748
- const fullDataDir = join15(this.baseDir, dataDir);
13749
- mkdirSync11(join15(fullDataDir, "history"), { recursive: true });
14443
+ const dataDir = join16(USERS_DIR, username);
14444
+ const fullDataDir = join16(this.baseDir, dataDir);
14445
+ mkdirSync11(join16(fullDataDir, "history"), { recursive: true });
13750
14446
  const user = {
13751
14447
  username,
13752
14448
  passwordHash,
@@ -13861,7 +14557,7 @@ var AuthManager = class {
13861
14557
  getUserDataDir(username) {
13862
14558
  const user = this.db.users.find((u) => u.username === username);
13863
14559
  if (!user) throw new Error(`User not found: ${username}`);
13864
- return join15(this.baseDir, user.dataDir);
14560
+ return join16(this.baseDir, user.dataDir);
13865
14561
  }
13866
14562
  /** List all usernames */
13867
14563
  listUsers() {
@@ -13897,30 +14593,30 @@ var AuthManager = class {
13897
14593
  const err = this.register(username, password);
13898
14594
  if (err) return err;
13899
14595
  const userDir = this.getUserDataDir(username);
13900
- const globalConfig = join15(this.baseDir, "config.json");
14596
+ const globalConfig = join16(this.baseDir, "config.json");
13901
14597
  if (existsSync22(globalConfig)) {
13902
14598
  try {
13903
14599
  const content = readFileSync15(globalConfig, "utf-8");
13904
- writeFileSync10(join15(userDir, "config.json"), content, "utf-8");
14600
+ writeFileSync10(join16(userDir, "config.json"), content, "utf-8");
13905
14601
  } catch {
13906
14602
  }
13907
14603
  }
13908
- const globalMemory = join15(this.baseDir, "memory.md");
14604
+ const globalMemory = join16(this.baseDir, "memory.md");
13909
14605
  if (existsSync22(globalMemory)) {
13910
14606
  try {
13911
14607
  const content = readFileSync15(globalMemory, "utf-8");
13912
- writeFileSync10(join15(userDir, "memory.md"), content, "utf-8");
14608
+ writeFileSync10(join16(userDir, "memory.md"), content, "utf-8");
13913
14609
  } catch {
13914
14610
  }
13915
14611
  }
13916
- const globalHistory = join15(this.baseDir, "history");
14612
+ const globalHistory = join16(this.baseDir, "history");
13917
14613
  if (existsSync22(globalHistory)) {
13918
14614
  try {
13919
14615
  const files = readdirSync10(globalHistory).filter((f) => f.endsWith(".json"));
13920
- const userHistory = join15(userDir, "history");
14616
+ const userHistory = join16(userDir, "history");
13921
14617
  for (const f of files) {
13922
14618
  try {
13923
- copyFileSync(join15(globalHistory, f), join15(userHistory, f));
14619
+ copyFileSync(join16(globalHistory, f), join16(userHistory, f));
13924
14620
  } catch {
13925
14621
  }
13926
14622
  }
@@ -14060,7 +14756,7 @@ async function startWebServer(options = {}) {
14060
14756
  }
14061
14757
  }
14062
14758
  let skillManager = null;
14063
- const skillsDir = join16(config.getConfigDir(), SKILLS_DIR_NAME);
14759
+ const skillsDir = join17(config.getConfigDir(), SKILLS_DIR_NAME);
14064
14760
  if (existsSync23(skillsDir)) {
14065
14761
  skillManager = new SkillManager(skillsDir, config.get("ui").skillSizeWarn);
14066
14762
  skillManager.loadSkills();
@@ -14130,15 +14826,15 @@ async function startWebServer(options = {}) {
14130
14826
  next();
14131
14827
  };
14132
14828
  const moduleDir = getModuleDir();
14133
- let clientDir = join16(moduleDir, "web", "client");
14829
+ let clientDir = join17(moduleDir, "web", "client");
14134
14830
  if (!existsSync23(clientDir)) {
14135
- clientDir = join16(moduleDir, "client");
14831
+ clientDir = join17(moduleDir, "client");
14136
14832
  }
14137
14833
  if (!existsSync23(clientDir)) {
14138
- clientDir = join16(moduleDir, "..", "..", "src", "web", "client");
14834
+ clientDir = join17(moduleDir, "..", "..", "src", "web", "client");
14139
14835
  }
14140
14836
  if (!existsSync23(clientDir)) {
14141
- clientDir = join16(process.cwd(), "src", "web", "client");
14837
+ clientDir = join17(process.cwd(), "src", "web", "client");
14142
14838
  }
14143
14839
  console.log(` Static files: ${clientDir}`);
14144
14840
  app.use(express.static(clientDir));
@@ -14207,7 +14903,7 @@ async function startWebServer(options = {}) {
14207
14903
  app.get("/api/files", requireAuth, (req, res) => {
14208
14904
  const cwd = process.cwd();
14209
14905
  const prefix = req.query.prefix || "";
14210
- const targetDir = join16(cwd, prefix);
14906
+ const targetDir = join17(cwd, prefix);
14211
14907
  try {
14212
14908
  const canonicalTarget = realpathSync(resolve6(targetDir));
14213
14909
  const canonicalCwd = realpathSync(resolve6(cwd));
@@ -14224,7 +14920,7 @@ async function startWebServer(options = {}) {
14224
14920
  const entries = readdirSync11(targetDir, { withFileTypes: true });
14225
14921
  const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
14226
14922
  name: e.name,
14227
- path: relative3(cwd, join16(targetDir, e.name)).replace(/\\/g, "/"),
14923
+ path: relative3(cwd, join17(targetDir, e.name)).replace(/\\/g, "/"),
14228
14924
  isDir: e.isDirectory()
14229
14925
  }));
14230
14926
  res.json({ files });
@@ -14260,7 +14956,7 @@ async function startWebServer(options = {}) {
14260
14956
  try {
14261
14957
  const authUser = req._authUser;
14262
14958
  const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
14263
- const filePath = join16(histDir, `${id}.json`);
14959
+ const filePath = join17(histDir, `${id}.json`);
14264
14960
  if (!existsSync23(filePath)) {
14265
14961
  res.status(404).json({ error: "Session not found" });
14266
14962
  return;
@@ -14289,7 +14985,7 @@ async function startWebServer(options = {}) {
14289
14985
  return;
14290
14986
  }
14291
14987
  const cwd = process.cwd();
14292
- const fullPath = resolve6(join16(cwd, filePath));
14988
+ const fullPath = resolve6(join17(cwd, filePath));
14293
14989
  try {
14294
14990
  const canonicalFull = realpathSync(fullPath);
14295
14991
  const canonicalCwd = realpathSync(resolve6(cwd));
@@ -14561,14 +15257,14 @@ function resolveProjectMcpPath() {
14561
15257
  const cwd = process.cwd();
14562
15258
  const gitRoot = getGitRoot(cwd);
14563
15259
  const projectRoot = gitRoot ?? cwd;
14564
- const configPath = join16(projectRoot, MCP_PROJECT_CONFIG_NAME);
15260
+ const configPath = join17(projectRoot, MCP_PROJECT_CONFIG_NAME);
14565
15261
  return existsSync23(configPath) ? configPath : null;
14566
15262
  }
14567
15263
  function loadProjectMcpConfig() {
14568
15264
  const cwd = process.cwd();
14569
15265
  const gitRoot = getGitRoot(cwd);
14570
15266
  const projectRoot = gitRoot ?? cwd;
14571
- const configPath = join16(projectRoot, MCP_PROJECT_CONFIG_NAME);
15267
+ const configPath = join17(projectRoot, MCP_PROJECT_CONFIG_NAME);
14572
15268
  if (!existsSync23(configPath)) return null;
14573
15269
  try {
14574
15270
  const raw = JSON.parse(readFileSync16(configPath, "utf-8"));