@codyswann/lisa 1.94.0 → 1.96.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 (104) hide show
  1. package/dist/cli/index.d.ts.map +1 -1
  2. package/dist/cli/index.js +41 -5
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/codex/agent-installer.d.ts +56 -0
  5. package/dist/codex/agent-installer.d.ts.map +1 -0
  6. package/dist/codex/agent-installer.js +201 -0
  7. package/dist/codex/agent-installer.js.map +1 -0
  8. package/dist/codex/agent-transformer.d.ts +53 -0
  9. package/dist/codex/agent-transformer.d.ts.map +1 -0
  10. package/dist/codex/agent-transformer.js +181 -0
  11. package/dist/codex/agent-transformer.js.map +1 -0
  12. package/dist/codex/agents-md-installer.d.ts +24 -0
  13. package/dist/codex/agents-md-installer.d.ts.map +1 -0
  14. package/dist/codex/agents-md-installer.js +63 -0
  15. package/dist/codex/agents-md-installer.js.map +1 -0
  16. package/dist/codex/hooks-installer.d.ts +24 -0
  17. package/dist/codex/hooks-installer.d.ts.map +1 -0
  18. package/dist/codex/hooks-installer.js +206 -0
  19. package/dist/codex/hooks-installer.js.map +1 -0
  20. package/dist/codex/hooks-merger.d.ts +82 -0
  21. package/dist/codex/hooks-merger.d.ts.map +1 -0
  22. package/dist/codex/hooks-merger.js +127 -0
  23. package/dist/codex/hooks-merger.js.map +1 -0
  24. package/dist/codex/manifest.d.ts +32 -0
  25. package/dist/codex/manifest.d.ts.map +1 -0
  26. package/dist/codex/manifest.js +86 -0
  27. package/dist/codex/manifest.js.map +1 -0
  28. package/dist/codex/settings-installer.d.ts +48 -0
  29. package/dist/codex/settings-installer.d.ts.map +1 -0
  30. package/dist/codex/settings-installer.js +276 -0
  31. package/dist/codex/settings-installer.js.map +1 -0
  32. package/dist/codex/skills-installer.d.ts +46 -0
  33. package/dist/codex/skills-installer.d.ts.map +1 -0
  34. package/dist/codex/skills-installer.js +344 -0
  35. package/dist/codex/skills-installer.js.map +1 -0
  36. package/dist/core/config.d.ts +19 -0
  37. package/dist/core/config.d.ts.map +1 -1
  38. package/dist/core/config.js +13 -0
  39. package/dist/core/config.js.map +1 -1
  40. package/dist/core/lisa.d.ts +12 -0
  41. package/dist/core/lisa.d.ts.map +1 -1
  42. package/dist/core/lisa.js +48 -0
  43. package/dist/core/lisa.js.map +1 -1
  44. package/dist/core/project-config.d.ts +49 -0
  45. package/dist/core/project-config.d.ts.map +1 -0
  46. package/dist/core/project-config.js +119 -0
  47. package/dist/core/project-config.js.map +1 -0
  48. package/package.json +3 -1
  49. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa/agents/jira-agent.md +21 -8
  51. package/plugins/lisa/agents/jira-build-intake.md +58 -0
  52. package/plugins/lisa/agents/notion-prd-intake.md +57 -0
  53. package/plugins/lisa/commands/jira/build-intake.md +7 -0
  54. package/plugins/lisa/commands/jira/source-artifacts.md +6 -0
  55. package/plugins/lisa/commands/jira/validate-ticket.md +7 -0
  56. package/plugins/lisa/commands/notion-prd-intake.md +7 -0
  57. package/plugins/lisa/commands/prd-ticket-coverage.md +7 -0
  58. package/plugins/lisa/commands/product-walkthrough.md +7 -0
  59. package/plugins/lisa/rules/base-rules.md +9 -1
  60. package/plugins/lisa/skills/jira-build-intake/SKILL.md +134 -0
  61. package/plugins/lisa/skills/jira-create/SKILL.md +53 -30
  62. package/plugins/lisa/skills/jira-source-artifacts/SKILL.md +107 -0
  63. package/plugins/lisa/skills/jira-validate-ticket/SKILL.md +224 -0
  64. package/plugins/lisa/skills/jira-verify/SKILL.md +15 -35
  65. package/plugins/lisa/skills/jira-write-ticket/SKILL.md +72 -19
  66. package/plugins/lisa/skills/notion-prd-intake/SKILL.md +169 -0
  67. package/plugins/lisa/skills/notion-to-jira/SKILL.md +137 -95
  68. package/plugins/lisa/skills/prd-ticket-coverage/SKILL.md +137 -0
  69. package/plugins/lisa/skills/product-walkthrough/SKILL.md +129 -0
  70. package/plugins/lisa/skills/ticket-triage/SKILL.md +19 -2
  71. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-expo/skills/jira-create/SKILL.md +60 -28
  74. package/plugins/lisa-expo/skills/jira-verify/SKILL.md +14 -34
  75. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-rails/skills/jira-create/SKILL.md +59 -28
  78. package/plugins/lisa-rails/skills/jira-verify/SKILL.md +13 -16
  79. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  80. package/plugins/src/base/agents/jira-agent.md +21 -8
  81. package/plugins/src/base/agents/jira-build-intake.md +58 -0
  82. package/plugins/src/base/agents/notion-prd-intake.md +57 -0
  83. package/plugins/src/base/commands/jira/build-intake.md +7 -0
  84. package/plugins/src/base/commands/jira/source-artifacts.md +6 -0
  85. package/plugins/src/base/commands/jira/validate-ticket.md +7 -0
  86. package/plugins/src/base/commands/notion-prd-intake.md +7 -0
  87. package/plugins/src/base/commands/prd-ticket-coverage.md +7 -0
  88. package/plugins/src/base/commands/product-walkthrough.md +7 -0
  89. package/plugins/src/base/rules/base-rules.md +9 -1
  90. package/plugins/src/base/skills/jira-build-intake/SKILL.md +134 -0
  91. package/plugins/src/base/skills/jira-create/SKILL.md +53 -30
  92. package/plugins/src/base/skills/jira-source-artifacts/SKILL.md +107 -0
  93. package/plugins/src/base/skills/jira-validate-ticket/SKILL.md +224 -0
  94. package/plugins/src/base/skills/jira-verify/SKILL.md +15 -35
  95. package/plugins/src/base/skills/jira-write-ticket/SKILL.md +72 -19
  96. package/plugins/src/base/skills/notion-prd-intake/SKILL.md +169 -0
  97. package/plugins/src/base/skills/notion-to-jira/SKILL.md +137 -95
  98. package/plugins/src/base/skills/prd-ticket-coverage/SKILL.md +137 -0
  99. package/plugins/src/base/skills/product-walkthrough/SKILL.md +129 -0
  100. package/plugins/src/base/skills/ticket-triage/SKILL.md +19 -2
  101. package/plugins/src/expo/skills/jira-create/SKILL.md +60 -28
  102. package/plugins/src/expo/skills/jira-verify/SKILL.md +14 -34
  103. package/plugins/src/rails/skills/jira-create/SKILL.md +59 -28
  104. package/plugins/src/rails/skills/jira-verify/SKILL.md +13 -16
@@ -0,0 +1,24 @@
1
+ /** Filename of the Codex project doc at the host project root */
2
+ export declare const AGENTS_MD_FILENAME = "AGENTS.md";
3
+ /** Result of the AGENTS.md install pass */
4
+ export interface AgentsMdInstallResult {
5
+ /** True if Lisa created the file (false if it already existed) */
6
+ readonly created: boolean;
7
+ /** Path written, relative to the host project root (or empty if no-op) */
8
+ readonly relativePath: string | undefined;
9
+ }
10
+ /**
11
+ * Write a starter AGENTS.md template at the host project root, but only if
12
+ * the file doesn't already exist (create-only semantics).
13
+ * @param destDir - Absolute path to the host project root
14
+ * @returns Result describing whether a file was created
15
+ */
16
+ export declare function installAgentsMd(destDir: string): Promise<AgentsMdInstallResult>;
17
+ /**
18
+ * Starter template content. Intentionally short — the heavy lifting (Lisa
19
+ * rules, intent routing, orchestration) lives in `.codex/lisa-rules/` and
20
+ * is injected via the SessionStart hook, so AGENTS.md is reserved for
21
+ * host-specific notes.
22
+ */
23
+ export declare const AGENTS_MD_TEMPLATE = "# Project Guidance for Codex\n\nThis project uses Lisa governance. Codex sessions automatically receive\nLisa's rules, agents, and skills via the SessionStart hook in\n`.codex/hooks/lisa/inject-rules.sh`.\n\nThis file is for **project-specific guidance** \u2014 add anything Codex should\nknow about *this particular* project that isn't covered by Lisa's rules.\n\n## What lives where\n\n- `.codex/agents/lisa/` \u2014 Lisa-managed subagent role definitions\n- `.codex/skills/lisa/` \u2014 Lisa-managed skills (invoke via `$<name>`)\n- `.codex/lisa-rules/` \u2014 Lisa rules content (injected at session start)\n- `.codex/hooks/lisa/` \u2014 Lisa-managed hook scripts\n- `.codex/config.toml` \u2014 partly Lisa-managed Codex config\n\n## Custom rules for this project\n\nAdd project-specific guidance below. This section is preserved across Lisa\nupdates (`AGENTS.md` is create-only).\n";
24
+ //# sourceMappingURL=agents-md-installer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents-md-installer.d.ts","sourceRoot":"","sources":["../../src/codex/agents-md-installer.ts"],"names":[],"mappings":"AAmBA,iEAAiE;AACjE,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAE9C,2CAA2C;AAC3C,MAAM,WAAW,qBAAqB;IACpC,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3C;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,qBAAqB,CAAC,CAOhC;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,23BAqB9B,CAAC"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Emit a create-only AGENTS.md template into the host project root.
3
+ *
4
+ * Codex auto-loads `AGENTS.md` from the project tree on every session
5
+ * (per `developers.openai.com/codex/guides/agents-md`). Lisa ships a
6
+ * starter template so hosts know:
7
+ * - Lisa governance is active in this project (rules injected via
8
+ * SessionStart hook from `.codex/lisa-rules/`)
9
+ * - This file is the place to add project-specific guidance
10
+ *
11
+ * Create-only: never overwritten on subsequent `lisa` runs. The host owns
12
+ * the file once it exists. (Lisa's actual rules go via the inject-rules
13
+ * hook, so AGENTS.md is purely a host-facing knob.)
14
+ * @module codex/agents-md-installer
15
+ */
16
+ import * as fse from "fs-extra";
17
+ import { writeFile } from "node:fs/promises";
18
+ import * as path from "node:path";
19
+ /** Filename of the Codex project doc at the host project root */
20
+ export const AGENTS_MD_FILENAME = "AGENTS.md";
21
+ /**
22
+ * Write a starter AGENTS.md template at the host project root, but only if
23
+ * the file doesn't already exist (create-only semantics).
24
+ * @param destDir - Absolute path to the host project root
25
+ * @returns Result describing whether a file was created
26
+ */
27
+ export async function installAgentsMd(destDir) {
28
+ const filePath = path.join(destDir, AGENTS_MD_FILENAME);
29
+ if (await fse.pathExists(filePath)) {
30
+ return { created: false, relativePath: undefined };
31
+ }
32
+ await writeFile(filePath, AGENTS_MD_TEMPLATE, "utf8");
33
+ return { created: true, relativePath: AGENTS_MD_FILENAME };
34
+ }
35
+ /**
36
+ * Starter template content. Intentionally short — the heavy lifting (Lisa
37
+ * rules, intent routing, orchestration) lives in `.codex/lisa-rules/` and
38
+ * is injected via the SessionStart hook, so AGENTS.md is reserved for
39
+ * host-specific notes.
40
+ */
41
+ export const AGENTS_MD_TEMPLATE = `# Project Guidance for Codex
42
+
43
+ This project uses Lisa governance. Codex sessions automatically receive
44
+ Lisa's rules, agents, and skills via the SessionStart hook in
45
+ \`.codex/hooks/lisa/inject-rules.sh\`.
46
+
47
+ This file is for **project-specific guidance** — add anything Codex should
48
+ know about *this particular* project that isn't covered by Lisa's rules.
49
+
50
+ ## What lives where
51
+
52
+ - \`.codex/agents/lisa/\` — Lisa-managed subagent role definitions
53
+ - \`.codex/skills/lisa/\` — Lisa-managed skills (invoke via \`$<name>\`)
54
+ - \`.codex/lisa-rules/\` — Lisa rules content (injected at session start)
55
+ - \`.codex/hooks/lisa/\` — Lisa-managed hook scripts
56
+ - \`.codex/config.toml\` — partly Lisa-managed Codex config
57
+
58
+ ## Custom rules for this project
59
+
60
+ Add project-specific guidance below. This section is preserved across Lisa
61
+ updates (\`AGENTS.md\` is create-only).
62
+ `;
63
+ //# sourceMappingURL=agents-md-installer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents-md-installer.js","sourceRoot":"","sources":["../../src/codex/agents-md-installer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,iEAAiE;AACjE,MAAM,CAAC,MAAM,kBAAkB,GAAG,WAAW,CAAC;AAU9C;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAAe;IAEf,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,kBAAkB,CAAC,CAAC;IACxD,IAAI,MAAM,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC;IACrD,CAAC;IACD,MAAM,SAAS,CAAC,QAAQ,EAAE,kBAAkB,EAAE,MAAM,CAAC,CAAC;IACtD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,kBAAkB,EAAE,CAAC;AAC7D,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;CAqBjC,CAAC"}
@@ -0,0 +1,24 @@
1
+ import type { ProjectType } from "../core/config.js";
2
+ /** Subdirectory inside `.codex/` for Lisa-managed hook scripts */
3
+ export declare const LISA_HOOKS_SUBDIR: string;
4
+ /** Subdirectory inside `.codex/` for Lisa rules content (read by inject-rules) */
5
+ export declare const LISA_RULES_SUBDIR = "lisa-rules";
6
+ /** Filename of the Codex hooks config file inside `.codex/` */
7
+ export declare const HOOKS_FILENAME = "hooks.json";
8
+ /** Result of the hooks install pass */
9
+ export interface HooksInstallResult {
10
+ /** Files written, relative to `.codex/`. Used to update the manifest. */
11
+ readonly managedFiles: readonly string[];
12
+ /** Number of Lisa hook entries written into hooks.json */
13
+ readonly hookEntries: number;
14
+ }
15
+ /**
16
+ * Install Lisa hook scripts + rules + hooks.json entries.
17
+ * @param lisaDir - Absolute path to the Lisa repo root (or installed package)
18
+ * @param destDir - Absolute path to the host project root
19
+ * @param detectedTypes - Project types Lisa detected; used to filter stack-
20
+ * specific hooks. Always includes the universal hooks regardless.
21
+ * @returns Result describing what was written
22
+ */
23
+ export declare function installHooks(lisaDir: string, destDir: string, detectedTypes: readonly ProjectType[]): Promise<HooksInstallResult>;
24
+ //# sourceMappingURL=hooks-installer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks-installer.d.ts","sourceRoot":"","sources":["../../src/codex/hooks-installer.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAUrD,kEAAkE;AAClE,eAAO,MAAM,iBAAiB,QAA6B,CAAC;AAE5D,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,eAAe,CAAC;AAE9C,+DAA+D;AAC/D,eAAO,MAAM,cAAc,eAAe,CAAC;AAuF3C,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IACjC,yEAAyE;IACzE,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,0DAA0D;IAC1D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,SAAS,WAAW,EAAE,GACpC,OAAO,CAAC,kBAAkB,CAAC,CA2C7B"}
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Install Lisa-managed Codex hooks into a host project.
3
+ *
4
+ * Pipeline:
5
+ * 1. Filter hook catalog by detected project types
6
+ * 2. Copy each script from `src/codex/scripts/` → `.codex/hooks/lisa/`
7
+ * 3. For inject-rules: also mirror Lisa rules into `.codex/lisa-rules/`
8
+ * 4. Tagged-merge `.codex/hooks.json`
9
+ *
10
+ * Codex hook event support map (vs. Lisa's existing Claude Code hooks):
11
+ * - SessionStart, PreToolUse, PostToolUse, UserPromptSubmit, Stop ✅
12
+ * - PermissionRequest ✅ (Codex-only)
13
+ * - SubagentStart, SessionEnd, Notification, PreCompact ❌ (Codex doesn't have these)
14
+ * @module codex/hooks-installer
15
+ */
16
+ import * as fse from "fs-extra";
17
+ import { chmod, copyFile, readFile, readdir, writeFile, } from "node:fs/promises";
18
+ import * as path from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { mergeLisaHooks, parseHooksFile, serializeHooksFile, } from "./hooks-merger.js";
21
+ /** Subdirectory inside `.codex/` for Lisa-managed hook scripts */
22
+ export const LISA_HOOKS_SUBDIR = path.join("hooks", "lisa");
23
+ /** Subdirectory inside `.codex/` for Lisa rules content (read by inject-rules) */
24
+ export const LISA_RULES_SUBDIR = "lisa-rules";
25
+ /** Filename of the Codex hooks config file inside `.codex/` */
26
+ export const HOOKS_FILENAME = "hooks.json";
27
+ /**
28
+ * Matcher regex shared by every PostToolUse/PreToolUse hook that fires on
29
+ * a file write. Codex emits these tool names for filesystem edits across
30
+ * its three write paths (text Edit, file Write, apply_patch diff).
31
+ */
32
+ const WRITE_MATCHER = "Edit|Write|apply_patch";
33
+ /**
34
+ * Hook catalog. Adding a new hook? Three steps:
35
+ * 1. Drop the script into `src/codex/scripts/<filename>`
36
+ * 2. Add an entry here
37
+ * 3. Add tests in `tests/unit/codex/hooks-installer.test.ts`
38
+ *
39
+ * Stack-specific hooks are gated by `forProjectTypes` so they're only shipped
40
+ * when the relevant project type is detected.
41
+ */
42
+ const HOOK_CATALOG = [
43
+ {
44
+ id: "inject-rules",
45
+ event: "SessionStart",
46
+ matcher: "",
47
+ scriptFilename: "inject-rules.sh",
48
+ forProjectTypes: ["*"],
49
+ statusMessage: "Injecting Lisa rules into session context",
50
+ },
51
+ {
52
+ id: "notify-ntfy",
53
+ event: "Stop",
54
+ matcher: "",
55
+ scriptFilename: "notify-ntfy.sh",
56
+ forProjectTypes: ["*"],
57
+ },
58
+ {
59
+ id: "format-on-edit",
60
+ event: "PostToolUse",
61
+ matcher: WRITE_MATCHER,
62
+ scriptFilename: "format-on-edit.sh",
63
+ forProjectTypes: ["typescript"],
64
+ },
65
+ {
66
+ id: "lint-on-edit",
67
+ event: "PostToolUse",
68
+ matcher: WRITE_MATCHER,
69
+ scriptFilename: "lint-on-edit.sh",
70
+ forProjectTypes: ["typescript"],
71
+ },
72
+ {
73
+ id: "sg-scan-on-edit",
74
+ event: "PostToolUse",
75
+ matcher: WRITE_MATCHER,
76
+ scriptFilename: "sg-scan-on-edit.sh",
77
+ forProjectTypes: ["typescript"],
78
+ },
79
+ {
80
+ id: "rubocop-on-edit",
81
+ event: "PostToolUse",
82
+ matcher: WRITE_MATCHER,
83
+ scriptFilename: "rubocop-on-edit.sh",
84
+ forProjectTypes: ["rails"],
85
+ },
86
+ {
87
+ id: "block-migration-edits",
88
+ event: "PreToolUse",
89
+ matcher: WRITE_MATCHER,
90
+ scriptFilename: "block-migration-edits.sh",
91
+ forProjectTypes: ["nestjs"],
92
+ },
93
+ ];
94
+ /**
95
+ * Install Lisa hook scripts + rules + hooks.json entries.
96
+ * @param lisaDir - Absolute path to the Lisa repo root (or installed package)
97
+ * @param destDir - Absolute path to the host project root
98
+ * @param detectedTypes - Project types Lisa detected; used to filter stack-
99
+ * specific hooks. Always includes the universal hooks regardless.
100
+ * @returns Result describing what was written
101
+ */
102
+ export async function installHooks(lisaDir, destDir, detectedTypes) {
103
+ const codexDir = path.join(destDir, ".codex");
104
+ const hooksDir = path.join(codexDir, LISA_HOOKS_SUBDIR);
105
+ const rulesDir = path.join(codexDir, LISA_RULES_SUBDIR);
106
+ await fse.ensureDir(hooksDir);
107
+ await fse.ensureDir(rulesDir);
108
+ const applicable = filterCatalogByTypes(detectedTypes);
109
+ // Step 1: copy every applicable script and collect their relative paths
110
+ const scriptFiles = await Promise.all(applicable.map(async (entry) => {
111
+ const scriptSource = resolveBundledScript(entry.scriptFilename);
112
+ const scriptDest = path.join(hooksDir, entry.scriptFilename);
113
+ await copyFile(scriptSource, scriptDest);
114
+ await chmod(scriptDest, 0o755);
115
+ return path.join(LISA_HOOKS_SUBDIR, entry.scriptFilename);
116
+ }));
117
+ // Step 2: mirror rules from Lisa into .codex/lisa-rules/ (only when
118
+ // inject-rules is being installed — i.e., always, since it's a "*" hook)
119
+ const ruleFiles = applicable.some(e => e.id === "inject-rules")
120
+ ? (await mirrorRules(lisaDir, rulesDir)).map(file => path.join(LISA_RULES_SUBDIR, file))
121
+ : [];
122
+ // Step 3: tagged-merge hooks.json
123
+ const hooksFilePath = path.join(codexDir, HOOKS_FILENAME);
124
+ const existing = await readHooksFile(hooksFilePath);
125
+ const lisaHookSpecs = applicable.map(entry => catalogEntryToSpec(entry, destDir));
126
+ const merged = mergeLisaHooks(existing, lisaHookSpecs);
127
+ await writeFile(hooksFilePath, serializeHooksFile(merged), "utf8");
128
+ return {
129
+ managedFiles: Object.freeze([...scriptFiles, ...ruleFiles, HOOKS_FILENAME]),
130
+ hookEntries: lisaHookSpecs.length,
131
+ };
132
+ }
133
+ /**
134
+ * Filter the catalog by detected project types. Universal hooks (`"*"`)
135
+ * always pass; stack-specific hooks pass only if their type is detected.
136
+ * @param detectedTypes - Project types Lisa detected for the host
137
+ * @returns The catalog entries that apply to this host
138
+ */
139
+ function filterCatalogByTypes(detectedTypes) {
140
+ const detectedSet = new Set(detectedTypes);
141
+ return HOOK_CATALOG.filter(entry => entry.forProjectTypes.some(t => t === "*" || detectedSet.has(t)));
142
+ }
143
+ /**
144
+ * Convert a catalog entry into the LisaHookSpec the merger consumes.
145
+ *
146
+ * The script path resolves at hook-firing time via
147
+ * `git rev-parse --show-toplevel` because Codex sets the hook script's cwd
148
+ * to the session cwd, not the repo root. Falls back to `pwd` if not in a git
149
+ * repo (rare).
150
+ * @param entry - One catalog entry to translate
151
+ * @param _destDir - Reserved for future per-host customization; currently unused
152
+ * @returns A LisaHookSpec ready to feed into mergeLisaHooks
153
+ */
154
+ function catalogEntryToSpec(entry, _destDir) {
155
+ const command = `bash "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.codex/${LISA_HOOKS_SUBDIR}/${entry.scriptFilename}"`;
156
+ return {
157
+ id: entry.id,
158
+ event: entry.event,
159
+ matcher: entry.matcher,
160
+ command,
161
+ ...(entry.statusMessage !== undefined
162
+ ? { statusMessage: entry.statusMessage }
163
+ : {}),
164
+ };
165
+ }
166
+ /**
167
+ * Read an existing hooks.json file, returning {} if absent.
168
+ * @param hooksFilePath - Absolute path to `<destDir>/.codex/hooks.json`
169
+ * @returns Parsed HooksFile (empty object if the file doesn't exist)
170
+ */
171
+ async function readHooksFile(hooksFilePath) {
172
+ if (!(await fse.pathExists(hooksFilePath))) {
173
+ return {};
174
+ }
175
+ const raw = await readFile(hooksFilePath, "utf8");
176
+ return parseHooksFile(raw);
177
+ }
178
+ /**
179
+ * Resolve a bundled script path. Scripts ship inside the Lisa source tree at
180
+ * `src/codex/scripts/<name>` and are accessible at runtime by computing the
181
+ * path relative to this module's URL.
182
+ * @param filename - Script filename (e.g. "inject-rules.sh")
183
+ * @returns Absolute path to the bundled script in the Lisa install
184
+ */
185
+ function resolveBundledScript(filename) {
186
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
187
+ return path.join(moduleDir, "scripts", filename);
188
+ }
189
+ /**
190
+ * Copy every .md file from Lisa's `plugins/lisa/rules/` into the host's
191
+ * `.codex/lisa-rules/`. Returns the list of filenames copied (without
192
+ * directory).
193
+ * @param lisaDir - Absolute path to the Lisa repo / installed package
194
+ * @param rulesDestDir - Absolute path to `<destDir>/.codex/lisa-rules/`
195
+ * @returns Filenames (without directory) of every rule .md file copied
196
+ */
197
+ async function mirrorRules(lisaDir, rulesDestDir) {
198
+ const rulesSourceDir = path.join(lisaDir, "plugins", "lisa", "rules");
199
+ if (!(await fse.pathExists(rulesSourceDir))) {
200
+ return [];
201
+ }
202
+ const files = (await readdir(rulesSourceDir)).filter(name => name.endsWith(".md"));
203
+ await Promise.all(files.map(file => copyFile(path.join(rulesSourceDir, file), path.join(rulesDestDir, file))));
204
+ return Object.freeze(files);
205
+ }
206
+ //# sourceMappingURL=hooks-installer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks-installer.js","sourceRoot":"","sources":["../../src/codex/hooks-installer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Tagged-merge writer for `.codex/hooks.json`.
3
+ *
4
+ * Codex's hooks parser is not configured with `deny_unknown_fields` (verified
5
+ * against `codex-rs/config/src/hook_config.rs`), so Lisa can attach
6
+ * `_lisaManaged: true` and `_lisaId: "..."` marker fields to individual hook
7
+ * entries. On every install the merger:
8
+ *
9
+ * 1. Reads the existing hooks.json (if any)
10
+ * 2. Strips every entry where `_lisaManaged === true`
11
+ * 3. Appends the current Lisa hook set with markers attached
12
+ * 4. Preserves all entries the host added (no `_lisaManaged` marker)
13
+ *
14
+ * This mirrors the pattern Lisa uses for `enabledPlugins` deep-merge in
15
+ * `.claude/settings.json` — ownership is keyed on a marker field instead of
16
+ * a key path.
17
+ * @module codex/hooks-merger
18
+ */
19
+ /** Markers attached to every Lisa-owned hook entry */
20
+ export declare const LISA_MANAGED_MARKER: "_lisaManaged";
21
+ export declare const LISA_ID_MARKER: "_lisaId";
22
+ /** A single hook handler entry (innermost: type/command/timeout/etc.) */
23
+ export interface HookHandler {
24
+ readonly type: string;
25
+ readonly command?: string;
26
+ readonly timeout?: number;
27
+ readonly statusMessage?: string;
28
+ /** Lisa ownership marker (set on Lisa-owned entries only) */
29
+ readonly [LISA_MANAGED_MARKER]?: boolean;
30
+ /** Stable identifier so logs can name the hook */
31
+ readonly [LISA_ID_MARKER]?: string;
32
+ }
33
+ /** A matcher group: a regex matcher + an array of handlers it triggers */
34
+ export interface MatcherGroup {
35
+ readonly matcher: string;
36
+ readonly hooks: readonly HookHandler[];
37
+ }
38
+ /** All hook events Codex understands */
39
+ export type CodexHookEvent = "PreToolUse" | "PostToolUse" | "UserPromptSubmit" | "Stop" | "SessionStart" | "PermissionRequest";
40
+ /** The shape of `.codex/hooks.json` */
41
+ export interface HooksFile {
42
+ readonly hooks?: Readonly<Partial<Record<CodexHookEvent, readonly MatcherGroup[]>>>;
43
+ }
44
+ /** A Lisa-owned hook to be installed (mutable input — markers added by merger) */
45
+ export interface LisaHookSpec {
46
+ /** Stable identifier; goes into `_lisaId` so logs can name the hook */
47
+ readonly id: string;
48
+ /** Codex hook event the entry applies to */
49
+ readonly event: CodexHookEvent;
50
+ /** Matcher regex for `matcher` field (use "" for events that don't match) */
51
+ readonly matcher: string;
52
+ /** Shell command to run */
53
+ readonly command: string;
54
+ /** Optional timeout in seconds (Codex default is 600) */
55
+ readonly timeout?: number;
56
+ /** Optional human-readable status message */
57
+ readonly statusMessage?: string;
58
+ }
59
+ /**
60
+ * Merge Lisa's current hook set into an existing hooks.json document,
61
+ * preserving any host-authored entries.
62
+ * @param existing - Parsed hooks.json (or {} if none)
63
+ * @param lisaHooks - Lisa-owned hook specs to install
64
+ * @returns New HooksFile object with Lisa entries replaced and host entries preserved
65
+ */
66
+ export declare function mergeLisaHooks(existing: HooksFile, lisaHooks: readonly LisaHookSpec[]): HooksFile;
67
+ /**
68
+ * Serialize a HooksFile to JSON suitable for writing to disk. Two-space
69
+ * indent, trailing newline.
70
+ * @param file - Hooks file to serialize
71
+ * @returns JSON string ending with a newline
72
+ */
73
+ export declare function serializeHooksFile(file: HooksFile): string;
74
+ /**
75
+ * Type guard: parse raw JSON into a HooksFile, validating only the shape we
76
+ * touch. We do NOT reject unknown fields since Codex's parser allows them
77
+ * and we want to preserve host extensions on round-trip.
78
+ * @param raw - Untrusted JSON string from disk
79
+ * @returns Parsed HooksFile
80
+ */
81
+ export declare function parseHooksFile(raw: string): HooksFile;
82
+ //# sourceMappingURL=hooks-merger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks-merger.d.ts","sourceRoot":"","sources":["../../src/codex/hooks-merger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,EAAG,cAAuB,CAAC;AAC3D,eAAO,MAAM,cAAc,EAAG,SAAkB,CAAC;AAEjD,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,6DAA6D;IAC7D,QAAQ,CAAC,CAAC,mBAAmB,CAAC,CAAC,EAAE,OAAO,CAAC;IACzC,kDAAkD;IAClD,QAAQ,CAAC,CAAC,cAAc,CAAC,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;CACxC;AAED,wCAAwC;AACxC,MAAM,MAAM,cAAc,GACtB,YAAY,GACZ,aAAa,GACb,kBAAkB,GAClB,MAAM,GACN,cAAc,GACd,mBAAmB,CAAC;AAExB,uCAAuC;AACvC,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CACvB,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,SAAS,YAAY,EAAE,CAAC,CAAC,CACzD,CAAC;CACH;AAED,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,2BAA2B;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yDAAyD;IACzD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,6CAA6C;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,SAAS,EACnB,SAAS,EAAE,SAAS,YAAY,EAAE,GACjC,SAAS,CAuCX;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAE1D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAgBrD"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Tagged-merge writer for `.codex/hooks.json`.
3
+ *
4
+ * Codex's hooks parser is not configured with `deny_unknown_fields` (verified
5
+ * against `codex-rs/config/src/hook_config.rs`), so Lisa can attach
6
+ * `_lisaManaged: true` and `_lisaId: "..."` marker fields to individual hook
7
+ * entries. On every install the merger:
8
+ *
9
+ * 1. Reads the existing hooks.json (if any)
10
+ * 2. Strips every entry where `_lisaManaged === true`
11
+ * 3. Appends the current Lisa hook set with markers attached
12
+ * 4. Preserves all entries the host added (no `_lisaManaged` marker)
13
+ *
14
+ * This mirrors the pattern Lisa uses for `enabledPlugins` deep-merge in
15
+ * `.claude/settings.json` — ownership is keyed on a marker field instead of
16
+ * a key path.
17
+ * @module codex/hooks-merger
18
+ */
19
+ /** Markers attached to every Lisa-owned hook entry */
20
+ export const LISA_MANAGED_MARKER = "_lisaManaged";
21
+ export const LISA_ID_MARKER = "_lisaId";
22
+ /**
23
+ * Merge Lisa's current hook set into an existing hooks.json document,
24
+ * preserving any host-authored entries.
25
+ * @param existing - Parsed hooks.json (or {} if none)
26
+ * @param lisaHooks - Lisa-owned hook specs to install
27
+ * @returns New HooksFile object with Lisa entries replaced and host entries preserved
28
+ */
29
+ export function mergeLisaHooks(existing, lisaHooks) {
30
+ // Step 1: drop every matcher-group that's pure-Lisa, strip Lisa handlers
31
+ // out of mixed-ownership groups, keep purely-host groups unchanged.
32
+ const existingEvents = Object.keys(existing.hooks ?? {});
33
+ const hostOnly = Object.fromEntries(existingEvents
34
+ .map(event => {
35
+ const filtered = stripLisaHandlers(existing.hooks?.[event] ?? []);
36
+ return [event, filtered];
37
+ })
38
+ .filter(([, groups]) => groups.length > 0));
39
+ // Step 2: append Lisa's current hook set, grouping by event
40
+ const lisaByEvent = lisaHooks.reduce((acc, spec) => {
41
+ const existingGroups = acc[spec.event] ?? [];
42
+ return {
43
+ ...acc,
44
+ [spec.event]: [...existingGroups, lisaSpecToMatcherGroup(spec)],
45
+ };
46
+ }, {});
47
+ // Step 3: combine — for events touched by both, host first then Lisa
48
+ const allEvents = new Set([
49
+ ...Object.keys(hostOnly),
50
+ ...Object.keys(lisaByEvent),
51
+ ]);
52
+ const merged = Object.fromEntries(Array.from(allEvents).map(event => [
53
+ event,
54
+ [...(hostOnly[event] ?? []), ...(lisaByEvent[event] ?? [])],
55
+ ]));
56
+ return { hooks: merged };
57
+ }
58
+ /**
59
+ * Serialize a HooksFile to JSON suitable for writing to disk. Two-space
60
+ * indent, trailing newline.
61
+ * @param file - Hooks file to serialize
62
+ * @returns JSON string ending with a newline
63
+ */
64
+ export function serializeHooksFile(file) {
65
+ return `${JSON.stringify(file, null, 2)}\n`;
66
+ }
67
+ /**
68
+ * Type guard: parse raw JSON into a HooksFile, validating only the shape we
69
+ * touch. We do NOT reject unknown fields since Codex's parser allows them
70
+ * and we want to preserve host extensions on round-trip.
71
+ * @param raw - Untrusted JSON string from disk
72
+ * @returns Parsed HooksFile
73
+ */
74
+ export function parseHooksFile(raw) {
75
+ if (raw.trim().length === 0) {
76
+ return {};
77
+ }
78
+ const parsed = JSON.parse(raw);
79
+ if (parsed === null || typeof parsed !== "object") {
80
+ throw new Error("hooks.json must contain a JSON object at the root");
81
+ }
82
+ const obj = parsed;
83
+ if (obj.hooks === undefined) {
84
+ return {};
85
+ }
86
+ if (typeof obj.hooks !== "object" || obj.hooks === null) {
87
+ throw new Error("hooks.json: 'hooks' field must be an object");
88
+ }
89
+ return parsed;
90
+ }
91
+ /**
92
+ * Drop every Lisa-managed handler from a list of matcher groups. Groups
93
+ * whose only handlers were Lisa-managed are removed entirely. Groups with
94
+ * mixed ownership keep their host handlers.
95
+ * @param groups - Existing matcher groups for one event
96
+ * @returns The same groups with Lisa-managed handlers removed (empty groups dropped)
97
+ */
98
+ function stripLisaHandlers(groups) {
99
+ return groups
100
+ .map(group => ({
101
+ matcher: group.matcher,
102
+ hooks: group.hooks.filter(handler => handler[LISA_MANAGED_MARKER] !== true),
103
+ }))
104
+ .filter(group => group.hooks.length > 0);
105
+ }
106
+ /**
107
+ * Convert a LisaHookSpec into a MatcherGroup with markers attached.
108
+ * @param spec - One Lisa hook spec to install
109
+ * @returns A matcher group containing one Lisa-marked handler
110
+ */
111
+ function lisaSpecToMatcherGroup(spec) {
112
+ const handler = {
113
+ type: "command",
114
+ command: spec.command,
115
+ [LISA_MANAGED_MARKER]: true,
116
+ [LISA_ID_MARKER]: spec.id,
117
+ ...(spec.timeout !== undefined ? { timeout: spec.timeout } : {}),
118
+ ...(spec.statusMessage !== undefined
119
+ ? { statusMessage: spec.statusMessage }
120
+ : {}),
121
+ };
122
+ return {
123
+ matcher: spec.matcher,
124
+ hooks: [handler],
125
+ };
126
+ }
127
+ //# sourceMappingURL=hooks-merger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks-merger.js","sourceRoot":"","sources":["../../src/codex/hooks-merger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /** Filename of the Lisa-managed manifest, relative to `.codex/` */
2
+ export declare const LISA_MANAGED_MANIFEST_FILENAME = ".lisa-managed.json";
3
+ /** Schema of `.codex/.lisa-managed.json` */
4
+ export interface LisaManagedManifest {
5
+ /**
6
+ * Paths of files Lisa wrote, relative to the `.codex/` directory.
7
+ * Used to identify which files to delete when they stop being shipped.
8
+ */
9
+ readonly files: readonly string[];
10
+ }
11
+ /**
12
+ * Read the Lisa-managed manifest from `<destDir>/.codex/.lisa-managed.json`.
13
+ * Returns an empty manifest if the file doesn't exist.
14
+ * @param destDir - Absolute path to the destination project root
15
+ * @returns Parsed manifest (with empty file list if absent)
16
+ */
17
+ export declare function readManagedManifest(destDir: string): Promise<LisaManagedManifest>;
18
+ /**
19
+ * Write the Lisa-managed manifest to disk, replacing any existing content.
20
+ * @param destDir - Absolute path to the destination project root
21
+ * @param files - Sorted list of relative-to-`.codex/` file paths Lisa shipped
22
+ */
23
+ export declare function writeManagedManifest(destDir: string, files: readonly string[]): Promise<void>;
24
+ /**
25
+ * Compute the set of stale files: in the previous manifest but not in the
26
+ * new shipment. These are candidates for deletion.
27
+ * @param previous - Manifest from the prior run
28
+ * @param current - File list Lisa is shipping this run (relative to `.codex/`)
29
+ * @returns Files that should be removed from the host project
30
+ */
31
+ export declare function diffManifests(previous: LisaManagedManifest, current: readonly string[]): readonly string[];
32
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/codex/manifest.ts"],"names":[],"mappings":"AAuBA,mEAAmE;AACnE,eAAO,MAAM,8BAA8B,uBAAuB,CAAC;AAEnE,4CAA4C;AAC5C,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,CAAC,CAY9B;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,SAAS,MAAM,EAAE,CAGnB"}