@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,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
|
+
}
|
|
@@ -244,8 +244,6 @@ export function autoInit(projectRoot, opts = {}) {
|
|
|
244
244
|
model_mode: 'claude-only',
|
|
245
245
|
default_depth: 'standard',
|
|
246
246
|
default_intent: 'feature',
|
|
247
|
-
team_mode: 'sequential',
|
|
248
|
-
parallel_backend: 'none',
|
|
249
247
|
context_mode: contextMode,
|
|
250
248
|
detected_host: host.host,
|
|
251
249
|
detected_stack: stack,
|
|
@@ -4,10 +4,9 @@ import path from 'node:path';
|
|
|
4
4
|
import { autoInit, detectHost, detectProjectStack } from './auto-detect.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* wazir init [--auto|--
|
|
7
|
+
* wazir init [--auto|--force]
|
|
8
8
|
*
|
|
9
9
|
* Default: --auto (zero-config, no prompts, infer everything)
|
|
10
|
-
* --interactive: legacy mode with @inquirer/prompts (may fail in non-TTY)
|
|
11
10
|
* --force: reinitialize even if already initialized
|
|
12
11
|
*/
|
|
13
12
|
export async function runInitCommand(parsed, context = {}) {
|
|
@@ -15,7 +14,6 @@ export async function runInitCommand(parsed, context = {}) {
|
|
|
15
14
|
const wazirDir = path.join(cwd, '.wazir');
|
|
16
15
|
const configPath = path.join(wazirDir, 'state', 'config.json');
|
|
17
16
|
const isForce = parsed.args.includes('--force');
|
|
18
|
-
const isInteractive = parsed.args.includes('--interactive');
|
|
19
17
|
|
|
20
18
|
// Already initialized check
|
|
21
19
|
if (fs.existsSync(configPath) && !isForce) {
|
|
@@ -25,12 +23,7 @@ export async function runInitCommand(parsed, context = {}) {
|
|
|
25
23
|
};
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
//
|
|
29
|
-
if (isInteractive) {
|
|
30
|
-
return runInteractiveInit(parsed, context);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Default: auto mode — zero-config
|
|
26
|
+
// Auto mode — zero-config
|
|
34
27
|
try {
|
|
35
28
|
const result = autoInit(cwd, { context, force: isForce });
|
|
36
29
|
|
|
@@ -65,8 +58,7 @@ export async function runInitCommand(parsed, context = {}) {
|
|
|
65
58
|
'',
|
|
66
59
|
'Next: /wazir <what you want to build>',
|
|
67
60
|
'',
|
|
68
|
-
'
|
|
69
|
-
'Override: `wazir config set model_mode multi-tool`',
|
|
61
|
+
'Override: `wazir config set model_mode multi-tool`',
|
|
70
62
|
'',
|
|
71
63
|
];
|
|
72
64
|
|
|
@@ -75,87 +67,3 @@ export async function runInitCommand(parsed, context = {}) {
|
|
|
75
67
|
return { exitCode: 1, stderr: `Auto-init failed: ${error.message}\n` };
|
|
76
68
|
}
|
|
77
69
|
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Legacy interactive init with @inquirer/prompts.
|
|
81
|
-
* Kept for power users who want manual control.
|
|
82
|
-
* Will fail in non-TTY environments (Claude Code Bash tool).
|
|
83
|
-
*/
|
|
84
|
-
async function runInteractiveInit(parsed, context = {}) {
|
|
85
|
-
const cwd = context.cwd ?? process.cwd();
|
|
86
|
-
const wazirDir = path.join(cwd, '.wazir');
|
|
87
|
-
const configPath = path.join(wazirDir, 'state', 'config.json');
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
const { select } = await import('@inquirer/prompts');
|
|
91
|
-
|
|
92
|
-
for (const dir of ['input', 'state', 'runs']) {
|
|
93
|
-
fs.mkdirSync(path.join(wazirDir, dir), { recursive: true });
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const modelMode = await select({
|
|
97
|
-
message: 'How should Wazir run in this project?',
|
|
98
|
-
choices: [
|
|
99
|
-
{ name: 'Single model (Recommended)', value: 'claude-only' },
|
|
100
|
-
{ name: 'Multi-model (Haiku/Sonnet/Opus routing)', value: 'multi-model' },
|
|
101
|
-
{ name: 'Multi-tool (current model + external reviewers)', value: 'multi-tool' },
|
|
102
|
-
],
|
|
103
|
-
default: 'claude-only',
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
let multiToolTools = [];
|
|
107
|
-
if (modelMode === 'multi-tool') {
|
|
108
|
-
const toolChoice = await select({
|
|
109
|
-
message: 'Which external tools for reviews?',
|
|
110
|
-
choices: [
|
|
111
|
-
{ name: 'Codex', value: 'codex' },
|
|
112
|
-
{ name: 'Gemini', value: 'gemini' },
|
|
113
|
-
{ name: 'Both', value: 'both' },
|
|
114
|
-
],
|
|
115
|
-
});
|
|
116
|
-
multiToolTools = toolChoice === 'both' ? ['codex', 'gemini'] : [toolChoice];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let codexModel = null;
|
|
120
|
-
if (multiToolTools.includes('codex')) {
|
|
121
|
-
codexModel = await select({
|
|
122
|
-
message: 'Codex model?',
|
|
123
|
-
choices: [
|
|
124
|
-
{ name: 'gpt-5.3-codex-spark (Recommended)', value: 'gpt-5.3-codex-spark' },
|
|
125
|
-
{ name: 'gpt-5.4', value: 'gpt-5.4' },
|
|
126
|
-
],
|
|
127
|
-
default: 'gpt-5.3-codex-spark',
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const host = detectHost();
|
|
132
|
-
const stack = detectProjectStack(cwd);
|
|
133
|
-
|
|
134
|
-
const config = {
|
|
135
|
-
model_mode: modelMode,
|
|
136
|
-
...(modelMode === 'multi-tool' && {
|
|
137
|
-
multi_tool: {
|
|
138
|
-
tools: multiToolTools,
|
|
139
|
-
...(codexModel && { codex: { model: codexModel } }),
|
|
140
|
-
},
|
|
141
|
-
}),
|
|
142
|
-
default_depth: 'standard',
|
|
143
|
-
default_intent: 'feature',
|
|
144
|
-
team_mode: 'sequential',
|
|
145
|
-
parallel_backend: 'none',
|
|
146
|
-
detected_host: host.host,
|
|
147
|
-
detected_stack: stack,
|
|
148
|
-
};
|
|
149
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
exitCode: 0,
|
|
153
|
-
stdout: `\nInitialized (${modelMode}). Host: ${host.host}. Next: /wazir <request>\n`,
|
|
154
|
-
};
|
|
155
|
-
} catch (error) {
|
|
156
|
-
if (error.name === 'ExitPromptError') {
|
|
157
|
-
return { exitCode: 130, stderr: '\nInit cancelled.\n' };
|
|
158
|
-
}
|
|
159
|
-
return { exitCode: 1, stderr: `${error.message}\n` };
|
|
160
|
-
}
|
|
161
|
-
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learning Pipeline — Findings-to-Antipattern Promotion
|
|
3
|
+
*
|
|
4
|
+
* 4-stage pipeline: TALLY → CANDIDATE → PROMOTE → ACTIVE
|
|
5
|
+
*
|
|
6
|
+
* Stage 1 (TALLY): Automatic. Every finding is hashed, categorized, and
|
|
7
|
+
* clustered by canonical pattern. Happens at finding insertion time.
|
|
8
|
+
*
|
|
9
|
+
* Stage 2 (CANDIDATE): Automatic. When a cluster reaches the promotion
|
|
10
|
+
* threshold (3+ occurrences across 2+ runs), it becomes a candidate.
|
|
11
|
+
*
|
|
12
|
+
* Stage 3 (PROMOTE): Human gate. Candidates are proposed for review.
|
|
13
|
+
* User accepts or rejects. Accepted candidates become active antipatterns.
|
|
14
|
+
*
|
|
15
|
+
* Stage 4 (ACTIVE): Automatic. Active antipatterns are loaded into
|
|
16
|
+
* reviewer context for future runs. Hit-rate tracking enables demotion.
|
|
17
|
+
*
|
|
18
|
+
* Drift prevention (from research):
|
|
19
|
+
* - Max 30 active project-level antipatterns
|
|
20
|
+
* - 90-day TTL on candidates (auto-expire if not reviewed)
|
|
21
|
+
* - 5% hit-rate demotion threshold (antipatterns that never trigger get demoted)
|
|
22
|
+
* - Principle consolidation when count exceeds 25
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import crypto from 'node:crypto';
|
|
26
|
+
import {
|
|
27
|
+
upsertFindingCluster,
|
|
28
|
+
getClustersReadyForPromotion,
|
|
29
|
+
promoteClusterToCandidate,
|
|
30
|
+
insertAntipatternCandidate,
|
|
31
|
+
getActiveLearningsCount,
|
|
32
|
+
expireStaleAntipatternCandidates,
|
|
33
|
+
} from '../state/db.js';
|
|
34
|
+
|
|
35
|
+
const MAX_ACTIVE_ANTIPATTERNS = 30;
|
|
36
|
+
const PROMOTION_THRESHOLD_OCCURRENCES = 3;
|
|
37
|
+
const PROMOTION_THRESHOLD_RUNS = 2;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalize a finding description to a canonical form for clustering.
|
|
41
|
+
* Strips file paths, line numbers, variable names, and normalizes whitespace.
|
|
42
|
+
*/
|
|
43
|
+
export function canonicalizeFinding(description) {
|
|
44
|
+
return description
|
|
45
|
+
// Remove file paths
|
|
46
|
+
.replace(/[a-zA-Z0-9_\-./]+\.[a-zA-Z]{1,4}(:\d+)?/g, '<FILE>')
|
|
47
|
+
// Remove line numbers
|
|
48
|
+
.replace(/line \d+/gi, 'line <N>')
|
|
49
|
+
// Remove quoted identifiers
|
|
50
|
+
.replace(/['"`][\w.]+['"`]/g, '<ID>')
|
|
51
|
+
// Remove hex hashes
|
|
52
|
+
.replace(/[0-9a-f]{7,40}/gi, '<HASH>')
|
|
53
|
+
// Normalize whitespace
|
|
54
|
+
.replace(/\s+/g, ' ')
|
|
55
|
+
.trim()
|
|
56
|
+
.toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Hash a canonicalized finding for dedup and clustering.
|
|
61
|
+
*/
|
|
62
|
+
export function hashCanonical(canonicalized) {
|
|
63
|
+
return crypto.createHash('sha256').update(canonicalized).digest('hex').slice(0, 16);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Stage 1: TALLY — Process a finding and cluster it.
|
|
68
|
+
* Called after each finding is inserted into the findings table.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} db - open state database
|
|
71
|
+
* @param {object} finding - { description, category, finding_hash, run_id }
|
|
72
|
+
* @returns {string} cluster ID
|
|
73
|
+
*/
|
|
74
|
+
export function tallyFinding(db, finding) {
|
|
75
|
+
const canonical = canonicalizeFinding(finding.description);
|
|
76
|
+
const canonicalHash = hashCanonical(canonical);
|
|
77
|
+
|
|
78
|
+
return upsertFindingCluster(db, {
|
|
79
|
+
canonical_hash: canonicalHash,
|
|
80
|
+
category: finding.category || '',
|
|
81
|
+
pattern_description: canonical,
|
|
82
|
+
finding_hash: finding.finding_hash,
|
|
83
|
+
run_id: finding.run_id,
|
|
84
|
+
evidence_runs: JSON.stringify([finding.run_id]),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Stage 2: CANDIDATE — Check clusters that meet the promotion threshold.
|
|
90
|
+
* Returns clusters ready for promotion.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} db - open state database
|
|
93
|
+
* @returns {Array} clusters ready for promotion
|
|
94
|
+
*/
|
|
95
|
+
export function identifyCandidates(db) {
|
|
96
|
+
return getClustersReadyForPromotion(
|
|
97
|
+
db,
|
|
98
|
+
PROMOTION_THRESHOLD_OCCURRENCES,
|
|
99
|
+
PROMOTION_THRESHOLD_RUNS,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Stage 2→3: Promote eligible clusters to candidates and generate
|
|
105
|
+
* antipattern proposals.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} db - open state database
|
|
108
|
+
* @param {Array} clusters - from identifyCandidates()
|
|
109
|
+
* @returns {Array} created candidate IDs
|
|
110
|
+
*/
|
|
111
|
+
export function promoteToCandidates(db, clusters) {
|
|
112
|
+
const activeCount = getActiveLearningsCount(db);
|
|
113
|
+
if (activeCount >= MAX_ACTIVE_ANTIPATTERNS) {
|
|
114
|
+
return []; // Drift prevention: don't propose more if at cap
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const candidateIds = [];
|
|
118
|
+
|
|
119
|
+
for (const cluster of clusters) {
|
|
120
|
+
promoteClusterToCandidate(db, cluster.id);
|
|
121
|
+
|
|
122
|
+
const runIds = JSON.parse(cluster.run_ids || '[]');
|
|
123
|
+
const candidateId = insertAntipatternCandidate(db, {
|
|
124
|
+
cluster_id: cluster.id,
|
|
125
|
+
title: `Recurring: ${cluster.category || 'uncategorized'}`,
|
|
126
|
+
description: cluster.pattern_description,
|
|
127
|
+
detection_signal: `Pattern occurred ${cluster.occurrence_count} times across ${cluster.distinct_runs} runs`,
|
|
128
|
+
severity: cluster.occurrence_count >= 5 ? 'high' : 'medium',
|
|
129
|
+
evidence_runs: runIds,
|
|
130
|
+
evidence_count: cluster.occurrence_count,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
candidateIds.push(candidateId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return candidateIds;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Run the full pipeline pass: tally → identify → promote → expire stale.
|
|
141
|
+
* Called by the learn workflow after a run completes.
|
|
142
|
+
*
|
|
143
|
+
* @param {object} db - open state database
|
|
144
|
+
* @param {string} runId - current run ID
|
|
145
|
+
* @param {Array} findings - array of { description, category, severity, source }
|
|
146
|
+
* @returns {object} pipeline results
|
|
147
|
+
*/
|
|
148
|
+
export function runLearningPipeline(db, runId, findings) {
|
|
149
|
+
// Stage 1: Tally all findings
|
|
150
|
+
const clusterIds = [];
|
|
151
|
+
for (const finding of findings) {
|
|
152
|
+
const hash = crypto.createHash('sha256').update(finding.description).digest('hex');
|
|
153
|
+
const clusterId = tallyFinding(db, {
|
|
154
|
+
description: finding.description,
|
|
155
|
+
category: finding.category || '',
|
|
156
|
+
finding_hash: hash,
|
|
157
|
+
run_id: runId,
|
|
158
|
+
});
|
|
159
|
+
clusterIds.push(clusterId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Stage 2: Identify clusters ready for promotion
|
|
163
|
+
const readyClusters = identifyCandidates(db);
|
|
164
|
+
|
|
165
|
+
// Stage 2→3: Promote to candidates
|
|
166
|
+
const newCandidateIds = promoteToCandidates(db, readyClusters);
|
|
167
|
+
|
|
168
|
+
// Housekeeping: expire stale candidates
|
|
169
|
+
expireStaleAntipatternCandidates(db);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
findings_tallied: findings.length,
|
|
173
|
+
clusters_touched: new Set(clusterIds).size,
|
|
174
|
+
new_candidates: newCandidateIds.length,
|
|
175
|
+
candidate_ids: newCandidateIds,
|
|
176
|
+
};
|
|
177
|
+
}
|