jinzd-ai-cli 0.4.155 → 0.4.157

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-ATL6V3UE.js} +2 -2
  2. package/dist/{chat-index-IF4EINLQ.js → chat-index-2I7ZHRE5.js} +2 -2
  3. package/dist/{chunk-E5ICQT3P.js → chunk-3HJW556L.js} +4 -4
  4. package/dist/{chunk-JOJRBV2K.js → chunk-62CD2T5F.js} +1 -1
  5. package/dist/{chunk-D6GJTJQH.js → chunk-77DDRYFM.js} +1 -1
  6. package/dist/{chunk-CIZQZ7CC.js → chunk-A4JROOGF.js} +2 -2
  7. package/dist/{chunk-NFRTSL3N.js → chunk-GRFQ2QD5.js} +1 -1
  8. package/dist/{chunk-JXSWY54M.js → chunk-LLQMVGNP.js} +1 -1
  9. package/dist/{chunk-B3LFGPU2.js → chunk-N3VGZTEJ.js} +1 -1
  10. package/dist/chunk-NZ4X6GUC.js +230 -0
  11. package/dist/{chunk-O6MLS5QO.js → chunk-OJL3PY36.js} +0 -226
  12. package/dist/{hub-ZILVZWI2.js → chunk-Q3ZUDA6S.js} +6 -249
  13. package/dist/{persist-3EBOLHFZ.js → chunk-RUJQ5OUB.js} +1 -2
  14. package/dist/{chunk-IBBYW6PM.js → chunk-V5OQOKU3.js} +1 -1
  15. package/dist/{ci-34ZQH43L.js → ci-E7MDZSB6.js} +3 -3
  16. package/dist/{constants-DQ5VJOGS.js → constants-NL5ETRA5.js} +1 -1
  17. package/dist/{doctor-cli-TSCI4ORL.js → doctor-cli-XJNT745O.js} +6 -6
  18. package/dist/electron-server.js +758 -44
  19. package/dist/hub-RXZ5IDBY.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-I3EBH7O6.js} +2 -2
  24. package/dist/{run-tests-5CJRMOMI.js → run-tests-O46AI32W.js} +1 -1
  25. package/dist/{server-35OQV62B.js → server-4PJJHZWM.js} +142 -32
  26. package/dist/{server-DVIP7NLW.js → server-SVBHOHTC.js} +6 -6
  27. package/dist/{task-orchestrator-AXSS7ROD.js → task-orchestrator-NMX3CYW2.js} +6 -6
  28. package/dist/web/client/app.js +173 -0
  29. package/dist/web/client/index.html +31 -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-LLQMVGNP.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,10 @@ 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;
11403
+ /** P4b: pending between-rounds human-steer prompts (requestId → resolver). */
11404
+ pendingHubReview = /* @__PURE__ */ new Map();
10788
11405
  /** Pending ask_user promises */
10789
11406
  pendingAskUser = /* @__PURE__ */ new Map();
10790
11407
  /** Pending auto-pause promises */
@@ -10923,6 +11540,19 @@ var SessionHandler = class _SessionHandler {
10923
11540
  this.abortController.abort();
10924
11541
  }
10925
11542
  return;
11543
+ case "hub_start":
11544
+ return this.handleHubStart(msg);
11545
+ case "hub_abort":
11546
+ this.hubOrchestrator?.abort();
11547
+ return;
11548
+ case "hub_steer": {
11549
+ const resolve7 = this.pendingHubReview.get(msg.requestId);
11550
+ if (resolve7) {
11551
+ this.pendingHubReview.delete(msg.requestId);
11552
+ resolve7({ action: msg.action, message: msg.message });
11553
+ }
11554
+ return;
11555
+ }
10926
11556
  case "interjection":
10927
11557
  this.userInterjection = msg.content;
10928
11558
  return;
@@ -11013,6 +11643,90 @@ var SessionHandler = class _SessionHandler {
11013
11643
  }
11014
11644
  }
11015
11645
  // ── Chat handling ────────────────────────────────────────────────
11646
+ /**
11647
+ * P4: run a multi-agent hub discussion in the browser room. Bridges the
11648
+ * UI-agnostic DiscussionOrchestrator (onEvent) to WebSocket hub_event
11649
+ * messages, then persists the result (P3) so it can be replayed.
11650
+ */
11651
+ async handleHubStart(msg) {
11652
+ if (this.processing || this.hubOrchestrator) {
11653
+ this.send({ type: "error", message: "Already running a request. Abort first." });
11654
+ return;
11655
+ }
11656
+ const topic = (msg.topic ?? "").trim();
11657
+ if (!topic) {
11658
+ this.send({ type: "error", message: "Hub topic is required." });
11659
+ return;
11660
+ }
11661
+ const preset = getPreset(msg.preset ?? "brainstorm");
11662
+ if (!preset) {
11663
+ this.send({ type: "error", message: `Unknown preset "${msg.preset}".` });
11664
+ return;
11665
+ }
11666
+ const defaultProvider = this.currentProvider;
11667
+ const defaultModel = this.currentModel;
11668
+ const resolution = resolveRoleProviders(
11669
+ preset.roles,
11670
+ {
11671
+ has: (id) => this.providers.has(id),
11672
+ defaultModelFor: (id) => this.providers.has(id) ? this.providers.get(id).info.defaultModel : void 0
11673
+ },
11674
+ defaultProvider,
11675
+ defaultModel,
11676
+ this.providers.listAvailable().map((p) => p.info.id),
11677
+ msg.mix
11678
+ );
11679
+ const roles = resolution.roles;
11680
+ for (const w of resolution.warnings) this.send({ type: "info", message: w });
11681
+ const config = {
11682
+ mode: "discuss",
11683
+ roles,
11684
+ defaultProvider,
11685
+ defaultModel,
11686
+ maxRounds: msg.maxRounds && msg.maxRounds > 0 ? Math.min(msg.maxRounds, 20) : 6,
11687
+ voteConverge: msg.vote === true,
11688
+ humanSteer: msg.steer === true
11689
+ };
11690
+ this.send({
11691
+ type: "hub_started",
11692
+ topic,
11693
+ maxRounds: config.maxRounds,
11694
+ roles: roles.map((r) => ({ id: r.id, name: r.name, color: r.color, provider: r.provider, model: r.model }))
11695
+ });
11696
+ const orchestrator = new DiscussionOrchestrator(config, this.providers);
11697
+ this.hubOrchestrator = orchestrator;
11698
+ orchestrator.onEvent = (event) => this.send({ type: "hub_event", event });
11699
+ if (config.humanSteer) {
11700
+ orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve7) => {
11701
+ const requestId = `hubrev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
11702
+ this.pendingHubReview.set(requestId, resolve7);
11703
+ this.send({ type: "hub_review", requestId, round, maxRounds });
11704
+ });
11705
+ }
11706
+ this.processing = true;
11707
+ try {
11708
+ const state2 = await orchestrator.run(topic);
11709
+ let sessionId;
11710
+ let saved = false;
11711
+ if (state2.messages.length > 0) {
11712
+ try {
11713
+ const res = await persistDiscussion(state2, this.config, defaultProvider, defaultModel);
11714
+ sessionId = res.id;
11715
+ saved = true;
11716
+ } catch (err) {
11717
+ this.send({ type: "info", message: `Could not save discussion: ${err.message}` });
11718
+ }
11719
+ }
11720
+ this.send({ type: "hub_done", sessionId, saved });
11721
+ if (saved) this.sendSessionList();
11722
+ } catch (err) {
11723
+ this.send({ type: "error", message: `Hub error: ${err.message}` });
11724
+ this.send({ type: "hub_done", saved: false });
11725
+ } finally {
11726
+ this.processing = false;
11727
+ this.hubOrchestrator = null;
11728
+ }
11729
+ }
11016
11730
  async handleChat(content, images) {
11017
11731
  if (this.processing) {
11018
11732
  this.send({ type: "error", message: "Already processing a request. Use abort first." });
@@ -12715,7 +13429,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
12715
13429
  case "test": {
12716
13430
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
12717
13431
  try {
12718
- const { executeTests } = await import("./run-tests-5CJRMOMI.js");
13432
+ const { executeTests } = await import("./run-tests-O46AI32W.js");
12719
13433
  const argStr = args.join(" ").trim();
12720
13434
  let testArgs = {};
12721
13435
  if (argStr) {
@@ -12732,7 +13446,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
12732
13446
  // ── /init ───────────────────────────────────────────────────────
12733
13447
  case "init": {
12734
13448
  const cwd = process.cwd();
12735
- const targetPath = join14(cwd, "AICLI.md");
13449
+ const targetPath = join15(cwd, "AICLI.md");
12736
13450
  const force = args.includes("--force");
12737
13451
  if (existsSync21(targetPath) && !force) {
12738
13452
  this.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
@@ -12777,9 +13491,9 @@ Use /context reload to load it.` });
12777
13491
  }
12778
13492
  lines.push(` ${exists ? "\u2713" : "\u2013"} ${label.padEnd(14)} ${exists ? filePath + extra : "(not found)"}`);
12779
13493
  };
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"));
13494
+ checkFile("config.json", join15(configDir, "config.json"));
13495
+ checkFile("memory.md", join15(configDir, MEMORY_FILE_NAME));
13496
+ checkFile("dev-state.md", join15(configDir, "dev-state.md"));
12783
13497
  lines.push("");
12784
13498
  if (this.mcpManager) {
12785
13499
  lines.push("**MCP Servers:**");
@@ -12886,7 +13600,7 @@ ${this.config.toFormattedJSON()}
12886
13600
  const layers = ["\u{1F4DA} **Context Layers:**", ""];
12887
13601
  const checkLayer = (label, dir) => {
12888
13602
  for (const name2 of CONTEXT_FILE_CANDIDATES) {
12889
- const fullPath = join14(dir, name2);
13603
+ const fullPath = join15(dir, name2);
12890
13604
  if (existsSync21(fullPath)) {
12891
13605
  try {
12892
13606
  const size = statSync8(fullPath).size;
@@ -12988,7 +13702,7 @@ It will be included in AI context for subsequent messages.` });
12988
13702
  // ── /commands ───────────────────────────────────────────────────
12989
13703
  case "commands": {
12990
13704
  const configDir = this.config.getConfigDir();
12991
- const commandsDir = join14(configDir, CUSTOM_COMMANDS_DIR_NAME);
13705
+ const commandsDir = join15(configDir, CUSTOM_COMMANDS_DIR_NAME);
12992
13706
  if (!existsSync21(commandsDir)) {
12993
13707
  this.send({ type: "info", message: `No custom commands directory.
12994
13708
  Create: ${commandsDir}/ with .md files.` });
@@ -13015,7 +13729,7 @@ Add .md files to create commands.` });
13015
13729
  // ── /plugins ────────────────────────────────────────────────────
13016
13730
  case "plugins": {
13017
13731
  const configDir = this.config.getConfigDir();
13018
- const pluginsDir = join14(configDir, PLUGINS_DIR_NAME);
13732
+ const pluginsDir = join15(configDir, PLUGINS_DIR_NAME);
13019
13733
  const pluginTools = this.toolRegistry.listPluginTools();
13020
13734
  const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
13021
13735
  if (pluginTools.length === 0) {
@@ -13189,7 +13903,7 @@ Add .md files to create commands.` });
13189
13903
  }
13190
13904
  memoryShow() {
13191
13905
  const configDir = this.config.getConfigDir();
13192
- const memPath = join14(configDir, MEMORY_FILE_NAME);
13906
+ const memPath = join15(configDir, MEMORY_FILE_NAME);
13193
13907
  let content = "";
13194
13908
  try {
13195
13909
  if (existsSync21(memPath)) {
@@ -13207,7 +13921,7 @@ Add .md files to create commands.` });
13207
13921
  }
13208
13922
  memoryAdd(text) {
13209
13923
  const configDir = this.config.getConfigDir();
13210
- const memPath = join14(configDir, MEMORY_FILE_NAME);
13924
+ const memPath = join15(configDir, MEMORY_FILE_NAME);
13211
13925
  try {
13212
13926
  mkdirSync10(configDir, { recursive: true });
13213
13927
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
@@ -13221,7 +13935,7 @@ Add .md files to create commands.` });
13221
13935
  }
13222
13936
  memoryClear() {
13223
13937
  const configDir = this.config.getConfigDir();
13224
- const memPath = join14(configDir, MEMORY_FILE_NAME);
13938
+ const memPath = join15(configDir, MEMORY_FILE_NAME);
13225
13939
  try {
13226
13940
  writeFileSync9(memPath, "", "utf-8");
13227
13941
  this.send({ type: "info", message: "\u{1F5D1}\uFE0F Persistent memory cleared." });
@@ -13430,7 +14144,7 @@ Add .md files to create commands.` });
13430
14144
  */
13431
14145
  findContextFile(dir) {
13432
14146
  for (const name of CONTEXT_FILE_CANDIDATES) {
13433
- const fullPath = join14(dir, name);
14147
+ const fullPath = join15(dir, name);
13434
14148
  try {
13435
14149
  if (existsSync21(fullPath)) {
13436
14150
  const content = readFileSync14(fullPath, "utf-8").trim();
@@ -13491,11 +14205,11 @@ Add .md files to create commands.` });
13491
14205
  const sorted = filtered.sort((a, b) => {
13492
14206
  let aIsDir = false, bIsDir = false;
13493
14207
  try {
13494
- aIsDir = statSync8(join14(d, a)).isDirectory();
14208
+ aIsDir = statSync8(join15(d, a)).isDirectory();
13495
14209
  } catch {
13496
14210
  }
13497
14211
  try {
13498
- bIsDir = statSync8(join14(d, b)).isDirectory();
14212
+ bIsDir = statSync8(join15(d, b)).isDirectory();
13499
14213
  } catch {
13500
14214
  }
13501
14215
  if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
@@ -13503,7 +14217,7 @@ Add .md files to create commands.` });
13503
14217
  });
13504
14218
  for (let i = 0; i < sorted.length && count < maxEntries; i++) {
13505
14219
  const name = sorted[i];
13506
- const fullPath = join14(d, name);
14220
+ const fullPath = join15(d, name);
13507
14221
  const isLast = i === sorted.length - 1;
13508
14222
  let isDir;
13509
14223
  try {
@@ -13522,7 +14236,7 @@ Add .md files to create commands.` });
13522
14236
  }
13523
14237
  scanProject(cwd) {
13524
14238
  const info = { type: "unknown", language: "unknown", configFiles: [], directoryStructure: "" };
13525
- const check = (file) => existsSync21(join14(cwd, file));
14239
+ const check = (file) => existsSync21(join15(cwd, file));
13526
14240
  const configCandidates = [
13527
14241
  "package.json",
13528
14242
  "tsconfig.json",
@@ -13549,7 +14263,7 @@ Add .md files to create commands.` });
13549
14263
  info.type = "node";
13550
14264
  info.language = check("tsconfig.json") ? "TypeScript" : "JavaScript";
13551
14265
  try {
13552
- const pkg = JSON.parse(readFileSync14(join14(cwd, "package.json"), "utf-8"));
14266
+ const pkg = JSON.parse(readFileSync14(join15(cwd, "package.json"), "utf-8"));
13553
14267
  const scripts = pkg.scripts ?? {};
13554
14268
  info.buildCommand = scripts.build ? "npm run build" : void 0;
13555
14269
  info.testCommand = scripts.test ? "npm test" : void 0;
@@ -13700,7 +14414,7 @@ async function setupProxy(configProxy) {
13700
14414
 
13701
14415
  // src/web/auth.ts
13702
14416
  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";
14417
+ import { join as join16 } from "path";
13704
14418
  import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
13705
14419
  var USERS_FILE = "users.json";
13706
14420
  var TOKEN_EXPIRY_HOURS = 24;
@@ -13715,7 +14429,7 @@ var AuthManager = class {
13715
14429
  db;
13716
14430
  constructor(baseDir) {
13717
14431
  this.baseDir = baseDir;
13718
- this.usersFile = join15(baseDir, USERS_FILE);
14432
+ this.usersFile = join16(baseDir, USERS_FILE);
13719
14433
  this.db = this.loadOrCreate();
13720
14434
  }
13721
14435
  // ── Public API ─────────────────────────────────────────────────
@@ -13744,9 +14458,9 @@ var AuthManager = class {
13744
14458
  }
13745
14459
  const salt = randomBytes(16).toString("hex");
13746
14460
  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 });
14461
+ const dataDir = join16(USERS_DIR, username);
14462
+ const fullDataDir = join16(this.baseDir, dataDir);
14463
+ mkdirSync11(join16(fullDataDir, "history"), { recursive: true });
13750
14464
  const user = {
13751
14465
  username,
13752
14466
  passwordHash,
@@ -13861,7 +14575,7 @@ var AuthManager = class {
13861
14575
  getUserDataDir(username) {
13862
14576
  const user = this.db.users.find((u) => u.username === username);
13863
14577
  if (!user) throw new Error(`User not found: ${username}`);
13864
- return join15(this.baseDir, user.dataDir);
14578
+ return join16(this.baseDir, user.dataDir);
13865
14579
  }
13866
14580
  /** List all usernames */
13867
14581
  listUsers() {
@@ -13897,30 +14611,30 @@ var AuthManager = class {
13897
14611
  const err = this.register(username, password);
13898
14612
  if (err) return err;
13899
14613
  const userDir = this.getUserDataDir(username);
13900
- const globalConfig = join15(this.baseDir, "config.json");
14614
+ const globalConfig = join16(this.baseDir, "config.json");
13901
14615
  if (existsSync22(globalConfig)) {
13902
14616
  try {
13903
14617
  const content = readFileSync15(globalConfig, "utf-8");
13904
- writeFileSync10(join15(userDir, "config.json"), content, "utf-8");
14618
+ writeFileSync10(join16(userDir, "config.json"), content, "utf-8");
13905
14619
  } catch {
13906
14620
  }
13907
14621
  }
13908
- const globalMemory = join15(this.baseDir, "memory.md");
14622
+ const globalMemory = join16(this.baseDir, "memory.md");
13909
14623
  if (existsSync22(globalMemory)) {
13910
14624
  try {
13911
14625
  const content = readFileSync15(globalMemory, "utf-8");
13912
- writeFileSync10(join15(userDir, "memory.md"), content, "utf-8");
14626
+ writeFileSync10(join16(userDir, "memory.md"), content, "utf-8");
13913
14627
  } catch {
13914
14628
  }
13915
14629
  }
13916
- const globalHistory = join15(this.baseDir, "history");
14630
+ const globalHistory = join16(this.baseDir, "history");
13917
14631
  if (existsSync22(globalHistory)) {
13918
14632
  try {
13919
14633
  const files = readdirSync10(globalHistory).filter((f) => f.endsWith(".json"));
13920
- const userHistory = join15(userDir, "history");
14634
+ const userHistory = join16(userDir, "history");
13921
14635
  for (const f of files) {
13922
14636
  try {
13923
- copyFileSync(join15(globalHistory, f), join15(userHistory, f));
14637
+ copyFileSync(join16(globalHistory, f), join16(userHistory, f));
13924
14638
  } catch {
13925
14639
  }
13926
14640
  }
@@ -14060,7 +14774,7 @@ async function startWebServer(options = {}) {
14060
14774
  }
14061
14775
  }
14062
14776
  let skillManager = null;
14063
- const skillsDir = join16(config.getConfigDir(), SKILLS_DIR_NAME);
14777
+ const skillsDir = join17(config.getConfigDir(), SKILLS_DIR_NAME);
14064
14778
  if (existsSync23(skillsDir)) {
14065
14779
  skillManager = new SkillManager(skillsDir, config.get("ui").skillSizeWarn);
14066
14780
  skillManager.loadSkills();
@@ -14130,15 +14844,15 @@ async function startWebServer(options = {}) {
14130
14844
  next();
14131
14845
  };
14132
14846
  const moduleDir = getModuleDir();
14133
- let clientDir = join16(moduleDir, "web", "client");
14847
+ let clientDir = join17(moduleDir, "web", "client");
14134
14848
  if (!existsSync23(clientDir)) {
14135
- clientDir = join16(moduleDir, "client");
14849
+ clientDir = join17(moduleDir, "client");
14136
14850
  }
14137
14851
  if (!existsSync23(clientDir)) {
14138
- clientDir = join16(moduleDir, "..", "..", "src", "web", "client");
14852
+ clientDir = join17(moduleDir, "..", "..", "src", "web", "client");
14139
14853
  }
14140
14854
  if (!existsSync23(clientDir)) {
14141
- clientDir = join16(process.cwd(), "src", "web", "client");
14855
+ clientDir = join17(process.cwd(), "src", "web", "client");
14142
14856
  }
14143
14857
  console.log(` Static files: ${clientDir}`);
14144
14858
  app.use(express.static(clientDir));
@@ -14207,7 +14921,7 @@ async function startWebServer(options = {}) {
14207
14921
  app.get("/api/files", requireAuth, (req, res) => {
14208
14922
  const cwd = process.cwd();
14209
14923
  const prefix = req.query.prefix || "";
14210
- const targetDir = join16(cwd, prefix);
14924
+ const targetDir = join17(cwd, prefix);
14211
14925
  try {
14212
14926
  const canonicalTarget = realpathSync(resolve6(targetDir));
14213
14927
  const canonicalCwd = realpathSync(resolve6(cwd));
@@ -14224,7 +14938,7 @@ async function startWebServer(options = {}) {
14224
14938
  const entries = readdirSync11(targetDir, { withFileTypes: true });
14225
14939
  const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
14226
14940
  name: e.name,
14227
- path: relative3(cwd, join16(targetDir, e.name)).replace(/\\/g, "/"),
14941
+ path: relative3(cwd, join17(targetDir, e.name)).replace(/\\/g, "/"),
14228
14942
  isDir: e.isDirectory()
14229
14943
  }));
14230
14944
  res.json({ files });
@@ -14260,7 +14974,7 @@ async function startWebServer(options = {}) {
14260
14974
  try {
14261
14975
  const authUser = req._authUser;
14262
14976
  const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
14263
- const filePath = join16(histDir, `${id}.json`);
14977
+ const filePath = join17(histDir, `${id}.json`);
14264
14978
  if (!existsSync23(filePath)) {
14265
14979
  res.status(404).json({ error: "Session not found" });
14266
14980
  return;
@@ -14289,7 +15003,7 @@ async function startWebServer(options = {}) {
14289
15003
  return;
14290
15004
  }
14291
15005
  const cwd = process.cwd();
14292
- const fullPath = resolve6(join16(cwd, filePath));
15006
+ const fullPath = resolve6(join17(cwd, filePath));
14293
15007
  try {
14294
15008
  const canonicalFull = realpathSync(fullPath);
14295
15009
  const canonicalCwd = realpathSync(resolve6(cwd));
@@ -14561,14 +15275,14 @@ function resolveProjectMcpPath() {
14561
15275
  const cwd = process.cwd();
14562
15276
  const gitRoot = getGitRoot(cwd);
14563
15277
  const projectRoot = gitRoot ?? cwd;
14564
- const configPath = join16(projectRoot, MCP_PROJECT_CONFIG_NAME);
15278
+ const configPath = join17(projectRoot, MCP_PROJECT_CONFIG_NAME);
14565
15279
  return existsSync23(configPath) ? configPath : null;
14566
15280
  }
14567
15281
  function loadProjectMcpConfig() {
14568
15282
  const cwd = process.cwd();
14569
15283
  const gitRoot = getGitRoot(cwd);
14570
15284
  const projectRoot = gitRoot ?? cwd;
14571
- const configPath = join16(projectRoot, MCP_PROJECT_CONFIG_NAME);
15285
+ const configPath = join17(projectRoot, MCP_PROJECT_CONFIG_NAME);
14572
15286
  if (!existsSync23(configPath)) return null;
14573
15287
  try {
14574
15288
  const raw = JSON.parse(readFileSync16(configPath, "utf-8"));