@wazir-dev/cli 1.2.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +39 -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/expertise/antipatterns/process/ai-coding-antipatterns.md +117 -0
  17. package/exports/hosts/claude/.claude/commands/plan-review.md +3 -1
  18. package/exports/hosts/claude/.claude/commands/verify.md +30 -1
  19. package/exports/hosts/claude/export.manifest.json +2 -2
  20. package/exports/hosts/codex/export.manifest.json +2 -2
  21. package/exports/hosts/cursor/export.manifest.json +2 -2
  22. package/exports/hosts/gemini/export.manifest.json +2 -2
  23. package/llms-full.txt +48 -18
  24. package/package.json +2 -3
  25. package/schemas/phase-report.schema.json +9 -0
  26. package/skills/brainstorming/SKILL.md +14 -2
  27. package/skills/clarifier/SKILL.md +189 -35
  28. package/skills/executor/SKILL.md +67 -0
  29. package/skills/init-pipeline/SKILL.md +0 -1
  30. package/skills/reviewer/SKILL.md +86 -13
  31. package/skills/self-audit/SKILL.md +20 -0
  32. package/skills/skill-research/SKILL.md +188 -0
  33. package/skills/verification/SKILL.md +41 -3
  34. package/skills/wazir/SKILL.md +304 -38
  35. package/tooling/src/capture/command.js +17 -1
  36. package/tooling/src/capture/store.js +32 -0
  37. package/tooling/src/capture/user-input.js +66 -0
  38. package/tooling/src/checks/security-sensitivity.js +69 -0
  39. package/tooling/src/cli.js +28 -26
  40. package/tooling/src/guards/phase-prerequisite-guard.js +58 -0
  41. package/tooling/src/init/auto-detect.js +0 -2
  42. package/tooling/src/init/command.js +3 -95
  43. package/tooling/src/status/command.js +6 -1
  44. package/tooling/src/verify/proof-collector.js +299 -0
  45. package/workflows/plan-review.md +3 -1
  46. package/workflows/verify.md +30 -1
@@ -0,0 +1,69 @@
1
+ const SECURITY_PATTERNS = [
2
+ 'auth', 'password', 'passwd', 'token', 'query', 'sql',
3
+ 'fetch', 'upload', 'secret', 'env', 'api[._-]?key',
4
+ 'session', 'cookie', 'cors', 'csrf', 'jwt', 'oauth',
5
+ 'encrypt', 'decrypt', 'hash', 'salt', 'credential',
6
+ 'private[._-]?key', 'access[._-]?token', 'refresh[._-]?token',
7
+ ];
8
+
9
+ const PATTERN_REGEX = new RegExp(
10
+ `\\b(${SECURITY_PATTERNS.join('|')})\\b`,
11
+ 'gi'
12
+ );
13
+
14
+ /**
15
+ * Scan diff text for security-sensitive patterns.
16
+ * Returns which patterns were found and in which files.
17
+ */
18
+ export function detectSecurityPatterns(diffText) {
19
+ if (!diffText || typeof diffText !== 'string') {
20
+ return { triggered: false, patterns: [], files: [] };
21
+ }
22
+
23
+ const matchedPatterns = new Set();
24
+ const matchedFiles = new Set();
25
+ let currentFile = null;
26
+
27
+ for (const line of diffText.split('\n')) {
28
+ // Track current file from diff headers
29
+ const fileMatch = line.match(/^(?:diff --git a\/\S+ b\/|[+]{3} b\/)(.+)/);
30
+ if (fileMatch) {
31
+ currentFile = fileMatch[1];
32
+ continue;
33
+ }
34
+
35
+ // Only scan added/modified lines (starting with +, not +++)
36
+ if (!line.startsWith('+') || line.startsWith('+++')) continue;
37
+
38
+ const lineMatches = line.match(PATTERN_REGEX);
39
+ if (lineMatches) {
40
+ for (const m of lineMatches) {
41
+ matchedPatterns.add(m.toLowerCase());
42
+ }
43
+ if (currentFile) {
44
+ matchedFiles.add(currentFile);
45
+ }
46
+ }
47
+ }
48
+
49
+ const patterns = [...matchedPatterns].sort();
50
+ const files = [...matchedFiles].sort();
51
+
52
+ return {
53
+ triggered: patterns.length > 0,
54
+ patterns,
55
+ files,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Security review dimensions to add when patterns are detected.
61
+ */
62
+ export const SECURITY_REVIEW_DIMENSIONS = [
63
+ 'Injection (SQL, command, template, header)',
64
+ 'Authentication bypass',
65
+ 'Data exposure (PII, secrets, tokens in logs/responses)',
66
+ 'CSRF / SSRF',
67
+ 'XSS (stored, reflected, DOM)',
68
+ 'Secrets leakage (hardcoded keys, env vars in client code)',
69
+ ];
@@ -1,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Suppress Node.js ExperimentalWarning for built-in SQLite (node:sqlite).
4
+ // Must run before any module that transitively imports node:sqlite loads,
5
+ // so command handlers are lazy-imported below instead of using static imports.
6
+ const _originalEmit = process.emit;
7
+ process.emit = function (event, ...args) {
8
+ if (event === 'warning' && args[0]?.name === 'ExperimentalWarning' &&
9
+ args[0]?.message?.includes('SQLite')) {
10
+ return false;
11
+ }
12
+ return _originalEmit.apply(this, [event, ...args]);
13
+ };
14
+
3
15
  import fs from 'node:fs';
4
16
  import { fileURLToPath } from 'node:url';
5
17
 
6
- import { runCaptureCommand } from './capture/command.js';
7
- import { runValidateCommand } from './commands/validate.js';
8
- import { runDoctorCommand } from './doctor/command.js';
9
- import { runExportCommand as runGeneratedExportCommand } from './export/command.js';
10
- import { runIndexCommand } from './index/command.js';
11
- import { runInitCommand } from './init/command.js';
12
- import { runRecallCommand } from './recall/command.js';
13
- import { runReportCommand } from './reports/command.js';
14
- import { runStateCommand } from './state/command.js';
15
- import { runStatsCommand } from './commands/stats.js';
16
- import { runStatusCommand } from './status/command.js';
17
-
18
18
  const COMMAND_FAMILIES = [
19
19
  'export',
20
20
  'validate',
@@ -29,18 +29,19 @@ const COMMAND_FAMILIES = [
29
29
  'capture'
30
30
  ];
31
31
 
32
- const COMMAND_HANDLERS = {
33
- export: runGeneratedExportCommand,
34
- validate: runValidateCommand,
35
- doctor: runDoctorCommand,
36
- index: runIndexCommand,
37
- init: runInitCommand,
38
- recall: runRecallCommand,
39
- report: runReportCommand,
40
- state: runStateCommand,
41
- status: runStatusCommand,
42
- stats: runStatsCommand,
43
- capture: runCaptureCommand,
32
+ // Lazy-load command handlers so the warning filter is active before node:sqlite loads
33
+ const COMMAND_LOADERS = {
34
+ export: () => import('./export/command.js').then(m => m.runExportCommand),
35
+ validate: () => import('./commands/validate.js').then(m => m.runValidateCommand),
36
+ doctor: () => import('./doctor/command.js').then(m => m.runDoctorCommand),
37
+ index: () => import('./index/command.js').then(m => m.runIndexCommand),
38
+ init: () => import('./init/command.js').then(m => m.runInitCommand),
39
+ recall: () => import('./recall/command.js').then(m => m.runRecallCommand),
40
+ report: () => import('./reports/command.js').then(m => m.runReportCommand),
41
+ state: () => import('./state/command.js').then(m => m.runStateCommand),
42
+ status: () => import('./status/command.js').then(m => m.runStatusCommand),
43
+ stats: () => import('./commands/stats.js').then(m => m.runStatsCommand),
44
+ capture: () => import('./capture/command.js').then(m => m.runCaptureCommand),
44
45
  };
45
46
 
46
47
  export function parseArgs(argv) {
@@ -88,9 +89,9 @@ export async function main(argv = process.argv.slice(2)) {
88
89
  return 1;
89
90
  }
90
91
 
91
- const handler = COMMAND_HANDLERS[parsed.command];
92
+ const loader = COMMAND_LOADERS[parsed.command];
92
93
 
93
- if (!handler) {
94
+ if (!loader) {
94
95
  console.error(`wazir ${parsed.command} is not implemented yet`);
95
96
  return 2;
96
97
  }
@@ -98,6 +99,7 @@ export async function main(argv = process.argv.slice(2)) {
98
99
  let result;
99
100
 
100
101
  try {
102
+ const handler = await loader();
101
103
  result = await handler(parsed);
102
104
  } catch (error) {
103
105
  console.error(error.message);
@@ -4,6 +4,64 @@ import path from 'node:path';
4
4
  import { readYamlFile } from '../loaders.js';
5
5
  import { getRunPaths, readPhaseExitEvents } from '../capture/store.js';
6
6
 
7
+ /**
8
+ * Validates that every enabled workflow has a phase_exit event
9
+ * in the run's events.ndjson before the run can be marked complete.
10
+ *
11
+ * If a run-config with workflow_policy exists, only workflows with
12
+ * enabled: true are checked. Otherwise falls back to the manifest list.
13
+ */
14
+ export function validateRunCompletion(runDir, manifestPath) {
15
+ const manifest = readYamlFile(manifestPath);
16
+ const declaredWorkflows = manifest.workflows ?? [];
17
+
18
+ if (declaredWorkflows.length === 0) {
19
+ return { complete: true, missing: [] };
20
+ }
21
+
22
+ // Filter to enabled workflows if run-config exists
23
+ const runConfigPath = path.join(runDir, 'run-config.yaml');
24
+ let enabledWorkflows = declaredWorkflows;
25
+ if (fs.existsSync(runConfigPath)) {
26
+ try {
27
+ const runConfig = readYamlFile(runConfigPath);
28
+ const policy = runConfig.workflow_policy;
29
+ if (policy && typeof policy === 'object') {
30
+ enabledWorkflows = declaredWorkflows.filter(w => {
31
+ const wPolicy = policy[w] ?? policy[w.replace(/_/g, '-')];
32
+ // If no policy entry, assume enabled; if entry exists, check enabled field
33
+ return wPolicy ? (wPolicy.enabled !== false) : true;
34
+ });
35
+ }
36
+ } catch {
37
+ // If run-config can't be read, fall back to full manifest list
38
+ }
39
+ }
40
+
41
+ const eventsPath = path.join(runDir, 'events.ndjson');
42
+ const completedWorkflows = new Set();
43
+
44
+ if (fs.existsSync(eventsPath)) {
45
+ const content = fs.readFileSync(eventsPath, 'utf8');
46
+ for (const line of content.split('\n')) {
47
+ const trimmed = line.trim();
48
+ if (!trimmed) continue;
49
+ try {
50
+ const event = JSON.parse(trimmed);
51
+ if (event.event === 'phase_exit' && event.status === 'completed' && event.phase) {
52
+ completedWorkflows.add(event.phase);
53
+ }
54
+ } catch {
55
+ // Skip malformed lines
56
+ }
57
+ }
58
+ }
59
+
60
+ const missing = enabledWorkflows.filter(w => !completedWorkflows.has(w));
61
+
62
+ return { complete: missing.length === 0, missing };
63
+ }
64
+
7
65
  export function evaluateScopeCoverageGuard(payload) {
8
66
  const { input_item_count: inputCount, plan_task_count: planCount, user_approved_reduction: userApproved } = payload;
9
67
 
@@ -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
- }
@@ -54,7 +54,12 @@ function success(payload, options = {}) {
54
54
  };
55
55
  }
56
56
 
57
- let output = `${payload.run_id} ${payload.phase} ${payload.status}\n`;
57
+ const parentPhase = payload.parent_phase ?? payload.phase;
58
+ const workflow = payload.workflow;
59
+ const phaseLabel = workflow
60
+ ? `Phase: ${parentPhase} > Workflow: ${workflow}`
61
+ : `Phase: ${parentPhase}`;
62
+ let output = `${payload.run_id} ${phaseLabel} ${payload.status}\n`;
58
63
 
59
64
  if (payload.savings_summary) {
60
65
  output += `${payload.savings_summary}\n`;
@@ -0,0 +1,299 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+
5
+ const WEB_FRAMEWORKS = ['next', 'vite', 'react-scripts', '@angular/cli', 'nuxt', 'astro', 'gatsby'];
6
+ const API_FRAMEWORKS = ['express', 'fastify', 'hono', 'koa', '@nestjs/core', '@hapi/hapi'];
7
+
8
+ /**
9
+ * Detect whether a project produces runnable output and what type.
10
+ *
11
+ * @param {string} projectRoot
12
+ * @returns {'web' | 'api' | 'cli' | 'library'}
13
+ */
14
+ export function detectRunnableType(projectRoot) {
15
+ const pkgPath = path.join(projectRoot, 'package.json');
16
+ if (!fs.existsSync(pkgPath)) return 'library';
17
+
18
+ let pkg;
19
+ try {
20
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
21
+ } catch {
22
+ return 'library';
23
+ }
24
+
25
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
26
+
27
+ if (WEB_FRAMEWORKS.some((fw) => fw in allDeps)) return 'web';
28
+ if (API_FRAMEWORKS.some((fw) => fw in allDeps)) return 'api';
29
+ if (pkg.bin) return 'cli';
30
+
31
+ return 'library';
32
+ }
33
+
34
+ /**
35
+ * Run a command safely using execFileSync (no shell injection).
36
+ *
37
+ * @param {string} cmd - The executable
38
+ * @param {string[]} args - Arguments array
39
+ * @param {string} cwd
40
+ * @returns {{ exit_code: number, stdout: string, stderr: string }}
41
+ */
42
+ function runCommand(cmd, args, cwd) {
43
+ try {
44
+ const stdout = execFileSync(cmd, args, {
45
+ cwd,
46
+ encoding: 'utf8',
47
+ timeout: 60000,
48
+ stdio: ['pipe', 'pipe', 'pipe'],
49
+ });
50
+ return { exit_code: 0, stdout: stdout.trim(), stderr: '' };
51
+ } catch (err) {
52
+ return {
53
+ exit_code: err.status ?? 1,
54
+ stdout: (err.stdout ?? '').trim(),
55
+ stderr: (err.stderr ?? '').trim(),
56
+ };
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Summarize command output to a short string.
62
+ *
63
+ * @param {string} stdout
64
+ * @param {number} maxLen
65
+ * @returns {string}
66
+ */
67
+ function summarize(stdout, maxLen = 200) {
68
+ if (!stdout) return '';
69
+ const lines = stdout.split('\n');
70
+ if (lines.length <= 5) return stdout.slice(0, maxLen);
71
+ return [...lines.slice(0, 3), `... (${lines.length} lines total)`, ...lines.slice(-2)]
72
+ .join('\n')
73
+ .slice(0, maxLen);
74
+ }
75
+
76
+ /**
77
+ * Check if a package.json has a specific script.
78
+ *
79
+ * @param {string} projectRoot
80
+ * @param {string} scriptName
81
+ * @returns {boolean}
82
+ */
83
+ function hasScript(projectRoot, scriptName) {
84
+ try {
85
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
86
+ return !!(pkg.scripts && pkg.scripts[scriptName]);
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Check if a config file exists for a tool.
94
+ *
95
+ * @param {string} projectRoot
96
+ * @param {string[]} candidates
97
+ * @returns {boolean}
98
+ */
99
+ function hasConfigFile(projectRoot, candidates) {
100
+ return candidates.some((f) => fs.existsSync(path.join(projectRoot, f)));
101
+ }
102
+
103
+ /**
104
+ * Collect library-type proof: tests, lint, format, type-check.
105
+ *
106
+ * @param {string} projectRoot
107
+ * @returns {{ tool: string, command: string, exit_code: number, stdout_summary: string, passed: boolean }[]}
108
+ */
109
+ function collectLibraryEvidence(projectRoot) {
110
+ const evidence = [];
111
+
112
+ // npm test
113
+ if (hasScript(projectRoot, 'test')) {
114
+ const result = runCommand('npm', ['test'], projectRoot);
115
+ evidence.push({
116
+ tool: 'npm test',
117
+ command: 'npm test',
118
+ exit_code: result.exit_code,
119
+ stdout_summary: summarize(result.stdout),
120
+ passed: result.exit_code === 0,
121
+ });
122
+ }
123
+
124
+ // TypeScript type check
125
+ if (
126
+ hasConfigFile(projectRoot, ['tsconfig.json']) ||
127
+ hasScript(projectRoot, 'typecheck')
128
+ ) {
129
+ const cmd = hasScript(projectRoot, 'typecheck')
130
+ ? ['npm', ['run', 'typecheck']]
131
+ : ['npx', ['tsc', '--noEmit']];
132
+ const result = runCommand(cmd[0], cmd[1], projectRoot);
133
+ evidence.push({
134
+ tool: 'tsc',
135
+ command: cmd[0] + ' ' + cmd[1].join(' '),
136
+ exit_code: result.exit_code,
137
+ stdout_summary: summarize(result.exit_code === 0 ? 'No type errors' : result.stdout || result.stderr),
138
+ passed: result.exit_code === 0,
139
+ });
140
+ }
141
+
142
+ // ESLint
143
+ if (
144
+ hasConfigFile(projectRoot, ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml', 'eslint.config.js', 'eslint.config.mjs']) ||
145
+ hasScript(projectRoot, 'lint')
146
+ ) {
147
+ const cmd = hasScript(projectRoot, 'lint')
148
+ ? ['npm', ['run', 'lint']]
149
+ : ['npx', ['eslint', '.']];
150
+ const result = runCommand(cmd[0], cmd[1], projectRoot);
151
+ evidence.push({
152
+ tool: 'eslint',
153
+ command: cmd[0] + ' ' + cmd[1].join(' '),
154
+ exit_code: result.exit_code,
155
+ stdout_summary: summarize(result.exit_code === 0 ? 'No lint errors' : result.stdout || result.stderr),
156
+ passed: result.exit_code === 0,
157
+ });
158
+ }
159
+
160
+ // Prettier
161
+ if (
162
+ hasConfigFile(projectRoot, ['.prettierrc', '.prettierrc.js', '.prettierrc.json', '.prettierrc.yml', 'prettier.config.js', 'prettier.config.mjs']) ||
163
+ hasScript(projectRoot, 'format:check')
164
+ ) {
165
+ const cmd = hasScript(projectRoot, 'format:check')
166
+ ? ['npm', ['run', 'format:check']]
167
+ : ['npx', ['prettier', '--check', '.']];
168
+ const result = runCommand(cmd[0], cmd[1], projectRoot);
169
+ evidence.push({
170
+ tool: 'prettier',
171
+ command: cmd[0] + ' ' + cmd[1].join(' '),
172
+ exit_code: result.exit_code,
173
+ stdout_summary: summarize(result.exit_code === 0 ? 'All files formatted' : result.stdout || result.stderr),
174
+ passed: result.exit_code === 0,
175
+ });
176
+ }
177
+
178
+ return evidence;
179
+ }
180
+
181
+ /**
182
+ * Collect web-type proof: build + library checks.
183
+ *
184
+ * @param {string} projectRoot
185
+ * @returns {{ tool: string, command: string, exit_code: number, stdout_summary: string, passed: boolean }[]}
186
+ */
187
+ function collectWebEvidence(projectRoot) {
188
+ const evidence = [];
189
+
190
+ // Build
191
+ if (hasScript(projectRoot, 'build')) {
192
+ const result = runCommand('npm', ['run', 'build'], projectRoot);
193
+ evidence.push({
194
+ tool: 'build',
195
+ command: 'npm run build',
196
+ exit_code: result.exit_code,
197
+ stdout_summary: summarize(result.stdout),
198
+ passed: result.exit_code === 0,
199
+ });
200
+ }
201
+
202
+ // Also run library checks (tests, lint, etc.)
203
+ evidence.push(...collectLibraryEvidence(projectRoot));
204
+
205
+ return evidence;
206
+ }
207
+
208
+ /**
209
+ * Collect API-type proof: library checks (server start/stop is complex, defer to manual).
210
+ *
211
+ * @param {string} projectRoot
212
+ * @returns {{ tool: string, command: string, exit_code: number, stdout_summary: string, passed: boolean }[]}
213
+ */
214
+ function collectApiEvidence(projectRoot) {
215
+ return collectLibraryEvidence(projectRoot);
216
+ }
217
+
218
+ /**
219
+ * Collect CLI-type proof: --help output + library checks.
220
+ *
221
+ * @param {string} projectRoot
222
+ * @returns {{ tool: string, command: string, exit_code: number, stdout_summary: string, passed: boolean }[]}
223
+ */
224
+ function collectCliEvidence(projectRoot) {
225
+ const evidence = [];
226
+
227
+ try {
228
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
229
+ const binEntry = typeof pkg.bin === 'string' ? pkg.bin : Object.values(pkg.bin || {})[0];
230
+ if (binEntry) {
231
+ const binPath = path.join(projectRoot, binEntry);
232
+ if (fs.existsSync(binPath)) {
233
+ const result = runCommand('node', [binPath, '--help'], projectRoot);
234
+ evidence.push({
235
+ tool: 'cli --help',
236
+ command: `node ${binEntry} --help`,
237
+ exit_code: result.exit_code,
238
+ stdout_summary: summarize(result.stdout),
239
+ passed: result.exit_code === 0,
240
+ });
241
+ }
242
+ }
243
+ } catch { /* ignore */ }
244
+
245
+ evidence.push(...collectLibraryEvidence(projectRoot));
246
+
247
+ return evidence;
248
+ }
249
+
250
+ /**
251
+ * Collect proof of implementation for a task.
252
+ *
253
+ * @param {{ id: string, title: string }} taskSpec
254
+ * @param {{ projectRoot: string, runId?: string, stateRoot?: string }} runConfig
255
+ * @returns {Promise<{ task_id: string, type: string, timestamp: string, evidence: object[], status: string, all_passed: boolean }>}
256
+ */
257
+ export async function collectProof(taskSpec, runConfig) {
258
+ const { projectRoot } = runConfig;
259
+ const type = detectRunnableType(projectRoot);
260
+
261
+ let evidence;
262
+ switch (type) {
263
+ case 'web':
264
+ evidence = collectWebEvidence(projectRoot);
265
+ break;
266
+ case 'api':
267
+ evidence = collectApiEvidence(projectRoot);
268
+ break;
269
+ case 'cli':
270
+ evidence = collectCliEvidence(projectRoot);
271
+ break;
272
+ default:
273
+ evidence = collectLibraryEvidence(projectRoot);
274
+ }
275
+
276
+ const allPassed = evidence.length === 0 || evidence.every((e) => e.passed);
277
+
278
+ const result = {
279
+ task_id: taskSpec.id,
280
+ type,
281
+ timestamp: new Date().toISOString(),
282
+ evidence,
283
+ status: allPassed ? 'pass' : 'fail',
284
+ all_passed: allPassed,
285
+ };
286
+
287
+ // Save to artifacts if runId provided
288
+ if (runConfig.runId && runConfig.stateRoot) {
289
+ const artifactDir = path.join(runConfig.stateRoot, 'runs', runConfig.runId, 'artifacts');
290
+ if (fs.existsSync(artifactDir)) {
291
+ fs.writeFileSync(
292
+ path.join(artifactDir, `proof-${taskSpec.id}.json`),
293
+ JSON.stringify(result, null, 2) + '\n',
294
+ );
295
+ }
296
+ }
297
+
298
+ return result;
299
+ }
@@ -39,7 +39,9 @@ On completing this phase, run:
39
39
 
40
40
  ## Loop Structure
41
41
 
42
- Follows the review loop pattern in `docs/reference/review-loop-pattern.md` with plan dimensions. The planner role resolves findings. Pass count determined by depth. No extension.
42
+ Follows the review loop pattern in `docs/reference/review-loop-pattern.md` with 8 plan dimensions (including Input Coverage). The planner role resolves findings. Pass count determined by depth. No extension.
43
+
44
+ **Input Coverage dimension:** The reviewer reads the original input/briefing, counts distinct items, and compares against tasks in the plan. If `tasks_in_plan < items_in_input`, this is a HIGH finding listing the missing items. This prevents silent scope reduction where 21 input items become 5 tasks.
43
45
 
44
46
  ## Failure Conditions
45
47