@wazir-dev/cli 1.2.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 (161) hide show
  1. package/CHANGELOG.md +54 -44
  2. package/README.md +13 -13
  3. package/assets/demo.cast +47 -0
  4. package/assets/demo.gif +0 -0
  5. package/docs/anti-patterns/AP-23-skipping-enabled-workflows.md +28 -0
  6. package/docs/anti-patterns/AP-24-clarifier-deciding-scope.md +34 -0
  7. package/docs/concepts/architecture.md +1 -1
  8. package/docs/concepts/why-wazir.md +1 -1
  9. package/docs/readmes/INDEX.md +1 -1
  10. package/docs/readmes/features/expertise/README.md +1 -1
  11. package/docs/readmes/features/hooks/pre-compact-summary.md +1 -1
  12. package/docs/reference/hooks.md +1 -0
  13. package/docs/reference/launch-checklist.md +3 -3
  14. package/docs/reference/review-loop-pattern.md +3 -2
  15. package/docs/reference/skill-tiers.md +2 -2
  16. package/docs/research/2026-03-20-agents/a18fb002157904af5.txt +187 -0
  17. package/docs/research/2026-03-20-agents/a1d0ac79ac2f11e6f.txt +2 -0
  18. package/docs/research/2026-03-20-agents/a324079de037abd7c.txt +198 -0
  19. package/docs/research/2026-03-20-agents/a357586bccfafb0e5.txt +256 -0
  20. package/docs/research/2026-03-20-agents/a4365394e4d753105.txt +137 -0
  21. package/docs/research/2026-03-20-agents/a492af28bc52d3613.txt +136 -0
  22. package/docs/research/2026-03-20-agents/a4984db0b6a8eee07.txt +124 -0
  23. package/docs/research/2026-03-20-agents/a5b30e59d34bbb062.txt +214 -0
  24. package/docs/research/2026-03-20-agents/a5cf7829dab911586.txt +165 -0
  25. package/docs/research/2026-03-20-agents/a607157c30dd97c9e.txt +96 -0
  26. package/docs/research/2026-03-20-agents/a60b68b1e19d1e16b.txt +115 -0
  27. package/docs/research/2026-03-20-agents/a722af01c5594aba0.txt +166 -0
  28. package/docs/research/2026-03-20-agents/a787bdc516faa5829.txt +181 -0
  29. package/docs/research/2026-03-20-agents/a7c46d1bba1056ed2.txt +132 -0
  30. package/docs/research/2026-03-20-agents/a7e5abbab2b281a0d.txt +100 -0
  31. package/docs/research/2026-03-20-agents/a8dbadc66cd0d7d5a.txt +95 -0
  32. package/docs/research/2026-03-20-agents/a904d9f45d6b86a6d.txt +75 -0
  33. package/docs/research/2026-03-20-agents/a927659a942ee7f60.txt +102 -0
  34. package/docs/research/2026-03-20-agents/a962cb569191f7583.txt +125 -0
  35. package/docs/research/2026-03-20-agents/aab6decea538aac41.txt +148 -0
  36. package/docs/research/2026-03-20-agents/abd58b853dd938a1b.txt +295 -0
  37. package/docs/research/2026-03-20-agents/ac009da573eff7f65.txt +100 -0
  38. package/docs/research/2026-03-20-agents/ac1bc783364405e5f.txt +190 -0
  39. package/docs/research/2026-03-20-agents/aca5e2b57fde152a0.txt +132 -0
  40. package/docs/research/2026-03-20-agents/ad849b8c0a7e95b8b.txt +176 -0
  41. package/docs/research/2026-03-20-agents/adc2b12a4da32c962.txt +258 -0
  42. package/docs/research/2026-03-20-agents/af97caaaa9a80e4cb.txt +146 -0
  43. package/docs/research/2026-03-20-agents/afc5faceee368b3ca.txt +111 -0
  44. package/docs/research/2026-03-20-agents/afdb282d866e3c1e4.txt +164 -0
  45. package/docs/research/2026-03-20-agents/afe9d1f61c02b1e8d.txt +299 -0
  46. package/docs/research/2026-03-20-agents/b4hmkwril.txt +1856 -0
  47. package/docs/research/2026-03-20-agents/b80ptk89g.txt +1856 -0
  48. package/docs/research/2026-03-20-agents/bf54s1jss.txt +1150 -0
  49. package/docs/research/2026-03-20-agents/bhd6kq2kx.txt +1856 -0
  50. package/docs/research/2026-03-20-agents/bmb2fodyr.txt +988 -0
  51. package/docs/research/2026-03-20-agents/bmmsrij8i.txt +826 -0
  52. package/docs/research/2026-03-20-agents/bn4t2ywpu.txt +2175 -0
  53. package/docs/research/2026-03-20-agents/bu22t9f1z.txt +0 -0
  54. package/docs/research/2026-03-20-agents/bwvl98v2p.txt +738 -0
  55. package/docs/research/2026-03-20-agents/psych-a3697a7fd06eb64fd.txt +135 -0
  56. package/docs/research/2026-03-20-agents/psych-a37776fabc870feae.txt +123 -0
  57. package/docs/research/2026-03-20-agents/psych-a5b1fe05c0589efaf.txt +2 -0
  58. package/docs/research/2026-03-20-agents/psych-a95c15b1f29424435.txt +76 -0
  59. package/docs/research/2026-03-20-agents/psych-a9c26f4d9172dde7c.txt +2 -0
  60. package/docs/research/2026-03-20-agents/psych-aa19c69f0ca2c5ad3.txt +2 -0
  61. package/docs/research/2026-03-20-agents/psych-aa4e4cb70e1be5ecb.txt +95 -0
  62. package/docs/research/2026-03-20-agents/psych-ab5b302f26a554663.txt +102 -0
  63. package/docs/research/2026-03-20-deep-research-complete.md +101 -0
  64. package/docs/research/2026-03-20-deep-research-status.md +38 -0
  65. package/docs/research/2026-03-20-enforcement-research.md +107 -0
  66. package/expertise/antipatterns/process/ai-coding-antipatterns.md +117 -0
  67. package/expertise/composition-map.yaml +27 -8
  68. package/expertise/digests/reviewer/ai-coding-digest.md +83 -0
  69. package/expertise/digests/reviewer/architectural-thinking-digest.md +63 -0
  70. package/expertise/digests/reviewer/architecture-antipatterns-digest.md +49 -0
  71. package/expertise/digests/reviewer/code-smells-digest.md +53 -0
  72. package/expertise/digests/reviewer/coupling-cohesion-digest.md +54 -0
  73. package/expertise/digests/reviewer/ddd-digest.md +60 -0
  74. package/expertise/digests/reviewer/dependency-risk-digest.md +40 -0
  75. package/expertise/digests/reviewer/error-handling-digest.md +55 -0
  76. package/expertise/digests/reviewer/review-methodology-digest.md +49 -0
  77. package/exports/hosts/claude/.claude/commands/learn.md +61 -8
  78. package/exports/hosts/claude/.claude/commands/plan-review.md +3 -1
  79. package/exports/hosts/claude/.claude/commands/verify.md +30 -1
  80. package/exports/hosts/claude/.claude/settings.json +7 -6
  81. package/exports/hosts/claude/export.manifest.json +8 -5
  82. package/exports/hosts/claude/host-package.json +3 -0
  83. package/exports/hosts/codex/export.manifest.json +8 -5
  84. package/exports/hosts/codex/host-package.json +3 -0
  85. package/exports/hosts/cursor/.cursor/hooks.json +6 -6
  86. package/exports/hosts/cursor/export.manifest.json +8 -5
  87. package/exports/hosts/cursor/host-package.json +3 -0
  88. package/exports/hosts/gemini/export.manifest.json +8 -5
  89. package/exports/hosts/gemini/host-package.json +3 -0
  90. package/hooks/definitions/pretooluse_dispatcher.yaml +26 -0
  91. package/hooks/definitions/pretooluse_pipeline_guard.yaml +22 -0
  92. package/hooks/definitions/stop_pipeline_gate.yaml +22 -0
  93. package/hooks/hooks.json +7 -6
  94. package/hooks/pretooluse-dispatcher +84 -0
  95. package/hooks/pretooluse-pipeline-guard +9 -0
  96. package/hooks/stop-pipeline-gate +9 -0
  97. package/llms-full.txt +48 -18
  98. package/package.json +2 -3
  99. package/schemas/decision.schema.json +15 -0
  100. package/schemas/hook.schema.json +4 -1
  101. package/schemas/phase-report.schema.json +9 -0
  102. package/skills/TEMPLATE-3-ZONE.md +160 -0
  103. package/skills/brainstorming/SKILL.md +137 -21
  104. package/skills/clarifier/SKILL.md +364 -53
  105. package/skills/claude-cli/SKILL.md +91 -12
  106. package/skills/codex-cli/SKILL.md +91 -12
  107. package/skills/debugging/SKILL.md +133 -38
  108. package/skills/design/SKILL.md +173 -37
  109. package/skills/dispatching-parallel-agents/SKILL.md +129 -31
  110. package/skills/executing-plans/SKILL.md +113 -25
  111. package/skills/executor/SKILL.md +252 -21
  112. package/skills/finishing-a-development-branch/SKILL.md +107 -18
  113. package/skills/gemini-cli/SKILL.md +91 -12
  114. package/skills/humanize/SKILL.md +92 -13
  115. package/skills/init-pipeline/SKILL.md +90 -18
  116. package/skills/prepare-next/SKILL.md +93 -24
  117. package/skills/receiving-code-review/SKILL.md +90 -16
  118. package/skills/requesting-code-review/SKILL.md +100 -24
  119. package/skills/requesting-code-review/code-reviewer.md +29 -17
  120. package/skills/reviewer/SKILL.md +270 -57
  121. package/skills/run-audit/SKILL.md +92 -15
  122. package/skills/scan-project/SKILL.md +93 -14
  123. package/skills/self-audit/SKILL.md +133 -39
  124. package/skills/skill-research/SKILL.md +275 -0
  125. package/skills/subagent-driven-development/SKILL.md +129 -30
  126. package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +30 -2
  127. package/skills/subagent-driven-development/implementer-prompt.md +40 -27
  128. package/skills/subagent-driven-development/spec-reviewer-prompt.md +25 -12
  129. package/skills/tdd/SKILL.md +125 -20
  130. package/skills/using-git-worktrees/SKILL.md +118 -28
  131. package/skills/using-skills/SKILL.md +116 -29
  132. package/skills/verification/SKILL.md +160 -17
  133. package/skills/wazir/SKILL.md +750 -120
  134. package/skills/writing-plans/SKILL.md +134 -28
  135. package/skills/writing-skills/SKILL.md +91 -13
  136. package/skills/writing-skills/anthropic-best-practices.md +104 -64
  137. package/skills/writing-skills/persuasion-principles.md +100 -34
  138. package/tooling/src/capture/command.js +46 -2
  139. package/tooling/src/capture/decision.js +40 -0
  140. package/tooling/src/capture/store.js +33 -0
  141. package/tooling/src/capture/user-input.js +66 -0
  142. package/tooling/src/checks/security-sensitivity.js +69 -0
  143. package/tooling/src/cli.js +28 -26
  144. package/tooling/src/config/depth-table.js +60 -0
  145. package/tooling/src/export/compiler.js +7 -8
  146. package/tooling/src/guards/guardrail-functions.js +131 -0
  147. package/tooling/src/guards/phase-prerequisite-guard.js +97 -3
  148. package/tooling/src/hooks/pretooluse-dispatcher.js +300 -0
  149. package/tooling/src/hooks/pretooluse-pipeline-guard.js +141 -0
  150. package/tooling/src/hooks/stop-pipeline-gate.js +92 -0
  151. package/tooling/src/init/auto-detect.js +0 -2
  152. package/tooling/src/init/command.js +3 -95
  153. package/tooling/src/learn/pipeline.js +177 -0
  154. package/tooling/src/state/db.js +251 -2
  155. package/tooling/src/state/pipeline-state.js +262 -0
  156. package/tooling/src/status/command.js +6 -1
  157. package/tooling/src/verify/proof-collector.js +299 -0
  158. package/wazir.manifest.yaml +3 -0
  159. package/workflows/learn.md +61 -8
  160. package/workflows/plan-review.md +3 -1
  161. package/workflows/verify.md +30 -1
@@ -0,0 +1,131 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Helpers
6
+ // ---------------------------------------------------------------------------
7
+
8
+ function fileExistsAndNonEmpty(filePath) {
9
+ if (!fs.existsSync(filePath)) return false;
10
+ const stat = fs.statSync(filePath);
11
+ return stat.size > 0;
12
+ }
13
+
14
+ function result(passed, reason, missing = []) {
15
+ return { passed, reason, ...(missing.length > 0 ? { missing } : {}) };
16
+ }
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Per-phase validators
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const CLARIFY_ARTIFACTS = [
23
+ 'clarified/clarification.md',
24
+ 'clarified/spec-hardened.md',
25
+ 'clarified/design.md',
26
+ 'clarified/execution-plan.md',
27
+ ];
28
+
29
+ /**
30
+ * Validates clarify phase produced all required artifacts.
31
+ */
32
+ export function validateClarifyComplete(_state, runDir) {
33
+ const missing = [];
34
+ for (const relPath of CLARIFY_ARTIFACTS) {
35
+ const full = path.join(runDir, relPath);
36
+ if (!fileExistsAndNonEmpty(full)) {
37
+ missing.push(relPath);
38
+ }
39
+ }
40
+ if (missing.length > 0) {
41
+ return result(false, `Missing clarify artifacts: ${missing.join(', ')}`, missing);
42
+ }
43
+ return result(true, 'All clarify artifacts present and non-empty.');
44
+ }
45
+
46
+ /**
47
+ * Validates execute phase: at least one task artifact dir and verification proof.
48
+ */
49
+ export function validateExecuteComplete(_state, runDir) {
50
+ const missing = [];
51
+ const artifactsDir = path.join(runDir, 'artifacts');
52
+
53
+ // Check for at least one task-NNN directory with content
54
+ const taskDirs = fs.existsSync(artifactsDir)
55
+ ? fs.readdirSync(artifactsDir).filter(d => d.startsWith('task-') && fs.statSync(path.join(artifactsDir, d)).isDirectory())
56
+ : [];
57
+
58
+ if (taskDirs.length === 0) {
59
+ missing.push('artifacts/task-NNN/ (no task artifacts found)');
60
+ }
61
+
62
+ // Check verification proof
63
+ const proofPath = path.join(artifactsDir, 'verification-proof.md');
64
+ if (!fileExistsAndNonEmpty(proofPath)) {
65
+ missing.push('artifacts/verification-proof.md');
66
+ }
67
+
68
+ if (missing.length > 0) {
69
+ return result(false, `Missing execute artifacts: ${missing.join(', ')}`, missing);
70
+ }
71
+ return result(true, `Execute complete: ${taskDirs.length} task(s) + verification proof.`);
72
+ }
73
+
74
+ /**
75
+ * Validates verify phase: proof exists and has substantive content.
76
+ */
77
+ export function validateVerifyComplete(_state, runDir) {
78
+ const proofPath = path.join(runDir, 'artifacts', 'verification-proof.md');
79
+ if (!fileExistsAndNonEmpty(proofPath)) {
80
+ return result(false, 'Verification proof missing or empty.', ['artifacts/verification-proof.md']);
81
+ }
82
+
83
+ const content = fs.readFileSync(proofPath, 'utf8');
84
+ if (content.trim().length < 20) {
85
+ return result(false, 'Verification proof exists but has insufficient content.', ['artifacts/verification-proof.md']);
86
+ }
87
+
88
+ return result(true, 'Verification proof present with evidence.');
89
+ }
90
+
91
+ /**
92
+ * Validates review phase: verdict.json with a numeric score.
93
+ */
94
+ export function validateReviewComplete(_state, runDir) {
95
+ const verdictPath = path.join(runDir, 'reviews', 'verdict.json');
96
+ if (!fs.existsSync(verdictPath)) {
97
+ return result(false, 'Review verdict missing.', ['reviews/verdict.json']);
98
+ }
99
+
100
+ try {
101
+ const verdict = JSON.parse(fs.readFileSync(verdictPath, 'utf8'));
102
+ if (typeof verdict.score !== 'number') {
103
+ return result(false, 'Review verdict has no numeric score.', ['reviews/verdict.json (missing score)']);
104
+ }
105
+ return result(true, `Review complete with score ${verdict.score}.`);
106
+ } catch {
107
+ return result(false, 'Review verdict is not valid JSON.', ['reviews/verdict.json']);
108
+ }
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Dispatcher
113
+ // ---------------------------------------------------------------------------
114
+
115
+ const VALIDATORS = {
116
+ clarify: validateClarifyComplete,
117
+ execute: validateExecuteComplete,
118
+ verify: validateVerifyComplete,
119
+ review: validateReviewComplete,
120
+ };
121
+
122
+ /**
123
+ * Run the guardrail for a given phase.
124
+ */
125
+ export function runGuardrail(phase, state, runDir) {
126
+ const validator = VALIDATORS[phase];
127
+ if (!validator) {
128
+ throw new Error(`Unknown phase for guardrail: ${phase}`);
129
+ }
130
+ return validator(state, runDir);
131
+ }
@@ -4,6 +4,64 @@ import path from 'node:path';
4
4
  import { readYamlFile } from '../loaders.js';
5
5
  import { getRunPaths, readPhaseExitEvents } from '../capture/store.js';
6
6
 
7
+ /**
8
+ * Validates that every enabled workflow has a phase_exit event
9
+ * in the run's events.ndjson before the run can be marked complete.
10
+ *
11
+ * If a run-config with workflow_policy exists, only workflows with
12
+ * enabled: true are checked. Otherwise falls back to the manifest list.
13
+ */
14
+ export function validateRunCompletion(runDir, manifestPath) {
15
+ const manifest = readYamlFile(manifestPath);
16
+ const declaredWorkflows = manifest.workflows ?? [];
17
+
18
+ if (declaredWorkflows.length === 0) {
19
+ return { complete: true, missing: [] };
20
+ }
21
+
22
+ // Filter to enabled workflows if run-config exists
23
+ const runConfigPath = path.join(runDir, 'run-config.yaml');
24
+ let enabledWorkflows = declaredWorkflows;
25
+ if (fs.existsSync(runConfigPath)) {
26
+ try {
27
+ const runConfig = readYamlFile(runConfigPath);
28
+ const policy = runConfig.workflow_policy;
29
+ if (policy && typeof policy === 'object') {
30
+ enabledWorkflows = declaredWorkflows.filter(w => {
31
+ const wPolicy = policy[w] ?? policy[w.replace(/_/g, '-')];
32
+ // If no policy entry, assume enabled; if entry exists, check enabled field
33
+ return wPolicy ? (wPolicy.enabled !== false) : true;
34
+ });
35
+ }
36
+ } catch {
37
+ // If run-config can't be read, fall back to full manifest list
38
+ }
39
+ }
40
+
41
+ const eventsPath = path.join(runDir, 'events.ndjson');
42
+ const completedWorkflows = new Set();
43
+
44
+ if (fs.existsSync(eventsPath)) {
45
+ const content = fs.readFileSync(eventsPath, 'utf8');
46
+ for (const line of content.split('\n')) {
47
+ const trimmed = line.trim();
48
+ if (!trimmed) continue;
49
+ try {
50
+ const event = JSON.parse(trimmed);
51
+ if (event.event === 'phase_exit' && event.status === 'completed' && event.phase) {
52
+ completedWorkflows.add(event.phase);
53
+ }
54
+ } catch {
55
+ // Skip malformed lines
56
+ }
57
+ }
58
+ }
59
+
60
+ const missing = enabledWorkflows.filter(w => !completedWorkflows.has(w));
61
+
62
+ return { complete: missing.length === 0, missing };
63
+ }
64
+
7
65
  export function evaluateScopeCoverageGuard(payload) {
8
66
  const { input_item_count: inputCount, plan_task_count: planCount, user_approved_reduction: userApproved } = payload;
9
67
 
@@ -85,10 +143,42 @@ export function evaluatePhasePrerequisiteGuard(payload) {
85
143
  const requiredPhaseExits = prerequisites.required_phase_exits ?? [];
86
144
 
87
145
  const missingArtifacts = [];
146
+ const failedProofs = [];
88
147
  for (const artifact of requiredArtifacts) {
89
148
  const artifactPath = path.join(runPaths.runRoot, artifact);
90
149
  if (!fs.existsSync(artifactPath)) {
91
150
  missingArtifacts.push(artifact);
151
+ continue;
152
+ }
153
+
154
+ const basename = path.basename(artifact);
155
+
156
+ // Content validation for proof JSON files (e.g. proof-task-001.json, verification-proof.json)
157
+ if (basename.includes('proof') && basename.endsWith('.json')) {
158
+ try {
159
+ const content = fs.readFileSync(artifactPath, 'utf8');
160
+ const parsed = JSON.parse(content);
161
+ if (parsed.all_passed !== true) {
162
+ failedProofs.push(`${artifact}: all_passed is not true (got ${JSON.stringify(parsed.all_passed)})`);
163
+ }
164
+ } catch {
165
+ // Fail closed: malformed JSON blocks the phase
166
+ failedProofs.push(`${artifact}: malformed or unreadable JSON`);
167
+ }
168
+ continue;
169
+ }
170
+
171
+ // Content validation for verification-proof.md
172
+ if (basename === 'verification-proof.md') {
173
+ try {
174
+ const content = fs.readFileSync(artifactPath, 'utf8');
175
+ const lower = content.toLowerCase();
176
+ if (!lower.includes('status: pass') && !content.includes('PASS')) {
177
+ failedProofs.push(`${artifact}: does not contain "status: pass" or "PASS"`);
178
+ }
179
+ } catch {
180
+ failedProofs.push(`${artifact}: unreadable`);
181
+ }
92
182
  }
93
183
  }
94
184
 
@@ -100,10 +190,10 @@ export function evaluatePhasePrerequisiteGuard(payload) {
100
190
  }
101
191
  }
102
192
 
103
- // OR-logic for resumed runs: if all artifacts exist, pass even without phase_exit events.
193
+ // OR-logic for resumed runs: if all artifacts exist and proofs pass, allow even without phase_exit events.
104
194
  // Artifacts are the hard evidence; phase_exits are supplementary.
105
- // But if artifacts are missing, phase_exits alone are not sufficient.
106
- if (missingArtifacts.length === 0) {
195
+ // But if artifacts are missing or proofs fail, phase_exits alone are not sufficient.
196
+ if (missingArtifacts.length === 0 && failedProofs.length === 0) {
107
197
  return {
108
198
  allowed: true,
109
199
  reason: `All prerequisite artifacts present for phase ${phase}.`,
@@ -114,6 +204,9 @@ export function evaluatePhasePrerequisiteGuard(payload) {
114
204
  if (missingArtifacts.length > 0) {
115
205
  reasons.push(`Missing artifacts: ${missingArtifacts.join(', ')}`);
116
206
  }
207
+ if (failedProofs.length > 0) {
208
+ reasons.push(`Failed proof validation: ${failedProofs.join('; ')}`);
209
+ }
117
210
  if (missingPhaseExits.length > 0) {
118
211
  reasons.push(`Missing phase exits: ${missingPhaseExits.join(', ')}`);
119
212
  }
@@ -122,6 +215,7 @@ export function evaluatePhasePrerequisiteGuard(payload) {
122
215
  allowed: false,
123
216
  reason: reasons.join('. '),
124
217
  missing_artifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
218
+ failed_proofs: failedProofs.length > 0 ? failedProofs : undefined,
125
219
  missing_phase_exits: missingPhaseExits.length > 0 ? missingPhaseExits : undefined,
126
220
  };
127
221
  }
@@ -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
+ }