@wazir-dev/cli 1.0.0 → 1.2.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 (163) hide show
  1. package/CHANGELOG.md +100 -2
  2. package/README.md +6 -6
  3. package/docs/concepts/architecture.md +1 -1
  4. package/docs/concepts/roles-and-workflows.md +2 -0
  5. package/docs/concepts/why-wazir.md +59 -0
  6. package/docs/decisions/2026-03-19-deferred-items.md +564 -0
  7. package/docs/decisions/2026-03-19-enhancement-decisions.md +300 -0
  8. package/docs/plans/2026-03-15-cli-pipeline-integration-plan.md +1 -1
  9. package/docs/readmes/INDEX.md +21 -5
  10. package/docs/readmes/features/expertise/README.md +2 -2
  11. package/docs/readmes/features/exports/README.md +2 -2
  12. package/docs/readmes/features/schemas/README.md +3 -0
  13. package/docs/readmes/features/skills/README.md +17 -0
  14. package/docs/readmes/features/skills/clarifier.md +5 -0
  15. package/docs/readmes/features/skills/claude-cli.md +5 -0
  16. package/docs/readmes/features/skills/codex-cli.md +5 -0
  17. package/docs/readmes/features/skills/dispatching-parallel-agents.md +5 -0
  18. package/docs/readmes/features/skills/executing-plans.md +5 -0
  19. package/docs/readmes/features/skills/executor.md +5 -0
  20. package/docs/readmes/features/skills/finishing-a-development-branch.md +5 -0
  21. package/docs/readmes/features/skills/gemini-cli.md +5 -0
  22. package/docs/readmes/features/skills/humanize.md +5 -0
  23. package/docs/readmes/features/skills/init-pipeline.md +5 -0
  24. package/docs/readmes/features/skills/receiving-code-review.md +5 -0
  25. package/docs/readmes/features/skills/requesting-code-review.md +5 -0
  26. package/docs/readmes/features/skills/reviewer.md +5 -0
  27. package/docs/readmes/features/skills/subagent-driven-development.md +5 -0
  28. package/docs/readmes/features/skills/using-git-worktrees.md +5 -0
  29. package/docs/readmes/features/skills/wazir.md +5 -0
  30. package/docs/readmes/features/skills/writing-skills.md +5 -0
  31. package/docs/readmes/features/workflows/prepare-next.md +1 -1
  32. package/docs/reference/configuration-reference.md +47 -6
  33. package/docs/reference/launch-checklist.md +4 -4
  34. package/docs/reference/review-loop-pattern.md +538 -0
  35. package/docs/reference/roles-reference.md +1 -0
  36. package/docs/reference/skill-tiers.md +147 -0
  37. package/docs/reference/tooling-cli.md +5 -1
  38. package/docs/truth-claims.yaml +18 -0
  39. package/expertise/antipatterns/process/ai-coding-antipatterns.md +97 -1
  40. package/exports/hosts/claude/.claude/agents/clarifier.md +3 -0
  41. package/exports/hosts/claude/.claude/agents/designer.md +3 -0
  42. package/exports/hosts/claude/.claude/agents/executor.md +2 -0
  43. package/exports/hosts/claude/.claude/agents/planner.md +3 -0
  44. package/exports/hosts/claude/.claude/agents/researcher.md +2 -0
  45. package/exports/hosts/claude/.claude/agents/reviewer.md +5 -1
  46. package/exports/hosts/claude/.claude/agents/specifier.md +3 -0
  47. package/exports/hosts/claude/.claude/commands/clarify.md +4 -0
  48. package/exports/hosts/claude/.claude/commands/design-review.md +4 -0
  49. package/exports/hosts/claude/.claude/commands/design.md +4 -0
  50. package/exports/hosts/claude/.claude/commands/discover.md +4 -0
  51. package/exports/hosts/claude/.claude/commands/execute.md +4 -0
  52. package/exports/hosts/claude/.claude/commands/plan-review.md +4 -0
  53. package/exports/hosts/claude/.claude/commands/plan.md +4 -0
  54. package/exports/hosts/claude/.claude/commands/spec-challenge.md +4 -0
  55. package/exports/hosts/claude/.claude/commands/specify.md +4 -0
  56. package/exports/hosts/claude/.claude/commands/verify.md +4 -0
  57. package/exports/hosts/claude/.claude/settings.json +9 -0
  58. package/exports/hosts/claude/CLAUDE.md +1 -1
  59. package/exports/hosts/claude/export.manifest.json +22 -20
  60. package/exports/hosts/claude/host-package.json +3 -1
  61. package/exports/hosts/codex/AGENTS.md +1 -1
  62. package/exports/hosts/codex/export.manifest.json +22 -20
  63. package/exports/hosts/codex/host-package.json +3 -1
  64. package/exports/hosts/cursor/.cursor/hooks.json +4 -0
  65. package/exports/hosts/cursor/.cursor/rules/wazir-core.mdc +1 -1
  66. package/exports/hosts/cursor/export.manifest.json +22 -20
  67. package/exports/hosts/cursor/host-package.json +3 -1
  68. package/exports/hosts/gemini/GEMINI.md +1 -1
  69. package/exports/hosts/gemini/export.manifest.json +22 -20
  70. package/exports/hosts/gemini/host-package.json +3 -1
  71. package/hooks/context-mode-router +191 -0
  72. package/hooks/definitions/context_mode_router.yaml +19 -0
  73. package/hooks/definitions/loop_cap_guard.yaml +1 -1
  74. package/hooks/hooks.json +43 -0
  75. package/hooks/protected-path-write-guard +8 -0
  76. package/hooks/routing-matrix.json +45 -0
  77. package/hooks/session-start +62 -1
  78. package/llms-full.txt +905 -132
  79. package/package.json +3 -3
  80. package/roles/clarifier.md +3 -0
  81. package/roles/designer.md +3 -0
  82. package/roles/executor.md +2 -0
  83. package/roles/planner.md +3 -0
  84. package/roles/researcher.md +2 -0
  85. package/roles/reviewer.md +5 -1
  86. package/roles/specifier.md +3 -0
  87. package/schemas/hook.schema.json +2 -1
  88. package/schemas/phase-report.schema.json +80 -0
  89. package/schemas/usage.schema.json +25 -1
  90. package/schemas/wazir-manifest.schema.json +19 -0
  91. package/skills/brainstorming/SKILL.md +20 -56
  92. package/skills/clarifier/SKILL.md +243 -0
  93. package/skills/claude-cli/SKILL.md +320 -0
  94. package/skills/codex-cli/SKILL.md +260 -0
  95. package/skills/debugging/SKILL.md +24 -1
  96. package/skills/design/SKILL.md +13 -0
  97. package/skills/dispatching-parallel-agents/SKILL.md +13 -0
  98. package/skills/executing-plans/SKILL.md +28 -2
  99. package/skills/executor/SKILL.md +129 -0
  100. package/skills/finishing-a-development-branch/SKILL.md +13 -0
  101. package/skills/gemini-cli/SKILL.md +260 -0
  102. package/skills/humanize/SKILL.md +13 -0
  103. package/skills/init-pipeline/SKILL.md +76 -78
  104. package/skills/prepare-next/SKILL.md +81 -10
  105. package/skills/receiving-code-review/SKILL.md +21 -0
  106. package/skills/requesting-code-review/SKILL.md +38 -5
  107. package/skills/reviewer/SKILL.md +423 -0
  108. package/skills/run-audit/SKILL.md +13 -0
  109. package/skills/scan-project/SKILL.md +13 -0
  110. package/skills/self-audit/SKILL.md +197 -16
  111. package/skills/subagent-driven-development/SKILL.md +38 -2
  112. package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +2 -0
  113. package/skills/subagent-driven-development/implementer-prompt.md +8 -0
  114. package/skills/subagent-driven-development/spec-reviewer-prompt.md +7 -0
  115. package/skills/tdd/SKILL.md +21 -0
  116. package/skills/using-git-worktrees/SKILL.md +13 -0
  117. package/skills/using-skills/SKILL.md +13 -0
  118. package/skills/verification/SKILL.md +13 -0
  119. package/skills/wazir/SKILL.md +286 -262
  120. package/skills/writing-plans/SKILL.md +44 -4
  121. package/skills/writing-skills/SKILL.md +13 -0
  122. package/templates/artifacts/implementation-plan.md +3 -0
  123. package/templates/artifacts/tasks-template.md +133 -0
  124. package/templates/examples/phase-report.example.json +48 -0
  125. package/templates/examples/wazir-manifest.example.yaml +1 -1
  126. package/tooling/src/adapters/composition-engine.js +256 -0
  127. package/tooling/src/adapters/model-router.js +84 -0
  128. package/tooling/src/capture/command.js +111 -2
  129. package/tooling/src/capture/run-config.js +23 -0
  130. package/tooling/src/capture/store.js +24 -0
  131. package/tooling/src/capture/usage.js +106 -0
  132. package/tooling/src/checks/ac-matrix.js +256 -0
  133. package/tooling/src/checks/brand-truth.js +3 -6
  134. package/tooling/src/checks/command-registry.js +13 -0
  135. package/tooling/src/checks/docs-truth.js +1 -1
  136. package/tooling/src/checks/runtime-surface.js +3 -7
  137. package/tooling/src/checks/skills.js +111 -0
  138. package/tooling/src/cli.js +17 -3
  139. package/tooling/src/commands/stats.js +161 -0
  140. package/tooling/src/commands/validate.js +5 -1
  141. package/tooling/src/export/compiler.js +33 -37
  142. package/tooling/src/gating/agent.js +145 -0
  143. package/tooling/src/guards/phase-prerequisite-guard.js +127 -0
  144. package/tooling/src/hooks/routing-logic.js +69 -0
  145. package/tooling/src/init/auto-detect.js +260 -0
  146. package/tooling/src/init/command.js +161 -0
  147. package/tooling/src/input/scanner.js +46 -0
  148. package/tooling/src/reports/command.js +103 -0
  149. package/tooling/src/reports/phase-report.js +323 -0
  150. package/tooling/src/state/command.js +160 -0
  151. package/tooling/src/state/db.js +287 -0
  152. package/tooling/src/status/command.js +53 -1
  153. package/wazir.manifest.yaml +26 -17
  154. package/workflows/clarify.md +4 -0
  155. package/workflows/design-review.md +4 -0
  156. package/workflows/design.md +4 -0
  157. package/workflows/discover.md +4 -0
  158. package/workflows/execute.md +4 -0
  159. package/workflows/plan-review.md +4 -0
  160. package/workflows/plan.md +4 -0
  161. package/workflows/spec-challenge.md +4 -0
  162. package/workflows/specify.md +4 -0
  163. package/workflows/verify.md +4 -0
@@ -0,0 +1,127 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { readYamlFile } from '../loaders.js';
5
+ import { getRunPaths, readPhaseExitEvents } from '../capture/store.js';
6
+
7
+ export function evaluateScopeCoverageGuard(payload) {
8
+ const { input_item_count: inputCount, plan_task_count: planCount, user_approved_reduction: userApproved } = payload;
9
+
10
+ const safeInputCount = inputCount ?? 0;
11
+ const safePlanCount = planCount ?? 0;
12
+
13
+ if (safeInputCount === 0) {
14
+ return {
15
+ allowed: true,
16
+ reason: 'No input items to check against.',
17
+ input_count: safeInputCount,
18
+ plan_count: safePlanCount,
19
+ };
20
+ }
21
+
22
+ if (safePlanCount >= safeInputCount) {
23
+ return {
24
+ allowed: true,
25
+ reason: `Plan covers all input items (${safePlanCount} tasks >= ${safeInputCount} items).`,
26
+ input_count: safeInputCount,
27
+ plan_count: safePlanCount,
28
+ };
29
+ }
30
+
31
+ if (userApproved === true) {
32
+ return {
33
+ allowed: true,
34
+ reason: `User explicitly approved scope reduction (${safePlanCount} tasks < ${safeInputCount} items).`,
35
+ input_count: safeInputCount,
36
+ plan_count: safePlanCount,
37
+ };
38
+ }
39
+
40
+ return {
41
+ allowed: false,
42
+ reason: `Scope reduction detected: plan has ${safePlanCount} tasks but input has ${safeInputCount} items. User approval required.`,
43
+ input_count: safeInputCount,
44
+ plan_count: safePlanCount,
45
+ };
46
+ }
47
+
48
+ export function evaluatePhasePrerequisiteGuard(payload) {
49
+ const { run_id: runId, phase, state_root: stateRoot, project_root: projectRoot } = payload;
50
+
51
+ if (!runId) {
52
+ throw new Error('run_id is required');
53
+ }
54
+
55
+ if (!phase) {
56
+ throw new Error('phase is required');
57
+ }
58
+
59
+ if (!stateRoot) {
60
+ throw new Error('state_root is required');
61
+ }
62
+
63
+ if (!projectRoot) {
64
+ throw new Error('project_root is required');
65
+ }
66
+
67
+ const runPaths = getRunPaths(stateRoot, runId);
68
+
69
+ if (!fs.existsSync(runPaths.statusPath)) {
70
+ throw new Error(`status.json not found for run ${runId}`);
71
+ }
72
+
73
+ const manifestPath = path.join(projectRoot, 'wazir.manifest.yaml');
74
+ const manifest = readYamlFile(manifestPath);
75
+ const prerequisites = manifest.phase_prerequisites?.[phase];
76
+
77
+ if (!prerequisites || (Object.keys(prerequisites).length === 0)) {
78
+ return {
79
+ allowed: true,
80
+ reason: `No prerequisites defined for phase ${phase}.`,
81
+ };
82
+ }
83
+
84
+ const requiredArtifacts = prerequisites.required_artifacts ?? [];
85
+ const requiredPhaseExits = prerequisites.required_phase_exits ?? [];
86
+
87
+ const missingArtifacts = [];
88
+ for (const artifact of requiredArtifacts) {
89
+ const artifactPath = path.join(runPaths.runRoot, artifact);
90
+ if (!fs.existsSync(artifactPath)) {
91
+ missingArtifacts.push(artifact);
92
+ }
93
+ }
94
+
95
+ const completedPhases = readPhaseExitEvents(runPaths);
96
+ const missingPhaseExits = [];
97
+ for (const requiredPhase of requiredPhaseExits) {
98
+ if (!completedPhases.includes(requiredPhase)) {
99
+ missingPhaseExits.push(requiredPhase);
100
+ }
101
+ }
102
+
103
+ // OR-logic for resumed runs: if all artifacts exist, pass even without phase_exit events.
104
+ // 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) {
107
+ return {
108
+ allowed: true,
109
+ reason: `All prerequisite artifacts present for phase ${phase}.`,
110
+ };
111
+ }
112
+
113
+ const reasons = [];
114
+ if (missingArtifacts.length > 0) {
115
+ reasons.push(`Missing artifacts: ${missingArtifacts.join(', ')}`);
116
+ }
117
+ if (missingPhaseExits.length > 0) {
118
+ reasons.push(`Missing phase exits: ${missingPhaseExits.join(', ')}`);
119
+ }
120
+
121
+ return {
122
+ allowed: false,
123
+ reason: reasons.join('. '),
124
+ missing_artifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
125
+ missing_phase_exits: missingPhaseExits.length > 0 ? missingPhaseExits : undefined,
126
+ };
127
+ }
@@ -0,0 +1,69 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ function firstToken(cmd) {
5
+ return cmd.split(/\s+/)[0] || '';
6
+ }
7
+
8
+ function hasPipe(cmd) {
9
+ return /(?<![\\])\|/.test(cmd);
10
+ }
11
+
12
+ function hasRedirect(cmd) {
13
+ return /(?<![\\])[>]/.test(cmd);
14
+ }
15
+
16
+ export function loadRoutingMatrix(projectRoot) {
17
+ const matrixPath = join(projectRoot, 'hooks', 'routing-matrix.json');
18
+ return JSON.parse(readFileSync(matrixPath, 'utf8'));
19
+ }
20
+
21
+ export function classifyCommand(cmd, matrix) {
22
+ const command = (cmd || '').trim();
23
+
24
+ if (!matrix) {
25
+ return { route: 'small', reason: 'matrix missing — safe fallback' };
26
+ }
27
+
28
+ // 1. Explicit context-mode marker always wins
29
+ if (command.includes('# wazir:context-mode')) {
30
+ return { route: 'large', reason: 'explicit context-mode marker' };
31
+ }
32
+
33
+ // 2. Check large patterns FIRST — large commands are never downgraded
34
+ for (const pattern of matrix.large) {
35
+ if (command === pattern || command.startsWith(pattern + ' ') || command.startsWith(pattern + '\t')) {
36
+ return { route: 'large', reason: `matched large pattern: ${pattern}` };
37
+ }
38
+ }
39
+
40
+ // 3. Passthrough marker — only honoured when command is NOT large
41
+ if (command.includes('# wazir:passthrough')) {
42
+ return { route: 'small', reason: 'explicit passthrough marker' };
43
+ }
44
+
45
+ // 4. Check small patterns
46
+ for (const pattern of matrix.small) {
47
+ if (command === pattern || command.startsWith(pattern + ' ') || command.startsWith(pattern + '\t')) {
48
+ return { route: 'small', reason: `matched small pattern: ${pattern}` };
49
+ }
50
+ }
51
+
52
+ // 5. Ambiguous heuristics
53
+ const heuristic = matrix.ambiguous_heuristic || {};
54
+
55
+ if (heuristic.pipe_detected && hasPipe(command)) {
56
+ return { route: 'ambiguous', reason: 'pipe detected' };
57
+ }
58
+ if (heuristic.redirect_detected && hasRedirect(command)) {
59
+ return { route: 'ambiguous', reason: 'redirect detected' };
60
+ }
61
+
62
+ const bin = firstToken(command);
63
+ if (Array.isArray(heuristic.verbose_binaries) && heuristic.verbose_binaries.includes(bin)) {
64
+ return { route: 'ambiguous', reason: `verbose binary: ${bin}` };
65
+ }
66
+
67
+ // Default: unknown commands pass through
68
+ return { route: 'small', reason: 'no pattern matched — default passthrough' };
69
+ }
@@ -0,0 +1,260 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ /**
6
+ * Detect which AI host CLI is running this process.
7
+ * Checks environment variables, process ancestry, and known file markers.
8
+ *
9
+ * @returns {{ host: string, confidence: string, signals: string[] }}
10
+ */
11
+ export function detectHost() {
12
+ const signals = [];
13
+
14
+ // Claude Code detection
15
+ if (process.env.CLAUDE_CODE || process.env.CLAUDE_CODE_ENTRYPOINT) {
16
+ signals.push('CLAUDE_CODE env var');
17
+ return { host: 'claude', confidence: 'high', signals };
18
+ }
19
+ if (fs.existsSync(path.join(os.homedir(), '.claude'))) {
20
+ signals.push('~/.claude directory exists');
21
+ }
22
+
23
+ // Codex detection
24
+ if (process.env.CODEX_CLI || process.env.OPENAI_API_KEY) {
25
+ signals.push('CODEX_CLI or OPENAI_API_KEY env var');
26
+ }
27
+ if (process.env.CODEX_SANDBOX_MODE) {
28
+ signals.push('CODEX_SANDBOX_MODE env var');
29
+ return { host: 'codex', confidence: 'high', signals };
30
+ }
31
+
32
+ // Gemini detection
33
+ if (process.env.GEMINI_API_KEY || process.env.GOOGLE_AI_API_KEY) {
34
+ signals.push('Gemini API key env var');
35
+ }
36
+ if (process.env.GEMINI_CLI) {
37
+ signals.push('GEMINI_CLI env var');
38
+ return { host: 'gemini', confidence: 'high', signals };
39
+ }
40
+
41
+ // Cursor detection
42
+ if (process.env.CURSOR_SESSION || process.env.CURSOR_TRACE_ID) {
43
+ signals.push('Cursor session env var');
44
+ return { host: 'cursor', confidence: 'high', signals };
45
+ }
46
+
47
+ // Fallback: check for marker files
48
+ const cwd = process.cwd();
49
+ if (fs.existsSync(path.join(cwd, '.claude', 'settings.json'))) {
50
+ signals.push('.claude/settings.json exists in project');
51
+ return { host: 'claude', confidence: 'medium', signals };
52
+ }
53
+ if (fs.existsSync(path.join(cwd, 'AGENTS.md'))) {
54
+ signals.push('AGENTS.md exists (Codex marker)');
55
+ return { host: 'codex', confidence: 'medium', signals };
56
+ }
57
+ if (fs.existsSync(path.join(cwd, 'GEMINI.md'))) {
58
+ signals.push('GEMINI.md exists (Gemini marker)');
59
+ return { host: 'gemini', confidence: 'medium', signals };
60
+ }
61
+ if (fs.existsSync(path.join(cwd, '.cursorrules'))) {
62
+ signals.push('.cursorrules exists (Cursor marker)');
63
+ return { host: 'cursor', confidence: 'medium', signals };
64
+ }
65
+
66
+ // Default: assume Claude Code (most common)
67
+ if (signals.includes('~/.claude directory exists')) {
68
+ return { host: 'claude', confidence: 'low', signals };
69
+ }
70
+
71
+ return { host: 'claude', confidence: 'low', signals: ['no host detected, defaulting to claude'] };
72
+ }
73
+
74
+ /**
75
+ * Auto-detect project stack from package files, config, and structure.
76
+ *
77
+ * @param {string} projectRoot
78
+ * @returns {{ language: string, framework: string|null, stack: string[] }}
79
+ */
80
+ export function detectProjectStack(projectRoot) {
81
+ const stack = [];
82
+ let language = 'unknown';
83
+ let framework = null;
84
+
85
+ // Node.js / JavaScript
86
+ if (fs.existsSync(path.join(projectRoot, 'package.json'))) {
87
+ language = 'javascript';
88
+ stack.push('node');
89
+
90
+ try {
91
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
92
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
93
+
94
+ if (allDeps.next) { framework = 'nextjs'; stack.push('next'); }
95
+ else if (allDeps.react) { framework = 'react'; stack.push('react'); }
96
+ else if (allDeps.vue) { framework = 'vue'; stack.push('vue'); }
97
+ else if (allDeps.angular || allDeps['@angular/core']) { framework = 'angular'; stack.push('angular'); }
98
+ else if (allDeps.express || allDeps.fastify || allDeps.koa) { framework = 'node-api'; stack.push('node-api'); }
99
+
100
+ if (allDeps.typescript) { language = 'typescript'; stack.push('typescript'); }
101
+ } catch { /* ignore parse errors */ }
102
+ }
103
+
104
+ // Python
105
+ if (fs.existsSync(path.join(projectRoot, 'pyproject.toml')) ||
106
+ fs.existsSync(path.join(projectRoot, 'requirements.txt')) ||
107
+ fs.existsSync(path.join(projectRoot, 'setup.py'))) {
108
+ language = 'python';
109
+ stack.push('python');
110
+
111
+ if (fs.existsSync(path.join(projectRoot, 'manage.py'))) { framework = 'django'; stack.push('django'); }
112
+ }
113
+
114
+ // Go
115
+ if (fs.existsSync(path.join(projectRoot, 'go.mod'))) {
116
+ language = 'go';
117
+ stack.push('go');
118
+ }
119
+
120
+ // Rust
121
+ if (fs.existsSync(path.join(projectRoot, 'Cargo.toml'))) {
122
+ language = 'rust';
123
+ stack.push('rust');
124
+ }
125
+
126
+ // Flutter / Dart
127
+ if (fs.existsSync(path.join(projectRoot, 'pubspec.yaml'))) {
128
+ language = 'dart';
129
+ framework = 'flutter';
130
+ stack.push('dart', 'flutter');
131
+ }
132
+
133
+ // Java
134
+ if (fs.existsSync(path.join(projectRoot, 'pom.xml')) ||
135
+ fs.existsSync(path.join(projectRoot, 'build.gradle'))) {
136
+ language = 'java';
137
+ stack.push('java');
138
+ }
139
+
140
+ return { language, framework, stack };
141
+ }
142
+
143
+ /**
144
+ * Infer intent from request text using keyword matching.
145
+ *
146
+ * @param {string} requestText
147
+ * @returns {string} One of: bugfix, refactor, docs, spike, feature
148
+ */
149
+ export function inferIntent(requestText) {
150
+ if (!requestText) return 'feature';
151
+ const lower = requestText.toLowerCase();
152
+
153
+ const patterns = [
154
+ { keywords: ['fix', 'bug', 'broken', 'crash', 'error', 'issue', 'wrong'], intent: 'bugfix' },
155
+ { keywords: ['refactor', 'clean', 'restructure', 'reorganize', 'rename', 'simplify'], intent: 'refactor' },
156
+ { keywords: ['doc', 'document', 'readme', 'guide', 'explain'], intent: 'docs' },
157
+ { keywords: ['research', 'spike', 'explore', 'investigate', 'prototype'], intent: 'spike' },
158
+ ];
159
+
160
+ for (const { keywords, intent } of patterns) {
161
+ if (keywords.some((kw) => lower.includes(kw))) return intent;
162
+ }
163
+
164
+ return 'feature';
165
+ }
166
+
167
+ /**
168
+ * Parse inline depth modifiers from request text.
169
+ *
170
+ * @param {string} requestText
171
+ * @returns {{ depth: string, cleanedText: string }}
172
+ */
173
+ export function parseDepthModifier(requestText) {
174
+ if (!requestText) return { depth: 'standard', cleanedText: '' };
175
+
176
+ const match = requestText.match(/^\s*(quick|deep)\s+/i);
177
+ if (match) {
178
+ return {
179
+ depth: match[1].toLowerCase(),
180
+ cleanedText: requestText.slice(match[0].length),
181
+ };
182
+ }
183
+
184
+ return { depth: 'standard', cleanedText: requestText };
185
+ }
186
+
187
+ /**
188
+ * Run zero-config auto-initialization.
189
+ * Creates .wazir directories, detects host, scans project, writes config.
190
+ * No interactive prompts — everything is inferred.
191
+ *
192
+ * @param {string} projectRoot
193
+ * @param {object} [opts]
194
+ * @param {object} [opts.context] - Runtime context (availableTools, etc.)
195
+ * @param {boolean} [opts.force] - Force reinitialize even if config exists
196
+ * @returns {{ config: object, host: object, stack: object, filesCreated: string[] }}
197
+ */
198
+ export function autoInit(projectRoot, opts = {}) {
199
+ const wazirDir = path.join(projectRoot, '.wazir');
200
+ const configPath = path.join(wazirDir, 'state', 'config.json');
201
+
202
+ // If already initialized and not forced, return existing config
203
+ if (fs.existsSync(configPath) && !opts.force) {
204
+ const existing = JSON.parse(fs.readFileSync(configPath, 'utf8'));
205
+ return {
206
+ config: existing,
207
+ host: detectHost(),
208
+ stack: detectProjectStack(projectRoot),
209
+ filesCreated: [],
210
+ alreadyInitialized: true,
211
+ };
212
+ }
213
+
214
+ // Create directories
215
+ for (const dir of ['input', 'state', 'runs']) {
216
+ fs.mkdirSync(path.join(wazirDir, dir), { recursive: true });
217
+ }
218
+
219
+ const host = detectHost();
220
+ const stack = detectProjectStack(projectRoot);
221
+
222
+ // Detect context-mode MCP
223
+ const contextMode = { enabled: false, has_execute_file: false };
224
+ if (opts.context?.availableTools) {
225
+ const prefix = 'mcp__plugin_context-mode_context-mode__';
226
+ const hasExecute = opts.context.availableTools.includes(`${prefix}execute`);
227
+ const hasFetchAndIndex = opts.context.availableTools.includes(`${prefix}fetch_and_index`);
228
+ const hasSearch = opts.context.availableTools.includes(`${prefix}search`);
229
+ const hasExecuteFile = opts.context.availableTools.includes(`${prefix}execute_file`);
230
+ if (hasExecute && hasFetchAndIndex && hasSearch) {
231
+ contextMode.enabled = true;
232
+ contextMode.has_execute_file = hasExecuteFile;
233
+ }
234
+ } else {
235
+ const pluginDir = path.join(os.homedir(), '.claude', 'plugins', 'cache', 'context-mode');
236
+ if (fs.existsSync(pluginDir)) {
237
+ contextMode.enabled = true;
238
+ contextMode.has_execute_file = true;
239
+ }
240
+ }
241
+
242
+ // Sensible defaults — no questions
243
+ const config = {
244
+ model_mode: 'claude-only',
245
+ default_depth: 'standard',
246
+ default_intent: 'feature',
247
+ team_mode: 'sequential',
248
+ parallel_backend: 'none',
249
+ context_mode: contextMode,
250
+ detected_host: host.host,
251
+ detected_stack: stack,
252
+ auto_initialized: true,
253
+ };
254
+
255
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
256
+
257
+ const filesCreated = ['.wazir/input/', '.wazir/state/', '.wazir/runs/', '.wazir/state/config.json'];
258
+
259
+ return { config, host, stack, filesCreated, alreadyInitialized: false };
260
+ }
@@ -0,0 +1,161 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { autoInit, detectHost, detectProjectStack } from './auto-detect.js';
5
+
6
+ /**
7
+ * wazir init [--auto|--interactive|--force]
8
+ *
9
+ * Default: --auto (zero-config, no prompts, infer everything)
10
+ * --interactive: legacy mode with @inquirer/prompts (may fail in non-TTY)
11
+ * --force: reinitialize even if already initialized
12
+ */
13
+ export async function runInitCommand(parsed, context = {}) {
14
+ const cwd = context.cwd ?? process.cwd();
15
+ const wazirDir = path.join(cwd, '.wazir');
16
+ const configPath = path.join(wazirDir, 'state', 'config.json');
17
+ const isForce = parsed.args.includes('--force');
18
+ const isInteractive = parsed.args.includes('--interactive');
19
+
20
+ // Already initialized check
21
+ if (fs.existsSync(configPath) && !isForce) {
22
+ return {
23
+ exitCode: 1,
24
+ stderr: 'Pipeline already initialized. Use --force to reinitialize.\n',
25
+ };
26
+ }
27
+
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
34
+ try {
35
+ const result = autoInit(cwd, { context, force: isForce });
36
+
37
+ if (result.alreadyInitialized && !isForce) {
38
+ return {
39
+ exitCode: 0,
40
+ stdout: `Already initialized. Host: ${result.host.host}, Stack: ${result.stack.language}\n`,
41
+ };
42
+ }
43
+
44
+ // Auto-export for detected host
45
+ let exportNote = '';
46
+ try {
47
+ const { buildHostExports } = await import('../export/compiler.js');
48
+ buildHostExports(cwd);
49
+ exportNote = ` Exports: generated for ${result.host.host}\n`;
50
+ } catch {
51
+ exportNote = ' Exports: skipped (run `wazir export build` manually)\n';
52
+ }
53
+
54
+ const lines = [
55
+ '',
56
+ 'Wazir initialized (zero-config).',
57
+ '',
58
+ ` Host: ${result.host.host} (${result.host.confidence} confidence)`,
59
+ ` Stack: ${result.stack.language}${result.stack.framework ? ` / ${result.stack.framework}` : ''}`,
60
+ ` Mode: ${result.config.model_mode}`,
61
+ ` Depth: ${result.config.default_depth}`,
62
+ exportNote,
63
+ 'Files created:',
64
+ ...result.filesCreated.map((f) => ` - ${f}`),
65
+ '',
66
+ 'Next: /wazir <what you want to build>',
67
+ '',
68
+ 'Power users: `wazir init --interactive` for manual config.',
69
+ 'Override: `wazir config set model_mode multi-tool`',
70
+ '',
71
+ ];
72
+
73
+ return { exitCode: 0, stdout: lines.join('\n') };
74
+ } catch (error) {
75
+ return { exitCode: 1, stderr: `Auto-init failed: ${error.message}\n` };
76
+ }
77
+ }
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,46 @@
1
+ import { readdirSync, existsSync } from 'node:fs';
2
+ import { join, resolve, basename } from 'node:path';
3
+
4
+ /**
5
+ * Scan input directories for briefing materials.
6
+ * Globs input/*.md and .wazir/input/*.md (flat, not recursive).
7
+ * Excludes README.md.
8
+ *
9
+ * @param {string} projectRoot - Absolute path to project root
10
+ * @returns {Array<{path: string, auto: boolean}>} Found files with auto flag
11
+ */
12
+ export function scanInputDirectories(projectRoot) {
13
+ const root = resolve(projectRoot);
14
+ const dirs = [
15
+ join(root, 'input'),
16
+ join(root, '.wazir', 'input'),
17
+ ];
18
+
19
+ const results = [];
20
+
21
+ for (const dir of dirs) {
22
+ if (!existsSync(dir)) continue;
23
+
24
+ let entries;
25
+ try {
26
+ entries = readdirSync(dir, { withFileTypes: true });
27
+ } catch {
28
+ continue;
29
+ }
30
+
31
+ for (const entry of entries) {
32
+ if (!entry.isFile()) continue;
33
+ if (!entry.name.endsWith('.md')) continue;
34
+ if (basename(entry.name).toLowerCase() === 'readme.md') continue;
35
+
36
+ results.push({ path: join(dir, entry.name), auto: false });
37
+ }
38
+ }
39
+
40
+ // Single file auto-use
41
+ if (results.length === 1) {
42
+ results[0].auto = true;
43
+ }
44
+
45
+ return results;
46
+ }