@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,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|--interactive|--force]
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
- // Interactive mode — legacy prompts (may fail in non-TTY environments like Claude Code)
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
- 'Power users: `wazir init --interactive` for manual config.',
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
+ }