@codyswann/lisa 2.162.0 → 2.163.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +12 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/hooks.json +11 -0
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/hooks/enforce-config-extensions.mjs +143 -0
- package/plugins/lisa-harper-fabric/hooks/enforce-config-extensions.sh +19 -0
- package/plugins/lisa-harper-fabric/rules/harper-fabric.md +1 -0
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +12 -1
- package/plugins/lisa-harper-fabric-copilot/hooks/enforce-config-extensions.mjs +143 -0
- package/plugins/lisa-harper-fabric-copilot/hooks/enforce-config-extensions.sh +19 -0
- package/plugins/lisa-harper-fabric-copilot/rules/harper-fabric.md +1 -0
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/hooks/enforce-config-extensions.mjs +143 -0
- package/plugins/lisa-harper-fabric-cursor/hooks/enforce-config-extensions.sh +19 -0
- package/plugins/lisa-harper-fabric-cursor/hooks/hooks.json +6 -0
- package/plugins/lisa-harper-fabric-cursor/rules/harper-fabric.mdc +1 -0
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/harper-fabric/.claude-plugin/plugin.json +8 -0
- package/plugins/src/harper-fabric/hooks/enforce-config-extensions.mjs +143 -0
- package/plugins/src/harper-fabric/hooks/enforce-config-extensions.sh +19 -0
- package/plugins/src/harper-fabric/rules/harper-fabric.md +1 -0
- package/typescript/copy-contents/.husky/pre-push +21 -9
|
@@ -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-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.163.1",
|
|
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.
|
|
3
|
+
"version": "2.163.1",
|
|
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.
|
|
3
|
+
"version": "2.163.1",
|
|
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.
|
|
3
|
+
"version": "2.163.1",
|
|
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.
|
|
3
|
+
"version": "2.163.1",
|
|
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"
|
|
@@ -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
|
|