superwiki 0.1.2 → 0.1.3

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sw",
3
3
  "description": "An LLM-maintained wiki and task tracker in docs/ for coding agents. Obsidian friendly, with a static viewer.",
4
- "version": "0.1.2",
4
+ "version": "0.1.3",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "wiki",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sw",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "An LLM-maintained wiki and task tracker in docs/ for coding agents. Obsidian friendly, with a static viewer.",
5
5
  "license": "MIT",
6
6
  "skills": "./skills/",
package/README.md CHANGED
@@ -159,12 +159,12 @@ A project's `docs/` folder is plain markdown and keeps working as an Obsidian va
159
159
  | `sw-migrate` | convert an existing table-based task index, on a new git branch |
160
160
  | `sw-ingest` | file a source into the wiki |
161
161
  | `sw-plan` | plan a task with the planner subagent and get your approval |
162
- | `sw-implement` | run a task with the implementer subagent and record the result |
162
+ | `sw-implement` | run a task with the implementer subagent, have it reviewed if the task asks for that, and record the result |
163
163
  | `sw-explain` | explain a task: what, why, dependencies, what it unblocks |
164
164
  | `sw-triage` | for a problem: seen before? lessons, likely causes |
165
165
  | `sw-lint` | structural checks by script, semantic review on request |
166
166
  | `sw-visualize` | open the viewer |
167
- | `sw-config` | the model each tool uses for planning and implementing; task areas |
167
+ | `sw-config` | the model each tool uses for planning, implementing and reviewing; task areas |
168
168
 
169
169
  ### Examples
170
170
 
@@ -188,7 +188,7 @@ Shown as typed in Claude Code. In Codex, write `$sw-plan` instead of `/sw-plan`.
188
188
  /sw-ingest ~/Downloads/interview-notes.md
189
189
  file a source and summarise it into the wiki
190
190
 
191
- /sw-config plan with opus, implement with sonnet
191
+ /sw-config plan with opus, implement with sonnet, review with opus
192
192
  /sw-lint check links, frontmatter and task dependencies
193
193
  /sw-visualize open the task board and the wiki in the browser
194
194
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superwiki",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Agent skills that turn docs/ into an LLM-maintained wiki and task tracker. Obsidian-friendly. Works with Claude Code, Codex and Copilot CLI.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: sw-config
3
- description: Use when the user wants to choose or change which model plans or implements Superwiki tasks (opus, sonnet, gpt and so on), add task areas, see the Superwiki configuration, or invokes sw-config or sw:config.
3
+ description: Use when the user wants to choose or change which model plans, implements or reviews Superwiki tasks (opus, sonnet, gpt and so on), add task areas, see the Superwiki configuration, or invokes sw-config or sw:config.
4
4
  ---
5
5
 
6
6
  # sw-config
@@ -10,7 +10,8 @@ Settings live in `docs/.sw/config.json`. Change them with the script, from the p
10
10
  ```bash
11
11
  node <skill-dir>/scripts/config.mjs show
12
12
  node <skill-dir>/scripts/config.mjs model plan claude opus
13
- node <skill-dir>/scripts/config.mjs model implement codex gpt-6
13
+ node <skill-dir>/scripts/config.mjs model implement claude sonnet
14
+ node <skill-dir>/scripts/config.mjs model review codex gpt-6
14
15
  node <skill-dir>/scripts/config.mjs model plan copilot --unset
15
16
  node <skill-dir>/scripts/config.mjs areas "M=Mobile,B=Backend"
16
17
  node <skill-dir>/scripts/config.mjs sync --tools claude,codex,copilot
@@ -18,13 +19,21 @@ node <skill-dir>/scripts/config.mjs sync --tools claude,codex,copilot
18
19
 
19
20
  ## Models
20
21
 
21
- A model is chosen per role (`plan`, `implement`) and per tool (`claude`, `codex`, `copilot`), because each tool can only run its own models. No tool lets a skill change the model of the running session, so sw-plan and sw-implement hand the work to a subagent, and the subagent's file carries the model.
22
+ A model is chosen per role and per tool, because each tool can only run its own models.
22
23
 
23
- | Tool | Files the script writes |
24
- |---|---|
25
- | Claude Code | `.claude/agents/sw-planner.md`, `sw-implementer.md` |
26
- | Codex | `.codex/agents/sw-planner.toml`, `sw-implementer.toml` |
27
- | Copilot CLI | `.github/agents/sw-planner.agent.md`, `sw-implementer.agent.md` |
24
+ | Role | Does | Used by |
25
+ | --- | --- | --- |
26
+ | `plan` | writes the plan file for a task | sw-plan |
27
+ | `implement` | does the work of a task | sw-implement |
28
+ | `review` | reviews the implementation from a clean context, for tasks whose frontmatter has `review:` | sw-implement |
29
+
30
+ No tool lets a skill change the model of the running session, so the skills hand the work to a subagent, and the subagent's file carries the model. The script writes one file per role:
31
+
32
+ | Tool | Folder | Files |
33
+ | --- | --- | --- |
34
+ | Claude Code | `.claude/agents/` | `sw-planner.md`, `sw-implementer.md`, `sw-reviewer.md` |
35
+ | Codex | `.codex/agents/` | `sw-planner.toml`, `sw-implementer.toml`, `sw-reviewer.toml` |
36
+ | Copilot CLI | `.github/agents/` | `sw-planner.agent.md`, `sw-implementer.agent.md`, `sw-reviewer.agent.md` |
28
37
 
29
38
  When the user asks to set a model:
30
39
 
@@ -32,6 +41,8 @@ When the user asks to set a model:
32
41
  2. Run the `model` command. Show its output.
33
42
  3. Say that a tool picks up new agent files when its next session starts.
34
43
 
44
+ After Superwiki itself is updated, run `sync` once: the roles' instructions are part of the agent files.
45
+
35
46
  Do not edit the generated agent files or `config.json` by hand; the next `sync` overwrites agent files.
36
47
 
37
48
  ## Areas
@@ -0,0 +1,41 @@
1
+ # Reviewer
2
+
3
+ You review the implementation of one task in a Superwiki vault. You start from a clean context on purpose: you judge the change as it stands, not the reasoning that produced it. You change no file in the repository.
4
+
5
+ Input: a task id and the list of files the implementation changed; possibly which checks you may run that need services or data.
6
+
7
+ What you read is what this review costs, and every extra step re-sends everything you have read so far. Read little, in few steps. Reading less must not soften the review: every claim below gets an attempt to break it.
8
+
9
+ ## Start
10
+
11
+ 1. Read `docs/tasks/<ID>.md`. If `docs/plans/<ID>-plan.md` exists, read its `## Approach` only.
12
+ 2. Project rules (`AGENTS.md` and the like): if they are not already in your context, list their headings and read the sections on review, testing and the area the change touches. Where the project defines how a review is done or what its review class demands, that definition comes first; this file fills in what it leaves open.
13
+ 3. Read the change: the listed files, at the places that changed. Use the version control diff if you may run it; otherwise read the files.
14
+
15
+ ## Review
16
+
17
+ 1. **List the claims.** Write down what the change claims to be true: each requirement of the task ("Done when", scope, states, constraints) as implemented, and each invariant the code now relies on (a value is never missing, a rule is defined once, an error is not swallowed). Aim for the claims whose failure would be silent.
18
+ 2. **Try to break each claim.** For each one, look for evidence against it: an input the code mishandles, a caller that bypasses the new rule, a second definition of the same rule, a test that passes for the wrong reason. Prefer running something over reasoning: a short script or a one-off test, kept in a temporary folder outside the repository.
19
+ 3. **Check the tests.** Does a test fail if the claim is false? Remove or invert the behaviour in your head, or in a scratch copy, and see whether a test would notice.
20
+ 4. **Classify what you find.**
21
+ - `blocking`: the task's requirement is not met, or the change can produce a wrong result without anyone noticing.
22
+ - `important`: a real defect or gap that does not make the result wrong today.
23
+ - `minor`: clarity, naming, small cleanups.
24
+
25
+ A check marked `needs: ...` in the plan runs only if your input says it may.
26
+
27
+ ## How to read
28
+
29
+ - **Locate, then open.** Search for the symbol or string first; open the range the search points at.
30
+ - **Follow the change outward only as far as a claim needs.** A caller matters when a claim depends on how it calls.
31
+ - **Batch lookups.** One command that searches for three things costs a third of three commands.
32
+ - **Never read twice.**
33
+
34
+ ## Report
35
+
36
+ About 30 lines:
37
+
38
+ - `Verdict:` `pass` when nothing is blocking, otherwise `changes needed`;
39
+ - `Claims:` each claim, one line, with what you tried against it and the result;
40
+ - `Findings:` each finding with its class, the file and line, and the evidence (the input, command or reading that shows it). No finding without evidence;
41
+ - `Not checked:` what you could not verify, and what it would need.
@@ -21,6 +21,11 @@ const ROLES = {
21
21
  instructions: 'implementer.md',
22
22
  description: 'Implements one Superwiki task from its task and plan files. Use from sw-implement.',
23
23
  },
24
+ review: {
25
+ name: 'sw-reviewer',
26
+ instructions: 'reviewer.md',
27
+ description: 'Reviews the implementation of one Superwiki task from a clean context. Use from sw-implement.',
28
+ },
24
29
  };
25
30
 
26
31
  // How each tool wants an agent defined: where the file goes and what it looks like.
@@ -81,7 +86,7 @@ function loadConfig(root) {
81
86
  const path = join(root, 'docs/.sw/config.json');
82
87
  if (!existsSync(path)) throw new UsageError('no docs/.sw/config.json here; run sw-init first');
83
88
  const config = JSON.parse(readFileSync(path, 'utf8'));
84
- config.models = { plan: {}, implement: {}, ...config.models };
89
+ config.models = { ...Object.fromEntries(ROLE_NAMES.map(role => [role, {}])), ...config.models };
85
90
  config.tools ??= [];
86
91
  return { config, save: () => writeFileSync(path, JSON.stringify(config, null, 2) + '\n') };
87
92
  }
@@ -5,7 +5,9 @@ description: Use when the user wants to implement, build, execute, start or cont
5
5
 
6
6
  # sw-implement
7
7
 
8
- Runs one task. You keep the task's status true and judge the result; an implementer subagent, running the model set in sw-config, does the work from the task and plan files. You do not read the code or the plan: the report is your input.
8
+ Runs one task. You keep the task's status true and judge the result. The work is done by subagents that start from a clean context, on the models set in sw-config: an implementer, and a reviewer when the task asks for one. You do not read the code or the plan: their reports are your input.
9
+
10
+ That split is what keeps a task cheap. A long session re-sends its whole context on every step; work done in a fresh context does not carry yours, and yours stays small because the work never enters it.
9
11
 
10
12
  Run commands from the project root. `<skill-dir>` is the directory this SKILL.md is in.
11
13
 
@@ -18,39 +20,49 @@ Run commands from the project root. `<skill-dir>` is the directory this SKILL.md
18
20
  - `can start: n/a, status is done` or `cancelled`: stop and ask what the user wants.
19
21
  - `plan: ... (draft, not approved)`: stop; the plan needs the user's approval (sw-plan).
20
22
  - `plan: none`: fine for a small task (one area, three "Done when" items or fewer, nothing open in its notes, a few files). For anything larger, recommend sw-plan first and let the user choose.
23
+ - `review: required (...)`: remember it for step 7.
21
24
  3. **Mark it started** before any work: in the frontmatter of `docs/tasks/<ID>.md` set `status: in-progress` and `started:` today. Append `## [date] task | <ID> started` to `docs/log.md`, in the layout its last entries use.
22
- 4. **Checks that need the environment.** If the task has a plan, look for `needs:` in it: `grep -n 'needs:' docs/plans/<ID>-plan.md`. Each hit is a check that starts services or changes data. Ask the user which of them the implementer may run; without a yes, none.
23
- 5. **Dispatch the implementer.** Its prompt is the task id, the project root if it is not your working directory, and which `needs:` checks it may run. Do not paste the plan into the prompt; it reads the files.
24
-
25
- | Tool | How |
26
- | --- | --- |
27
- | Claude Code | agent `sw-implementer`. If it is not among your agent types, use a general-purpose agent, tell it to read `<skill-dir>/../sw-config/assets/implementer.md` first and follow it, and pass the model from `models.implement.claude` in `docs/.sw/config.json` if set |
28
- | Codex | spawn the custom agent `sw_implementer` |
29
- | Copilot CLI | `task` tool with agent `sw-implementer` |
30
- | No subagents available, or the agent is not defined | follow `implementer.md` yourself, in this session, and tell the user the configured model was not used |
31
-
25
+ 4. **Checks that need the environment.** If the task has a plan, look for `needs:` in it: `grep -n 'needs:' docs/plans/<ID>-plan.md`. Each hit is a check that starts services or changes data. Ask the user which of them may run; without a yes, none.
26
+ 5. **Dispatch the implementer** (how: "Dispatching" below). Its prompt is the task id, the project root if it is not your working directory, and which `needs:` checks it may run. Do not paste the plan into the prompt; it reads the files.
32
27
  6. **Judge the report.** Its `Requirements:` list must name every "Done when" item and every scope, state or constraint item of the task; compare it with the task file.
33
28
  - `met` needs evidence: a command or test and its result. Re-run one verification command yourself when the evidence is vague.
34
29
  - `not met`, or missing from the list: the task is not done.
35
30
  - `differs`: the implementer built something other than what the task says. That is the user's call: show it and ask. Until they accept it, the item is not met.
36
- 7. **Record the outcome.**
31
+ 7. **Review, if the task requires it.** Only when every requirement is met or accepted: dispatch the reviewer with the task id, the files the implementer changed, and which `needs:` checks it may run.
32
+ - `Verdict: pass`: go on. Pass `important` and `minor` findings to the user in your report; they do not block.
33
+ - `Verdict: changes needed`: dispatch the implementer again with the blocking findings, word for word, then the reviewer again with the files changed since. After two rounds that still end in `changes needed`, stop and put the findings to the user.
34
+ - Do not review the change yourself in place of the reviewer, and do not argue a blocking finding away. If you think a finding is wrong, say so to the user and let them decide.
35
+ 8. **Record the outcome.**
37
36
 
38
37
  | Outcome | Task file | Log entry |
39
38
  | --- | --- | --- |
40
- | Every requirement met or accepted, and `check <ID>` says `can finish: yes` | `status: done`, `finished:` today | `task \| <ID> done`, then one body line on what was verified |
39
+ | Every requirement met or accepted, review passed where required, and `check <ID>` says `can finish: yes` | `status: done`, `finished:` today | `task \| <ID> done`, then one body line on what was verified and, where it ran, the review verdict |
41
40
  | Requirements met but soft deps open | stays `in-progress` | `task \| <ID> waiting on <ids>` |
42
- | Anything not met, unverified or awaiting the user's call | stays `in-progress`; add what is left to "Notes" | `task \| <ID> blocked: <reason>` |
41
+ | Anything not met, unverified, not reviewed or awaiting the user's call | stays `in-progress`; add what is left to "Notes" | `task \| <ID> blocked: <reason>` |
43
42
 
44
- 8. **Keep the area guide, if the area has one.** `node docs/.sw/sw.mjs explain <ID>` prints `area guide:` with a path or `none`.
45
- - A guide exists: add the report's `Guide:` lines to it, one line per fact under Layout, Patterns, Verify or Gotchas. Replace a line the new fact corrects, and keep the page under 60 lines.
43
+ 9. **Keep the area guide, if the area has one.** `node docs/.sw/sw.mjs explain <ID>` prints `area guide:` with a path or `none`.
44
+ - A guide exists: add the reports' `Guide:` lines to it, one line per fact under Layout, Patterns, Verify or Gotchas. Replace a line the new fact corrects, and keep the page under 60 lines.
46
45
  - No guide: do nothing. A guide is worth starting once several tasks in an area have needed the same facts; if the user asks for one, create `docs/wiki/guide-<area, lowercase>.md` from `docs/.sw/templates/guide.md` and list it in `index.md`.
47
- 9. **File what else was learned.** If the implementer reported a decision or constraint the wiki should hold, offer to save it as a wiki page (`type: decision` or `concept`) and add it to `index.md`. If the task fixed a problem whose cause is now known, offer a `type: lesson` page (Symptom, Cause, Fix, How to notice it earlier); sw-triage finds these later. If it reported follow-up work, offer to create the tasks. These are separate offers: act on each only when the user says yes to that one.
48
- 10. **Report** to the user: outcome, each requirement with its evidence, anything that differs from the task, files changed, and which tasks this unblocked (`node docs/.sw/sw.mjs ready`). Commit only if the user asks.
46
+ 10. **File what else was learned.** If a report held a decision or constraint the wiki should keep, offer to save it as a wiki page (`type: decision` or `concept`) and add it to `index.md`. If the task fixed a problem whose cause is now known, or the review caught a defect worth remembering, offer a `type: lesson` page (Symptom, Cause, Fix, How to notice it earlier); sw-triage finds these later. If a report named follow-up work, offer to create the tasks. These are separate offers: act on each only when the user says yes to that one.
47
+ 11. **Report** to the user: outcome, each requirement with its evidence, the review verdict and findings, anything that differs from the task, files changed, and which tasks this unblocked (`node docs/.sw/sw.mjs ready`). Commit only if the user asks. End with one line: the task is recorded, so the next task is cheapest in a new session.
48
+
49
+ ## Dispatching
50
+
51
+ The same table serves both roles: `sw-implementer` with `implementer.md`, `sw-reviewer` with `reviewer.md`, model from `models.implement` or `models.review`.
52
+
53
+ | Tool | How |
54
+ | --- | --- |
55
+ | Claude Code | the agent by name. If it is not among your agent types, use a general-purpose agent, tell it to read `<skill-dir>/../sw-config/assets/<role file>` first and follow it, and pass the model from `docs/.sw/config.json` (`models.<role>.claude`) if set |
56
+ | Codex | spawn the custom agent `sw_implementer` or `sw_reviewer` |
57
+ | Copilot CLI | `task` tool with the agent name |
58
+ | No subagents available, the agent is not defined, or the project's rules forbid subagents | follow the role file yourself, in this session, and tell the user the configured model and the clean context were not used. A review done this way is weaker: say so |
49
59
 
50
60
  ## Common mistakes
51
61
 
52
62
  - Marking `done` because the implementer said so. Done means every requirement has evidence.
53
63
  - Accepting a `differs` item on the user's behalf. A sensible alternative is still not what the task asked for.
64
+ - Skipping the review on a task that requires it, or doing it yourself in the same context that judged the implementation.
54
65
  - Starting work before the task file says `in-progress`. If the session dies, nobody knows the task was touched.
55
- - Letting the implementer edit the task file or the log. One writer for status: you.
56
- - Reading the plan or the code "to follow along". The implementer already paid for that.
66
+ - Letting a subagent edit the task file or the log. One writer for status: you.
67
+ - Reading the plan or the code "to follow along". The subagents already paid for that.
68
+ - Running the next task in the same session out of momentum.
@@ -115,6 +115,8 @@ export function buildVault(files) {
115
115
  deps: asList(d.deps).map(String), softDeps: asList(d.soft_deps).map(String),
116
116
  milestone: d.milestone || '', priority: d.priority == null || d.priority === '' ? null : Number(d.priority),
117
117
  started: d.started || '', finished: d.finished || '',
118
+ // Any value asks for a separate review before the task may be done; the value names the kind.
119
+ review: d.review ? String(d.review) : '',
118
120
  state: null, wave: 0, dependents: [], plan: null,
119
121
  });
120
122
  }
@@ -436,12 +438,13 @@ function check(ctx) {
436
438
  : `can start: n/a, status is ${t.status}${openDeps}`;
437
439
  const draft = t.plan?.data.status === 'draft' ? ' (draft, not approved)' : '';
438
440
  return {
439
- data: { id: t.id, status: t.status, canStart, canFinish, openDeps: t.openDeps, openSoftDeps, plan },
441
+ data: { id: t.id, status: t.status, canStart, canFinish, openDeps: t.openDeps, openSoftDeps, plan, review: t.review || null },
440
442
  text: [
441
443
  `${t.id} ${t.status} ${t.title}`,
442
444
  startLine,
443
445
  `can finish: ${canFinish ? 'yes' : 'no'}${openSoftDeps.length ? ` open soft deps: ${openSoftDeps.join(', ')}` : ''}`,
444
446
  `plan: ${plan ? plan + draft : 'none'}`,
447
+ `review: ${t.review ? `required (${t.review})` : 'not required'}`,
445
448
  ].join('\n'),
446
449
  };
447
450
  }
@@ -466,6 +469,7 @@ function explain(ctx) {
466
469
  t.priority != null && `priority: ${t.priority}`,
467
470
  t.started && `started: ${t.started}`,
468
471
  t.finished && `finished: ${t.finished}`,
472
+ t.review && `review: ${t.review}`,
469
473
  ].filter(Boolean);
470
474
  return {
471
475
  data: {
@@ -7,6 +7,7 @@ deps: []
7
7
  soft_deps: []
8
8
  milestone:
9
9
  priority:
10
+ review:
10
11
  started:
11
12
  finished:
12
13
  ---
@@ -29,5 +30,8 @@ What exists when this is done, and why it matters.
29
30
  id: from `node docs/.sw/sw.mjs next-id <AREA>`; the file is docs/tasks/<id>.md.
30
31
  status: todo | in-progress | done | cancelled. Cancelled tasks stay; ids are never reused.
31
32
  deps: must be done before this starts. soft_deps: may start, cannot finish before them.
32
- Steps go in docs/plans/<id>-plan.md, not here. Delete this comment.
33
+ review: leave empty for no separate review. Any value (for example `required`, or the name of
34
+ the project's review class) makes sw-implement run a reviewer before the task can be done.
35
+ Keep this file short: steps go in docs/plans/<id>-plan.md, what happened goes in docs/log.md.
36
+ Delete this comment.
33
37
  -->
@@ -494,6 +494,8 @@ function buildVault(files) {
494
494
  deps: asList(d.deps).map(String), softDeps: asList(d.soft_deps).map(String),
495
495
  milestone: d.milestone || '', priority: d.priority == null || d.priority === '' ? null : Number(d.priority),
496
496
  started: d.started || '', finished: d.finished || '',
497
+ // Any value asks for a separate review before the task may be done; the value names the kind.
498
+ review: d.review ? String(d.review) : '',
497
499
  state: null, wave: 0, dependents: [], plan: null,
498
500
  });
499
501
  }
@@ -1,118 +1,197 @@
1
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';
2
+ // Scaffolds docs/ as a Superwiki vault. Safe to re-run: user content is kept, tool files are
3
+ // replaced with this version, and the report says which was which.
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
4
5
  import { basename, dirname, join, resolve } from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
6
7
 
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; };
8
+ const ASSETS = join(dirname(fileURLToPath(import.meta.url)), '..', 'assets');
9
+ const AREA_ID = /^[A-Za-z][A-Za-z0-9]*$/;
10
+ const MANAGED_BLOCK = /<!-- sw:start[\s\S]*?<!-- sw:end -->/;
11
+ // What a vault consists of at the top of docs/. Anything else there belongs to someone else.
12
+ const VAULT_ENTRIES = ['index.md', 'log.md', 'raw', 'wiki', 'tasks', 'plans', 'viewer.html'];
13
+ const ROLES = ['plan', 'implement', 'review'];
10
14
 
11
- if (argv.includes('--help')) {
12
- console.log(`init.mjs [--root <dir>] [--tasks | --no-tasks] [--areas "M=Mobile,B=Backend"]
15
+ const HELP = `init.mjs [--root <dir>] [--tasks | --no-tasks] [--areas "M=Mobile,B=Backend"]
13
16
 
14
17
  --root project folder (default: current directory)
15
18
  --tasks add the task module (docs/tasks, docs/plans)
16
19
  --no-tasks wiki only
17
- --areas task id prefixes and their names (default: T=Tasks)`);
18
- process.exit(0);
20
+ --areas task id prefixes and their names (default: T=Tasks)
21
+
22
+ Run without --tasks or --no-tasks on an existing vault to upgrade it with its saved choices.`;
23
+
24
+ class UsageError extends Error {}
25
+
26
+ function parseArgs(argv) {
27
+ const options = { root: '.', tasks: null, areas: {}, help: false };
28
+ for (let i = 0; i < argv.length; i++) {
29
+ const arg = argv[i];
30
+ if (arg === '--help') options.help = true;
31
+ else if (arg === '--root') options.root = argv[++i] ?? '.';
32
+ else if (arg === '--tasks') options.tasks = true;
33
+ else if (arg === '--no-tasks') options.tasks = false;
34
+ else if (arg === '--areas') options.areas = parseAreas(argv[++i] ?? '');
35
+ else throw new UsageError(`unknown argument: ${arg}\n\n${HELP}`);
36
+ }
37
+ return options;
38
+ }
39
+
40
+ function parseAreas(text) {
41
+ const areas = {};
42
+ for (const pair of text.split(',').map(part => part.trim()).filter(Boolean)) {
43
+ const [id, ...name] = pair.split('=');
44
+ if (!AREA_ID.test(id)) throw new UsageError(`bad area id "${id}": letters and digits, starting with a letter`);
45
+ areas[id] = name.join('=').trim() || id;
46
+ }
47
+ return areas;
19
48
  }
20
49
 
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);
50
+ // Collects one report line per file or folder touched. The state of a tool-owned file is
51
+ // judged by its content, so an upgrade that changes nothing says "unchanged".
52
+ function createReport(root) {
53
+ const lines = [];
54
+ const rel = path => path.slice(root.length + 1);
55
+ const add = (state, text) => lines.push(`${state.padEnd(9)} ${text}`);
56
+ return {
57
+ lines,
58
+ folder(path) {
59
+ if (existsSync(path)) return;
60
+ mkdirSync(path, { recursive: true });
61
+ add('created', `${rel(path)}/`);
62
+ },
63
+ // User content: written once, never replaced.
64
+ keep(path, content) {
65
+ if (existsSync(path)) return add('kept', rel(path));
66
+ writeFileSync(path, content);
67
+ add('created', rel(path));
68
+ },
69
+ // Tool-owned content: always brought up to date.
70
+ write(path, content, label = rel(path)) {
71
+ const before = existsSync(path) ? readFileSync(path, 'utf8') : null;
72
+ if (before !== content) writeFileSync(path, content);
73
+ add(before === null ? 'created' : before === content ? 'unchanged' : 'updated', label);
74
+ },
75
+ copy(from, to) {
76
+ if (existsSync(from)) this.write(to, readFileSync(from, 'utf8'));
77
+ else add('missing', `${rel(to)} (not in this Superwiki build; report this to the user)`);
78
+ },
79
+ line: add,
80
+ };
30
81
  }
31
82
 
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;
83
+ function readConfig(path) {
84
+ return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : null;
37
85
  }
38
86
 
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'), '');
87
+ function buildConfig(previous, { root, tasks, areas }) {
88
+ const models = Object.fromEntries(ROLES.map(role => [role, previous?.models?.[role] ?? {}]));
89
+ const chosenAreas = Object.keys(areas).length ? areas : previous?.areas ?? { T: 'Tasks' };
90
+ return {
91
+ version: 1,
92
+ name: previous?.name ?? basename(root),
93
+ tasks,
94
+ areas: tasks ? chosenAreas : {},
95
+ models,
96
+ ...(previous?.tools ? { tools: previous.tools } : {}),
97
+ };
73
98
  }
74
99
 
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', 'guide.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)');
100
+ // The schema block keeps {{#tasks}}..{{/tasks}} parts with the task module and
101
+ // {{^tasks}}..{{/tasks}} parts without it.
102
+ function schemaBlock(tasks) {
103
+ return readFileSync(join(ASSETS, 'agents-block.md'), 'utf8')
104
+ .replace(/\{\{#tasks\}\}\n([\s\S]*?)\{\{\/tasks\}\}\n/g, (_, body) => (tasks ? body : ''))
105
+ .replace(/\{\{\^tasks\}\}\n([\s\S]*?)\{\{\/tasks\}\}\n/g, (_, body) => (tasks ? '' : body))
106
+ .trimEnd();
105
107
  }
106
108
 
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.`);
109
+ function writeVault(docs, tasks, report) {
110
+ const folders = ['raw/assets', 'wiki', ...(tasks ? ['tasks', 'plans'] : [])];
111
+ report.folder(docs);
112
+ for (const folder of ['raw', ...folders, '.sw', '.sw/templates']) report.folder(join(docs, folder));
113
+ // Git drops empty folders; the vault needs them to exist.
114
+ for (const folder of folders) {
115
+ if (!readdirSync(join(docs, folder)).length) writeFileSync(join(docs, folder, '.gitkeep'), '');
116
+ }
117
+
118
+ const today = new Date().toISOString().slice(0, 10);
119
+ report.keep(join(docs, 'index.md'), '# Index\n\nCatalog of the wiki: one line per page, `- [[file-name]]: summary`, grouped by type.\n');
120
+ report.keep(join(docs, 'log.md'), `# Log\n\nAppend-only. Entry format: \`## [YYYY-MM-DD] kind | title\`.\n\n## [${today}] init | Superwiki vault created\n`);
121
+
122
+ // The viewer snapshot and the local server's address are per-machine and regenerated on demand.
123
+ report.write(join(docs, '.sw', '.gitignore'), 'data.js\nserver.json\n');
124
+ report.copy(join(ASSETS, 'sw.mjs'), join(docs, '.sw', 'sw.mjs'));
125
+ report.copy(join(ASSETS, 'viewer.html'), join(docs, 'viewer.html'));
126
+ for (const template of ['page.md', ...(tasks ? ['task.md', 'plan.md', 'guide.md'] : [])]) {
127
+ report.copy(join(ASSETS, 'templates', template), join(docs, '.sw', 'templates', template));
128
+ }
129
+ }
130
+
131
+ function writeAgentRules(root, tasks, report) {
132
+ const block = schemaBlock(tasks);
133
+ const agentsPath = join(root, 'AGENTS.md');
134
+ if (existsSync(agentsPath)) {
135
+ const text = readFileSync(agentsPath, 'utf8');
136
+ const updated = MANAGED_BLOCK.test(text) ? text.replace(MANAGED_BLOCK, () => block) : `${text.trimEnd()}\n\n${block}\n`;
137
+ report.write(agentsPath, updated, 'AGENTS.md (Superwiki block)');
138
+ } else {
139
+ report.write(agentsPath, `# Agent instructions\n\n${block}\n`);
140
+ }
141
+
142
+ // Claude Code reads CLAUDE.md, not AGENTS.md; an import keeps one source of truth.
143
+ const claudePath = join(root, 'CLAUDE.md');
144
+ if (!existsSync(claudePath)) {
145
+ writeFileSync(claudePath, '@AGENTS.md\n');
146
+ report.line('created', 'CLAUDE.md (imports AGENTS.md)');
147
+ } else if (/AGENTS\.md/.test(readFileSync(claudePath, 'utf8'))) {
148
+ report.line('kept', 'CLAUDE.md');
149
+ } else {
150
+ report.line('note', 'CLAUDE.md does not mention AGENTS.md; add a line "@AGENTS.md" so Claude Code reads the Superwiki rules');
151
+ }
152
+ }
153
+
154
+ // What was in docs/ before the first init and is not part of a vault.
155
+ function foreignEntries(docs) {
156
+ if (!existsSync(docs)) return [];
157
+ return readdirSync(docs).filter(name => !name.startsWith('.') && !VAULT_ENTRIES.includes(name));
158
+ }
159
+
160
+ function main(argv) {
161
+ const options = parseArgs(argv);
162
+ if (options.help) {
163
+ console.log(HELP);
164
+ return;
165
+ }
166
+ const root = resolve(options.root);
167
+ const docs = join(root, 'docs');
168
+ const configPath = join(docs, '.sw', 'config.json');
169
+ const previous = readConfig(configPath);
170
+ const tasks = options.tasks ?? previous?.tasks ?? null;
171
+ if (tasks === null) throw new UsageError('say --tasks or --no-tasks');
172
+
173
+ const foreign = previous ? [] : foreignEntries(docs);
174
+ const hadTaskFiles = !previous && existsSync(join(docs, 'tasks'));
175
+ const report = createReport(root);
176
+ writeVault(docs, tasks, report);
177
+ const config = buildConfig(previous, { root, tasks, areas: options.areas });
178
+ report.write(configPath, JSON.stringify(config, null, 2) + '\n');
179
+ writeAgentRules(root, tasks, report);
180
+
181
+ console.log(report.lines.join('\n'));
182
+ console.log(`\nvault: docs/ tasks: ${tasks ? `on (areas: ${Object.keys(config.areas).join(', ')})` : 'off'}`);
183
+ if (foreign.length) {
184
+ const shown = `${foreign.slice(0, 8).join(', ')}${foreign.length > 8 ? ', ...' : ''}`;
185
+ // After sw-migrate the task files are already there; only a first init on old docs needs the hint.
186
+ const hint = hadTaskFiles ? '' : ' If it holds a task index, sw-migrate converts it.';
187
+ console.log(`\ndocs/ already had content (${shown}). It was left untouched and is outside the vault.${hint}`);
188
+ }
189
+ }
190
+
191
+ try {
192
+ main(process.argv.slice(2));
193
+ } catch (error) {
194
+ if (!(error instanceof UsageError)) throw error;
195
+ console.error(error.message);
196
+ process.exitCode = 2;
197
+ }