guardian-framework 0.1.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 (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +463 -0
  3. package/dist/cli.js +12958 -0
  4. package/dist/exports.js +12765 -0
  5. package/package.json +61 -0
  6. package/templates/languages/go-patterns.md +411 -0
  7. package/templates/languages/java-patterns.md +545 -0
  8. package/templates/languages/python-patterns.md +312 -0
  9. package/templates/languages/rust-patterns.md +232 -0
  10. package/templates/languages/typescript-patterns.md +313 -0
  11. package/templates/pi/INDEX.md +437 -0
  12. package/templates/pi/agent/AGENTS.md +166 -0
  13. package/templates/pi/agents/architecture-coordinator.md +40 -0
  14. package/templates/pi/agents/architecture-validator.md +36 -0
  15. package/templates/pi/agents/bootstrap-implementer.md +37 -0
  16. package/templates/pi/agents/issue-factory.md +31 -0
  17. package/templates/pi/agents/operations-validator.md +32 -0
  18. package/templates/pi/agents/security-validator.md +33 -0
  19. package/templates/pi/architecture/CHANGELOG.md +78 -0
  20. package/templates/pi/architecture/decisions/ADR-template.md +76 -0
  21. package/templates/pi/architecture/diagrams/system-overview.md +159 -0
  22. package/templates/pi/architecture/modules/module-template.md +152 -0
  23. package/templates/pi/context/checklists.md +91 -0
  24. package/templates/pi/context/domain-workflow.md +180 -0
  25. package/templates/pi/context/output-formats.md +135 -0
  26. package/templates/pi/context/patterns-base.md +47 -0
  27. package/templates/pi/context/patterns.md +38 -0
  28. package/templates/pi/context/project.md +88 -0
  29. package/templates/pi/domain/exploration.md +93 -0
  30. package/templates/pi/domain/ubiquitous-language.md +24 -0
  31. package/templates/pi/extensions/architect-lib/generators.ts +528 -0
  32. package/templates/pi/extensions/architect-lib/helpers.ts +344 -0
  33. package/templates/pi/extensions/architect-lib/types.ts +117 -0
  34. package/templates/pi/extensions/architect.ts +1425 -0
  35. package/templates/pi/extensions/ask-user-question.ts +579 -0
  36. package/templates/pi/extensions/bash-guard.ts +556 -0
  37. package/templates/pi/extensions/config-reload.ts +115 -0
  38. package/templates/pi/extensions/coordinator.ts +304 -0
  39. package/templates/pi/extensions/curator.ts +543 -0
  40. package/templates/pi/extensions/domain-explorer.ts +990 -0
  41. package/templates/pi/extensions/filechanges.ts +656 -0
  42. package/templates/pi/extensions/goal-loop.ts +717 -0
  43. package/templates/pi/extensions/hooks.ts +452 -0
  44. package/templates/pi/extensions/kanban.ts +547 -0
  45. package/templates/pi/extensions/pipeline.ts +1246 -0
  46. package/templates/pi/extensions/plan-mode.ts +209 -0
  47. package/templates/pi/extensions/project-scaffolder.ts +284 -0
  48. package/templates/pi/extensions/read-only-mode.ts +192 -0
  49. package/templates/pi/extensions/redaction.ts +128 -0
  50. package/templates/pi/extensions/session-persistence.ts +174 -0
  51. package/templates/pi/extensions/slash-commands.ts +229 -0
  52. package/templates/pi/extensions/snippets.ts +201 -0
  53. package/templates/pi/extensions/validation-runner.ts +100 -0
  54. package/templates/pi/github/agents/architecture-coordinator.agent.md +27 -0
  55. package/templates/pi/github/agents/architecture-validator.agent.md +20 -0
  56. package/templates/pi/github/agents/bootstrap-implementer.agent.md +20 -0
  57. package/templates/pi/github/agents/epic-planner.agent.md +70 -0
  58. package/templates/pi/github/agents/issue-factory.agent.md +20 -0
  59. package/templates/pi/github/agents/operations-validator.agent.md +20 -0
  60. package/templates/pi/github/agents/security-validator.agent.md +20 -0
  61. package/templates/pi/github/copilot/settings.json +16 -0
  62. package/templates/pi/github/copilot-instructions.md +155 -0
  63. package/templates/pi/github/instructions/architecture.instructions.md +71 -0
  64. package/templates/pi/github/instructions/validation.instructions.md +69 -0
  65. package/templates/pi/github/workflows/01-planning-workflow.md +75 -0
  66. package/templates/pi/github/workflows/02-issue-generation-workflow.md +69 -0
  67. package/templates/pi/github/workflows/03-implementation-workflow.md +80 -0
  68. package/templates/pi/github/workflows/04-validation-workflow.md +68 -0
  69. package/templates/pi/preflight_report.json +99 -0
  70. package/templates/pi/prompts/blueprint-update.md +262 -0
  71. package/templates/pi/prompts/blueprint-validate.md +218 -0
  72. package/templates/pi/prompts/bug-fix.md +50 -0
  73. package/templates/pi/prompts/ci-blueprint.md +131 -0
  74. package/templates/pi/prompts/context-refresh.md +258 -0
  75. package/templates/pi/prompts/epic-plan.md +356 -0
  76. package/templates/pi/prompts/epic-template.md +186 -0
  77. package/templates/pi/prompts/feature-development.md +131 -0
  78. package/templates/pi/prompts/git-issues.md +243 -0
  79. package/templates/pi/prompts/hotfix.md +71 -0
  80. package/templates/pi/prompts/issue-closeout.md +342 -0
  81. package/templates/pi/prompts/issue-draft.md +218 -0
  82. package/templates/pi/prompts/issue-implementation-series.md +173 -0
  83. package/templates/pi/prompts/issue-merge.md +372 -0
  84. package/templates/pi/prompts/issue-template-set.md +392 -0
  85. package/templates/pi/prompts/issue-template.md +207 -0
  86. package/templates/pi/prompts/pattern-extract.md +269 -0
  87. package/templates/pi/prompts/plan-to-issues.md +430 -0
  88. package/templates/pi/prompts/refactoring.md +82 -0
  89. package/templates/pi/prompts/scope-analyzer.md +254 -0
  90. package/templates/pi/prompts/sync-check.md +230 -0
  91. package/templates/pi/scripts/categorize-issues.sh +171 -0
  92. package/templates/pi/scripts/ci/check_architecture_conformance.sh +748 -0
  93. package/templates/pi/scripts/ci/check_planning_packet.py +228 -0
  94. package/templates/pi/scripts/ci/run_hardening_stages.sh +264 -0
  95. package/templates/pi/scripts/ci/run_preflight.sh +649 -0
  96. package/templates/pi/scripts/ci/run_stage.sh +193 -0
  97. package/templates/pi/scripts/ci/stage_docs_policy.sh +82 -0
  98. package/templates/pi/scripts/ci/stage_integration.sh +3 -0
  99. package/templates/pi/scripts/ci/stage_lint.sh +130 -0
  100. package/templates/pi/scripts/ci/stage_migration_verify.sh +3 -0
  101. package/templates/pi/scripts/ci/stage_package_build.sh +3 -0
  102. package/templates/pi/scripts/ci/stage_release_readiness.sh +3 -0
  103. package/templates/pi/scripts/ci/stage_remaining.sh +242 -0
  104. package/templates/pi/scripts/ci/stage_security.sh +3 -0
  105. package/templates/pi/scripts/ci/stage_static_analysis.sh +114 -0
  106. package/templates/pi/scripts/ci/stage_unit.sh +119 -0
  107. package/templates/pi/scripts/ci/validate_agent_output.py +181 -0
  108. package/templates/pi/scripts/ci/validate_agent_output.sh +243 -0
  109. package/templates/pi/scripts/create-feature-branch.sh +136 -0
  110. package/templates/pi/scripts/create-mr.sh +117 -0
  111. package/templates/pi/scripts/fetch-issues.sh +131 -0
  112. package/templates/pi/scripts/generate-architecture.sh +161 -0
  113. package/templates/pi/scripts/git/close-epic.sh +69 -0
  114. package/templates/pi/scripts/git/close-issue.sh +35 -0
  115. package/templates/pi/scripts/git/create-tracking-issue.sh +174 -0
  116. package/templates/pi/scripts/git/link-issue-to-epic.sh +51 -0
  117. package/templates/pi/scripts/git/update-tracking-issue.sh +66 -0
  118. package/templates/pi/scripts/languages/go/validate-architecture.sh +139 -0
  119. package/templates/pi/scripts/languages/go/validate-canonical.sh +129 -0
  120. package/templates/pi/scripts/languages/go/validate-ci.sh +132 -0
  121. package/templates/pi/scripts/languages/go/validate-integration.sh +156 -0
  122. package/templates/pi/scripts/languages/go/validate-operations.sh +146 -0
  123. package/templates/pi/scripts/languages/go/validate-security.sh +135 -0
  124. package/templates/pi/scripts/languages/go/validate-tests.sh +125 -0
  125. package/templates/pi/scripts/languages/java/validate-annotations.sh +185 -0
  126. package/templates/pi/scripts/languages/java/validate-architecture.sh +159 -0
  127. package/templates/pi/scripts/languages/java/validate-canonical.sh +114 -0
  128. package/templates/pi/scripts/languages/java/validate-ci.sh +269 -0
  129. package/templates/pi/scripts/languages/java/validate-integration.sh +149 -0
  130. package/templates/pi/scripts/languages/java/validate-operations.sh +142 -0
  131. package/templates/pi/scripts/languages/java/validate-security.sh +174 -0
  132. package/templates/pi/scripts/languages/java/validate-spring-architecture.sh +227 -0
  133. package/templates/pi/scripts/languages/java/validate-tests.sh +159 -0
  134. package/templates/pi/scripts/languages/python/validate-architecture.sh +99 -0
  135. package/templates/pi/scripts/languages/python/validate-canonical.sh +124 -0
  136. package/templates/pi/scripts/languages/python/validate-ci.sh +130 -0
  137. package/templates/pi/scripts/languages/python/validate-integration.sh +119 -0
  138. package/templates/pi/scripts/languages/python/validate-operations.sh +107 -0
  139. package/templates/pi/scripts/languages/python/validate-security.sh +68 -0
  140. package/templates/pi/scripts/languages/python/validate-tests.sh +102 -0
  141. package/templates/pi/scripts/languages/rust/validate-architecture.sh +188 -0
  142. package/templates/pi/scripts/languages/rust/validate-canonical.sh +146 -0
  143. package/templates/pi/scripts/languages/rust/validate-ci.sh +147 -0
  144. package/templates/pi/scripts/languages/rust/validate-integration.sh +149 -0
  145. package/templates/pi/scripts/languages/rust/validate-operations.sh +154 -0
  146. package/templates/pi/scripts/languages/rust/validate-security.sh +153 -0
  147. package/templates/pi/scripts/languages/rust/validate-tests.sh +150 -0
  148. package/templates/pi/scripts/languages/typescript/validate-architecture.sh +157 -0
  149. package/templates/pi/scripts/languages/typescript/validate-canonical.sh +135 -0
  150. package/templates/pi/scripts/languages/typescript/validate-ci.sh +244 -0
  151. package/templates/pi/scripts/languages/typescript/validate-integration.sh +166 -0
  152. package/templates/pi/scripts/languages/typescript/validate-operations.sh +162 -0
  153. package/templates/pi/scripts/languages/typescript/validate-security.sh +169 -0
  154. package/templates/pi/scripts/languages/typescript/validate-tests.sh +158 -0
  155. package/templates/pi/scripts/merge-mr.sh +95 -0
  156. package/templates/pi/scripts/mr-validation.sh +228 -0
  157. package/templates/pi/scripts/validate-architecture-readiness.sh +153 -0
  158. package/templates/pi/scripts/validate-architecture.sh +22 -0
  159. package/templates/pi/scripts/validate-canonical.sh +22 -0
  160. package/templates/pi/scripts/validate-ci.sh +37 -0
  161. package/templates/pi/scripts/validate-integration.sh +22 -0
  162. package/templates/pi/scripts/validate-operations.sh +22 -0
  163. package/templates/pi/scripts/validate-security.sh +22 -0
  164. package/templates/pi/scripts/validate-tests.sh +22 -0
  165. package/templates/pi/scripts/validate-ubiquitous-language.sh +291 -0
  166. package/templates/pi/scripts/validation-cache.sh +139 -0
  167. package/templates/pi/skills/agents/architecture-coordinator.md +178 -0
  168. package/templates/pi/skills/agents/architecture-generator.md +79 -0
  169. package/templates/pi/skills/agents/architecture-validator.md +74 -0
  170. package/templates/pi/skills/agents/ci-mr-validator.md +46 -0
  171. package/templates/pi/skills/agents/code-developer.md +76 -0
  172. package/templates/pi/skills/agents/commit.md +46 -0
  173. package/templates/pi/skills/agents/curator.md +71 -0
  174. package/templates/pi/skills/agents/debug.md +59 -0
  175. package/templates/pi/skills/agents/documentation-maintainer.md +36 -0
  176. package/templates/pi/skills/agents/goal-loop.md +125 -0
  177. package/templates/pi/skills/agents/hooks.md +122 -0
  178. package/templates/pi/skills/agents/integration-validator.md +35 -0
  179. package/templates/pi/skills/agents/issue-creator.md +75 -0
  180. package/templates/pi/skills/agents/issue-factory.md +143 -0
  181. package/templates/pi/skills/agents/kanban.md +85 -0
  182. package/templates/pi/skills/agents/land.md +59 -0
  183. package/templates/pi/skills/agents/operations-validator.md +43 -0
  184. package/templates/pi/skills/agents/pipeline.md +113 -0
  185. package/templates/pi/skills/agents/plan-mode.md +77 -0
  186. package/templates/pi/skills/agents/pull.md +40 -0
  187. package/templates/pi/skills/agents/push.md +41 -0
  188. package/templates/pi/skills/agents/security-validator.md +85 -0
  189. package/templates/pi/skills/agents/session-persistence.md +119 -0
  190. package/templates/pi/skills/agents/slash-commands.md +117 -0
  191. package/templates/pi/skills/agents/snippets.md +99 -0
  192. package/templates/pi/skills/agents/subagent-registry.md +78 -0
  193. package/templates/pi/skills/agents/test-validator.md +42 -0
  194. package/templates/pi/skills/validators/architecture-validator.md +43 -0
  195. package/templates/pi/skills/validators/ci-validator.md +27 -0
  196. package/templates/pi/skills/validators/context-compaction.md +73 -0
  197. package/templates/pi/skills/validators/integration-validator.md +38 -0
  198. package/templates/pi/skills/validators/model-registry.md +96 -0
  199. package/templates/pi/skills/validators/operations-validator.md +34 -0
  200. package/templates/pi/skills/validators/security-guards.md +72 -0
  201. package/templates/pi/skills/validators/security-validator.md +41 -0
  202. package/templates/pi/skills/validators/system-prompt-tiers.md +69 -0
  203. package/templates/pi/skills/validators/test-validator.md +33 -0
  204. package/templates/pi/types.ts +62 -0
  205. package/templates/pi/validators/README.md +93 -0
  206. package/templates/pi/validators/default.toml +209 -0
  207. package/templates/pi/validators/spring.toml +142 -0
  208. package/templates/pi/workpad.md +53 -0
  209. package/templates/project/java/Dockerfile +14 -0
  210. package/templates/project/java/README.md +40 -0
  211. package/templates/project/java/pom.xml +132 -0
  212. package/templates/project/typescript/Dockerfile +15 -0
  213. package/templates/project/typescript/README.md +29 -0
  214. package/templates/project/typescript/package.json +23 -0
  215. package/templates/project/typescript/tsconfig.json +21 -0
  216. package/templates/workflow.md +123 -0
@@ -0,0 +1,452 @@
1
+ /**
2
+ * Shell Hooks Extension for pi
3
+ *
4
+ * Declarative shell-script hooks that fire on lifecycle events.
5
+ * Hooks are defined in AGENTS.md front matter under the `hooks:` section.
6
+ * Scripts can block tool calls, inject context, or observe events.
7
+ *
8
+ * Inspired by Hermes-Agent's 3-layer hook system — adapted for Guardian's
9
+ * TypeScript extension model with shell-script isolation.
10
+ *
11
+ * Supported events:
12
+ * pre_tool_call — before any tool executes (can block)
13
+ * post_tool_call — after any tool returns
14
+ * pre_llm_call — before LLM turn (can inject context)
15
+ * post_llm_call — after LLM turn completes
16
+ * on_session_start — new session created
17
+ * on_session_end — session ended
18
+ * subagent_stop — subagent completed
19
+ *
20
+ * Hook JSON protocol:
21
+ * stdin → { hook_event_name, tool_name, tool_input, session_id, cwd, extra }
22
+ * stdout → { decision: "block", reason: "..." } (pre_tool_call)
23
+ * → { context: "..." } (pre_llm_call)
24
+ * → {} (observer hooks)
25
+ */
26
+
27
+ import { spawn } from "node:child_process";
28
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
29
+ import { dirname, join } from "node:path";
30
+
31
+ // ── Types ──
32
+
33
+ type ExtensionContext = {
34
+ cwd: string;
35
+ ui: {
36
+ notify(message: string, level?: string): void;
37
+ setStatus(key: string, message: string | null): void;
38
+ };
39
+ shell: {
40
+ execute(
41
+ command: string,
42
+ options?: { signal?: AbortSignal },
43
+ ): Promise<{
44
+ exitCode: number;
45
+ stdout: string;
46
+ }>;
47
+ };
48
+ tools: { execute(name: string, params: Record<string, unknown>): Promise<unknown> };
49
+ };
50
+
51
+ type ExtensionAPI = {
52
+ on(event: string, handler: (event: unknown, ctx: ExtensionContext) => void | Promise<void>): void;
53
+ registerTool(options: {
54
+ name: string;
55
+ label: string;
56
+ description: string;
57
+ parameters: unknown;
58
+ execute(
59
+ toolCallId: string,
60
+ params: Record<string, unknown>,
61
+ signal: AbortSignal,
62
+ onUpdate: (update: { type: string; message: string }) => void,
63
+ ctx: ExtensionContext,
64
+ ): unknown | Promise<unknown>;
65
+ }): void;
66
+ registerCommand(
67
+ name: string,
68
+ options: {
69
+ description: string;
70
+ handler(args: string, ctx: ExtensionContext): unknown | Promise<unknown>;
71
+ },
72
+ ): void;
73
+ };
74
+
75
+ type HookEntry = {
76
+ matcher?: string;
77
+ command: string;
78
+ timeout?: number;
79
+ };
80
+
81
+ type HookConfig = {
82
+ [event: string]: HookEntry[];
83
+ };
84
+
85
+ type HookPayload = {
86
+ hook_event_name: string;
87
+ tool_name?: string | null;
88
+ tool_input?: Record<string, unknown> | null;
89
+ session_id?: string | null;
90
+ cwd: string;
91
+ extra?: Record<string, unknown>;
92
+ };
93
+
94
+ type HookBlockResponse = {
95
+ decision: "block";
96
+ reason: string;
97
+ };
98
+
99
+ type HookContextResponse = {
100
+ context: string;
101
+ };
102
+
103
+ // ── Constants ──
104
+
105
+ const VALID_HOOKS = [
106
+ "pre_tool_call",
107
+ "post_tool_call",
108
+ "pre_llm_call",
109
+ "post_llm_call",
110
+ "on_session_start",
111
+ "on_session_end",
112
+ "subagent_stop",
113
+ ];
114
+
115
+ const DEFAULT_TIMEOUT = 60;
116
+ const MAX_TIMEOUT = 300;
117
+ const HOOKS_DIR = ".pi/hooks";
118
+
119
+ // ── YAML Parser (minimal) ──
120
+
121
+ function parseHookConfig(frontMatter: Record<string, unknown>): HookConfig {
122
+ const config: HookConfig = {};
123
+ const hooksRaw = frontMatter.hooks as Record<string, unknown> | undefined;
124
+ if (!hooksRaw) return config;
125
+
126
+ for (const [eventName, entries] of Object.entries(hooksRaw)) {
127
+ if (!Array.isArray(entries)) continue;
128
+
129
+ const parsed: HookEntry[] = [];
130
+ for (const entry of entries) {
131
+ if (!entry || typeof entry !== "object") continue;
132
+ const e = entry as Record<string, unknown>;
133
+ const cmd = e.command as string;
134
+ if (!cmd) continue;
135
+
136
+ parsed.push({
137
+ matcher: (e.matcher as string) || undefined,
138
+ command: cmd,
139
+ timeout: Math.min(Math.max(Number(e.timeout) || DEFAULT_TIMEOUT, 1), MAX_TIMEOUT),
140
+ });
141
+ }
142
+
143
+ if (parsed.length > 0) {
144
+ if (VALID_HOOKS.includes(eventName)) {
145
+ config[eventName] = parsed;
146
+ }
147
+ }
148
+ }
149
+
150
+ return config;
151
+ }
152
+
153
+ function loadHooksConfig(cwd: string): HookConfig {
154
+ const agentsPath = join(cwd, ".pi/agent/AGENTS.md");
155
+ if (!existsSync(agentsPath)) return {};
156
+
157
+ const content = readFileSync(agentsPath, "utf-8");
158
+ if (!content.startsWith("---\n")) return {};
159
+
160
+ const endIdx = content.indexOf("\n---\n", 4);
161
+ if (endIdx === -1) return {};
162
+
163
+ const yamlBlock = content.slice(4, endIdx);
164
+ try {
165
+ // Minimal YAML parser for hooks config
166
+ // For production, use the `yaml` package like workflow-config.ts
167
+ return parseHookConfig(parseMinimalYaml(yamlBlock));
168
+ } catch {
169
+ return {};
170
+ }
171
+ }
172
+
173
+ function parseMinimalYaml(yaml: string): Record<string, unknown> {
174
+ const result: Record<string, unknown> = {};
175
+ const lines = yaml.split("\n");
176
+ let currentTopKey: string | null = null;
177
+ let currentList: unknown[] | null = null;
178
+ let currentDict: Record<string, unknown> | null = null;
179
+
180
+ for (const line of lines) {
181
+ const trimmed = line.trim();
182
+ if (!trimmed || trimmed.startsWith("#")) continue;
183
+
184
+ // Top-level key (no indent)
185
+ if (!line.startsWith(" ") && !line.startsWith("\t")) {
186
+ if (trimmed.endsWith(":")) {
187
+ currentTopKey = trimmed.slice(0, -1);
188
+ result[currentTopKey] = [];
189
+ currentList = result[currentTopKey] as unknown[];
190
+ currentDict = null;
191
+ }
192
+ continue;
193
+ }
194
+
195
+ // List item
196
+ if (trimmed.startsWith("- ")) {
197
+ if (currentList) {
198
+ const itemStr = trimmed.slice(2).trim();
199
+ if (itemStr.includes(":")) {
200
+ // Inline dict start: - command: "..."
201
+ const dict: Record<string, unknown> = {};
202
+ const parts = itemStr.split(/:\s*/);
203
+ if (parts.length >= 2) {
204
+ const key = parts[0].trim();
205
+ const val = parts
206
+ .slice(1)
207
+ .join(":")
208
+ .trim()
209
+ .replace(/^["']|["']$/g, "");
210
+ dict[key] = val;
211
+ }
212
+ currentList.push(dict);
213
+ currentDict = dict;
214
+ } else {
215
+ currentList.push(itemStr);
216
+ currentDict = null;
217
+ }
218
+ }
219
+ continue;
220
+ }
221
+
222
+ // Dict continuation (indented key: value)
223
+ if (trimmed.includes(":") && currentDict) {
224
+ const parts = trimmed.split(/:\s*/);
225
+ if (parts.length >= 2) {
226
+ const key = parts[0].trim();
227
+ const val = parts
228
+ .slice(1)
229
+ .join(":")
230
+ .trim()
231
+ .replace(/^["']|["']$/g, "");
232
+ currentDict[key] = Number.isNaN(Number(val)) ? val : Number(val);
233
+ }
234
+ }
235
+ }
236
+
237
+ return result;
238
+ }
239
+
240
+ // ── Hook Execution ──
241
+
242
+ async function runHook(
243
+ cwd: string,
244
+ entry: HookEntry,
245
+ eventName: string,
246
+ payload: HookPayload,
247
+ ): Promise<{ blocked?: string; context?: string }> {
248
+ const timeout = entry.timeout ?? DEFAULT_TIMEOUT;
249
+
250
+ return new Promise((resolve) => {
251
+ // Check matcher
252
+ if (entry.matcher && payload.tool_name) {
253
+ try {
254
+ const re = new RegExp(entry.matcher);
255
+ if (!re.test(payload.tool_name)) {
256
+ resolve({});
257
+ return;
258
+ }
259
+ } catch {
260
+ resolve({}); // Invalid regex — skip
261
+ return;
262
+ }
263
+ }
264
+
265
+ const child = spawn("bash", ["-lc", entry.command], {
266
+ cwd,
267
+ timeout: timeout * 1000,
268
+ stdio: ["pipe", "pipe", "pipe"],
269
+ });
270
+
271
+ let stdout = "";
272
+ let stderr = "";
273
+
274
+ child.stdin.write(JSON.stringify(payload));
275
+ child.stdin.end();
276
+
277
+ child.stdout.on("data", (d) => {
278
+ stdout += d.toString();
279
+ });
280
+ child.stderr.on("data", (d) => {
281
+ stderr += d.toString();
282
+ });
283
+
284
+ child.on("close", (code) => {
285
+ if (code !== 0) {
286
+ resolve({}); // Non-zero exit — skip silently
287
+ return;
288
+ }
289
+
290
+ try {
291
+ const trimmed = stdout.trim();
292
+ if (!trimmed) {
293
+ resolve({});
294
+ return;
295
+ }
296
+ const parsed = JSON.parse(trimmed);
297
+ if (parsed.decision === "block" || parsed.action === "block") {
298
+ resolve({ blocked: parsed.reason || parsed.message || "blocked by hook" });
299
+ } else if (parsed.context) {
300
+ resolve({ context: parsed.context });
301
+ } else {
302
+ resolve({});
303
+ }
304
+ } catch {
305
+ resolve({}); // Malformed JSON — skip
306
+ }
307
+ });
308
+
309
+ child.on("error", () => resolve({}));
310
+ });
311
+ }
312
+
313
+ // ── Extension ──
314
+
315
+ export default function (pi: ExtensionAPI) {
316
+ let hooksConfig: HookConfig = {};
317
+ const blockedTools: string[] = [];
318
+ const injectedContext: string | null = null;
319
+
320
+ pi.on("session_start", async (_event, ctx) => {
321
+ hooksConfig = loadHooksConfig(ctx.cwd);
322
+ const eventCount = Object.values(hooksConfig).reduce((sum, e) => sum + e.length, 0);
323
+ ctx.ui.notify(
324
+ `Guardian hooks initialized (${eventCount} hooks across ${Object.keys(hooksConfig).length} events)`,
325
+ "info",
326
+ );
327
+
328
+ // Fire on_session_start hooks
329
+ for (const entry of hooksConfig.on_session_start || []) {
330
+ await runHook(ctx.cwd, entry, "on_session_start", {
331
+ hook_event_name: "on_session_start",
332
+ tool_name: null,
333
+ tool_input: null,
334
+ cwd: ctx.cwd,
335
+ });
336
+ }
337
+ });
338
+
339
+ // ── pre_tool_call — block dangerous operations ──
340
+ pi.on("tool_call", async (event, ctx) => {
341
+ const eventName = "tool_call" in (event as object) ? "tool_call" : "";
342
+ if (eventName !== "tool_call") return;
343
+
344
+ const toolEvent = event as { toolName: string; input: Record<string, unknown> };
345
+ const toolName = toolEvent.toolName;
346
+ const toolInput = toolEvent.input || {};
347
+
348
+ const hooks = hooksConfig.pre_tool_call || [];
349
+ if (hooks.length === 0) return;
350
+
351
+ for (const entry of hooks) {
352
+ const result = await runHook(ctx.cwd, entry, "pre_tool_call", {
353
+ hook_event_name: "pre_tool_call",
354
+ tool_name: toolName,
355
+ tool_input: toolInput,
356
+ cwd: ctx.cwd,
357
+ });
358
+
359
+ if (result.blocked) {
360
+ ctx.ui.notify(`🚫 Hook blocked ${toolName}: ${result.blocked}`, "error");
361
+ // Return block directive
362
+ return {
363
+ block: true,
364
+ reason: `Guardian hook blocked: ${result.blocked}`,
365
+ };
366
+ }
367
+ }
368
+ });
369
+
370
+ // ── post_tool_call — auto-format, logging, etc. ──
371
+ pi.on("tool_result", async (event, ctx) => {
372
+ const toolEvent = event as { toolName: string; input?: Record<string, unknown> };
373
+ const toolName = toolEvent.toolName;
374
+
375
+ const hooks = hooksConfig.post_tool_call || [];
376
+ if (hooks.length === 0) return;
377
+
378
+ for (const entry of hooks) {
379
+ await runHook(ctx.cwd, entry, "post_tool_call", {
380
+ hook_event_name: "post_tool_call",
381
+ tool_name: toolName,
382
+ tool_input: toolEvent.input || null,
383
+ cwd: ctx.cwd,
384
+ extra: { event: "tool_result" },
385
+ });
386
+ }
387
+ });
388
+
389
+ // ── /hooks command ──
390
+ pi.registerCommand("hooks", {
391
+ description: "List or manage Guardian shell hooks",
392
+ handler: async (args, ctx) => {
393
+ hooksConfig = loadHooksConfig(ctx.cwd);
394
+ const raw = typeof args === "string" ? args : "";
395
+ const tokens = raw.split(/\s+/).filter(Boolean);
396
+ const action = tokens[0];
397
+
398
+ if (!action || action === "list") {
399
+ if (Object.keys(hooksConfig).length === 0) {
400
+ ctx.ui.notify("No hooks configured. Add hooks: section to AGENTS.md.", "info");
401
+ return;
402
+ }
403
+
404
+ const lines = ["## Guardian Shell Hooks\n"];
405
+ for (const [event, entries] of Object.entries(hooksConfig)) {
406
+ lines.push(`### ${event} (${entries.length} hook(s))`);
407
+ for (const entry of entries) {
408
+ const matcherInfo = entry.matcher ? ` [matcher: ${entry.matcher}]` : "";
409
+ const timeoutInfo =
410
+ entry.timeout !== DEFAULT_TIMEOUT ? ` [timeout: ${entry.timeout}s]` : "";
411
+ lines.push(` - \`${entry.command}\`${matcherInfo}${timeoutInfo}`);
412
+ }
413
+ lines.push("");
414
+ }
415
+ ctx.ui.notify(lines.join("\n"), "info");
416
+ return;
417
+ }
418
+
419
+ if (action === "test") {
420
+ const eventName = tokens[1];
421
+ if (!eventName) {
422
+ ctx.ui.notify("Usage: /hooks test <event_name>", "error");
423
+ return;
424
+ }
425
+ const hooks = hooksConfig[eventName];
426
+ if (!hooks || hooks.length === 0) {
427
+ ctx.ui.notify(`No hooks registered for event: ${eventName}`, "warn");
428
+ return;
429
+ }
430
+ ctx.ui.notify(`Testing ${hooks.length} hook(s) for ${eventName}...`, "info");
431
+ for (const entry of hooks) {
432
+ const result = await runHook(ctx.cwd, entry, eventName, {
433
+ hook_event_name: eventName,
434
+ tool_name: "test",
435
+ tool_input: { test: true },
436
+ cwd: ctx.cwd,
437
+ });
438
+ if (result.blocked) {
439
+ ctx.ui.notify(` ✗ blocked: ${result.blocked}`, "warn");
440
+ } else if (result.context) {
441
+ ctx.ui.notify(` ℹ context: ${result.context.slice(0, 100)}`, "info");
442
+ } else {
443
+ ctx.ui.notify(" ✓ ok", "success");
444
+ }
445
+ }
446
+ return;
447
+ }
448
+
449
+ ctx.ui.notify("Usage: /hooks [list|test <event>]", "info");
450
+ },
451
+ });
452
+ }