@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.
- package/CHANGELOG.md +54 -44
- package/README.md +13 -13
- package/assets/demo.cast +47 -0
- package/assets/demo.gif +0 -0
- package/docs/anti-patterns/AP-23-skipping-enabled-workflows.md +28 -0
- package/docs/anti-patterns/AP-24-clarifier-deciding-scope.md +34 -0
- package/docs/concepts/architecture.md +1 -1
- package/docs/concepts/why-wazir.md +1 -1
- package/docs/readmes/INDEX.md +1 -1
- package/docs/readmes/features/expertise/README.md +1 -1
- package/docs/readmes/features/hooks/pre-compact-summary.md +1 -1
- package/docs/reference/hooks.md +1 -0
- package/docs/reference/launch-checklist.md +3 -3
- package/docs/reference/review-loop-pattern.md +3 -2
- package/docs/reference/skill-tiers.md +2 -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/antipatterns/process/ai-coding-antipatterns.md +117 -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/commands/plan-review.md +3 -1
- package/exports/hosts/claude/.claude/commands/verify.md +30 -1
- package/exports/hosts/claude/.claude/settings.json +7 -6
- package/exports/hosts/claude/export.manifest.json +8 -5
- package/exports/hosts/claude/host-package.json +3 -0
- package/exports/hosts/codex/export.manifest.json +8 -5
- 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 +8 -5
- package/exports/hosts/cursor/host-package.json +3 -0
- package/exports/hosts/gemini/export.manifest.json +8 -5
- 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/llms-full.txt +48 -18
- package/package.json +2 -3
- package/schemas/decision.schema.json +15 -0
- package/schemas/hook.schema.json +4 -1
- package/schemas/phase-report.schema.json +9 -0
- package/skills/TEMPLATE-3-ZONE.md +160 -0
- package/skills/brainstorming/SKILL.md +137 -21
- package/skills/clarifier/SKILL.md +364 -53
- 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 +252 -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 -18
- 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 +270 -57
- package/skills/run-audit/SKILL.md +92 -15
- package/skills/scan-project/SKILL.md +93 -14
- package/skills/self-audit/SKILL.md +133 -39
- package/skills/skill-research/SKILL.md +275 -0
- 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 +160 -17
- package/skills/wazir/SKILL.md +750 -120
- 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 +46 -2
- package/tooling/src/capture/decision.js +40 -0
- package/tooling/src/capture/store.js +33 -0
- package/tooling/src/capture/user-input.js +66 -0
- package/tooling/src/checks/security-sensitivity.js +69 -0
- package/tooling/src/cli.js +28 -26
- 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 +97 -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/init/auto-detect.js +0 -2
- package/tooling/src/init/command.js +3 -95
- 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/tooling/src/status/command.js +6 -1
- package/tooling/src/verify/proof-collector.js +299 -0
- package/wazir.manifest.yaml +3 -0
- package/workflows/learn.md +61 -8
- package/workflows/plan-review.md +3 -1
- 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
|
|
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
|
+
}
|