superwiki 0.1.0

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 (39) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/.claude-plugin/plugin.json +17 -0
  3. package/.codex-plugin/plugin.json +12 -0
  4. package/LICENSE +21 -0
  5. package/README.md +197 -0
  6. package/bin/superwiki.mjs +116 -0
  7. package/commands/config.md +5 -0
  8. package/commands/explain.md +5 -0
  9. package/commands/implement.md +5 -0
  10. package/commands/ingest.md +5 -0
  11. package/commands/init.md +5 -0
  12. package/commands/lint.md +5 -0
  13. package/commands/migrate.md +5 -0
  14. package/commands/plan.md +5 -0
  15. package/commands/triage.md +5 -0
  16. package/commands/visualize.md +5 -0
  17. package/install.sh +27 -0
  18. package/package.json +45 -0
  19. package/skills/sw-config/SKILL.md +39 -0
  20. package/skills/sw-config/assets/implementer.md +15 -0
  21. package/skills/sw-config/assets/planner.md +17 -0
  22. package/skills/sw-config/scripts/config.mjs +104 -0
  23. package/skills/sw-explain/SKILL.md +30 -0
  24. package/skills/sw-implement/SKILL.md +46 -0
  25. package/skills/sw-ingest/SKILL.md +43 -0
  26. package/skills/sw-init/SKILL.md +52 -0
  27. package/skills/sw-init/assets/agents-block.md +29 -0
  28. package/skills/sw-init/assets/sw.mjs +523 -0
  29. package/skills/sw-init/assets/templates/page.md +18 -0
  30. package/skills/sw-init/assets/templates/plan.md +25 -0
  31. package/skills/sw-init/assets/templates/task.md +33 -0
  32. package/skills/sw-init/assets/viewer.html +1660 -0
  33. package/skills/sw-init/scripts/init.mjs +118 -0
  34. package/skills/sw-lint/SKILL.md +61 -0
  35. package/skills/sw-migrate/SKILL.md +61 -0
  36. package/skills/sw-migrate/scripts/migrate.mjs +225 -0
  37. package/skills/sw-plan/SKILL.md +46 -0
  38. package/skills/sw-triage/SKILL.md +42 -0
  39. package/skills/sw-visualize/SKILL.md +28 -0
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ // Scaffolds docs/ as a Superwiki vault. Safe to re-run: user content is kept, tool files are refreshed.
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
4
+ import { basename, dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const assets = join(dirname(fileURLToPath(import.meta.url)), '..', 'assets');
8
+ const argv = process.argv.slice(2);
9
+ const opt = name => { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : undefined; };
10
+
11
+ if (argv.includes('--help')) {
12
+ console.log(`init.mjs [--root <dir>] [--tasks | --no-tasks] [--areas "M=Mobile,B=Backend"]
13
+
14
+ --root project folder (default: current directory)
15
+ --tasks add the task module (docs/tasks, docs/plans)
16
+ --no-tasks wiki only
17
+ --areas task id prefixes and their names (default: T=Tasks)`);
18
+ process.exit(0);
19
+ }
20
+
21
+ const root = resolve(opt('--root') || '.');
22
+ const docs = join(root, 'docs');
23
+ const dotdir = join(docs, '.sw');
24
+ const configPath = join(dotdir, 'config.json');
25
+ const previous = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf8')) : null;
26
+ const tasks = argv.includes('--no-tasks') ? false : argv.includes('--tasks') ? true : previous?.tasks ?? null;
27
+ if (tasks === null) {
28
+ console.error('say --tasks or --no-tasks');
29
+ process.exit(2);
30
+ }
31
+
32
+ const areas = {};
33
+ for (const pair of (opt('--areas') || '').split(',').map(s => s.trim()).filter(Boolean)) {
34
+ const [id, ...name] = pair.split('=');
35
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(id)) { console.error(`bad area id "${id}": letters and digits, starting with a letter`); process.exit(2); }
36
+ areas[id] = name.join('=').trim() || id;
37
+ }
38
+
39
+ const today = new Date().toISOString().slice(0, 10);
40
+ const report = [];
41
+ const rel = p => p.slice(root.length + 1);
42
+ const dir = p => { if (!existsSync(p)) { mkdirSync(p, { recursive: true }); report.push(`created ${rel(p)}/`); } };
43
+ const keep = (p, content) => {
44
+ if (existsSync(p)) { report.push(`kept ${rel(p)}`); return false; }
45
+ writeFileSync(p, content);
46
+ report.push(`created ${rel(p)}`);
47
+ return true;
48
+ };
49
+ // Writes a tool-owned file and reports created / updated / unchanged from the actual content.
50
+ const write = (p, content, label = rel(p)) => {
51
+ const before = existsSync(p) ? readFileSync(p, 'utf8') : null;
52
+ if (before !== content) writeFileSync(p, content);
53
+ report.push(`${before === null ? 'created ' : before === content ? 'unchanged' : 'updated '} ${label}`);
54
+ };
55
+ const refresh = (from, to) => {
56
+ if (!existsSync(from)) { report.push(`missing ${rel(to)} (not in this Superwiki build; report this to the user)`); return; }
57
+ write(to, readFileSync(from, 'utf8'));
58
+ };
59
+
60
+ const foreign = existsSync(docs) && !previous ? readdirSync(docs).filter(n => !n.startsWith('.')) : [];
61
+
62
+ dir(docs);
63
+ dir(join(docs, 'raw'));
64
+ dir(join(docs, 'raw', 'assets'));
65
+ dir(join(docs, 'wiki'));
66
+ if (tasks) { dir(join(docs, 'tasks')); dir(join(docs, 'plans')); }
67
+ dir(dotdir);
68
+ dir(join(dotdir, 'templates'));
69
+ // Git drops empty folders; the vault needs them to exist.
70
+ for (const d of ['raw/assets', 'wiki', ...(tasks ? ['tasks', 'plans'] : [])]) {
71
+ const p = join(docs, d);
72
+ if (!readdirSync(p).length) writeFileSync(join(p, '.gitkeep'), '');
73
+ }
74
+
75
+ keep(join(docs, 'index.md'), '# Index\n\nCatalog of the wiki: one line per page, `- [[file-name]]: summary`, grouped by type.\n');
76
+ keep(join(docs, 'log.md'), `# Log\n\nAppend-only. Entry format: \`## [YYYY-MM-DD] kind | title\`.\n\n## [${today}] init | Superwiki vault created\n`);
77
+
78
+ // The viewer snapshot and the local server's address are per-machine and regenerated on demand.
79
+ write(join(dotdir, '.gitignore'), 'data.js\nserver.json\n');
80
+ refresh(join(assets, 'sw.mjs'), join(dotdir, 'sw.mjs'));
81
+ refresh(join(assets, 'viewer.html'), join(docs, 'viewer.html'));
82
+ for (const t of ['page.md', ...(tasks ? ['task.md', 'plan.md'] : [])]) refresh(join(assets, 'templates', t), join(dotdir, 'templates', t));
83
+
84
+ const config = {
85
+ version: 1,
86
+ name: previous?.name ?? basename(root),
87
+ tasks,
88
+ areas: tasks ? (Object.keys(areas).length ? areas : previous?.areas ?? { T: 'Tasks' }) : {},
89
+ models: previous?.models ?? { plan: {}, implement: {} },
90
+ ...(previous?.tools ? { tools: previous.tools } : {}),
91
+ };
92
+ write(configPath, JSON.stringify(config, null, 2) + '\n');
93
+
94
+ // Schema block: {{#tasks}}..{{/tasks}} kept with the task module, {{^tasks}}..{{/tasks}} without it.
95
+ const block = readFileSync(join(assets, 'agents-block.md'), 'utf8')
96
+ .replace(/\{\{#tasks\}\}\n([\s\S]*?)\{\{\/tasks\}\}\n/g, (_, body) => (tasks ? body : ''))
97
+ .replace(/\{\{\^tasks\}\}\n([\s\S]*?)\{\{\/tasks\}\}\n/g, (_, body) => (tasks ? '' : body))
98
+ .trimEnd();
99
+ const agentsPath = join(root, 'AGENTS.md');
100
+ const managed = /<!-- sw:start[\s\S]*?<!-- sw:end -->/;
101
+ if (!existsSync(agentsPath)) write(agentsPath, `# Agent instructions\n\n${block}\n`);
102
+ else {
103
+ const text = readFileSync(agentsPath, 'utf8');
104
+ write(agentsPath, managed.test(text) ? text.replace(managed, () => block) : `${text.trimEnd()}\n\n${block}\n`, 'AGENTS.md (Superwiki block)');
105
+ }
106
+
107
+ // Claude Code reads CLAUDE.md, not AGENTS.md; an import keeps one source of truth.
108
+ const claudePath = join(root, 'CLAUDE.md');
109
+ if (!existsSync(claudePath)) {
110
+ writeFileSync(claudePath, '@AGENTS.md\n');
111
+ report.push('created CLAUDE.md (imports AGENTS.md)');
112
+ } else if (!/AGENTS\.md/.test(readFileSync(claudePath, 'utf8'))) {
113
+ report.push('note CLAUDE.md does not mention AGENTS.md; add a line "@AGENTS.md" so Claude Code reads the Superwiki rules');
114
+ } else report.push('kept CLAUDE.md');
115
+
116
+ console.log(report.join('\n'));
117
+ console.log(`\nvault: docs/ tasks: ${tasks ? `on (areas: ${Object.keys(config.areas).join(', ')})` : 'off'}`);
118
+ if (foreign.length) console.log(`\ndocs/ already had content (${foreign.slice(0, 8).join(', ')}${foreign.length > 8 ? ', ...' : ''}). It was left untouched and is outside the vault; sw-migrate converts it.`);
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: sw-lint
3
+ description: Use when the user asks to check, lint, health-check, clean up or audit the Superwiki vault or wiki, after bulk edits or a migration, or invokes sw-lint or sw:lint.
4
+ ---
5
+
6
+ # sw-lint
7
+
8
+ Two passes. The first is a script and costs almost nothing. The second reads pages and costs tokens, so it runs only when the user approves it.
9
+
10
+ ## Pass 1: structure (always)
11
+
12
+ 1. Run `node docs/.sw/sw.mjs lint` from the project root. Read only its output, not the vault. It exits with code 1 while errors remain; that is expected.
13
+ 2. **Fix broken links first, then run lint again**, because one rename produces several findings (`broken-link` in every page that linked the old name, plus `not-in-index` and `orphan-page` for the new one).
14
+ - A rename is established when exactly one page is reported `not-in-index` or `orphan-page` and its name is a variation of the broken target. Re-point every `[[old]]` to `[[new]]` in the files lint named, including `index.md` and task files.
15
+ - Two candidates, or no resemblance: do not guess. Report it.
16
+ 3. Fix what else has one correct answer:
17
+
18
+ | Finding | Fix |
19
+ |---|---|
20
+ | `not-in-index` | add `- [[page]]: summary` to `index.md`, using the page's `summary:` |
21
+ | `missing-field` `summary` | copy it from the page's line in `index.md`; if there is none, read the page and write it |
22
+ | `missing-field` `type` | read that page, write the field |
23
+ | `missing-date` | fill from `git log --follow --format=%ad --date=short -- <file>` when the project is under git and the date is unambiguous |
24
+
25
+ 4. Report the rest and let the user decide. Do not change a task's `status` or `deps` to silence a finding.
26
+
27
+ | Finding | Why it needs a human |
28
+ |---|---|
29
+ | `started-before-deps`, `done-before-deps`, `dep-cycle`, `cancelled-dep` | either the status or the dependency is wrong; only the user knows which |
30
+ | `missing-date` you could not fill | no history to take it from, or it hangs on a finding above |
31
+ | `duplicate-name` | one of the pages must be renamed and every link to it re-pointed |
32
+ | `broken-link` with no clear target, `orphan-plan`, `unknown-dep` | the page may be missing or the reference stale |
33
+ | `orphan-page` | may be fine; may want a link from a related page |
34
+
35
+ 5. Run lint again and state the before and after counts.
36
+
37
+ ## Pass 2: meaning (ask first)
38
+
39
+ Offer it with its size: "The wiki has N pages; a semantic review reads them. Run it?" (N from `node docs/.sw/sw.mjs status`). If approved:
40
+
41
+ 1. Read `index.md`. Collect follow-ups that ingests deferred: find the last lint entry with `grep -n '^## \[.*\] lint' docs/log.md | tail -1` and read the log from that line to the end (the whole log only if there is no lint entry and it is short; otherwise `tail -60`).
42
+ 2. Read the wiki pages. Up to about twenty pages, read them all; beyond that, go group by group (same `type`, or linked to each other). Open a file in `raw/` only to settle whether two pages really contradict each other. Look for:
43
+ - claims that contradict each other;
44
+ - a `decision` page superseded by a later one without saying so;
45
+ - concepts named on several pages that have no page of their own;
46
+ - pages that should link to each other and do not;
47
+ - summaries or titles that no longer match the page;
48
+ - each collected follow-up: still needed, or already handled.
49
+ 3. Present the findings as a numbered list with the pages involved, and say which ones you recommend applying now. Apply only what the user agrees to.
50
+
51
+ ## Finish
52
+
53
+ 1. Run `node docs/.sw/sw.mjs lint` once more if you edited anything after the last run.
54
+ 2. Append to `docs/log.md`, in the layout its last entries use:
55
+
56
+ ```
57
+ ## [YYYY-MM-DD] lint | <N> fixed, <M> open
58
+ Fixed: <what>. Open: <findings left for the user>. Follow-up: <semantic findings and ingest follow-ups not yet handled>.
59
+ ```
60
+
61
+ Carry every unhandled follow-up into this entry: the next lint reads only from here on. Name a page that was renamed or removed in backticks, not as a `[[link]]`.
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: sw-migrate
3
+ description: Use when the user wants to convert an existing docs folder, task index, markdown task tables, backlog or changelog into a Superwiki vault, or invokes sw-migrate or sw:migrate.
4
+ ---
5
+
6
+ # sw-migrate
7
+
8
+ Converts a project that tracks work in markdown tables into a Superwiki vault: one file per task, wikilinks, an append-only log. A script does the bulk conversion from a mapping you write; you only read samples, never the whole index.
9
+
10
+ Run everything from the project root. `<skill-dir>` is this skill's directory; `<init-dir>` is the sw-init skill's directory (a sibling folder).
11
+
12
+ ## Preconditions
13
+
14
+ Stop and tell the user if any of these fails; do not work around them.
15
+
16
+ - The project is a git repository and `git status --porcelain` is empty.
17
+ - `docs/.sw/` does not exist (already a vault).
18
+ - Node 18 or newer.
19
+
20
+ ## Steps
21
+
22
+ 1. **Branch**: `git switch -c sw-migrate`. All changes stay on this branch. Never merge, push or delete the branch yourself.
23
+ 2. **Sample, do not read.** The index can be hundreds of kilobytes.
24
+ - `ls docs docs/*/ | head -60` for the layout.
25
+ - `grep -n '^|' docs/<index> | awk 'length > 0' | cut -c1-200 | head -40` for table headers and a few rows.
26
+ - `grep -n '^## ' docs/<detail file> | head` and about 30 lines of one section, if tasks have detail files.
27
+ - The status values in use: `grep -o '| *[A-Za-z ✅]* *|' ... | sort | uniq -c` on the status column, or read ten rows.
28
+ 3. **Write the mapping** to a temporary file outside the repo. `node <skill-dir>/scripts/migrate.mjs --help` prints the format. Decide:
29
+ - which columns are id, title, status, dependencies, milestone, order, dates;
30
+ - every status value → `todo`, `in-progress`, `done` or `cancelled`;
31
+ - the marker for soft dependencies, if the project has them;
32
+ - which other columns to keep as body sections (sources, notes);
33
+ - where per-task detail sections live and their heading prefix;
34
+ - the changelog file and its columns, if any.
35
+ 4. **Dry run**: `node <skill-dir>/scripts/migrate.mjs --mapping <file> --dry-run`.
36
+ 5. **Show the user the mapping and the dry-run report, and wait for approval.** State the task counts per status next to the project's own numbers (its summary table, or a `grep -c`). If they differ, find out why before going on. Fix every line under `problems`.
37
+ 6. **Convert**: the same command without `--dry-run`, then the `init.mjs` command the report prints (replace the area names with real ones), then `node docs/.sw/sw.mjs status` and `node docs/.sw/sw.mjs lint`.
38
+ 7. **Verify**: `status` totals equal the dry-run counts and the project's own numbers; `lint` has 0 errors. Lint errors here are real inconsistencies in the source (a task started before its dependency finished, a dependency cycle). Report them; do not edit task files to make them pass.
39
+ 8. **Report** what moved where, the counts, the lint result, and what is left for a human decision (next section). Commit only if the user asks.
40
+
41
+ ## What the script does not do
42
+
43
+ Tell the user about each of these; act only on what they choose.
44
+
45
+ | Left over | Where it is | Options |
46
+ |---|---|---|
47
+ | Rules and conventions written in the old index | `docs/legacy/` | most are replaced by the Superwiki block in `AGENTS.md`; project-specific ones go to `AGENTS.md` outside the Superwiki block |
48
+ | Milestones, glossaries, decision records in the old index | `docs/legacy/` | turn into `docs/wiki/` pages with `type:` and `summary:`, and list them in `index.md` |
49
+ | Research, specs, decisions in other folders | unchanged, outside the vault | leave, or move into `raw/` (sources) or `wiki/` (maintained pages) |
50
+ | Plans written by other tools | unchanged | leave, or move to `docs/plans/<ID>-plan.md` when a plan belongs to exactly one task |
51
+ | The old viewer or scripts that parse the old index | unchanged | delete once `docs/viewer.html` shows the same numbers |
52
+ | Instructions in `AGENTS.md` / `CLAUDE.md` that describe the old index | unchanged | rewrite to point at the Superwiki rules; show the diff first |
53
+
54
+ `docs/legacy/` is an archive, not part of the vault. Delete it only when the user says so.
55
+
56
+ ## Common mistakes
57
+
58
+ - Reading the whole index to "understand it first". Samples are enough; the dry run tells you what did not parse.
59
+ - Mapping a status the script reported as unknown to `todo` without asking. Ask what it means.
60
+ - Fixing lint errors by changing statuses. The old data was inconsistent; the user decides which side is right.
61
+ - Hand-editing hundreds of links. If the script missed a link shape, fix the mapping or report it.
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env node
2
+ // Converts a table-based task index into Superwiki task files, driven by a mapping file.
3
+ // Bulk, mechanical work only: what a table row and a detail section say goes into one task file,
4
+ // links to tasks become wikilinks, consumed files move to an archive folder. Nothing is deleted.
5
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, renameSync, statSync } from 'node:fs';
6
+ import { dirname, join, relative, resolve, posix } from 'node:path';
7
+
8
+ const HELP = `migrate.mjs --mapping <file.json> [--docs <dir>] [--dry-run]
9
+
10
+ Mapping (paths relative to the docs folder):
11
+ {
12
+ "index": "index.md", file holding the task tables
13
+ "columns": { "id": "ID", "title": "Task", "status": "Status", "deps": "Depends on",
14
+ "milestone": "Target", "priority": "Order", "started": "Start", "finished": "End" },
15
+ "sections": { "Sources": "Source", "Notes": "Note" }, table columns kept as body sections
16
+ "status": { "Not started": "todo", "In progress": "in-progress", "Done": "done" },
17
+ "softPrefix": "~", marks a soft dependency in the deps column
18
+ "details": { "dir": "tasks", "heading": "## " }, per-task detail sections: "<heading><ID>"
19
+ "log": { "file": "changelog.md", "columns": { "date": "Date", "id": "Task", "text": "Change" } },
20
+ "archive": "legacy" consumed files are moved here
21
+ }
22
+ Only "index", "columns.id", "columns.title", "columns.status" and "status" are required.`;
23
+
24
+ const argv = process.argv.slice(2);
25
+ const opt = n => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : undefined; };
26
+ if (argv.includes('--help') || !opt('--mapping')) { console.log(HELP); process.exit(argv.includes('--help') ? 0 : 2); }
27
+ const dry = argv.includes('--dry-run');
28
+ const docs = resolve(opt('--docs') || 'docs');
29
+ const map = JSON.parse(readFileSync(resolve(opt('--mapping')), 'utf8'));
30
+ const archive = map.archive || 'legacy';
31
+ const soft = map.softPrefix ?? '~';
32
+ const ID = /[A-Z][A-Z0-9]*-\d+/;
33
+ const fail = msg => { console.error(msg); process.exit(2); };
34
+
35
+ const indexPath = join(docs, map.index);
36
+ if (!existsSync(indexPath)) fail(`no ${map.index} in ${docs}`);
37
+ // Empty folders left by an aborted run are not a vault and not an archive.
38
+ const hasFiles = d => existsSync(d) && readdirSync(d, { recursive: true, withFileTypes: true }).some(e => e.isFile());
39
+ if (existsSync(join(docs, '.sw', 'config.json'))) fail('docs/.sw/config.json exists: this folder is already a Superwiki vault');
40
+ if (hasFiles(join(docs, archive))) fail(`docs/${archive} has files in it: choose another "archive" name or remove it`);
41
+
42
+ // ---------- Markdown tables ----------
43
+ const splitRow = line => line.trim().replace(/^\|/, '').replace(/\|$/, '').split(/(?<!\\)\|/).map(c => c.trim().replace(/\\\|/g, '|'));
44
+ function tables(text) {
45
+ const lines = text.split(/\r?\n/);
46
+ const out = [];
47
+ for (let i = 0; i + 1 < lines.length; i++) {
48
+ if (!lines[i].trim().startsWith('|') || !/^\s*\|?[\s:|-]+\|[\s:|-]*$/.test(lines[i + 1])) continue;
49
+ const head = splitRow(lines[i]);
50
+ const rows = [];
51
+ let j = i + 2;
52
+ for (; j < lines.length && lines[j].trim().startsWith('|'); j++) {
53
+ const cells = splitRow(lines[j]);
54
+ rows.push(Object.fromEntries(head.map((h, k) => [h, cells[k] ?? ''])));
55
+ }
56
+ out.push({ head, rows });
57
+ i = j - 1;
58
+ }
59
+ return out;
60
+ }
61
+
62
+ // ---------- Links ----------
63
+ const consumed = new Set([map.index]); // files whose task anchors turn into wikilinks
64
+ const detailDir = map.details?.dir;
65
+ if (detailDir && existsSync(join(docs, detailDir))) {
66
+ for (const n of readdirSync(join(docs, detailDir))) if (n.endsWith('.md')) consumed.add(posix.join(detailDir, n));
67
+ }
68
+ if (map.log?.file) consumed.add(map.log.file);
69
+
70
+ const stats = { taskLinks: 0, relinked: 0, deadLinks: [] };
71
+ let ids = new Set();
72
+
73
+ // from: the file the text was in. to: the file it will be in. Both relative to docs.
74
+ function rewrite(text, from, to) {
75
+ const fromDir = posix.dirname(from);
76
+ const toDir = posix.dirname(to);
77
+ const decode = s => { try { return decodeURI(s); } catch { return s; } };
78
+ // Row anchors only existed as link targets for the old index; in a foreign file they are not ours to remove.
79
+ return (from === to ? text : text.replace(/<a id="[^"]*"><\/a>/g, ''))
80
+ .replace(/\[([^\[\]]*)\]\(([^)\s]+)\)( ✅)?/g, (all, label, href, tick) => {
81
+ if (/^[a-z][a-z0-9+.-]*:/i.test(href)) return all;
82
+ const [path, anchor = ''] = href.split('#');
83
+ const target = path ? posix.normalize(posix.join(fromDir, decode(path))) : from;
84
+ if (consumed.has(target)) {
85
+ const id = (anchor.match(new RegExp(`^${ID.source}$`, 'i')) || [])[0]?.toUpperCase();
86
+ if (id && ids.has(id)) { stats.taskLinks++; return label.trim() === id ? `[[${id}]]` : `[[${id}|${label}]]`; }
87
+ stats.deadLinks.push(`${to}: ${all.slice(0, 80)}`);
88
+ return `[${label}](${encodeURI(posix.relative(toDir, posix.join(archive, target)))}${anchor ? `#${anchor}` : ''})${tick ?? ''}`;
89
+ }
90
+ if (!path || fromDir === toDir) return all;
91
+ stats.relinked++;
92
+ return `[${label}](${encodeURI(posix.relative(toDir, target))}${anchor ? `#${anchor}` : ''})${tick ?? ''}`;
93
+ });
94
+ }
95
+
96
+ // ---------- Read the index ----------
97
+ const col = map.columns;
98
+ const indexText = readFileSync(indexPath, 'utf8');
99
+ const rows = tables(indexText).filter(t => [col.id, col.title, col.status].every(h => t.head.includes(h))).flatMap(t => t.rows);
100
+ if (!rows.length) fail(`no table in ${map.index} has the columns "${col.id}", "${col.title}", "${col.status}"`);
101
+
102
+ const plain = s => s.replace(/<[^>]+>/g, '').replace(/\[([^\[\]]*)\]\([^)]*\)/g, '$1').replace(/[`*]/g, '').trim();
103
+ const statusOf = cell => {
104
+ const text = plain(cell).replace(/✅/g, '').trim();
105
+ const hit = Object.keys(map.status).find(k => k.toLowerCase() === text.toLowerCase());
106
+ return hit ? map.status[hit] : null;
107
+ };
108
+
109
+ const tasks = [];
110
+ const problems = [];
111
+ for (const r of rows) {
112
+ const id = (plain(r[col.id]).match(ID) || [])[0];
113
+ if (!id) { problems.push(`row without an id: ${r[col.id].slice(0, 60)}`); continue; }
114
+ if (ids.has(id)) { problems.push(`duplicate id ${id}`); continue; }
115
+ ids.add(id);
116
+ const status = statusOf(r[col.status]);
117
+ if (!status) problems.push(`${id}: status "${plain(r[col.status])}" is not in the mapping`);
118
+ const deps = [];
119
+ const softDeps = [];
120
+ for (const m of (col.deps ? r[col.deps] : '').matchAll(new RegExp(`(${soft.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})?\\[?(${ID.source})`, 'g'))) {
121
+ const list = m[1] ? softDeps : deps;
122
+ if (!list.includes(m[2])) list.push(m[2]);
123
+ }
124
+ const get = k => (col[k] ? plain(r[col[k]] ?? '') : '');
125
+ tasks.push({ id, row: r, title: get('title'), status: status || 'todo', deps, softDeps, milestone: get('milestone'), priority: get('priority'), started: get('started'), finished: get('finished') });
126
+ }
127
+
128
+ // ---------- Detail sections ----------
129
+ const details = new Map();
130
+ const strayDetails = [];
131
+ if (detailDir) {
132
+ const mark = map.details.heading || '## ';
133
+ for (const file of [...consumed].filter(f => f.startsWith(`${detailDir}/`))) {
134
+ let cur = null;
135
+ for (const line of readFileSync(join(docs, file), 'utf8').split(/\r?\n/)) {
136
+ const id = line.startsWith(mark) ? (line.slice(mark.length).trim().match(new RegExp(`^${ID.source}$`)) || [])[0] : null;
137
+ if (id) { cur = { file, lines: [] }; (ids.has(id) ? details.set(id, cur) : strayDetails.push(`${file}: ${id}`)); continue; }
138
+ if (line.startsWith(mark)) cur = null; // a same-level heading that is not a task ends the section
139
+ if (cur) cur.lines.push(line);
140
+ }
141
+ }
142
+ }
143
+
144
+ // ---------- Write ----------
145
+ // Single-quoted YAML: the only escape is a doubled quote, so titles with " or \\ survive as written.
146
+ const yaml = v => (/^[\w./-]*$/.test(v) ? v : `'${v.replace(/'/g, "''")}'`);
147
+ const out = new Map();
148
+ for (const t of tasks) {
149
+ const to = `tasks/${t.id}.md`;
150
+ const fm = ['---', 'type: task', `id: ${t.id}`, `title: ${yaml(t.title)}`, `status: ${t.status}`, `deps: [${t.deps.join(', ')}]`];
151
+ if (t.softDeps.length) fm.push(`soft_deps: [${t.softDeps.join(', ')}]`);
152
+ if (t.milestone) fm.push(`milestone: ${yaml(t.milestone)}`);
153
+ if (/^\d+$/.test(t.priority)) fm.push(`priority: ${t.priority}`);
154
+ fm.push(`started: ${t.started}`.trimEnd(), `finished: ${t.finished}`.trimEnd(), '---', '');
155
+ const body = [];
156
+ const d = details.get(t.id);
157
+ if (d) body.push(rewrite(d.lines.join('\n').trim(), d.file, to), '');
158
+ for (const [name, column] of Object.entries(map.sections || {})) {
159
+ const cell = (t.row[column] || '').trim();
160
+ if (cell) body.push(`## ${name}`, '', rewrite(cell, map.index, to), '');
161
+ }
162
+ out.set(to, fm.join('\n') + '\n' + body.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n');
163
+ }
164
+
165
+ let logEntries = 0;
166
+ if (map.log?.file && existsSync(join(docs, map.log.file))) {
167
+ const lc = map.log.columns;
168
+ const entries = tables(readFileSync(join(docs, map.log.file), 'utf8'))
169
+ .filter(t => [lc.date, lc.text].every(h => t.head.includes(h))).flatMap(t => t.rows)
170
+ .filter(r => /^\d{4}-\d{2}-\d{2}/.test(plain(r[lc.date])));
171
+ // Stable sort keeps same-day rows in reverse source order: sources list newest first, the log is oldest first.
172
+ entries.reverse().sort((a, b) => plain(a[lc.date]).localeCompare(plain(b[lc.date])));
173
+ logEntries = entries.length;
174
+ const text = entries.map(r => `## [${plain(r[lc.date]).slice(0, 10)}] task | ${lc.id ? plain(r[lc.id]) || 'general' : 'general'}\n\n${rewrite(r[lc.text], map.log.file, 'log.md')}\n`).join('\n');
175
+ out.set('log.md', `# Log\n\nAppend-only. Entry format: \`## [YYYY-MM-DD] kind | title\`.\n\n${text}`);
176
+ }
177
+
178
+ // Other markdown files that pointed at tasks in the consumed files.
179
+ const others = [];
180
+ (function walk(dir, rel) {
181
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
182
+ if (e.name.startsWith('.') || e.name === 'node_modules') continue;
183
+ const p = rel ? `${rel}/${e.name}` : e.name;
184
+ if (e.isDirectory()) walk(join(dir, e.name), p);
185
+ else if (e.name.endsWith('.md') && !consumed.has(p) && statSync(join(dir, e.name)).size < 2e6) others.push(p);
186
+ }
187
+ })(docs, '');
188
+ let touchedOthers = 0;
189
+ for (const p of others) {
190
+ const before = readFileSync(join(docs, p), 'utf8');
191
+ const after = rewrite(before, p, p);
192
+ if (after === before) continue;
193
+ touchedOthers++;
194
+ out.set(p, after);
195
+ }
196
+
197
+ const counts = {};
198
+ for (const t of tasks) counts[t.status] = (counts[t.status] || 0) + 1;
199
+ const areas = [...new Set(tasks.map(t => t.id.slice(0, t.id.lastIndexOf('-'))))];
200
+ const unknownDeps = tasks.flatMap(t => [...t.deps, ...t.softDeps].filter(d => !ids.has(d)).map(d => `${t.id} -> ${d}`));
201
+
202
+ if (!dry) {
203
+ for (const f of consumed) {
204
+ if (!existsSync(join(docs, f))) continue;
205
+ mkdirSync(dirname(join(docs, archive, f)), { recursive: true });
206
+ renameSync(join(docs, f), join(docs, archive, f));
207
+ }
208
+ for (const [p, text] of out) {
209
+ mkdirSync(dirname(join(docs, p)), { recursive: true });
210
+ writeFileSync(join(docs, p), text);
211
+ }
212
+ }
213
+
214
+ const show = (title, list, max = 10) => list.length && console.log(`\n${title} (${list.length}):\n${list.slice(0, max).map(s => ` ${s}`).join('\n')}${list.length > max ? `\n ... ${list.length - max} more` : ''}`);
215
+ console.log(`${dry ? 'DRY RUN, nothing written\n' : ''}tasks: ${tasks.length} ${Object.entries(counts).map(([k, v]) => `${k} ${v}`).join(' ')}`);
216
+ console.log(`areas: ${areas.join(', ')}`);
217
+ console.log(`with detail section: ${[...details.keys()].length} without: ${tasks.length - details.size}`);
218
+ console.log(`task links turned into wikilinks: ${stats.taskLinks} relative links re-based: ${stats.relinked} other files updated: ${touchedOthers}`);
219
+ if (map.log?.file) console.log(`log entries: ${logEntries}`);
220
+ console.log(`archived to docs/${archive}/: ${[...consumed].join(', ')}`);
221
+ show('problems', problems);
222
+ show('dependencies on unknown ids', unknownDeps);
223
+ show('detail sections without a table row (left in the archive)', strayDetails);
224
+ show(`links into archived files that are not task links (now point into ${archive}/)`, stats.deadLinks);
225
+ console.log(`\nnext: node <sw-init>/scripts/init.mjs --tasks --areas "${areas.map(a => `${a}=${a}`).join(',')}" then node docs/.sw/sw.mjs lint`);
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: sw-plan
3
+ description: Use when the user wants to plan a task or a piece of work in a Superwiki project before building it, asks what to work on next, or invokes sw-plan or sw:plan, with or without a task id.
4
+ ---
5
+
6
+ # sw-plan
7
+
8
+ Produces `docs/plans/<ID>-plan.md` for one task. You clarify with the user and get approval; a planner subagent, running the model set in sw-config, reads the code and writes the plan. Nothing but task and plan files changes during planning.
9
+
10
+ Needs the task module (`docs/tasks/`). If it is missing, say so and offer sw-init with `--tasks`. Run commands from the project root. `<skill-dir>` is the directory this SKILL.md is in.
11
+
12
+ ## Steps
13
+
14
+ 1. **Pick the task.**
15
+ - Id given: `node docs/.sw/sw.mjs check <ID>`, then read `docs/tasks/<ID>.md`. Status `done` or `cancelled`: stop and ask what the user wants. A plan already exists: this run revises it; say so. Open deps do not prevent planning.
16
+ - No id, existing work: `node docs/.sw/sw.mjs ready`, and let the user choose.
17
+ - New work: agree on title, area and dependencies with the user, get the id from `node docs/.sw/sw.mjs next-id <AREA>`, and write `docs/tasks/<ID>.md` from `docs/.sw/templates/task.md` with `status: todo`, a "Goal" and a "Done when" list. Append `## [date] task | <ID> created` to `docs/log.md`.
18
+ 2. **Clarify.** Ask the user only what the task file leaves open about scope or intent, one question at a time, each with your recommendation. Add the answers to the task's "Notes" now. Do not read code to find questions; the planner surfaces the technical ones in step 5. Skip this when nothing is open.
19
+ 3. **Enter plan mode** if this tool gives you a way to (Claude Code: `EnterPlanMode`). Otherwise continue. From here on you change no file until step 6.
20
+ 4. **Dispatch the planner.** Its prompt is: the task id, today's date, the project root if it is not your working directory, and any feedback from an earlier round. Do not read the code yourself; that is the planner's job and its context, not yours.
21
+
22
+ | Tool | How |
23
+ |---|---|
24
+ | Claude Code | agent `sw-planner`. If it is not among your agent types, use the read-only `Plan` agent, put the content of `<skill-dir>/../sw-config/assets/planner.md` at the top of its prompt, and pass the model from `models.plan.claude` in `docs/.sw/config.json` if set |
25
+ | Codex | spawn the custom agent `sw_planner` |
26
+ | Copilot CLI | `task` tool with agent `sw-planner` |
27
+ | No subagents available, or the agent is not defined | follow `planner.md` yourself, in this session, and tell the user the configured model was not used |
28
+
29
+ 5. **Show the plan and get approval.** Present the approach, the steps and the planner's notes (Claude Code: through `ExitPlanMode`; anywhere else, as a normal message). The planner returns the plan, then a line `=== notes ===`, then its notes.
30
+ - Open questions in the notes: ask them, each with the planner's assumed answer as your recommendation. If an answer differs from what the plan assumed, dispatch the planner again with the answers.
31
+ - The user wants changes: dispatch the planner again with their feedback.
32
+ - The planner says the work needs splitting: propose the tasks; create them (step 1, "New work") only when the user agrees.
33
+ 6. **Write**, after approval:
34
+ - `docs/plans/<ID>-plan.md`: the text before `=== notes ===`, unchanged. If something in it is wrong, send it back to the planner; do not edit it yourself.
35
+ - answers given in step 5: add to the task's "Notes".
36
+ - `docs/log.md`: append `## [date] plan | <ID>`, in the layout the log's last entries use.
37
+
38
+ Then run `node docs/.sw/sw.mjs lint`.
39
+ 7. **Stop.** Do not start implementing. Tell the user the plan is saved and that sw-implement `<ID>` runs it.
40
+
41
+ ## Common mistakes
42
+
43
+ - Exploring the codebase before dispatching. You pay for it twice.
44
+ - Writing the plan file before approval.
45
+ - Setting the task to `in-progress`. Planning does not change status.
46
+ - Planning several tasks in one plan file. One task, one plan; shared design goes to a `type: decision` wiki page that the plans link.
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: sw-triage
3
+ description: Use when the user reports a bug, failure, error, regression, incident or unexpected behavior in a Superwiki project and wants to know whether it happened before, what was learned, or what the likely causes are, or invokes sw-triage or sw:triage.
4
+ ---
5
+
6
+ # sw-triage
7
+
8
+ Answers "have we seen this before, and what does the brain say about it?" before anyone starts debugging. It searches the vault with a script, reads only the best matches, and reports. It does not fix anything and does not change files unless the user accepts an offer at the end.
9
+
10
+ Run commands from the project root.
11
+
12
+ ## Steps
13
+
14
+ 1. **State the problem in one line**: the symptom, where it shows, since when. If the user gave no symptom, ask for it. If "since when" is missing, go on and say in the report that recent changes cannot be matched to the problem's start.
15
+ 2. **Search the brain**, two or three times with different words: the error text or code, the component or feature name, the symptom in plain words. Search in the language the vault is written in (look at the lines of `docs/index.md`), whatever language the user wrote in; translate their words.
16
+
17
+ ```bash
18
+ node docs/.sw/sw.mjs search <three to six distinctive words>
19
+ ```
20
+
21
+ Each result has the page's type and summary and the first matching line; matching log entries follow, with their first line. A page is listed when any word matches, best match first, so the tail of the list is often noise.
22
+ 3. **Look at what changed recently**: `tail -40 docs/log.md` and `node docs/.sw/sw.mjs ready` (the "in progress" part). Work finished or under way in the same area just before the problem appeared is a suspect.
23
+ 4. **Read at most four files**, plans included, in this order of preference: `lesson` pages, `decision` pages, tasks that touched the same area (done or in progress), then the rest. Read a task's plan only if steps 2 and 3 point at that task as the cause.
24
+ 5. **Report in the user's language**, under these headings. Give the source of each claim: a page as `[[page]]`, a log entry as "log, <date>".
25
+ - **Seen before**: earlier occurrences, what the cause was, how it was fixed. If no page records this problem, say plainly that the brain has no record of it; do not stretch a weak match. If a recorded fix exists and the problem is back, say so: it is a regression.
26
+ - **Lessons that apply**: rules or warnings from lesson and decision pages that bear on this problem.
27
+ - **Likely causes**: ranked, each with the evidence for it and one concrete check that would confirm or rule it out. Where the brain records no cause, these are hypotheses drawn from what the pages say about the area: label them as hypotheses.
28
+ - **Recent changes in the area**: tasks and log entries from step 3.
29
+ - **Gaps**: what the brain does not cover and would have helped.
30
+ 6. **Offer, and act only on a yes to that offer**:
31
+ - a task for the fix. Say which area you would file it under and let the user correct it; then the id comes from `node docs/.sw/sw.mjs next-id <AREA>`, the file from `docs/.sw/templates/task.md`, with the related pages under "Sources" and the confirming check as the first "Done when" item;
32
+ - running the top check now, if it is one you can run from this session (a command, a test, reading code). Otherwise say who can run it and what to look for.
33
+
34
+ ## After the problem is solved
35
+
36
+ When the cause is known and fixed (in this session or a later one), offer to save a lesson: `docs/wiki/<slug>.md` with `type: lesson`, a one-line `summary:` that names the symptom, and the sections **Symptom**, **Cause**, **Fix**, **How to notice it earlier**; linked from the task that fixed it and listed in `index.md`. This page is what the next triage finds.
37
+
38
+ ## Common mistakes
39
+
40
+ - Reading code or reproducing the bug before searching. Triage is the lookup; debugging comes after, with the report in hand.
41
+ - Opening every search result. Summaries and matching lines decide which four are worth reading.
42
+ - Presenting a guess as history. "Seen before" is only for what a page actually records.
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: sw-visualize
3
+ description: Use when the user wants to see, open, browse or visualize the Superwiki vault, task board, dependency waves or wiki in a browser, or invokes sw-visualize or sw:visualize.
4
+ ---
5
+
6
+ # sw-visualize
7
+
8
+ Opens the viewer on this project's vault. One command; the user picks nothing. Do not read vault files or generate HTML yourself.
9
+
10
+ Run from the project root:
11
+
12
+ ```bash
13
+ node docs/.sw/sw.mjs serve --open
14
+ ```
15
+
16
+ It starts a small local server for this project (or reuses the one already running), prints its address, and opens it in the default browser. The page reads the files as they are: after the vault changes, **Refresh** in the page shows the new state. The server answers only on this machine and stops by itself after two hours without use.
17
+
18
+ Tell the user the address it printed and that Refresh re-reads the files. Nothing else.
19
+
20
+ ## If it fails
21
+
22
+ | Problem | Do |
23
+ |---|---|
24
+ | `docs/.sw/sw.mjs` or `docs/viewer.html` missing | say so; offer to run sw-init, which installs them |
25
+ | "could not start the viewer server" | `node docs/.sw/sw.mjs snapshot`, then open `docs/viewer.html` as a file (`open`, `xdg-open` or `start`). Tell the user this view is frozen at the moment of the snapshot; running sw-visualize again refreshes it |
26
+ | No browser on this machine (remote session) | give the user the snapshot path instead: `docs/viewer.html` opens on any machine that has the `docs/` folder |
27
+
28
+ If the user asks a question the viewer answers (counts, what is ready, what blocks a task), answer it with `node docs/.sw/sw.mjs status|ready|check <ID>` instead of opening the browser.