@codyswann/lisa 2.106.1 → 2.106.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) 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/commands/plugin-sync-explain.md +8 -0
  5. package/plugins/lisa/scripts/plugin-sync-explain.mjs +332 -0
  6. package/plugins/lisa/skills/plugin-sync-explain/SKILL.md +53 -0
  7. package/plugins/lisa/skills/plugin-sync-explain/agents/openai.yaml +4 -0
  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-expo/.claude-plugin/plugin.json +1 -1
  11. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  12. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  14. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  20. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  24. package/plugins/src/base/commands/plugin-sync-explain.md +8 -0
  25. package/plugins/src/base/scripts/plugin-sync-explain.mjs +332 -0
  26. package/plugins/src/base/skills/plugin-sync-explain/SKILL.md +53 -0
package/package.json CHANGED
@@ -82,7 +82,7 @@
82
82
  "lodash": ">=4.18.1"
83
83
  },
84
84
  "name": "@codyswann/lisa",
85
- "version": "2.106.1",
85
+ "version": "2.106.2",
86
86
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
87
87
  "main": "dist/index.js",
88
88
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -0,0 +1,8 @@
1
+ ---
2
+ description: "Explain plugin source/generated drift and marketplace registration gaps without modifying the working tree."
3
+ argument-hint: "[path]"
4
+ ---
5
+
6
+ Use the /lisa:plugin-sync-explain skill to inspect plugin source/generated synchronization for the current Lisa repo. $ARGUMENTS
7
+
8
+ This command is read-only. It reports source-not-built edits, generated-only edits, marketplace registration drift, and the next source-first remediation step before an operator runs `bun run build:plugins` or `bun run check:plugins`.
@@ -0,0 +1,332 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Read-only plugin sync drift classifier.
4
+ *
5
+ * This helper intentionally uses committed git status as evidence instead of
6
+ * rebuilding plugins. Rebuilding is the job of `bun run build:plugins`; this
7
+ * diagnostic explains likely causes before an operator mutates the tree.
8
+ */
9
+ import { execFileSync } from "node:child_process";
10
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
11
+ import path from "node:path";
12
+ import process from "node:process";
13
+
14
+ export const PLUGIN_SYNC_CLASSIFICATIONS = [
15
+ "SOURCE_NOT_BUILT",
16
+ "GENERATED_ONLY",
17
+ "MARKETPLACE_REGISTRATION_DRIFT",
18
+ "OUT_OF_SYNC",
19
+ "IN_SYNC",
20
+ ];
21
+
22
+ const PLUGINS_DIR = "plugins";
23
+ const SOURCE_ROOT = "plugins/src";
24
+ const MARKETPLACE = ".claude-plugin/marketplace.json";
25
+ const GIT_BIN = "/usr/bin/git";
26
+
27
+ /**
28
+ * @typedef {{
29
+ * readonly classification: string
30
+ * readonly path: string
31
+ * readonly counterpart?: string
32
+ * readonly nextAction: string
33
+ * }} PluginSyncFinding
34
+ *
35
+ * @typedef {{
36
+ * readonly root: string
37
+ * readonly findings: readonly PluginSyncFinding[]
38
+ * readonly statusBefore: string
39
+ * readonly statusAfter: string
40
+ * readonly readOnly: boolean
41
+ * readonly text: string
42
+ * }} PluginSyncReport
43
+ */
44
+
45
+ /**
46
+ * @param {string} root
47
+ * @returns {PluginSyncReport}
48
+ */
49
+ export function explainPluginSync(root = process.cwd()) {
50
+ const repoRoot = path.resolve(root);
51
+ const statusBefore = gitStatus(repoRoot);
52
+ const statusEntries = parseGitStatus(statusBefore);
53
+ const findings = [
54
+ ...classifySourceGeneratedDrift(statusEntries),
55
+ ...classifyMarketplaceDrift(repoRoot),
56
+ ];
57
+ const statusAfter = gitStatus(repoRoot);
58
+ const readOnly = statusAfter === statusBefore;
59
+
60
+ return {
61
+ root: repoRoot,
62
+ findings,
63
+ statusBefore,
64
+ statusAfter,
65
+ readOnly,
66
+ text: renderPluginSyncReport({
67
+ root: repoRoot,
68
+ findings,
69
+ statusBefore,
70
+ statusAfter,
71
+ readOnly,
72
+ }),
73
+ };
74
+ }
75
+
76
+ /**
77
+ * @param {PluginSyncReport} report
78
+ * @returns {string}
79
+ */
80
+ export function renderPluginSyncReport(report) {
81
+ const lines = [
82
+ "Plugin sync explain",
83
+ `Repo: ${report.root}`,
84
+ `Read-only: ${report.readOnly ? "yes" : "no"}`,
85
+ ];
86
+
87
+ if (report.findings.length === 0) {
88
+ lines.push(
89
+ "Verdict: IN_SYNC",
90
+ "No plugin source/generated or marketplace registration drift detected."
91
+ );
92
+ return `${lines.join("\n")}\n`;
93
+ }
94
+
95
+ lines.push(`Verdict: ${highestClassification(report.findings)}`, "Findings:");
96
+ for (const finding of report.findings) {
97
+ lines.push(
98
+ `- ${finding.classification}: ${finding.path}`,
99
+ ` Evidence: ${finding.counterpart ? `${finding.path} -> ${finding.counterpart}` : finding.path}`,
100
+ ` Next action: ${finding.nextAction}`
101
+ );
102
+ }
103
+
104
+ return `${lines.join("\n")}\n`;
105
+ }
106
+
107
+ /**
108
+ * @param {readonly PluginSyncFinding[]} findings
109
+ * @returns {string}
110
+ */
111
+ function highestClassification(findings) {
112
+ if (findings.some(f => f.classification === "OUT_OF_SYNC")) {
113
+ return "OUT_OF_SYNC";
114
+ }
115
+ if (
116
+ findings.some(f => f.classification === "MARKETPLACE_REGISTRATION_DRIFT")
117
+ ) {
118
+ return "MARKETPLACE_REGISTRATION_DRIFT";
119
+ }
120
+ if (findings.some(f => f.classification === "GENERATED_ONLY")) {
121
+ return "GENERATED_ONLY";
122
+ }
123
+ if (findings.some(f => f.classification === "SOURCE_NOT_BUILT")) {
124
+ return "SOURCE_NOT_BUILT";
125
+ }
126
+ return "IN_SYNC";
127
+ }
128
+
129
+ /**
130
+ * @param {readonly { readonly code: string, readonly file: string }[]} entries
131
+ * @returns {PluginSyncFinding[]}
132
+ */
133
+ function classifySourceGeneratedDrift(entries) {
134
+ const changed = new Set(entries.map(entry => entry.file));
135
+ const findings = [];
136
+
137
+ for (const entry of entries) {
138
+ const sourceCounterpart = sourceToBuilt(entry.file);
139
+ if (sourceCounterpart) {
140
+ const generatedChanged = changed.has(sourceCounterpart);
141
+ findings.push({
142
+ classification: generatedChanged ? "OUT_OF_SYNC" : "SOURCE_NOT_BUILT",
143
+ path: entry.file,
144
+ counterpart: sourceCounterpart,
145
+ nextAction: generatedChanged
146
+ ? "Review both source and generated changes, keep source authoritative, then run `bun run build:plugins && bun run check:plugins`."
147
+ : "Run `bun run build:plugins`, then `bun run check:plugins`, and commit source plus regenerated artifacts.",
148
+ });
149
+ continue;
150
+ }
151
+
152
+ const builtCounterpart = builtToSource(entry.file);
153
+ if (builtCounterpart && !changed.has(builtCounterpart)) {
154
+ findings.push({
155
+ classification: "GENERATED_ONLY",
156
+ path: entry.file,
157
+ counterpart: builtCounterpart,
158
+ nextAction:
159
+ "Move the edit to the matching plugins/src path, rebuild with `bun run build:plugins`, then run `bun run check:plugins`.",
160
+ });
161
+ }
162
+ }
163
+
164
+ return dedupeFindings(findings);
165
+ }
166
+
167
+ /**
168
+ * @param {string} root
169
+ * @returns {PluginSyncFinding[]}
170
+ */
171
+ function classifyMarketplaceDrift(root) {
172
+ const marketplacePath = path.join(root, MARKETPLACE);
173
+ if (!existsSync(marketplacePath)) {
174
+ return [];
175
+ }
176
+
177
+ const marketplace = JSON.parse(readFileSync(marketplacePath, "utf8"));
178
+ const sources = new Set(
179
+ (marketplace.plugins ?? [])
180
+ .map(plugin => plugin.source)
181
+ .filter(source => typeof source === "string")
182
+ );
183
+ const pluginsRoot = path.join(root, PLUGINS_DIR);
184
+ if (!existsSync(pluginsRoot)) {
185
+ return [];
186
+ }
187
+
188
+ const builtSources = readdirSync(pluginsRoot)
189
+ .filter(name => name !== "src")
190
+ .map(name => path.join(pluginsRoot, name))
191
+ .filter(abs => statSync(abs).isDirectory())
192
+ .map(abs => `./${path.relative(root, abs).split(path.sep).join("/")}`);
193
+
194
+ const findings = [];
195
+ for (const source of builtSources) {
196
+ if (!sources.has(source)) {
197
+ findings.push({
198
+ classification: "MARKETPLACE_REGISTRATION_DRIFT",
199
+ path: source.replace(/^\.\//, ""),
200
+ counterpart: MARKETPLACE,
201
+ nextAction:
202
+ "Add the built plugin source to `.claude-plugin/marketplace.json`, then run `bun run check:plugins`.",
203
+ });
204
+ }
205
+ }
206
+
207
+ for (const source of sources) {
208
+ if (!existsSync(path.join(root, source))) {
209
+ findings.push({
210
+ classification: "MARKETPLACE_REGISTRATION_DRIFT",
211
+ path: MARKETPLACE,
212
+ counterpart: source.replace(/^\.\//, ""),
213
+ nextAction:
214
+ "Either restore the built plugin directory or remove the stale marketplace source, then run `bun run check:plugins`.",
215
+ });
216
+ }
217
+ }
218
+
219
+ return findings;
220
+ }
221
+
222
+ /**
223
+ * @param {string} root
224
+ * @returns {string}
225
+ */
226
+ function gitStatus(root) {
227
+ return execFileSync(
228
+ GIT_BIN,
229
+ ["status", "--porcelain", "--", PLUGINS_DIR, MARKETPLACE],
230
+ {
231
+ cwd: root,
232
+ encoding: "utf8",
233
+ env: gitEnv(),
234
+ }
235
+ );
236
+ }
237
+
238
+ /**
239
+ * @param {string} status
240
+ * @returns {{ readonly code: string, readonly file: string }[]}
241
+ */
242
+ function parseGitStatus(status) {
243
+ return status
244
+ .split("\n")
245
+ .filter(Boolean)
246
+ .map(line => ({
247
+ code: line.slice(0, 2),
248
+ file: normalizeStatusPath(line.slice(3)),
249
+ }));
250
+ }
251
+
252
+ /**
253
+ * @param {string} file
254
+ * @returns {string}
255
+ */
256
+ function normalizeStatusPath(file) {
257
+ const renamedPath = file.includes(" -> ") ? file.split(" -> ").at(-1) : file;
258
+ return renamedPath.replace(/^"|"$/g, "");
259
+ }
260
+
261
+ /**
262
+ * @param {string} file
263
+ * @returns {string | undefined}
264
+ */
265
+ function sourceToBuilt(file) {
266
+ if (!file.startsWith(`${SOURCE_ROOT}/`)) {
267
+ return undefined;
268
+ }
269
+ const [, , sourceName, ...rest] = file.split("/");
270
+ if (!sourceName || rest.length === 0) {
271
+ return undefined;
272
+ }
273
+ const builtName = sourceName === "base" ? "lisa" : `lisa-${sourceName}`;
274
+ return [PLUGINS_DIR, builtName, ...rest].join("/");
275
+ }
276
+
277
+ /**
278
+ * @param {string} file
279
+ * @returns {string | undefined}
280
+ */
281
+ function builtToSource(file) {
282
+ if (!file.startsWith(`${PLUGINS_DIR}/lisa`)) {
283
+ return undefined;
284
+ }
285
+ const [, builtName, ...rest] = file.split("/");
286
+ if (!builtName || rest.length === 0) {
287
+ return undefined;
288
+ }
289
+ const sourceName =
290
+ builtName === "lisa" ? "base" : builtName.replace(/^lisa-/, "");
291
+ return [SOURCE_ROOT, sourceName, ...rest].join("/");
292
+ }
293
+
294
+ /**
295
+ * @param {PluginSyncFinding[]} findings
296
+ * @returns {PluginSyncFinding[]}
297
+ */
298
+ function dedupeFindings(findings) {
299
+ const seen = new Set();
300
+ return findings.filter(finding => {
301
+ const key = `${finding.classification}:${finding.path}:${finding.counterpart ?? ""}`;
302
+ if (seen.has(key)) {
303
+ return false;
304
+ }
305
+ seen.add(key);
306
+ return true;
307
+ });
308
+ }
309
+
310
+ /**
311
+ * Remove parent-hook Git environment so diagnostics inspect the requested repo.
312
+ * @returns {NodeJS.ProcessEnv} Process environment for nested git commands.
313
+ */
314
+ function gitEnv() {
315
+ const env = { ...process.env };
316
+ delete env.GIT_DIR;
317
+ delete env.GIT_WORK_TREE;
318
+ return env;
319
+ }
320
+
321
+ if (import.meta.url === `file://${process.argv[1]}`) {
322
+ try {
323
+ const report = explainPluginSync(process.argv[2] ?? process.cwd());
324
+ process.stdout.write(report.text);
325
+ process.exitCode = report.findings.length === 0 ? 0 : 1;
326
+ } catch (error) {
327
+ process.stderr.write(
328
+ `plugin-sync-explain failed: ${error instanceof Error ? error.message : String(error)}\n`
329
+ );
330
+ process.exitCode = 2;
331
+ }
332
+ }
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: plugin-sync-explain
3
+ description: "Read-only diagnostic for Lisa plugin source/generated drift. Compares plugins/src against generated plugins/lisa* status, reports source-not-built, generated-only, and marketplace registration drift, and preserves the working tree."
4
+ allowed-tools: ["Bash", "Read"]
5
+ ---
6
+
7
+ # Plugin Sync Explain: $ARGUMENTS
8
+
9
+ `/lisa:plugin-sync-explain` explains why the plugin sync gate would need attention without mutating the working tree.
10
+
11
+ ## Scope
12
+
13
+ Inspect the current Lisa repository, or an optional path passed as `$ARGUMENTS`, using `scripts/plugin-sync-explain.mjs`.
14
+
15
+ The diagnostic is **read-only**:
16
+
17
+ - Do not run `bun run build:plugins`.
18
+ - Do not run `bun run check:plugins`.
19
+ - Do not edit source files, generated plugin artifacts, or `.claude-plugin/marketplace.json`.
20
+ - Do not stash, commit, reset, or clean local changes.
21
+
22
+ ## What to Report
23
+
24
+ Report a concise terminal-first summary with stable classifications:
25
+
26
+ - `SOURCE_NOT_BUILT`: files under `plugins/src/**` changed without their generated `plugins/lisa*` counterpart.
27
+ - `GENERATED_ONLY`: files under `plugins/lisa*` changed without their `plugins/src/**` source counterpart.
28
+ - `MARKETPLACE_REGISTRATION_DRIFT`: a built plugin directory is missing from `.claude-plugin/marketplace.json`, or a marketplace source points at a missing built directory.
29
+ - `OUT_OF_SYNC`: source and generated counterparts both changed and need human review.
30
+ - `IN_SYNC`: no plugin source/generated or marketplace registration drift was detected.
31
+
32
+ For every finding, include the evidence path and the smallest source-first next action. Prefer `bun run build:plugins` only after source edits are in the right place, and preserve `bun run check:plugins` as the final reproducibility gate.
33
+
34
+ ## Process
35
+
36
+ 1. Resolve the repo path from `$ARGUMENTS` or the current directory.
37
+ 2. Capture `git status --porcelain` before the diagnostic.
38
+ 3. Run:
39
+ ```bash
40
+ node plugins/lisa/scripts/plugin-sync-explain.mjs "$REPO_PATH"
41
+ ```
42
+ If running from the source tree before generated artifacts are rebuilt, use:
43
+ ```bash
44
+ node plugins/src/base/scripts/plugin-sync-explain.mjs "$REPO_PATH"
45
+ ```
46
+ 4. Capture `git status --porcelain` after the diagnostic and confirm it is unchanged.
47
+ 5. Surface the script output plus the read-only confirmation.
48
+
49
+ ## Rules
50
+
51
+ - Keep this surface aligned with `scripts/check-plugins-sync.sh`; it explains the gate but does not replace it.
52
+ - Treat `plugins/src/**` as the source of truth and `plugins/lisa*` as generated artifacts.
53
+ - If the diagnostic cannot inspect git status or marketplace JSON, report the error plainly and do not guess.
@@ -0,0 +1,4 @@
1
+ display_name: "Plugin Sync Explain"
2
+ short_description: "Read-only diagnostic for Lisa plugin source/generated drift"
3
+ default_prompt:
4
+ - "Use $plugin-sync-explain: Read-only diagnostic for Lisa plugin source/generated drift."
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "Harper/Fabric-specific Lisa rules for TypeScript component apps.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "NestJS-specific skills and migration write-protection hooks.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.106.1",
3
+ "version": "2.106.2",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -0,0 +1,8 @@
1
+ ---
2
+ description: "Explain plugin source/generated drift and marketplace registration gaps without modifying the working tree."
3
+ argument-hint: "[path]"
4
+ ---
5
+
6
+ Use the /lisa:plugin-sync-explain skill to inspect plugin source/generated synchronization for the current Lisa repo. $ARGUMENTS
7
+
8
+ This command is read-only. It reports source-not-built edits, generated-only edits, marketplace registration drift, and the next source-first remediation step before an operator runs `bun run build:plugins` or `bun run check:plugins`.
@@ -0,0 +1,332 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Read-only plugin sync drift classifier.
4
+ *
5
+ * This helper intentionally uses committed git status as evidence instead of
6
+ * rebuilding plugins. Rebuilding is the job of `bun run build:plugins`; this
7
+ * diagnostic explains likely causes before an operator mutates the tree.
8
+ */
9
+ import { execFileSync } from "node:child_process";
10
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
11
+ import path from "node:path";
12
+ import process from "node:process";
13
+
14
+ export const PLUGIN_SYNC_CLASSIFICATIONS = [
15
+ "SOURCE_NOT_BUILT",
16
+ "GENERATED_ONLY",
17
+ "MARKETPLACE_REGISTRATION_DRIFT",
18
+ "OUT_OF_SYNC",
19
+ "IN_SYNC",
20
+ ];
21
+
22
+ const PLUGINS_DIR = "plugins";
23
+ const SOURCE_ROOT = "plugins/src";
24
+ const MARKETPLACE = ".claude-plugin/marketplace.json";
25
+ const GIT_BIN = "/usr/bin/git";
26
+
27
+ /**
28
+ * @typedef {{
29
+ * readonly classification: string
30
+ * readonly path: string
31
+ * readonly counterpart?: string
32
+ * readonly nextAction: string
33
+ * }} PluginSyncFinding
34
+ *
35
+ * @typedef {{
36
+ * readonly root: string
37
+ * readonly findings: readonly PluginSyncFinding[]
38
+ * readonly statusBefore: string
39
+ * readonly statusAfter: string
40
+ * readonly readOnly: boolean
41
+ * readonly text: string
42
+ * }} PluginSyncReport
43
+ */
44
+
45
+ /**
46
+ * @param {string} root
47
+ * @returns {PluginSyncReport}
48
+ */
49
+ export function explainPluginSync(root = process.cwd()) {
50
+ const repoRoot = path.resolve(root);
51
+ const statusBefore = gitStatus(repoRoot);
52
+ const statusEntries = parseGitStatus(statusBefore);
53
+ const findings = [
54
+ ...classifySourceGeneratedDrift(statusEntries),
55
+ ...classifyMarketplaceDrift(repoRoot),
56
+ ];
57
+ const statusAfter = gitStatus(repoRoot);
58
+ const readOnly = statusAfter === statusBefore;
59
+
60
+ return {
61
+ root: repoRoot,
62
+ findings,
63
+ statusBefore,
64
+ statusAfter,
65
+ readOnly,
66
+ text: renderPluginSyncReport({
67
+ root: repoRoot,
68
+ findings,
69
+ statusBefore,
70
+ statusAfter,
71
+ readOnly,
72
+ }),
73
+ };
74
+ }
75
+
76
+ /**
77
+ * @param {PluginSyncReport} report
78
+ * @returns {string}
79
+ */
80
+ export function renderPluginSyncReport(report) {
81
+ const lines = [
82
+ "Plugin sync explain",
83
+ `Repo: ${report.root}`,
84
+ `Read-only: ${report.readOnly ? "yes" : "no"}`,
85
+ ];
86
+
87
+ if (report.findings.length === 0) {
88
+ lines.push(
89
+ "Verdict: IN_SYNC",
90
+ "No plugin source/generated or marketplace registration drift detected."
91
+ );
92
+ return `${lines.join("\n")}\n`;
93
+ }
94
+
95
+ lines.push(`Verdict: ${highestClassification(report.findings)}`, "Findings:");
96
+ for (const finding of report.findings) {
97
+ lines.push(
98
+ `- ${finding.classification}: ${finding.path}`,
99
+ ` Evidence: ${finding.counterpart ? `${finding.path} -> ${finding.counterpart}` : finding.path}`,
100
+ ` Next action: ${finding.nextAction}`
101
+ );
102
+ }
103
+
104
+ return `${lines.join("\n")}\n`;
105
+ }
106
+
107
+ /**
108
+ * @param {readonly PluginSyncFinding[]} findings
109
+ * @returns {string}
110
+ */
111
+ function highestClassification(findings) {
112
+ if (findings.some(f => f.classification === "OUT_OF_SYNC")) {
113
+ return "OUT_OF_SYNC";
114
+ }
115
+ if (
116
+ findings.some(f => f.classification === "MARKETPLACE_REGISTRATION_DRIFT")
117
+ ) {
118
+ return "MARKETPLACE_REGISTRATION_DRIFT";
119
+ }
120
+ if (findings.some(f => f.classification === "GENERATED_ONLY")) {
121
+ return "GENERATED_ONLY";
122
+ }
123
+ if (findings.some(f => f.classification === "SOURCE_NOT_BUILT")) {
124
+ return "SOURCE_NOT_BUILT";
125
+ }
126
+ return "IN_SYNC";
127
+ }
128
+
129
+ /**
130
+ * @param {readonly { readonly code: string, readonly file: string }[]} entries
131
+ * @returns {PluginSyncFinding[]}
132
+ */
133
+ function classifySourceGeneratedDrift(entries) {
134
+ const changed = new Set(entries.map(entry => entry.file));
135
+ const findings = [];
136
+
137
+ for (const entry of entries) {
138
+ const sourceCounterpart = sourceToBuilt(entry.file);
139
+ if (sourceCounterpart) {
140
+ const generatedChanged = changed.has(sourceCounterpart);
141
+ findings.push({
142
+ classification: generatedChanged ? "OUT_OF_SYNC" : "SOURCE_NOT_BUILT",
143
+ path: entry.file,
144
+ counterpart: sourceCounterpart,
145
+ nextAction: generatedChanged
146
+ ? "Review both source and generated changes, keep source authoritative, then run `bun run build:plugins && bun run check:plugins`."
147
+ : "Run `bun run build:plugins`, then `bun run check:plugins`, and commit source plus regenerated artifacts.",
148
+ });
149
+ continue;
150
+ }
151
+
152
+ const builtCounterpart = builtToSource(entry.file);
153
+ if (builtCounterpart && !changed.has(builtCounterpart)) {
154
+ findings.push({
155
+ classification: "GENERATED_ONLY",
156
+ path: entry.file,
157
+ counterpart: builtCounterpart,
158
+ nextAction:
159
+ "Move the edit to the matching plugins/src path, rebuild with `bun run build:plugins`, then run `bun run check:plugins`.",
160
+ });
161
+ }
162
+ }
163
+
164
+ return dedupeFindings(findings);
165
+ }
166
+
167
+ /**
168
+ * @param {string} root
169
+ * @returns {PluginSyncFinding[]}
170
+ */
171
+ function classifyMarketplaceDrift(root) {
172
+ const marketplacePath = path.join(root, MARKETPLACE);
173
+ if (!existsSync(marketplacePath)) {
174
+ return [];
175
+ }
176
+
177
+ const marketplace = JSON.parse(readFileSync(marketplacePath, "utf8"));
178
+ const sources = new Set(
179
+ (marketplace.plugins ?? [])
180
+ .map(plugin => plugin.source)
181
+ .filter(source => typeof source === "string")
182
+ );
183
+ const pluginsRoot = path.join(root, PLUGINS_DIR);
184
+ if (!existsSync(pluginsRoot)) {
185
+ return [];
186
+ }
187
+
188
+ const builtSources = readdirSync(pluginsRoot)
189
+ .filter(name => name !== "src")
190
+ .map(name => path.join(pluginsRoot, name))
191
+ .filter(abs => statSync(abs).isDirectory())
192
+ .map(abs => `./${path.relative(root, abs).split(path.sep).join("/")}`);
193
+
194
+ const findings = [];
195
+ for (const source of builtSources) {
196
+ if (!sources.has(source)) {
197
+ findings.push({
198
+ classification: "MARKETPLACE_REGISTRATION_DRIFT",
199
+ path: source.replace(/^\.\//, ""),
200
+ counterpart: MARKETPLACE,
201
+ nextAction:
202
+ "Add the built plugin source to `.claude-plugin/marketplace.json`, then run `bun run check:plugins`.",
203
+ });
204
+ }
205
+ }
206
+
207
+ for (const source of sources) {
208
+ if (!existsSync(path.join(root, source))) {
209
+ findings.push({
210
+ classification: "MARKETPLACE_REGISTRATION_DRIFT",
211
+ path: MARKETPLACE,
212
+ counterpart: source.replace(/^\.\//, ""),
213
+ nextAction:
214
+ "Either restore the built plugin directory or remove the stale marketplace source, then run `bun run check:plugins`.",
215
+ });
216
+ }
217
+ }
218
+
219
+ return findings;
220
+ }
221
+
222
+ /**
223
+ * @param {string} root
224
+ * @returns {string}
225
+ */
226
+ function gitStatus(root) {
227
+ return execFileSync(
228
+ GIT_BIN,
229
+ ["status", "--porcelain", "--", PLUGINS_DIR, MARKETPLACE],
230
+ {
231
+ cwd: root,
232
+ encoding: "utf8",
233
+ env: gitEnv(),
234
+ }
235
+ );
236
+ }
237
+
238
+ /**
239
+ * @param {string} status
240
+ * @returns {{ readonly code: string, readonly file: string }[]}
241
+ */
242
+ function parseGitStatus(status) {
243
+ return status
244
+ .split("\n")
245
+ .filter(Boolean)
246
+ .map(line => ({
247
+ code: line.slice(0, 2),
248
+ file: normalizeStatusPath(line.slice(3)),
249
+ }));
250
+ }
251
+
252
+ /**
253
+ * @param {string} file
254
+ * @returns {string}
255
+ */
256
+ function normalizeStatusPath(file) {
257
+ const renamedPath = file.includes(" -> ") ? file.split(" -> ").at(-1) : file;
258
+ return renamedPath.replace(/^"|"$/g, "");
259
+ }
260
+
261
+ /**
262
+ * @param {string} file
263
+ * @returns {string | undefined}
264
+ */
265
+ function sourceToBuilt(file) {
266
+ if (!file.startsWith(`${SOURCE_ROOT}/`)) {
267
+ return undefined;
268
+ }
269
+ const [, , sourceName, ...rest] = file.split("/");
270
+ if (!sourceName || rest.length === 0) {
271
+ return undefined;
272
+ }
273
+ const builtName = sourceName === "base" ? "lisa" : `lisa-${sourceName}`;
274
+ return [PLUGINS_DIR, builtName, ...rest].join("/");
275
+ }
276
+
277
+ /**
278
+ * @param {string} file
279
+ * @returns {string | undefined}
280
+ */
281
+ function builtToSource(file) {
282
+ if (!file.startsWith(`${PLUGINS_DIR}/lisa`)) {
283
+ return undefined;
284
+ }
285
+ const [, builtName, ...rest] = file.split("/");
286
+ if (!builtName || rest.length === 0) {
287
+ return undefined;
288
+ }
289
+ const sourceName =
290
+ builtName === "lisa" ? "base" : builtName.replace(/^lisa-/, "");
291
+ return [SOURCE_ROOT, sourceName, ...rest].join("/");
292
+ }
293
+
294
+ /**
295
+ * @param {PluginSyncFinding[]} findings
296
+ * @returns {PluginSyncFinding[]}
297
+ */
298
+ function dedupeFindings(findings) {
299
+ const seen = new Set();
300
+ return findings.filter(finding => {
301
+ const key = `${finding.classification}:${finding.path}:${finding.counterpart ?? ""}`;
302
+ if (seen.has(key)) {
303
+ return false;
304
+ }
305
+ seen.add(key);
306
+ return true;
307
+ });
308
+ }
309
+
310
+ /**
311
+ * Remove parent-hook Git environment so diagnostics inspect the requested repo.
312
+ * @returns {NodeJS.ProcessEnv} Process environment for nested git commands.
313
+ */
314
+ function gitEnv() {
315
+ const env = { ...process.env };
316
+ delete env.GIT_DIR;
317
+ delete env.GIT_WORK_TREE;
318
+ return env;
319
+ }
320
+
321
+ if (import.meta.url === `file://${process.argv[1]}`) {
322
+ try {
323
+ const report = explainPluginSync(process.argv[2] ?? process.cwd());
324
+ process.stdout.write(report.text);
325
+ process.exitCode = report.findings.length === 0 ? 0 : 1;
326
+ } catch (error) {
327
+ process.stderr.write(
328
+ `plugin-sync-explain failed: ${error instanceof Error ? error.message : String(error)}\n`
329
+ );
330
+ process.exitCode = 2;
331
+ }
332
+ }
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: plugin-sync-explain
3
+ description: "Read-only diagnostic for Lisa plugin source/generated drift. Compares plugins/src against generated plugins/lisa* status, reports source-not-built, generated-only, and marketplace registration drift, and preserves the working tree."
4
+ allowed-tools: ["Bash", "Read"]
5
+ ---
6
+
7
+ # Plugin Sync Explain: $ARGUMENTS
8
+
9
+ `/lisa:plugin-sync-explain` explains why the plugin sync gate would need attention without mutating the working tree.
10
+
11
+ ## Scope
12
+
13
+ Inspect the current Lisa repository, or an optional path passed as `$ARGUMENTS`, using `scripts/plugin-sync-explain.mjs`.
14
+
15
+ The diagnostic is **read-only**:
16
+
17
+ - Do not run `bun run build:plugins`.
18
+ - Do not run `bun run check:plugins`.
19
+ - Do not edit source files, generated plugin artifacts, or `.claude-plugin/marketplace.json`.
20
+ - Do not stash, commit, reset, or clean local changes.
21
+
22
+ ## What to Report
23
+
24
+ Report a concise terminal-first summary with stable classifications:
25
+
26
+ - `SOURCE_NOT_BUILT`: files under `plugins/src/**` changed without their generated `plugins/lisa*` counterpart.
27
+ - `GENERATED_ONLY`: files under `plugins/lisa*` changed without their `plugins/src/**` source counterpart.
28
+ - `MARKETPLACE_REGISTRATION_DRIFT`: a built plugin directory is missing from `.claude-plugin/marketplace.json`, or a marketplace source points at a missing built directory.
29
+ - `OUT_OF_SYNC`: source and generated counterparts both changed and need human review.
30
+ - `IN_SYNC`: no plugin source/generated or marketplace registration drift was detected.
31
+
32
+ For every finding, include the evidence path and the smallest source-first next action. Prefer `bun run build:plugins` only after source edits are in the right place, and preserve `bun run check:plugins` as the final reproducibility gate.
33
+
34
+ ## Process
35
+
36
+ 1. Resolve the repo path from `$ARGUMENTS` or the current directory.
37
+ 2. Capture `git status --porcelain` before the diagnostic.
38
+ 3. Run:
39
+ ```bash
40
+ node plugins/lisa/scripts/plugin-sync-explain.mjs "$REPO_PATH"
41
+ ```
42
+ If running from the source tree before generated artifacts are rebuilt, use:
43
+ ```bash
44
+ node plugins/src/base/scripts/plugin-sync-explain.mjs "$REPO_PATH"
45
+ ```
46
+ 4. Capture `git status --porcelain` after the diagnostic and confirm it is unchanged.
47
+ 5. Surface the script output plus the read-only confirmation.
48
+
49
+ ## Rules
50
+
51
+ - Keep this surface aligned with `scripts/check-plugins-sync.sh`; it explains the gate but does not replace it.
52
+ - Treat `plugins/src/**` as the source of truth and `plugins/lisa*` as generated artifacts.
53
+ - If the diagnostic cannot inspect git status or marketplace JSON, report the error plainly and do not guess.