@codyswann/lisa 2.162.0 → 2.163.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 (66) 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-agy/plugin.json +1 -1
  5. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  8. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  9. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  11. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  14. package/plugins/lisa-expo-agy/plugin.json +1 -1
  15. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +12 -1
  18. package/plugins/lisa-harper-fabric/.codex-plugin/hooks.json +11 -0
  19. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  20. package/plugins/lisa-harper-fabric/hooks/enforce-config-extensions.mjs +143 -0
  21. package/plugins/lisa-harper-fabric/hooks/enforce-config-extensions.sh +19 -0
  22. package/plugins/lisa-harper-fabric/rules/harper-fabric.md +1 -0
  23. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  24. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +12 -1
  25. package/plugins/lisa-harper-fabric-copilot/hooks/enforce-config-extensions.mjs +143 -0
  26. package/plugins/lisa-harper-fabric-copilot/hooks/enforce-config-extensions.sh +19 -0
  27. package/plugins/lisa-harper-fabric-copilot/rules/harper-fabric.md +1 -0
  28. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric-cursor/hooks/enforce-config-extensions.mjs +143 -0
  30. package/plugins/lisa-harper-fabric-cursor/hooks/enforce-config-extensions.sh +19 -0
  31. package/plugins/lisa-harper-fabric-cursor/hooks/hooks.json +6 -0
  32. package/plugins/lisa-harper-fabric-cursor/rules/harper-fabric.mdc +1 -0
  33. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  36. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  41. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  46. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails-agy/plugin.json +1 -1
  51. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  56. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  61. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/src/harper-fabric/.claude-plugin/plugin.json +8 -0
  64. package/plugins/src/harper-fabric/hooks/enforce-config-extensions.mjs +143 -0
  65. package/plugins/src/harper-fabric/hooks/enforce-config-extensions.sh +19 -0
  66. package/plugins/src/harper-fabric/rules/harper-fabric.md +1 -0
package/package.json CHANGED
@@ -85,7 +85,7 @@
85
85
  "lodash": ">=4.18.1"
86
86
  },
87
87
  "name": "@codyswann/lisa",
88
- "version": "2.162.0",
88
+ "version": "2.163.0",
89
89
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
90
90
  "main": "dist/index.js",
91
91
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.162.0",
3
+ "version": "2.163.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.162.0",
3
+ "version": "2.163.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.162.0",
3
+ "version": "2.163.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-cdk",
3
- "version": "2.162.0",
3
+ "version": "2.163.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.162.0",
3
+ "version": "2.163.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.162.0",
3
+ "version": "2.163.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.162.0",
3
+ "version": "2.163.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.162.0",
3
+ "version": "2.163.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.162.0",
3
+ "version": "2.163.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.162.0",
3
+ "version": "2.163.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-expo",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
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.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
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.162.0",
3
+ "version": "2.163.0",
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.162.0",
3
+ "version": "2.163.0",
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-harper-fabric",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -20,6 +20,17 @@
20
20
  ]
21
21
  }
22
22
  ],
23
+ "PostToolUse": [
24
+ {
25
+ "matcher": "Write|Edit|MultiEdit",
26
+ "hooks": [
27
+ {
28
+ "type": "command",
29
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/enforce-config-extensions.sh"
30
+ }
31
+ ]
32
+ }
33
+ ],
23
34
  "SessionStart": [
24
35
  {
25
36
  "matcher": "",
@@ -11,6 +11,17 @@
11
11
  ]
12
12
  }
13
13
  ],
14
+ "PostToolUse": [
15
+ {
16
+ "matcher": "Write|Edit|MultiEdit",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "${PLUGIN_ROOT}/hooks/enforce-config-extensions.sh"
21
+ }
22
+ ]
23
+ }
24
+ ],
14
25
  "SessionStart": [
15
26
  {
16
27
  "matcher": "",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Harper/Fabric-specific Lisa rules for TypeScript component apps.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { readFileSync } from "node:fs";
4
+ import { spawnSync } from "node:child_process";
5
+ import path from "node:path";
6
+
7
+ const BLOCKED = 2;
8
+ const ALLOWED = 0;
9
+ const CONFIG_PATH = "harper-app/config.yaml";
10
+ const ALLOWLIST_PATH = ".lisa/harper-config-extension-allowlist.json";
11
+
12
+ const readStdin = () => {
13
+ try {
14
+ return readFileSync(0, "utf8");
15
+ } catch {
16
+ return "";
17
+ }
18
+ };
19
+
20
+ const parseHookInput = raw => {
21
+ try {
22
+ return JSON.parse(raw);
23
+ } catch {
24
+ return {};
25
+ }
26
+ };
27
+
28
+ const normalizePath = filePath =>
29
+ filePath.replace(/\\/g, "/").replace(/^\.\//, "");
30
+
31
+ const isConfigPath = filePath => {
32
+ const normalized = normalizePath(filePath);
33
+ return normalized === CONFIG_PATH || normalized.endsWith(`/${CONFIG_PATH}`);
34
+ };
35
+
36
+ const repoRelativeConfigPath = filePath => {
37
+ const normalized = normalizePath(filePath);
38
+ const index = normalized.lastIndexOf(CONFIG_PATH);
39
+ return index === -1 ? normalized : normalized.slice(index);
40
+ };
41
+
42
+ const loadYaml = () => {
43
+ const require = createRequire(import.meta.url);
44
+ return require("js-yaml");
45
+ };
46
+
47
+ const topLevelExtensionKeys = yamlText => {
48
+ const yaml = loadYaml();
49
+ let parsed;
50
+ try {
51
+ parsed = yaml.load(yamlText);
52
+ } catch {
53
+ return null;
54
+ }
55
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
56
+ return Object.keys(parsed).sort();
57
+ };
58
+
59
+ const gitEnv = () =>
60
+ Object.fromEntries(
61
+ Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_"))
62
+ );
63
+
64
+ const readGitBlob = repoRoot => {
65
+ const result = spawnSync(
66
+ "git",
67
+ ["-C", repoRoot, "show", `HEAD:${CONFIG_PATH}`],
68
+ {
69
+ encoding: "utf8",
70
+ env: gitEnv(),
71
+ }
72
+ );
73
+ return result.status === 0 ? result.stdout : null;
74
+ };
75
+
76
+ const readAllowlist = (repoRoot, configPath) => {
77
+ const allowlistFile = path.join(repoRoot, ALLOWLIST_PATH);
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(readFileSync(allowlistFile, "utf8"));
81
+ } catch {
82
+ return new Set();
83
+ }
84
+
85
+ const entry = parsed?.[configPath] ?? parsed?.[CONFIG_PATH];
86
+ const values = Array.isArray(entry)
87
+ ? entry
88
+ : Array.isArray(entry?.allowedRemovedExtensions)
89
+ ? entry.allowedRemovedExtensions
90
+ : [];
91
+ return new Set(values.filter(value => typeof value === "string"));
92
+ };
93
+
94
+ const main = () => {
95
+ const input = parseHookInput(readStdin());
96
+ const filePath = input?.tool_input?.file_path;
97
+ if (typeof filePath !== "string" || !isConfigPath(filePath)) return ALLOWED;
98
+
99
+ const repoRoot = process.cwd();
100
+ const configPath = repoRelativeConfigPath(filePath);
101
+ let currentText;
102
+ try {
103
+ currentText = readFileSync(path.join(repoRoot, configPath), "utf8");
104
+ } catch {
105
+ return ALLOWED;
106
+ }
107
+
108
+ const previousText = readGitBlob(repoRoot);
109
+ if (previousText === null) return ALLOWED;
110
+
111
+ const previousExtensions = topLevelExtensionKeys(previousText);
112
+ const currentExtensionKeys = topLevelExtensionKeys(currentText);
113
+ if (previousExtensions === null || currentExtensionKeys === null)
114
+ return ALLOWED;
115
+ const currentExtensions = new Set(currentExtensionKeys);
116
+ const allowedRemovals = readAllowlist(repoRoot, configPath);
117
+ const missing = previousExtensions.filter(
118
+ extension =>
119
+ !currentExtensions.has(extension) && !allowedRemovals.has(extension)
120
+ );
121
+
122
+ if (missing.length === 0) return ALLOWED;
123
+
124
+ process.stderr
125
+ .write(`Blocked: harper-app/config.yaml dropped required Harper extension(s).
126
+
127
+ Missing extension(s): ${missing.join(", ")}
128
+
129
+ Harper does not merge a custom config.yaml with defaults. Removing a top-level
130
+ extension silently disables that runtime surface and may only fail after deploy.
131
+ Re-add the missing extension(s), or document an intentional removal in
132
+ ${ALLOWLIST_PATH}:
133
+
134
+ {
135
+ "${CONFIG_PATH}": {
136
+ "allowedRemovedExtensions": ["${missing[0]}"]
137
+ }
138
+ }
139
+ `);
140
+ return BLOCKED;
141
+ };
142
+
143
+ process.exitCode = main();
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+ # This file is managed by Lisa.
3
+ # Do not edit directly - changes will be overwritten on the next `lisa` run.
4
+
5
+ # PostToolUse hook: after a harper-app/config.yaml edit, compare the edited
6
+ # extension set against HEAD and block silent removals. Harper does not merge a
7
+ # custom config.yaml with defaults, so removing a top-level extension can disable
8
+ # runtime surfaces without a build-time failure.
9
+
10
+ PLUGIN_ROOT=${CLAUDE_PLUGIN_ROOT:-}
11
+ if [ -z "$PLUGIN_ROOT" ]; then
12
+ PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
13
+ fi
14
+
15
+ if command -v bun >/dev/null 2>&1; then
16
+ exec bun "$PLUGIN_ROOT/hooks/enforce-config-extensions.mjs"
17
+ fi
18
+
19
+ exec node "$PLUGIN_ROOT/hooks/enforce-config-extensions.mjs"
@@ -12,6 +12,7 @@ These rules apply to Harper/Fabric component apps managed by Lisa.
12
12
 
13
13
  - TypeScript under `src/` is the source of truth for Harper resources, browser modules, shared libraries, and operational scripts.
14
14
  - `harper-app/config.yaml`, `harper-app/schema.graphql`, HTML, CSS, docs, and research fixtures are source assets.
15
+ - `harper-app/config.yaml` does not merge with Harper defaults. Keep every required top-level extension declared when editing it; the Harper Fabric hook blocks accidental extension drops unless the removal is documented in `.lisa/harper-config-extension-allowlist.json`.
15
16
  - `harper-app/resources.js` and `harper-app/web/**/*.js` are generated deploy artifacts. Never edit them directly; change the matching TypeScript and run `bun run build`.
16
17
  - Deployment, bootstrap, smoke, seed, verify, preview, token, crawl, ingest, and extraction commands must run from compiled JavaScript or generated Harper assets, not stale checked-in JavaScript.
17
18
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -20,6 +20,17 @@
20
20
  ]
21
21
  }
22
22
  ],
23
+ "postToolUse": [
24
+ {
25
+ "matcher": "Write|Edit|MultiEdit",
26
+ "hooks": [
27
+ {
28
+ "type": "command",
29
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/enforce-config-extensions.sh"
30
+ }
31
+ ]
32
+ }
33
+ ],
23
34
  "sessionStart": [
24
35
  {
25
36
  "matcher": "",
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { readFileSync } from "node:fs";
4
+ import { spawnSync } from "node:child_process";
5
+ import path from "node:path";
6
+
7
+ const BLOCKED = 2;
8
+ const ALLOWED = 0;
9
+ const CONFIG_PATH = "harper-app/config.yaml";
10
+ const ALLOWLIST_PATH = ".lisa/harper-config-extension-allowlist.json";
11
+
12
+ const readStdin = () => {
13
+ try {
14
+ return readFileSync(0, "utf8");
15
+ } catch {
16
+ return "";
17
+ }
18
+ };
19
+
20
+ const parseHookInput = raw => {
21
+ try {
22
+ return JSON.parse(raw);
23
+ } catch {
24
+ return {};
25
+ }
26
+ };
27
+
28
+ const normalizePath = filePath =>
29
+ filePath.replace(/\\/g, "/").replace(/^\.\//, "");
30
+
31
+ const isConfigPath = filePath => {
32
+ const normalized = normalizePath(filePath);
33
+ return normalized === CONFIG_PATH || normalized.endsWith(`/${CONFIG_PATH}`);
34
+ };
35
+
36
+ const repoRelativeConfigPath = filePath => {
37
+ const normalized = normalizePath(filePath);
38
+ const index = normalized.lastIndexOf(CONFIG_PATH);
39
+ return index === -1 ? normalized : normalized.slice(index);
40
+ };
41
+
42
+ const loadYaml = () => {
43
+ const require = createRequire(import.meta.url);
44
+ return require("js-yaml");
45
+ };
46
+
47
+ const topLevelExtensionKeys = yamlText => {
48
+ const yaml = loadYaml();
49
+ let parsed;
50
+ try {
51
+ parsed = yaml.load(yamlText);
52
+ } catch {
53
+ return null;
54
+ }
55
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
56
+ return Object.keys(parsed).sort();
57
+ };
58
+
59
+ const gitEnv = () =>
60
+ Object.fromEntries(
61
+ Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_"))
62
+ );
63
+
64
+ const readGitBlob = repoRoot => {
65
+ const result = spawnSync(
66
+ "git",
67
+ ["-C", repoRoot, "show", `HEAD:${CONFIG_PATH}`],
68
+ {
69
+ encoding: "utf8",
70
+ env: gitEnv(),
71
+ }
72
+ );
73
+ return result.status === 0 ? result.stdout : null;
74
+ };
75
+
76
+ const readAllowlist = (repoRoot, configPath) => {
77
+ const allowlistFile = path.join(repoRoot, ALLOWLIST_PATH);
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(readFileSync(allowlistFile, "utf8"));
81
+ } catch {
82
+ return new Set();
83
+ }
84
+
85
+ const entry = parsed?.[configPath] ?? parsed?.[CONFIG_PATH];
86
+ const values = Array.isArray(entry)
87
+ ? entry
88
+ : Array.isArray(entry?.allowedRemovedExtensions)
89
+ ? entry.allowedRemovedExtensions
90
+ : [];
91
+ return new Set(values.filter(value => typeof value === "string"));
92
+ };
93
+
94
+ const main = () => {
95
+ const input = parseHookInput(readStdin());
96
+ const filePath = input?.tool_input?.file_path;
97
+ if (typeof filePath !== "string" || !isConfigPath(filePath)) return ALLOWED;
98
+
99
+ const repoRoot = process.cwd();
100
+ const configPath = repoRelativeConfigPath(filePath);
101
+ let currentText;
102
+ try {
103
+ currentText = readFileSync(path.join(repoRoot, configPath), "utf8");
104
+ } catch {
105
+ return ALLOWED;
106
+ }
107
+
108
+ const previousText = readGitBlob(repoRoot);
109
+ if (previousText === null) return ALLOWED;
110
+
111
+ const previousExtensions = topLevelExtensionKeys(previousText);
112
+ const currentExtensionKeys = topLevelExtensionKeys(currentText);
113
+ if (previousExtensions === null || currentExtensionKeys === null)
114
+ return ALLOWED;
115
+ const currentExtensions = new Set(currentExtensionKeys);
116
+ const allowedRemovals = readAllowlist(repoRoot, configPath);
117
+ const missing = previousExtensions.filter(
118
+ extension =>
119
+ !currentExtensions.has(extension) && !allowedRemovals.has(extension)
120
+ );
121
+
122
+ if (missing.length === 0) return ALLOWED;
123
+
124
+ process.stderr
125
+ .write(`Blocked: harper-app/config.yaml dropped required Harper extension(s).
126
+
127
+ Missing extension(s): ${missing.join(", ")}
128
+
129
+ Harper does not merge a custom config.yaml with defaults. Removing a top-level
130
+ extension silently disables that runtime surface and may only fail after deploy.
131
+ Re-add the missing extension(s), or document an intentional removal in
132
+ ${ALLOWLIST_PATH}:
133
+
134
+ {
135
+ "${CONFIG_PATH}": {
136
+ "allowedRemovedExtensions": ["${missing[0]}"]
137
+ }
138
+ }
139
+ `);
140
+ return BLOCKED;
141
+ };
142
+
143
+ process.exitCode = main();
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+ # This file is managed by Lisa.
3
+ # Do not edit directly - changes will be overwritten on the next `lisa` run.
4
+
5
+ # PostToolUse hook: after a harper-app/config.yaml edit, compare the edited
6
+ # extension set against HEAD and block silent removals. Harper does not merge a
7
+ # custom config.yaml with defaults, so removing a top-level extension can disable
8
+ # runtime surfaces without a build-time failure.
9
+
10
+ PLUGIN_ROOT=${CLAUDE_PLUGIN_ROOT:-}
11
+ if [ -z "$PLUGIN_ROOT" ]; then
12
+ PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
13
+ fi
14
+
15
+ if command -v bun >/dev/null 2>&1; then
16
+ exec bun "$PLUGIN_ROOT/hooks/enforce-config-extensions.mjs"
17
+ fi
18
+
19
+ exec node "$PLUGIN_ROOT/hooks/enforce-config-extensions.mjs"
@@ -12,6 +12,7 @@ These rules apply to Harper/Fabric component apps managed by Lisa.
12
12
 
13
13
  - TypeScript under `src/` is the source of truth for Harper resources, browser modules, shared libraries, and operational scripts.
14
14
  - `harper-app/config.yaml`, `harper-app/schema.graphql`, HTML, CSS, docs, and research fixtures are source assets.
15
+ - `harper-app/config.yaml` does not merge with Harper defaults. Keep every required top-level extension declared when editing it; the Harper Fabric hook blocks accidental extension drops unless the removal is documented in `.lisa/harper-config-extension-allowlist.json`.
15
16
  - `harper-app/resources.js` and `harper-app/web/**/*.js` are generated deploy artifacts. Never edit them directly; change the matching TypeScript and run `bun run build`.
16
17
  - Deployment, bootstrap, smoke, seed, verify, preview, token, crawl, ingest, and extraction commands must run from compiled JavaScript or generated Harper assets, not stale checked-in JavaScript.
17
18
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"