@wazir-dev/cli 1.1.0 → 1.3.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 +74 -10
- package/README.md +15 -15
- 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/roles-and-workflows.md +2 -0
- package/docs/concepts/why-wazir.md +59 -0
- package/docs/decisions/2026-03-19-deferred-items.md +564 -0
- package/docs/decisions/2026-03-19-enhancement-decisions.md +300 -0
- package/docs/readmes/INDEX.md +21 -5
- package/docs/readmes/features/expertise/README.md +2 -2
- package/docs/readmes/features/exports/README.md +2 -2
- package/docs/readmes/features/hooks/pre-compact-summary.md +1 -1
- package/docs/readmes/features/schemas/README.md +3 -0
- package/docs/readmes/features/skills/README.md +17 -0
- package/docs/readmes/features/skills/clarifier.md +5 -0
- package/docs/readmes/features/skills/claude-cli.md +5 -0
- package/docs/readmes/features/skills/codex-cli.md +5 -0
- package/docs/readmes/features/skills/dispatching-parallel-agents.md +5 -0
- package/docs/readmes/features/skills/executing-plans.md +5 -0
- package/docs/readmes/features/skills/executor.md +5 -0
- package/docs/readmes/features/skills/finishing-a-development-branch.md +5 -0
- package/docs/readmes/features/skills/gemini-cli.md +5 -0
- package/docs/readmes/features/skills/humanize.md +5 -0
- package/docs/readmes/features/skills/init-pipeline.md +5 -0
- package/docs/readmes/features/skills/receiving-code-review.md +5 -0
- package/docs/readmes/features/skills/requesting-code-review.md +5 -0
- package/docs/readmes/features/skills/reviewer.md +5 -0
- package/docs/readmes/features/skills/subagent-driven-development.md +5 -0
- package/docs/readmes/features/skills/using-git-worktrees.md +5 -0
- package/docs/readmes/features/skills/wazir.md +5 -0
- package/docs/readmes/features/skills/writing-skills.md +5 -0
- package/docs/readmes/features/workflows/prepare-next.md +1 -1
- package/docs/reference/configuration-reference.md +47 -6
- package/docs/reference/hooks.md +1 -0
- package/docs/reference/launch-checklist.md +4 -4
- package/docs/reference/review-loop-pattern.md +119 -9
- package/docs/reference/roles-reference.md +1 -0
- package/docs/reference/skill-tiers.md +147 -0
- package/docs/reference/tooling-cli.md +3 -1
- package/docs/truth-claims.yaml +12 -0
- package/expertise/antipatterns/process/ai-coding-antipatterns.md +214 -1
- 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 +9 -0
- package/exports/hosts/claude/CLAUDE.md +1 -1
- package/exports/hosts/claude/export.manifest.json +6 -4
- package/exports/hosts/claude/host-package.json +3 -1
- package/exports/hosts/codex/AGENTS.md +1 -1
- package/exports/hosts/codex/export.manifest.json +6 -4
- package/exports/hosts/codex/host-package.json +3 -1
- package/exports/hosts/cursor/.cursor/hooks.json +4 -0
- package/exports/hosts/cursor/.cursor/rules/wazir-core.mdc +1 -1
- package/exports/hosts/cursor/export.manifest.json +6 -4
- package/exports/hosts/cursor/host-package.json +3 -1
- package/exports/hosts/gemini/GEMINI.md +1 -1
- package/exports/hosts/gemini/export.manifest.json +6 -4
- package/exports/hosts/gemini/host-package.json +3 -1
- package/hooks/context-mode-router +191 -0
- package/hooks/definitions/context_mode_router.yaml +19 -0
- package/hooks/hooks.json +31 -6
- package/hooks/protected-path-write-guard +8 -0
- package/hooks/routing-matrix.json +45 -0
- package/hooks/session-start +62 -1
- package/llms-full.txt +937 -134
- package/package.json +2 -4
- package/schemas/hook.schema.json +2 -1
- package/schemas/phase-report.schema.json +89 -0
- package/schemas/usage.schema.json +25 -1
- package/schemas/wazir-manifest.schema.json +19 -0
- package/skills/brainstorming/SKILL.md +32 -157
- package/skills/clarifier/SKILL.md +289 -111
- package/skills/claude-cli/SKILL.md +320 -0
- package/skills/codex-cli/SKILL.md +260 -0
- package/skills/debugging/SKILL.md +13 -0
- package/skills/design/SKILL.md +13 -0
- package/skills/dispatching-parallel-agents/SKILL.md +13 -0
- package/skills/executing-plans/SKILL.md +13 -0
- package/skills/executor/SKILL.md +139 -19
- package/skills/finishing-a-development-branch/SKILL.md +13 -0
- package/skills/gemini-cli/SKILL.md +260 -0
- package/skills/humanize/SKILL.md +13 -0
- package/skills/init-pipeline/SKILL.md +72 -164
- package/skills/prepare-next/SKILL.md +81 -10
- package/skills/receiving-code-review/SKILL.md +13 -0
- package/skills/requesting-code-review/SKILL.md +13 -0
- package/skills/reviewer/SKILL.md +369 -24
- package/skills/run-audit/SKILL.md +13 -0
- package/skills/scan-project/SKILL.md +13 -0
- package/skills/self-audit/SKILL.md +217 -16
- package/skills/skill-research/SKILL.md +188 -0
- package/skills/subagent-driven-development/SKILL.md +13 -0
- package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +2 -0
- package/skills/subagent-driven-development/implementer-prompt.md +8 -0
- package/skills/subagent-driven-development/spec-reviewer-prompt.md +7 -0
- package/skills/tdd/SKILL.md +13 -0
- package/skills/using-git-worktrees/SKILL.md +13 -0
- package/skills/using-skills/SKILL.md +13 -0
- package/skills/verification/SKILL.md +54 -3
- package/skills/wazir/SKILL.md +464 -381
- package/skills/writing-plans/SKILL.md +14 -1
- package/skills/writing-skills/SKILL.md +13 -0
- package/templates/artifacts/implementation-plan.md +3 -0
- package/templates/artifacts/tasks-template.md +133 -0
- package/templates/examples/phase-report.example.json +48 -0
- package/tooling/src/adapters/composition-engine.js +256 -0
- package/tooling/src/adapters/model-router.js +84 -0
- package/tooling/src/capture/command.js +41 -2
- package/tooling/src/capture/run-config.js +3 -1
- package/tooling/src/capture/store.js +56 -0
- package/tooling/src/capture/usage.js +106 -0
- package/tooling/src/capture/user-input.js +66 -0
- package/tooling/src/checks/ac-matrix.js +256 -0
- package/tooling/src/checks/command-registry.js +12 -0
- package/tooling/src/checks/docs-truth.js +1 -1
- package/tooling/src/checks/security-sensitivity.js +69 -0
- package/tooling/src/checks/skills.js +111 -0
- package/tooling/src/cli.js +31 -20
- package/tooling/src/commands/stats.js +161 -0
- package/tooling/src/commands/validate.js +5 -1
- package/tooling/src/export/compiler.js +33 -37
- package/tooling/src/gating/agent.js +145 -0
- package/tooling/src/guards/phase-prerequisite-guard.js +185 -0
- package/tooling/src/hooks/routing-logic.js +69 -0
- package/tooling/src/init/auto-detect.js +258 -0
- package/tooling/src/init/command.js +38 -170
- package/tooling/src/input/scanner.js +46 -0
- package/tooling/src/reports/command.js +103 -0
- package/tooling/src/reports/phase-report.js +323 -0
- package/tooling/src/state/command.js +160 -0
- package/tooling/src/state/db.js +287 -0
- package/tooling/src/status/command.js +58 -1
- package/tooling/src/verify/proof-collector.js +299 -0
- package/wazir.manifest.yaml +26 -14
- package/workflows/plan-review.md +3 -1
- package/workflows/verify.md +30 -1
|
@@ -0,0 +1,185 @@
|
|
|
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
|
+
/**
|
|
8
|
+
* Validates that every enabled workflow has a phase_exit event
|
|
9
|
+
* in the run's events.ndjson before the run can be marked complete.
|
|
10
|
+
*
|
|
11
|
+
* If a run-config with workflow_policy exists, only workflows with
|
|
12
|
+
* enabled: true are checked. Otherwise falls back to the manifest list.
|
|
13
|
+
*/
|
|
14
|
+
export function validateRunCompletion(runDir, manifestPath) {
|
|
15
|
+
const manifest = readYamlFile(manifestPath);
|
|
16
|
+
const declaredWorkflows = manifest.workflows ?? [];
|
|
17
|
+
|
|
18
|
+
if (declaredWorkflows.length === 0) {
|
|
19
|
+
return { complete: true, missing: [] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Filter to enabled workflows if run-config exists
|
|
23
|
+
const runConfigPath = path.join(runDir, 'run-config.yaml');
|
|
24
|
+
let enabledWorkflows = declaredWorkflows;
|
|
25
|
+
if (fs.existsSync(runConfigPath)) {
|
|
26
|
+
try {
|
|
27
|
+
const runConfig = readYamlFile(runConfigPath);
|
|
28
|
+
const policy = runConfig.workflow_policy;
|
|
29
|
+
if (policy && typeof policy === 'object') {
|
|
30
|
+
enabledWorkflows = declaredWorkflows.filter(w => {
|
|
31
|
+
const wPolicy = policy[w] ?? policy[w.replace(/_/g, '-')];
|
|
32
|
+
// If no policy entry, assume enabled; if entry exists, check enabled field
|
|
33
|
+
return wPolicy ? (wPolicy.enabled !== false) : true;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// If run-config can't be read, fall back to full manifest list
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const eventsPath = path.join(runDir, 'events.ndjson');
|
|
42
|
+
const completedWorkflows = new Set();
|
|
43
|
+
|
|
44
|
+
if (fs.existsSync(eventsPath)) {
|
|
45
|
+
const content = fs.readFileSync(eventsPath, 'utf8');
|
|
46
|
+
for (const line of content.split('\n')) {
|
|
47
|
+
const trimmed = line.trim();
|
|
48
|
+
if (!trimmed) continue;
|
|
49
|
+
try {
|
|
50
|
+
const event = JSON.parse(trimmed);
|
|
51
|
+
if (event.event === 'phase_exit' && event.status === 'completed' && event.phase) {
|
|
52
|
+
completedWorkflows.add(event.phase);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Skip malformed lines
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const missing = enabledWorkflows.filter(w => !completedWorkflows.has(w));
|
|
61
|
+
|
|
62
|
+
return { complete: missing.length === 0, missing };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function evaluateScopeCoverageGuard(payload) {
|
|
66
|
+
const { input_item_count: inputCount, plan_task_count: planCount, user_approved_reduction: userApproved } = payload;
|
|
67
|
+
|
|
68
|
+
const safeInputCount = inputCount ?? 0;
|
|
69
|
+
const safePlanCount = planCount ?? 0;
|
|
70
|
+
|
|
71
|
+
if (safeInputCount === 0) {
|
|
72
|
+
return {
|
|
73
|
+
allowed: true,
|
|
74
|
+
reason: 'No input items to check against.',
|
|
75
|
+
input_count: safeInputCount,
|
|
76
|
+
plan_count: safePlanCount,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (safePlanCount >= safeInputCount) {
|
|
81
|
+
return {
|
|
82
|
+
allowed: true,
|
|
83
|
+
reason: `Plan covers all input items (${safePlanCount} tasks >= ${safeInputCount} items).`,
|
|
84
|
+
input_count: safeInputCount,
|
|
85
|
+
plan_count: safePlanCount,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (userApproved === true) {
|
|
90
|
+
return {
|
|
91
|
+
allowed: true,
|
|
92
|
+
reason: `User explicitly approved scope reduction (${safePlanCount} tasks < ${safeInputCount} items).`,
|
|
93
|
+
input_count: safeInputCount,
|
|
94
|
+
plan_count: safePlanCount,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
allowed: false,
|
|
100
|
+
reason: `Scope reduction detected: plan has ${safePlanCount} tasks but input has ${safeInputCount} items. User approval required.`,
|
|
101
|
+
input_count: safeInputCount,
|
|
102
|
+
plan_count: safePlanCount,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function evaluatePhasePrerequisiteGuard(payload) {
|
|
107
|
+
const { run_id: runId, phase, state_root: stateRoot, project_root: projectRoot } = payload;
|
|
108
|
+
|
|
109
|
+
if (!runId) {
|
|
110
|
+
throw new Error('run_id is required');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!phase) {
|
|
114
|
+
throw new Error('phase is required');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!stateRoot) {
|
|
118
|
+
throw new Error('state_root is required');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!projectRoot) {
|
|
122
|
+
throw new Error('project_root is required');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const runPaths = getRunPaths(stateRoot, runId);
|
|
126
|
+
|
|
127
|
+
if (!fs.existsSync(runPaths.statusPath)) {
|
|
128
|
+
throw new Error(`status.json not found for run ${runId}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const manifestPath = path.join(projectRoot, 'wazir.manifest.yaml');
|
|
132
|
+
const manifest = readYamlFile(manifestPath);
|
|
133
|
+
const prerequisites = manifest.phase_prerequisites?.[phase];
|
|
134
|
+
|
|
135
|
+
if (!prerequisites || (Object.keys(prerequisites).length === 0)) {
|
|
136
|
+
return {
|
|
137
|
+
allowed: true,
|
|
138
|
+
reason: `No prerequisites defined for phase ${phase}.`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const requiredArtifacts = prerequisites.required_artifacts ?? [];
|
|
143
|
+
const requiredPhaseExits = prerequisites.required_phase_exits ?? [];
|
|
144
|
+
|
|
145
|
+
const missingArtifacts = [];
|
|
146
|
+
for (const artifact of requiredArtifacts) {
|
|
147
|
+
const artifactPath = path.join(runPaths.runRoot, artifact);
|
|
148
|
+
if (!fs.existsSync(artifactPath)) {
|
|
149
|
+
missingArtifacts.push(artifact);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const completedPhases = readPhaseExitEvents(runPaths);
|
|
154
|
+
const missingPhaseExits = [];
|
|
155
|
+
for (const requiredPhase of requiredPhaseExits) {
|
|
156
|
+
if (!completedPhases.includes(requiredPhase)) {
|
|
157
|
+
missingPhaseExits.push(requiredPhase);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// OR-logic for resumed runs: if all artifacts exist, pass even without phase_exit events.
|
|
162
|
+
// Artifacts are the hard evidence; phase_exits are supplementary.
|
|
163
|
+
// But if artifacts are missing, phase_exits alone are not sufficient.
|
|
164
|
+
if (missingArtifacts.length === 0) {
|
|
165
|
+
return {
|
|
166
|
+
allowed: true,
|
|
167
|
+
reason: `All prerequisite artifacts present for phase ${phase}.`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const reasons = [];
|
|
172
|
+
if (missingArtifacts.length > 0) {
|
|
173
|
+
reasons.push(`Missing artifacts: ${missingArtifacts.join(', ')}`);
|
|
174
|
+
}
|
|
175
|
+
if (missingPhaseExits.length > 0) {
|
|
176
|
+
reasons.push(`Missing phase exits: ${missingPhaseExits.join(', ')}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
allowed: false,
|
|
181
|
+
reason: reasons.join('. '),
|
|
182
|
+
missing_artifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
|
|
183
|
+
missing_phase_exits: missingPhaseExits.length > 0 ? missingPhaseExits : undefined,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -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,258 @@
|
|
|
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
|
+
context_mode: contextMode,
|
|
248
|
+
detected_host: host.host,
|
|
249
|
+
detected_stack: stack,
|
|
250
|
+
auto_initialized: true,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
254
|
+
|
|
255
|
+
const filesCreated = ['.wazir/input/', '.wazir/state/', '.wazir/runs/', '.wazir/state/config.json'];
|
|
256
|
+
|
|
257
|
+
return { config, host, stack, filesCreated, alreadyInitialized: false };
|
|
258
|
+
}
|