@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
@@ -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"
@@ -6,6 +6,12 @@
6
6
  "command": "${CURSOR_PLUGIN_ROOT}/hooks/block-generated-artifact-edits.sh",
7
7
  "matcher": "Write|Edit|MultiEdit"
8
8
  }
9
+ ],
10
+ "postToolUse": [
11
+ {
12
+ "command": "${CURSOR_PLUGIN_ROOT}/hooks/enforce-config-extensions.sh",
13
+ "matcher": "Write|Edit|MultiEdit"
14
+ }
9
15
  ]
10
16
  }
11
17
  }
@@ -17,6 +17,7 @@ These rules apply to Harper/Fabric component apps managed by Lisa.
17
17
 
18
18
  - TypeScript under `src/` is the source of truth for Harper resources, browser modules, shared libraries, and operational scripts.
19
19
  - `harper-app/config.yaml`, `harper-app/schema.graphql`, HTML, CSS, docs, and research fixtures are source assets.
20
+ - `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`.
20
21
  - `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`.
21
22
  - 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.
22
23
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "NestJS-specific skills and migration write-protection hooks.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
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.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
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.162.0",
3
+ "version": "2.163.0",
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.162.0",
3
+ "version": "2.163.0",
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-phaser",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -13,6 +13,14 @@
13
13
  ]
14
14
  }
15
15
  ],
16
+ "PostToolUse": [
17
+ {
18
+ "matcher": "Write|Edit|MultiEdit",
19
+ "hooks": [
20
+ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/enforce-config-extensions.sh" }
21
+ ]
22
+ }
23
+ ],
16
24
  "SessionStart": [
17
25
  { "matcher": "", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/inject-rules.sh" }] }
18
26
  ],
@@ -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