codeharbor 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -17,6 +17,11 @@ CODEX_EXTRA_ARGS=
17
17
  # Optional JSON object for additional environment variables passed to codex child process.
18
18
  CODEX_EXTRA_ENV_JSON=
19
19
 
20
+ # Multi-agent workflow (Phase B, opt-in).
21
+ AGENT_WORKFLOW_ENABLED=false
22
+ # Auto-repair rounds after reviewer rejects output.
23
+ AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS=1
24
+
20
25
  # SQLite state database path.
21
26
  STATE_DB_PATH=data/state.db
22
27
  # Legacy JSON path for one-time migration import.
package/README.md CHANGED
@@ -67,27 +67,33 @@ curl -fsSL https://raw.githubusercontent.com/biglone/CodeHarbor/main/scripts/ins
67
67
  Install first, then enable systemd service with one command:
68
68
 
69
69
  ```bash
70
- sudo "$(command -v codeharbor)" service install
70
+ codeharbor service install
71
71
  ```
72
72
 
73
73
  Install + enable main and admin services:
74
74
 
75
75
  ```bash
76
- sudo "$(command -v codeharbor)" service install --with-admin
76
+ codeharbor service install --with-admin
77
77
  ```
78
78
 
79
79
  Restart installed service(s):
80
80
 
81
81
  ```bash
82
- sudo "$(command -v codeharbor)" service restart --with-admin
82
+ codeharbor service restart --with-admin
83
83
  ```
84
84
 
85
85
  Remove installed services:
86
86
 
87
87
  ```bash
88
- sudo "$(command -v codeharbor)" service uninstall --with-admin
88
+ codeharbor service uninstall --with-admin
89
89
  ```
90
90
 
91
+ Notes:
92
+
93
+ - Service commands auto-elevate with `sudo` when root privileges are required.
94
+ - If your environment blocks interactive `sudo`, use explicit fallback:
95
+ - `sudo <node-bin> <codeharbor-cli-script> service ...`
96
+
91
97
  Enable Admin service at install time:
92
98
 
93
99
  ```bash
@@ -391,6 +397,17 @@ If any check fails, it prints actionable fix commands (for example `codeharbor i
391
397
  - `/status` show session + limiter + metrics + runtime worker status
392
398
  - `/reset` clear bound Codex session and keep conversation active
393
399
  - `/stop` cancel in-flight execution (if running) and reset session context
400
+ - `/agents status` show multi-agent workflow status for current session (when enabled)
401
+ - `/agents run <objective>` run Planner -> Executor -> Reviewer workflow (when enabled)
402
+
403
+ ### Multi-Agent Workflow (Phase B, Opt-In)
404
+
405
+ - `AGENT_WORKFLOW_ENABLED=true`
406
+ - enable `/agents` workflow commands
407
+ - `AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS`
408
+ - reviewer reject loop upper bound (default `1`)
409
+
410
+ Default is disabled to keep legacy behavior unchanged.
394
411
 
395
412
  ## CLI Compatibility Mode
396
413
 
package/dist/cli.js CHANGED
@@ -24,6 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
+ var import_node_child_process6 = require("child_process");
27
28
  var import_node_fs11 = __toESM(require("fs"));
28
29
  var import_node_path12 = __toESM(require("path"));
29
30
  var import_commander = require("commander");
@@ -599,6 +600,27 @@ var AdminServer = class {
599
600
  updatedKeys.push("cliCompat.fetchMedia");
600
601
  }
601
602
  }
603
+ if ("agentWorkflow" in body) {
604
+ const workflow = asObject(body.agentWorkflow, "agentWorkflow");
605
+ const currentAgentWorkflow = ensureAgentWorkflowConfig(this.config);
606
+ if ("enabled" in workflow) {
607
+ const value = normalizeBoolean(workflow.enabled, currentAgentWorkflow.enabled);
608
+ currentAgentWorkflow.enabled = value;
609
+ envUpdates.AGENT_WORKFLOW_ENABLED = String(value);
610
+ updatedKeys.push("agentWorkflow.enabled");
611
+ }
612
+ if ("autoRepairMaxRounds" in workflow) {
613
+ const value = normalizePositiveInt(
614
+ workflow.autoRepairMaxRounds,
615
+ currentAgentWorkflow.autoRepairMaxRounds,
616
+ 0,
617
+ 10
618
+ );
619
+ currentAgentWorkflow.autoRepairMaxRounds = value;
620
+ envUpdates.AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS = String(value);
621
+ updatedKeys.push("agentWorkflow.autoRepairMaxRounds");
622
+ }
623
+ }
602
624
  if (updatedKeys.length === 0) {
603
625
  throw new HttpError(400, "No supported global config fields provided.");
604
626
  }
@@ -719,8 +741,22 @@ function buildGlobalConfigSnapshot(config) {
719
741
  matrixProgressMinIntervalMs: config.matrixProgressMinIntervalMs,
720
742
  matrixTypingTimeoutMs: config.matrixTypingTimeoutMs,
721
743
  sessionActiveWindowMinutes: config.sessionActiveWindowMinutes,
722
- cliCompat: { ...config.cliCompat }
744
+ cliCompat: { ...config.cliCompat },
745
+ agentWorkflow: { ...ensureAgentWorkflowConfig(config) }
746
+ };
747
+ }
748
+ function ensureAgentWorkflowConfig(config) {
749
+ const mutable = config;
750
+ const existing = mutable.agentWorkflow;
751
+ if (existing && typeof existing.enabled === "boolean" && Number.isFinite(existing.autoRepairMaxRounds)) {
752
+ return existing;
753
+ }
754
+ const fallback = {
755
+ enabled: false,
756
+ autoRepairMaxRounds: 1
723
757
  };
758
+ mutable.agentWorkflow = fallback;
759
+ return fallback;
724
760
  }
725
761
  function formatAuditEntry(entry) {
726
762
  return {
@@ -1221,6 +1257,11 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
1221
1257
  <input id="global-cli-throttle" type="number" min="0" />
1222
1258
  </label>
1223
1259
  <label class="checkbox"><input id="global-cli-fetch-media" type="checkbox" /><span>Fetch media attachments</span></label>
1260
+ <label class="checkbox"><input id="global-agent-enabled" type="checkbox" /><span>Enable multi-agent workflow</span></label>
1261
+ <label class="field">
1262
+ <span class="field-label">Workflow auto-repair rounds</span>
1263
+ <input id="global-agent-repair-rounds" type="number" min="0" max="10" />
1264
+ </label>
1224
1265
  </div>
1225
1266
  <div class="actions">
1226
1267
  <button id="global-save-btn" type="button">Save Global Config</button>
@@ -1488,6 +1529,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
1488
1529
  var rateLimiter = data.rateLimiter || {};
1489
1530
  var trigger = data.defaultGroupTriggerPolicy || {};
1490
1531
  var cliCompat = data.cliCompat || {};
1532
+ var agentWorkflow = data.agentWorkflow || {};
1491
1533
 
1492
1534
  document.getElementById("global-matrix-prefix").value = data.matrixCommandPrefix || "";
1493
1535
  document.getElementById("global-workdir").value = data.codexWorkdir || "";
@@ -1513,6 +1555,10 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
1513
1555
  document.getElementById("global-cli-disable-split").checked = Boolean(cliCompat.disableReplyChunkSplit);
1514
1556
  document.getElementById("global-cli-throttle").value = String(cliCompat.progressThrottleMs || 0);
1515
1557
  document.getElementById("global-cli-fetch-media").checked = Boolean(cliCompat.fetchMedia);
1558
+ document.getElementById("global-agent-enabled").checked = Boolean(agentWorkflow.enabled);
1559
+ document.getElementById("global-agent-repair-rounds").value = String(
1560
+ typeof agentWorkflow.autoRepairMaxRounds === "number" ? agentWorkflow.autoRepairMaxRounds : 1
1561
+ );
1516
1562
 
1517
1563
  showNotice("ok", "Global config loaded.");
1518
1564
  } catch (error) {
@@ -1550,6 +1596,10 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
1550
1596
  disableReplyChunkSplit: asBool("global-cli-disable-split"),
1551
1597
  progressThrottleMs: asNumber("global-cli-throttle", 300),
1552
1598
  fetchMedia: asBool("global-cli-fetch-media")
1599
+ },
1600
+ agentWorkflow: {
1601
+ enabled: asBool("global-agent-enabled"),
1602
+ autoRepairMaxRounds: asNumber("global-agent-repair-rounds", 1)
1553
1603
  }
1554
1604
  };
1555
1605
  var response = await apiRequest("/api/admin/config/global", "PUT", body);
@@ -2880,6 +2930,273 @@ function computeRetryAfter(timestamps, windowMs, now) {
2880
2930
  return Math.max(0, oldest + windowMs - now);
2881
2931
  }
2882
2932
 
2933
+ // src/workflow/multi-agent-workflow.ts
2934
+ var MultiAgentWorkflowRunner = class {
2935
+ executor;
2936
+ logger;
2937
+ config;
2938
+ constructor(executor, logger, config) {
2939
+ this.executor = executor;
2940
+ this.logger = logger;
2941
+ this.config = config;
2942
+ }
2943
+ isEnabled() {
2944
+ return this.config.enabled;
2945
+ }
2946
+ async run(input) {
2947
+ const startedAt = Date.now();
2948
+ const objective = input.objective.trim();
2949
+ if (!objective) {
2950
+ throw new Error("workflow objective cannot be empty.");
2951
+ }
2952
+ const maxRepairRounds = Math.max(0, this.config.autoRepairMaxRounds);
2953
+ let plannerSessionId = null;
2954
+ let executorSessionId = null;
2955
+ let reviewerSessionId = null;
2956
+ let activeHandle = null;
2957
+ let cancelled = false;
2958
+ input.onRegisterCancel?.(() => {
2959
+ cancelled = true;
2960
+ activeHandle?.cancel();
2961
+ });
2962
+ await emitProgress(input, {
2963
+ stage: "planner",
2964
+ round: 0,
2965
+ message: "Planner \u6B63\u5728\u751F\u6210\u6267\u884C\u8BA1\u5212"
2966
+ });
2967
+ const planResult = await this.executeRole(
2968
+ "planner",
2969
+ buildPlannerPrompt(objective),
2970
+ plannerSessionId,
2971
+ input.workdir,
2972
+ () => cancelled,
2973
+ (handle) => {
2974
+ activeHandle = handle;
2975
+ }
2976
+ );
2977
+ plannerSessionId = planResult.sessionId;
2978
+ const plan = planResult.reply;
2979
+ await emitProgress(input, {
2980
+ stage: "executor",
2981
+ round: 0,
2982
+ message: "Executor \u6B63\u5728\u6839\u636E\u8BA1\u5212\u6267\u884C\u4EFB\u52A1"
2983
+ });
2984
+ let outputResult = await this.executeRole(
2985
+ "executor",
2986
+ buildExecutorPrompt(objective, plan),
2987
+ executorSessionId,
2988
+ input.workdir,
2989
+ () => cancelled,
2990
+ (handle) => {
2991
+ activeHandle = handle;
2992
+ }
2993
+ );
2994
+ executorSessionId = outputResult.sessionId;
2995
+ let finalReviewReply = "";
2996
+ let approved = false;
2997
+ let repairRounds = 0;
2998
+ for (let attempt = 0; attempt <= maxRepairRounds; attempt += 1) {
2999
+ await emitProgress(input, {
3000
+ stage: "reviewer",
3001
+ round: attempt,
3002
+ message: `Reviewer \u6B63\u5728\u8FDB\u884C\u8D28\u91CF\u5BA1\u67E5\uFF08round ${attempt + 1}\uFF09`
3003
+ });
3004
+ const reviewResult = await this.executeRole(
3005
+ "reviewer",
3006
+ buildReviewerPrompt(objective, plan, outputResult.reply),
3007
+ reviewerSessionId,
3008
+ input.workdir,
3009
+ () => cancelled,
3010
+ (handle) => {
3011
+ activeHandle = handle;
3012
+ }
3013
+ );
3014
+ reviewerSessionId = reviewResult.sessionId;
3015
+ finalReviewReply = reviewResult.reply;
3016
+ const verdict = parseReviewerVerdict(finalReviewReply);
3017
+ if (verdict.approved) {
3018
+ approved = true;
3019
+ break;
3020
+ }
3021
+ if (attempt >= maxRepairRounds) {
3022
+ break;
3023
+ }
3024
+ repairRounds = attempt + 1;
3025
+ await emitProgress(input, {
3026
+ stage: "repair",
3027
+ round: repairRounds,
3028
+ message: `Executor \u6B63\u5728\u6309 Reviewer \u53CD\u9988\u8FDB\u884C\u4FEE\u590D\uFF08round ${repairRounds}\uFF09`
3029
+ });
3030
+ outputResult = await this.executeRole(
3031
+ "executor",
3032
+ buildRepairPrompt(objective, plan, outputResult.reply, verdict.feedback, repairRounds),
3033
+ executorSessionId,
3034
+ input.workdir,
3035
+ () => cancelled,
3036
+ (handle) => {
3037
+ activeHandle = handle;
3038
+ }
3039
+ );
3040
+ executorSessionId = outputResult.sessionId;
3041
+ }
3042
+ const durationMs = Date.now() - startedAt;
3043
+ this.logger.info("Multi-agent workflow finished", {
3044
+ objective,
3045
+ approved,
3046
+ repairRounds,
3047
+ durationMs
3048
+ });
3049
+ return {
3050
+ objective,
3051
+ plan,
3052
+ output: outputResult.reply,
3053
+ review: finalReviewReply,
3054
+ approved,
3055
+ repairRounds,
3056
+ durationMs
3057
+ };
3058
+ }
3059
+ async executeRole(role, prompt, sessionId, workdir, getCancelled, setActiveHandle) {
3060
+ if (getCancelled()) {
3061
+ throw new CodexExecutionCancelledError("workflow cancelled");
3062
+ }
3063
+ const handle = this.executor.startExecution(prompt, sessionId, void 0, { workdir });
3064
+ setActiveHandle(handle);
3065
+ try {
3066
+ const result = await handle.result;
3067
+ return {
3068
+ sessionId: result.sessionId,
3069
+ reply: result.reply
3070
+ };
3071
+ } finally {
3072
+ setActiveHandle(null);
3073
+ }
3074
+ }
3075
+ };
3076
+ async function emitProgress(input, event) {
3077
+ if (!input.onProgress) {
3078
+ return;
3079
+ }
3080
+ await input.onProgress(event);
3081
+ }
3082
+ function buildPlannerPrompt(objective) {
3083
+ return [
3084
+ "[role:planner]",
3085
+ "\u4F60\u662F\u8F6F\u4EF6\u4EA4\u4ED8\u89C4\u5212\u4EE3\u7406\u3002\u8BF7\u57FA\u4E8E\u76EE\u6807\u7ED9\u51FA\u53EF\u6267\u884C\u8BA1\u5212\u3002",
3086
+ "\u8F93\u51FA\u8981\u6C42\uFF1A",
3087
+ "1. \u4EFB\u52A1\u62C6\u89E3\uFF083-7 \u6B65\uFF09",
3088
+ "2. \u6BCF\u6B65\u8F93\u5165/\u8F93\u51FA",
3089
+ "3. \u98CE\u9669\u4E0E\u56DE\u9000\u65B9\u6848",
3090
+ "",
3091
+ `\u76EE\u6807\uFF1A${objective}`
3092
+ ].join("\n");
3093
+ }
3094
+ function buildExecutorPrompt(objective, plan) {
3095
+ return [
3096
+ "[role:executor]",
3097
+ "\u4F60\u662F\u8F6F\u4EF6\u6267\u884C\u4EE3\u7406\u3002\u8BF7\u6839\u636E\u8BA1\u5212\u5B8C\u6210\u4EA4\u4ED8\u5185\u5BB9\u3002",
3098
+ "\u8F93\u51FA\u8981\u6C42\uFF1A",
3099
+ "1. \u76F4\u63A5\u7ED9\u51FA\u6700\u7EC8\u53EF\u6267\u884C\u7ED3\u679C",
3100
+ "2. \u8BF4\u660E\u4F60\u5B9E\u9645\u5B8C\u6210\u4E86\u54EA\u4E9B\u6B65\u9AA4",
3101
+ "3. \u5982\u679C\u9700\u8981\u6587\u4EF6\u843D\u76D8\uFF0C\u8BF7\u7ED9\u51FA\u7EDD\u5BF9\u8DEF\u5F84",
3102
+ "",
3103
+ `\u76EE\u6807\uFF1A${objective}`,
3104
+ "",
3105
+ "[planner_plan]",
3106
+ plan,
3107
+ "[/planner_plan]"
3108
+ ].join("\n");
3109
+ }
3110
+ function buildReviewerPrompt(objective, plan, output) {
3111
+ return [
3112
+ "[role:reviewer]",
3113
+ "\u4F60\u662F\u8D28\u91CF\u5BA1\u67E5\u4EE3\u7406\u3002\u8BF7\u4E25\u683C\u5BA1\u67E5\u6267\u884C\u7ED3\u679C\u662F\u5426\u8FBE\u6210\u76EE\u6807\u3002",
3114
+ "\u8F93\u51FA\u683C\u5F0F\u5FC5\u987B\u5305\u542B\u4EE5\u4E0B\u5B57\u6BB5\uFF1A",
3115
+ "VERDICT: APPROVED \u6216 REJECTED",
3116
+ "SUMMARY: \u4E00\u53E5\u8BDD\u603B\u7ED3",
3117
+ "ISSUES:",
3118
+ "- issue 1",
3119
+ "- issue 2",
3120
+ "SUGGESTIONS:",
3121
+ "- suggestion 1",
3122
+ "- suggestion 2",
3123
+ "",
3124
+ `\u76EE\u6807\uFF1A${objective}`,
3125
+ "",
3126
+ "[planner_plan]",
3127
+ plan,
3128
+ "[/planner_plan]",
3129
+ "",
3130
+ "[executor_output]",
3131
+ output,
3132
+ "[/executor_output]"
3133
+ ].join("\n");
3134
+ }
3135
+ function buildRepairPrompt(objective, plan, previousOutput, reviewerFeedback, round) {
3136
+ return [
3137
+ "[role:executor]",
3138
+ `\u4F60\u662F\u8F6F\u4EF6\u6267\u884C\u4EE3\u7406\u3002\u8BF7\u6839\u636E\u5BA1\u67E5\u53CD\u9988\u8FDB\u884C\u7B2C ${round} \u8F6E\u4FEE\u590D\u5E76\u8F93\u51FA\u6700\u7EC8\u7248\u672C\u3002`,
3139
+ "\u8981\u6C42\uFF1A\u4FDD\u6301\u6B63\u786E\u5185\u5BB9\uFF0C\u4FEE\u590D\u95EE\u9898\uFF0C\u4E0D\u8981\u4E22\u5931\u5DF2\u5B8C\u6210\u90E8\u5206\u3002",
3140
+ "",
3141
+ `\u76EE\u6807\uFF1A${objective}`,
3142
+ "",
3143
+ "[planner_plan]",
3144
+ plan,
3145
+ "[/planner_plan]",
3146
+ "",
3147
+ "[previous_output]",
3148
+ previousOutput,
3149
+ "[/previous_output]",
3150
+ "",
3151
+ "[reviewer_feedback]",
3152
+ reviewerFeedback,
3153
+ "[/reviewer_feedback]"
3154
+ ].join("\n");
3155
+ }
3156
+ function parseReviewerVerdict(review) {
3157
+ const approved = /\bVERDICT\s*:\s*APPROVED\b/i.test(review);
3158
+ const rejected = /\bVERDICT\s*:\s*REJECTED\b/i.test(review);
3159
+ if (approved) {
3160
+ return { approved: true, feedback: review };
3161
+ }
3162
+ if (rejected) {
3163
+ return { approved: false, feedback: review };
3164
+ }
3165
+ return {
3166
+ approved: false,
3167
+ feedback: review.trim() || "Reviewer \u672A\u8FD4\u56DE\u89C4\u8303 verdict\uFF0C\u9ED8\u8BA4\u6309 REJECTED \u5904\u7406\u3002"
3168
+ };
3169
+ }
3170
+ function parseWorkflowCommand(text) {
3171
+ const normalized = text.trim();
3172
+ if (!normalized.startsWith("/agents")) {
3173
+ return null;
3174
+ }
3175
+ const parts = normalized.split(/\s+/);
3176
+ if (parts.length === 1 || parts[1]?.toLowerCase() === "status") {
3177
+ return { kind: "status" };
3178
+ }
3179
+ if (parts[1]?.toLowerCase() !== "run") {
3180
+ return null;
3181
+ }
3182
+ const objective = normalized.replace(/^\/agents\s+run\s*/i, "").trim();
3183
+ return {
3184
+ kind: "run",
3185
+ objective
3186
+ };
3187
+ }
3188
+ function createIdleWorkflowSnapshot() {
3189
+ return {
3190
+ state: "idle",
3191
+ startedAt: null,
3192
+ endedAt: null,
3193
+ objective: null,
3194
+ approved: null,
3195
+ repairRounds: 0,
3196
+ error: null
3197
+ };
3198
+ }
3199
+
2883
3200
  // src/orchestrator.ts
2884
3201
  var RequestMetrics = class {
2885
3202
  total = 0;
@@ -2965,6 +3282,8 @@ var Orchestrator = class {
2965
3282
  rateLimiter;
2966
3283
  cliCompat;
2967
3284
  cliCompatRecorder;
3285
+ workflowRunner;
3286
+ workflowSnapshots = /* @__PURE__ */ new Map();
2968
3287
  metrics = new RequestMetrics();
2969
3288
  lastLockPruneAt = 0;
2970
3289
  constructor(channel, executor, stateStore, logger, options) {
@@ -3011,6 +3330,10 @@ var Orchestrator = class {
3011
3330
  maxConcurrentPerRoom: 4
3012
3331
  }
3013
3332
  );
3333
+ this.workflowRunner = new MultiAgentWorkflowRunner(this.executor, this.logger, {
3334
+ enabled: options?.multiAgentWorkflow?.enabled ?? false,
3335
+ autoRepairMaxRounds: options?.multiAgentWorkflow?.autoRepairMaxRounds ?? 1
3336
+ });
3014
3337
  this.sessionRuntime = new CodexSessionRuntime(this.executor);
3015
3338
  }
3016
3339
  async handleMessage(message) {
@@ -3054,6 +3377,12 @@ var Orchestrator = class {
3054
3377
  this.stateStore.markEventProcessed(sessionKey, message.eventId);
3055
3378
  return;
3056
3379
  }
3380
+ const workflowCommand = this.workflowRunner.isEnabled() ? parseWorkflowCommand(route.prompt) : null;
3381
+ if (workflowCommand?.kind === "status") {
3382
+ await this.handleWorkflowStatusCommand(sessionKey, message);
3383
+ this.stateStore.markEventProcessed(sessionKey, message.eventId);
3384
+ return;
3385
+ }
3057
3386
  const rateDecision = this.rateLimiter.tryAcquire({
3058
3387
  userId: message.senderId,
3059
3388
  roomId: message.conversationId
@@ -3071,6 +3400,37 @@ var Orchestrator = class {
3071
3400
  });
3072
3401
  return;
3073
3402
  }
3403
+ if (workflowCommand?.kind === "run") {
3404
+ const executionStartedAt = Date.now();
3405
+ let sendDurationMs2 = 0;
3406
+ this.stateStore.activateSession(sessionKey, this.sessionActiveWindowMs);
3407
+ try {
3408
+ const sendStartedAt = Date.now();
3409
+ await this.handleWorkflowRunCommand(
3410
+ workflowCommand.objective,
3411
+ sessionKey,
3412
+ message,
3413
+ requestId,
3414
+ roomConfig.workdir
3415
+ );
3416
+ sendDurationMs2 += Date.now() - sendStartedAt;
3417
+ this.stateStore.markEventProcessed(sessionKey, message.eventId);
3418
+ this.metrics.record("success", queueWaitMs, Date.now() - executionStartedAt, sendDurationMs2);
3419
+ } catch (error) {
3420
+ sendDurationMs2 += await this.sendWorkflowFailure(message.conversationId, error);
3421
+ this.stateStore.commitExecutionHandled(sessionKey, message.eventId);
3422
+ const status = classifyExecutionOutcome(error);
3423
+ this.metrics.record(status, queueWaitMs, Date.now() - executionStartedAt, sendDurationMs2);
3424
+ this.logger.error("Workflow request failed", {
3425
+ requestId,
3426
+ sessionKey,
3427
+ error: formatError2(error)
3428
+ });
3429
+ } finally {
3430
+ rateDecision.release?.();
3431
+ }
3432
+ return;
3433
+ }
3074
3434
  this.stateStore.activateSession(sessionKey, this.sessionActiveWindowMs);
3075
3435
  const previousCodexSessionId = this.stateStore.getCodexSessionId(sessionKey);
3076
3436
  const executionPrompt = this.buildExecutionPrompt(route.prompt, message);
@@ -3291,6 +3651,7 @@ var Orchestrator = class {
3291
3651
  const metrics = this.metrics.snapshot(this.runningExecutions.size);
3292
3652
  const limiter = this.rateLimiter.snapshot();
3293
3653
  const runtime = this.sessionRuntime.getRuntimeStats();
3654
+ const workflow = this.workflowSnapshots.get(sessionKey) ?? createIdleWorkflowSnapshot();
3294
3655
  await this.channel.sendNotice(
3295
3656
  message.conversationId,
3296
3657
  `[CodeHarbor] \u5F53\u524D\u72B6\u6001
@@ -3303,9 +3664,122 @@ var Orchestrator = class {
3303
3664
  - \u6307\u6807: total=${metrics.total}, success=${metrics.success}, failed=${metrics.failed}, timeout=${metrics.timeout}, cancelled=${metrics.cancelled}, rate_limited=${metrics.rateLimited}
3304
3665
  - \u5E73\u5747\u8017\u65F6: queue=${metrics.avgQueueMs}ms, exec=${metrics.avgExecMs}ms, send=${metrics.avgSendMs}ms
3305
3666
  - \u9650\u6D41\u5E76\u53D1: global=${limiter.activeGlobal}, users=${limiter.activeUsers}, rooms=${limiter.activeRooms}
3306
- - CLI runtime: workers=${runtime.workerCount}, running=${runtime.runningCount}, compat_mode=${this.cliCompat.enabled ? "on" : "off"}`
3667
+ - CLI runtime: workers=${runtime.workerCount}, running=${runtime.runningCount}, compat_mode=${this.cliCompat.enabled ? "on" : "off"}
3668
+ - Multi-Agent workflow: enabled=${this.workflowRunner.isEnabled() ? "on" : "off"}, state=${workflow.state}`
3307
3669
  );
3308
3670
  }
3671
+ async handleWorkflowStatusCommand(sessionKey, message) {
3672
+ const snapshot = this.workflowSnapshots.get(sessionKey) ?? createIdleWorkflowSnapshot();
3673
+ await this.channel.sendNotice(
3674
+ message.conversationId,
3675
+ `[CodeHarbor] Multi-Agent \u5DE5\u4F5C\u6D41\u72B6\u6001
3676
+ - state: ${snapshot.state}
3677
+ - startedAt: ${snapshot.startedAt ?? "N/A"}
3678
+ - endedAt: ${snapshot.endedAt ?? "N/A"}
3679
+ - objective: ${snapshot.objective ?? "N/A"}
3680
+ - approved: ${snapshot.approved === null ? "N/A" : snapshot.approved ? "yes" : "no"}
3681
+ - repairRounds: ${snapshot.repairRounds}
3682
+ - error: ${snapshot.error ?? "N/A"}`
3683
+ );
3684
+ }
3685
+ async handleWorkflowRunCommand(objective, sessionKey, message, requestId, workdir) {
3686
+ const normalizedObjective = objective.trim();
3687
+ if (!normalizedObjective) {
3688
+ await this.channel.sendNotice(message.conversationId, "[CodeHarbor] /agents run \u9700\u8981\u63D0\u4F9B\u4EFB\u52A1\u76EE\u6807\u3002");
3689
+ return;
3690
+ }
3691
+ const requestStartedAt = Date.now();
3692
+ let progressNoticeEventId = null;
3693
+ const progressCtx = {
3694
+ conversationId: message.conversationId,
3695
+ isDirectMessage: message.isDirectMessage,
3696
+ getProgressNoticeEventId: () => progressNoticeEventId,
3697
+ setProgressNoticeEventId: (next) => {
3698
+ progressNoticeEventId = next;
3699
+ }
3700
+ };
3701
+ const startedAtIso = (/* @__PURE__ */ new Date()).toISOString();
3702
+ this.workflowSnapshots.set(sessionKey, {
3703
+ state: "running",
3704
+ startedAt: startedAtIso,
3705
+ endedAt: null,
3706
+ objective: normalizedObjective,
3707
+ approved: null,
3708
+ repairRounds: 0,
3709
+ error: null
3710
+ });
3711
+ const stopTyping = this.startTypingHeartbeat(message.conversationId);
3712
+ let cancelWorkflow = () => {
3713
+ };
3714
+ let cancelRequested = false;
3715
+ this.runningExecutions.set(sessionKey, {
3716
+ requestId,
3717
+ startedAt: requestStartedAt,
3718
+ cancel: () => {
3719
+ cancelRequested = true;
3720
+ cancelWorkflow();
3721
+ }
3722
+ });
3723
+ await this.sendProgressUpdate(progressCtx, "[CodeHarbor] Multi-Agent workflow \u542F\u52A8\uFF1APlanner -> Executor -> Reviewer");
3724
+ try {
3725
+ const result = await this.workflowRunner.run({
3726
+ objective: normalizedObjective,
3727
+ workdir,
3728
+ onRegisterCancel: (cancel) => {
3729
+ cancelWorkflow = cancel;
3730
+ if (cancelRequested) {
3731
+ cancelWorkflow();
3732
+ }
3733
+ },
3734
+ onProgress: async (event) => {
3735
+ const stepLabel = event.stage.toUpperCase();
3736
+ await this.sendProgressUpdate(progressCtx, `[CodeHarbor] [${stepLabel}] ${event.message}`);
3737
+ }
3738
+ });
3739
+ const endedAtIso = (/* @__PURE__ */ new Date()).toISOString();
3740
+ this.workflowSnapshots.set(sessionKey, {
3741
+ state: "succeeded",
3742
+ startedAt: startedAtIso,
3743
+ endedAt: endedAtIso,
3744
+ objective: normalizedObjective,
3745
+ approved: result.approved,
3746
+ repairRounds: result.repairRounds,
3747
+ error: null
3748
+ });
3749
+ await this.channel.sendMessage(message.conversationId, buildWorkflowResultReply(result));
3750
+ await this.finishProgress(progressCtx, `\u591A\u667A\u80FD\u4F53\u6D41\u7A0B\u5B8C\u6210\uFF08\u8017\u65F6 ${formatDurationMs(Date.now() - requestStartedAt)}\uFF09`);
3751
+ } catch (error) {
3752
+ const status = classifyExecutionOutcome(error);
3753
+ const endedAtIso = (/* @__PURE__ */ new Date()).toISOString();
3754
+ this.workflowSnapshots.set(sessionKey, {
3755
+ state: status === "cancelled" ? "idle" : "failed",
3756
+ startedAt: startedAtIso,
3757
+ endedAt: endedAtIso,
3758
+ objective: normalizedObjective,
3759
+ approved: null,
3760
+ repairRounds: 0,
3761
+ error: formatError2(error)
3762
+ });
3763
+ await this.finishProgress(progressCtx, buildFailureProgressSummary(status, requestStartedAt, error));
3764
+ throw error;
3765
+ } finally {
3766
+ const running = this.runningExecutions.get(sessionKey);
3767
+ if (running?.requestId === requestId) {
3768
+ this.runningExecutions.delete(sessionKey);
3769
+ }
3770
+ await stopTyping();
3771
+ }
3772
+ }
3773
+ async sendWorkflowFailure(conversationId, error) {
3774
+ const startedAt = Date.now();
3775
+ const status = classifyExecutionOutcome(error);
3776
+ if (status === "cancelled") {
3777
+ await this.channel.sendNotice(conversationId, "[CodeHarbor] Multi-Agent workflow \u5DF2\u53D6\u6D88\u3002");
3778
+ return Date.now() - startedAt;
3779
+ }
3780
+ await this.channel.sendMessage(conversationId, `[CodeHarbor] Multi-Agent workflow \u5931\u8D25: ${formatError2(error)}`);
3781
+ return Date.now() - startedAt;
3782
+ }
3309
3783
  async handleStopCommand(sessionKey, message, requestId) {
3310
3784
  this.stateStore.deactivateSession(sessionKey);
3311
3785
  this.stateStore.clearCodexSessionId(sessionKey);
@@ -3619,6 +4093,25 @@ function buildFailureProgressSummary(status, startedAt, error) {
3619
4093
  }
3620
4094
  return `\u5904\u7406\u5931\u8D25\uFF08\u8017\u65F6 ${elapsed}\uFF09: ${formatError2(error)}`;
3621
4095
  }
4096
+ function buildWorkflowResultReply(result) {
4097
+ return `[CodeHarbor] Multi-Agent workflow \u5B8C\u6210
4098
+ - objective: ${result.objective}
4099
+ - approved: ${result.approved ? "yes" : "no"}
4100
+ - repairRounds: ${result.repairRounds}
4101
+ - duration: ${formatDurationMs(result.durationMs)}
4102
+
4103
+ [planner]
4104
+ ${result.plan}
4105
+ [/planner]
4106
+
4107
+ [executor]
4108
+ ${result.output}
4109
+ [/executor]
4110
+
4111
+ [reviewer]
4112
+ ${result.review}
4113
+ [/reviewer]`;
4114
+ }
3622
4115
 
3623
4116
  // src/store/state-store.ts
3624
4117
  var import_node_fs5 = __toESM(require("fs"));
@@ -4061,6 +4554,7 @@ var CodeHarborApp = class {
4061
4554
  roomTriggerPolicies: config.roomTriggerPolicies,
4062
4555
  rateLimiterOptions: config.rateLimiter,
4063
4556
  cliCompat: config.cliCompat,
4557
+ multiAgentWorkflow: config.agentWorkflow,
4064
4558
  configService: this.configService,
4065
4559
  defaultCodexWorkdir: config.codexWorkdir
4066
4560
  });
@@ -4185,6 +4679,8 @@ var configSchema = import_zod.z.object({
4185
4679
  CODEX_APPROVAL_POLICY: import_zod.z.string().optional(),
4186
4680
  CODEX_EXTRA_ARGS: import_zod.z.string().default(""),
4187
4681
  CODEX_EXTRA_ENV_JSON: import_zod.z.string().default(""),
4682
+ AGENT_WORKFLOW_ENABLED: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
4683
+ AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS: import_zod.z.string().default("1").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().min(0).max(10)),
4188
4684
  STATE_DB_PATH: import_zod.z.string().default("data/state.db"),
4189
4685
  STATE_PATH: import_zod.z.string().default("data/state.json"),
4190
4686
  MAX_PROCESSED_EVENTS_PER_SESSION: import_zod.z.string().default("200").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
@@ -4234,6 +4730,10 @@ var configSchema = import_zod.z.object({
4234
4730
  codexApprovalPolicy: v.CODEX_APPROVAL_POLICY?.trim() || null,
4235
4731
  codexExtraArgs: parseExtraArgs(v.CODEX_EXTRA_ARGS),
4236
4732
  codexExtraEnv: parseExtraEnv(v.CODEX_EXTRA_ENV_JSON),
4733
+ agentWorkflow: {
4734
+ enabled: v.AGENT_WORKFLOW_ENABLED,
4735
+ autoRepairMaxRounds: v.AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS
4736
+ },
4237
4737
  stateDbPath: import_node_path7.default.resolve(v.STATE_DB_PATH),
4238
4738
  legacyStateJsonPath: v.STATE_PATH.trim() ? import_node_path7.default.resolve(v.STATE_PATH) : null,
4239
4739
  maxProcessedEventsPerSession: v.MAX_PROCESSED_EVENTS_PER_SESSION,
@@ -4382,6 +4882,8 @@ var CONFIG_SNAPSHOT_ENV_KEYS = [
4382
4882
  "CODEX_APPROVAL_POLICY",
4383
4883
  "CODEX_EXTRA_ARGS",
4384
4884
  "CODEX_EXTRA_ENV_JSON",
4885
+ "AGENT_WORKFLOW_ENABLED",
4886
+ "AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS",
4385
4887
  "STATE_DB_PATH",
4386
4888
  "STATE_PATH",
4387
4889
  "MAX_PROCESSED_EVENTS_PER_SESSION",
@@ -4444,6 +4946,8 @@ var envSnapshotSchema = import_zod2.z.object({
4444
4946
  CODEX_APPROVAL_POLICY: import_zod2.z.string(),
4445
4947
  CODEX_EXTRA_ARGS: import_zod2.z.string(),
4446
4948
  CODEX_EXTRA_ENV_JSON: jsonObjectStringSchema("CODEX_EXTRA_ENV_JSON", true),
4949
+ AGENT_WORKFLOW_ENABLED: booleanStringSchema("AGENT_WORKFLOW_ENABLED"),
4950
+ AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS: integerStringSchema("AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS", 0, 10),
4447
4951
  STATE_DB_PATH: import_zod2.z.string().min(1),
4448
4952
  STATE_PATH: import_zod2.z.string(),
4449
4953
  MAX_PROCESSED_EVENTS_PER_SESSION: integerStringSchema("MAX_PROCESSED_EVENTS_PER_SESSION", 1),
@@ -4630,6 +5134,8 @@ function buildSnapshotEnv(config) {
4630
5134
  CODEX_APPROVAL_POLICY: config.codexApprovalPolicy ?? "",
4631
5135
  CODEX_EXTRA_ARGS: config.codexExtraArgs.join(" "),
4632
5136
  CODEX_EXTRA_ENV_JSON: serializeJsonObject(config.codexExtraEnv),
5137
+ AGENT_WORKFLOW_ENABLED: String(config.agentWorkflow.enabled),
5138
+ AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS: String(config.agentWorkflow.autoRepairMaxRounds),
4633
5139
  STATE_DB_PATH: config.stateDbPath,
4634
5140
  STATE_PATH: config.legacyStateJsonPath ?? "",
4635
5141
  MAX_PROCESSED_EVENTS_PER_SESSION: String(config.maxProcessedEventsPerSession),
@@ -4988,7 +5494,7 @@ function buildMainServiceUnit(options) {
4988
5494
  "NoNewPrivileges=true",
4989
5495
  "PrivateTmp=true",
4990
5496
  "ProtectSystem=full",
4991
- "ProtectHome=read-only",
5497
+ "ProtectHome=false",
4992
5498
  `ReadWritePaths=${runtimeHome2}`,
4993
5499
  "",
4994
5500
  "[Install]",
@@ -5016,7 +5522,7 @@ function buildAdminServiceUnit(options) {
5016
5522
  "NoNewPrivileges=true",
5017
5523
  "PrivateTmp=true",
5018
5524
  "ProtectSystem=full",
5019
- "ProtectHome=read-only",
5525
+ "ProtectHome=false",
5020
5526
  `ReadWritePaths=${runtimeHome2}`,
5021
5527
  "",
5022
5528
  "[Install]",
@@ -5302,6 +5808,7 @@ configCommand.command("import").description("Import config snapshot from JSON").
5302
5808
  });
5303
5809
  serviceCommand.command("install").description("Install and enable codeharbor systemd service (requires root)").option("--run-user <user>", "service user (default: sudo user or current user)").option("--runtime-home <path>", "runtime home used as CODEHARBOR_HOME").option("--with-admin", "also install codeharbor-admin.service").option("--no-start", "enable service without starting immediately").action((options) => {
5304
5810
  try {
5811
+ maybeReexecServiceCommandWithSudo();
5305
5812
  const runUser = options.runUser?.trim() || resolveDefaultRunUser();
5306
5813
  const runtimeHomePath = resolveRuntimeHomeForUser(runUser, process.env, options.runtimeHome);
5307
5814
  installSystemdServices({
@@ -5317,9 +5824,10 @@ serviceCommand.command("install").description("Install and enable codeharbor sys
5317
5824
  `);
5318
5825
  process.stderr.write(
5319
5826
  [
5320
- "Hint: run with sudo and absolute CLI path, for example:",
5321
- ' sudo "$(command -v codeharbor)" service install --with-admin',
5322
- " (remove --with-admin if you only want the main service)",
5827
+ "Hint:",
5828
+ " - Run directly: codeharbor service install --with-admin",
5829
+ " - The command auto-elevates with sudo when needed.",
5830
+ ` - Fallback explicit form: ${buildExplicitSudoCommand("service install --with-admin")}`,
5323
5831
  ""
5324
5832
  ].join("\n")
5325
5833
  );
@@ -5328,6 +5836,7 @@ serviceCommand.command("install").description("Install and enable codeharbor sys
5328
5836
  });
5329
5837
  serviceCommand.command("uninstall").description("Remove codeharbor systemd service (requires root)").option("--with-admin", "also remove codeharbor-admin.service").action((options) => {
5330
5838
  try {
5839
+ maybeReexecServiceCommandWithSudo();
5331
5840
  uninstallSystemdServices({
5332
5841
  removeAdmin: options.withAdmin ?? false
5333
5842
  });
@@ -5336,8 +5845,10 @@ serviceCommand.command("uninstall").description("Remove codeharbor systemd servi
5336
5845
  `);
5337
5846
  process.stderr.write(
5338
5847
  [
5339
- "Hint: run with sudo and absolute CLI path, for example:",
5340
- ' sudo "$(command -v codeharbor)" service uninstall --with-admin',
5848
+ "Hint:",
5849
+ " - Run directly: codeharbor service uninstall --with-admin",
5850
+ " - The command auto-elevates with sudo when needed.",
5851
+ ` - Fallback explicit form: ${buildExplicitSudoCommand("service uninstall --with-admin")}`,
5341
5852
  ""
5342
5853
  ].join("\n")
5343
5854
  );
@@ -5346,6 +5857,7 @@ serviceCommand.command("uninstall").description("Remove codeharbor systemd servi
5346
5857
  });
5347
5858
  serviceCommand.command("restart").description("Restart installed codeharbor systemd service (requires root)").option("--with-admin", "also restart codeharbor-admin.service").action((options) => {
5348
5859
  try {
5860
+ maybeReexecServiceCommandWithSudo();
5349
5861
  restartSystemdServices({
5350
5862
  restartAdmin: options.withAdmin ?? false
5351
5863
  });
@@ -5354,9 +5866,10 @@ serviceCommand.command("restart").description("Restart installed codeharbor syst
5354
5866
  `);
5355
5867
  process.stderr.write(
5356
5868
  [
5357
- "Hint: run with sudo and absolute CLI path, for example:",
5358
- ' sudo "$(command -v codeharbor)" service restart --with-admin',
5359
- " (remove --with-admin if you only want the main service)",
5869
+ "Hint:",
5870
+ " - Run directly: codeharbor service restart --with-admin",
5871
+ " - The command auto-elevates with sudo when needed.",
5872
+ ` - Fallback explicit form: ${buildExplicitSudoCommand("service restart --with-admin")}`,
5360
5873
  ""
5361
5874
  ].join("\n")
5362
5875
  );
@@ -5439,6 +5952,32 @@ function resolveCliScriptPath() {
5439
5952
  }
5440
5953
  return import_node_path12.default.resolve(__dirname, "cli.js");
5441
5954
  }
5955
+ function maybeReexecServiceCommandWithSudo() {
5956
+ if (typeof process.getuid !== "function" || process.getuid() === 0) {
5957
+ return;
5958
+ }
5959
+ const serviceArgs = process.argv.slice(2);
5960
+ if (serviceArgs.length === 0 || serviceArgs[0] !== "service") {
5961
+ return;
5962
+ }
5963
+ const cliScriptPath = resolveCliScriptPath();
5964
+ const child = (0, import_node_child_process6.spawnSync)("sudo", [process.execPath, cliScriptPath, ...serviceArgs], {
5965
+ stdio: "inherit"
5966
+ });
5967
+ if (child.error) {
5968
+ throw new Error(`failed to auto-elevate with sudo: ${child.error.message}`);
5969
+ }
5970
+ process.exit(child.status ?? 1);
5971
+ }
5972
+ function shellQuote(value) {
5973
+ if (!value) {
5974
+ return "''";
5975
+ }
5976
+ return `'${value.replace(/'/g, `'\\''`)}'`;
5977
+ }
5978
+ function buildExplicitSudoCommand(subcommand) {
5979
+ return `sudo ${shellQuote(process.execPath)} ${shellQuote(resolveCliScriptPath())} ${subcommand}`;
5980
+ }
5442
5981
  function formatError3(error) {
5443
5982
  if (error instanceof Error) {
5444
5983
  return error.message;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeharbor",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Instant-messaging bridge for Codex CLI sessions",
5
5
  "license": "MIT",
6
6
  "main": "dist/cli.js",