@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.
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/commands/plugin-sync-explain.md +8 -0
- package/plugins/lisa/scripts/plugin-sync-explain.mjs +332 -0
- package/plugins/lisa/skills/plugin-sync-explain/SKILL.md +53 -0
- package/plugins/lisa/skills/plugin-sync-explain/agents/openai.yaml +4 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/src/base/commands/plugin-sync-explain.md +8 -0
- package/plugins/src/base/scripts/plugin-sync-explain.mjs +332 -0
- 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.
|
|
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": {
|
|
@@ -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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.106.
|
|
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.
|
|
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"
|
|
@@ -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.
|