@codyswann/lisa 2.155.7 → 2.157.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 (73) hide show
  1. package/package.json +1 -1
  2. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  3. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  4. package/plugins/lisa/rules/eager/wiki-knowledge-source.md +5 -3
  5. package/plugins/lisa/rules/reference/config-resolution.md +32 -0
  6. package/plugins/lisa/rules/reference/wiki-knowledge-source.md +10 -3
  7. package/plugins/lisa-agy/plugin.json +1 -1
  8. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  9. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  10. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  11. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-copilot/rules/eager/wiki-knowledge-source.md +5 -3
  15. package/plugins/lisa-copilot/rules/reference/config-resolution.md +32 -0
  16. package/plugins/lisa-copilot/rules/reference/wiki-knowledge-source.md +10 -3
  17. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +32 -0
  19. package/plugins/lisa-cursor/rules/wiki-knowledge-source-reference.mdc +10 -3
  20. package/plugins/lisa-cursor/rules/wiki-knowledge-source.mdc +5 -3
  21. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-expo-agy/plugin.json +1 -1
  24. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  34. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  39. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-rails-agy/plugin.json +1 -1
  44. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  49. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-wiki/scripts/ensure-wiki.mjs +267 -0
  54. package/plugins/lisa-wiki/skills/lisa-wiki-ingest/SKILL.md +14 -1
  55. package/plugins/lisa-wiki/skills/lisa-wiki-query/SKILL.md +7 -1
  56. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  57. package/plugins/lisa-wiki-agy/scripts/ensure-wiki.mjs +267 -0
  58. package/plugins/lisa-wiki-agy/skills/lisa-wiki-ingest/SKILL.md +14 -1
  59. package/plugins/lisa-wiki-agy/skills/lisa-wiki-query/SKILL.md +7 -1
  60. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki-copilot/scripts/ensure-wiki.mjs +267 -0
  62. package/plugins/lisa-wiki-copilot/skills/lisa-wiki-ingest/SKILL.md +14 -1
  63. package/plugins/lisa-wiki-copilot/skills/lisa-wiki-query/SKILL.md +7 -1
  64. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki-cursor/scripts/ensure-wiki.mjs +267 -0
  66. package/plugins/lisa-wiki-cursor/skills/lisa-wiki-ingest/SKILL.md +14 -1
  67. package/plugins/lisa-wiki-cursor/skills/lisa-wiki-query/SKILL.md +7 -1
  68. package/plugins/src/base/rules/eager/wiki-knowledge-source.md +5 -3
  69. package/plugins/src/base/rules/reference/config-resolution.md +32 -0
  70. package/plugins/src/base/rules/reference/wiki-knowledge-source.md +10 -3
  71. package/plugins/src/wiki/scripts/ensure-wiki.mjs +267 -0
  72. package/plugins/src/wiki/skills/lisa-wiki-ingest/SKILL.md +14 -1
  73. package/plugins/src/wiki/skills/lisa-wiki-query/SKILL.md +7 -1
@@ -8,7 +8,13 @@ description: Answer a question from the LLM Wiki with citations. Reads the index
8
8
  Answer from the wiki, with citations, without changing it (by default).
9
9
 
10
10
  ## Workflow
11
- 1. Read `wiki/index.md` to locate candidate pages; consult `wiki/start-here.md` for orientation.
11
+ 0. **Resolve the wiki root.** Run `node scripts/ensure-wiki.mjs --json` and use the returned
12
+ `wikiRoot` as the base for every read below — never assume `wiki/`. A local wiki resolves
13
+ instantly (no-op); a wiki whose `.lisa.config.json` declares `wiki.source.url` is mirrored and
14
+ refreshed transparently first. The script is offline-tolerant (it proceeds with the existing
15
+ mirror and warns rather than blocking), so freshness is guaranteed here and the caller never has
16
+ to think about it.
17
+ 1. Read `<wikiRoot>/index.md` to locate candidate pages; consult `<wikiRoot>/start-here.md` for orientation.
12
18
  2. Drill into the relevant synthesis pages and their cited source notes.
13
19
  3. Synthesize an answer. **Every claim cites its wiki page and/or source note.** If the wiki does not
14
20
  support an answer, say so plainly rather than inventing one; suggest an `/ingest` that would fill
@@ -1,16 +1,18 @@
1
1
  # Wiki as Knowledge Source (load-bearing)
2
2
 
3
- If the project has an LLM Wiki (a `wiki/` directory with `index.md`), treat it as the canonical source of durable project knowledge.
3
+ If the project has an LLM Wiki, treat it as the canonical source of durable project knowledge. A project has a wiki when **either** a local `wiki/` directory with `index.md` exists **or** `.lisa.config.json` declares a `wiki.source` pointer to a remote wiki repo. Documentation rolls UP into that wiki; individual repos are not expected to carry their own prose docs beyond inline code comments.
4
+
5
+ You never have to fetch or freshness-check the wiki yourself: the query and ingest skills resolve the wiki root and guarantee it exists and is current (via `scripts/ensure-wiki.mjs`) as their own first step — a local wiki resolves instantly, a remote wiki is mirrored/refreshed transparently into a gitignored working copy. Just call the skill.
4
6
 
5
7
  Before researching background, conventions, ownership, architecture, glossary, or "how/why does X work here":
6
8
 
7
- 1. **Consult the wiki first.** Start from `wiki/index.md` or use the wiki query skill (`/lisa-wiki-query`).
9
+ 1. **Consult the wiki first.** Use the wiki query skill (`/lisa-wiki-query`), which resolves the wiki root for you; for a local wiki you may also start from `wiki/index.md` directly.
8
10
  2. **Use what the wiki says** as the authoritative answer when it covers the question — do not re-derive it from raw sources.
9
11
  3. **Fall back to primary sources** (code, tickets, commit history, external docs) only when the wiki is silent, ambiguous, or contradicted by what you observe.
10
12
  4. **Surface gaps.** If the wiki is wrong, stale, or missing knowledge that belongs there, flag it — and where the workflow supports it, capture the correction via `/lisa-wiki-ingest`.
11
13
 
12
14
  The wiki documents knowledge; it does NOT override executable behavior. When wiki and running code disagree about what the system does, trust the code and treat the wiki as out of date.
13
15
 
14
- If the project has no `wiki/`, this rule does not apply.
16
+ If the project has neither a local `wiki/` nor a `wiki.source` pointer, this rule does not apply.
15
17
 
16
18
  Full prose: [reference/wiki-knowledge-source.md](../reference/wiki-knowledge-source.md).
@@ -167,6 +167,38 @@ fi
167
167
  | `tracker` | **yes** | — | Destination for ticket writes. One of `"jira"`, `"github"`, `"linear"`. Missing → fail with instruction to run the matching `/lisa:setup:*` skill. |
168
168
  | `source` | no | — | Default PRD source for batch skills (`/lisa:intake`) and arg-less single-PRD skills. One of `"notion"`, `"confluence"`, `"linear"`, `"github"`, `"jira"`. Explicit URLs/keys passed to a skill always win over `source`; this is a default, not a lock. |
169
169
  | `usage` | no | — | Optional token/cost pricing metadata consumed by the `usage-accounting` rule. Missing pricing never blocks a lifecycle flow; Lisa records token counts with `estimated_cost: null` when no trustworthy price source is configured. |
170
+ | `wiki` | no | — | Wiki location for the `wiki-knowledge-source` rule. Omit for a local in-repo wiki (`wiki/`). See **Wiki source** below. |
171
+
172
+ ### Wiki source (`wiki`)
173
+
174
+ Declares **where this repo's LLM Wiki lives** so the query/ingest skills can resolve and (for a remote wiki) mirror it. `wiki.source` has two shapes — **local** (`path`) and **remote** (`url`) — and the block belongs in the **consumer** repo's `.lisa.config.json`, not in `wiki/lisa-wiki.config.json` (which describes a wiki from the inside and is unavailable until a remote wiki is mirrored — chicken-and-egg). The whole `wiki` block is optional; omit it and the resolver falls back to the in-repo `wiki/` convention.
175
+
176
+ ```json
177
+ // local: an explicit path (optional — equivalent to the default convention)
178
+ "wiki": { "source": { "path": "wiki" } }
179
+
180
+ // remote: mirror a separate wiki repo
181
+ "wiki": {
182
+ "source": {
183
+ "url": "git@github.com:org/wiki.git",
184
+ "ref": "main",
185
+ "mirrorPath": ".lisa/wiki",
186
+ "subdir": "wiki"
187
+ },
188
+ "ttlSeconds": 300
189
+ }
190
+ ```
191
+
192
+ | Field | Required | Default | Notes |
193
+ |-------|----------|---------|-------|
194
+ | `wiki.source.path` | no | `wiki` (via convention) | **Local** wiki root, relative to the repo. The explicit form of the in-repo default. Mutually exclusive with `url`. |
195
+ | `wiki.source.url` | no | — | Clone URL of a separate wiki repo. **Its presence selects REMOTE mode.** Mutually exclusive with `path`. |
196
+ | `wiki.source.ref` | no | remote HEAD | Branch/ref to mirror (remote only). |
197
+ | `wiki.source.mirrorPath` | no | `.lisa/wiki` | Where the gitignored mirror is materialized (remote only). `ensure-wiki` keeps this path gitignored automatically. |
198
+ | `wiki.source.subdir` | no | auto | Wiki root within the cloned repo (remote only). Auto-detected as `wiki/` if present, else the repo root. |
199
+ | `wiki.ttlSeconds` | no | `300` | Skip the refresh fetch if the mirror was synced more recently than this (remote only). |
200
+
201
+ `scripts/ensure-wiki.mjs` is the single resolver (`node scripts/ensure-wiki.mjs --json` → `{mode, wikiRoot, …}`). **LOCAL** mode (no `url`) is a no-op that resolves the wiki root in precedence order `wiki.source.path` → `wikiRoot` in `wiki/lisa-wiki.config.json` → `wiki`; **REMOTE** mode (`url` set) clones-if-missing, fast-forwards when stale, and is offline-tolerant (proceeds with the existing mirror and warns rather than blocking). Callers (`lisa-wiki-query`, `lisa-wiki-ingest`) invoke it as step 0 and never hardcode `wiki/`; the freshness guarantee is the tool's, not the caller's.
170
202
 
171
203
  ### Vendor sections
172
204
 
@@ -1,14 +1,21 @@
1
1
  # Wiki as Knowledge Source
2
2
 
3
- If this project has an LLM Wiki (a `wiki/` directory with an `index.md`), treat it as the canonical source of durable project knowledge. The wiki is curated and current; ad-hoc scraping of code, tickets, chat history, or stale READMEs is not.
3
+ If this project has an LLM Wiki, treat it as the canonical source of durable project knowledge. The wiki is curated and current; ad-hoc scraping of code, tickets, chat history, or stale READMEs is not.
4
+
5
+ A project has a wiki in one of two shapes:
6
+
7
+ - **Local** — a `wiki/` directory with an `index.md` lives in this repo (`wikiRoot` in `wiki/lisa-wiki.config.json`, default `wiki`).
8
+ - **Remote** — `.lisa.config.json` declares a `wiki.source` pointer (`url`, optional `ref` / `mirrorPath` / `subdir`) at the canonical wiki repo. The wiki is **not** committed into this repo; instead the query/ingest skills maintain a gitignored mirror of it. This is the model for an organization whose documentation rolls up into one shared wiki that every repo reads: each repo carries only inline code comments locally, and the full cross-repo knowledge is the mirrored wiki.
9
+
10
+ Either way, freshness is not your concern. The query and ingest skills run `scripts/ensure-wiki.mjs` as their own first step, which resolves the wiki root and — for a remote wiki — clones the mirror if missing and fast-forwards it when stale (subject to a short TTL, and tolerant of being offline: it proceeds with the existing mirror and warns rather than blocking). The freshness guarantee lives in the tool, not in the caller's discipline. Do **not** add a separate "make sure the wiki is current" step to your own workflow — calling the skill already does it.
4
11
 
5
12
  Before researching project background, conventions, ownership, architecture, glossary terms, or "how/why does X work here":
6
13
 
7
- 1. Consult the wiki first. Start from `wiki/index.md` and follow links, or use the wiki query skill (`/lisa-wiki-query`, or the runtime's wiki query skill) to find the relevant page.
14
+ 1. Consult the wiki first via the wiki query skill (`/lisa-wiki-query`, or the runtime's wiki query skill), which resolves the wiki root for you. For a local wiki you may also start from `wiki/index.md` and follow links.
8
15
  2. Use what the wiki says as the authoritative answer when it covers the question. Do not re-derive it from raw sources when the wiki already documents it.
9
16
  3. Fall back to primary sources (code, tickets, commit history, external docs) only when the wiki is silent, ambiguous, or contradicted by what you observe in the code.
10
17
  4. If you find the wiki is wrong, stale, or missing knowledge that belongs there, surface the gap — and where the project's workflow supports it, capture the correction back into the wiki via its ingestion path (`/lisa-wiki-ingest` or equivalent) rather than leaving the knowledge only in this session.
11
18
 
12
19
  The wiki documents knowledge; it does not override executable behavior. When the wiki and the running code disagree about what the system actually does, trust the code and treat the wiki as out of date. See the `documentation-source-paths` rule for how source-material directories relate to the wiki.
13
20
 
14
- If the project has no `wiki/`, this rule does not apply.
21
+ If the project has neither a local `wiki/` nor a `wiki.source` pointer in `.lisa.config.json`, this rule does not apply.
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ensure-wiki.mjs — resolve the project's wiki root, mirroring/refreshing a
4
+ * remote wiki when one is configured. Dependency-free (Node built-ins only),
5
+ * so it stays portable to any downstream repo that installs the plugin.
6
+ *
7
+ * This is the single resolver that `lisa-wiki-query` and `lisa-wiki-ingest`
8
+ * call as step 0 so they never hardcode `wiki/` and never have to know whether
9
+ * the wiki is local or remote. The mode decision lives HERE, not in the skills:
10
+ *
11
+ * - LOCAL — the wiki lives on the local filesystem (the common case). Resolve
12
+ * the wiki root, in precedence order, from `wiki.source.path`, else
13
+ * `wikiRoot` in `wiki/lisa-wiki.config.json`, else `wiki`. No
14
+ * network, a no-op. `wiki.source.path` is just the explicit form of
15
+ * what the convention resolves implicitly.
16
+ * - REMOTE — `.lisa.config.json` declares `wiki.source.url`. Maintain a
17
+ * gitignored mirror of that repo and return the wiki root inside
18
+ * it. Clone-if-missing, fetch+fast-forward when stale (TTL), and
19
+ * tolerate being offline (proceed with the existing mirror + warn).
20
+ *
21
+ * `url` (remote) and `path` (local) are the two shapes of `wiki.source`; `url`
22
+ * takes precedence if both are somehow present.
23
+ *
24
+ * Config (consumer repo `.lisa.config.json`, with `.lisa.config.local.json`
25
+ * overriding per the config-resolution rule):
26
+ *
27
+ * "wiki": {
28
+ * "source": {
29
+ * // LOCAL shape — optional; defaults to the in-repo `wiki/` convention:
30
+ * "path": "wiki", // local wiki root, relative to repo root
31
+ * // REMOTE shape (instead of path):
32
+ * "url": "git@github.com:org/wiki.git", // present => REMOTE mode
33
+ * "ref": "main", // default: remote HEAD / "main"
34
+ * "mirrorPath": ".lisa/wiki", // default; always gitignored
35
+ * "subdir": "wiki" // optional: wiki root within the repo
36
+ * },
37
+ * "ttlSeconds": 300 // skip the fetch if synced more recently
38
+ * }
39
+ *
40
+ * Usage: node ensure-wiki.mjs [--cwd <dir>] [--json] [--ttl <seconds>] [--offline]
41
+ * --cwd project dir to resolve config/mirror against (default cwd)
42
+ * --json emit {mode, wikiRoot, mirrored, fetched, stale, offline} on stdout
43
+ * --ttl override ttlSeconds (0 = always fetch)
44
+ * --offline never touch the network; use whatever is already on disk
45
+ *
46
+ * Output: the resolved absolute wiki root is the LAST line on stdout (so a
47
+ * caller can `WIKI_ROOT=$(node ensure-wiki.mjs | tail -1)`). All human-facing
48
+ * progress goes to stderr. Exit 0 = a usable wiki root was resolved; 1 = not.
49
+ */
50
+ import fs from "node:fs";
51
+ import path from "node:path";
52
+ import { execFileSync } from "node:child_process";
53
+
54
+ const GITIGNORE_BEGIN = "# BEGIN: AI GUARDRAILS WIKI MIRROR";
55
+ const GITIGNORE_END = "# END: AI GUARDRAILS WIKI MIRROR";
56
+ const DEFAULT_MIRROR = ".lisa/wiki";
57
+ const DEFAULT_TTL_SECONDS = 300;
58
+
59
+ function log(msg) {
60
+ process.stderr.write(`${msg}\n`);
61
+ }
62
+ function fail(msg) {
63
+ log(`✗ ${msg}`);
64
+ process.exit(1);
65
+ }
66
+ function readJsonSafe(file) {
67
+ try {
68
+ return JSON.parse(fs.readFileSync(file, "utf8"));
69
+ } catch {
70
+ return undefined;
71
+ }
72
+ }
73
+ function git(args, cwd) {
74
+ return execFileSync("git", args, {
75
+ cwd,
76
+ stdio: ["ignore", "pipe", "pipe"],
77
+ encoding: "utf8",
78
+ }).trim();
79
+ }
80
+
81
+ // ── args ────────────────────────────────────────────────────────────────────
82
+ const argv = process.argv.slice(2);
83
+ function flagValue(name) {
84
+ const i = argv.indexOf(name);
85
+ return i !== -1 && argv[i + 1] ? argv[i + 1] : undefined;
86
+ }
87
+ const projectDir = path.resolve(flagValue("--cwd") ?? process.cwd());
88
+ const asJson = argv.includes("--json");
89
+ const offline = argv.includes("--offline");
90
+ const ttlOverride = flagValue("--ttl");
91
+
92
+ // ── resolve the wiki source from .lisa.config.json (+ .local override) ────────
93
+ const committed =
94
+ readJsonSafe(path.join(projectDir, ".lisa.config.json")) ?? {};
95
+ const local =
96
+ readJsonSafe(path.join(projectDir, ".lisa.config.local.json")) ?? {};
97
+ const wikiCfg = { ...(committed.wiki ?? {}), ...(local.wiki ?? {}) };
98
+ const source = {
99
+ ...(committed.wiki?.source ?? {}),
100
+ ...(local.wiki?.source ?? {}),
101
+ };
102
+ const ttlSeconds = Number(
103
+ ttlOverride ?? wikiCfg.ttlSeconds ?? DEFAULT_TTL_SECONDS
104
+ );
105
+
106
+ if (source.url !== undefined && typeof source.url !== "string") {
107
+ fail("`wiki.source.url` in .lisa.config.json must be a string");
108
+ }
109
+ if (source.path !== undefined && typeof source.path !== "string") {
110
+ fail("`wiki.source.path` in .lisa.config.json must be a string");
111
+ }
112
+
113
+ function emit(result) {
114
+ if (asJson) process.stdout.write(`${JSON.stringify(result)}\n`);
115
+ else process.stdout.write(`${result.wikiRoot}\n`);
116
+ process.exit(0);
117
+ }
118
+
119
+ // ── LOCAL mode (no remote clone) ───────────────────────────────────────────────
120
+ // The wiki lives on the local filesystem. Its root is, in precedence order:
121
+ // 1. `wiki.source.path` — explicit override in .lisa.config.json
122
+ // 2. `wikiRoot` from wiki/lisa-wiki.config.json — the in-repo convention
123
+ // 3. `wiki` — the default
124
+ // No network, a no-op resolve. `wiki.source.path` is just the explicit form of
125
+ // the same thing the convention resolves implicitly.
126
+ if (!source.url) {
127
+ const wikiRoot = path.resolve(
128
+ projectDir,
129
+ source.path ??
130
+ readJsonSafe(path.join(projectDir, "wiki", "lisa-wiki.config.json"))
131
+ ?.wikiRoot ??
132
+ "wiki"
133
+ );
134
+ if (!fs.existsSync(path.join(wikiRoot, "index.md"))) {
135
+ log(
136
+ `⚠ no index.md under ${wikiRoot} — check wiki.source.path, or run /lisa-wiki:setup if the wiki is not scaffolded yet`
137
+ );
138
+ }
139
+ log(`✓ local wiki: ${wikiRoot}`);
140
+ emit({
141
+ mode: "local",
142
+ wikiRoot,
143
+ mirrored: false,
144
+ fetched: false,
145
+ stale: false,
146
+ offline,
147
+ });
148
+ }
149
+
150
+ // ── REMOTE mode ───────────────────────────────────────────────────────────────
151
+ const mirrorPath = path.resolve(
152
+ projectDir,
153
+ source.mirrorPath ?? DEFAULT_MIRROR
154
+ );
155
+ const ref = source.ref || "";
156
+ ensureGitignored(projectDir, source.mirrorPath ?? DEFAULT_MIRROR);
157
+
158
+ const isClone = fs.existsSync(path.join(mirrorPath, ".git"));
159
+ let fetched = false;
160
+ let stale = false;
161
+
162
+ if (!isClone) {
163
+ if (offline) fail(`offline and no mirror present at ${mirrorPath}`);
164
+ log(`↧ cloning wiki ${source.url} → ${mirrorPath}`);
165
+ try {
166
+ fs.mkdirSync(path.dirname(mirrorPath), { recursive: true });
167
+ const cloneArgs = ["clone", "--depth", "1"];
168
+ if (ref) cloneArgs.push("--branch", ref);
169
+ cloneArgs.push(source.url, mirrorPath);
170
+ git(cloneArgs, projectDir);
171
+ fetched = true;
172
+ stampSync(mirrorPath);
173
+ } catch (e) {
174
+ fail(`clone failed: ${String(e.message ?? e).split("\n")[0]}`);
175
+ }
176
+ } else if (offline) {
177
+ log("• offline — using existing mirror without fetching");
178
+ stale = true;
179
+ } else if (isFresh(mirrorPath, ttlSeconds)) {
180
+ log(`• mirror synced < ${ttlSeconds}s ago — skipping fetch`);
181
+ } else {
182
+ // Stale: fetch and hard-reset. The mirror is a read-only working copy of the
183
+ // canonical wiki — never edited in place — so resetting to the remote ref is
184
+ // safe and keeps it byte-identical to upstream.
185
+ try {
186
+ const branch =
187
+ ref || git(["rev-parse", "--abbrev-ref", "HEAD"], mirrorPath);
188
+ log(`↻ refreshing wiki mirror (${branch})`);
189
+ git(["fetch", "--depth", "1", "origin", branch], mirrorPath);
190
+ git(["reset", "--hard", `origin/${branch}`], mirrorPath);
191
+ fetched = true;
192
+ stampSync(mirrorPath);
193
+ } catch (e) {
194
+ // Offline-tolerant: a fetch failure must not block work when we already
195
+ // have a usable (if stale) copy on disk.
196
+ log(
197
+ `⚠ refresh failed (${String(e.message ?? e).split("\n")[0]}) — using stale mirror`
198
+ );
199
+ stale = true;
200
+ }
201
+ }
202
+
203
+ const wikiRoot = resolveWikiRootInMirror(mirrorPath, source.subdir);
204
+ if (!fs.existsSync(path.join(wikiRoot, "index.md"))) {
205
+ log(
206
+ `⚠ no index.md under ${wikiRoot} — check wiki.source.subdir / the wiki repo layout`
207
+ );
208
+ }
209
+ log(`✓ remote wiki mirror: ${wikiRoot}`);
210
+ emit({ mode: "remote", wikiRoot, mirrored: true, fetched, stale, offline });
211
+
212
+ // ── helpers ───────────────────────────────────────────────────────────────────
213
+
214
+ /** Locate the wiki content root inside a cloned wiki repo. */
215
+ function resolveWikiRootInMirror(mirror, subdir) {
216
+ if (subdir) return path.resolve(mirror, subdir);
217
+ if (fs.existsSync(path.join(mirror, "wiki", "index.md")))
218
+ return path.join(mirror, "wiki");
219
+ return mirror; // dedicated wiki repo with content at its root
220
+ }
221
+
222
+ /** TTL stamp lives under .git so it is never part of the wiki content. */
223
+ function stampPath(mirror) {
224
+ return path.join(mirror, ".git", "lisa-wiki-sync");
225
+ }
226
+ function stampSync(mirror) {
227
+ try {
228
+ fs.writeFileSync(stampPath(mirror), String(Date.now()));
229
+ } catch {
230
+ /* best-effort */
231
+ }
232
+ }
233
+ function isFresh(mirror, ttl) {
234
+ if (ttl <= 0) return false;
235
+ let last = NaN;
236
+ try {
237
+ last = Number(fs.readFileSync(stampPath(mirror), "utf8"));
238
+ } catch {
239
+ return false;
240
+ }
241
+ if (!Number.isFinite(last)) return false;
242
+ return Date.now() - last < ttl * 1000;
243
+ }
244
+
245
+ /** Idempotently keep the mirror path out of version control. */
246
+ function ensureGitignored(dir, relPath) {
247
+ const gitignorePath = path.join(dir, ".gitignore");
248
+ const line = `/${relPath.replace(/^\/+/, "").replace(/\/+$/, "")}/`;
249
+ const block = `${GITIGNORE_BEGIN}\n# Remote wiki mirror — gitignored working copy, managed by ensure-wiki.\n${line}\n${GITIGNORE_END}`;
250
+ const existing = fs.existsSync(gitignorePath)
251
+ ? fs.readFileSync(gitignorePath, "utf8")
252
+ : null;
253
+ if (existing && existing.includes(line)) return; // already ignored (any block)
254
+ const start = existing?.indexOf(GITIGNORE_BEGIN) ?? -1;
255
+ let merged;
256
+ if (existing === null) {
257
+ merged = `${block}\n`;
258
+ } else if (start !== -1) {
259
+ const end = existing.indexOf(GITIGNORE_END, start) + GITIGNORE_END.length;
260
+ merged = `${existing.slice(0, start)}${block}${existing.slice(end)}`;
261
+ } else {
262
+ const base = existing.endsWith("\n") ? existing : `${existing}\n`;
263
+ merged = `${base}\n${block}\n`;
264
+ }
265
+ fs.writeFileSync(gitignorePath, merged);
266
+ log(`✓ ensured ${line} is gitignored`);
267
+ }
@@ -19,10 +19,23 @@ performs the shared, ordered pipeline.
19
19
  run includes explicit external-write intent**.
20
20
  - **Dry run:** `/ingest --dry-run` — list the sources a full ingest would run; perform no writes.
21
21
 
22
+ ## Step 0 — resolve the wiki root (once per run)
23
+
24
+ Before anything else, run `node scripts/ensure-wiki.mjs --json` and use the returned `wikiRoot` as the
25
+ base for the whole pipeline — never hardcode `wiki/`. This is mode-agnostic by design:
26
+
27
+ - **Local wiki** (no `wiki.source` in `.lisa.config.json`) — resolves the in-repo wiki root instantly;
28
+ the branch sync below proceeds against **this** repo's remote, exactly as before.
29
+ - **Remote wiki** (`wiki.source.url` set) — `ensure-wiki` mirrors/refreshes the gitignored working copy
30
+ of the canonical wiki repo and returns its path. In this mode the "sync the branch" step below, and
31
+ the Commit/PR step (7), operate against the **wiki repo's own remote** (the mirror), not the host
32
+ project's `origin`. The host project repo is never written to.
33
+
22
34
  ## Before ingesting — sync the branch (once per run)
23
35
 
24
36
  Run this **once per ingest invocation, before any source is processed** (skip for `--dry-run`, which
25
- writes nothing). The point is to ingest on top of fresh state, never stale state.
37
+ writes nothing). The point is to ingest on top of fresh state, never stale state. Operate in the wiki
38
+ root resolved in Step 0 — the host repo for a local wiki, the mirror for a remote one.
26
39
 
27
40
  1. **Resolve the default remote branch** — `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`,
28
41
  or `git remote show origin | sed -n 's/.*HEAD branch: //p'`, or the `origin/HEAD` symbolic ref. If
@@ -8,7 +8,13 @@ description: Answer a question from the LLM Wiki with citations. Reads the index
8
8
  Answer from the wiki, with citations, without changing it (by default).
9
9
 
10
10
  ## Workflow
11
- 1. Read `wiki/index.md` to locate candidate pages; consult `wiki/start-here.md` for orientation.
11
+ 0. **Resolve the wiki root.** Run `node scripts/ensure-wiki.mjs --json` and use the returned
12
+ `wikiRoot` as the base for every read below — never assume `wiki/`. A local wiki resolves
13
+ instantly (no-op); a wiki whose `.lisa.config.json` declares `wiki.source.url` is mirrored and
14
+ refreshed transparently first. The script is offline-tolerant (it proceeds with the existing
15
+ mirror and warns rather than blocking), so freshness is guaranteed here and the caller never has
16
+ to think about it.
17
+ 1. Read `<wikiRoot>/index.md` to locate candidate pages; consult `<wikiRoot>/start-here.md` for orientation.
12
18
  2. Drill into the relevant synthesis pages and their cited source notes.
13
19
  3. Synthesize an answer. **Every claim cites its wiki page and/or source note.** If the wiki does not
14
20
  support an answer, say so plainly rather than inventing one; suggest an `/ingest` that would fill