@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.
- package/CHANGELOG.md +17 -2
- package/docs/research/2026-03-20-agents/a18fb002157904af5.txt +187 -0
- package/docs/research/2026-03-20-agents/a1d0ac79ac2f11e6f.txt +2 -0
- package/docs/research/2026-03-20-agents/a324079de037abd7c.txt +198 -0
- package/docs/research/2026-03-20-agents/a357586bccfafb0e5.txt +256 -0
- package/docs/research/2026-03-20-agents/a4365394e4d753105.txt +137 -0
- package/docs/research/2026-03-20-agents/a492af28bc52d3613.txt +136 -0
- package/docs/research/2026-03-20-agents/a4984db0b6a8eee07.txt +124 -0
- package/docs/research/2026-03-20-agents/a5b30e59d34bbb062.txt +214 -0
- package/docs/research/2026-03-20-agents/a5cf7829dab911586.txt +165 -0
- package/docs/research/2026-03-20-agents/a607157c30dd97c9e.txt +96 -0
- package/docs/research/2026-03-20-agents/a60b68b1e19d1e16b.txt +115 -0
- package/docs/research/2026-03-20-agents/a722af01c5594aba0.txt +166 -0
- package/docs/research/2026-03-20-agents/a787bdc516faa5829.txt +181 -0
- package/docs/research/2026-03-20-agents/a7c46d1bba1056ed2.txt +132 -0
- package/docs/research/2026-03-20-agents/a7e5abbab2b281a0d.txt +100 -0
- package/docs/research/2026-03-20-agents/a8dbadc66cd0d7d5a.txt +95 -0
- package/docs/research/2026-03-20-agents/a904d9f45d6b86a6d.txt +75 -0
- package/docs/research/2026-03-20-agents/a927659a942ee7f60.txt +102 -0
- package/docs/research/2026-03-20-agents/a962cb569191f7583.txt +125 -0
- package/docs/research/2026-03-20-agents/aab6decea538aac41.txt +148 -0
- package/docs/research/2026-03-20-agents/abd58b853dd938a1b.txt +295 -0
- package/docs/research/2026-03-20-agents/ac009da573eff7f65.txt +100 -0
- package/docs/research/2026-03-20-agents/ac1bc783364405e5f.txt +190 -0
- package/docs/research/2026-03-20-agents/aca5e2b57fde152a0.txt +132 -0
- package/docs/research/2026-03-20-agents/ad849b8c0a7e95b8b.txt +176 -0
- package/docs/research/2026-03-20-agents/adc2b12a4da32c962.txt +258 -0
- package/docs/research/2026-03-20-agents/af97caaaa9a80e4cb.txt +146 -0
- package/docs/research/2026-03-20-agents/afc5faceee368b3ca.txt +111 -0
- package/docs/research/2026-03-20-agents/afdb282d866e3c1e4.txt +164 -0
- package/docs/research/2026-03-20-agents/afe9d1f61c02b1e8d.txt +299 -0
- package/docs/research/2026-03-20-agents/b4hmkwril.txt +1856 -0
- package/docs/research/2026-03-20-agents/b80ptk89g.txt +1856 -0
- package/docs/research/2026-03-20-agents/bf54s1jss.txt +1150 -0
- package/docs/research/2026-03-20-agents/bhd6kq2kx.txt +1856 -0
- package/docs/research/2026-03-20-agents/bmb2fodyr.txt +988 -0
- package/docs/research/2026-03-20-agents/bmmsrij8i.txt +826 -0
- package/docs/research/2026-03-20-agents/bn4t2ywpu.txt +2175 -0
- package/docs/research/2026-03-20-agents/bu22t9f1z.txt +0 -0
- package/docs/research/2026-03-20-agents/bwvl98v2p.txt +738 -0
- package/docs/research/2026-03-20-agents/psych-a3697a7fd06eb64fd.txt +135 -0
- package/docs/research/2026-03-20-agents/psych-a37776fabc870feae.txt +123 -0
- package/docs/research/2026-03-20-agents/psych-a5b1fe05c0589efaf.txt +2 -0
- package/docs/research/2026-03-20-agents/psych-a95c15b1f29424435.txt +76 -0
- package/docs/research/2026-03-20-agents/psych-a9c26f4d9172dde7c.txt +2 -0
- package/docs/research/2026-03-20-agents/psych-aa19c69f0ca2c5ad3.txt +2 -0
- package/docs/research/2026-03-20-agents/psych-aa4e4cb70e1be5ecb.txt +95 -0
- package/docs/research/2026-03-20-agents/psych-ab5b302f26a554663.txt +102 -0
- package/docs/research/2026-03-20-deep-research-complete.md +101 -0
- package/docs/research/2026-03-20-deep-research-status.md +38 -0
- package/docs/research/2026-03-20-enforcement-research.md +107 -0
- package/expertise/composition-map.yaml +27 -8
- package/expertise/digests/reviewer/ai-coding-digest.md +83 -0
- package/expertise/digests/reviewer/architectural-thinking-digest.md +63 -0
- package/expertise/digests/reviewer/architecture-antipatterns-digest.md +49 -0
- package/expertise/digests/reviewer/code-smells-digest.md +53 -0
- package/expertise/digests/reviewer/coupling-cohesion-digest.md +54 -0
- package/expertise/digests/reviewer/ddd-digest.md +60 -0
- package/expertise/digests/reviewer/dependency-risk-digest.md +40 -0
- package/expertise/digests/reviewer/error-handling-digest.md +55 -0
- package/expertise/digests/reviewer/review-methodology-digest.md +49 -0
- package/exports/hosts/claude/.claude/commands/learn.md +61 -8
- package/exports/hosts/claude/.claude/settings.json +7 -6
- package/exports/hosts/claude/export.manifest.json +6 -3
- package/exports/hosts/claude/host-package.json +3 -0
- package/exports/hosts/codex/export.manifest.json +6 -3
- package/exports/hosts/codex/host-package.json +3 -0
- package/exports/hosts/cursor/.cursor/hooks.json +6 -6
- package/exports/hosts/cursor/export.manifest.json +6 -3
- package/exports/hosts/cursor/host-package.json +3 -0
- package/exports/hosts/gemini/export.manifest.json +6 -3
- package/exports/hosts/gemini/host-package.json +3 -0
- package/hooks/definitions/pretooluse_dispatcher.yaml +26 -0
- package/hooks/definitions/pretooluse_pipeline_guard.yaml +22 -0
- package/hooks/definitions/stop_pipeline_gate.yaml +22 -0
- package/hooks/hooks.json +7 -6
- package/hooks/pretooluse-dispatcher +84 -0
- package/hooks/pretooluse-pipeline-guard +9 -0
- package/hooks/stop-pipeline-gate +9 -0
- package/package.json +2 -2
- package/schemas/decision.schema.json +15 -0
- package/schemas/hook.schema.json +4 -1
- package/skills/TEMPLATE-3-ZONE.md +160 -0
- package/skills/brainstorming/SKILL.md +127 -23
- package/skills/clarifier/SKILL.md +175 -18
- package/skills/claude-cli/SKILL.md +91 -12
- package/skills/codex-cli/SKILL.md +91 -12
- package/skills/debugging/SKILL.md +133 -38
- package/skills/design/SKILL.md +173 -37
- package/skills/dispatching-parallel-agents/SKILL.md +129 -31
- package/skills/executing-plans/SKILL.md +113 -25
- package/skills/executor/SKILL.md +185 -21
- package/skills/finishing-a-development-branch/SKILL.md +107 -18
- package/skills/gemini-cli/SKILL.md +91 -12
- package/skills/humanize/SKILL.md +92 -13
- package/skills/init-pipeline/SKILL.md +90 -17
- package/skills/prepare-next/SKILL.md +93 -24
- package/skills/receiving-code-review/SKILL.md +90 -16
- package/skills/requesting-code-review/SKILL.md +100 -24
- package/skills/requesting-code-review/code-reviewer.md +29 -17
- package/skills/reviewer/SKILL.md +190 -50
- package/skills/run-audit/SKILL.md +92 -15
- package/skills/scan-project/SKILL.md +93 -14
- package/skills/self-audit/SKILL.md +113 -39
- package/skills/skill-research/SKILL.md +94 -7
- package/skills/subagent-driven-development/SKILL.md +129 -30
- package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +30 -2
- package/skills/subagent-driven-development/implementer-prompt.md +40 -27
- package/skills/subagent-driven-development/spec-reviewer-prompt.md +25 -12
- package/skills/tdd/SKILL.md +125 -20
- package/skills/using-git-worktrees/SKILL.md +118 -28
- package/skills/using-skills/SKILL.md +116 -29
- package/skills/verification/SKILL.md +127 -22
- package/skills/wazir/SKILL.md +517 -153
- package/skills/writing-plans/SKILL.md +134 -28
- package/skills/writing-skills/SKILL.md +91 -13
- package/skills/writing-skills/anthropic-best-practices.md +104 -64
- package/skills/writing-skills/persuasion-principles.md +100 -34
- package/tooling/src/capture/command.js +29 -1
- package/tooling/src/capture/decision.js +40 -0
- package/tooling/src/capture/store.js +1 -0
- package/tooling/src/config/depth-table.js +60 -0
- package/tooling/src/export/compiler.js +7 -8
- package/tooling/src/guards/guardrail-functions.js +131 -0
- package/tooling/src/guards/phase-prerequisite-guard.js +39 -3
- package/tooling/src/hooks/pretooluse-dispatcher.js +300 -0
- package/tooling/src/hooks/pretooluse-pipeline-guard.js +141 -0
- package/tooling/src/hooks/stop-pipeline-gate.js +92 -0
- package/tooling/src/learn/pipeline.js +177 -0
- package/tooling/src/state/db.js +251 -2
- package/tooling/src/state/pipeline-state.js +262 -0
- package/wazir.manifest.yaml +3 -0
- 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
|
+
}
|