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,125 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import print from '../print';
4
+ import { guardPath, refusalMessage } from './pathGuard';
5
+
6
+ const PURPOSE_TMPL = `# Purpose
7
+
8
+ This wiki is <author>'s <scope> (single, do not mix).
9
+ Key questions it aims to answer:
10
+ - ...
11
+ - ...
12
+ What's in scope: ...
13
+ What's out of scope: ...
14
+ Audience: <author> + agents.
15
+ `;
16
+
17
+ const SCHEMA_TMPL = `# Schema
18
+
19
+ ## Layers
20
+ - raw/, purpose.md, schema.md: read-only for LLM
21
+ - wiki/, index.md, overview.md, log.md, reviews.md: LLM is sole author
22
+
23
+ ## Page types
24
+ - sources/<slug>.md : 1-source summary
25
+ - entities/<slug>.md : 1 thing with properties
26
+ - concepts/<slug>.md : 1 abstract idea
27
+ - synthesis/<slug>.md : cross-source integration
28
+ - queries/<slug>.md : archived Q&A worth keeping
29
+
30
+ ## Frontmatter (every wiki page MUST have)
31
+ - type: source | entity | concept | synthesis | query
32
+ - sources: [<source-slug>, ...]
33
+ - source-sha: <sha> # source pages only
34
+ - source-path: raw/... | <relpath> # source pages only
35
+ - ingested-at: <ISO>
36
+ - last-updated: <ISO>
37
+
38
+ ## Naming
39
+ - kebab-case; no spaces; no dates in wiki/ filenames
40
+ - raw/ filenames keep YYYY-MM-DD prefix
41
+
42
+ ## Linking
43
+ - use [[wikilink]] to other wiki pages by slug
44
+ - every wiki page must link to >=1 other page or be flagged orphan
45
+
46
+ ## Contradictions
47
+ - flag inline: > Contradiction: A vs B (see [[source-A]], [[source-B]])
48
+ - never silently merge; lint surfaces them for human resolution
49
+
50
+ ## Hard rules
51
+ - never edit raw/, purpose.md, schema.md
52
+ - raw/ file sha drift = error, not a re-ingest trigger
53
+ - living-doc paths (in include[]) sha drift = MODIFIED, propose re-ingest
54
+ `;
55
+
56
+ const SUBDIRS = ['sources', 'entities', 'concepts', 'synthesis', 'queries'];
57
+
58
+ // What lives inside the wiki dir but must not enter git / Obsidian Sync:
59
+ // - qmd's project-local vector cache (sqlite-vec, non-deterministic, rebuilds
60
+ // locally with `rig wiki index` / `rig wiki rebuild`)
61
+ // - lint reports (auto-regenerated)
62
+ // - daemon proposal diffs (transient, per-machine)
63
+ // - editor scratch
64
+ const GITIGNORE_TMPL = `# rig wiki — local-only artifacts (do not commit)
65
+ # qmd vector cache (sqlite-vec, machine-specific, rebuildable)
66
+ .qmd/index.sqlite*
67
+ .qmd/*.sqlite-wal
68
+ .qmd/*.sqlite-shm
69
+ # auto-generated reports
70
+ lint-report-*.md
71
+ # daemon proposal queue (per-machine)
72
+ proposals/
73
+ # editor scratch
74
+ .DS_Store
75
+ *.swp
76
+ `;
77
+
78
+ export default function wikiInit(givenPath?: string): void {
79
+ if (!givenPath || !givenPath.trim()) {
80
+ print.error('rig wiki init requires a target subdirectory.');
81
+ print.info('usage: rig wiki init <subdir> (e.g. `rig wiki init knowledge` / `rig wiki init harness/llm-wiki`)');
82
+ print.info('refusing to default to CWD — that would litter the project root with wiki templates.');
83
+ process.exit(1);
84
+ }
85
+ const root = path.resolve(givenPath);
86
+ const guard = guardPath(root, process.cwd());
87
+ if (!guard.ok) {
88
+ print.error('refusing to initialize a wiki at a hidden or gitignored path.');
89
+ // eslint-disable-next-line no-console
90
+ console.error(refusalMessage(root, guard));
91
+ process.exit(1);
92
+ }
93
+ fs.mkdirSync(root, { recursive: true });
94
+
95
+ writeIfMissing(path.join(root, 'purpose.md'), PURPOSE_TMPL);
96
+ writeIfMissing(path.join(root, 'schema.md'), SCHEMA_TMPL);
97
+ writeIfMissing(path.join(root, 'index.md'), '# Index\n');
98
+ writeIfMissing(path.join(root, 'overview.md'), '# Overview\n');
99
+ writeIfMissing(path.join(root, 'log.md'), '# Log\n');
100
+ writeIfMissing(path.join(root, 'reviews.md'), '# Reviews\n');
101
+ writeIfMissing(path.join(root, '.gitignore'), GITIGNORE_TMPL);
102
+
103
+ fs.mkdirSync(path.join(root, 'raw'), { recursive: true });
104
+ writeIfMissing(path.join(root, 'raw', '.gitkeep'), '');
105
+
106
+ for (const sub of SUBDIRS) {
107
+ const d = path.join(root, 'wiki', sub);
108
+ fs.mkdirSync(d, { recursive: true });
109
+ writeIfMissing(path.join(d, '.gitkeep'), '');
110
+ }
111
+
112
+ print.succeed(`wiki initialized at ${root}`);
113
+ print.info(`next: edit purpose.md + schema.md, then \`rig wiki register ${shortPath(root)}\``);
114
+ print.info('on a new device, after cloning, run `rig wiki rebuild` to refresh local caches.');
115
+ }
116
+
117
+ function writeIfMissing(file: string, content: string) {
118
+ if (fs.existsSync(file)) return;
119
+ fs.writeFileSync(file, content, 'utf8');
120
+ }
121
+
122
+ function shortPath(p: string) {
123
+ const home = process.env.HOME || '';
124
+ return home && p.startsWith(home) ? '~' + p.slice(home.length) : p;
125
+ }
@@ -0,0 +1,92 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import print from '../print';
4
+ import { paths } from './paths';
5
+
6
+ const BUNDLED_SKILLS = [
7
+ { name: 'rig-wiki', file: 'RIG_WIKI_SKILL.md' },
8
+ { name: 'rig-crew', file: 'RIG_CREW_SKILL.md' },
9
+ ];
10
+
11
+ /** Find rig's package root by walking up from built/ or lib/. */
12
+ function findRigRoot(): string | undefined {
13
+ // Walk up from `built/index.js` (prod) or `lib/wiki/installSkill.ts` (dev)
14
+ // looking for the rigjs package root.
15
+ let dir = __dirname;
16
+ for (let i = 0; i < 10; i++) {
17
+ const pkg = path.join(dir, 'package.json');
18
+ if (fs.existsSync(pkg)) {
19
+ try {
20
+ const p = JSON.parse(fs.readFileSync(pkg, 'utf8'));
21
+ if (p.name === 'rigjs') return dir;
22
+ } catch { /* keep walking */ }
23
+ }
24
+ const parent = path.dirname(dir);
25
+ if (parent === dir) break;
26
+ dir = parent;
27
+ }
28
+ return undefined;
29
+ }
30
+
31
+ interface InstallOpts { force?: boolean; }
32
+
33
+ export default function wikiInstallSkill(opts: InstallOpts): void {
34
+ const root = findRigRoot();
35
+ if (!root) {
36
+ print.error('could not locate the rigjs install root. Reinstall rigjs?');
37
+ process.exit(1);
38
+ }
39
+
40
+ if (!fs.existsSync(paths.claudeSkillsDir)) {
41
+ print.error(`Claude Code skills dir not found: ${paths.claudeSkillsDir}`);
42
+ print.info('install Claude Code first, then re-run `rig wiki install-skill`.');
43
+ process.exit(1);
44
+ }
45
+
46
+ for (const skill of BUNDLED_SKILLS) {
47
+ const src = path.join(root, skill.file);
48
+ if (!fs.existsSync(src)) {
49
+ print.warn(`skipping ${skill.name}: ${skill.file} not found inside rigjs install`);
50
+ continue;
51
+ }
52
+ linkSkill(skill.name, src, opts);
53
+ }
54
+
55
+ print.info('restart Claude Code to pick up new or updated skills.');
56
+ }
57
+
58
+ function linkSkill(name: string, src: string, opts: InstallOpts): void {
59
+ const targetDir = path.join(paths.claudeSkillsDir, name);
60
+ const target = path.join(targetDir, 'SKILL.md');
61
+ fs.mkdirSync(targetDir, { recursive: true });
62
+
63
+ if (fs.existsSync(target) || isBrokenSymlink(target)) {
64
+ const existing = safeReadlink(target);
65
+ if (existing === src) {
66
+ print.info(`already linked: ${target}`);
67
+ return;
68
+ }
69
+ if (!opts.force) {
70
+ const what = existing ? `symlink -> ${existing}` : 'a regular file';
71
+ print.error(`${target} exists as ${what}. Pass --force to replace.`);
72
+ process.exit(1);
73
+ }
74
+ fs.rmSync(target, { force: true });
75
+ }
76
+
77
+ fs.symlinkSync(src, target);
78
+ print.succeed(`linked ${target} -> ${src}`);
79
+ }
80
+
81
+ function safeReadlink(p: string): string | null {
82
+ try { return fs.readlinkSync(p); } catch { return null; }
83
+ }
84
+
85
+ function isBrokenSymlink(p: string): boolean {
86
+ try {
87
+ fs.statSync(p);
88
+ return false;
89
+ } catch {
90
+ try { return Boolean(fs.readlinkSync(p)); } catch { return false; }
91
+ }
92
+ }
@@ -0,0 +1,252 @@
1
+ // `rig wiki lint` — health-check a wiki.
2
+ //
3
+ // Checks:
4
+ // - every wiki/**/*.md has YAML frontmatter with required keys
5
+ // - source pages reference a real raw/ file and the recorded source-sha
6
+ // still matches (drift → MODIFIED, not an error in lint, just info)
7
+ // - [[wikilink]] targets exist (broken refs → severe)
8
+ // - orphan pages (linked-by 0, excluding sources/) → warn
9
+ // - reviews.md backlog count → warn
10
+ //
11
+ // Output: human-readable summary on stdout, full report at
12
+ // <wiki>/lint-report-YYYY-MM-DD.md.
13
+ // Exit 11 if any severe finding. Other findings are non-fatal.
14
+
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import crypto from 'crypto';
18
+ import print from '../print';
19
+ import { loadWikiConfig, resolveWiki, WikiEntry } from './config';
20
+ import { recordLastRun } from './db';
21
+
22
+ interface LintOpts { wiki?: string; all?: boolean; json?: boolean; }
23
+
24
+ interface PageMeta {
25
+ rel: string; // wiki-relative path, e.g. "wiki/sources/foo.md"
26
+ slug: string; // filename without ext
27
+ sub: string; // "sources" | "entities" | "concepts" | "synthesis" | "queries"
28
+ frontmatter: Record<string, unknown> | null;
29
+ links: string[]; // [[wikilink]] targets
30
+ }
31
+
32
+ interface Findings {
33
+ missingFrontmatter: string[];
34
+ missingRequiredKey: { rel: string; key: string }[];
35
+ brokenWikilinks: { rel: string; target: string }[];
36
+ missingRawSource: { rel: string; sourcePath: string }[];
37
+ shaDriftSource: { rel: string; oldSha: string; newSha: string }[];
38
+ orphanPages: string[];
39
+ reviewsBacklog: number;
40
+ }
41
+
42
+ const REQUIRED_KEYS = ['type', 'sources', 'ingested-at', 'last-updated'] as const;
43
+ const SOURCE_EXTRA_KEYS = ['source-sha', 'source-path'] as const;
44
+ const WIKI_SUBDIRS = ['sources', 'entities', 'concepts', 'synthesis', 'queries'] as const;
45
+
46
+ export default async function wikiLint(opts: LintOpts): Promise<void> {
47
+ const cfg = loadWikiConfig();
48
+ const targets: WikiEntry[] = opts.all
49
+ ? cfg.wikis
50
+ : [resolveWiki(cfg, opts.wiki)].filter(Boolean) as WikiEntry[];
51
+ if (targets.length === 0) {
52
+ print.error('no wiki resolved. Pass --wiki <name>, --all, or run from inside a registered project.');
53
+ process.exit(1);
54
+ }
55
+
56
+ let severeFound = false;
57
+ const reports: { wiki: string; findings: Findings }[] = [];
58
+ for (const t of targets) {
59
+ const findings = lintOne(t);
60
+ reports.push({ wiki: t.name, findings });
61
+ const sev =
62
+ findings.missingFrontmatter.length +
63
+ findings.missingRequiredKey.length +
64
+ findings.brokenWikilinks.length +
65
+ findings.missingRawSource.length;
66
+ if (sev > 0) severeFound = true;
67
+ if (!opts.json) printSummary(t.name, findings);
68
+ writeReport(t, findings);
69
+ recordLastRun(t.name, 'lint', sev > 0 ? 11 : 0);
70
+ }
71
+
72
+ if (opts.json) {
73
+ // eslint-disable-next-line no-console
74
+ console.log(JSON.stringify({ ok: !severeFound, code: severeFound ? 11 : 0, data: reports }, null, 2));
75
+ }
76
+ if (severeFound) process.exit(11);
77
+ }
78
+
79
+ function lintOne(wiki: WikiEntry): Findings {
80
+ const f: Findings = {
81
+ missingFrontmatter: [],
82
+ missingRequiredKey: [],
83
+ brokenWikilinks: [],
84
+ missingRawSource: [],
85
+ shaDriftSource: [],
86
+ orphanPages: [],
87
+ reviewsBacklog: 0,
88
+ };
89
+
90
+ const pages: PageMeta[] = [];
91
+ for (const sub of WIKI_SUBDIRS) {
92
+ const dir = path.join(wiki.path, 'wiki', sub);
93
+ if (!fs.existsSync(dir)) continue;
94
+ for (const name of fs.readdirSync(dir)) {
95
+ if (!name.endsWith('.md') || name === '.gitkeep') continue;
96
+ const abs = path.join(dir, name);
97
+ pages.push(parsePage(wiki.path, abs, sub));
98
+ }
99
+ }
100
+
101
+ const slugToRel = new Map<string, string>();
102
+ for (const p of pages) slugToRel.set(p.slug, p.rel);
103
+
104
+ const linkedSlugs = new Set<string>();
105
+ for (const p of pages) {
106
+ if (!p.frontmatter) { f.missingFrontmatter.push(p.rel); continue; }
107
+ for (const k of REQUIRED_KEYS) {
108
+ if (!(k in p.frontmatter)) f.missingRequiredKey.push({ rel: p.rel, key: k });
109
+ }
110
+ if (p.sub === 'sources') {
111
+ for (const k of SOURCE_EXTRA_KEYS) {
112
+ if (!(k in p.frontmatter)) f.missingRequiredKey.push({ rel: p.rel, key: k });
113
+ }
114
+ const sourcePath = String(p.frontmatter['source-path'] || '');
115
+ if (sourcePath) {
116
+ const abs = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(wiki.path, sourcePath);
117
+ if (!fs.existsSync(abs)) {
118
+ f.missingRawSource.push({ rel: p.rel, sourcePath });
119
+ } else {
120
+ const newSha = sha256(abs);
121
+ const oldSha = String(p.frontmatter['source-sha'] || '');
122
+ if (oldSha && oldSha !== newSha) f.shaDriftSource.push({ rel: p.rel, oldSha, newSha });
123
+ }
124
+ }
125
+ }
126
+ for (const target of p.links) {
127
+ if (!slugToRel.has(target)) {
128
+ f.brokenWikilinks.push({ rel: p.rel, target });
129
+ } else {
130
+ linkedSlugs.add(target);
131
+ }
132
+ }
133
+ }
134
+
135
+ // Orphans: non-source pages that nothing else links to. Sources may
136
+ // legitimately have no inbound links until ingested into a synthesis.
137
+ for (const p of pages) {
138
+ if (p.sub === 'sources') continue;
139
+ if (!linkedSlugs.has(p.slug)) f.orphanPages.push(p.rel);
140
+ }
141
+
142
+ // Reviews backlog: count non-empty bullet lines in reviews.md.
143
+ const reviewsPath = path.join(wiki.path, 'reviews.md');
144
+ if (fs.existsSync(reviewsPath)) {
145
+ const lines = fs.readFileSync(reviewsPath, 'utf8').split('\n');
146
+ f.reviewsBacklog = lines.filter(l => /^\s*[-*]\s+\S/.test(l)).length;
147
+ }
148
+
149
+ return f;
150
+ }
151
+
152
+ function parsePage(wikiRoot: string, abs: string, sub: string): PageMeta {
153
+ const rel = path.relative(wikiRoot, abs);
154
+ const slug = path.basename(abs, path.extname(abs));
155
+ const content = fs.readFileSync(abs, 'utf8');
156
+ const { frontmatter, body } = splitFrontmatter(content);
157
+ const links = extractWikilinks(body);
158
+ return { rel, slug, sub, frontmatter, links };
159
+ }
160
+
161
+ function splitFrontmatter(content: string): { frontmatter: Record<string, unknown> | null; body: string } {
162
+ if (!content.startsWith('---\n')) return { frontmatter: null, body: content };
163
+ const end = content.indexOf('\n---\n', 4);
164
+ if (end < 0) return { frontmatter: null, body: content };
165
+ const yaml = content.slice(4, end);
166
+ const body = content.slice(end + 5);
167
+ return { frontmatter: parseTinyYaml(yaml), body };
168
+ }
169
+
170
+ // Minimal YAML parser — handles the rig wiki frontmatter shape only
171
+ // (key: scalar / key: [a, b, c]). Anything else returns null for that key.
172
+ function parseTinyYaml(src: string): Record<string, unknown> | null {
173
+ const out: Record<string, unknown> = {};
174
+ for (const line of src.split('\n')) {
175
+ const trimmed = line.replace(/#.*$/, '').trimEnd();
176
+ if (!trimmed) continue;
177
+ const m = /^([A-Za-z][\w-]*)\s*:\s*(.*)$/.exec(trimmed);
178
+ if (!m) continue;
179
+ const [, key, valRaw] = m;
180
+ const val = valRaw.trim();
181
+ if (val.startsWith('[') && val.endsWith(']')) {
182
+ out[key] = val.slice(1, -1).split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean);
183
+ } else {
184
+ out[key] = val.replace(/^['"]|['"]$/g, '');
185
+ }
186
+ }
187
+ return Object.keys(out).length > 0 ? out : null;
188
+ }
189
+
190
+ function extractWikilinks(body: string): string[] {
191
+ const out: string[] = [];
192
+ const re = /\[\[([^\]|\n]+)(?:\|[^\]]*)?\]\]/g;
193
+ let m: RegExpExecArray | null;
194
+ while ((m = re.exec(body)) !== null) {
195
+ const slug = m[1].trim().split('#')[0].split('/').pop() || '';
196
+ if (slug) out.push(slug);
197
+ }
198
+ return out;
199
+ }
200
+
201
+ function sha256(file: string): string {
202
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
203
+ }
204
+
205
+ function printSummary(wikiName: string, f: Findings): void {
206
+ print.info(`lint: ${wikiName}`);
207
+ const lines: string[] = [];
208
+ if (f.missingFrontmatter.length) lines.push(` MISSING FRONTMATTER (${f.missingFrontmatter.length})`);
209
+ if (f.missingRequiredKey.length) lines.push(` MISSING REQUIRED KEY (${f.missingRequiredKey.length})`);
210
+ if (f.brokenWikilinks.length) lines.push(` BROKEN WIKILINKS (${f.brokenWikilinks.length})`);
211
+ if (f.missingRawSource.length) lines.push(` MISSING RAW SOURCE (${f.missingRawSource.length})`);
212
+ if (f.shaDriftSource.length) lines.push(` SOURCE SHA DRIFT (${f.shaDriftSource.length}) [info — propose re-ingest]`);
213
+ if (f.orphanPages.length) lines.push(` ORPHAN PAGES (${f.orphanPages.length}) [warn]`);
214
+ if (f.reviewsBacklog) lines.push(` REVIEWS BACKLOG (${f.reviewsBacklog} item${f.reviewsBacklog === 1 ? '' : 's'}) [warn]`);
215
+ if (lines.length === 0) {
216
+ print.succeed(' clean');
217
+ } else {
218
+ for (const l of lines) {
219
+ // eslint-disable-next-line no-console
220
+ console.log(l);
221
+ }
222
+ }
223
+ }
224
+
225
+ function writeReport(wiki: WikiEntry, f: Findings): void {
226
+ const today = new Date().toISOString().slice(0, 10);
227
+ const out = path.join(wiki.path, `lint-report-${today}.md`);
228
+ const parts: string[] = [];
229
+ parts.push(`# lint report — ${wiki.name} — ${today}`);
230
+ parts.push('');
231
+ parts.push(`Generated by \`rig wiki lint\`. Severe sections trigger exit code 11.`);
232
+ parts.push('');
233
+
234
+ section(parts, 'Missing frontmatter (severe)', f.missingFrontmatter.map(r => `- ${r}`));
235
+ section(parts, 'Missing required key (severe)', f.missingRequiredKey.map(x => `- ${x.rel} — \`${x.key}\``));
236
+ section(parts, 'Broken wikilinks (severe)', f.brokenWikilinks.map(x => `- ${x.rel} → [[${x.target}]]`));
237
+ section(parts, 'Missing raw source (severe)', f.missingRawSource.map(x => `- ${x.rel} → ${x.sourcePath}`));
238
+ section(parts, 'Source sha drift (re-ingest recommended)', f.shaDriftSource.map(x => `- ${x.rel}\n - old: \`${x.oldSha.slice(0, 12)}…\`\n - new: \`${x.newSha.slice(0, 12)}…\``));
239
+ section(parts, 'Orphan pages', f.orphanPages.map(r => `- ${r}`));
240
+ parts.push(`## Reviews backlog\n\n${f.reviewsBacklog} item(s).`);
241
+ parts.push('');
242
+
243
+ fs.writeFileSync(out, parts.join('\n'), 'utf8');
244
+ }
245
+
246
+ function section(out: string[], title: string, items: string[]): void {
247
+ out.push(`## ${title}`);
248
+ out.push('');
249
+ if (items.length === 0) out.push('_(none)_');
250
+ else for (const item of items) out.push(item);
251
+ out.push('');
252
+ }
@@ -0,0 +1,69 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import print from '../print';
4
+ import { loadWikiConfig, loadRigConfig } from './config';
5
+ import { getLastRun } from './db';
6
+ import { detectQmd } from './qmd';
7
+ import { adapters } from './agent/registry';
8
+
9
+ export default async function wikiList(): Promise<void> {
10
+ const cfg = loadWikiConfig();
11
+ const rig = loadRigConfig();
12
+ if (cfg.wikis.length === 0) {
13
+ print.info('no wikis registered. Use `rig wiki register [<path>]` to add one.');
14
+ } else {
15
+ const rows = cfg.wikis.map(w => ({
16
+ name: w.name,
17
+ path: shortPath(w.path),
18
+ pages: countPages(w.path),
19
+ lastScan: fmtTs(getLastRun(w.name, 'scan')?.ts),
20
+ lastIngest: fmtTs(getLastRun(w.name, 'ingest')?.ts),
21
+ lastLint: fmtTs(getLastRun(w.name, 'lint')?.ts),
22
+ }));
23
+ const header = ['NAME', 'PATH', 'PAGES', 'LAST SCAN', 'LAST INGEST', 'LAST LINT'];
24
+ printTable(header, rows.map(r => [r.name, r.path, String(r.pages), r.lastScan, r.lastIngest, r.lastLint]));
25
+ }
26
+
27
+ const qmd = detectQmd();
28
+ const defaultAgent = rig.wiki?.defaultAgent || 'claude';
29
+ const agentDetect = await adapters.find(a => a.name === defaultAgent)?.detect();
30
+ // eslint-disable-next-line no-console
31
+ console.log(`\nagent: ${defaultAgent}${agentDetect?.installed ? ` (${agentDetect.version || 'installed'})` : ' (NOT installed)'}` +
32
+ ` qmd: ${qmd.installed ? qmd.version || 'installed' : 'not installed (fallback mode)'}`);
33
+ }
34
+
35
+ function countPages(wikiDir: string): number {
36
+ const wiki = path.join(wikiDir, 'wiki');
37
+ if (!fs.existsSync(wiki)) return 0;
38
+ let n = 0;
39
+ for (const sub of ['sources', 'entities', 'concepts', 'synthesis', 'queries']) {
40
+ const d = path.join(wiki, sub);
41
+ if (!fs.existsSync(d)) continue;
42
+ for (const f of fs.readdirSync(d)) if (f.endsWith('.md') && f !== '.gitkeep') n++;
43
+ }
44
+ return n;
45
+ }
46
+
47
+ function shortPath(p: string): string {
48
+ const home = process.env.HOME || '';
49
+ return home && p.startsWith(home) ? '~' + p.slice(home.length) : p;
50
+ }
51
+
52
+ function fmtTs(ts?: number): string {
53
+ if (!ts) return '—';
54
+ const d = new Date(ts);
55
+ return d.toISOString().replace('T', ' ').slice(0, 16);
56
+ }
57
+
58
+ function printTable(header: string[], rows: string[][]): void {
59
+ const widths = header.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] || '').length)));
60
+ const fmt = (cells: string[]) => cells.map((c, i) => (c || '').padEnd(widths[i])).join(' ');
61
+ // eslint-disable-next-line no-console
62
+ console.log(fmt(header));
63
+ // eslint-disable-next-line no-console
64
+ console.log(widths.map(w => '-'.repeat(w)).join(' '));
65
+ for (const r of rows) {
66
+ // eslint-disable-next-line no-console
67
+ console.log(fmt(r));
68
+ }
69
+ }
@@ -0,0 +1,87 @@
1
+ // Shared guards for rig wiki — refuse to operate on hidden paths or
2
+ // .gitignored content. The user must explicitly copy such files into a
3
+ // visible, tracked location before they can become wiki sources.
4
+
5
+ import path from 'path';
6
+ import { spawnSync } from 'child_process';
7
+
8
+ export interface PathGuardResult {
9
+ ok: boolean;
10
+ reason?: 'hidden' | 'gitignored';
11
+ segment?: string; // for hidden: the offending segment
12
+ detail?: string; // human-readable detail
13
+ }
14
+
15
+ /** True if any path segment (except `.` / `..`) starts with `.`. */
16
+ export function isHiddenPath(p: string): boolean {
17
+ for (const seg of segmentsOf(p)) {
18
+ if (seg.startsWith('.')) return true;
19
+ }
20
+ return false;
21
+ }
22
+
23
+ /** First hidden segment, or null. */
24
+ export function hiddenSegment(p: string): string | null {
25
+ for (const seg of segmentsOf(p)) {
26
+ if (seg.startsWith('.')) return seg;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ function segmentsOf(p: string): string[] {
32
+ return p.split(path.sep).filter(s => s && s !== '.' && s !== '..');
33
+ }
34
+
35
+ /**
36
+ * Returns true if the path is ignored by git in `repoCwd`'s repo.
37
+ * Returns false if tracked/non-ignored. Returns null if not in a git repo
38
+ * (so callers can pass the check transparently outside git contexts).
39
+ */
40
+ export function isGitignored(p: string, repoCwd: string): boolean | null {
41
+ const r = spawnSync('git', ['check-ignore', '-q', '--', p], {
42
+ cwd: repoCwd,
43
+ encoding: 'utf8',
44
+ });
45
+ if (r.status === 0) return true; // ignored
46
+ if (r.status === 1) return false; // not ignored
47
+ return null; // exit 128 (not a repo) or git missing
48
+ }
49
+
50
+ /**
51
+ * Validate a path as a wiki source / target. Returns ok if visible AND
52
+ * not gitignored. Use for `init` target and `ingest` source.
53
+ */
54
+ export function guardPath(absPath: string, repoCwd: string): PathGuardResult {
55
+ const seg = hiddenSegment(absPath);
56
+ if (seg) {
57
+ return {
58
+ ok: false,
59
+ reason: 'hidden',
60
+ segment: seg,
61
+ detail: `path contains a hidden segment "${seg}"`,
62
+ };
63
+ }
64
+ const gi = isGitignored(absPath, repoCwd);
65
+ if (gi === true) {
66
+ return {
67
+ ok: false,
68
+ reason: 'gitignored',
69
+ detail: '.gitignore matches this path',
70
+ };
71
+ }
72
+ return { ok: true };
73
+ }
74
+
75
+ export function refusalMessage(target: string, r: PathGuardResult): string {
76
+ const lines = [
77
+ `refused: ${target}`,
78
+ ` reason: ${r.reason} — ${r.detail}`,
79
+ '',
80
+ ' rig wiki refuses to operate on hidden files/dirs or .gitignored content.',
81
+ ' If you really need this content, copy it to a visible, tracked location first:',
82
+ '',
83
+ ' cp -R <hidden-or-ignored> <wiki>/raw/manual-copy/ # then `rig wiki ingest`',
84
+ '',
85
+ ];
86
+ return lines.join('\n');
87
+ }
@@ -0,0 +1,29 @@
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 paths = {
7
+ home: RIG_HOME,
8
+ config: path.join(RIG_HOME, 'config.json5'),
9
+ wikiConfig: path.join(RIG_HOME, 'wiki.config.json5'),
10
+ stateDb: path.join(RIG_HOME, 'state.db'),
11
+ locks: path.join(RIG_HOME, 'locks'),
12
+ logs: path.join(RIG_HOME, 'logs'),
13
+ daemonLog: path.join(RIG_HOME, 'logs', 'wiki-daemon.log'),
14
+ cache: path.join(RIG_HOME, 'cache'),
15
+ sandbox: path.join(RIG_HOME, 'cache', 'sandbox'),
16
+ launchAgent: path.join(os.homedir(), 'Library', 'LaunchAgents', 'ai.flashhand.rig.wiki.plist'),
17
+ claudeSkillsDir: path.join(os.homedir(), '.claude', 'skills'),
18
+ builtinSkillRelative: 'RIG_WIKI_SKILL.md',
19
+ };
20
+
21
+ export const daemonLabel = 'ai.flashhand.rig.wiki';
22
+
23
+ export function wikiLogDir(wikiName: string) {
24
+ return path.join(paths.logs, 'wikis', wikiName);
25
+ }
26
+
27
+ export function wikiLockFile(wikiName: string) {
28
+ return path.join(paths.locks, `${wikiName}.lock`);
29
+ }
@@ -0,0 +1,8 @@
1
+ import print from '../print';
2
+
3
+ export function requireMacOS(): void {
4
+ if (process.platform !== 'darwin') {
5
+ print.error(`rig wiki currently supports macOS only (detected: ${process.platform}). Linux support is on the P5 roadmap; Windows is not planned.`);
6
+ process.exit(32);
7
+ }
8
+ }