@codyswann/lisa 2.128.1 → 2.129.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 (56) 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/scripts/doctor-report.mjs +103 -0
  5. package/plugins/lisa/scripts/plugin-sync-explain.mjs +64 -7
  6. package/plugins/lisa-agy/plugin.json +1 -1
  7. package/plugins/lisa-agy/scripts/doctor-report.mjs +103 -0
  8. package/plugins/lisa-agy/scripts/plugin-sync-explain.mjs +64 -7
  9. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  11. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  12. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-copilot/scripts/doctor-report.mjs +103 -0
  16. package/plugins/lisa-copilot/scripts/plugin-sync-explain.mjs +64 -7
  17. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cursor/scripts/doctor-report.mjs +103 -0
  19. package/plugins/lisa-cursor/scripts/plugin-sync-explain.mjs +64 -7
  20. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-expo-agy/plugin.json +1 -1
  23. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  33. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  38. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-rails-agy/plugin.json +1 -1
  43. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  48. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  53. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/src/base/scripts/doctor-report.mjs +103 -0
  56. package/plugins/src/base/scripts/plugin-sync-explain.mjs +64 -7
package/package.json CHANGED
@@ -83,7 +83,7 @@
83
83
  "lodash": ">=4.18.1"
84
84
  },
85
85
  "name": "@codyswann/lisa",
86
- "version": "2.128.1",
86
+ "version": "2.129.0",
87
87
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
88
88
  "main": "dist/index.js",
89
89
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.128.1",
3
+ "version": "2.129.0",
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.128.1",
3
+ "version": "2.129.0",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -6,6 +6,11 @@
6
6
  * repo adds real readiness probes. Keep this file dependency-free so future
7
7
  * doctor scripts can reuse it from plugin distributions and downstream repos.
8
8
  */
9
+ import { existsSync } from "node:fs";
10
+ import path from "node:path";
11
+ import process from "node:process";
12
+
13
+ import { getPluginSyncResult } from "./plugin-sync-explain.mjs";
9
14
 
10
15
  export const DOCTOR_STATUSES = ["PASS", "WARN", "FAIL", "SKIP"];
11
16
  export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
@@ -34,6 +39,90 @@ export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
34
39
  * }} DoctorReportInput
35
40
  */
36
41
 
42
+ /**
43
+ * @param {string} root
44
+ * @returns {DoctorGroup}
45
+ */
46
+ export function createPluginSyncDoctorGroup(root = process.cwd()) {
47
+ const repoRoot = path.resolve(root);
48
+ if (
49
+ !existsSync(path.join(repoRoot, "plugins", "src")) &&
50
+ !existsSync(path.join(repoRoot, "plugins"))
51
+ ) {
52
+ return {
53
+ id: "plugin-sync",
54
+ title: "Plugin source/generated sync",
55
+ checks: [
56
+ {
57
+ id: "plugin-sync",
58
+ status: "SKIP",
59
+ summary: "plugin sync check is not applicable",
60
+ observed:
61
+ "No plugins/ or plugins/src/ directory was found in this repository.",
62
+ },
63
+ ],
64
+ };
65
+ }
66
+
67
+ try {
68
+ const result = getPluginSyncResult(repoRoot);
69
+ if (!result.readOnly) {
70
+ return {
71
+ id: "plugin-sync",
72
+ title: "Plugin source/generated sync",
73
+ checks: [
74
+ {
75
+ id: "plugin-sync",
76
+ status: "FAIL",
77
+ summary: "plugin sync readiness check mutated the working tree",
78
+ observed:
79
+ "Git status changed while collecting plugin sync evidence.",
80
+ remediation:
81
+ "Run `git status --short`, inspect the unexpected changes, and fix plugin-sync-explain before trusting doctor output.",
82
+ },
83
+ ],
84
+ };
85
+ }
86
+
87
+ return {
88
+ id: "plugin-sync",
89
+ title: "Plugin source/generated sync",
90
+ checks: [
91
+ {
92
+ id: "plugin-sync",
93
+ status: result.verdict,
94
+ summary:
95
+ result.verdict === "PASS"
96
+ ? "plugin source and generated artifacts are in sync"
97
+ : `plugin sync drift detected: ${result.driftClass}`,
98
+ observed: renderPluginSyncObserved(result),
99
+ remediation:
100
+ result.remediations.length > 0
101
+ ? result.remediations
102
+ .map(item => `${item.path}: ${item.nextAction}`)
103
+ .join(" ")
104
+ : undefined,
105
+ },
106
+ ],
107
+ };
108
+ } catch (error) {
109
+ return {
110
+ id: "plugin-sync",
111
+ title: "Plugin source/generated sync",
112
+ checks: [
113
+ {
114
+ id: "plugin-sync",
115
+ status: "FAIL",
116
+ summary: "plugin sync readiness check failed",
117
+ observed: error instanceof Error ? error.message : String(error),
118
+ remediation:
119
+ "Run `/lisa:plugin-sync-explain` or `bun run check:plugins` to inspect plugin sync health directly.",
120
+ },
121
+ ],
122
+ };
123
+ }
124
+ }
125
+
37
126
  /**
38
127
  * @param {readonly DoctorGroup[]} groups
39
128
  * @returns {DoctorVerdict}
@@ -141,3 +230,17 @@ function normalizeCheck(check) {
141
230
  status: normalizedStatus,
142
231
  };
143
232
  }
233
+
234
+ /**
235
+ * @param {import("./plugin-sync-explain.mjs").PluginSyncResult} result
236
+ * @returns {string}
237
+ */
238
+ function renderPluginSyncObserved(result) {
239
+ if (result.verdict === "PASS") {
240
+ return "Drift class IN_SYNC; plugin sync evidence was collected read-only.";
241
+ }
242
+ const paths = result.affectedPaths.length
243
+ ? result.affectedPaths.join(", ")
244
+ : "none";
245
+ return `Drift class ${result.driftClass}; affected paths: ${paths}.`;
246
+ }
@@ -50,6 +50,25 @@ const GIT_BIN = "/usr/bin/git";
50
50
  * readonly readOnly: boolean
51
51
  * readonly text: string
52
52
  * }} PluginSyncReport
53
+ *
54
+ * @typedef {{
55
+ * readonly path: string
56
+ * readonly counterpart?: string
57
+ * readonly classification: string
58
+ * readonly nextAction: string
59
+ * }} PluginSyncRemediation
60
+ *
61
+ * @typedef {{
62
+ * readonly root: string
63
+ * readonly verdict: "PASS" | "WARN"
64
+ * readonly driftClass: string
65
+ * readonly affectedPaths: readonly string[]
66
+ * readonly remediations: readonly PluginSyncRemediation[]
67
+ * readonly findings: readonly PluginSyncFinding[]
68
+ * readonly statusBefore: string
69
+ * readonly statusAfter: string
70
+ * readonly readOnly: boolean
71
+ * }} PluginSyncResult
53
72
  */
54
73
 
55
74
  /**
@@ -57,6 +76,25 @@ const GIT_BIN = "/usr/bin/git";
57
76
  * @returns {PluginSyncReport}
58
77
  */
59
78
  export function explainPluginSync(root = process.cwd()) {
79
+ const result = getPluginSyncResult(root);
80
+
81
+ return {
82
+ root: result.root,
83
+ findings: result.findings,
84
+ statusBefore: result.statusBefore,
85
+ statusAfter: result.statusAfter,
86
+ readOnly: result.readOnly,
87
+ text: renderPluginSyncReport(result),
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Return a structured, read-only plugin sync result for readiness surfaces such
93
+ * as doctor. CLI callers should still render this through renderPluginSyncReport.
94
+ * @param {string} root
95
+ * @returns {PluginSyncResult}
96
+ */
97
+ export function getPluginSyncResult(root = process.cwd()) {
60
98
  const repoRoot = path.resolve(root);
61
99
  const statusBefore = gitStatus(repoRoot);
62
100
  const statusEntries = parseGitStatus(statusBefore);
@@ -71,20 +109,23 @@ export function explainPluginSync(root = process.cwd()) {
71
109
  ];
72
110
  const statusAfter = gitStatus(repoRoot);
73
111
  const readOnly = statusAfter === statusBefore;
112
+ const driftClass = highestClassification(findings);
74
113
 
75
114
  return {
76
115
  root: repoRoot,
116
+ verdict: findings.length === 0 ? "PASS" : "WARN",
117
+ driftClass,
118
+ affectedPaths: affectedPaths(findings),
119
+ remediations: findings.map(finding => ({
120
+ path: finding.path,
121
+ counterpart: finding.counterpart,
122
+ classification: finding.classification,
123
+ nextAction: finding.nextAction,
124
+ })),
77
125
  findings,
78
126
  statusBefore,
79
127
  statusAfter,
80
128
  readOnly,
81
- text: renderPluginSyncReport({
82
- root: repoRoot,
83
- findings,
84
- statusBefore,
85
- statusAfter,
86
- readOnly,
87
- }),
88
129
  };
89
130
  }
90
131
 
@@ -142,6 +183,22 @@ function highestClassification(findings) {
142
183
  return "IN_SYNC";
143
184
  }
144
185
 
186
+ /**
187
+ * @param {readonly PluginSyncFinding[]} findings
188
+ * @returns {string[]}
189
+ */
190
+ function affectedPaths(findings) {
191
+ return [
192
+ ...new Set(
193
+ findings.flatMap(finding =>
194
+ [finding.path, finding.counterpart].filter(
195
+ pathValue => typeof pathValue === "string" && pathValue.length > 0
196
+ )
197
+ )
198
+ ),
199
+ ];
200
+ }
201
+
145
202
  /**
146
203
  * @param {readonly { readonly code: string, readonly file: string }[]} entries
147
204
  * @param {ReadonlyMap<string, Buffer> | undefined} expectedGeneratedFiles
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.128.1",
3
+ "version": "2.129.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -6,6 +6,11 @@
6
6
  * repo adds real readiness probes. Keep this file dependency-free so future
7
7
  * doctor scripts can reuse it from plugin distributions and downstream repos.
8
8
  */
9
+ import { existsSync } from "node:fs";
10
+ import path from "node:path";
11
+ import process from "node:process";
12
+
13
+ import { getPluginSyncResult } from "./plugin-sync-explain.mjs";
9
14
 
10
15
  export const DOCTOR_STATUSES = ["PASS", "WARN", "FAIL", "SKIP"];
11
16
  export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
@@ -34,6 +39,90 @@ export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
34
39
  * }} DoctorReportInput
35
40
  */
36
41
 
42
+ /**
43
+ * @param {string} root
44
+ * @returns {DoctorGroup}
45
+ */
46
+ export function createPluginSyncDoctorGroup(root = process.cwd()) {
47
+ const repoRoot = path.resolve(root);
48
+ if (
49
+ !existsSync(path.join(repoRoot, "plugins", "src")) &&
50
+ !existsSync(path.join(repoRoot, "plugins"))
51
+ ) {
52
+ return {
53
+ id: "plugin-sync",
54
+ title: "Plugin source/generated sync",
55
+ checks: [
56
+ {
57
+ id: "plugin-sync",
58
+ status: "SKIP",
59
+ summary: "plugin sync check is not applicable",
60
+ observed:
61
+ "No plugins/ or plugins/src/ directory was found in this repository.",
62
+ },
63
+ ],
64
+ };
65
+ }
66
+
67
+ try {
68
+ const result = getPluginSyncResult(repoRoot);
69
+ if (!result.readOnly) {
70
+ return {
71
+ id: "plugin-sync",
72
+ title: "Plugin source/generated sync",
73
+ checks: [
74
+ {
75
+ id: "plugin-sync",
76
+ status: "FAIL",
77
+ summary: "plugin sync readiness check mutated the working tree",
78
+ observed:
79
+ "Git status changed while collecting plugin sync evidence.",
80
+ remediation:
81
+ "Run `git status --short`, inspect the unexpected changes, and fix plugin-sync-explain before trusting doctor output.",
82
+ },
83
+ ],
84
+ };
85
+ }
86
+
87
+ return {
88
+ id: "plugin-sync",
89
+ title: "Plugin source/generated sync",
90
+ checks: [
91
+ {
92
+ id: "plugin-sync",
93
+ status: result.verdict,
94
+ summary:
95
+ result.verdict === "PASS"
96
+ ? "plugin source and generated artifacts are in sync"
97
+ : `plugin sync drift detected: ${result.driftClass}`,
98
+ observed: renderPluginSyncObserved(result),
99
+ remediation:
100
+ result.remediations.length > 0
101
+ ? result.remediations
102
+ .map(item => `${item.path}: ${item.nextAction}`)
103
+ .join(" ")
104
+ : undefined,
105
+ },
106
+ ],
107
+ };
108
+ } catch (error) {
109
+ return {
110
+ id: "plugin-sync",
111
+ title: "Plugin source/generated sync",
112
+ checks: [
113
+ {
114
+ id: "plugin-sync",
115
+ status: "FAIL",
116
+ summary: "plugin sync readiness check failed",
117
+ observed: error instanceof Error ? error.message : String(error),
118
+ remediation:
119
+ "Run `/lisa:plugin-sync-explain` or `bun run check:plugins` to inspect plugin sync health directly.",
120
+ },
121
+ ],
122
+ };
123
+ }
124
+ }
125
+
37
126
  /**
38
127
  * @param {readonly DoctorGroup[]} groups
39
128
  * @returns {DoctorVerdict}
@@ -141,3 +230,17 @@ function normalizeCheck(check) {
141
230
  status: normalizedStatus,
142
231
  };
143
232
  }
233
+
234
+ /**
235
+ * @param {import("./plugin-sync-explain.mjs").PluginSyncResult} result
236
+ * @returns {string}
237
+ */
238
+ function renderPluginSyncObserved(result) {
239
+ if (result.verdict === "PASS") {
240
+ return "Drift class IN_SYNC; plugin sync evidence was collected read-only.";
241
+ }
242
+ const paths = result.affectedPaths.length
243
+ ? result.affectedPaths.join(", ")
244
+ : "none";
245
+ return `Drift class ${result.driftClass}; affected paths: ${paths}.`;
246
+ }
@@ -50,6 +50,25 @@ const GIT_BIN = "/usr/bin/git";
50
50
  * readonly readOnly: boolean
51
51
  * readonly text: string
52
52
  * }} PluginSyncReport
53
+ *
54
+ * @typedef {{
55
+ * readonly path: string
56
+ * readonly counterpart?: string
57
+ * readonly classification: string
58
+ * readonly nextAction: string
59
+ * }} PluginSyncRemediation
60
+ *
61
+ * @typedef {{
62
+ * readonly root: string
63
+ * readonly verdict: "PASS" | "WARN"
64
+ * readonly driftClass: string
65
+ * readonly affectedPaths: readonly string[]
66
+ * readonly remediations: readonly PluginSyncRemediation[]
67
+ * readonly findings: readonly PluginSyncFinding[]
68
+ * readonly statusBefore: string
69
+ * readonly statusAfter: string
70
+ * readonly readOnly: boolean
71
+ * }} PluginSyncResult
53
72
  */
54
73
 
55
74
  /**
@@ -57,6 +76,25 @@ const GIT_BIN = "/usr/bin/git";
57
76
  * @returns {PluginSyncReport}
58
77
  */
59
78
  export function explainPluginSync(root = process.cwd()) {
79
+ const result = getPluginSyncResult(root);
80
+
81
+ return {
82
+ root: result.root,
83
+ findings: result.findings,
84
+ statusBefore: result.statusBefore,
85
+ statusAfter: result.statusAfter,
86
+ readOnly: result.readOnly,
87
+ text: renderPluginSyncReport(result),
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Return a structured, read-only plugin sync result for readiness surfaces such
93
+ * as doctor. CLI callers should still render this through renderPluginSyncReport.
94
+ * @param {string} root
95
+ * @returns {PluginSyncResult}
96
+ */
97
+ export function getPluginSyncResult(root = process.cwd()) {
60
98
  const repoRoot = path.resolve(root);
61
99
  const statusBefore = gitStatus(repoRoot);
62
100
  const statusEntries = parseGitStatus(statusBefore);
@@ -71,20 +109,23 @@ export function explainPluginSync(root = process.cwd()) {
71
109
  ];
72
110
  const statusAfter = gitStatus(repoRoot);
73
111
  const readOnly = statusAfter === statusBefore;
112
+ const driftClass = highestClassification(findings);
74
113
 
75
114
  return {
76
115
  root: repoRoot,
116
+ verdict: findings.length === 0 ? "PASS" : "WARN",
117
+ driftClass,
118
+ affectedPaths: affectedPaths(findings),
119
+ remediations: findings.map(finding => ({
120
+ path: finding.path,
121
+ counterpart: finding.counterpart,
122
+ classification: finding.classification,
123
+ nextAction: finding.nextAction,
124
+ })),
77
125
  findings,
78
126
  statusBefore,
79
127
  statusAfter,
80
128
  readOnly,
81
- text: renderPluginSyncReport({
82
- root: repoRoot,
83
- findings,
84
- statusBefore,
85
- statusAfter,
86
- readOnly,
87
- }),
88
129
  };
89
130
  }
90
131
 
@@ -142,6 +183,22 @@ function highestClassification(findings) {
142
183
  return "IN_SYNC";
143
184
  }
144
185
 
186
+ /**
187
+ * @param {readonly PluginSyncFinding[]} findings
188
+ * @returns {string[]}
189
+ */
190
+ function affectedPaths(findings) {
191
+ return [
192
+ ...new Set(
193
+ findings.flatMap(finding =>
194
+ [finding.path, finding.counterpart].filter(
195
+ pathValue => typeof pathValue === "string" && pathValue.length > 0
196
+ )
197
+ )
198
+ ),
199
+ ];
200
+ }
201
+
145
202
  /**
146
203
  * @param {readonly { readonly code: string, readonly file: string }[]} entries
147
204
  * @param {ReadonlyMap<string, Buffer> | undefined} expectedGeneratedFiles
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.128.1",
3
+ "version": "2.129.0",
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.128.1",
3
+ "version": "2.129.0",
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-cdk",
3
- "version": "2.128.1",
3
+ "version": "2.129.0",
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.128.1",
3
+ "version": "2.129.0",
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.128.1",
3
+ "version": "2.129.0",
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",
3
- "version": "2.128.1",
3
+ "version": "2.129.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -6,6 +6,11 @@
6
6
  * repo adds real readiness probes. Keep this file dependency-free so future
7
7
  * doctor scripts can reuse it from plugin distributions and downstream repos.
8
8
  */
9
+ import { existsSync } from "node:fs";
10
+ import path from "node:path";
11
+ import process from "node:process";
12
+
13
+ import { getPluginSyncResult } from "./plugin-sync-explain.mjs";
9
14
 
10
15
  export const DOCTOR_STATUSES = ["PASS", "WARN", "FAIL", "SKIP"];
11
16
  export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
@@ -34,6 +39,90 @@ export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
34
39
  * }} DoctorReportInput
35
40
  */
36
41
 
42
+ /**
43
+ * @param {string} root
44
+ * @returns {DoctorGroup}
45
+ */
46
+ export function createPluginSyncDoctorGroup(root = process.cwd()) {
47
+ const repoRoot = path.resolve(root);
48
+ if (
49
+ !existsSync(path.join(repoRoot, "plugins", "src")) &&
50
+ !existsSync(path.join(repoRoot, "plugins"))
51
+ ) {
52
+ return {
53
+ id: "plugin-sync",
54
+ title: "Plugin source/generated sync",
55
+ checks: [
56
+ {
57
+ id: "plugin-sync",
58
+ status: "SKIP",
59
+ summary: "plugin sync check is not applicable",
60
+ observed:
61
+ "No plugins/ or plugins/src/ directory was found in this repository.",
62
+ },
63
+ ],
64
+ };
65
+ }
66
+
67
+ try {
68
+ const result = getPluginSyncResult(repoRoot);
69
+ if (!result.readOnly) {
70
+ return {
71
+ id: "plugin-sync",
72
+ title: "Plugin source/generated sync",
73
+ checks: [
74
+ {
75
+ id: "plugin-sync",
76
+ status: "FAIL",
77
+ summary: "plugin sync readiness check mutated the working tree",
78
+ observed:
79
+ "Git status changed while collecting plugin sync evidence.",
80
+ remediation:
81
+ "Run `git status --short`, inspect the unexpected changes, and fix plugin-sync-explain before trusting doctor output.",
82
+ },
83
+ ],
84
+ };
85
+ }
86
+
87
+ return {
88
+ id: "plugin-sync",
89
+ title: "Plugin source/generated sync",
90
+ checks: [
91
+ {
92
+ id: "plugin-sync",
93
+ status: result.verdict,
94
+ summary:
95
+ result.verdict === "PASS"
96
+ ? "plugin source and generated artifacts are in sync"
97
+ : `plugin sync drift detected: ${result.driftClass}`,
98
+ observed: renderPluginSyncObserved(result),
99
+ remediation:
100
+ result.remediations.length > 0
101
+ ? result.remediations
102
+ .map(item => `${item.path}: ${item.nextAction}`)
103
+ .join(" ")
104
+ : undefined,
105
+ },
106
+ ],
107
+ };
108
+ } catch (error) {
109
+ return {
110
+ id: "plugin-sync",
111
+ title: "Plugin source/generated sync",
112
+ checks: [
113
+ {
114
+ id: "plugin-sync",
115
+ status: "FAIL",
116
+ summary: "plugin sync readiness check failed",
117
+ observed: error instanceof Error ? error.message : String(error),
118
+ remediation:
119
+ "Run `/lisa:plugin-sync-explain` or `bun run check:plugins` to inspect plugin sync health directly.",
120
+ },
121
+ ],
122
+ };
123
+ }
124
+ }
125
+
37
126
  /**
38
127
  * @param {readonly DoctorGroup[]} groups
39
128
  * @returns {DoctorVerdict}
@@ -141,3 +230,17 @@ function normalizeCheck(check) {
141
230
  status: normalizedStatus,
142
231
  };
143
232
  }
233
+
234
+ /**
235
+ * @param {import("./plugin-sync-explain.mjs").PluginSyncResult} result
236
+ * @returns {string}
237
+ */
238
+ function renderPluginSyncObserved(result) {
239
+ if (result.verdict === "PASS") {
240
+ return "Drift class IN_SYNC; plugin sync evidence was collected read-only.";
241
+ }
242
+ const paths = result.affectedPaths.length
243
+ ? result.affectedPaths.join(", ")
244
+ : "none";
245
+ return `Drift class ${result.driftClass}; affected paths: ${paths}.`;
246
+ }