@wazir-dev/cli 1.3.0 → 1.4.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 (133) hide show
  1. package/CHANGELOG.md +17 -2
  2. package/docs/research/2026-03-20-agents/a18fb002157904af5.txt +187 -0
  3. package/docs/research/2026-03-20-agents/a1d0ac79ac2f11e6f.txt +2 -0
  4. package/docs/research/2026-03-20-agents/a324079de037abd7c.txt +198 -0
  5. package/docs/research/2026-03-20-agents/a357586bccfafb0e5.txt +256 -0
  6. package/docs/research/2026-03-20-agents/a4365394e4d753105.txt +137 -0
  7. package/docs/research/2026-03-20-agents/a492af28bc52d3613.txt +136 -0
  8. package/docs/research/2026-03-20-agents/a4984db0b6a8eee07.txt +124 -0
  9. package/docs/research/2026-03-20-agents/a5b30e59d34bbb062.txt +214 -0
  10. package/docs/research/2026-03-20-agents/a5cf7829dab911586.txt +165 -0
  11. package/docs/research/2026-03-20-agents/a607157c30dd97c9e.txt +96 -0
  12. package/docs/research/2026-03-20-agents/a60b68b1e19d1e16b.txt +115 -0
  13. package/docs/research/2026-03-20-agents/a722af01c5594aba0.txt +166 -0
  14. package/docs/research/2026-03-20-agents/a787bdc516faa5829.txt +181 -0
  15. package/docs/research/2026-03-20-agents/a7c46d1bba1056ed2.txt +132 -0
  16. package/docs/research/2026-03-20-agents/a7e5abbab2b281a0d.txt +100 -0
  17. package/docs/research/2026-03-20-agents/a8dbadc66cd0d7d5a.txt +95 -0
  18. package/docs/research/2026-03-20-agents/a904d9f45d6b86a6d.txt +75 -0
  19. package/docs/research/2026-03-20-agents/a927659a942ee7f60.txt +102 -0
  20. package/docs/research/2026-03-20-agents/a962cb569191f7583.txt +125 -0
  21. package/docs/research/2026-03-20-agents/aab6decea538aac41.txt +148 -0
  22. package/docs/research/2026-03-20-agents/abd58b853dd938a1b.txt +295 -0
  23. package/docs/research/2026-03-20-agents/ac009da573eff7f65.txt +100 -0
  24. package/docs/research/2026-03-20-agents/ac1bc783364405e5f.txt +190 -0
  25. package/docs/research/2026-03-20-agents/aca5e2b57fde152a0.txt +132 -0
  26. package/docs/research/2026-03-20-agents/ad849b8c0a7e95b8b.txt +176 -0
  27. package/docs/research/2026-03-20-agents/adc2b12a4da32c962.txt +258 -0
  28. package/docs/research/2026-03-20-agents/af97caaaa9a80e4cb.txt +146 -0
  29. package/docs/research/2026-03-20-agents/afc5faceee368b3ca.txt +111 -0
  30. package/docs/research/2026-03-20-agents/afdb282d866e3c1e4.txt +164 -0
  31. package/docs/research/2026-03-20-agents/afe9d1f61c02b1e8d.txt +299 -0
  32. package/docs/research/2026-03-20-agents/b4hmkwril.txt +1856 -0
  33. package/docs/research/2026-03-20-agents/b80ptk89g.txt +1856 -0
  34. package/docs/research/2026-03-20-agents/bf54s1jss.txt +1150 -0
  35. package/docs/research/2026-03-20-agents/bhd6kq2kx.txt +1856 -0
  36. package/docs/research/2026-03-20-agents/bmb2fodyr.txt +988 -0
  37. package/docs/research/2026-03-20-agents/bmmsrij8i.txt +826 -0
  38. package/docs/research/2026-03-20-agents/bn4t2ywpu.txt +2175 -0
  39. package/docs/research/2026-03-20-agents/bu22t9f1z.txt +0 -0
  40. package/docs/research/2026-03-20-agents/bwvl98v2p.txt +738 -0
  41. package/docs/research/2026-03-20-agents/psych-a3697a7fd06eb64fd.txt +135 -0
  42. package/docs/research/2026-03-20-agents/psych-a37776fabc870feae.txt +123 -0
  43. package/docs/research/2026-03-20-agents/psych-a5b1fe05c0589efaf.txt +2 -0
  44. package/docs/research/2026-03-20-agents/psych-a95c15b1f29424435.txt +76 -0
  45. package/docs/research/2026-03-20-agents/psych-a9c26f4d9172dde7c.txt +2 -0
  46. package/docs/research/2026-03-20-agents/psych-aa19c69f0ca2c5ad3.txt +2 -0
  47. package/docs/research/2026-03-20-agents/psych-aa4e4cb70e1be5ecb.txt +95 -0
  48. package/docs/research/2026-03-20-agents/psych-ab5b302f26a554663.txt +102 -0
  49. package/docs/research/2026-03-20-deep-research-complete.md +101 -0
  50. package/docs/research/2026-03-20-deep-research-status.md +38 -0
  51. package/docs/research/2026-03-20-enforcement-research.md +107 -0
  52. package/expertise/composition-map.yaml +27 -8
  53. package/expertise/digests/reviewer/ai-coding-digest.md +83 -0
  54. package/expertise/digests/reviewer/architectural-thinking-digest.md +63 -0
  55. package/expertise/digests/reviewer/architecture-antipatterns-digest.md +49 -0
  56. package/expertise/digests/reviewer/code-smells-digest.md +53 -0
  57. package/expertise/digests/reviewer/coupling-cohesion-digest.md +54 -0
  58. package/expertise/digests/reviewer/ddd-digest.md +60 -0
  59. package/expertise/digests/reviewer/dependency-risk-digest.md +40 -0
  60. package/expertise/digests/reviewer/error-handling-digest.md +55 -0
  61. package/expertise/digests/reviewer/review-methodology-digest.md +49 -0
  62. package/exports/hosts/claude/.claude/commands/learn.md +61 -8
  63. package/exports/hosts/claude/.claude/settings.json +7 -6
  64. package/exports/hosts/claude/export.manifest.json +6 -3
  65. package/exports/hosts/claude/host-package.json +3 -0
  66. package/exports/hosts/codex/export.manifest.json +6 -3
  67. package/exports/hosts/codex/host-package.json +3 -0
  68. package/exports/hosts/cursor/.cursor/hooks.json +6 -6
  69. package/exports/hosts/cursor/export.manifest.json +6 -3
  70. package/exports/hosts/cursor/host-package.json +3 -0
  71. package/exports/hosts/gemini/export.manifest.json +6 -3
  72. package/exports/hosts/gemini/host-package.json +3 -0
  73. package/hooks/definitions/pretooluse_dispatcher.yaml +26 -0
  74. package/hooks/definitions/pretooluse_pipeline_guard.yaml +22 -0
  75. package/hooks/definitions/stop_pipeline_gate.yaml +22 -0
  76. package/hooks/hooks.json +7 -6
  77. package/hooks/pretooluse-dispatcher +84 -0
  78. package/hooks/pretooluse-pipeline-guard +9 -0
  79. package/hooks/stop-pipeline-gate +9 -0
  80. package/package.json +2 -2
  81. package/schemas/decision.schema.json +15 -0
  82. package/schemas/hook.schema.json +4 -1
  83. package/skills/TEMPLATE-3-ZONE.md +160 -0
  84. package/skills/brainstorming/SKILL.md +127 -23
  85. package/skills/clarifier/SKILL.md +175 -18
  86. package/skills/claude-cli/SKILL.md +91 -12
  87. package/skills/codex-cli/SKILL.md +91 -12
  88. package/skills/debugging/SKILL.md +133 -38
  89. package/skills/design/SKILL.md +173 -37
  90. package/skills/dispatching-parallel-agents/SKILL.md +129 -31
  91. package/skills/executing-plans/SKILL.md +113 -25
  92. package/skills/executor/SKILL.md +185 -21
  93. package/skills/finishing-a-development-branch/SKILL.md +107 -18
  94. package/skills/gemini-cli/SKILL.md +91 -12
  95. package/skills/humanize/SKILL.md +92 -13
  96. package/skills/init-pipeline/SKILL.md +90 -17
  97. package/skills/prepare-next/SKILL.md +93 -24
  98. package/skills/receiving-code-review/SKILL.md +90 -16
  99. package/skills/requesting-code-review/SKILL.md +100 -24
  100. package/skills/requesting-code-review/code-reviewer.md +29 -17
  101. package/skills/reviewer/SKILL.md +190 -50
  102. package/skills/run-audit/SKILL.md +92 -15
  103. package/skills/scan-project/SKILL.md +93 -14
  104. package/skills/self-audit/SKILL.md +113 -39
  105. package/skills/skill-research/SKILL.md +94 -7
  106. package/skills/subagent-driven-development/SKILL.md +129 -30
  107. package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +30 -2
  108. package/skills/subagent-driven-development/implementer-prompt.md +40 -27
  109. package/skills/subagent-driven-development/spec-reviewer-prompt.md +25 -12
  110. package/skills/tdd/SKILL.md +125 -20
  111. package/skills/using-git-worktrees/SKILL.md +118 -28
  112. package/skills/using-skills/SKILL.md +116 -29
  113. package/skills/verification/SKILL.md +127 -22
  114. package/skills/wazir/SKILL.md +517 -153
  115. package/skills/writing-plans/SKILL.md +134 -28
  116. package/skills/writing-skills/SKILL.md +91 -13
  117. package/skills/writing-skills/anthropic-best-practices.md +104 -64
  118. package/skills/writing-skills/persuasion-principles.md +100 -34
  119. package/tooling/src/capture/command.js +29 -1
  120. package/tooling/src/capture/decision.js +40 -0
  121. package/tooling/src/capture/store.js +1 -0
  122. package/tooling/src/config/depth-table.js +60 -0
  123. package/tooling/src/export/compiler.js +7 -8
  124. package/tooling/src/guards/guardrail-functions.js +131 -0
  125. package/tooling/src/guards/phase-prerequisite-guard.js +39 -3
  126. package/tooling/src/hooks/pretooluse-dispatcher.js +300 -0
  127. package/tooling/src/hooks/pretooluse-pipeline-guard.js +141 -0
  128. package/tooling/src/hooks/stop-pipeline-gate.js +92 -0
  129. package/tooling/src/learn/pipeline.js +177 -0
  130. package/tooling/src/state/db.js +251 -2
  131. package/tooling/src/state/pipeline-state.js +262 -0
  132. package/wazir.manifest.yaml +3 -0
  133. package/workflows/learn.md +61 -8
@@ -0,0 +1,300 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { readPipelineState } from '../state/pipeline-state.js';
5
+ import { readYamlFile } from '../loaders.js';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Constants
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const ALWAYS_ALLOWED_TOOLS = new Set([
12
+ 'Read', 'Grep', 'Glob', 'Agent', 'Skill',
13
+ 'TaskCreate', 'TaskUpdate', 'TaskList', 'TaskGet',
14
+ ]);
15
+
16
+ const WRITE_BLOCKED_PHASES = new Set(['clarify', 'verify', 'review']);
17
+ const GIT_BLOCKED_PHASES = new Set(['init', 'clarify', 'verify', 'review']);
18
+ const UNRESTRICTED_PHASES = new Set(['init', 'execute', 'complete']);
19
+
20
+ const GIT_MUTATING_PATTERNS = [
21
+ /^git\s+commit/,
22
+ /^git\s+push/,
23
+ /^git\s+merge/,
24
+ /^git\s+rebase/,
25
+ /^git\s+reset/,
26
+ /^git\s+checkout\s+--/,
27
+ ];
28
+
29
+ const DEFAULT_ROUTING_MATRIX = {
30
+ large: ['npm test', 'vitest', 'jest', 'pytest', 'npm run build', 'tsc --noEmit', 'npm ls', 'pip list', 'eslint .', 'prettier --check .', 'tail -f'],
31
+ small: ['git status', 'git log', 'git branch', 'git rev-parse', 'ls', 'pwd', 'mkdir', 'cp', 'mv', 'rm', 'wazir doctor', 'wazir index', 'wazir capture', 'wazir validate', 'which', 'echo'],
32
+ ambiguous_heuristic: { pipe_detected: true, redirect_detected: true, verbose_binaries: ['find', 'rg', 'grep', 'awk', 'sed', 'curl'] },
33
+ };
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Helpers
37
+ // ---------------------------------------------------------------------------
38
+
39
+ function isWazirPath(filePath) {
40
+ if (!filePath) return false;
41
+ return filePath.includes('.wazir/') || filePath.includes('/.wazir');
42
+ }
43
+
44
+ function isWazirCommand(command) {
45
+ if (!command) return false;
46
+ const trimmed = command.trim();
47
+ return trimmed.startsWith('wazir ') || trimmed === 'wazir';
48
+ }
49
+
50
+ function isGitMutating(command) {
51
+ if (!command) return false;
52
+ const trimmed = command.trim();
53
+ return GIT_MUTATING_PATTERNS.some(pattern => pattern.test(trimmed));
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Protected path check
58
+ // ---------------------------------------------------------------------------
59
+
60
+ const APPROVED_FLOWS = new Set([
61
+ 'host_export_regeneration',
62
+ 'pipeline_integration',
63
+ ]);
64
+
65
+ function checkProtectedPath(projectRoot, filePath, approvedFlow) {
66
+ if (!filePath || !projectRoot) return null;
67
+
68
+ let manifest;
69
+ try {
70
+ manifest = readYamlFile(path.join(projectRoot, 'wazir.manifest.yaml'));
71
+ } catch {
72
+ // If manifest can't be read, block writes defensively
73
+ return { decision: 'deny', reason: 'Cannot read manifest to check protected paths.' };
74
+ }
75
+
76
+ if (!manifest?.protected_paths) return null;
77
+
78
+ const absoluteTarget = path.isAbsolute(filePath)
79
+ ? path.resolve(filePath)
80
+ : path.resolve(projectRoot, filePath);
81
+ const relTarget = path.relative(projectRoot, absoluteTarget);
82
+
83
+ // Outside project = not protected
84
+ if (relTarget === '..' || relTarget.startsWith(`..${path.sep}`) || path.isAbsolute(relTarget)) {
85
+ return null;
86
+ }
87
+
88
+ const blocked = manifest.protected_paths.find(
89
+ (pp) => relTarget === pp || relTarget.startsWith(`${pp}${path.sep}`),
90
+ );
91
+
92
+ if (blocked) {
93
+ // Check approved flow override
94
+ if (APPROVED_FLOWS.has(approvedFlow)) {
95
+ return null; // approved flow may write protected paths
96
+ }
97
+ return { decision: 'deny', reason: `Protected path blocked: ${relTarget}` };
98
+ }
99
+
100
+ return null;
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Context-mode classification
105
+ // ---------------------------------------------------------------------------
106
+
107
+ function classifyCommand(cmd, matrix) {
108
+ if (!cmd) return { category: 'small', reason: 'empty command' };
109
+
110
+ if (cmd.includes('# wazir:context-mode')) return { category: 'large', reason: 'explicit marker' };
111
+
112
+ for (const pattern of matrix.large) {
113
+ if (cmd === pattern || cmd.startsWith(pattern + ' ') || cmd.startsWith(pattern + '\t')) {
114
+ return { category: 'large', reason: `matched pattern: ${pattern}` };
115
+ }
116
+ }
117
+
118
+ if (cmd.includes('# wazir:passthrough')) return { category: 'small', reason: 'passthrough marker' };
119
+
120
+ for (const pattern of matrix.small) {
121
+ if (cmd === pattern || cmd.startsWith(pattern + ' ') || cmd.startsWith(pattern + '\t')) {
122
+ return { category: 'small', reason: `matched pattern: ${pattern}` };
123
+ }
124
+ }
125
+
126
+ const heuristic = matrix.ambiguous_heuristic || {};
127
+ if (heuristic.pipe_detected && /(?<![\\])\|/.test(cmd)) {
128
+ return { category: 'ambiguous', reason: 'pipe detected' };
129
+ }
130
+ if (heuristic.redirect_detected && /(?<![\\])>/.test(cmd)) {
131
+ return { category: 'ambiguous', reason: 'redirect detected' };
132
+ }
133
+
134
+ const bin = cmd.split(/\s+/)[0] || '';
135
+ if (Array.isArray(heuristic.verbose_binaries) && heuristic.verbose_binaries.includes(bin)) {
136
+ return { category: 'ambiguous', reason: `verbose binary: ${bin}` };
137
+ }
138
+
139
+ return { category: 'small', reason: 'no pattern matched' };
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Load routing matrix
144
+ // ---------------------------------------------------------------------------
145
+
146
+ function loadRoutingMatrix(projectRoot) {
147
+ try {
148
+ const matrixPath = path.join(projectRoot, 'hooks', 'routing-matrix.json');
149
+ return JSON.parse(fs.readFileSync(matrixPath, 'utf8'));
150
+ } catch {
151
+ return DEFAULT_ROUTING_MATRIX;
152
+ }
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Main evaluation — consolidates all three PreToolUse concerns
157
+ // ---------------------------------------------------------------------------
158
+
159
+ /**
160
+ * Consolidated PreToolUse dispatcher.
161
+ *
162
+ * Evaluation order:
163
+ * 1. Always-allowed tools (reads, task tools)
164
+ * 2. .wazir/ path writes (pipeline state)
165
+ * 3. wazir CLI commands
166
+ * 4. No state = allow all
167
+ * 5. Protected path check (manifest protected_paths)
168
+ * 6. Phase restriction check (write/git blocks)
169
+ * 7. Context-mode routing (Bash classification)
170
+ *
171
+ * @param {string} stateRoot — pipeline state directory
172
+ * @param {string} projectRoot — project root directory
173
+ * @param {object} hookInput — { tool, input }
174
+ * @returns {{ decision: 'allow'|'deny', reason?: string, routing_decision?: object }}
175
+ */
176
+ export function evaluateDispatch(stateRoot, projectRoot, hookInput) {
177
+ const { tool, input = {} } = hookInput;
178
+
179
+ // 1. Always-allowed tools
180
+ if (ALWAYS_ALLOWED_TOOLS.has(tool)) {
181
+ return { decision: 'allow' };
182
+ }
183
+
184
+ // 2. .wazir/ path writes always allowed
185
+ if ((tool === 'Write' || tool === 'Edit') && isWazirPath(input.file_path)) {
186
+ return { decision: 'allow' };
187
+ }
188
+
189
+ // 3. wazir CLI commands always allowed
190
+ if (tool === 'Bash' && isWazirCommand(input.command)) {
191
+ return { decision: 'allow' };
192
+ }
193
+
194
+ // 4. Protected path check (Write/Edit only — always enforced regardless of phase)
195
+ if (tool === 'Write' || tool === 'Edit') {
196
+ const protectedResult = checkProtectedPath(projectRoot, input.file_path, input.approved_flow);
197
+ if (protectedResult) return protectedResult;
198
+ }
199
+
200
+ // 5. No state file = not a pipeline session = allow
201
+ let state;
202
+ try {
203
+ state = readPipelineState(stateRoot);
204
+ } catch {
205
+ return { decision: 'allow' };
206
+ }
207
+ if (!state || !state.current_phase) {
208
+ return { decision: 'allow' };
209
+ }
210
+
211
+ const phase = state.current_phase;
212
+
213
+ // 6. Unrestricted phases
214
+ if (UNRESTRICTED_PHASES.has(phase)) {
215
+ return addRoutingIfBash(tool, input, projectRoot);
216
+ }
217
+
218
+ // 7. Phase-based Write/Edit restriction
219
+ if ((tool === 'Write' || tool === 'Edit') && WRITE_BLOCKED_PHASES.has(phase)) {
220
+ return {
221
+ decision: 'deny',
222
+ reason: `Write/Edit blocked during "${phase}" phase. This phase is read-only for project files. Only .wazir/ writes are allowed.`,
223
+ };
224
+ }
225
+
226
+ // 8. Phase-based git mutation restriction
227
+ if (tool === 'Bash' && GIT_BLOCKED_PHASES.has(phase) && isGitMutating(input.command)) {
228
+ return {
229
+ decision: 'deny',
230
+ reason: `Git mutations (commit/push) blocked during "${phase}" phase. Git commits are only allowed during the execute phase.`,
231
+ };
232
+ }
233
+
234
+ // 9. Context-mode routing for Bash
235
+ return addRoutingIfBash(tool, input, projectRoot);
236
+ }
237
+
238
+ function isContextModeEnabled(projectRoot) {
239
+ const envVal = process.env.WAZIR_CONTEXT_MODE;
240
+ if (envVal !== undefined) return envVal === '1' || envVal === 'true';
241
+
242
+ try {
243
+ const manifestPath = path.join(projectRoot, 'wazir.manifest.yaml');
244
+ const manifestText = fs.readFileSync(manifestPath, 'utf8');
245
+ const match = manifestText.match(/context_mode:[\s\S]*?enabled_by_default:\s*(true|false)/);
246
+ if (match) return match[1] === 'true';
247
+ } catch { /* ignore */ }
248
+
249
+ return false;
250
+ }
251
+
252
+ function addRoutingIfBash(tool, input, projectRoot) {
253
+ if (tool === 'Bash') {
254
+ const matrix = loadRoutingMatrix(projectRoot);
255
+ const cmd = (input.command || '').trim();
256
+ const classification = classifyCommand(cmd, matrix);
257
+ const contextModeEnabled = isContextModeEnabled(projectRoot);
258
+
259
+ let route = 'passthrough';
260
+ if (contextModeEnabled && (classification.category === 'large' || classification.category === 'ambiguous')) {
261
+ route = 'context-mode';
262
+ }
263
+
264
+ const routing_decision = {
265
+ command: cmd,
266
+ category: classification.category,
267
+ reason: classification.reason,
268
+ route,
269
+ context_mode_enabled: contextModeEnabled,
270
+ };
271
+ return { decision: 'allow', routing_decision };
272
+ }
273
+ return { decision: 'allow' };
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // CLI entry point
278
+ // ---------------------------------------------------------------------------
279
+
280
+ const isDirectRun = process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/'));
281
+
282
+ if (isDirectRun) {
283
+ const stateRoot = process.argv[2] || process.env.WAZIR_STATE_ROOT;
284
+ const projectRoot = process.argv[3] || process.env.WAZIR_PROJECT_ROOT || process.cwd();
285
+
286
+ let hookInput = {};
287
+ try {
288
+ const stdin = fs.readFileSync(0, 'utf8').trim();
289
+ if (stdin) hookInput = JSON.parse(stdin);
290
+ } catch { /* no stdin */ }
291
+
292
+ if (!stateRoot) {
293
+ console.log(JSON.stringify({ decision: 'allow' }));
294
+ process.exit(0);
295
+ }
296
+
297
+ const result = evaluateDispatch(stateRoot, projectRoot, hookInput);
298
+ console.log(JSON.stringify(result));
299
+ process.exit(0);
300
+ }
@@ -0,0 +1,141 @@
1
+ import fs from 'node:fs';
2
+ import { readPipelineState } from '../state/pipeline-state.js';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Phase → tool restriction rules
6
+ // ---------------------------------------------------------------------------
7
+
8
+ // Phases where Write/Edit to project files are blocked
9
+ const WRITE_BLOCKED_PHASES = new Set(['clarify', 'verify', 'review']);
10
+
11
+ // Phases where git commit/push are blocked
12
+ const GIT_BLOCKED_PHASES = new Set(['init', 'clarify', 'verify', 'review']);
13
+
14
+ // Phases where all tools are unrestricted
15
+ const UNRESTRICTED_PHASES = new Set(['init', 'execute', 'complete']);
16
+
17
+ // Tools that are always allowed (read-only operations)
18
+ const ALWAYS_ALLOWED_TOOLS = new Set(['Read', 'Grep', 'Glob', 'Agent', 'Skill', 'TaskCreate', 'TaskUpdate', 'TaskList', 'TaskGet']);
19
+
20
+ // Git commands that modify state
21
+ const GIT_MUTATING_PATTERNS = [
22
+ /^git\s+commit/,
23
+ /^git\s+push/,
24
+ /^git\s+merge/,
25
+ /^git\s+rebase/,
26
+ /^git\s+reset/,
27
+ /^git\s+checkout\s+--/,
28
+ ];
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Evaluation
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Evaluate whether a tool call should be allowed in the current pipeline phase.
36
+ *
37
+ * @param {string} stateRoot — path to the pipeline state directory
38
+ * @param {object} hookInput — { tool: string, input: object }
39
+ * @returns {{ decision: 'allow'|'deny', reason?: string }}
40
+ */
41
+ export function evaluatePreToolUse(stateRoot, hookInput) {
42
+ const { tool, input = {} } = hookInput;
43
+
44
+ // 1. Always-allowed tools (reads are never blocked)
45
+ if (ALWAYS_ALLOWED_TOOLS.has(tool)) {
46
+ return { decision: 'allow' };
47
+ }
48
+
49
+ // 2. No state file → not a pipeline session → allow everything
50
+ let state;
51
+ try {
52
+ state = readPipelineState(stateRoot);
53
+ } catch {
54
+ return { decision: 'allow' };
55
+ }
56
+
57
+ if (!state || !state.current_phase) {
58
+ return { decision: 'allow' };
59
+ }
60
+
61
+ const phase = state.current_phase;
62
+
63
+ // 3. Unrestricted phases
64
+ if (UNRESTRICTED_PHASES.has(phase)) {
65
+ return { decision: 'allow' };
66
+ }
67
+
68
+ // 4. Always-allow: .wazir/ path writes (pipeline state management)
69
+ if ((tool === 'Write' || tool === 'Edit') && isWazirPath(input.file_path)) {
70
+ return { decision: 'allow' };
71
+ }
72
+
73
+ // 5. Always-allow: wazir CLI commands
74
+ if (tool === 'Bash' && isWazirCommand(input.command)) {
75
+ return { decision: 'allow' };
76
+ }
77
+
78
+ // 6. Check Write/Edit restrictions
79
+ if ((tool === 'Write' || tool === 'Edit') && WRITE_BLOCKED_PHASES.has(phase)) {
80
+ return {
81
+ decision: 'deny',
82
+ reason: `Write/Edit blocked during "${phase}" phase. This phase is read-only for project files. Only .wazir/ writes are allowed.`,
83
+ };
84
+ }
85
+
86
+ // 7. Check git mutation restrictions in Bash
87
+ if (tool === 'Bash' && GIT_BLOCKED_PHASES.has(phase) && isGitMutating(input.command)) {
88
+ return {
89
+ decision: 'deny',
90
+ reason: `Git mutations (commit/push) blocked during "${phase}" phase. Git commits are only allowed during the execute phase.`,
91
+ };
92
+ }
93
+
94
+ // 8. Default: allow
95
+ return { decision: 'allow' };
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Helpers
100
+ // ---------------------------------------------------------------------------
101
+
102
+ function isWazirPath(filePath) {
103
+ if (!filePath) return false;
104
+ return filePath.includes('.wazir/') || filePath.includes('/.wazir');
105
+ }
106
+
107
+ function isWazirCommand(command) {
108
+ if (!command) return false;
109
+ const trimmed = command.trim();
110
+ return trimmed.startsWith('wazir ') || trimmed === 'wazir';
111
+ }
112
+
113
+ function isGitMutating(command) {
114
+ if (!command) return false;
115
+ const trimmed = command.trim();
116
+ return GIT_MUTATING_PATTERNS.some(pattern => pattern.test(trimmed));
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // CLI entry point
121
+ // ---------------------------------------------------------------------------
122
+
123
+ const isDirectRun = process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/'));
124
+
125
+ if (isDirectRun) {
126
+ const stateRoot = process.argv[2] || process.env.WAZIR_STATE_ROOT;
127
+ if (!stateRoot) {
128
+ console.log(JSON.stringify({ decision: 'allow' }));
129
+ process.exit(0);
130
+ }
131
+
132
+ let hookInput = {};
133
+ try {
134
+ const input = fs.readFileSync(0, 'utf8').trim();
135
+ if (input) hookInput = JSON.parse(input);
136
+ } catch { /* no stdin */ }
137
+
138
+ const result = evaluatePreToolUse(stateRoot, hookInput);
139
+ console.log(JSON.stringify(result));
140
+ process.exit(0);
141
+ }
@@ -0,0 +1,92 @@
1
+ import fs from 'node:fs';
2
+ import { readPipelineState, setStopHookActive } from '../state/pipeline-state.js';
3
+
4
+ const SAFETY_VALVE_REASONS = new Set(['context-limit', 'user-abort']);
5
+
6
+ /**
7
+ * Evaluate whether the Stop hook should block or allow conversation end.
8
+ *
9
+ * @param {string} stateRoot — path to the pipeline state directory
10
+ * @param {object} context — stop context (may include stop_reason)
11
+ * @returns {{ decision: 'approve'|'block', reason: string }}
12
+ */
13
+ export function evaluateStopGate(stateRoot, context = {}) {
14
+ // 1. No state file → not a pipeline session → allow
15
+ let state;
16
+ try {
17
+ state = readPipelineState(stateRoot);
18
+ } catch {
19
+ return { decision: 'approve', reason: 'State read error — allowing stop.' };
20
+ }
21
+
22
+ if (!state) {
23
+ return { decision: 'approve', reason: 'No pipeline state — no pipeline active, allowing stop.' };
24
+ }
25
+
26
+ // 2. Malformed state (no current_phase)
27
+ if (!state.current_phase) {
28
+ return { decision: 'approve', reason: 'Pipeline state malformed — allowing stop.' };
29
+ }
30
+
31
+ // 3. Safety valve: stop_hook_active flag (infinite loop guard)
32
+ if (state.stop_hook_active) {
33
+ try { setStopHookActive(stateRoot, false); } catch { /* best effort */ }
34
+ return { decision: 'approve', reason: 'Stop hook loop guard active — allowing stop to break loop.' };
35
+ }
36
+
37
+ // 4. Safety valve: context-limit or user-abort
38
+ if (context.stop_reason && SAFETY_VALVE_REASONS.has(context.stop_reason)) {
39
+ return { decision: 'approve', reason: `Safety valve: ${context.stop_reason} — allowing stop.` };
40
+ }
41
+
42
+ // 5. Init phase — pipeline hasn't started real work yet
43
+ if (state.current_phase === 'init') {
44
+ return { decision: 'approve', reason: 'Pipeline at init — no work in progress, allowing stop.' };
45
+ }
46
+
47
+ // 6. Complete phase — all done
48
+ if (state.current_phase === 'complete') {
49
+ return { decision: 'approve', reason: 'Pipeline complete — all phases done.' };
50
+ }
51
+
52
+ // 7. Pipeline is in progress — block
53
+ try { setStopHookActive(stateRoot, true); } catch { /* best effort */ }
54
+
55
+ const remaining = getRemainingPhases(state.current_phase);
56
+ return {
57
+ decision: 'block',
58
+ reason: `Pipeline incomplete: currently in "${state.current_phase}" phase. Remaining: ${remaining.join(', ')}. Complete all phases before stopping.`,
59
+ };
60
+ }
61
+
62
+ function getRemainingPhases(currentPhase) {
63
+ const phases = ['clarify', 'execute', 'verify', 'review', 'complete'];
64
+ const idx = phases.indexOf(currentPhase);
65
+ if (idx === -1) return phases;
66
+ return phases.slice(idx);
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // CLI entry point — reads stateRoot from argv, prints JSON to stdout
71
+ // ---------------------------------------------------------------------------
72
+
73
+ const isDirectRun = process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/'));
74
+
75
+ if (isDirectRun) {
76
+ const stateRoot = process.argv[2] || process.env.WAZIR_STATE_ROOT;
77
+ if (!stateRoot) {
78
+ console.log(JSON.stringify({ decision: 'approve', reason: 'No state root provided.' }));
79
+ process.exit(0);
80
+ }
81
+
82
+ // Read context from stdin if available
83
+ let context = {};
84
+ try {
85
+ const input = fs.readFileSync(0, 'utf8').trim();
86
+ if (input) context = JSON.parse(input);
87
+ } catch { /* no stdin or invalid JSON */ }
88
+
89
+ const result = evaluateStopGate(stateRoot, context);
90
+ console.log(JSON.stringify(result));
91
+ process.exit(0);
92
+ }