@codyswann/lisa 2.128.1 → 2.129.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 (86) 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 +136 -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 +136 -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 +136 -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 +136 -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/commands/exploratory-qa.md +1 -1
  23. package/plugins/lisa-expo/skills/exploratory-qa/SKILL.md +7 -4
  24. package/plugins/lisa-expo-agy/commands/exploratory-qa.md +1 -1
  25. package/plugins/lisa-expo-agy/plugin.json +1 -1
  26. package/plugins/lisa-expo-agy/skills/exploratory-qa/SKILL.md +7 -4
  27. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo-copilot/commands/exploratory-qa.md +1 -1
  29. package/plugins/lisa-expo-copilot/skills/exploratory-qa/SKILL.md +7 -4
  30. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo-cursor/commands/exploratory-qa.md +1 -1
  32. package/plugins/lisa-expo-cursor/skills/exploratory-qa/SKILL.md +7 -4
  33. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric/commands/exploratory-qa.md +1 -1
  36. package/plugins/lisa-harper-fabric/skills/exploratory-qa/SKILL.md +7 -4
  37. package/plugins/lisa-harper-fabric-agy/commands/exploratory-qa.md +1 -1
  38. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric-agy/skills/exploratory-qa/SKILL.md +7 -4
  40. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric-copilot/commands/exploratory-qa.md +1 -1
  42. package/plugins/lisa-harper-fabric-copilot/skills/exploratory-qa/SKILL.md +7 -4
  43. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric-cursor/commands/exploratory-qa.md +1 -1
  45. package/plugins/lisa-harper-fabric-cursor/skills/exploratory-qa/SKILL.md +7 -4
  46. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  49. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  54. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails/commands/exploratory-qa.md +1 -1
  59. package/plugins/lisa-rails/skills/exploratory-qa/SKILL.md +7 -4
  60. package/plugins/lisa-rails-agy/commands/exploratory-qa.md +1 -1
  61. package/plugins/lisa-rails-agy/plugin.json +1 -1
  62. package/plugins/lisa-rails-agy/skills/exploratory-qa/SKILL.md +7 -4
  63. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-rails-copilot/commands/exploratory-qa.md +1 -1
  65. package/plugins/lisa-rails-copilot/skills/exploratory-qa/SKILL.md +7 -4
  66. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-rails-cursor/commands/exploratory-qa.md +1 -1
  68. package/plugins/lisa-rails-cursor/skills/exploratory-qa/SKILL.md +7 -4
  69. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  71. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  72. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  76. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  77. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  79. package/plugins/src/base/scripts/doctor-report.mjs +136 -0
  80. package/plugins/src/base/scripts/plugin-sync-explain.mjs +64 -7
  81. package/plugins/src/expo/commands/exploratory-qa.md +1 -1
  82. package/plugins/src/expo/skills/exploratory-qa/SKILL.md +7 -4
  83. package/plugins/src/harper-fabric/commands/exploratory-qa.md +1 -1
  84. package/plugins/src/harper-fabric/skills/exploratory-qa/SKILL.md +7 -4
  85. package/plugins/src/rails/commands/exploratory-qa.md +1 -1
  86. package/plugins/src/rails/skills/exploratory-qa/SKILL.md +7 -4
@@ -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,85 @@ 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: renderPluginSyncRemediation(result),
100
+ },
101
+ ],
102
+ };
103
+ } catch (error) {
104
+ return {
105
+ id: "plugin-sync",
106
+ title: "Plugin source/generated sync",
107
+ checks: [
108
+ {
109
+ id: "plugin-sync",
110
+ status: "FAIL",
111
+ summary: "plugin sync readiness check failed",
112
+ observed: error instanceof Error ? error.message : String(error),
113
+ remediation:
114
+ "Run `/lisa:plugin-sync-explain` or `bun run check:plugins` to inspect plugin sync health directly.",
115
+ },
116
+ ],
117
+ };
118
+ }
119
+ }
120
+
37
121
  /**
38
122
  * @param {readonly DoctorGroup[]} groups
39
123
  * @returns {DoctorVerdict}
@@ -141,3 +225,55 @@ function normalizeCheck(check) {
141
225
  status: normalizedStatus,
142
226
  };
143
227
  }
228
+
229
+ /**
230
+ * @param {import("./plugin-sync-explain.mjs").PluginSyncResult} result
231
+ * @returns {string}
232
+ */
233
+ function renderPluginSyncObserved(result) {
234
+ if (result.verdict === "PASS") {
235
+ return "Drift class IN_SYNC; plugin sync evidence was collected read-only.";
236
+ }
237
+ const paths = result.affectedPaths.length
238
+ ? result.affectedPaths.join(", ")
239
+ : "none";
240
+ return `Drift class ${result.driftClass}; affected paths: ${paths}.`;
241
+ }
242
+
243
+ /**
244
+ * @param {import("./plugin-sync-explain.mjs").PluginSyncResult} result
245
+ * @returns {string | undefined}
246
+ */
247
+ function renderPluginSyncRemediation(result) {
248
+ if (result.verdict === "PASS") {
249
+ return undefined;
250
+ }
251
+
252
+ const nextAction = pluginSyncNextAction(result.driftClass);
253
+ const details = result.remediations
254
+ .map(item => `${item.path}: ${item.nextAction}`)
255
+ .join(" ");
256
+ const explain =
257
+ "Run `/lisa:plugin-sync-explain` or `bun run check:plugins` for the detailed drift report.";
258
+
259
+ return [nextAction, details, explain].filter(Boolean).join(" ");
260
+ }
261
+
262
+ /**
263
+ * @param {string} driftClass
264
+ * @returns {string}
265
+ */
266
+ function pluginSyncNextAction(driftClass) {
267
+ switch (driftClass) {
268
+ case "SOURCE_NOT_BUILT":
269
+ return "Next action: run `bun run build:plugins && bun run check:plugins`, then commit source plus regenerated plugin artifacts.";
270
+ case "OUT_OF_SYNC":
271
+ return "Next action: review source and generated plugin changes, keep `plugins/src` authoritative, then run `bun run build:plugins && bun run check:plugins`.";
272
+ case "GENERATED_ONLY":
273
+ return "Next action: move generated-only edits upstream to `plugins/src`, or remove the generated artifact drift if it should not ship.";
274
+ case "MARKETPLACE_REGISTRATION_DRIFT":
275
+ return "Next action: align marketplace registration with the built plugin manifests, or remove stale marketplace entries.";
276
+ default:
277
+ return `Next action: inspect plugin sync drift class ${driftClass}.`;
278
+ }
279
+ }
@@ -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.2",
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,85 @@ 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: renderPluginSyncRemediation(result),
100
+ },
101
+ ],
102
+ };
103
+ } catch (error) {
104
+ return {
105
+ id: "plugin-sync",
106
+ title: "Plugin source/generated sync",
107
+ checks: [
108
+ {
109
+ id: "plugin-sync",
110
+ status: "FAIL",
111
+ summary: "plugin sync readiness check failed",
112
+ observed: error instanceof Error ? error.message : String(error),
113
+ remediation:
114
+ "Run `/lisa:plugin-sync-explain` or `bun run check:plugins` to inspect plugin sync health directly.",
115
+ },
116
+ ],
117
+ };
118
+ }
119
+ }
120
+
37
121
  /**
38
122
  * @param {readonly DoctorGroup[]} groups
39
123
  * @returns {DoctorVerdict}
@@ -141,3 +225,55 @@ function normalizeCheck(check) {
141
225
  status: normalizedStatus,
142
226
  };
143
227
  }
228
+
229
+ /**
230
+ * @param {import("./plugin-sync-explain.mjs").PluginSyncResult} result
231
+ * @returns {string}
232
+ */
233
+ function renderPluginSyncObserved(result) {
234
+ if (result.verdict === "PASS") {
235
+ return "Drift class IN_SYNC; plugin sync evidence was collected read-only.";
236
+ }
237
+ const paths = result.affectedPaths.length
238
+ ? result.affectedPaths.join(", ")
239
+ : "none";
240
+ return `Drift class ${result.driftClass}; affected paths: ${paths}.`;
241
+ }
242
+
243
+ /**
244
+ * @param {import("./plugin-sync-explain.mjs").PluginSyncResult} result
245
+ * @returns {string | undefined}
246
+ */
247
+ function renderPluginSyncRemediation(result) {
248
+ if (result.verdict === "PASS") {
249
+ return undefined;
250
+ }
251
+
252
+ const nextAction = pluginSyncNextAction(result.driftClass);
253
+ const details = result.remediations
254
+ .map(item => `${item.path}: ${item.nextAction}`)
255
+ .join(" ");
256
+ const explain =
257
+ "Run `/lisa:plugin-sync-explain` or `bun run check:plugins` for the detailed drift report.";
258
+
259
+ return [nextAction, details, explain].filter(Boolean).join(" ");
260
+ }
261
+
262
+ /**
263
+ * @param {string} driftClass
264
+ * @returns {string}
265
+ */
266
+ function pluginSyncNextAction(driftClass) {
267
+ switch (driftClass) {
268
+ case "SOURCE_NOT_BUILT":
269
+ return "Next action: run `bun run build:plugins && bun run check:plugins`, then commit source plus regenerated plugin artifacts.";
270
+ case "OUT_OF_SYNC":
271
+ return "Next action: review source and generated plugin changes, keep `plugins/src` authoritative, then run `bun run build:plugins && bun run check:plugins`.";
272
+ case "GENERATED_ONLY":
273
+ return "Next action: move generated-only edits upstream to `plugins/src`, or remove the generated artifact drift if it should not ship.";
274
+ case "MARKETPLACE_REGISTRATION_DRIFT":
275
+ return "Next action: align marketplace registration with the built plugin manifests, or remove stale marketplace entries.";
276
+ default:
277
+ return `Next action: inspect plugin sync drift class ${driftClass}.`;
278
+ }
279
+ }
@@ -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-expo",
3
- "version": "2.128.1",
3
+ "version": "2.129.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.128.1",
3
+ "version": "2.129.2",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Run a first-time-user exploratory QA walkthrough: experience the app like a brand-new human user, clicking through to find anything confusing, broken, or hard to understand (machine-style labels, slow or unclear loads, cramped or cut-off UI, inconsistent UX, awkward scroll behavior) across all breakpoints, and file each finding (bug or usability issue) as a tracked work item via lisa:tracker-write. The optional ready flag marks tickets build-ready (auto-picked-up by lisa:intake) or leaves them in the backlog for human triage (default). For gaps in the automated Playwright suite, use e2e-coverage-gaps instead."
2
+ description: "Run a first-time-user exploratory QA walkthrough: experience the app like a brand-new human user, clicking through to find anything confusing, broken, or hard to understand (human-facing jargon, machine-style labels, slow or unclear loads, cramped or cut-off UI, inconsistent UX, awkward scroll behavior) across all breakpoints, and file each finding (bug or usability issue) as a tracked work item via lisa:tracker-write. The optional ready flag marks tickets build-ready (auto-picked-up by lisa:intake) or leaves them in the backlog for human triage (default). For gaps in the automated Playwright suite, use e2e-coverage-gaps instead."
3
3
  allowed-tools: ["Skill"]
4
4
  argument-hint: "[target-url | env] [ready=true|false]"
5
5
  ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: exploratory-qa
3
- description: First-time-user exploratory QA walkthrough for web apps that FEEDS THE LIFECYCLE. Use when asked to experience an app the way a brand-new human user would — landing cold on the home page and clicking through to find anything confusing, broken, or hard to understand (machine-style labels, slow or unclear loads, cramped or cut-off UI, inconsistent/non-standard UX, awkward scroll behavior, unclear affordances) across all breakpoints. Instead of writing a report file, it files every finding as a tracked work item via lisa:tracker-write (bugs and usability/UX issues). A `ready` parameter controls whether those tickets are created build-ready (auto-picked-up by lisa:intake) or left in the backlog for human triage (default). For gaps in the automated Playwright test suite, use the e2e-coverage-gaps skill instead.
3
+ description: First-time-user exploratory QA walkthrough for web apps that FEEDS THE LIFECYCLE. Use when asked to experience an app the way a brand-new human user would — landing cold on the home page and clicking through to find anything confusing, broken, or hard to understand (human-facing jargon, machine-style labels, slow or unclear loads, cramped or cut-off UI, inconsistent/non-standard UX, awkward scroll behavior, unclear affordances) across all breakpoints. Instead of writing a report file, it files every finding as a tracked work item via lisa:tracker-write (bugs and usability/UX issues). A `ready` parameter controls whether those tickets are created build-ready (auto-picked-up by lisa:intake) or left in the backlog for human triage (default). For gaps in the automated Playwright test suite, use the e2e-coverage-gaps skill instead.
4
4
  ---
5
5
 
6
6
  # Exploratory QA
@@ -49,9 +49,12 @@ filed as a tracked work item so it enters the Lisa lifecycle — no static repor
49
49
  Click through the visible paths and actually attempt real tasks — a first-time user explores, makes
50
50
  mistakes, and tries the obvious thing. Cover at least these dimensions unless the user narrows scope:
51
51
 
52
- - **Comprehension & labeling:** machine-style or developer labels shown to users (raw IDs, enum keys,
53
- `snake_case`, `null`/`undefined`, untranslated i18n keys), jargon, unclear button/menu names, icons
54
- with no discernible meaning.
52
+ - **Comprehension & labeling:** human-facing copy must sound like something a normal first-time user
53
+ would understand. Flag machine-style or developer labels shown to users (raw IDs, enum keys,
54
+ `snake_case`, `null`/`undefined`, untranslated i18n keys), admin/database terms such as
55
+ "metadata", implementation identifiers such as slugs, unexplained domain jargon, unclear
56
+ button/menu names, and icons with no discernible meaning. If a heading, label, or field would make a
57
+ non-technical user ask "what does that mean?", file a usability/clarity ticket with plainer wording.
55
58
  - **Navigation clarity:** is it obvious how to get somewhere and back? Dead ends, hidden entry points,
56
59
  surprising redirects, broken links, no clear "home".
57
60
  - **Visual/layout quality:** cut-off or truncated text, overlap, cramped/crowded density, offscreen or
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Run a first-time-user exploratory QA walkthrough: experience the app like a brand-new human user, clicking through to find anything confusing, broken, or hard to understand (machine-style labels, slow or unclear loads, cramped or cut-off UI, inconsistent UX, awkward scroll behavior) across all breakpoints, and file each finding (bug or usability issue) as a tracked work item via lisa:tracker-write. The optional ready flag marks tickets build-ready (auto-picked-up by lisa:intake) or leaves them in the backlog for human triage (default). For gaps in the automated Playwright suite, use e2e-coverage-gaps instead."
2
+ description: "Run a first-time-user exploratory QA walkthrough: experience the app like a brand-new human user, clicking through to find anything confusing, broken, or hard to understand (human-facing jargon, machine-style labels, slow or unclear loads, cramped or cut-off UI, inconsistent UX, awkward scroll behavior) across all breakpoints, and file each finding (bug or usability issue) as a tracked work item via lisa:tracker-write. The optional ready flag marks tickets build-ready (auto-picked-up by lisa:intake) or leaves them in the backlog for human triage (default). For gaps in the automated Playwright suite, use e2e-coverage-gaps instead."
3
3
  allowed-tools: ["Skill"]
4
4
  argument-hint: "[target-url | env] [ready=true|false]"
5
5
  ---
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.128.1",
3
+ "version": "2.129.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: exploratory-qa
3
- description: First-time-user exploratory QA walkthrough for web apps that FEEDS THE LIFECYCLE. Use when asked to experience an app the way a brand-new human user would — landing cold on the home page and clicking through to find anything confusing, broken, or hard to understand (machine-style labels, slow or unclear loads, cramped or cut-off UI, inconsistent/non-standard UX, awkward scroll behavior, unclear affordances) across all breakpoints. Instead of writing a report file, it files every finding as a tracked work item via lisa:tracker-write (bugs and usability/UX issues). A `ready` parameter controls whether those tickets are created build-ready (auto-picked-up by lisa:intake) or left in the backlog for human triage (default). For gaps in the automated Playwright test suite, use the e2e-coverage-gaps skill instead.
3
+ description: First-time-user exploratory QA walkthrough for web apps that FEEDS THE LIFECYCLE. Use when asked to experience an app the way a brand-new human user would — landing cold on the home page and clicking through to find anything confusing, broken, or hard to understand (human-facing jargon, machine-style labels, slow or unclear loads, cramped or cut-off UI, inconsistent/non-standard UX, awkward scroll behavior, unclear affordances) across all breakpoints. Instead of writing a report file, it files every finding as a tracked work item via lisa:tracker-write (bugs and usability/UX issues). A `ready` parameter controls whether those tickets are created build-ready (auto-picked-up by lisa:intake) or left in the backlog for human triage (default). For gaps in the automated Playwright test suite, use the e2e-coverage-gaps skill instead.
4
4
  ---
5
5
 
6
6
  # Exploratory QA
@@ -49,9 +49,12 @@ filed as a tracked work item so it enters the Lisa lifecycle — no static repor
49
49
  Click through the visible paths and actually attempt real tasks — a first-time user explores, makes
50
50
  mistakes, and tries the obvious thing. Cover at least these dimensions unless the user narrows scope:
51
51
 
52
- - **Comprehension & labeling:** machine-style or developer labels shown to users (raw IDs, enum keys,
53
- `snake_case`, `null`/`undefined`, untranslated i18n keys), jargon, unclear button/menu names, icons
54
- with no discernible meaning.
52
+ - **Comprehension & labeling:** human-facing copy must sound like something a normal first-time user
53
+ would understand. Flag machine-style or developer labels shown to users (raw IDs, enum keys,
54
+ `snake_case`, `null`/`undefined`, untranslated i18n keys), admin/database terms such as
55
+ "metadata", implementation identifiers such as slugs, unexplained domain jargon, unclear
56
+ button/menu names, and icons with no discernible meaning. If a heading, label, or field would make a
57
+ non-technical user ask "what does that mean?", file a usability/clarity ticket with plainer wording.
55
58
  - **Navigation clarity:** is it obvious how to get somewhere and back? Dead ends, hidden entry points,
56
59
  surprising redirects, broken links, no clear "home".
57
60
  - **Visual/layout quality:** cut-off or truncated text, overlap, cramped/crowded density, offscreen or
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.128.1",
3
+ "version": "2.129.2",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Run a first-time-user exploratory QA walkthrough: experience the app like a brand-new human user, clicking through to find anything confusing, broken, or hard to understand (machine-style labels, slow or unclear loads, cramped or cut-off UI, inconsistent UX, awkward scroll behavior) across all breakpoints, and file each finding (bug or usability issue) as a tracked work item via lisa:tracker-write. The optional ready flag marks tickets build-ready (auto-picked-up by lisa:intake) or leaves them in the backlog for human triage (default). For gaps in the automated Playwright suite, use e2e-coverage-gaps instead."
2
+ description: "Run a first-time-user exploratory QA walkthrough: experience the app like a brand-new human user, clicking through to find anything confusing, broken, or hard to understand (human-facing jargon, machine-style labels, slow or unclear loads, cramped or cut-off UI, inconsistent UX, awkward scroll behavior) across all breakpoints, and file each finding (bug or usability issue) as a tracked work item via lisa:tracker-write. The optional ready flag marks tickets build-ready (auto-picked-up by lisa:intake) or leaves them in the backlog for human triage (default). For gaps in the automated Playwright suite, use e2e-coverage-gaps instead."
3
3
  allowed-tools: ["Skill"]
4
4
  argument-hint: "[target-url | env] [ready=true|false]"
5
5
  ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: exploratory-qa
3
- description: First-time-user exploratory QA walkthrough for web apps that FEEDS THE LIFECYCLE. Use when asked to experience an app the way a brand-new human user would — landing cold on the home page and clicking through to find anything confusing, broken, or hard to understand (machine-style labels, slow or unclear loads, cramped or cut-off UI, inconsistent/non-standard UX, awkward scroll behavior, unclear affordances) across all breakpoints. Instead of writing a report file, it files every finding as a tracked work item via lisa:tracker-write (bugs and usability/UX issues). A `ready` parameter controls whether those tickets are created build-ready (auto-picked-up by lisa:intake) or left in the backlog for human triage (default). For gaps in the automated Playwright test suite, use the e2e-coverage-gaps skill instead.
3
+ description: First-time-user exploratory QA walkthrough for web apps that FEEDS THE LIFECYCLE. Use when asked to experience an app the way a brand-new human user would — landing cold on the home page and clicking through to find anything confusing, broken, or hard to understand (human-facing jargon, machine-style labels, slow or unclear loads, cramped or cut-off UI, inconsistent/non-standard UX, awkward scroll behavior, unclear affordances) across all breakpoints. Instead of writing a report file, it files every finding as a tracked work item via lisa:tracker-write (bugs and usability/UX issues). A `ready` parameter controls whether those tickets are created build-ready (auto-picked-up by lisa:intake) or left in the backlog for human triage (default). For gaps in the automated Playwright test suite, use the e2e-coverage-gaps skill instead.
4
4
  ---
5
5
 
6
6
  # Exploratory QA
@@ -49,9 +49,12 @@ filed as a tracked work item so it enters the Lisa lifecycle — no static repor
49
49
  Click through the visible paths and actually attempt real tasks — a first-time user explores, makes
50
50
  mistakes, and tries the obvious thing. Cover at least these dimensions unless the user narrows scope:
51
51
 
52
- - **Comprehension & labeling:** machine-style or developer labels shown to users (raw IDs, enum keys,
53
- `snake_case`, `null`/`undefined`, untranslated i18n keys), jargon, unclear button/menu names, icons
54
- with no discernible meaning.
52
+ - **Comprehension & labeling:** human-facing copy must sound like something a normal first-time user
53
+ would understand. Flag machine-style or developer labels shown to users (raw IDs, enum keys,
54
+ `snake_case`, `null`/`undefined`, untranslated i18n keys), admin/database terms such as
55
+ "metadata", implementation identifiers such as slugs, unexplained domain jargon, unclear
56
+ button/menu names, and icons with no discernible meaning. If a heading, label, or field would make a
57
+ non-technical user ask "what does that mean?", file a usability/clarity ticket with plainer wording.
55
58
  - **Navigation clarity:** is it obvious how to get somewhere and back? Dead ends, hidden entry points,
56
59
  surprising redirects, broken links, no clear "home".
57
60
  - **Visual/layout quality:** cut-off or truncated text, overlap, cramped/crowded density, offscreen or