rigjs 4.0.7 → 4.0.8

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/lib/crew/board.ts CHANGED
@@ -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.fields.project === p.name));
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));
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)} |`;
@@ -89,7 +89,7 @@ function projectTable(crew: ReturnType<typeof requireCrew>, tasks: CrewTask[]):
89
89
  function roleTable(crew: ReturnType<typeof requireCrew>, tasks: CrewTask[]): string {
90
90
  const roles = roleDefinitionsForCrew(crew);
91
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);
92
+ const scoped = tasks.filter(t => t.scope === `legacy-role:${role.name}` || t.scope.includes(`:role:${role.name}`) || t.fields.role === role.name || t.fields.owner === role.name);
93
93
  const s = summarize(scoped);
94
94
  return `| ${role.title} | ${taskProgress(scoped)}% | ${s.doing} | ${s.blocked} | ${s.open} |`;
95
95
  });
@@ -111,7 +111,12 @@ function activeTable(tasks: CrewTask[]): string {
111
111
  }
112
112
 
113
113
  function displayScope(scope: string): string {
114
- return scope.startsWith('role:') ? scope.slice('role:'.length) : scope;
114
+ if (scope.startsWith('legacy-role:')) return scope.slice('legacy-role:'.length);
115
+ const projectRole = scope.match(/^project:([^:]+):role:([^:]+)/);
116
+ if (projectRole) return `${projectRole[1]}/${projectRole[2]}`;
117
+ const projectTasklist = scope.match(/^project:([^:]+):tasklist/);
118
+ if (projectTasklist) return projectTasklist[1];
119
+ return scope;
115
120
  }
116
121
 
117
122
  function cleanTaskText(text: string): string {
@@ -10,7 +10,7 @@ interface DoctorOpts { crew?: string; }
10
10
 
11
11
  export default function crewDoctor(opts: DoctorOpts): void {
12
12
  const crew = requireCrew(opts.crew);
13
- const checks: { name: string; ok: boolean; detail: string }[] = [];
13
+ const checks: { name: string; ok: boolean; detail: string; fatal?: boolean }[] = [];
14
14
  checks.push({ name: 'crew config', ok: fs.existsSync(crewPaths.config), detail: shortPath(crewPaths.config) });
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, '')) });
@@ -27,14 +27,18 @@ export default function crewDoctor(opts: DoctorOpts): void {
27
27
  for (const project of crew.projects || []) {
28
28
  const rig = path.join(project.path, 'RIG.md');
29
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) });
30
+ checks.push({ name: `project ${project.name} RIG.md`, ok: fs.existsSync(rig) || fs.existsSync(rigLower), detail: shortPath(rig), fatal: false });
31
31
  checks.push({ name: `project ${project.name} path`, ok: fs.existsSync(project.path), detail: shortPath(project.path) });
32
+ checks.push({ name: `project ${project.name} agent tasks`, ok: fs.existsSync(rootPath(crew, path.join('Projects', project.name, 'Agents'))), detail: path.join(crew.root, 'Projects', project.name, 'Agents') });
32
33
  }
33
34
 
34
35
  let failed = 0;
35
36
  for (const c of checks) {
36
37
  if (c.ok) print.succeed(`${c.name}: ${c.detail}`);
37
- else { failed++; print.warn(`${c.name}: missing (${c.detail})`); }
38
+ else {
39
+ if (c.fatal !== false) failed++;
40
+ print.warn(`${c.name}: missing (${c.detail})`);
41
+ }
38
42
  }
39
43
  if (failed > 0) process.exitCode = 1;
40
44
  }
package/lib/crew/index.ts CHANGED
@@ -6,7 +6,7 @@ import crewSync from './sync';
6
6
  import crewDoctor from './doctor';
7
7
  import crewAsk from './ask';
8
8
  import crewStub from './stub';
9
- import { projectAdd, projectList, projectStatus } from './project';
9
+ import { projectAdd, projectList, projectStatus, projectSync } from './project';
10
10
  import { roleAdd, roleList, roleShow } from './roleCommand';
11
11
 
12
12
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -66,6 +66,16 @@ export function registerCrewCommands(program: any): void {
66
66
  .option('--no-write', 'mark owner as read-only')
67
67
  .option('-c, --crew <name>', 'target crew name')
68
68
  .action(projectAdd);
69
+ project.command('sync')
70
+ .description('sync project owners from the vault projects/ directory')
71
+ .option('--from <path>', 'projects directory, relative to the vault (default: projects)')
72
+ .option('--executor <name>', 'default executor for newly discovered projects')
73
+ .option('--test-command <cmd>', 'default focused test command for newly discovered projects')
74
+ .option('--no-write', 'mark newly discovered owners as read-only')
75
+ .option('--keep-missing', 'keep projects that disappeared from the scanned directory')
76
+ .option('--no-archive-missing', 'do not archive stale rig-agents project folders')
77
+ .option('-c, --crew <name>', 'target crew name')
78
+ .action(projectSync);
69
79
  project.command('list')
70
80
  .description('list registered projects')
71
81
  .option('-c, --crew <name>', 'target crew name')
@@ -1,7 +1,8 @@
1
+ import fs from 'fs';
1
2
  import path from 'path';
2
3
  import print from '../print';
3
4
  import { loadCrewConfig, saveCrewConfig, requireCrew, normalizeCrew, CrewProject, shortPath } from './config';
4
- import { ensureProject } from './vault';
5
+ import { ensureProject, rootPath } from './vault';
5
6
  import { scanTasks, summarize } from './task';
6
7
  import { CrewExecutor } from './config';
7
8
 
@@ -12,6 +13,9 @@ interface ProjectOpts {
12
13
  executor?: string;
13
14
  testCommand?: string;
14
15
  noWrite?: boolean;
16
+ from?: string;
17
+ keepMissing?: boolean;
18
+ archiveMissing?: boolean;
15
19
  }
16
20
 
17
21
  export function projectAdd(name: string, opts: ProjectOpts): void {
@@ -66,7 +70,7 @@ export function projectStatus(name: string, opts: ProjectOpts): void {
66
70
  print.error(`unknown project: ${name}`);
67
71
  process.exit(1);
68
72
  }
69
- const tasks = scanTasks(crew).filter(t => t.scope !== 'inbox' && (t.scope === `project:${name}` || t.fields.project === name));
73
+ const tasks = scanTasks(crew).filter(t => t.scope !== 'inbox' && (t.scope === `project:${name}` || t.scope.startsWith(`project:${name}:`) || t.fields.project === name));
70
74
  const s = summarize(tasks);
71
75
  print.info(`project: ${name} (${project.owner})`);
72
76
  // eslint-disable-next-line no-console
@@ -77,8 +81,90 @@ export function projectStatus(name: string, opts: ProjectOpts): void {
77
81
  console.log(`tasks: ${s.done}/${s.total} done, ${s.open} open, ${s.blocked} blocked`);
78
82
  }
79
83
 
84
+ export function projectSync(opts: ProjectOpts): void {
85
+ const cfg = loadCrewConfig();
86
+ const crew = requireCrew(opts.crew);
87
+ const crewIndex = cfg.crews.findIndex(c => c.name === crew.name);
88
+ const entry = normalizeCrew(cfg.crews[crewIndex]);
89
+ const scanRoot = path.resolve(entry.vault, opts.from || 'projects');
90
+ if (!fs.existsSync(scanRoot)) {
91
+ print.error(`projects directory not found: ${shortPath(scanRoot)}`);
92
+ process.exit(1);
93
+ }
94
+
95
+ const found = fs.readdirSync(scanRoot, { withFileTypes: true })
96
+ .filter(d => d.isDirectory() && !d.name.startsWith('.') && !d.name.startsWith('_'))
97
+ .map(d => ({ name: d.name, path: path.join(scanRoot, d.name) }))
98
+ .sort((a, b) => a.name.localeCompare(b.name));
99
+
100
+ const existing = new Map((entry.projects || []).map(p => [p.name, p]));
101
+ const seen = new Set(found.map(p => p.name));
102
+ const next: CrewProject[] = [];
103
+ const added: string[] = [];
104
+ const updated: string[] = [];
105
+ const removed: CrewProject[] = [];
106
+
107
+ for (const item of found) {
108
+ const old = existing.get(item.name);
109
+ if (old) {
110
+ const project = { ...old, path: item.path };
111
+ next.push(project);
112
+ updated.push(item.name);
113
+ } else {
114
+ const project: CrewProject = {
115
+ name: item.name,
116
+ path: item.path,
117
+ owner: `maintainer:${item.name}`,
118
+ defaultExecutor: parseExecutor(opts.executor || entry.defaultExecutor || 'claude'),
119
+ canWriteCode: !opts.noWrite,
120
+ defaultTestCommand: opts.testCommand,
121
+ };
122
+ next.push(project);
123
+ added.push(item.name);
124
+ }
125
+ }
126
+
127
+ for (const project of entry.projects || []) {
128
+ if (seen.has(project.name)) continue;
129
+ if (!isInside(project.path, scanRoot)) {
130
+ next.push(project);
131
+ continue;
132
+ }
133
+ if (opts.keepMissing) next.push(project);
134
+ else removed.push(project);
135
+ }
136
+
137
+ entry.projects = next;
138
+ cfg.crews[crewIndex] = entry;
139
+ saveCrewConfig(cfg);
140
+
141
+ for (const project of next) ensureProject(entry, project);
142
+ if (!opts.keepMissing && opts.archiveMissing !== false) {
143
+ for (const project of removed) archiveProjectFolder(entry, project.name);
144
+ }
145
+
146
+ print.succeed(`project registry synced from ${shortPath(scanRoot)}`);
147
+ print.info(`added: ${added.length ? added.join(', ') : '-'}`);
148
+ print.info(`updated: ${updated.length ? updated.join(', ') : '-'}`);
149
+ print.info(`removed: ${removed.length ? removed.map(p => p.name).join(', ') : '-'}`);
150
+ }
151
+
80
152
  function parseExecutor(value: string): CrewExecutor {
81
153
  if (value === 'claude' || value === 'codex' || value === 'pi') return value;
82
154
  print.error(`unknown executor: ${value}. Expected claude, codex, or pi.`);
83
155
  process.exit(1);
84
156
  }
157
+
158
+ function isInside(child: string, parent: string): boolean {
159
+ const rel = path.relative(path.resolve(parent), path.resolve(child));
160
+ return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
161
+ }
162
+
163
+ function archiveProjectFolder(crew: ReturnType<typeof normalizeCrew>, name: string): void {
164
+ const from = rootPath(crew, path.join('Projects', name));
165
+ if (!fs.existsSync(from)) return;
166
+ const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '').replace('T', '-');
167
+ const to = rootPath(crew, path.join('Projects', '_Archived', `${name}-${stamp}`));
168
+ fs.mkdirSync(path.dirname(to), { recursive: true });
169
+ fs.renameSync(from, to);
170
+ }
package/lib/crew/role.ts CHANGED
@@ -17,12 +17,12 @@ export interface CrewRoleDefinition {
17
17
  export const BUILTIN_ROLE_NAMES = ['lead', 'designer', 'pm', 'coder', 'tester', 'researcher'];
18
18
 
19
19
  const BUILTIN_ROLES: CrewRoleDefinition[] = [
20
- { name: 'lead', title: 'Lead', folder: 'Lead', description: 'Primary user-facing coordinator.', builtIn: true },
21
- { name: 'designer', title: 'Designer', folder: 'Designer', description: 'Interaction, UX, information architecture, and visual review.', builtIn: true },
22
- { name: 'pm', title: 'PM', folder: 'PM', description: 'PRD generation, requirements review, scope, and acceptance criteria.', builtIn: true },
23
- { name: 'coder', title: 'Coder', folder: 'Coder', description: 'Implementation work when no project owner is more specific.', builtIn: true },
24
- { name: 'tester', title: 'Tester', folder: 'Tester', description: 'Verification, test planning, and PRD-scoped E2E checks.', builtIn: true },
25
- { name: 'researcher', title: 'Researcher', folder: 'Researcher', description: 'Source-backed research and durable reports.', builtIn: true },
20
+ { name: 'lead', title: 'Lead', folder: 'Lead', description: 'Coordinates goals, project owners, roles, Inbox decisions, and dashboard status.', builtIn: true },
21
+ { name: 'designer', title: 'Designer', folder: 'Designer', description: 'Reviews user flows, interaction details, information architecture, and visual fit.', builtIn: true },
22
+ { name: 'pm', title: 'PM', folder: 'PM', description: 'Turns goals into PRDs, scope boundaries, acceptance criteria, and open questions.', builtIn: true },
23
+ { name: 'coder', title: 'Coder', folder: 'Coder', description: 'Implements project-scoped code tasks assigned by a Project Owner or Lead.', builtIn: true },
24
+ { name: 'tester', title: 'Tester', folder: 'Tester', description: 'Plans and runs verification, defaulting frontend work to PRD-scoped Playwright E2E.', builtIn: true },
25
+ { name: 'researcher', title: 'Researcher', folder: 'Researcher', description: 'Produces source-backed research reports and keeps a lightweight research index.', builtIn: true },
26
26
  ];
27
27
 
28
28
  export function normalizeRoleName(input: string): string {
package/lib/crew/task.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { CrewEntry } from './config';
4
- import { rootPath } from './vault';
5
- import { roleDefinitionsForCrew } from './role';
4
+ import { projectAgentFolder, rootPath } from './vault';
5
+ import { normalizeRoleName, roleDefinitionsForCrew } from './role';
6
6
 
7
7
  export interface CrewTask {
8
8
  id?: string;
@@ -76,17 +76,66 @@ function parseTaskLine(line: string): Omit<CrewTask, 'file' | 'line' | 'scope'>
76
76
  function taskFiles(crew: CrewEntry): { file: string; scope: string }[] {
77
77
  const files = [
78
78
  { file: rootPath(crew, 'Inbox.md'), scope: 'inbox' },
79
- ...roleDefinitionsForCrew(crew).map(role => ({
80
- file: rootPath(crew, path.join(role.folder, 'Tasks.md')),
81
- scope: `role:${role.name}`,
82
- })),
83
79
  ];
80
+ const roles = roleDefinitionsForCrew(crew);
81
+
82
+ // Backward compatibility: old crew roots may still contain global role
83
+ // task files. New init writes role descriptions only and scopes concrete
84
+ // work under Projects/<project>/Agents/<role>/Tasks.md.
85
+ for (const role of roles) {
86
+ const legacy = rootPath(crew, path.join(role.folder, 'Tasks.md'));
87
+ if (fs.existsSync(legacy)) files.push({ file: legacy, scope: `legacy-role:${role.name}` });
88
+ }
89
+
84
90
  const projectsDir = rootPath(crew, 'Projects');
85
- if (fs.existsSync(projectsDir)) {
86
- for (const name of fs.readdirSync(projectsDir)) {
87
- const file = path.join(projectsDir, name, 'Tasks.md');
88
- if (fs.existsSync(file)) files.push({ file, scope: `project:${name}` });
91
+ const projectNames = configuredProjectNames(crew, projectsDir);
92
+ for (const name of projectNames) {
93
+ const base = path.join(projectsDir, name);
94
+ const file = path.join(base, 'Tasks.md');
95
+ if (fs.existsSync(file)) files.push({ file, scope: `project:${name}` });
96
+ files.push(...tasklistFiles(path.join(base, 'Tasklists', 'active'), `project:${name}:tasklist`));
97
+
98
+ const seenRoleFolders = new Set<string>();
99
+ for (const role of roles) {
100
+ const folder = projectAgentFolder(role);
101
+ seenRoleFolders.add(folder);
102
+ const roleFile = path.join(base, 'Agents', folder, 'Tasks.md');
103
+ if (fs.existsSync(roleFile)) files.push({ file: roleFile, scope: `project:${name}:role:${role.name}` });
104
+ files.push(...tasklistFiles(path.join(base, 'Agents', folder, 'Tasklists', 'active'), `project:${name}:role:${role.name}:tasklist`));
105
+ }
106
+
107
+ const agentsDir = path.join(base, 'Agents');
108
+ if (fs.existsSync(agentsDir)) {
109
+ for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
110
+ if (!entry.isDirectory() || seenRoleFolders.has(entry.name)) continue;
111
+ const roleFile = path.join(agentsDir, entry.name, 'Tasks.md');
112
+ const roleName = normalizeRoleName(entry.name);
113
+ if (fs.existsSync(roleFile)) files.push({ file: roleFile, scope: `project:${name}:role:${roleName}` });
114
+ files.push(...tasklistFiles(path.join(agentsDir, entry.name, 'Tasklists', 'active'), `project:${name}:role:${roleName}:tasklist`));
115
+ }
89
116
  }
90
117
  }
91
118
  return files.filter(item => fs.existsSync(item.file));
92
119
  }
120
+
121
+ function configuredProjectNames(crew: CrewEntry, projectsDir: string): string[] {
122
+ if (crew.projects && crew.projects.length > 0) return crew.projects.map(p => p.name);
123
+ if (!fs.existsSync(projectsDir)) return [];
124
+ return fs.readdirSync(projectsDir, { withFileTypes: true })
125
+ .filter(entry => entry.isDirectory() && !entry.name.startsWith('_') && !entry.name.startsWith('.'))
126
+ .map(entry => entry.name);
127
+ }
128
+
129
+ function tasklistFiles(dir: string, scope: string): { file: string; scope: string }[] {
130
+ if (!fs.existsSync(dir)) return [];
131
+ const files: { file: string; scope: string }[] = [];
132
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
133
+ const full = path.join(dir, entry.name);
134
+ if (entry.isDirectory()) {
135
+ files.push(...tasklistFiles(full, scope));
136
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
137
+ files.push({ file: full, scope });
138
+ }
139
+ }
140
+ return files.sort((a, b) => a.file.localeCompare(b.file));
141
+ }
package/lib/crew/vault.ts CHANGED
@@ -46,9 +46,12 @@ export function ensureProject(crew: CrewEntry, project: CrewProject): void {
46
46
  ensureDir(base);
47
47
  writeProjectOwnerFile(path.join(base, 'Owner.md'), project);
48
48
  writeIfMissing(path.join(base, 'Context.md'), `# ${project.name} Context\n\n`);
49
- writeIfMissing(path.join(base, 'Tasks.md'), `# ${project.name} Tasks\n\n`);
49
+ writeIfMissing(path.join(base, 'Tasks.md'), renderProjectTasksFile(project));
50
+ ensureTasklists(base);
50
51
  writeIfMissing(path.join(base, 'Decisions.md'), `# ${project.name} Decisions\n\n`);
51
52
  writeIfMissing(path.join(base, 'Test-Plan.md'), `# ${project.name} Test Plan\n\n`);
53
+ ensureDir(path.join(base, 'Agents'));
54
+ for (const role of roleDefinitionsForCrew(crew)) ensureProjectAgentTasks(base, project, role);
52
55
  ensureDir(path.join(base, 'PRDs'));
53
56
  writeIfMissing(path.join(base, 'PRDs', '.gitkeep'), '');
54
57
  ensureDir(path.join(base, 'Reports'));
@@ -77,17 +80,11 @@ function ensureRole(crew: CrewEntry, role: CrewRoleDefinition): void {
77
80
  const folder = role.folder;
78
81
  const base = rootPath(crew, folder);
79
82
  ensureDir(base);
80
- writeIfMissing(path.join(base, 'Tasks.md'), `# ${folder} Tasks\n\n`);
81
- writeIfMissing(path.join(base, 'Notes.md'), `# ${folder} Notes\n\n`);
83
+ writeIfMissing(path.join(base, 'Role.md'), renderRoleFile(role));
82
84
  if (role.name === 'lead') {
83
85
  ensureDir(path.join(base, 'Reports'));
84
86
  writeIfMissing(path.join(base, 'Reports', '.gitkeep'), '');
85
87
  }
86
- if (role.name === 'pm') {
87
- ensureDir(path.join(base, 'PRDs'));
88
- writeIfMissing(path.join(base, 'PRDs', '.gitkeep'), '');
89
- writeIfMissing(path.join(base, 'Reviews.md'), '# PM Reviews\n\n');
90
- }
91
88
  if (role.name === 'researcher') {
92
89
  ensureDir(path.join(base, 'Reports'));
93
90
  writeIfMissing(path.join(base, 'Reports', '.gitkeep'), '');
@@ -96,7 +93,6 @@ function ensureRole(crew: CrewEntry, role: CrewRoleDefinition): void {
96
93
  if (!role.builtIn) {
97
94
  ensureDir(path.join(base, 'Reports'));
98
95
  writeIfMissing(path.join(base, 'Reports', '.gitkeep'), '');
99
- writeIfMissing(path.join(base, 'Role.md'), renderRoleFile(role));
100
96
  }
101
97
  }
102
98
 
@@ -146,16 +142,68 @@ function renderRoleFile(role: CrewRoleDefinition): string {
146
142
  return [
147
143
  `# ${role.title}`,
148
144
  '',
149
- `Role: ${role.name}`,
150
- `Agent: ${role.agent || '-'}`,
151
- `Executor: ${role.defaultExecutor || '-'}`,
152
- `Prompt: ${role.promptPath || '-'}`,
145
+ `Role: \`${role.name}\``,
146
+ `Agent: \`${role.agent || '-'}\``,
147
+ `Default executor: \`${role.defaultExecutor || '-'}\``,
148
+ `Prompt: \`${role.promptPath || '-'}\``,
149
+ '',
150
+ '## Purpose',
153
151
  '',
154
152
  role.description || '',
155
153
  '',
154
+ '## Task Location',
155
+ '',
156
+ 'This role is reusable across projects. Do not keep normal project work here.',
157
+ 'Assign concrete work under `Projects/<project>/Agents/<role>/Tasks.md`, or under `Projects/<project>/Tasks.md` for project-owner work.',
158
+ '',
159
+ 'Users may edit this file to tune the role.',
160
+ '',
156
161
  ].join('\n');
157
162
  }
158
163
 
164
+ function ensureProjectAgentTasks(base: string, project: CrewProject, role: CrewRoleDefinition): void {
165
+ const dir = path.join(base, 'Agents', projectAgentFolder(role));
166
+ ensureDir(dir);
167
+ writeIfMissing(path.join(dir, 'Tasks.md'), renderProjectAgentTasksFile(project, role));
168
+ ensureTasklists(dir);
169
+ }
170
+
171
+ export function projectAgentFolder(role: CrewRoleDefinition): string {
172
+ if (role.builtIn && !role.folder.includes(path.sep)) return role.folder;
173
+ return role.name;
174
+ }
175
+
176
+ function renderProjectTasksFile(project: CrewProject): string {
177
+ return [
178
+ `# ${project.name} Project Tasks`,
179
+ '',
180
+ 'Keep this file short: current project-owner tasks and cross-role coordination only.',
181
+ 'For larger plans, split current work into `Tasklists/active/<feature-or-iteration>.md`.',
182
+ 'Move completed or stale batches to `Tasklists/archive/YYYY-MM.md`; archive files are not scanned for the active dashboard.',
183
+ 'Role-specific current work belongs in `Agents/<role>/Tasks.md` or `Agents/<role>/Tasklists/active/*.md`.',
184
+ '',
185
+ ].join('\n');
186
+ }
187
+
188
+ function renderProjectAgentTasksFile(project: CrewProject, role: CrewRoleDefinition): string {
189
+ return [
190
+ `# ${project.name} ${role.title} Tasks`,
191
+ '',
192
+ `Use this file for the short current ${role.title} queue scoped to \`${project.name}\`.`,
193
+ 'For larger batches, split current work into `Tasklists/active/<feature-or-iteration>.md`.',
194
+ 'Move completed or stale batches to `Tasklists/archive/YYYY-MM.md`; archive files are not scanned for the active dashboard.',
195
+ `Default inline fields: [project:: ${project.name}] [role:: ${role.name}] [owner:: ${project.owner || `maintainer:${project.name}`}] [status:: pending]`,
196
+ '',
197
+ ].join('\n');
198
+ }
199
+
200
+ function ensureTasklists(base: string): void {
201
+ ensureDir(path.join(base, 'Tasklists', 'active'));
202
+ writeIfMissing(path.join(base, 'Tasklists', 'active', '.gitkeep'), '');
203
+ ensureDir(path.join(base, 'Tasklists', 'archive'));
204
+ writeIfMissing(path.join(base, 'Tasklists', 'archive', '.gitkeep'), '');
205
+ }
206
+
159
207
  function crewPathForHome(): string {
160
208
  return process.env.RIG_HOME || path.join(process.env.HOME || '', '.rig');
161
209
  }
@@ -204,8 +252,10 @@ function renderVaultAgentInstructions(crew: CrewEntry): string {
204
252
  `- Dashboard: \`${root}/Team-Dashboard.md\``,
205
253
  `- Inbox: \`${root}/Inbox.md\``,
206
254
  `- Role registry: \`${root}/Shared/Roles.md\``,
255
+ `- Reusable role descriptions: \`${root}/<role>/Role.md\` and \`${root}/Roles/<custom-role>/Role.md\``,
207
256
  `- Project owner memory: \`${root}/Projects/<project>/\``,
208
- `- Custom role workspaces: \`${root}/Roles/<role>/\``,
257
+ `- Project-scoped agent tasks: \`${root}/Projects/<project>/Agents/<role>/Tasks.md\``,
258
+ `- Large active task batches: \`${root}/Projects/<project>/Tasklists/active/*.md\` and \`${root}/Projects/<project>/Agents/<role>/Tasklists/active/*.md\`. Keep \`Tasks.md\` short; archived tasklists are not part of the active dashboard.`,
209
259
  '- Vault-local scratch projects belong under `tmp/<project>/`.',
210
260
  '- User-level rules, test accounts, custom roles, and research output policy live under `~/.rig/`.',
211
261
  '- Coordinate through Vault files; do not start or assume a separate multi-agent runtime inside a project repo.',
@@ -217,10 +267,10 @@ function renderVaultAgentInstructions(crew: CrewEntry): string {
217
267
  '0. Do not treat `rig crew` as a human-facing command workflow. If you can run the command or update the Vault files yourself, do it instead of asking the human to run it.',
218
268
  '1. If the user asks for planning, multi-agent coordination, PRD, research, testing strategy, project owner work, role routing, reports, or broad project changes, hand the request to Crew Lead first.',
219
269
  `2. Preferred handoff: run \`rig crew "<user request>"\`, then read \`${root}/Team-Dashboard.md\`, \`${root}/Inbox.md\`, and \`${root}/Shared/Roles.md\`.`,
220
- `3. Maintain status awareness before and after work by checking \`${root}/Team-Dashboard.md\`, \`${root}/Inbox.md\`, \`${root}/Shared/Roles.md\`, role \`Tasks.md\`, and project \`Tasks.md\` files.`,
221
- `4. If the CLI is unavailable, append the request to \`${root}/Current-Goal.md\` and create/update a Lead task in \`${root}/Lead/Tasks.md\`; then refresh the dashboard when possible.`,
270
+ `3. Maintain status awareness before and after work by checking \`${root}/Team-Dashboard.md\`, \`${root}/Inbox.md\`, \`${root}/Shared/Roles.md\`, project \`Tasks.md\`, project agent \`Agents/<role>/Tasks.md\`, and active tasklists.`,
271
+ `4. If the CLI is unavailable, append the request to \`${root}/Current-Goal.md\`; when a project is known, route small/current work to \`${root}/Projects/<project>/Tasks.md\` or \`${root}/Projects/<project>/Agents/<role>/Tasks.md\`, and route larger batches to \`Tasklists/active/<feature-or-iteration>.md\`; then refresh the dashboard when possible.`,
222
272
  '5. Treat Crew Lead as the default orchestrator prompt/protocol, not as a required Claude/Codex subagent. Subagents may be used as optional executors for specific roles, but Vault files are the source of truth.',
223
- '6. Lead communicates with other roles through Markdown tasks and delegation packets, not private chat state. Use `[role:: <role>]`, `[owner:: <owner>]`, `[project:: <project>]`, `[executor:: <executor>]`, and status fields in the relevant `Tasks.md`.',
273
+ '6. Lead communicates with other roles through Markdown tasks and delegation packets, not private chat state. Use `[role:: <role>]`, `[owner:: <owner>]`, `[project:: <project>]`, `[executor:: <executor>]`, and status fields in the relevant project-scoped `Tasks.md`.',
224
274
  `7. Worker results must be written back to the relevant role/project files under \`${root}/\`; user-facing questions go to \`${root}/Inbox.md\` for Lead to surface.`,
225
275
  '',
226
276
  AGENT_RULES_END,
@@ -223,16 +223,37 @@ function appendLog(wiki: WikiEntry, relSource: string, applied: string[], dryRun
223
223
  // ----------------------------------------------------------------------
224
224
 
225
225
  function buildPrompt(wiki: WikiEntry, sourceAbs: string): string {
226
- const sourceRel = path.relative(wiki.path, sourceAbs);
227
226
  const sourceSha = crypto.createHash('sha256').update(fs.readFileSync(sourceAbs)).digest('hex');
228
227
  const today = new Date().toISOString();
229
228
 
229
+ // The Obsidian vault root is the parent of the rig-wiki/ metadata dir.
230
+ // The Obsidian vault NAME defaults to that dir's basename. All source-path
231
+ // references in generated pages use obsidian:// URLs so that links survive
232
+ // moves of the wiki dir and are clickable from inside Obsidian.
233
+ const obsidianRoot = path.dirname(wiki.path);
234
+ const obsidianVaultName = path.basename(obsidianRoot);
235
+ const sourceFromObsidianRoot = path.relative(obsidianRoot, sourceAbs);
236
+ const sourceObsidianUrl = obsidianUrl(obsidianVaultName, sourceFromObsidianRoot);
237
+ const ext = path.extname(sourceAbs).toLowerCase();
238
+ const isImage = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'].includes(ext);
239
+ const isPdf = ext === '.pdf';
240
+ const isSpreadsheet = ['.xlsx', '.xls', '.ods', '.numbers'].includes(ext);
241
+
230
242
  return [
231
243
  `You are running INGEST for the rig wiki at \`${wiki.path}\`.`,
244
+ `This wiki lives inside an Obsidian vault rooted at \`${obsidianRoot}\` (Obsidian vault name: "${obsidianVaultName}").`,
245
+ `Use obsidian:// URLs — never raw file paths — to reference sources or any file in the vault.`,
246
+ ``,
247
+ `Source for this ingest: ${sourceObsidianUrl}`,
248
+ `Source filesystem path (for your Read tool only): ${sourceAbs}`,
232
249
  ``,
233
250
  `Step 1 — ANALYSIS (do NOT write files yet):`,
234
- ` - Read \`purpose.md\`, \`schema.md\`, \`overview.md\`, \`index.md\`.`,
235
- ` - Read the source: \`${sourceRel}\`.`,
251
+ ` - Read \`purpose.md\`, \`schema.md\`, \`overview.md\`, \`index.md\` from the wiki.`,
252
+ ` - Read the source. Notes by type:`,
253
+ ` · text / markdown / json / code → use the Read tool normally`,
254
+ isImage ? ` · this source is an IMAGE (${ext}) — Read it; you'll receive it as a visual input. Describe what's depicted, plus any visible text / numbers / structures.` : '',
255
+ isPdf ? ` · this source is a PDF — Read it; if it is >10 pages, read in chunks with the \`pages\` parameter.` : '',
256
+ isSpreadsheet ? ` · this source is a SPREADSHEET (${ext}) — the Read tool does NOT natively decode spreadsheets in v1. Try Read first; if it fails, write a stub source page with the obsidian:// link + filename + last-modified date, and append a \`reviews.md\` bullet asking the user to export it as CSV / JSON for re-ingest. Do NOT invent contents.` : '',
236
257
  ` - In your head, list: entities mentioned, concepts touched, contradictions vs existing pages, items that need human review.`,
237
258
  ``,
238
259
  `Step 2 — GENERATION (write files):`,
@@ -251,16 +272,26 @@ function buildPrompt(wiki: WikiEntry, sourceAbs: string): string {
251
272
  `Source pages additionally need:`,
252
273
  '```yaml',
253
274
  `source-sha: ${sourceSha}`,
254
- `source-path: ${sourceRel}`,
275
+ `source-path: ${sourceObsidianUrl}`,
255
276
  '```',
256
277
  ``,
278
+ `Reference rules — IMPORTANT:`,
279
+ ` - To reference the original source file inside markdown body, use the obsidian:// URL: ${sourceObsidianUrl}`,
280
+ ` - To reference OTHER files in the same Obsidian vault, build URLs the same way: \`obsidian://open?vault=${obsidianVaultName}&file=<vault-relative-path>\`. URL-encode spaces / specials with encodeURI, keep forward slashes.`,
281
+ ` - To reference other wiki pages, use [[wikilink]] (slug only).`,
282
+ ``,
257
283
  `Hard rules — the host will REJECT any patch that violates these:`,
258
284
  ` - DO NOT modify \`raw/\`, \`purpose.md\`, or \`schema.md\`.`,
259
285
  ` - Use kebab-case slugs; no spaces; no date prefixes in page filenames.`,
260
- ` - Link related pages with [[wikilink]]. Every wiki page should link to ≥1 other page.`,
286
+ ` - Every wiki page should link to ≥1 other page (via [[wikilink]] or obsidian:// URL).`,
261
287
  ` - For contradictions, write inline: \`> Contradiction: A vs B (see [[page-A]], [[page-B]])\`.`,
262
288
  ``,
263
289
  `Output: stdout is for status only. All content goes to files via the Write/Edit tools.`,
264
290
  `When done, print a single line: \`INGEST DONE: <slug>\`.`,
265
- ].join('\n');
291
+ ].filter(Boolean).join('\n');
292
+ }
293
+
294
+ /** Build an Obsidian URL. encodeURI preserves `/`, encodes spaces and Unicode. */
295
+ function obsidianUrl(vaultName: string, fileRel: string): string {
296
+ return `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodeURI(fileRel)}`;
266
297
  }
package/lib/wiki/init.ts CHANGED
@@ -98,17 +98,30 @@ proposals/
98
98
  * Defaults for a freshly-scoped vault. The user can edit
99
99
  * `<vault>/.rig/config.yml` afterwards.
100
100
  *
101
- * Hidden directories (segments starting with `.`) and `.gitignore`'d files
102
- * are skipped automatically by the scanner no need to list them here.
101
+ * `include` defaults to `**` (everything) rig wiki is multimodal: Claude
102
+ * Read tool handles markdown / code / json natively, images and PDFs are
103
+ * read as visual / document inputs. The user can tighten this per-vault.
104
+ *
105
+ * `exclude` defaults to common binary-archive extensions whose contents
106
+ * can't be ingested without unpacking. Hidden directories (segments starting
107
+ * with `.`) and `.gitignore`'d files are skipped automatically by the
108
+ * scanner — no need to list them.
103
109
  */
104
110
  function defaultVaultConfig(scope: string, rootRel: string): VaultConfig {
105
111
  return {
106
112
  name: scope,
107
113
  root: rootRel,
108
- include: ['**/*.md'],
109
- exclude: [],
114
+ include: ['**'],
115
+ exclude: [
116
+ '*.zip', '**/*.zip',
117
+ '*.tar', '**/*.tar',
118
+ '*.tar.gz', '**/*.tar.gz',
119
+ '*.tgz', '**/*.tgz',
120
+ '*.7z', '**/*.7z',
121
+ '*.rar', '**/*.rar',
122
+ ],
110
123
  schedule: { scan: '0 */6 * * *', lint: '0 3 * * *', ingest: null },
111
- ingestRules: [{ match: 'raw/**/*.md', mode: 'auto-on-new' }],
124
+ ingestRules: [{ match: 'raw/**/*.*', mode: 'auto-on-new' }],
112
125
  };
113
126
  }
114
127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rigjs",
3
- "version": "4.0.7",
3
+ "version": "4.0.8",
4
4
  "versionCode": 26052414,
5
5
  "description": "A multi-repos dev tool based on yarn and git.Rigjs is intended to be the simplest way to develop,share and deliver codes between different developers or different projects.",
6
6
  "keywords": [