@enderfga/claw-orchestrator 4.1.2 → 4.3.0

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.
@@ -17,6 +17,7 @@ interface PersistedSession {
17
17
  import { type Logger } from './logger.js';
18
18
  import { type SessionConfig, type SessionInfo, type SendResult, type PluginConfig, type EffortLevel, type EngineType, type AgentInfo, type SkillInfo, type RuleInfo, type StreamEvent, type ISession, type CouncilConfig, type CouncilSession, type CouncilReviewResult, type CouncilAcceptResult, type CouncilRejectResult, type InboxMessage, type UltraplanResult, type UltrareviewResult } from './types.js';
19
19
  import { Council } from './council.js';
20
+ import { type FanoutConfig, type FanoutSession } from './fanout.js';
20
21
  import { AutoloopRunner } from './autoloop/runner.js';
21
22
  import { ClaudeAgentDispatcher } from './autoloop/dispatcher.js';
22
23
  import type { AutoloopState } from './autoloop/types.js';
@@ -255,6 +256,40 @@ export declare class SessionManager {
255
256
  ok: true;
256
257
  goal: unknown;
257
258
  };
259
+ private _getCodexAppSession;
260
+ codexInterrupt(name: string): Promise<{
261
+ ok: true;
262
+ interrupted: boolean;
263
+ }>;
264
+ codexSteer(name: string, text: string): Promise<{
265
+ ok: true;
266
+ steered: boolean;
267
+ turnId?: string;
268
+ text?: string;
269
+ }>;
270
+ codexForkThread(name: string): Promise<{
271
+ ok: true;
272
+ threadId: string;
273
+ }>;
274
+ codexRollback(name: string, numTurns: number): Promise<{
275
+ ok: true;
276
+ numTurns: number;
277
+ }>;
278
+ codexModels(name: string): Promise<{
279
+ ok: true;
280
+ models: unknown[];
281
+ }>;
282
+ codexThreads(name: string, opts?: {
283
+ cwd?: string;
284
+ searchTerm?: string;
285
+ archived?: boolean;
286
+ cursor?: string;
287
+ limit?: number;
288
+ }): Promise<{
289
+ ok: true;
290
+ data: unknown[];
291
+ nextCursor: string | null;
292
+ }>;
258
293
  private _assertClaudeSession;
259
294
  /** Send `/goal <objective>` to a claude session. Sets a completion condition that
260
295
  * Claude Code pursues across turns, evaluating after each turn via Haiku. */
@@ -272,6 +307,18 @@ export declare class SessionManager {
272
307
  stdout: string;
273
308
  stderr: string;
274
309
  }>;
310
+ /**
311
+ * Wraps `claude agents --json` — lists Claude Code background agent sessions
312
+ * (state/model/title/progress). One-shot spawn; not tied to a managed session.
313
+ * `all` adds `--all` (include completed); `cwd` scopes to a directory.
314
+ */
315
+ claudeAgentsList(opts?: {
316
+ all?: boolean;
317
+ cwd?: string;
318
+ }): Promise<{
319
+ ok: true;
320
+ agents: unknown[];
321
+ }>;
275
322
  private _codexBin;
276
323
  /**
277
324
  * Parse Codex `--json` JSONL output from a stdout buffer.
@@ -402,6 +449,17 @@ export declare class SessionManager {
402
449
  councilReview(id: string): Promise<CouncilReviewResult>;
403
450
  councilAccept(id: string): Promise<CouncilAcceptResult>;
404
451
  councilReject(id: string, feedback: string): Promise<CouncilRejectResult>;
452
+ private fanouts;
453
+ private fanoutCleanupTimers;
454
+ /**
455
+ * Start a fan-out: run the task across N engine/model agents in parallel and
456
+ * collect their answers (optional synthesis). Runs in the background; poll
457
+ * with fanoutStatus. Distinct from council — no rounds, votes, or worktrees.
458
+ */
459
+ fanoutStart(config: FanoutConfig): FanoutSession;
460
+ fanoutStatus(id: string): FanoutSession;
461
+ fanoutAbort(id: string): void;
462
+ private _scheduleFanoutCleanup;
405
463
  private get _sessionLookup();
406
464
  sessionSendTo(from: string, to: string, message: string, summary?: string): Promise<{
407
465
  delivered: boolean;
@@ -424,6 +482,7 @@ export declare class SessionManager {
424
482
  maxDurationMinutes?: number;
425
483
  model?: string;
426
484
  focus?: string;
485
+ engines?: EngineType[];
427
486
  }): UltrareviewResult;
428
487
  ultrareviewStatus(id: string): UltrareviewResult | undefined;
429
488
  private autoloops;
@@ -115,6 +115,7 @@ import { PersistentCustomSession } from './persistent-custom-session.js';
115
115
  import { overrideModelPricing, } from './types.js';
116
116
  import { resolveAlias, isClaudeModel } from './models.js';
117
117
  import { Council } from './council.js';
118
+ import { Fanout } from './fanout.js';
118
119
  import { AutoloopRunner } from './autoloop/runner.js';
119
120
  import { ClaudeAgentDispatcher } from './autoloop/dispatcher.js';
120
121
  import { DEFAULT_PUSH_POLICY } from './autoloop/types.js';
@@ -935,6 +936,47 @@ export class SessionManager {
935
936
  }
936
937
  return { ok: true, goal: session.goal ?? null };
937
938
  }
939
+ // ─── Codex app-server v2 RPCs (codex-app engine only, Codex 0.137) ────────
940
+ //
941
+ // turn/interrupt, turn/steer, thread/fork, thread/rollback, model/list — the
942
+ // high-value app-server surface beyond /goal. Each requires a `codex-app`
943
+ // session; the discriminator is the presence of the `interrupt` method.
944
+ _getCodexAppSession(name, feature) {
945
+ const managed = this.sessions.get(name);
946
+ if (!managed)
947
+ throw new Error(`Session not found: ${name}`);
948
+ const session = managed.session;
949
+ if (typeof session.interrupt !== 'function') {
950
+ const engine = managed.config.engine || 'claude';
951
+ throw new Error(`Session "${name}" uses engine "${engine}" which does not support ${feature}. ` +
952
+ `Start a session with engine: "codex-app".`);
953
+ }
954
+ return session;
955
+ }
956
+ async codexInterrupt(name) {
957
+ const r = await this._getCodexAppSession(name, 'turn/interrupt').interrupt();
958
+ return { ok: true, ...r };
959
+ }
960
+ async codexSteer(name, text) {
961
+ const r = await this._getCodexAppSession(name, 'turn/steer').steer(text);
962
+ return { ok: true, ...r };
963
+ }
964
+ async codexForkThread(name) {
965
+ const r = await this._getCodexAppSession(name, 'thread/fork').forkThread();
966
+ return { ok: true, ...r };
967
+ }
968
+ async codexRollback(name, numTurns) {
969
+ await this._getCodexAppSession(name, 'thread/rollback').rollback(numTurns);
970
+ return { ok: true, numTurns };
971
+ }
972
+ async codexModels(name) {
973
+ const models = await this._getCodexAppSession(name, 'model/list').listModels();
974
+ return { ok: true, models };
975
+ }
976
+ async codexThreads(name, opts = {}) {
977
+ const r = await this._getCodexAppSession(name, 'thread/list').listThreads(opts);
978
+ return { ok: true, ...r };
979
+ }
938
980
  // ─── Claude /goal helpers (CLI 2.1.139, claude engine only) ────────
939
981
  //
940
982
  // Claude Code's /goal slash command works in non-interactive stream-json
@@ -984,6 +1026,34 @@ export class SessionManager {
984
1026
  });
985
1027
  return { stdout, stderr };
986
1028
  }
1029
+ /**
1030
+ * Wraps `claude agents --json` — lists Claude Code background agent sessions
1031
+ * (state/model/title/progress). One-shot spawn; not tied to a managed session.
1032
+ * `all` adds `--all` (include completed); `cwd` scopes to a directory.
1033
+ */
1034
+ async claudeAgentsList(opts = {}) {
1035
+ const args = ['agents', '--json'];
1036
+ if (opts.all)
1037
+ args.push('--all');
1038
+ if (opts.cwd)
1039
+ args.push('--cwd', path.resolve(opts.cwd));
1040
+ const { stdout } = await execFileAsync(this.pluginConfig.claudeBin, args, {
1041
+ maxBuffer: 16 * 1024 * 1024,
1042
+ timeout: 30_000,
1043
+ });
1044
+ let agents = [];
1045
+ const trimmed = stdout.trim();
1046
+ if (trimmed) {
1047
+ try {
1048
+ const parsed = JSON.parse(trimmed);
1049
+ agents = Array.isArray(parsed) ? parsed : (parsed.agents ?? []);
1050
+ }
1051
+ catch {
1052
+ throw new Error(`claude agents --json returned non-JSON output: ${trimmed.slice(0, 200)}`);
1053
+ }
1054
+ }
1055
+ return { ok: true, agents };
1056
+ }
987
1057
  // ─── Codex one-shot wrappers ──────────────────────────────────────────
988
1058
  _codexBin() {
989
1059
  return process.env.CODEX_BIN || 'codex';
@@ -1600,6 +1670,55 @@ export class SessionManager {
1600
1670
  this._scheduleCouncilCleanup(id); // reset TTL — council may be restarted
1601
1671
  return result;
1602
1672
  }
1673
+ // ─── Fan-out (parallel multi-engine task, no consensus) ────────────────
1674
+ fanouts = new Map();
1675
+ fanoutCleanupTimers = new Map();
1676
+ /**
1677
+ * Start a fan-out: run the task across N engine/model agents in parallel and
1678
+ * collect their answers (optional synthesis). Runs in the background; poll
1679
+ * with fanoutStatus. Distinct from council — no rounds, votes, or worktrees.
1680
+ */
1681
+ fanoutStart(config) {
1682
+ if (!config.agents?.length)
1683
+ throw new Error('fanoutStart: at least one agent is required');
1684
+ const names = config.agents.map((a) => a.name);
1685
+ if (new Set(names).size !== names.length) {
1686
+ throw new Error('fanoutStart: agent names must be unique (they form session names)');
1687
+ }
1688
+ const fanout = new Fanout(config, this, this.logger);
1689
+ const session = fanout.init();
1690
+ this.fanouts.set(session.id, fanout);
1691
+ fanout
1692
+ .run()
1693
+ .catch((err) => this.logger.error(`Fanout ${session.id} failed:`, err))
1694
+ .finally(() => this._scheduleFanoutCleanup(session.id));
1695
+ return session;
1696
+ }
1697
+ fanoutStatus(id) {
1698
+ const fanout = this.fanouts.get(id);
1699
+ if (!fanout)
1700
+ throw new Error(`Fanout '${id}' not found`);
1701
+ return fanout.getSession();
1702
+ }
1703
+ fanoutAbort(id) {
1704
+ const fanout = this.fanouts.get(id);
1705
+ if (!fanout)
1706
+ throw new Error(`Fanout '${id}' not found`);
1707
+ fanout.abort();
1708
+ this._scheduleFanoutCleanup(id);
1709
+ }
1710
+ _scheduleFanoutCleanup(id) {
1711
+ const existing = this.fanoutCleanupTimers.get(id);
1712
+ if (existing)
1713
+ clearTimeout(existing);
1714
+ const timer = setTimeout(() => {
1715
+ this.fanouts.delete(id);
1716
+ this.fanoutCleanupTimers.delete(id);
1717
+ }, RESULT_TTL_MS);
1718
+ if (typeof timer.unref === 'function')
1719
+ timer.unref();
1720
+ this.fanoutCleanupTimers.set(id, timer);
1721
+ }
1603
1722
  // ─── Inbox (cross-session messaging) — delegated to InboxManager ────
1604
1723
  get _sessionLookup() {
1605
1724
  return {
@@ -1805,40 +1924,73 @@ export class SessionManager {
1805
1924
  persona: 'You are a memory reviewer. Focus on: memory leaks, circular references, large object retention, stream handling.',
1806
1925
  },
1807
1926
  ];
1808
- const agents = reviewAngles.slice(0, agentCount).map((a) => ({
1809
- ...a,
1810
- model: opts?.model,
1811
- }));
1812
1927
  const maxMinutes = Math.min(25, Math.max(5, opts?.maxDurationMinutes || 10));
1813
1928
  const focus = opts?.focus || 'Find bugs, security issues, and code quality problems';
1814
- const councilConfig = {
1815
- name: 'ultrareview',
1816
- agents,
1817
- maxRounds: 2, // Review doesn't need many rounds — find bugs, then synthesize
1818
- projectDir: cwd,
1819
- agentTimeoutMs: maxMinutes * 60 * 1000,
1820
- maxTurnsPerAgent: 20,
1821
- };
1822
- const councilSession = this.councilStart(`# Code Review Task\n\nReview the codebase in this project. ${focus}.\n\nEach reviewer: examine the code from your specialty angle, report bugs found with file paths and line numbers. Vote [CONSENSUS: YES] when your review is complete.`, councilConfig);
1823
- result.councilId = councilSession.id;
1824
- // Poll council for completion (store ref for shutdown cleanup)
1929
+ const reviewInstruction = `# Code Review Task\n\nReview the codebase in this project. ${focus}.\n\n` +
1930
+ `Examine the code from your specialty angle and report bugs found with file paths and line numbers.`;
1931
+ // Cross-engine review: round-robin the requested engines across reviewers
1932
+ // (default claude-only — unchanged behavior). Each reviewer's persona is
1933
+ // its prompt; per-agent failures are isolated by the fan-out runner.
1934
+ const engines = opts?.engines?.length ? opts.engines : ['claude'];
1935
+ const agents = reviewAngles.slice(0, agentCount).map((a, i) => ({
1936
+ name: a.name,
1937
+ engine: engines[i % engines.length],
1938
+ model: opts?.model,
1939
+ prompt: `${a.persona}\n\n${reviewInstruction}`,
1940
+ // Review is read-only: keep reviewers out of edit mode so they analyse and
1941
+ // report without modifying the very code they review. (Unlike council,
1942
+ // fan-out shares the project dir — there is no worktree to sandbox edits.
1943
+ // `plan` constrains the claude engine; non-claude reviewers, which are
1944
+ // opt-in via `engines`, run under their engine's default sandbox.)
1945
+ permissionMode: 'plan',
1946
+ }));
1947
+ let fanoutSession;
1948
+ try {
1949
+ fanoutSession = this.fanoutStart({
1950
+ task: reviewInstruction,
1951
+ projectDir: cwd,
1952
+ agents,
1953
+ synthesize: true,
1954
+ agentTimeoutMs: maxMinutes * 60 * 1000,
1955
+ maxTurnsPerAgent: 20,
1956
+ });
1957
+ }
1958
+ catch (err) {
1959
+ // Fan-out failed to even start (e.g. validation) — surface it on the
1960
+ // stored result instead of leaving it frozen at 'running'.
1961
+ result.status = 'error';
1962
+ result.error = err.message;
1963
+ result.endTime = new Date().toISOString();
1964
+ setTimeout(() => this.ultrareviews.delete(id), RESULT_TTL_MS);
1965
+ return result;
1966
+ }
1967
+ // `councilId` is kept for the UltrareviewResult contract; it now holds the
1968
+ // fan-out id (an opaque run id used only by ultrareview_status).
1969
+ result.councilId = fanoutSession.id;
1970
+ // Poll the fan-out for completion (store ref for shutdown cleanup).
1825
1971
  const pollInterval = setInterval(() => {
1826
1972
  try {
1827
- const status = this.councilStatus(councilSession.id);
1973
+ const status = this.fanoutStatus(fanoutSession.id);
1828
1974
  if (!status || status.status === 'running')
1829
1975
  return;
1830
1976
  clearInterval(pollInterval);
1831
1977
  this.ultrareviewPollers.delete(id);
1832
1978
  result.status = status.status === 'error' ? 'error' : 'completed';
1833
1979
  result.endTime = new Date().toISOString();
1834
- // Synthesize findings from all agent responses
1835
- if (status.responses.length > 0) {
1836
- result.findings = status.responses.map((r) => `## ${r.agent}\n\n${r.content}`).join('\n\n---\n\n');
1980
+ // Prefer the synthesis pass; fall back to joining successful results.
1981
+ if (status.synthesis) {
1982
+ result.findings = status.synthesis;
1983
+ }
1984
+ else if (status.results.length > 0) {
1985
+ result.findings = status.results
1986
+ .filter((r) => r.ok)
1987
+ .map((r) => `## ${r.agent}\n\n${r.output}`)
1988
+ .join('\n\n---\n\n');
1837
1989
  }
1838
1990
  setTimeout(() => this.ultrareviews.delete(id), RESULT_TTL_MS);
1839
1991
  }
1840
1992
  catch {
1841
- // Council may have been cleaned up; stop polling
1993
+ // Fan-out may have been cleaned up; stop polling.
1842
1994
  clearInterval(pollInterval);
1843
1995
  this.ultrareviewPollers.delete(id);
1844
1996
  }