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/.claude/skills/rig-wiki/SKILL.md +20 -1
- package/RIG_CREW_SKILL.md +31 -8
- package/RIG_WIKI_SKILL.md +20 -1
- package/built/index.js +176 -182
- package/lib/crew/board.ts +8 -3
- package/lib/crew/doctor.ts +7 -3
- package/lib/crew/index.ts +11 -1
- package/lib/crew/project.ts +88 -2
- package/lib/crew/role.ts +6 -6
- package/lib/crew/task.ts +59 -10
- package/lib/crew/vault.ts +67 -17
- package/lib/wiki/ingest.ts +37 -6
- package/lib/wiki/init.ts +18 -5
- package/package.json +1 -1
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
|
-
|
|
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 {
|
package/lib/crew/doctor.ts
CHANGED
|
@@ -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 {
|
|
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')
|
package/lib/crew/project.ts
CHANGED
|
@@ -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: '
|
|
21
|
-
{ name: 'designer', title: 'Designer', folder: 'Designer', description: '
|
|
22
|
-
{ name: 'pm', title: 'PM', folder: 'PM', description: '
|
|
23
|
-
{ name: 'coder', title: 'Coder', folder: 'Coder', description: '
|
|
24
|
-
{ name: 'tester', title: 'Tester', folder: 'Tester', description: '
|
|
25
|
-
{ name: 'researcher', title: 'Researcher', folder: 'Researcher', description: '
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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'),
|
|
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, '
|
|
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:
|
|
150
|
-
`Agent:
|
|
151
|
-
`
|
|
152
|
-
`Prompt:
|
|
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
|
-
`-
|
|
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\`,
|
|
221
|
-
`4. If the CLI is unavailable, append the request to \`${root}/Current-Goal.md
|
|
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,
|
package/lib/wiki/ingest.ts
CHANGED
|
@@ -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
|
|
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: ${
|
|
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
|
-
` -
|
|
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
|
-
*
|
|
102
|
-
*
|
|
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: ['
|
|
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
|
|
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.
|
|
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": [
|