rigjs 4.0.18 → 4.1.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.
@@ -38,6 +38,23 @@ export interface DeployTarget {
38
38
  original_regexp?: string;
39
39
  final?: string;
40
40
  } | undefined;
41
+ /**
42
+ * Edge provider for `rig publish` (回源改写 + 刷缓存).
43
+ * - 'cdn' (default): traditional Aliyun CDN, BatchSetCdnDomainConfig.
44
+ * - 'esa': Aliyun ESA (Edge Security Acceleration), site-scoped rewrite rules.
45
+ */
46
+ edge_provider?: 'cdn' | 'esa';
47
+ /**
48
+ * ESA OpenAPI endpoint. Only used when edge_provider === 'esa'.
49
+ * Defaults to 'esa.cn-hangzhou.aliyuncs.com'.
50
+ */
51
+ esa_endpoint?: string;
52
+ /**
53
+ * ESA site name (registrable zone, e.g. 'terncloud.com'). Only used when
54
+ * edge_provider === 'esa'. If omitted, derived from the endpoint domain's
55
+ * last two labels.
56
+ */
57
+ esa_site_name?: string;
41
58
  }
42
59
 
43
60
  /**
@@ -0,0 +1,117 @@
1
+ import ESAClient, {
2
+ ListSitesRequest,
3
+ CreateRewriteUrlRuleRequest,
4
+ PurgeCachesRequest,
5
+ PurgeCachesRequestContent,
6
+ } from '@alicloud/esa20240910';
7
+ import { $OpenApiUtil } from '@alicloud/openapi-core';
8
+ import { DeployTarget } from '../CICD';
9
+
10
+ /**
11
+ * ESA (Aliyun Edge Security Acceleration) deploy adapter — the ESA-flavoured
12
+ * counterpart of {@link ./CDN.ts}. Used by `rig publish` when the deploy target
13
+ * sets `edge_provider: 'esa'`.
14
+ *
15
+ * Differences from traditional CDN:
16
+ * - ESA config is **site-scoped**: every operation needs a numeric `siteId`,
17
+ * resolved from the registrable zone (e.g. `terncloud.com`) via `ListSites`.
18
+ * - Back-to-origin rewrite uses `CreateRewriteUrlRule` (rule expression +
19
+ * static/dynamic target URI), not CDN's regex `back_to_origin_url_rewrite`.
20
+ * - Cache refresh uses `PurgeCaches`, not `RefreshObjectCaches`.
21
+ *
22
+ * Credentials come from the DeployTarget (injected via `-p ak=...&as=...`),
23
+ * never hard-coded here.
24
+ */
25
+ class ESA {
26
+ private client: ESAClient;
27
+ private siteIdCache: Map<string, number> = new Map();
28
+ private explicitSiteName?: string;
29
+
30
+ constructor(target: DeployTarget) {
31
+ const config = new $OpenApiUtil.Config({
32
+ accessKeyId: target.access_key,
33
+ accessKeySecret: target.access_secret,
34
+ endpoint: target.esa_endpoint || 'esa.cn-hangzhou.aliyuncs.com',
35
+ });
36
+ this.client = new ESAClient(config);
37
+ this.explicitSiteName = target.esa_site_name;
38
+ }
39
+
40
+ /**
41
+ * Registrable zone for a domain: `test-esa.terncloud.com` -> `terncloud.com`.
42
+ * (Good enough for normal `*.com` / `*.cn` zones; pass `esa_site_name`
43
+ * explicitly for multi-label public suffixes like `*.com.cn`.)
44
+ */
45
+ private siteNameFor(domain: string): string {
46
+ if (this.explicitSiteName) return this.explicitSiteName;
47
+ const parts = domain.split('.').filter(Boolean);
48
+ return parts.length <= 2 ? domain : parts.slice(-2).join('.');
49
+ }
50
+
51
+ /** Resolve (and cache) the numeric ESA siteId for a domain's zone. */
52
+ public async resolveSiteId(domain: string): Promise<number> {
53
+ const siteName = this.siteNameFor(domain);
54
+ const cached = this.siteIdCache.get(siteName);
55
+ if (cached) return cached;
56
+
57
+ const resp = await this.client.listSites(
58
+ new ListSitesRequest({ siteName, siteSearchType: 'exact' })
59
+ );
60
+ const sites = resp.body?.sites || [];
61
+ const site = sites.find((s) => s.siteName === siteName) || sites[0];
62
+ if (!site || site.siteId == null) {
63
+ throw new Error(
64
+ `ESA site not found for "${siteName}" (domain ${domain}). ` +
65
+ `Create the ESA site (zone) and bind the OSS origin in the ESA console/API first.`
66
+ );
67
+ }
68
+ this.siteIdCache.set(siteName, site.siteId);
69
+ return site.siteId;
70
+ }
71
+
72
+ /**
73
+ * Create a single back-to-origin URI rewrite rule.
74
+ * @param rule ESA rule expression. `true` matches all requests; otherwise a
75
+ * conditional expression, e.g. `(http.request.uri.path.file_name ne "")`.
76
+ * @param rewriteUriType `static` (fixed `uri`) or `dynamic` (`uri` is an expression).
77
+ * @param uri target URI after rewrite (static path or dynamic expression).
78
+ * @param sequence rule priority (lower runs first).
79
+ */
80
+ public async setRewriteRule(
81
+ domain: string,
82
+ ruleName: string,
83
+ rule: string,
84
+ rewriteUriType: 'static' | 'dynamic',
85
+ uri: string,
86
+ sequence: number
87
+ ) {
88
+ const siteId = await this.resolveSiteId(domain);
89
+ const resp = await this.client.createRewriteUrlRule(
90
+ new CreateRewriteUrlRuleRequest({
91
+ siteId,
92
+ ruleName,
93
+ rule,
94
+ ruleEnable: 'on',
95
+ rewriteUriType,
96
+ uri,
97
+ sequence,
98
+ })
99
+ );
100
+ return resp.body;
101
+ }
102
+
103
+ /** Purge cached files by URL. */
104
+ public async purgeCache(domain: string, urls: string[]) {
105
+ const siteId = await this.resolveSiteId(domain);
106
+ const resp = await this.client.purgeCaches(
107
+ new PurgeCachesRequest({
108
+ siteId,
109
+ type: 'file',
110
+ content: new PurgeCachesRequestContent({ files: urls }),
111
+ })
112
+ );
113
+ return resp.body;
114
+ }
115
+ }
116
+
117
+ export default ESA;
package/lib/crew/ask.ts CHANGED
@@ -10,14 +10,14 @@ export default function crewAsk(messageParts: string[] | string | undefined, opt
10
10
  const crew = requireCrew(opts.crew);
11
11
  const message = Array.isArray(messageParts) ? messageParts.join(' ') : (messageParts || '');
12
12
  if (!message.trim()) {
13
- print.info('no message supplied; running a lightweight Lead tick.');
14
- print.info('MVP Lead tick only refreshes status. LLM delegation will be added in a later phase.');
13
+ print.info('no message supplied; running a lightweight Orchestrator tick.');
14
+ print.info('MVP Orchestrator tick only refreshes status. LLM delegation will be added in a later phase.');
15
15
  crewBoard({ crew: crew.name });
16
16
  return;
17
17
  }
18
18
  const file = rootPath(crew, 'Current-Goal.md');
19
19
  fs.appendFileSync(file, `\n- ${new Date().toISOString()} ${message.trim()}\n`, 'utf8');
20
- appendLog(crew, `Lead input: ${message.trim()}`);
20
+ appendLog(crew, `Orchestrator input: ${message.trim()}`);
21
21
  print.succeed(`added goal input to ${file}`);
22
22
  crewBoard({ crew: crew.name });
23
23
  }
package/lib/crew/board.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from 'path';
2
2
  import print from '../print';
3
3
  import { requireCrew, shortPath } from './config';
4
- import { scanTasks, openInboxTasks, summarize, taskProgress, CrewTask } from './task';
4
+ import { scanTasks, openPendingQuestions, summarize, taskProgress, CrewTask } from './task';
5
5
  import { rootPath, writeText, readText } from './vault';
6
6
  import { writeCrewState } from './state';
7
7
  import { roleDefinitionsForCrew } from './role';
@@ -11,35 +11,35 @@ interface BoardOpts { crew?: string; }
11
11
  export default function crewBoard(opts: BoardOpts): void {
12
12
  const crew = requireCrew(opts.crew);
13
13
  const tasks = scanTasks(crew);
14
- const inbox = openInboxTasks(crew);
14
+ const pending = openPendingQuestions(crew);
15
15
  const summary = summarize(tasks);
16
- const dashboard = renderDashboard(crew, tasks, inbox);
17
- const file = rootPath(crew, 'Team-Dashboard.md');
16
+ const dashboard = renderDashboard(crew, tasks, pending);
17
+ const file = rootPath(crew, 'Dashboard.md');
18
18
  writeText(file, dashboard);
19
19
  writeCrewState(crew, tasks);
20
20
  print.succeed(`crew dashboard refreshed: ${shortPath(file)}`);
21
- print.info(`tasks: ${summary.done}/${summary.total} done, inbox: ${inbox.length} open`);
21
+ print.info(`tasks: ${summary.done}/${summary.total} done, pending questions: ${pending.length} open`);
22
22
  }
23
23
 
24
- function renderDashboard(crew: ReturnType<typeof requireCrew>, tasks: CrewTask[], inbox: CrewTask[]): string {
24
+ function renderDashboard(crew: ReturnType<typeof requireCrew>, tasks: CrewTask[], pending: CrewTask[]): string {
25
25
  const summary = summarize(tasks);
26
26
  const health = summary.blocked > 0 ? 'At Risk' : 'On Track';
27
27
  const goal = currentGoal(rootPath(crew, 'Current-Goal.md'));
28
28
  return [
29
- '# Team Dashboard',
29
+ '# Dashboard',
30
30
  '',
31
31
  `Last updated: ${new Date().toISOString()}`,
32
32
  '',
33
- '## Lead Brief',
33
+ '## Orchestrator Brief',
34
34
  '',
35
35
  `Current Goal: ${goal || '_No current goal yet_'}`,
36
36
  `Overall: ${taskProgress(tasks)}% (${summary.done}/${summary.total})`,
37
37
  `Health: ${health}`,
38
- `Next Agent Action: ${inbox.length > 0 ? 'Read `rig crew inbox` and surface only needed decisions to the human.' : 'Run `rig crew` to continue the next Lead tick.'}`,
38
+ `Next Agent Action: ${pending.length > 0 ? 'Read `rig orchestrate pending-questions` and surface only needed decisions to the human.' : 'Run `rig orchestrate` to continue the next Orchestrator tick.'}`,
39
39
  '',
40
40
  '## Needs Your Attention',
41
41
  '',
42
- inboxTable(inbox),
42
+ pendingTable(pending),
43
43
  '',
44
44
  '## Project Progress',
45
45
  '',
@@ -68,8 +68,8 @@ function currentGoal(file: string): string {
68
68
  return lines.length ? lines[lines.length - 1].replace(/^\-\s*/, '') : '';
69
69
  }
70
70
 
71
- function inboxTable(tasks: CrewTask[]): string {
72
- if (tasks.length === 0) return '_No open inbox items._';
71
+ function pendingTable(tasks: CrewTask[]): string {
72
+ if (tasks.length === 0) return '_No open pending questions._';
73
73
  const rows = tasks.map(t => `| ${t.id || '-'} | ${t.fields.type || '-'} | ${cleanTaskText(t.text)} | ${t.fields.priority || '-'} |`);
74
74
  return ['| ID | Type | Item | Priority |', '|---|---|---|---|'].concat(rows).join('\n');
75
75
  }
@@ -78,7 +78,7 @@ function projectTable(crew: ReturnType<typeof requireCrew>, tasks: CrewTask[]):
78
78
  const projects = crew.projects || [];
79
79
  if (projects.length === 0) return '_No projects registered yet._';
80
80
  const rows = projects.map(p => {
81
- const scoped = tasks.filter(t => t.scope !== 'inbox' && (t.scope === `project:${p.name}` || t.scope.startsWith(`project:${p.name}:`) || t.fields.project === p.name));
81
+ const scoped = tasks.filter(t => t.scope !== 'pending' && (t.scope === `project:${p.name}` || t.scope.startsWith(`project:${p.name}:`) || t.fields.project === p.name));
82
82
  const s = summarize(scoped);
83
83
  const health = s.blocked > 0 ? 'At Risk' : 'On Track';
84
84
  return `| ${p.name} | ${p.owner} | ${p.defaultExecutor || crew.defaultExecutor || 'claude'} | ${health} | ${s.open} | ${s.blocked} | ${shortPath(p.path)} |`;
@@ -104,7 +104,7 @@ function blockersTable(tasks: CrewTask[]): string {
104
104
  }
105
105
 
106
106
  function activeTable(tasks: CrewTask[]): string {
107
- const active = tasks.filter(t => !t.done && t.scope !== 'inbox').slice(0, 20);
107
+ const active = tasks.filter(t => !t.done && t.scope !== 'pending').slice(0, 20);
108
108
  if (active.length === 0) return '_No active tasks._';
109
109
  const rows = active.map(t => `| ${t.id || '-'} | ${t.fields.project || '-'} | ${t.fields.owner || displayScope(t.scope)} | ${t.fields.status || 'pending'} | ${cleanTaskText(t.text)} |`);
110
110
  return ['| ID | Project | Owner | Status | Task |', '|---|---|---|---|---|'].concat(rows).join('\n');
@@ -33,7 +33,7 @@ export interface CrewConfig {
33
33
  }
34
34
 
35
35
  export const DEFAULT_ROLES: CrewRole[] = BUILTIN_ROLE_NAMES;
36
- export const DEFAULT_CREW_ROOT = 'rig-agents';
36
+ export const DEFAULT_CREW_ROOT = 'rig-crew';
37
37
 
38
38
  const DEFAULT_CONFIG: CrewConfig = { crews: [] };
39
39
 
@@ -86,7 +86,7 @@ export function normalizeCrew(entry: CrewEntry): CrewEntry {
86
86
  return {
87
87
  ...entry,
88
88
  root: entry.root || DEFAULT_CREW_ROOT,
89
- dashboard: entry.dashboard || path.join(entry.root || DEFAULT_CREW_ROOT, 'Team-Dashboard.md'),
89
+ dashboard: entry.dashboard || path.join(entry.root || DEFAULT_CREW_ROOT, 'Dashboard.md'),
90
90
  defaultExecutor: entry.defaultExecutor || 'claude',
91
91
  mode: entry.mode || 'leader-first',
92
92
  state: entry.state || { backend: 'json' },
@@ -0,0 +1,58 @@
1
+ import print from '../print';
2
+ import { requireCrew, shortPath } from './config';
3
+ import { resolveEngine } from './engine';
4
+ import { buildEngineInvocation, dispatchTask } from './runtime';
5
+
6
+ interface DispatchOpts {
7
+ crew?: string;
8
+ prompt: string;
9
+ engine?: string;
10
+ task?: string;
11
+ timeout?: string;
12
+ }
13
+
14
+ /**
15
+ * `rig orchestrate dispatch <project>` — MVP manual dispatch: resolve the engine for
16
+ * <project>, run the prompt in a fresh `task/<id>` worktree of that project, print the
17
+ * result. The full orchestrate loop (read task files → dispatch → verify → merge) builds
18
+ * on this primitive; this command exposes it for manual use / debugging.
19
+ */
20
+ export default async function crewDispatch(project: string, opts: DispatchOpts): Promise<void> {
21
+ const crew = requireCrew(opts.crew);
22
+ const proj = (crew.projects || []).find(p => p.name === project);
23
+ if (!proj) {
24
+ print.error(`unknown project: ${project}. Register it with \`rig orchestrate project add\` first.`);
25
+ process.exit(1);
26
+ }
27
+ let engine: string;
28
+ try {
29
+ const res = resolveEngine({ explicit: opts.engine, project: proj, crew });
30
+ if (!res.engine) {
31
+ print.error(`engine unresolved — ${res.detail}.`);
32
+ process.exit(1);
33
+ }
34
+ engine = res.engine;
35
+ print.info(`engine: ${res.engine} (source: ${res.source})`);
36
+ } catch (e: any) {
37
+ print.error(e.message);
38
+ process.exit(1);
39
+ return;
40
+ }
41
+
42
+ const taskId = opts.task || `adhoc-${Date.now()}`;
43
+ const timeoutMs = opts.timeout ? Number(opts.timeout) : 600000;
44
+ const inv = buildEngineInvocation(engine as any, opts.prompt);
45
+ print.info(`dispatching → ${proj.name} (${shortPath(proj.path)}) on branch task/${taskId} …`);
46
+
47
+ try {
48
+ const { worktree, result } = await dispatchTask(proj.path, taskId, inv, { timeoutMs });
49
+ print.info(`exit=${result.code} timedOut=${result.timedOut} truncated=${result.truncated} ${Math.round(result.durationMs / 1000)}s`);
50
+ print.info(`worktree: ${shortPath(worktree.path)} (branch task/${taskId}) — remove with \`git -C ${shortPath(proj.path)} worktree remove --force ${shortPath(worktree.path)}\``);
51
+ // eslint-disable-next-line no-console
52
+ console.log(result.stdout.slice(0, 4000));
53
+ if (result.code !== 0) process.exitCode = 1;
54
+ } catch (e: any) {
55
+ print.error(`dispatch failed: ${e.message}`);
56
+ process.exit(1);
57
+ }
58
+ }
@@ -15,7 +15,7 @@ export default function crewDoctor(opts: DoctorOpts): void {
15
15
  checks.push({ name: 'vault', ok: fs.existsSync(crew.vault), detail: shortPath(crew.vault) });
16
16
  checks.push({ name: 'crew root', ok: fs.existsSync(rootPath(crew, '')), detail: shortPath(rootPath(crew, '')) });
17
17
  checks.push({ name: 'current goal', ok: fs.existsSync(rootPath(crew, 'Current-Goal.md')), detail: path.join(crew.root, 'Current-Goal.md') });
18
- checks.push({ name: 'inbox', ok: fs.existsSync(rootPath(crew, 'Inbox.md')), detail: path.join(crew.root, 'Inbox.md') });
18
+ checks.push({ name: 'pending questions', ok: fs.existsSync(rootPath(crew, 'Pending-Questions.md')), detail: path.join(crew.root, 'Pending-Questions.md') });
19
19
  checks.push({ name: 'vault CLAUDE.md', ok: fs.existsSync(path.join(crew.vault, 'CLAUDE.md')), detail: 'CLAUDE.md' });
20
20
  checks.push({ name: 'vault AGENTS.md', ok: fs.existsSync(path.join(crew.vault, 'AGENTS.md')), detail: 'AGENTS.md' });
21
21
  checks.push({ name: 'user RIG.md', ok: fs.existsSync(crewPaths.userRules), detail: shortPath(crewPaths.userRules) });
@@ -0,0 +1,73 @@
1
+ import { detectHostEngine, resolveEngine, isEngine } from './engine';
2
+
3
+ describe('isEngine', () => {
4
+ it('accepts the three valid engines', () => {
5
+ expect(isEngine('claude')).toBe(true);
6
+ expect(isEngine('codex')).toBe(true);
7
+ expect(isEngine('pi')).toBe(true);
8
+ });
9
+ it('rejects anything else', () => {
10
+ expect(isEngine('gpt')).toBe(false);
11
+ expect(isEngine('')).toBe(false);
12
+ expect(isEngine(undefined)).toBe(false);
13
+ });
14
+ });
15
+
16
+ describe('detectHostEngine', () => {
17
+ it('detects claude from CLAUDECODE', () => {
18
+ expect(detectHostEngine({ CLAUDECODE: '1' })).toBe('claude');
19
+ });
20
+ it('detects claude from CLAUDE_CODE_* and AI_AGENT', () => {
21
+ expect(detectHostEngine({ CLAUDE_CODE_ENTRYPOINT: 'cli' })).toBe('claude');
22
+ expect(detectHostEngine({ AI_AGENT: 'claude-code' })).toBe('claude');
23
+ });
24
+ it('detects codex from CODEX_* / AI_AGENT', () => {
25
+ expect(detectHostEngine({ CODEX_SANDBOX: '1' })).toBe('codex');
26
+ expect(detectHostEngine({ AI_AGENT: 'codex' })).toBe('codex');
27
+ });
28
+ it('returns null when neither present', () => {
29
+ expect(detectHostEngine({ PATH: '/usr/bin' })).toBeNull();
30
+ });
31
+ it('returns null when ambiguous (both)', () => {
32
+ expect(detectHostEngine({ CLAUDECODE: '1', CODEX_SANDBOX: '1' })).toBeNull();
33
+ });
34
+ });
35
+
36
+ describe('resolveEngine — 5-level order (design §2.2)', () => {
37
+ const noHost = { PATH: '/usr/bin' };
38
+
39
+ it('1. explicit wins over everything', () => {
40
+ const r = resolveEngine({ explicit: 'codex', project: { defaultExecutor: 'claude' }, crew: { defaultExecutor: 'claude' }, env: { CLAUDECODE: '1' } });
41
+ expect(r).toEqual({ engine: 'codex', source: 'explicit' });
42
+ });
43
+
44
+ it('throws on invalid explicit engine', () => {
45
+ expect(() => resolveEngine({ explicit: 'gpt' })).toThrow(/invalid engine/);
46
+ });
47
+
48
+ it('2. project default beats crew + host', () => {
49
+ const r = resolveEngine({ project: { defaultExecutor: 'codex' }, crew: { defaultExecutor: 'claude' }, env: { CLAUDECODE: '1' } });
50
+ expect(r).toEqual({ engine: 'codex', source: 'project' });
51
+ });
52
+
53
+ it('3. crew default beats host', () => {
54
+ const r = resolveEngine({ crew: { defaultExecutor: 'codex' }, env: { CLAUDECODE: '1' } });
55
+ expect(r).toEqual({ engine: 'codex', source: 'crew' });
56
+ });
57
+
58
+ it('4. host self-detect when no config default', () => {
59
+ const r = resolveEngine({ crew: { defaultExecutor: undefined }, env: { CLAUDECODE: '1' } });
60
+ expect(r).toEqual({ engine: 'claude', source: 'host' });
61
+ });
62
+
63
+ it('5. unresolved when nothing resolves', () => {
64
+ const r = resolveEngine({ env: noHost });
65
+ expect(r.engine).toBeNull();
66
+ expect(r.source).toBe('unresolved');
67
+ });
68
+
69
+ it('skips invalid config values (treated as unset), falls through to host', () => {
70
+ const r = resolveEngine({ project: { defaultExecutor: 'bogus' }, crew: { defaultExecutor: 'also-bad' }, env: { CODEX_SANDBOX: '1' } });
71
+ expect(r).toEqual({ engine: 'codex', source: 'host' });
72
+ });
73
+ });
@@ -0,0 +1,103 @@
1
+ import print from '../print';
2
+ import { requireCrew } from './config';
3
+
4
+ export type Engine = 'claude' | 'codex' | 'pi';
5
+ const VALID_ENGINES: Engine[] = ['claude', 'codex', 'pi'];
6
+
7
+ export type EngineSource = 'explicit' | 'project' | 'crew' | 'host' | 'unresolved';
8
+
9
+ export interface EngineResolution {
10
+ engine: Engine | null;
11
+ source: EngineSource;
12
+ detail?: string;
13
+ }
14
+
15
+ export function isEngine(v: unknown): v is Engine {
16
+ return typeof v === 'string' && (VALID_ENGINES as string[]).includes(v);
17
+ }
18
+
19
+ /**
20
+ * Host engine self-detection from the environment (design §2.2).
21
+ * - claude: `CLAUDECODE`, any `CLAUDE_CODE*`, or `AI_AGENT` containing "claude"
22
+ * - codex: any `CODEX*`, or `AI_AGENT` containing "codex"
23
+ * Returns null when neither is present, or both are (ambiguous → caller asks).
24
+ */
25
+ export function detectHostEngine(env: NodeJS.ProcessEnv = process.env): Engine | null {
26
+ const keys = Object.keys(env);
27
+ const ai = (env.AI_AGENT || '').toLowerCase();
28
+ const claude = !!env.CLAUDECODE || keys.some(k => k.startsWith('CLAUDE_CODE')) || ai.includes('claude');
29
+ const codex = keys.some(k => k.startsWith('CODEX')) || ai.includes('codex');
30
+ if (claude && !codex) return 'claude';
31
+ if (codex && !claude) return 'codex';
32
+ return null;
33
+ }
34
+
35
+ /**
36
+ * 5-level engine resolution (design §2.2). Highest priority first:
37
+ * 1. explicit — task `engine:` field / conversation directive (override)
38
+ * 2. project — project config `defaultExecutor`
39
+ * 3. crew — crew config `defaultExecutor`
40
+ * 4. host — host self-detection (`detectHostEngine`)
41
+ * 5. unresolved — caller raises a pending question; agent never invents an engine
42
+ *
43
+ * An invalid explicit value throws; invalid project/crew values are skipped
44
+ * (treated as unset) so a typo in config can't silently pin the wrong engine.
45
+ */
46
+ export function resolveEngine(opts: {
47
+ explicit?: string | null;
48
+ project?: { defaultExecutor?: string } | null;
49
+ crew?: { defaultExecutor?: string } | null;
50
+ env?: NodeJS.ProcessEnv;
51
+ }): EngineResolution {
52
+ const { explicit, project, crew, env } = opts;
53
+ if (explicit != null && explicit !== '') {
54
+ if (!isEngine(explicit)) {
55
+ throw new Error(`invalid engine "${explicit}". Use one of: ${VALID_ENGINES.join(', ')}.`);
56
+ }
57
+ return { engine: explicit, source: 'explicit' };
58
+ }
59
+ if (project && isEngine(project.defaultExecutor)) {
60
+ return { engine: project.defaultExecutor as Engine, source: 'project' };
61
+ }
62
+ if (crew && isEngine(crew.defaultExecutor)) {
63
+ return { engine: crew.defaultExecutor as Engine, source: 'crew' };
64
+ }
65
+ const host = detectHostEngine(env);
66
+ if (host) return { engine: host, source: 'host' };
67
+ return {
68
+ engine: null,
69
+ source: 'unresolved',
70
+ detail: 'no explicit/project/crew engine and host undetected — raise a pending question instead of guessing',
71
+ };
72
+ }
73
+
74
+ interface EngineOpts { crew?: string; project?: string; engine?: string; json?: boolean; }
75
+
76
+ /** `rig orchestrate engine` — debug view of which engine resolves and why. */
77
+ export default function crewEngine(opts: EngineOpts): void {
78
+ const crew = requireCrew(opts.crew);
79
+ const project = opts.project ? (crew.projects || []).find(p => p.name === opts.project) : undefined;
80
+ if (opts.project && !project) {
81
+ print.error(`unknown project: ${opts.project}`);
82
+ process.exit(1);
83
+ }
84
+ let res: EngineResolution;
85
+ try {
86
+ res = resolveEngine({ explicit: opts.engine, project, crew });
87
+ } catch (e: any) {
88
+ print.error(e.message);
89
+ process.exit(1);
90
+ return;
91
+ }
92
+ if (opts.json) {
93
+ // eslint-disable-next-line no-console
94
+ console.log(JSON.stringify({ ok: res.engine != null, engine: res.engine, source: res.source, project: opts.project || null, detail: res.detail }, null, 2));
95
+ return;
96
+ }
97
+ if (res.engine == null) {
98
+ print.warn(`engine unresolved — ${res.detail}.`);
99
+ process.exitCode = 1;
100
+ return;
101
+ }
102
+ print.info(`engine: ${res.engine} (source: ${res.source}${opts.project ? `, project: ${opts.project}` : ''})`);
103
+ }
package/lib/crew/index.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import crewInit from './init';
2
2
  import crewStatus from './status';
3
3
  import crewBoard from './board';
4
- import crewInbox from './inbox';
4
+ import crewPendingQuestions from './pendingQuestions';
5
5
  import crewSync from './sync';
6
6
  import crewDoctor from './doctor';
7
+ import crewEngine from './engine';
8
+ import crewDispatch from './dispatchCommand';
7
9
  import crewAsk from './ask';
8
10
  import crewStub from './stub';
9
11
  import { projectAdd, projectList, projectStatus, projectSync } from './project';
@@ -12,52 +14,71 @@ import { pendingAdd, pendingAnswer, pendingList, pendingRemove } from './pending
12
14
 
13
15
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
16
  export function registerCrewCommands(program: any): void {
15
- const crew = program.command('crew [message...]')
16
- .description('Leader-first multi-agent workspace over an Obsidian vault')
17
+ const orchestrate = program.command('orchestrate [message...]')
18
+ .alias('crew')
19
+ .description('Orchestrator-first multi-agent workspace over an Obsidian vault (alias: crew)')
17
20
  .option('-c, --crew <name>', 'target crew name')
18
21
  .action((message: string[] | undefined, opts: { crew?: string }) => crewAsk(message, opts));
19
22
 
20
- crew.command('init')
23
+ orchestrate.command('init')
21
24
  .description('initialize a crew vault')
22
25
  .requiredOption('--vault <path>', 'Obsidian vault path')
23
26
  .option('-n, --as <name>', 'crew name')
24
- .option('--root <path>', 'root folder inside the vault (default: Agents)')
27
+ .option('--root <path>', 'root folder inside the vault (default: rig-crew)')
25
28
  .option('--allow-project-vault', 'allow a vault path under projects/<submodule>')
26
29
  .action(crewInit);
27
30
 
28
- crew.command('ask <message...>')
29
- .description('send a message to the Lead (MVP appends to Current-Goal.md)')
31
+ orchestrate.command('ask <message...>')
32
+ .description('send a message to the Orchestrator (MVP appends to Current-Goal.md)')
30
33
  .option('-c, --crew <name>', 'target crew name')
31
34
  .action(crewAsk);
32
35
 
33
- crew.command('status')
36
+ orchestrate.command('status')
34
37
  .description('show crew progress summary')
35
38
  .option('-c, --crew <name>', 'target crew name')
36
39
  .option('--json', 'machine-readable output')
37
40
  .action(crewStatus);
38
41
 
39
- crew.command('inbox')
40
- .description('show open user attention items')
42
+ orchestrate.command('pending-questions')
43
+ .alias('inbox')
44
+ .description('show open system→user pending questions (alias: inbox)')
41
45
  .option('-c, --crew <name>', 'target crew name')
42
46
  .option('--json', 'machine-readable output')
43
- .action(crewInbox);
47
+ .action(crewPendingQuestions);
44
48
 
45
- crew.command('board')
46
- .description('refresh Agents/Team-Dashboard.md')
49
+ orchestrate.command('board')
50
+ .description('refresh Dashboard.md')
47
51
  .option('-c, --crew <name>', 'target crew name')
48
52
  .action(crewBoard);
49
53
 
50
- crew.command('sync')
54
+ orchestrate.command('sync')
51
55
  .description('scan Markdown tasks and update crew state cache')
52
56
  .option('-c, --crew <name>', 'target crew name')
53
57
  .action(crewSync);
54
58
 
55
- crew.command('doctor')
59
+ orchestrate.command('doctor')
56
60
  .description('check crew config, vault, rules, and project wiring')
57
61
  .option('-c, --crew <name>', 'target crew name')
58
62
  .action(crewDoctor);
59
63
 
60
- const project = crew.command('project').description('manage project owners');
64
+ orchestrate.command('engine')
65
+ .description('show which execution engine resolves and why (5-level order; debug)')
66
+ .option('-c, --crew <name>', 'target crew name')
67
+ .option('-p, --project <name>', 'resolve as if dispatching for this project')
68
+ .option('--engine <engine>', 'explicit task-level engine override (claude | codex | pi)')
69
+ .option('--json', 'machine-readable output')
70
+ .action(crewEngine);
71
+
72
+ orchestrate.command('dispatch <project>')
73
+ .description('run a prompt via the resolved engine in a fresh task/<id> worktree of <project> (MVP runtime)')
74
+ .requiredOption('--prompt <text>', 'prompt for the engine')
75
+ .option('--engine <engine>', 'engine override (claude | codex | pi); else resolved from project/crew/host')
76
+ .option('--task <id>', 'task id for the worktree branch (default: adhoc-<ts>)')
77
+ .option('--timeout <ms>', 'timeout in ms (default 600000)')
78
+ .option('-c, --crew <name>', 'target crew name')
79
+ .action(crewDispatch);
80
+
81
+ const project = orchestrate.command('project').description('manage project owners');
61
82
  project.command('add <name>')
62
83
  .description('register a project owner')
63
84
  .requiredOption('--path <path>', 'project path')
@@ -86,7 +107,7 @@ export function registerCrewCommands(program: any): void {
86
107
  .option('-c, --crew <name>', 'target crew name')
87
108
  .action(projectStatus);
88
109
 
89
- const role = crew.command('role').description('manage global crew roles');
110
+ const role = orchestrate.command('role').description('manage global crew roles');
90
111
  role.command('add <name>')
91
112
  .description('add or update a global role from a markdown description')
92
113
  .requiredOption('--from <file>', 'role description markdown file')
@@ -106,10 +127,10 @@ export function registerCrewCommands(program: any): void {
106
127
  .option('-c, --crew <name>', 'target crew name')
107
128
  .action(roleShow);
108
129
 
109
- const pending = crew.command('pending')
130
+ const pending = orchestrate.command('pending')
110
131
  .description('list and manage materials the user must supply (per project)');
111
132
  pending.command('list', { isDefault: true })
112
- .description('list pending questions (default action; runs when `crew pending` is used without a subcommand)')
133
+ .description('list pending questions (default action; runs when `orchestrate pending` is used without a subcommand)')
113
134
  .option('--crew <name>', 'target crew name')
114
135
  .option('-p, --project <name>', 'limit to one project')
115
136
  .option('--all', 'include resolved questions')
@@ -122,7 +143,7 @@ export function registerCrewCommands(program: any): void {
122
143
  .option('--why <text>', 'why this information is needed')
123
144
  .option('--need <text>', 'what to provide (file path, value, decision, etc.)')
124
145
  .option('--priority <level>', 'high | medium | low')
125
- .option('--asked-by <role>', 'role or person who raised the question (default: lead)')
146
+ .option('--asked-by <role>', 'role or person who raised the question (default: orchestrator)')
126
147
  .action((title: string[], opts: { crew?: string; project?: string; why?: string; need?: string; priority?: string; askedBy?: string }) => pendingAdd(title, opts));
127
148
  pending.command('answer <id>')
128
149
  .description('mark a pending question as resolved')
@@ -136,14 +157,14 @@ export function registerCrewCommands(program: any): void {
136
157
  .option('-p, --project <name>', 'limit to one project')
137
158
  .action(pendingRemove);
138
159
 
139
- crew.command('plan').description('planned: Lead refine + decompose').action(crewStub('plan'));
140
- crew.command('refine').description('planned: update Shared/Spec.md').action(crewStub('refine'));
141
- crew.command('decompose').description('planned: split Spec into owner/role tasks').action(crewStub('decompose'));
142
- crew.command('run [target]').description('planned: run owner/role work').action(crewStub('run'));
143
- crew.command('research <topic...>').description('planned: ask Researcher to write a report').action(crewStub('research'));
144
- crew.command('report').description('planned: generate Lead report').action(crewStub('report'));
160
+ orchestrate.command('plan').description('planned: Orchestrator refine + decompose').action(crewStub('plan'));
161
+ orchestrate.command('refine').description('planned: update Shared/Spec.md').action(crewStub('refine'));
162
+ orchestrate.command('decompose').description('planned: split Spec into owner/role tasks').action(crewStub('decompose'));
163
+ orchestrate.command('run [target]').description('planned: run owner/role work').action(crewStub('run'));
164
+ orchestrate.command('research <topic...>').description('planned: ask Researcher to write a report').action(crewStub('research'));
165
+ orchestrate.command('report').description('planned: generate Orchestrator report').action(crewStub('report'));
145
166
 
146
- const pm = crew.command('pm').description('PM tools');
167
+ const pm = orchestrate.command('pm').description('PM tools');
147
168
  pm.command('prd').description('planned: generate/update PRD').action(crewStub('pm prd'));
148
169
  pm.command('review <file>').description('planned: review PRD/Spec').action(crewStub('pm review'));
149
170
  }