rigjs 3.0.33 → 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 (99) 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/wiki/README.md +79 -0
  33. package/lib/wiki/agent/claude.ts +65 -0
  34. package/lib/wiki/agent/codex.ts +22 -0
  35. package/lib/wiki/agent/index.ts +11 -0
  36. package/lib/wiki/agent/list.ts +27 -0
  37. package/lib/wiki/agent/pi.ts +21 -0
  38. package/lib/wiki/agent/registry.ts +16 -0
  39. package/lib/wiki/agent/types.ts +37 -0
  40. package/lib/wiki/agent/use.ts +21 -0
  41. package/lib/wiki/config.ts +99 -0
  42. package/lib/wiki/daemon/index.ts +25 -0
  43. package/lib/wiki/daemon/install.ts +69 -0
  44. package/lib/wiki/daemon/logs.ts +16 -0
  45. package/lib/wiki/daemon/runner.ts +42 -0
  46. package/lib/wiki/daemon/start.ts +20 -0
  47. package/lib/wiki/daemon/status.ts +23 -0
  48. package/lib/wiki/daemon/stop.ts +16 -0
  49. package/lib/wiki/daemon/uninstall.ts +17 -0
  50. package/lib/wiki/db.ts +71 -0
  51. package/lib/wiki/fetch.ts +206 -0
  52. package/lib/wiki/index.ts +106 -0
  53. package/lib/wiki/indexCmd.ts +23 -0
  54. package/lib/wiki/ingest.ts +271 -0
  55. package/lib/wiki/init.ts +125 -0
  56. package/lib/wiki/installSkill.ts +92 -0
  57. package/lib/wiki/lint.ts +252 -0
  58. package/lib/wiki/list.ts +69 -0
  59. package/lib/wiki/pathGuard.ts +87 -0
  60. package/lib/wiki/paths.ts +29 -0
  61. package/lib/wiki/platform.ts +8 -0
  62. package/lib/wiki/qmd.ts +205 -0
  63. package/lib/wiki/query.ts +144 -0
  64. package/lib/wiki/rebuild.ts +56 -0
  65. package/lib/wiki/register.ts +94 -0
  66. package/lib/wiki/scan.ts +0 -0
  67. package/lib/wiki/uninstallSkill.ts +37 -0
  68. package/lib/wiki/unregister.ts +16 -0
  69. package/package.json +36 -6
  70. package/scripts/postinstall.mjs +108 -0
  71. package/scripts/publish.mjs +93 -0
  72. package/scripts/sync-skill.mjs +33 -0
  73. package/scripts/version-code.mjs +86 -0
  74. package/skills.md +54 -0
  75. package/.github/workflows/npm-publish.yml +0 -22
  76. package/demo/.env.oem1 +0 -4
  77. package/demo/.env.oem2 +0 -4
  78. package/demo/babel.config.js +0 -5
  79. package/demo/env.rig.json5 +0 -8
  80. package/demo/jsconfig.json +0 -19
  81. package/demo/package.json +0 -59
  82. package/demo/package.rig.json5 +0 -78
  83. package/demo/public/favicon.ico +0 -0
  84. package/demo/public/index.html +0 -17
  85. package/demo/rig_dev/.gitkeep +0 -0
  86. package/demo/rig_helper.d.ts +0 -4
  87. package/demo/rig_helper.js +0 -10
  88. package/demo/rigs/.gitkeep +0 -0
  89. package/demo/src/App.vue +0 -34
  90. package/demo/src/assets/logo.png +0 -0
  91. package/demo/src/components/HelloWorld.vue +0 -58
  92. package/demo/src/main.js +0 -8
  93. package/demo/vue.config.js +0 -8
  94. package/demo/yarn.lock +0 -6312
  95. package/develop.png +0 -0
  96. package/jest/test.rig.json5 +0 -14
  97. package/jest.config.ts +0 -16
  98. package/production.png +0 -0
  99. package/tsconfig.json +0 -53
@@ -0,0 +1,121 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { crewPaths } from './paths';
4
+
5
+ export interface CrewRoleDefinition {
6
+ name: string;
7
+ title: string;
8
+ folder: string;
9
+ description?: string;
10
+ agent?: string;
11
+ defaultExecutor?: string;
12
+ promptPath?: string;
13
+ configPath?: string;
14
+ builtIn?: boolean;
15
+ }
16
+
17
+ export const BUILTIN_ROLE_NAMES = ['lead', 'designer', 'pm', 'coder', 'tester', 'researcher'];
18
+
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 },
26
+ ];
27
+
28
+ export function normalizeRoleName(input: string): string {
29
+ const name = input.trim().toLowerCase().replace(/\s+/g, '-');
30
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
31
+ throw new Error(`invalid role name "${input}". Use lowercase letters, numbers, and hyphens.`);
32
+ }
33
+ return name;
34
+ }
35
+
36
+ export function normalizeRoleNames(names: string[]): string[] {
37
+ const out: string[] = [];
38
+ const seen = new Set<string>();
39
+ for (const raw of names) {
40
+ const name = normalizeRoleName(raw);
41
+ if (seen.has(name)) continue;
42
+ seen.add(name);
43
+ out.push(name);
44
+ }
45
+ return out;
46
+ }
47
+
48
+ export function roleFolderName(name: string): string {
49
+ return titleFromRoleName(name).replace(/[^A-Za-z0-9]/g, '');
50
+ }
51
+
52
+ export function roleConfigDir(name: string): string {
53
+ return path.join(crewPaths.rolesDir, normalizeRoleName(name));
54
+ }
55
+
56
+ export function roleConfigPath(name: string): string {
57
+ return path.join(roleConfigDir(name), 'role.json');
58
+ }
59
+
60
+ export function loadGlobalRoleNames(): string[] {
61
+ return loadCustomRoleDefinitions().map(r => r.name);
62
+ }
63
+
64
+ export function roleDefinitionsForCrew(crew: { roles?: string[] }): CrewRoleDefinition[] {
65
+ const known = new Map<string, CrewRoleDefinition>();
66
+ for (const role of BUILTIN_ROLES) known.set(role.name, role);
67
+ for (const role of loadCustomRoleDefinitions()) known.set(role.name, role);
68
+ const names = normalizeRoleNames(crew.roles && crew.roles.length ? crew.roles : BUILTIN_ROLE_NAMES);
69
+ return names.map(name => known.get(name) || fallbackRole(name));
70
+ }
71
+
72
+ export function loadCustomRoleDefinitions(): CrewRoleDefinition[] {
73
+ if (!fs.existsSync(crewPaths.rolesDir)) return [];
74
+ const roles: CrewRoleDefinition[] = [];
75
+ for (const entry of fs.readdirSync(crewPaths.rolesDir)) {
76
+ const cfg = path.join(crewPaths.rolesDir, entry, 'role.json');
77
+ if (!fs.existsSync(cfg)) continue;
78
+ try {
79
+ const raw = JSON.parse(fs.readFileSync(cfg, 'utf8')) as Partial<CrewRoleDefinition>;
80
+ if (!raw.name) continue;
81
+ const name = normalizeRoleName(raw.name);
82
+ roles.push({
83
+ name,
84
+ title: raw.title || titleFromRoleName(name),
85
+ folder: raw.folder || path.join('Roles', name),
86
+ description: raw.description,
87
+ agent: raw.agent,
88
+ defaultExecutor: raw.defaultExecutor,
89
+ promptPath: raw.promptPath || path.join(roleConfigDir(name), 'prompt.md'),
90
+ configPath: cfg,
91
+ });
92
+ } catch {
93
+ // Invalid role configs are ignored by passive loaders; `rig crew role show`
94
+ // surfaces parse errors when the user inspects a specific role.
95
+ }
96
+ }
97
+ return roles.sort((a, b) => a.name.localeCompare(b.name));
98
+ }
99
+
100
+ export function roleByName(name: string, crew?: { roles?: string[] }): CrewRoleDefinition | undefined {
101
+ const normalized = normalizeRoleName(name);
102
+ const defs = crew ? roleDefinitionsForCrew(crew) : [...BUILTIN_ROLES, ...loadCustomRoleDefinitions()];
103
+ return defs.find(r => r.name === normalized);
104
+ }
105
+
106
+ export function titleFromRoleName(name: string): string {
107
+ return name
108
+ .split('-')
109
+ .filter(Boolean)
110
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
111
+ .join(' ');
112
+ }
113
+
114
+ function fallbackRole(name: string): CrewRoleDefinition {
115
+ return {
116
+ name,
117
+ title: titleFromRoleName(name),
118
+ folder: path.join('Roles', name),
119
+ description: 'Custom role without a global role.json definition.',
120
+ };
121
+ }
@@ -0,0 +1,150 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import print from '../print';
4
+ import { requireCrew, resolveCrew, shortPath } from './config';
5
+ import { ensureCrewVault } from './vault';
6
+ import {
7
+ BUILTIN_ROLE_NAMES,
8
+ loadGlobalRoleNames,
9
+ normalizeRoleName,
10
+ roleByName,
11
+ roleConfigDir,
12
+ roleConfigPath,
13
+ roleDefinitionsForCrew,
14
+ titleFromRoleName,
15
+ } from './role';
16
+
17
+ interface RoleAddOpts {
18
+ from: string;
19
+ title?: string;
20
+ summary?: string;
21
+ agent?: string;
22
+ executor?: string;
23
+ force?: boolean;
24
+ crew?: string;
25
+ }
26
+
27
+ interface RoleListOpts { crew?: string; }
28
+ interface RoleShowOpts { crew?: string; }
29
+
30
+ export function roleAdd(nameInput: string, opts: RoleAddOpts): void {
31
+ const name = normalizeRoleName(nameInput);
32
+ const existing = roleByName(name);
33
+ if (existing && existing.builtIn) {
34
+ print.error(`cannot override built-in role: ${name}`);
35
+ process.exit(1);
36
+ }
37
+ if (!opts.from) {
38
+ print.error('missing --from <file>');
39
+ process.exit(1);
40
+ }
41
+ const source = path.resolve(opts.from);
42
+ if (!fs.existsSync(source)) {
43
+ print.error(`role description file not found: ${source}`);
44
+ process.exit(1);
45
+ }
46
+ const dir = roleConfigDir(name);
47
+ const cfg = roleConfigPath(name);
48
+ if (fs.existsSync(cfg) && !opts.force) {
49
+ print.error(`role already exists: ${name}. Pass --force to update it.`);
50
+ process.exit(1);
51
+ }
52
+
53
+ const body = fs.readFileSync(source, 'utf8');
54
+ const now = new Date().toISOString();
55
+ const previous = readRoleConfig(cfg);
56
+ const role = {
57
+ name,
58
+ title: opts.title || markdownTitle(body) || previous?.title || titleFromRoleName(name),
59
+ folder: previous?.folder || `Roles/${name}`,
60
+ description: opts.summary || firstParagraph(body) || previous?.description || '',
61
+ agent: opts.agent || previous?.agent,
62
+ defaultExecutor: normalizeExecutor(opts.executor || previous?.defaultExecutor),
63
+ promptPath: path.join(dir, 'prompt.md'),
64
+ sourcePath: source,
65
+ createdAt: previous?.createdAt || now,
66
+ updatedAt: now,
67
+ };
68
+
69
+ fs.mkdirSync(dir, { recursive: true });
70
+ fs.writeFileSync(path.join(dir, 'prompt.md'), body, 'utf8');
71
+ fs.writeFileSync(path.join(dir, 'source.md'), sourceHeader(source) + body, 'utf8');
72
+ fs.writeFileSync(cfg, JSON.stringify(role, null, 2) + '\n', 'utf8');
73
+
74
+ print.succeed(`role "${name}" saved: ${shortPath(cfg)}`);
75
+ const crew = opts.crew ? requireCrew(opts.crew) : resolveCrew();
76
+ if (crew) {
77
+ ensureCrewVault(requireCrew(crew.name));
78
+ print.info(`role "${name}" is available to crew "${crew.name}"`);
79
+ } else {
80
+ print.info('no crew configured yet; the role will load after `rig crew init`.');
81
+ }
82
+ }
83
+
84
+ export function roleList(opts: RoleListOpts): void {
85
+ const crew = opts.crew ? requireCrew(opts.crew) : undefined;
86
+ const roles = crew
87
+ ? roleDefinitionsForCrew(crew)
88
+ : roleDefinitionsForCrew({ roles: [...BUILTIN_ROLE_NAMES, ...loadGlobalRoleNames()] });
89
+ // eslint-disable-next-line no-console
90
+ console.log('ROLE TITLE AGENT EXECUTOR FOLDER CONFIG');
91
+ // eslint-disable-next-line no-console
92
+ console.log('---- ----- ----- -------- ------ ------');
93
+ for (const role of roles) {
94
+ // eslint-disable-next-line no-console
95
+ console.log(`${role.name} ${role.title} ${role.agent || '-'} ${role.defaultExecutor || '-'} ${role.folder} ${role.configPath ? shortPath(role.configPath) : 'built-in'}`);
96
+ }
97
+ }
98
+
99
+ export function roleShow(nameInput: string, opts: RoleShowOpts): void {
100
+ const crew = opts.crew ? requireCrew(opts.crew) : undefined;
101
+ const role = roleByName(nameInput, crew);
102
+ if (!role) {
103
+ print.error(`unknown role: ${nameInput}`);
104
+ process.exit(1);
105
+ }
106
+ print.info(`role: ${role.name}`);
107
+ // eslint-disable-next-line no-console
108
+ console.log(`title: ${role.title}`);
109
+ // eslint-disable-next-line no-console
110
+ console.log(`folder: ${role.folder}`);
111
+ // eslint-disable-next-line no-console
112
+ console.log(`agent: ${role.agent || '-'}`);
113
+ // eslint-disable-next-line no-console
114
+ console.log(`executor: ${role.defaultExecutor || '-'}`);
115
+ // eslint-disable-next-line no-console
116
+ console.log(`config: ${role.configPath ? shortPath(role.configPath) : 'built-in'}`);
117
+ // eslint-disable-next-line no-console
118
+ console.log(`prompt: ${role.promptPath ? shortPath(role.promptPath) : '-'}`);
119
+ if (role.description) {
120
+ // eslint-disable-next-line no-console
121
+ console.log(`description: ${role.description}`);
122
+ }
123
+ }
124
+
125
+ function normalizeExecutor(value?: string): string | undefined {
126
+ if (!value) return undefined;
127
+ if (value === 'claude' || value === 'codex' || value === 'pi') return value;
128
+ print.error(`unknown executor: ${value}. Expected claude, codex, or pi.`);
129
+ process.exit(1);
130
+ }
131
+
132
+ function readRoleConfig(file: string): any | undefined {
133
+ if (!fs.existsSync(file)) return undefined;
134
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return undefined; }
135
+ }
136
+
137
+ function markdownTitle(text: string): string | undefined {
138
+ const line = text.split(/\r?\n/).find(l => /^#\s+/.test(l));
139
+ return line ? line.replace(/^#\s+/, '').trim() : undefined;
140
+ }
141
+
142
+ function firstParagraph(text: string): string | undefined {
143
+ const lines = text.split(/\r?\n/).map(l => l.trim());
144
+ const paragraph = lines.find(l => l && !l.startsWith('#') && !l.startsWith('---'));
145
+ return paragraph ? paragraph.slice(0, 240) : undefined;
146
+ }
147
+
148
+ function sourceHeader(source: string): string {
149
+ return `<!-- Imported from ${source} at ${new Date().toISOString()} -->\n\n`;
150
+ }
@@ -0,0 +1,19 @@
1
+ import fs from 'fs';
2
+ import { crewPaths } from './paths';
3
+ import { CrewEntry } from './config';
4
+ import { CrewTask, summarize } from './task';
5
+
6
+ export function writeCrewState(crew: CrewEntry, tasks: CrewTask[]): void {
7
+ fs.mkdirSync(crewPaths.home, { recursive: true });
8
+ const summary = summarize(tasks);
9
+ const data = {
10
+ updatedAt: new Date().toISOString(),
11
+ crew: crew.name,
12
+ vault: crew.vault,
13
+ root: crew.root,
14
+ summary,
15
+ tasks,
16
+ };
17
+ fs.writeFileSync(crewPaths.state, JSON.stringify(data, null, 2) + '\n', 'utf8');
18
+ }
19
+
@@ -0,0 +1,27 @@
1
+ import print from '../print';
2
+ import { requireCrew, shortPath } from './config';
3
+ import { scanTasks, openInboxTasks, summarize } from './task';
4
+ import { writeCrewState } from './state';
5
+
6
+ interface StatusOpts { crew?: string; json?: boolean; }
7
+
8
+ export default function crewStatus(opts: StatusOpts): void {
9
+ const crew = requireCrew(opts.crew);
10
+ const tasks = scanTasks(crew);
11
+ const inbox = openInboxTasks(crew);
12
+ const s = summarize(tasks);
13
+ writeCrewState(crew, tasks);
14
+ if (opts.json) {
15
+ // eslint-disable-next-line no-console
16
+ console.log(JSON.stringify({ ok: true, crew: crew.name, vault: crew.vault, summary: s, inbox: inbox.length }, null, 2));
17
+ return;
18
+ }
19
+ print.info(`crew: ${crew.name}`);
20
+ // eslint-disable-next-line no-console
21
+ console.log(`vault: ${shortPath(crew.vault)}`);
22
+ // eslint-disable-next-line no-console
23
+ console.log(`tasks: ${s.done}/${s.total} done, ${s.open} open, ${s.blocked} blocked, ${s.doing} doing`);
24
+ // eslint-disable-next-line no-console
25
+ console.log(`inbox: ${inbox.length} open`);
26
+ }
27
+
@@ -0,0 +1,9 @@
1
+ import print from '../print';
2
+
3
+ export default function crewStub(name: string): () => void {
4
+ return () => {
5
+ print.warn(`rig crew ${name} is not implemented yet.`);
6
+ print.info('Current MVP supports init, status, inbox, board, sync, doctor, ask, and project add/list/status.');
7
+ };
8
+ }
9
+
@@ -0,0 +1,15 @@
1
+ import print from '../print';
2
+ import { requireCrew } from './config';
3
+ import { scanTasks, summarize } from './task';
4
+ import { writeCrewState } from './state';
5
+
6
+ interface SyncOpts { crew?: string; }
7
+
8
+ export default function crewSync(opts: SyncOpts): void {
9
+ const crew = requireCrew(opts.crew);
10
+ const tasks = scanTasks(crew);
11
+ writeCrewState(crew, tasks);
12
+ const s = summarize(tasks);
13
+ print.succeed(`crew state synced: ${s.done}/${s.total} tasks done`);
14
+ }
15
+
@@ -0,0 +1,92 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { CrewEntry } from './config';
4
+ import { rootPath } from './vault';
5
+ import { roleDefinitionsForCrew } from './role';
6
+
7
+ export interface CrewTask {
8
+ id?: string;
9
+ text: string;
10
+ done: boolean;
11
+ file: string;
12
+ line: number;
13
+ scope: string;
14
+ fields: { [key: string]: string };
15
+ }
16
+
17
+ export interface CrewSummary {
18
+ total: number;
19
+ done: number;
20
+ open: number;
21
+ blocked: number;
22
+ doing: number;
23
+ }
24
+
25
+ export function scanTasks(crew: CrewEntry): CrewTask[] {
26
+ const files = taskFiles(crew);
27
+ const tasks: CrewTask[] = [];
28
+ for (const item of files) tasks.push(...parseTasks(item.file, item.scope));
29
+ return tasks;
30
+ }
31
+
32
+ export function parseTasks(file: string, scope: string): CrewTask[] {
33
+ if (!fs.existsSync(file)) return [];
34
+ const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
35
+ const tasks: CrewTask[] = [];
36
+ for (let i = 0; i < lines.length; i++) {
37
+ const parsed = parseTaskLine(lines[i]);
38
+ if (parsed) tasks.push({ ...parsed, file, line: i + 1, scope });
39
+ }
40
+ return tasks;
41
+ }
42
+
43
+ export function summarize(tasks: CrewTask[]): CrewSummary {
44
+ let done = 0, blocked = 0, doing = 0;
45
+ for (const t of tasks) {
46
+ if (t.done) done++;
47
+ const status = (t.fields.status || '').toLowerCase();
48
+ if (status === 'blocked') blocked++;
49
+ if (status === 'doing' || status === 'in-progress') doing++;
50
+ }
51
+ return { total: tasks.length, done, open: tasks.length - done, blocked, doing };
52
+ }
53
+
54
+ export function openInboxTasks(crew: CrewEntry): CrewTask[] {
55
+ return parseTasks(rootPath(crew, 'Inbox.md'), 'inbox').filter(t => !t.done);
56
+ }
57
+
58
+ export function taskProgress(tasks: CrewTask[]): number {
59
+ if (tasks.length === 0) return 0;
60
+ return Math.round((summarize(tasks).done / tasks.length) * 100);
61
+ }
62
+
63
+ function parseTaskLine(line: string): Omit<CrewTask, 'file' | 'line' | 'scope'> | undefined {
64
+ const match = line.match(/^\s*-\s+\[([ xX-])\]\s+(.+)$/);
65
+ if (!match) return undefined;
66
+ const done = match[1].toLowerCase() === 'x';
67
+ const raw = match[2].trim();
68
+ const fields: { [key: string]: string } = {};
69
+ const fieldRe = /\[([A-Za-z0-9_-]+)::\s*([^\]]+)\]/g;
70
+ let f: RegExpExecArray | null;
71
+ while ((f = fieldRe.exec(raw))) fields[f[1]] = f[2].trim();
72
+ const idMatch = raw.match(/\b(?:AT|D|Q|A|R)-\d{6}-\d{3}\b/);
73
+ return { id: idMatch ? idMatch[0] : undefined, text: raw, done, fields };
74
+ }
75
+
76
+ function taskFiles(crew: CrewEntry): { file: string; scope: string }[] {
77
+ const files = [
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
+ ];
84
+ 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}` });
89
+ }
90
+ }
91
+ return files.filter(item => fs.existsSync(item.file));
92
+ }