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.
- package/.claude/skills/rig-cicd/SKILL.md +288 -0
- package/.claude/skills/rig-package/SKILL.md +162 -0
- package/RIG_CICD_SKILL.md +288 -0
- package/RIG_CREW_SKILL.md +50 -50
- package/RIG_PACKAGE_SKILL.md +162 -0
- package/built/index.js +346 -259
- package/lib/classes/cicd/CICD.ts +17 -0
- package/lib/classes/cicd/Deploy/ESA.ts +117 -0
- package/lib/crew/ask.ts +3 -3
- package/lib/crew/board.ts +14 -14
- package/lib/crew/config.ts +2 -2
- package/lib/crew/dispatchCommand.ts +58 -0
- package/lib/crew/doctor.ts +1 -1
- package/lib/crew/engine.test.ts +73 -0
- package/lib/crew/engine.ts +103 -0
- package/lib/crew/index.ts +48 -27
- package/lib/crew/init.ts +3 -3
- package/lib/crew/{inbox.ts → pendingQuestions.ts} +6 -7
- package/lib/crew/project.ts +1 -1
- package/lib/crew/role.ts +3 -3
- package/lib/crew/runtime.test.ts +160 -0
- package/lib/crew/runtime.ts +192 -0
- package/lib/crew/status.ts +4 -4
- package/lib/crew/stub.ts +2 -2
- package/lib/crew/task.ts +3 -3
- package/lib/crew/vault.ts +14 -14
- package/lib/init/index.ts +16 -9
- package/lib/publish/index.ts +78 -1
- package/lib/wiki/lint.ts +23 -1
- package/package.json +11 -3
- package/scripts/sync-skill.mjs +2 -0
- package/skills.md +5 -1
- package/lib/utils/redact.test.ts +0 -43
package/lib/classes/cicd/CICD.ts
CHANGED
|
@@ -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
|
|
14
|
-
print.info('MVP
|
|
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, `
|
|
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,
|
|
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
|
|
14
|
+
const pending = openPendingQuestions(crew);
|
|
15
15
|
const summary = summarize(tasks);
|
|
16
|
-
const dashboard = renderDashboard(crew, tasks,
|
|
17
|
-
const file = rootPath(crew, '
|
|
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,
|
|
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[],
|
|
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
|
-
'#
|
|
29
|
+
'# Dashboard',
|
|
30
30
|
'',
|
|
31
31
|
`Last updated: ${new Date().toISOString()}`,
|
|
32
32
|
'',
|
|
33
|
-
'##
|
|
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: ${
|
|
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
|
-
|
|
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
|
|
72
|
-
if (tasks.length === 0) return '_No open
|
|
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 !== '
|
|
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 !== '
|
|
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');
|
package/lib/crew/config.ts
CHANGED
|
@@ -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-
|
|
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, '
|
|
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
|
+
}
|
package/lib/crew/doctor.ts
CHANGED
|
@@ -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: '
|
|
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
|
|
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
|
|
16
|
-
.
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
29
|
-
.description('send a message to the
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
.
|
|
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(
|
|
47
|
+
.action(crewPendingQuestions);
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
.description('refresh
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 `
|
|
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:
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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 =
|
|
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
|
}
|