@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.
- package/CHANGELOG.md +39 -44
- package/README.md +13 -13
- package/assets/demo.cast +47 -0
- package/assets/demo.gif +0 -0
- package/docs/anti-patterns/AP-23-skipping-enabled-workflows.md +28 -0
- package/docs/anti-patterns/AP-24-clarifier-deciding-scope.md +34 -0
- package/docs/concepts/architecture.md +1 -1
- package/docs/concepts/why-wazir.md +1 -1
- package/docs/readmes/INDEX.md +1 -1
- package/docs/readmes/features/expertise/README.md +1 -1
- package/docs/readmes/features/hooks/pre-compact-summary.md +1 -1
- package/docs/reference/hooks.md +1 -0
- package/docs/reference/launch-checklist.md +3 -3
- package/docs/reference/review-loop-pattern.md +3 -2
- package/docs/reference/skill-tiers.md +2 -2
- package/expertise/antipatterns/process/ai-coding-antipatterns.md +117 -0
- 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/export.manifest.json +2 -2
- package/exports/hosts/codex/export.manifest.json +2 -2
- package/exports/hosts/cursor/export.manifest.json +2 -2
- package/exports/hosts/gemini/export.manifest.json +2 -2
- package/llms-full.txt +48 -18
- package/package.json +2 -3
- package/schemas/phase-report.schema.json +9 -0
- package/skills/brainstorming/SKILL.md +14 -2
- package/skills/clarifier/SKILL.md +189 -35
- package/skills/executor/SKILL.md +67 -0
- package/skills/init-pipeline/SKILL.md +0 -1
- package/skills/reviewer/SKILL.md +86 -13
- package/skills/self-audit/SKILL.md +20 -0
- package/skills/skill-research/SKILL.md +188 -0
- package/skills/verification/SKILL.md +41 -3
- package/skills/wazir/SKILL.md +304 -38
- package/tooling/src/capture/command.js +17 -1
- package/tooling/src/capture/store.js +32 -0
- package/tooling/src/capture/user-input.js +66 -0
- package/tooling/src/checks/security-sensitivity.js +69 -0
- package/tooling/src/cli.js +28 -26
- package/tooling/src/guards/phase-prerequisite-guard.js +58 -0
- package/tooling/src/init/auto-detect.js +0 -2
- package/tooling/src/init/command.js +3 -95
- package/tooling/src/status/command.js +6 -1
- package/tooling/src/verify/proof-collector.js +299 -0
- package/workflows/plan-review.md +3 -1
- 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
|
+
];
|
package/tooling/src/cli.js
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
92
|
+
const loader = COMMAND_LOADERS[parsed.command];
|
|
92
93
|
|
|
93
|
-
if (!
|
|
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|--
|
|
7
|
+
* wazir init [--auto|--force]
|
|
8
8
|
*
|
|
9
9
|
* Default: --auto (zero-config, no prompts, infer everything)
|
|
10
|
-
* --interactive: legacy mode with @inquirer/prompts (may fail in non-TTY)
|
|
11
10
|
* --force: reinitialize even if already initialized
|
|
12
11
|
*/
|
|
13
12
|
export async function runInitCommand(parsed, context = {}) {
|
|
@@ -15,7 +14,6 @@ export async function runInitCommand(parsed, context = {}) {
|
|
|
15
14
|
const wazirDir = path.join(cwd, '.wazir');
|
|
16
15
|
const configPath = path.join(wazirDir, 'state', 'config.json');
|
|
17
16
|
const isForce = parsed.args.includes('--force');
|
|
18
|
-
const isInteractive = parsed.args.includes('--interactive');
|
|
19
17
|
|
|
20
18
|
// Already initialized check
|
|
21
19
|
if (fs.existsSync(configPath) && !isForce) {
|
|
@@ -25,12 +23,7 @@ export async function runInitCommand(parsed, context = {}) {
|
|
|
25
23
|
};
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
//
|
|
29
|
-
if (isInteractive) {
|
|
30
|
-
return runInteractiveInit(parsed, context);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Default: auto mode — zero-config
|
|
26
|
+
// Auto mode — zero-config
|
|
34
27
|
try {
|
|
35
28
|
const result = autoInit(cwd, { context, force: isForce });
|
|
36
29
|
|
|
@@ -65,8 +58,7 @@ export async function runInitCommand(parsed, context = {}) {
|
|
|
65
58
|
'',
|
|
66
59
|
'Next: /wazir <what you want to build>',
|
|
67
60
|
'',
|
|
68
|
-
'
|
|
69
|
-
'Override: `wazir config set model_mode multi-tool`',
|
|
61
|
+
'Override: `wazir config set model_mode multi-tool`',
|
|
70
62
|
'',
|
|
71
63
|
];
|
|
72
64
|
|
|
@@ -75,87 +67,3 @@ export async function runInitCommand(parsed, context = {}) {
|
|
|
75
67
|
return { exitCode: 1, stderr: `Auto-init failed: ${error.message}\n` };
|
|
76
68
|
}
|
|
77
69
|
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Legacy interactive init with @inquirer/prompts.
|
|
81
|
-
* Kept for power users who want manual control.
|
|
82
|
-
* Will fail in non-TTY environments (Claude Code Bash tool).
|
|
83
|
-
*/
|
|
84
|
-
async function runInteractiveInit(parsed, context = {}) {
|
|
85
|
-
const cwd = context.cwd ?? process.cwd();
|
|
86
|
-
const wazirDir = path.join(cwd, '.wazir');
|
|
87
|
-
const configPath = path.join(wazirDir, 'state', 'config.json');
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
const { select } = await import('@inquirer/prompts');
|
|
91
|
-
|
|
92
|
-
for (const dir of ['input', 'state', 'runs']) {
|
|
93
|
-
fs.mkdirSync(path.join(wazirDir, dir), { recursive: true });
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const modelMode = await select({
|
|
97
|
-
message: 'How should Wazir run in this project?',
|
|
98
|
-
choices: [
|
|
99
|
-
{ name: 'Single model (Recommended)', value: 'claude-only' },
|
|
100
|
-
{ name: 'Multi-model (Haiku/Sonnet/Opus routing)', value: 'multi-model' },
|
|
101
|
-
{ name: 'Multi-tool (current model + external reviewers)', value: 'multi-tool' },
|
|
102
|
-
],
|
|
103
|
-
default: 'claude-only',
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
let multiToolTools = [];
|
|
107
|
-
if (modelMode === 'multi-tool') {
|
|
108
|
-
const toolChoice = await select({
|
|
109
|
-
message: 'Which external tools for reviews?',
|
|
110
|
-
choices: [
|
|
111
|
-
{ name: 'Codex', value: 'codex' },
|
|
112
|
-
{ name: 'Gemini', value: 'gemini' },
|
|
113
|
-
{ name: 'Both', value: 'both' },
|
|
114
|
-
],
|
|
115
|
-
});
|
|
116
|
-
multiToolTools = toolChoice === 'both' ? ['codex', 'gemini'] : [toolChoice];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let codexModel = null;
|
|
120
|
-
if (multiToolTools.includes('codex')) {
|
|
121
|
-
codexModel = await select({
|
|
122
|
-
message: 'Codex model?',
|
|
123
|
-
choices: [
|
|
124
|
-
{ name: 'gpt-5.3-codex-spark (Recommended)', value: 'gpt-5.3-codex-spark' },
|
|
125
|
-
{ name: 'gpt-5.4', value: 'gpt-5.4' },
|
|
126
|
-
],
|
|
127
|
-
default: 'gpt-5.3-codex-spark',
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const host = detectHost();
|
|
132
|
-
const stack = detectProjectStack(cwd);
|
|
133
|
-
|
|
134
|
-
const config = {
|
|
135
|
-
model_mode: modelMode,
|
|
136
|
-
...(modelMode === 'multi-tool' && {
|
|
137
|
-
multi_tool: {
|
|
138
|
-
tools: multiToolTools,
|
|
139
|
-
...(codexModel && { codex: { model: codexModel } }),
|
|
140
|
-
},
|
|
141
|
-
}),
|
|
142
|
-
default_depth: 'standard',
|
|
143
|
-
default_intent: 'feature',
|
|
144
|
-
team_mode: 'sequential',
|
|
145
|
-
parallel_backend: 'none',
|
|
146
|
-
detected_host: host.host,
|
|
147
|
-
detected_stack: stack,
|
|
148
|
-
};
|
|
149
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
exitCode: 0,
|
|
153
|
-
stdout: `\nInitialized (${modelMode}). Host: ${host.host}. Next: /wazir <request>\n`,
|
|
154
|
-
};
|
|
155
|
-
} catch (error) {
|
|
156
|
-
if (error.name === 'ExitPromptError') {
|
|
157
|
-
return { exitCode: 130, stderr: '\nInit cancelled.\n' };
|
|
158
|
-
}
|
|
159
|
-
return { exitCode: 1, stderr: `${error.message}\n` };
|
|
160
|
-
}
|
|
161
|
-
}
|
|
@@ -54,7 +54,12 @@ function success(payload, options = {}) {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
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
|
+
}
|
package/workflows/plan-review.md
CHANGED
|
@@ -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
|
|