rigjs 3.0.32 → 4.0.2

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.
Files changed (100) hide show
  1. package/.claude/skills/rig-wiki/SKILL.md +104 -0
  2. package/.claude-plugin/plugin.json +14 -0
  3. package/README.md +18 -1
  4. package/README_CN.md +17 -1
  5. package/RIG_CREW_SKILL.md +274 -0
  6. package/RIG_WIKI_SKILL.md +104 -0
  7. package/bin/rig.js +0 -0
  8. package/built/index.js +376 -299
  9. package/doc/architecture/README.md +139 -0
  10. package/doc/architecture/agents.md +180 -0
  11. package/doc/architecture/fc.md +17 -0
  12. package/doc/architecture/wiki.md +278 -0
  13. package/lib/crew/ask.ts +24 -0
  14. package/lib/crew/board.ts +123 -0
  15. package/lib/crew/config.ts +109 -0
  16. package/lib/crew/doctor.ts +40 -0
  17. package/lib/crew/inbox.ts +29 -0
  18. package/lib/crew/index.ts +108 -0
  19. package/lib/crew/init.ts +113 -0
  20. package/lib/crew/paths.ts +13 -0
  21. package/lib/crew/project.ts +84 -0
  22. package/lib/crew/role.ts +121 -0
  23. package/lib/crew/roleCommand.ts +150 -0
  24. package/lib/crew/state.ts +19 -0
  25. package/lib/crew/status.ts +27 -0
  26. package/lib/crew/stub.ts +9 -0
  27. package/lib/crew/sync.ts +15 -0
  28. package/lib/crew/task.ts +92 -0
  29. package/lib/crew/vault.ts +266 -0
  30. package/lib/installLocal.ts +189 -0
  31. package/lib/rig/index.ts +26 -3
  32. package/lib/tag/index.ts +1 -1
  33. package/lib/wiki/README.md +79 -0
  34. package/lib/wiki/agent/claude.ts +65 -0
  35. package/lib/wiki/agent/codex.ts +22 -0
  36. package/lib/wiki/agent/index.ts +11 -0
  37. package/lib/wiki/agent/list.ts +27 -0
  38. package/lib/wiki/agent/pi.ts +21 -0
  39. package/lib/wiki/agent/registry.ts +16 -0
  40. package/lib/wiki/agent/types.ts +37 -0
  41. package/lib/wiki/agent/use.ts +21 -0
  42. package/lib/wiki/config.ts +99 -0
  43. package/lib/wiki/daemon/index.ts +25 -0
  44. package/lib/wiki/daemon/install.ts +69 -0
  45. package/lib/wiki/daemon/logs.ts +16 -0
  46. package/lib/wiki/daemon/runner.ts +42 -0
  47. package/lib/wiki/daemon/start.ts +20 -0
  48. package/lib/wiki/daemon/status.ts +23 -0
  49. package/lib/wiki/daemon/stop.ts +16 -0
  50. package/lib/wiki/daemon/uninstall.ts +17 -0
  51. package/lib/wiki/db.ts +71 -0
  52. package/lib/wiki/fetch.ts +206 -0
  53. package/lib/wiki/index.ts +106 -0
  54. package/lib/wiki/indexCmd.ts +23 -0
  55. package/lib/wiki/ingest.ts +271 -0
  56. package/lib/wiki/init.ts +125 -0
  57. package/lib/wiki/installSkill.ts +92 -0
  58. package/lib/wiki/lint.ts +252 -0
  59. package/lib/wiki/list.ts +69 -0
  60. package/lib/wiki/pathGuard.ts +87 -0
  61. package/lib/wiki/paths.ts +29 -0
  62. package/lib/wiki/platform.ts +8 -0
  63. package/lib/wiki/qmd.ts +205 -0
  64. package/lib/wiki/query.ts +144 -0
  65. package/lib/wiki/rebuild.ts +56 -0
  66. package/lib/wiki/register.ts +94 -0
  67. package/lib/wiki/scan.ts +0 -0
  68. package/lib/wiki/uninstallSkill.ts +37 -0
  69. package/lib/wiki/unregister.ts +16 -0
  70. package/package.json +36 -6
  71. package/scripts/postinstall.mjs +108 -0
  72. package/scripts/publish.mjs +93 -0
  73. package/scripts/sync-skill.mjs +33 -0
  74. package/scripts/version-code.mjs +86 -0
  75. package/skills.md +54 -0
  76. package/.github/workflows/npm-publish.yml +0 -22
  77. package/demo/.env.oem1 +0 -4
  78. package/demo/.env.oem2 +0 -4
  79. package/demo/babel.config.js +0 -5
  80. package/demo/env.rig.json5 +0 -8
  81. package/demo/jsconfig.json +0 -19
  82. package/demo/package.json +0 -59
  83. package/demo/package.rig.json5 +0 -78
  84. package/demo/public/favicon.ico +0 -0
  85. package/demo/public/index.html +0 -17
  86. package/demo/rig_dev/.gitkeep +0 -0
  87. package/demo/rig_helper.d.ts +0 -4
  88. package/demo/rig_helper.js +0 -10
  89. package/demo/rigs/.gitkeep +0 -0
  90. package/demo/src/App.vue +0 -34
  91. package/demo/src/assets/logo.png +0 -0
  92. package/demo/src/components/HelloWorld.vue +0 -58
  93. package/demo/src/main.js +0 -8
  94. package/demo/vue.config.js +0 -8
  95. package/demo/yarn.lock +0 -6312
  96. package/develop.png +0 -0
  97. package/jest/test.rig.json5 +0 -14
  98. package/jest.config.ts +0 -16
  99. package/production.png +0 -0
  100. package/tsconfig.json +0 -53
@@ -0,0 +1,123 @@
1
+ import path from 'path';
2
+ import print from '../print';
3
+ import { requireCrew, shortPath } from './config';
4
+ import { scanTasks, openInboxTasks, summarize, taskProgress, CrewTask } from './task';
5
+ import { rootPath, writeText, readText } from './vault';
6
+ import { writeCrewState } from './state';
7
+ import { roleDefinitionsForCrew } from './role';
8
+
9
+ interface BoardOpts { crew?: string; }
10
+
11
+ export default function crewBoard(opts: BoardOpts): void {
12
+ const crew = requireCrew(opts.crew);
13
+ const tasks = scanTasks(crew);
14
+ const inbox = openInboxTasks(crew);
15
+ const summary = summarize(tasks);
16
+ const dashboard = renderDashboard(crew, tasks, inbox);
17
+ const file = rootPath(crew, 'Team-Dashboard.md');
18
+ writeText(file, dashboard);
19
+ writeCrewState(crew, tasks);
20
+ print.succeed(`crew dashboard refreshed: ${shortPath(file)}`);
21
+ print.info(`tasks: ${summary.done}/${summary.total} done, inbox: ${inbox.length} open`);
22
+ }
23
+
24
+ function renderDashboard(crew: ReturnType<typeof requireCrew>, tasks: CrewTask[], inbox: CrewTask[]): string {
25
+ const summary = summarize(tasks);
26
+ const health = summary.blocked > 0 ? 'At Risk' : 'On Track';
27
+ const goal = currentGoal(rootPath(crew, 'Current-Goal.md'));
28
+ return [
29
+ '# Team Dashboard',
30
+ '',
31
+ `Last updated: ${new Date().toISOString()}`,
32
+ '',
33
+ '## Lead Brief',
34
+ '',
35
+ `Current Goal: ${goal || '_No current goal yet_'}`,
36
+ `Overall: ${taskProgress(tasks)}% (${summary.done}/${summary.total})`,
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.'}`,
39
+ '',
40
+ '## Needs Your Attention',
41
+ '',
42
+ inboxTable(inbox),
43
+ '',
44
+ '## Project Progress',
45
+ '',
46
+ projectTable(crew, tasks),
47
+ '',
48
+ '## Role Progress',
49
+ '',
50
+ roleTable(crew, tasks),
51
+ '',
52
+ '## Blockers',
53
+ '',
54
+ blockersTable(tasks),
55
+ '',
56
+ '## Active Tasks',
57
+ '',
58
+ activeTable(tasks),
59
+ '',
60
+ ].join('\n');
61
+ }
62
+
63
+ function currentGoal(file: string): string {
64
+ const lines = readText(file)
65
+ .split(/\r?\n/)
66
+ .map(l => l.trim())
67
+ .filter(l => l && l.charAt(0) !== '#');
68
+ return lines.length ? lines[lines.length - 1].replace(/^\-\s*/, '') : '';
69
+ }
70
+
71
+ function inboxTable(tasks: CrewTask[]): string {
72
+ if (tasks.length === 0) return '_No open inbox items._';
73
+ const rows = tasks.map(t => `| ${t.id || '-'} | ${t.fields.type || '-'} | ${cleanTaskText(t.text)} | ${t.fields.priority || '-'} |`);
74
+ return ['| ID | Type | Item | Priority |', '|---|---|---|---|'].concat(rows).join('\n');
75
+ }
76
+
77
+ function projectTable(crew: ReturnType<typeof requireCrew>, tasks: CrewTask[]): string {
78
+ const projects = crew.projects || [];
79
+ if (projects.length === 0) return '_No projects registered yet._';
80
+ const rows = projects.map(p => {
81
+ const scoped = tasks.filter(t => t.scope !== 'inbox' && (t.scope === `project:${p.name}` || t.fields.project === p.name));
82
+ const s = summarize(scoped);
83
+ const health = s.blocked > 0 ? 'At Risk' : 'On Track';
84
+ return `| ${p.name} | ${p.owner} | ${p.defaultExecutor || crew.defaultExecutor || 'claude'} | ${health} | ${s.open} | ${s.blocked} | ${shortPath(p.path)} |`;
85
+ });
86
+ return ['| Project | Owner | Executor | Health | Open | Blocked | Path |', '|---|---|---|---|---:|---:|---|'].concat(rows).join('\n');
87
+ }
88
+
89
+ function roleTable(crew: ReturnType<typeof requireCrew>, tasks: CrewTask[]): string {
90
+ const roles = roleDefinitionsForCrew(crew);
91
+ const rows = roles.map(role => {
92
+ const scoped = tasks.filter(t => t.scope === `role:${role.name}` || t.fields.role === role.name || t.fields.owner === role.name);
93
+ const s = summarize(scoped);
94
+ return `| ${role.title} | ${taskProgress(scoped)}% | ${s.doing} | ${s.blocked} | ${s.open} |`;
95
+ });
96
+ return ['| Role | Progress | WIP | Blocked | Open |', '|---|---:|---:|---:|---:|'].concat(rows).join('\n');
97
+ }
98
+
99
+ function blockersTable(tasks: CrewTask[]): string {
100
+ const blocked = tasks.filter(t => (t.fields.status || '').toLowerCase() === 'blocked');
101
+ if (blocked.length === 0) return '_No blockers._';
102
+ const rows = blocked.slice(0, 20).map(t => `| ${t.id || '-'} | ${t.fields.owner || t.scope} | ${cleanTaskText(t.text)} | ${relFile(t)} |`);
103
+ return ['| ID | Owner | Blocker | File |', '|---|---|---|---|'].concat(rows).join('\n');
104
+ }
105
+
106
+ function activeTable(tasks: CrewTask[]): string {
107
+ const active = tasks.filter(t => !t.done && t.scope !== 'inbox').slice(0, 20);
108
+ if (active.length === 0) return '_No active tasks._';
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
+ return ['| ID | Project | Owner | Status | Task |', '|---|---|---|---|---|'].concat(rows).join('\n');
111
+ }
112
+
113
+ function displayScope(scope: string): string {
114
+ return scope.startsWith('role:') ? scope.slice('role:'.length) : scope;
115
+ }
116
+
117
+ function cleanTaskText(text: string): string {
118
+ return text.replace(/\[[A-Za-z0-9_-]+::\s*[^\]]+\]/g, '').trim().replace(/\|/g, '\\|');
119
+ }
120
+
121
+ function relFile(t: CrewTask): string {
122
+ return path.basename(t.file) + ':' + t.line;
123
+ }
@@ -0,0 +1,109 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { crewPaths } from './paths';
4
+ import { BUILTIN_ROLE_NAMES, loadGlobalRoleNames, normalizeRoleNames } from './role';
5
+
6
+ export type CrewRole = string;
7
+ export type CrewExecutor = 'claude' | 'codex' | 'pi';
8
+
9
+ export interface CrewProject {
10
+ name: string;
11
+ path: string;
12
+ owner: string;
13
+ defaultExecutor?: CrewExecutor;
14
+ canWriteCode?: boolean;
15
+ defaultTestCommand?: string;
16
+ }
17
+
18
+ export interface CrewEntry {
19
+ name: string;
20
+ vault: string;
21
+ root: string;
22
+ defaultExecutor?: CrewExecutor;
23
+ mode?: 'leader-first';
24
+ dashboard?: string;
25
+ state?: { backend?: 'sqlite' | 'json' };
26
+ roles?: CrewRole[];
27
+ projects?: CrewProject[];
28
+ }
29
+
30
+ export interface CrewConfig {
31
+ defaultCrew?: string;
32
+ crews: CrewEntry[];
33
+ }
34
+
35
+ export const DEFAULT_ROLES: CrewRole[] = BUILTIN_ROLE_NAMES;
36
+ export const DEFAULT_CREW_ROOT = 'rig-agents';
37
+
38
+ const DEFAULT_CONFIG: CrewConfig = { crews: [] };
39
+
40
+ export function ensureCrewHome(): void {
41
+ fs.mkdirSync(crewPaths.home, { recursive: true });
42
+ fs.mkdirSync(crewPaths.crewDir, { recursive: true });
43
+ fs.mkdirSync(crewPaths.rolesDir, { recursive: true });
44
+ }
45
+
46
+ export function loadCrewConfig(): CrewConfig {
47
+ ensureCrewHome();
48
+ if (!fs.existsSync(crewPaths.config)) return { ...DEFAULT_CONFIG };
49
+ try {
50
+ const cfg = JSON.parse(fs.readFileSync(crewPaths.config, 'utf8')) as CrewConfig;
51
+ if (!Array.isArray(cfg.crews)) cfg.crews = [];
52
+ return cfg;
53
+ } catch (e: any) {
54
+ throw new Error(`failed to parse ${crewPaths.config}: ${e.message}`);
55
+ }
56
+ }
57
+
58
+ export function saveCrewConfig(cfg: CrewConfig): void {
59
+ ensureCrewHome();
60
+ fs.writeFileSync(crewPaths.config, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
61
+ }
62
+
63
+ export function upsertCrew(entry: CrewEntry): CrewConfig {
64
+ const cfg = loadCrewConfig();
65
+ const i = cfg.crews.findIndex(c => c.name === entry.name);
66
+ if (i >= 0) cfg.crews[i] = entry;
67
+ else cfg.crews.push(entry);
68
+ if (!cfg.defaultCrew) cfg.defaultCrew = entry.name;
69
+ saveCrewConfig(cfg);
70
+ return cfg;
71
+ }
72
+
73
+ export function resolveCrew(name?: string): CrewEntry | undefined {
74
+ const cfg = loadCrewConfig();
75
+ if (name) return cfg.crews.find(c => c.name === name);
76
+ if (cfg.defaultCrew) {
77
+ const found = cfg.crews.find(c => c.name === cfg.defaultCrew);
78
+ if (found) return found;
79
+ }
80
+ if (cfg.crews.length === 1) return cfg.crews[0];
81
+ const cwd = process.cwd();
82
+ return cfg.crews.find(c => cwd === c.vault || cwd.startsWith(c.vault + path.sep));
83
+ }
84
+
85
+ export function normalizeCrew(entry: CrewEntry): CrewEntry {
86
+ return {
87
+ ...entry,
88
+ root: entry.root || DEFAULT_CREW_ROOT,
89
+ dashboard: entry.dashboard || path.join(entry.root || DEFAULT_CREW_ROOT, 'Team-Dashboard.md'),
90
+ defaultExecutor: entry.defaultExecutor || 'claude',
91
+ mode: entry.mode || 'leader-first',
92
+ state: entry.state || { backend: 'json' },
93
+ roles: normalizeRoleNames([...(entry.roles || DEFAULT_ROLES), ...loadGlobalRoleNames()]),
94
+ projects: entry.projects || [],
95
+ };
96
+ }
97
+
98
+ export function requireCrew(name?: string): CrewEntry {
99
+ const crew = resolveCrew(name);
100
+ if (!crew) {
101
+ throw new Error('no crew configured. Run `rig crew init --vault <path>` first.');
102
+ }
103
+ return normalizeCrew(crew);
104
+ }
105
+
106
+ export function shortPath(p: string): string {
107
+ const home = process.env.HOME || '';
108
+ return home && p.startsWith(home) ? '~' + p.slice(home.length) : p;
109
+ }
@@ -0,0 +1,40 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import print from '../print';
4
+ import { requireCrew, shortPath } from './config';
5
+ import { crewPaths } from './paths';
6
+ import { rootPath } from './vault';
7
+ import { loadCustomRoleDefinitions } from './role';
8
+
9
+ interface DoctorOpts { crew?: string; }
10
+
11
+ export default function crewDoctor(opts: DoctorOpts): void {
12
+ const crew = requireCrew(opts.crew);
13
+ const checks: { name: string; ok: boolean; detail: string }[] = [];
14
+ checks.push({ name: 'crew config', ok: fs.existsSync(crewPaths.config), detail: shortPath(crewPaths.config) });
15
+ checks.push({ name: 'vault', ok: fs.existsSync(crew.vault), detail: shortPath(crew.vault) });
16
+ checks.push({ name: 'crew root', ok: fs.existsSync(rootPath(crew, '')), detail: shortPath(rootPath(crew, '')) });
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') });
19
+ checks.push({ name: 'vault CLAUDE.md', ok: fs.existsSync(path.join(crew.vault, 'CLAUDE.md')), detail: 'CLAUDE.md' });
20
+ checks.push({ name: 'vault AGENTS.md', ok: fs.existsSync(path.join(crew.vault, 'AGENTS.md')), detail: 'AGENTS.md' });
21
+ checks.push({ name: 'user RIG.md', ok: fs.existsSync(crewPaths.userRules), detail: shortPath(crewPaths.userRules) });
22
+ checks.push({ name: 'roles dir', ok: fs.existsSync(crewPaths.rolesDir), detail: shortPath(crewPaths.rolesDir) });
23
+ checks.push({ name: 'role registry', ok: fs.existsSync(rootPath(crew, 'Shared/Roles.md')), detail: path.join(crew.root, 'Shared/Roles.md') });
24
+ for (const role of loadCustomRoleDefinitions()) {
25
+ checks.push({ name: `role ${role.name} prompt`, ok: !!role.promptPath && fs.existsSync(role.promptPath), detail: role.promptPath ? shortPath(role.promptPath) : '-' });
26
+ }
27
+ for (const project of crew.projects || []) {
28
+ const rig = path.join(project.path, 'RIG.md');
29
+ const rigLower = path.join(project.path, 'rig.md');
30
+ checks.push({ name: `project ${project.name} RIG.md`, ok: fs.existsSync(rig) || fs.existsSync(rigLower), detail: shortPath(rig) });
31
+ checks.push({ name: `project ${project.name} path`, ok: fs.existsSync(project.path), detail: shortPath(project.path) });
32
+ }
33
+
34
+ let failed = 0;
35
+ for (const c of checks) {
36
+ if (c.ok) print.succeed(`${c.name}: ${c.detail}`);
37
+ else { failed++; print.warn(`${c.name}: missing (${c.detail})`); }
38
+ }
39
+ if (failed > 0) process.exitCode = 1;
40
+ }
@@ -0,0 +1,29 @@
1
+ import print from '../print';
2
+ import { requireCrew } from './config';
3
+ import { openInboxTasks } from './task';
4
+
5
+ interface InboxOpts { crew?: string; json?: boolean; }
6
+
7
+ export default function crewInbox(opts: InboxOpts): void {
8
+ const crew = requireCrew(opts.crew);
9
+ const items = openInboxTasks(crew);
10
+ if (opts.json) {
11
+ // eslint-disable-next-line no-console
12
+ console.log(JSON.stringify({ ok: true, data: items }, null, 2));
13
+ return;
14
+ }
15
+ if (items.length === 0) {
16
+ print.info('inbox is empty.');
17
+ return;
18
+ }
19
+ print.info(`open inbox items: ${items.length}`);
20
+ for (const t of items) {
21
+ // eslint-disable-next-line no-console
22
+ console.log(`- ${t.id || 'NO-ID'} ${clean(t.text)} (${t.fields.priority || 'no priority'})`);
23
+ }
24
+ }
25
+
26
+ function clean(text: string): string {
27
+ return text.replace(/\[[A-Za-z0-9_-]+::\s*[^\]]+\]/g, '').trim();
28
+ }
29
+
@@ -0,0 +1,108 @@
1
+ import crewInit from './init';
2
+ import crewStatus from './status';
3
+ import crewBoard from './board';
4
+ import crewInbox from './inbox';
5
+ import crewSync from './sync';
6
+ import crewDoctor from './doctor';
7
+ import crewAsk from './ask';
8
+ import crewStub from './stub';
9
+ import { projectAdd, projectList, projectStatus } from './project';
10
+ import { roleAdd, roleList, roleShow } from './roleCommand';
11
+
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ export function registerCrewCommands(program: any): void {
14
+ const crew = program.command('crew [message...]')
15
+ .description('Leader-first multi-agent workspace over an Obsidian vault')
16
+ .option('-c, --crew <name>', 'target crew name')
17
+ .action((message: string[] | undefined, opts: { crew?: string }) => crewAsk(message, opts));
18
+
19
+ crew.command('init')
20
+ .description('initialize a crew vault')
21
+ .requiredOption('--vault <path>', 'Obsidian vault path')
22
+ .option('-n, --as <name>', 'crew name')
23
+ .option('--root <path>', 'root folder inside the vault (default: Agents)')
24
+ .option('--allow-project-vault', 'allow a vault path under projects/<submodule>')
25
+ .action(crewInit);
26
+
27
+ crew.command('ask <message...>')
28
+ .description('send a message to the Lead (MVP appends to Current-Goal.md)')
29
+ .option('-c, --crew <name>', 'target crew name')
30
+ .action(crewAsk);
31
+
32
+ crew.command('status')
33
+ .description('show crew progress summary')
34
+ .option('-c, --crew <name>', 'target crew name')
35
+ .option('--json', 'machine-readable output')
36
+ .action(crewStatus);
37
+
38
+ crew.command('inbox')
39
+ .description('show open user attention items')
40
+ .option('-c, --crew <name>', 'target crew name')
41
+ .option('--json', 'machine-readable output')
42
+ .action(crewInbox);
43
+
44
+ crew.command('board')
45
+ .description('refresh Agents/Team-Dashboard.md')
46
+ .option('-c, --crew <name>', 'target crew name')
47
+ .action(crewBoard);
48
+
49
+ crew.command('sync')
50
+ .description('scan Markdown tasks and update crew state cache')
51
+ .option('-c, --crew <name>', 'target crew name')
52
+ .action(crewSync);
53
+
54
+ crew.command('doctor')
55
+ .description('check crew config, vault, rules, and project wiring')
56
+ .option('-c, --crew <name>', 'target crew name')
57
+ .action(crewDoctor);
58
+
59
+ const project = crew.command('project').description('manage project owners');
60
+ project.command('add <name>')
61
+ .description('register a project owner')
62
+ .requiredOption('--path <path>', 'project path')
63
+ .option('--owner <name>', 'owner alias (default: maintainer:<name>)')
64
+ .option('--executor <name>', 'claude | codex | pi (default: crew defaultExecutor)')
65
+ .option('--test-command <cmd>', 'default focused test command')
66
+ .option('--no-write', 'mark owner as read-only')
67
+ .option('-c, --crew <name>', 'target crew name')
68
+ .action(projectAdd);
69
+ project.command('list')
70
+ .description('list registered projects')
71
+ .option('-c, --crew <name>', 'target crew name')
72
+ .action(projectList);
73
+ project.command('status <name>')
74
+ .description('show one project status')
75
+ .option('-c, --crew <name>', 'target crew name')
76
+ .action(projectStatus);
77
+
78
+ const role = crew.command('role').description('manage global crew roles');
79
+ role.command('add <name>')
80
+ .description('add or update a global role from a markdown description')
81
+ .requiredOption('--from <file>', 'role description markdown file')
82
+ .option('--title <title>', 'display title')
83
+ .option('--summary <text>', 'short description')
84
+ .option('--agent <name>', 'agent/subagent name to use for this role')
85
+ .option('--executor <name>', 'claude | codex | pi')
86
+ .option('--force', 'update an existing custom role')
87
+ .option('-c, --crew <name>', 'also materialize role files in this crew vault')
88
+ .action(roleAdd);
89
+ role.command('list')
90
+ .description('list built-in and custom roles')
91
+ .option('-c, --crew <name>', 'target crew name')
92
+ .action(roleList);
93
+ role.command('show <name>')
94
+ .description('show one role definition')
95
+ .option('-c, --crew <name>', 'target crew name')
96
+ .action(roleShow);
97
+
98
+ crew.command('plan').description('planned: Lead refine + decompose').action(crewStub('plan'));
99
+ crew.command('refine').description('planned: update Shared/Spec.md').action(crewStub('refine'));
100
+ crew.command('decompose').description('planned: split Spec into owner/role tasks').action(crewStub('decompose'));
101
+ crew.command('run [target]').description('planned: run owner/role work').action(crewStub('run'));
102
+ crew.command('research <topic...>').description('planned: ask Researcher to write a report').action(crewStub('research'));
103
+ crew.command('report').description('planned: generate Lead report').action(crewStub('report'));
104
+
105
+ const pm = crew.command('pm').description('PM tools');
106
+ pm.command('prd').description('planned: generate/update PRD').action(crewStub('pm prd'));
107
+ pm.command('review <file>').description('planned: review PRD/Spec').action(crewStub('pm review'));
108
+ }
@@ -0,0 +1,113 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import print from '../print';
4
+ import { CrewEntry, DEFAULT_CREW_ROOT, DEFAULT_ROLES, loadCrewConfig, saveCrewConfig, normalizeCrew, shortPath } from './config';
5
+ import { ensureCrewVault } from './vault';
6
+ import { crewPaths } from './paths';
7
+
8
+ interface InitOpts {
9
+ vault: string;
10
+ as?: string;
11
+ root?: string;
12
+ allowProjectVault?: boolean;
13
+ }
14
+
15
+ export default function crewInit(opts: InitOpts): void {
16
+ if (!opts.vault) {
17
+ print.error('missing --vault <path>');
18
+ process.exit(1);
19
+ }
20
+ const vault = path.resolve(opts.vault);
21
+ if (isUnderProjects(vault) && !opts.allowProjectVault) {
22
+ print.error('refusing to initialize a crew vault under projects/<submodule>. Pass --allow-project-vault only if you understand the data exposure risk.');
23
+ process.exit(1);
24
+ }
25
+ fs.mkdirSync(vault, { recursive: true });
26
+ const name = opts.as || path.basename(vault) || 'personal';
27
+ const cfg = loadCrewConfig();
28
+ const idx = cfg.crews.findIndex(c => c.name === name);
29
+ const existing = idx >= 0 ? normalizeCrew(cfg.crews[idx]) : undefined;
30
+ const root = opts.root || existing?.root || DEFAULT_CREW_ROOT;
31
+ const entry: CrewEntry = normalizeCrew({
32
+ name,
33
+ vault,
34
+ root,
35
+ defaultExecutor: existing?.defaultExecutor || 'claude',
36
+ mode: 'leader-first',
37
+ dashboard: path.join(root, 'Team-Dashboard.md'),
38
+ state: existing?.state || { backend: 'json' },
39
+ roles: existing?.roles || DEFAULT_ROLES,
40
+ projects: existing?.projects || [],
41
+ });
42
+
43
+ if (idx >= 0) cfg.crews[idx] = entry;
44
+ else cfg.crews.push(entry);
45
+ if (!cfg.defaultCrew) cfg.defaultCrew = name;
46
+ saveCrewConfig(cfg);
47
+ writeUserRulesIfMissing();
48
+ ensureCrewVault(entry);
49
+ print.succeed(`crew "${name}" initialized at ${shortPath(vault)}`);
50
+ print.info(`agent next: use \`rig crew "<user request>"\` or update ${path.join(root, 'Current-Goal.md')} when coordinating this Vault`);
51
+ }
52
+
53
+ function isUnderProjects(p: string): boolean {
54
+ const workspaceRoot = findOvermindRoot(process.cwd()) || findOvermindRoot(p);
55
+ if (!workspaceRoot) return false;
56
+ const projectsDir = path.join(workspaceRoot, 'projects');
57
+ return p === projectsDir || p.startsWith(projectsDir + path.sep);
58
+ }
59
+
60
+ function findOvermindRoot(start: string): string | undefined {
61
+ let dir = path.resolve(start);
62
+ while (true) {
63
+ if (fs.existsSync(path.join(dir, 'AGENTS.md')) && fs.existsSync(path.join(dir, 'projects'))) return dir;
64
+ const parent = path.dirname(dir);
65
+ if (parent === dir) return undefined;
66
+ dir = parent;
67
+ }
68
+ }
69
+
70
+ function writeUserRulesIfMissing(): void {
71
+ if (fs.existsSync(crewPaths.userRules)) return;
72
+ fs.writeFileSync(crewPaths.userRules, `# RIG User Rules
73
+
74
+ ## Credential Sources
75
+
76
+ - Default credential doc: <path-to-private-keychain-or-env-doc>
77
+ - Prefer env vars or system keychain for actual passwords.
78
+ - Never copy passwords, cookies, auth storage, production traces, or real user data into project repos.
79
+
80
+ ## Account Aliases
81
+
82
+ | Alias | Source | Usage |
83
+ |---|---|---|
84
+ | \`<project>.staging.viewer\` | <env/keychain/doc section> | Read-only staging E2E |
85
+ | \`<project>.production.readonly\` | <env/keychain/doc section> | Production read-only reproduction, \`@prod-readonly\` only |
86
+
87
+ ## Research Output Policy
88
+
89
+ - Default research report directory: <crew-root>/Researcher/Reports
90
+ - Resolve relative paths from the current crew Vault root.
91
+ - If the user requests an explicit output directory, use that directory unless it is inside a project submodule or contains secrets.
92
+ - If neither the user request nor this section gives a clear destination, ask Lead to create an Inbox question instead of guessing.
93
+
94
+ | Scope | Directory | Notes |
95
+ |---|---|---|
96
+ | default | <crew-root>/Researcher/Reports | General research reports |
97
+ | project:<name> | <crew-root>/Projects/<name>/Research | Project-specific research notes |
98
+
99
+ ## Production Rules
100
+
101
+ - Production tests are opt-in only.
102
+ - Use read-only test accounts only.
103
+ - Do not create, update, delete, pay, publish, deploy, or message real users.
104
+
105
+ ## Frontend Testing Rules
106
+
107
+ - Default to PRD-scoped Playwright E2E for frontend/UI behavior.
108
+ - Do not add frontend unit tests by default.
109
+ - Do not add frontend integration tests by default.
110
+ - Do not run full historical E2E unless risk requires it.
111
+ - Always report what was run and what was intentionally skipped.
112
+ `, 'utf8');
113
+ }
@@ -0,0 +1,13 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+
4
+ export const RIG_HOME = process.env.RIG_HOME || path.join(os.homedir(), '.rig');
5
+
6
+ export const crewPaths = {
7
+ home: RIG_HOME,
8
+ crewDir: path.join(RIG_HOME, 'crew'),
9
+ rolesDir: path.join(RIG_HOME, 'crew', 'roles'),
10
+ config: path.join(RIG_HOME, 'crew.config.json'),
11
+ state: path.join(RIG_HOME, 'crew-state.json'),
12
+ userRules: path.join(RIG_HOME, 'RIG.md'),
13
+ };
@@ -0,0 +1,84 @@
1
+ import path from 'path';
2
+ import print from '../print';
3
+ import { loadCrewConfig, saveCrewConfig, requireCrew, normalizeCrew, CrewProject, shortPath } from './config';
4
+ import { ensureProject } from './vault';
5
+ import { scanTasks, summarize } from './task';
6
+ import { CrewExecutor } from './config';
7
+
8
+ interface ProjectOpts {
9
+ crew?: string;
10
+ path?: string;
11
+ owner?: string;
12
+ executor?: string;
13
+ testCommand?: string;
14
+ noWrite?: boolean;
15
+ }
16
+
17
+ export function projectAdd(name: string, opts: ProjectOpts): void {
18
+ const cfg = loadCrewConfig();
19
+ const crew = requireCrew(opts.crew);
20
+ if (!opts.path) {
21
+ print.error('missing --path <path>');
22
+ process.exit(1);
23
+ }
24
+ const project: CrewProject = {
25
+ name,
26
+ path: path.resolve(opts.path),
27
+ owner: opts.owner || `maintainer:${name}`,
28
+ defaultExecutor: parseExecutor(opts.executor || crew.defaultExecutor || 'claude'),
29
+ canWriteCode: !opts.noWrite,
30
+ defaultTestCommand: opts.testCommand,
31
+ };
32
+ const crewIndex = cfg.crews.findIndex(c => c.name === crew.name);
33
+ const entry = normalizeCrew(cfg.crews[crewIndex]);
34
+ const projects = entry.projects || [];
35
+ const i = projects.findIndex(p => p.name === name);
36
+ if (i >= 0) projects[i] = project;
37
+ else projects.push(project);
38
+ entry.projects = projects;
39
+ cfg.crews[crewIndex] = entry;
40
+ saveCrewConfig(cfg);
41
+ ensureProject(entry, project);
42
+ print.succeed(`project "${name}" registered: ${shortPath(project.path)}`);
43
+ }
44
+
45
+ export function projectList(opts: ProjectOpts): void {
46
+ const crew = requireCrew(opts.crew);
47
+ const projects = crew.projects || [];
48
+ if (projects.length === 0) {
49
+ print.info('no projects registered. Use `rig crew project add <name> --path <path>`.');
50
+ return;
51
+ }
52
+ // eslint-disable-next-line no-console
53
+ console.log('NAME OWNER EXECUTOR CAN WRITE TEST COMMAND PATH');
54
+ // eslint-disable-next-line no-console
55
+ console.log('---- ----- -------- --------- ------------ ----');
56
+ for (const p of projects) {
57
+ // eslint-disable-next-line no-console
58
+ console.log(`${p.name} ${p.owner} ${p.defaultExecutor || crew.defaultExecutor || 'claude'} ${p.canWriteCode === false ? 'no' : 'yes'} ${p.defaultTestCommand || '-'} ${shortPath(p.path)}`);
59
+ }
60
+ }
61
+
62
+ export function projectStatus(name: string, opts: ProjectOpts): void {
63
+ const crew = requireCrew(opts.crew);
64
+ const project = (crew.projects || []).find(p => p.name === name);
65
+ if (!project) {
66
+ print.error(`unknown project: ${name}`);
67
+ process.exit(1);
68
+ }
69
+ const tasks = scanTasks(crew).filter(t => t.scope !== 'inbox' && (t.scope === `project:${name}` || t.fields.project === name));
70
+ const s = summarize(tasks);
71
+ print.info(`project: ${name} (${project.owner})`);
72
+ // eslint-disable-next-line no-console
73
+ console.log(`path: ${shortPath(project.path)}`);
74
+ // eslint-disable-next-line no-console
75
+ console.log(`executor: ${project.defaultExecutor || crew.defaultExecutor || 'claude'}`);
76
+ // eslint-disable-next-line no-console
77
+ console.log(`tasks: ${s.done}/${s.total} done, ${s.open} open, ${s.blocked} blocked`);
78
+ }
79
+
80
+ function parseExecutor(value: string): CrewExecutor {
81
+ if (value === 'claude' || value === 'codex' || value === 'pi') return value;
82
+ print.error(`unknown executor: ${value}. Expected claude, codex, or pi.`);
83
+ process.exit(1);
84
+ }