orquesta-agent 0.2.240 → 0.2.242

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -102,7 +102,8 @@ async function setup() {
102
102
  console.log(' orquesta-agent ui --open');
103
103
  console.log('');
104
104
  console.log(' Then add your agents from the browser interface');
105
- console.log(' URL: http://localhost:8080');
105
+ console.log(' URL: http://localhost:8080 (if 8080 is taken the manager picks a');
106
+ console.log(' free port and prints the real URL on startup)');
106
107
  console.log('');
107
108
  console.log('Or connect directly from command line:');
108
109
  console.log(' orquesta-agent --token <your-token> --daemon');
@@ -141,6 +142,104 @@ async function refreshInjectedCredentials() {
141
142
  // Non-fatal: keep the previously injected credentials.
142
143
  }
143
144
  }
145
+ /**
146
+ * Commit assisted work and hand the server the raw facts: commit sha, branch,
147
+ * and `git diff --numstat` text.
148
+ *
149
+ * Shared by the TWO ways an assisted item gets worked — a dispatched prompt
150
+ * (the agent drove the CLI) and a claimed interactive session (a person sat at
151
+ * the terminal). Both finish identically: the server evaluates the gate and
152
+ * parks the item at `needs_review`. The split is the point — a gate computed on
153
+ * the machine being gated protects nobody, and this machine is the customer's.
154
+ *
155
+ * Best-effort throughout: a failure here must not fail work that already
156
+ * happened. The reviewer sees the item stuck in_progress and can nudge the run.
157
+ */
158
+ async function postAssistedReport(opts) {
159
+ try {
160
+ const commit = await gitCommitAndPush(opts.execDir, opts.commitMessage);
161
+ // With an explicit base we always diff, even when this call committed
162
+ // nothing — a person working interactively may well have committed by
163
+ // hand, and reporting an empty diff for real work would send the reviewer
164
+ // an item that looks like a no-op.
165
+ const base = opts.diffBase && opts.diffBase !== opts.branch
166
+ ? opts.diffBase
167
+ : (commit.sha ? 'HEAD~1' : null);
168
+ const numstat = base ? gitNumstatSince(opts.execDir, base) : '';
169
+ let sha = commit.sha;
170
+ if (!sha && base) {
171
+ try {
172
+ sha = execSync('git rev-parse HEAD', { cwd: opts.execDir, stdio: 'pipe' }).toString().trim() || null;
173
+ }
174
+ catch { /* leave null — the server treats a missing sha as "no commit" */ }
175
+ }
176
+ const apiUrl = process.env.ORQUESTA_API_URL || 'https://getorquesta.com';
177
+ const res = await fetch(`${apiUrl}/api/agent/assisted-report`, {
178
+ method: 'POST',
179
+ headers: { 'Content-Type': 'application/json' },
180
+ body: JSON.stringify({
181
+ token: opts.token,
182
+ promptId: opts.promptId,
183
+ commitSha: sha,
184
+ branch: opts.branch,
185
+ numstat,
186
+ }),
187
+ });
188
+ if (!res.ok) {
189
+ logger.warn(`[assisted] report rejected (${res.status}) for item ${opts.itemId}`);
190
+ return;
191
+ }
192
+ logger.success(`[assisted] reported ${sha ? sha.slice(0, 8) : 'no commit'} on ${opts.branch}`);
193
+ }
194
+ catch (err) {
195
+ logger.warn(`[assisted] report failed: ${err}`);
196
+ }
197
+ }
198
+ /**
199
+ * Prepare a claimed assisted item for an interactive session: put the checkout
200
+ * on the run's branch and remember where it started.
201
+ *
202
+ * Returns the envelope the executor carries for the life of the session, or
203
+ * undefined when this session is not part of an assisted run.
204
+ */
205
+ function prepareAssistedSession(assisted, sessDir) {
206
+ if (!assisted?.branch)
207
+ return undefined;
208
+ if (!fs.existsSync(path.join(sessDir, '.git'))) {
209
+ logger.warn(`[assisted] ${sessDir} is not a git checkout — session runs without branch isolation`);
210
+ return assisted;
211
+ }
212
+ try {
213
+ execSync(`git checkout ${assisted.branch} 2>/dev/null || git checkout -b ${assisted.branch}`, { cwd: sessDir, stdio: 'pipe' });
214
+ logger.info(`[assisted] Checked out branch: ${assisted.branch}`);
215
+ }
216
+ catch (err) {
217
+ logger.warn(`[assisted] Branch checkout failed: ${err}`);
218
+ }
219
+ let baseSha;
220
+ try {
221
+ baseSha = execSync('git rev-parse HEAD', { cwd: sessDir, stdio: 'pipe' }).toString().trim() || undefined;
222
+ }
223
+ catch { /* a branch with no commits yet — HEAD~1 fallback covers it */ }
224
+ return { ...assisted, baseSha };
225
+ }
226
+ /** Report a finished interactive session that was working a claimed item. */
227
+ async function reportAssistedSession(info, token) {
228
+ const assisted = info.assisted;
229
+ if (!assisted)
230
+ return;
231
+ if (!fs.existsSync(path.join(info.workingDirectory, '.git')))
232
+ return;
233
+ await postAssistedReport({
234
+ token,
235
+ promptId: assisted.promptId,
236
+ itemId: assisted.itemId,
237
+ execDir: info.workingDirectory,
238
+ branch: assisted.branch,
239
+ diffBase: assisted.baseSha || null,
240
+ commitMessage: `assisted: interactive session for item ${assisted.itemId.slice(0, 8)}`,
241
+ });
242
+ }
144
243
  async function connect(options) {
145
244
  logger.banner(VERSION);
146
245
  // Single-instance guard for DIRECT runs (e.g. `npx -y orquesta-agent` with no
@@ -544,7 +643,9 @@ async function connect(options) {
544
643
  executePromptSequentialOSS(next);
545
644
  }
546
645
  }
547
- setOnSessionEnded(() => {
646
+ setOnSessionEnded((info) => {
647
+ if (info?.assisted && token)
648
+ void reportAssistedSession(info, token);
548
649
  if (promptQueue.length > 0) {
549
650
  logger.info(`Session ended — draining ${promptQueue.length} queued prompt(s)`);
550
651
  drainQueueOSS();
@@ -600,6 +701,8 @@ async function connect(options) {
600
701
  else
601
702
  logger.warn(`session:start requested dir "${rawDir}" does not exist — falling back to ${workingDirectory}`);
602
703
  }
704
+ // A claimed assisted item pins the session to the run's branch.
705
+ const assisted = prepareAssistedSession(payload.assisted, sessDir);
603
706
  // Pick up credentials created/rotated since startup before spawning.
604
707
  void refreshInjectedCredentials().finally(() => {
605
708
  startSession({
@@ -611,6 +714,7 @@ async function connect(options) {
611
714
  subAgentId: payload.subAgentId,
612
715
  subAgentName: payload.subAgentName,
613
716
  resume: payload.resume,
717
+ assisted,
614
718
  });
615
719
  });
616
720
  },
@@ -691,47 +795,20 @@ async function connect(options) {
691
795
  return promptQueue.some(q => q.promptId === promptId) ||
692
796
  [...runningExecutions.values()].some(e => e.promptId === promptId);
693
797
  }
694
- /**
695
- * Commit the assisted item's work and hand the server the raw facts.
696
- *
697
- * Facts only: the commit sha, the branch, and `git diff --numstat` text.
698
- * The server evaluates the gate. That split is the point — a gate computed
699
- * on the machine being gated protects nobody, and this machine belongs to
700
- * the customer.
701
- *
702
- * Best-effort throughout: a failure here must not fail the prompt, which
703
- * has already produced real work. The reviewer sees the item stuck
704
- * in_progress and can nudge the run instead.
705
- */
798
+ /** Report a finished dispatched prompt. See `postAssistedReport`. */
706
799
  async function reportAssistedResult(payload, execDir, baseBranch) {
707
800
  const assisted = payload.assisted;
708
- try {
709
- const commit = await gitCommitAndPush(execDir, `assisted: ${payload.content.split('\n')[0].slice(0, 120)}`);
710
- // Diff against the branch we started from when we know it; otherwise
711
- // the last commit only, which is what the item just produced anyway.
712
- const base = baseBranch && baseBranch !== assisted.branch ? baseBranch : 'HEAD~1';
713
- const numstat = commit.sha ? gitNumstatSince(execDir, base) : '';
714
- const apiUrl = process.env.ORQUESTA_API_URL || 'https://getorquesta.com';
715
- const res = await fetch(`${apiUrl}/api/agent/assisted-report`, {
716
- method: 'POST',
717
- headers: { 'Content-Type': 'application/json' },
718
- body: JSON.stringify({
719
- token,
720
- promptId: payload.promptId,
721
- commitSha: commit.sha,
722
- branch: assisted.branch,
723
- numstat,
724
- }),
725
- });
726
- if (!res.ok) {
727
- logger.warn(`[assisted] report rejected (${res.status}) for item ${assisted.itemId}`);
728
- return;
729
- }
730
- logger.success(`[assisted] reported ${commit.sha ? commit.sha.slice(0, 8) : 'no commit'} on ${assisted.branch}`);
731
- }
732
- catch (err) {
733
- logger.warn(`[assisted] report failed: ${err}`);
734
- }
801
+ if (!token)
802
+ return;
803
+ await postAssistedReport({
804
+ token,
805
+ promptId: payload.promptId,
806
+ itemId: assisted.itemId,
807
+ execDir,
808
+ branch: assisted.branch,
809
+ diffBase: baseBranch,
810
+ commitMessage: `assisted: ${payload.content.split('\n')[0].slice(0, 120)}`,
811
+ });
735
812
  }
736
813
  async function executePrompt(payload) {
737
814
  const execId = payload.id;
@@ -853,8 +930,12 @@ async function connect(options) {
853
930
  executePrompt(next); // Fire and forget — runs concurrently
854
931
  }
855
932
  }
856
- // When an interactive session ends, drain any prompts that were queued during it
857
- setOnSessionEnded(() => {
933
+ // When an interactive session ends, report it if it was a claimed assisted
934
+ // item (the person's diff goes to review like any agent-produced one), then
935
+ // drain any prompts that were queued during it.
936
+ setOnSessionEnded((info) => {
937
+ if (info?.assisted && token)
938
+ void reportAssistedSession(info, token);
858
939
  if (promptQueue.length > 0) {
859
940
  logger.info(`Session ended — draining ${promptQueue.length} queued prompt(s)`);
860
941
  drainQueue();
@@ -1060,6 +1141,11 @@ async function connect(options) {
1060
1141
  logger.warn(`[workstream:${subCfg.name}] Branch checkout failed: ${err}`);
1061
1142
  }
1062
1143
  }
1144
+ // A claimed assisted item pins the session to the run's branch. It runs
1145
+ // AFTER the workstream checkout above so an assisted claim wins — the
1146
+ // two are never combined in the UI, and the run's isolation is the
1147
+ // whole reason the branch exists.
1148
+ const assisted = prepareAssistedSession(payload.assisted, sessDir);
1063
1149
  // Pick up credentials created/rotated since startup before spawning.
1064
1150
  void refreshInjectedCredentials().finally(() => {
1065
1151
  startSession({
@@ -1072,6 +1158,7 @@ async function connect(options) {
1072
1158
  subAgentName: payload.subAgentName || subCfg?.name,
1073
1159
  resume: payload.resume,
1074
1160
  resumeSessionId: payload.resumeSessionId,
1161
+ assisted,
1075
1162
  });
1076
1163
  });
1077
1164
  },
@@ -1215,7 +1302,7 @@ async function connect(options) {
1215
1302
  logger.info('Agent is ready and waiting for prompts...');
1216
1303
  if (channel && validation.channelName) {
1217
1304
  setLocalServerContext(channel, validation.channelName);
1218
- startLocalServer().catch(err => logger.warn(`local server failed to start: ${err?.message || err}`));
1305
+ startLocalServer(options.localPort).catch(err => logger.warn(`local server failed to start: ${err?.message || err}`));
1219
1306
  }
1220
1307
  // First-run only: open the guided welcome tour (or print its URL on a
1221
1308
  // headless host). Wired to the live agent so the "Configure now" step
@@ -1545,6 +1632,7 @@ program
1545
1632
  .option('--cli-preference <preference>', 'CLI preference: auto (prefer orquesta), orquesta, claude, kimi, or cursor (cursor-agent). Defaults to auto.')
1546
1633
  .option('--mode <mode>', 'Alias for --cli-preference: auto, orquesta, claude, kimi or cursor')
1547
1634
  .option('--renew', 'Rotate this agent token: mint a fresh oat_, revoke the current one, print the new token, and exit. Update your start command/script with it to reconnect.')
1635
+ .option('--port <port>', 'Port for the agent\'s local bridge (default 7790). NOT the manager UI port — that one is `orquesta-agent ui --port <port>`.')
1548
1636
  .option('-D, --daemon', 'Run in daemon mode with automatic restart on exit')
1549
1637
  .option('--max-restarts <number>', 'Maximum restart attempts in daemon mode (0=unlimited)', '0')
1550
1638
  .action((options) => {
@@ -1571,6 +1659,20 @@ program
1571
1659
  logger.error('Invalid CLI preference/mode. Use "auto", "orquesta", "claude", or "kimi".');
1572
1660
  process.exit(1);
1573
1661
  }
1662
+ // `--port` used to be an UNKNOWN option on this command, and commander
1663
+ // exits(1) on those: the second agent someone started with
1664
+ // `orquesta-agent --token … --port 8081` (after the manager reported 8080
1665
+ // busy) never ran at all, so its project sat grey in the dashboard forever
1666
+ // and the port looked like the culprit. QA-54.
1667
+ let localPort;
1668
+ if (options.port !== undefined) {
1669
+ localPort = parseInt(String(options.port), 10);
1670
+ if (!Number.isFinite(localPort) || localPort < 0 || localPort > 65535) {
1671
+ logger.error(`Invalid --port "${options.port}". Use a number between 0 and 65535.`);
1672
+ process.exit(1);
1673
+ }
1674
+ logger.info(`Local bridge port set to ${localPort}. (The web manager's port is separate: orquesta-agent ui --port <port>.)`);
1675
+ }
1574
1676
  // Check if daemon mode is requested (and we're not already a daemon child)
1575
1677
  if (options.daemon && !isDaemonChild()) {
1576
1678
  // Start in daemon mode - this will spawn the agent as a child process
@@ -1595,6 +1697,7 @@ program
1595
1697
  sandboxMode: options.sandboxStrict === true ? 'strict' : undefined,
1596
1698
  cliPreference: options.cliPreference,
1597
1699
  agentName: options.name,
1700
+ localPort,
1598
1701
  });
1599
1702
  });
1600
1703
  // Hook command — Claude Code / Cursor hook event handler