@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.
- package/CHANGELOG.md +100 -2
- package/README.md +6 -6
- 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/plans/2026-03-15-cli-pipeline-integration-plan.md +1 -1
- 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/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/launch-checklist.md +4 -4
- package/docs/reference/review-loop-pattern.md +538 -0
- package/docs/reference/roles-reference.md +1 -0
- package/docs/reference/skill-tiers.md +147 -0
- package/docs/reference/tooling-cli.md +5 -1
- package/docs/truth-claims.yaml +18 -0
- package/expertise/antipatterns/process/ai-coding-antipatterns.md +97 -1
- package/exports/hosts/claude/.claude/agents/clarifier.md +3 -0
- package/exports/hosts/claude/.claude/agents/designer.md +3 -0
- package/exports/hosts/claude/.claude/agents/executor.md +2 -0
- package/exports/hosts/claude/.claude/agents/planner.md +3 -0
- package/exports/hosts/claude/.claude/agents/researcher.md +2 -0
- package/exports/hosts/claude/.claude/agents/reviewer.md +5 -1
- package/exports/hosts/claude/.claude/agents/specifier.md +3 -0
- package/exports/hosts/claude/.claude/commands/clarify.md +4 -0
- package/exports/hosts/claude/.claude/commands/design-review.md +4 -0
- package/exports/hosts/claude/.claude/commands/design.md +4 -0
- package/exports/hosts/claude/.claude/commands/discover.md +4 -0
- package/exports/hosts/claude/.claude/commands/execute.md +4 -0
- package/exports/hosts/claude/.claude/commands/plan-review.md +4 -0
- package/exports/hosts/claude/.claude/commands/plan.md +4 -0
- package/exports/hosts/claude/.claude/commands/spec-challenge.md +4 -0
- package/exports/hosts/claude/.claude/commands/specify.md +4 -0
- package/exports/hosts/claude/.claude/commands/verify.md +4 -0
- package/exports/hosts/claude/.claude/settings.json +9 -0
- package/exports/hosts/claude/CLAUDE.md +1 -1
- package/exports/hosts/claude/export.manifest.json +22 -20
- package/exports/hosts/claude/host-package.json +3 -1
- package/exports/hosts/codex/AGENTS.md +1 -1
- package/exports/hosts/codex/export.manifest.json +22 -20
- 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 +22 -20
- package/exports/hosts/cursor/host-package.json +3 -1
- package/exports/hosts/gemini/GEMINI.md +1 -1
- package/exports/hosts/gemini/export.manifest.json +22 -20
- 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/definitions/loop_cap_guard.yaml +1 -1
- package/hooks/hooks.json +43 -0
- 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 +905 -132
- package/package.json +3 -3
- package/roles/clarifier.md +3 -0
- package/roles/designer.md +3 -0
- package/roles/executor.md +2 -0
- package/roles/planner.md +3 -0
- package/roles/researcher.md +2 -0
- package/roles/reviewer.md +5 -1
- package/roles/specifier.md +3 -0
- package/schemas/hook.schema.json +2 -1
- package/schemas/phase-report.schema.json +80 -0
- package/schemas/usage.schema.json +25 -1
- package/schemas/wazir-manifest.schema.json +19 -0
- package/skills/brainstorming/SKILL.md +20 -56
- package/skills/clarifier/SKILL.md +243 -0
- package/skills/claude-cli/SKILL.md +320 -0
- package/skills/codex-cli/SKILL.md +260 -0
- package/skills/debugging/SKILL.md +24 -1
- package/skills/design/SKILL.md +13 -0
- package/skills/dispatching-parallel-agents/SKILL.md +13 -0
- package/skills/executing-plans/SKILL.md +28 -2
- package/skills/executor/SKILL.md +129 -0
- 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 +76 -78
- package/skills/prepare-next/SKILL.md +81 -10
- package/skills/receiving-code-review/SKILL.md +21 -0
- package/skills/requesting-code-review/SKILL.md +38 -5
- package/skills/reviewer/SKILL.md +423 -0
- package/skills/run-audit/SKILL.md +13 -0
- package/skills/scan-project/SKILL.md +13 -0
- package/skills/self-audit/SKILL.md +197 -16
- package/skills/subagent-driven-development/SKILL.md +38 -2
- 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 +21 -0
- package/skills/using-git-worktrees/SKILL.md +13 -0
- package/skills/using-skills/SKILL.md +13 -0
- package/skills/verification/SKILL.md +13 -0
- package/skills/wazir/SKILL.md +286 -262
- package/skills/writing-plans/SKILL.md +44 -4
- 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/templates/examples/wazir-manifest.example.yaml +1 -1
- package/tooling/src/adapters/composition-engine.js +256 -0
- package/tooling/src/adapters/model-router.js +84 -0
- package/tooling/src/capture/command.js +111 -2
- package/tooling/src/capture/run-config.js +23 -0
- package/tooling/src/capture/store.js +24 -0
- package/tooling/src/capture/usage.js +106 -0
- package/tooling/src/checks/ac-matrix.js +256 -0
- package/tooling/src/checks/brand-truth.js +3 -6
- package/tooling/src/checks/command-registry.js +13 -0
- package/tooling/src/checks/docs-truth.js +1 -1
- package/tooling/src/checks/runtime-surface.js +3 -7
- package/tooling/src/checks/skills.js +111 -0
- package/tooling/src/cli.js +17 -3
- 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 +127 -0
- package/tooling/src/hooks/routing-logic.js +69 -0
- package/tooling/src/init/auto-detect.js +260 -0
- package/tooling/src/init/command.js +161 -0
- 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 +53 -1
- package/wazir.manifest.yaml +26 -17
- package/workflows/clarify.md +4 -0
- package/workflows/design-review.md +4 -0
- package/workflows/design.md +4 -0
- package/workflows/discover.md +4 -0
- package/workflows/execute.md +4 -0
- package/workflows/plan-review.md +4 -0
- package/workflows/plan.md +4 -0
- package/workflows/spec-challenge.md +4 -0
- package/workflows/specify.md +4 -0
- package/workflows/verify.md +4 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { parseCommandOptions } from '../command-options.js';
|
|
4
|
+
import { readYamlFile } from '../loaders.js';
|
|
5
|
+
import { findProjectRoot } from '../project-root.js';
|
|
6
|
+
import { resolveStateRoot } from '../state-root.js';
|
|
7
|
+
import { collectPhaseMetrics, buildPhaseReport } from './phase-report.js';
|
|
8
|
+
|
|
9
|
+
const USAGE = 'Usage: wazir report phase --run <run-id> --phase <phase> [--base <branch>] [--json]';
|
|
10
|
+
|
|
11
|
+
function handlePhase(parsed, context = {}) {
|
|
12
|
+
const { options } = parseCommandOptions(parsed.args, {
|
|
13
|
+
boolean: ['json', 'help'],
|
|
14
|
+
string: ['run', 'phase', 'base', 'state-root'],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (options.help) {
|
|
18
|
+
return {
|
|
19
|
+
exitCode: 0,
|
|
20
|
+
stdout: `${USAGE}\n\nGenerate a structured phase report with metrics from git, tests, and run state.\n`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!options.run) {
|
|
25
|
+
return {
|
|
26
|
+
exitCode: 1,
|
|
27
|
+
stderr: `wazir report phase requires --run <id>\n${USAGE}\n`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!options.phase) {
|
|
32
|
+
return {
|
|
33
|
+
exitCode: 1,
|
|
34
|
+
stderr: `wazir report phase requires --phase <phase>\n${USAGE}\n`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const projectRoot = findProjectRoot(context.cwd ?? process.cwd());
|
|
39
|
+
const manifest = readYamlFile(path.join(projectRoot, 'wazir.manifest.yaml'));
|
|
40
|
+
const stateRoot = resolveStateRoot(projectRoot, manifest, {
|
|
41
|
+
cwd: context.cwd ?? process.cwd(),
|
|
42
|
+
override: options.stateRoot,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const metrics = collectPhaseMetrics({
|
|
46
|
+
projectRoot,
|
|
47
|
+
stateRoot,
|
|
48
|
+
runId: options.run,
|
|
49
|
+
phase: options.phase,
|
|
50
|
+
baseBranch: options.base ?? 'main',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const report = buildPhaseReport(metrics);
|
|
54
|
+
|
|
55
|
+
if (options.json) {
|
|
56
|
+
return {
|
|
57
|
+
exitCode: 0,
|
|
58
|
+
stdout: `${JSON.stringify(report, null, 2)}\n`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Human-readable text output
|
|
63
|
+
const lines = [
|
|
64
|
+
`Phase Report: ${report.phase} (run ${report.run_id})`,
|
|
65
|
+
`Generated: ${report.generated_at}`,
|
|
66
|
+
'',
|
|
67
|
+
'Tests:',
|
|
68
|
+
` total: ${report.metrics.tests.total}, passed: ${report.metrics.tests.passed}, failed: ${report.metrics.tests.failed}, skipped: ${report.metrics.tests.skipped}`,
|
|
69
|
+
'',
|
|
70
|
+
'Diff:',
|
|
71
|
+
` files changed: ${report.metrics.diff.files_changed}, insertions: ${report.metrics.diff.insertions}, deletions: ${report.metrics.diff.deletions}`,
|
|
72
|
+
'',
|
|
73
|
+
'Files:',
|
|
74
|
+
` added: ${report.metrics.files.added.length}, modified: ${report.metrics.files.modified.length}, deleted: ${report.metrics.files.deleted.length}`,
|
|
75
|
+
'',
|
|
76
|
+
`Artifacts: ${report.metrics.artifacts.length}`,
|
|
77
|
+
`Duration: ${report.metrics.duration_seconds !== null ? `${report.metrics.duration_seconds}s` : 'N/A'}`,
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
exitCode: 0,
|
|
82
|
+
stdout: `${lines.join('\n')}\n`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function runReportCommand(parsed, context = {}) {
|
|
87
|
+
try {
|
|
88
|
+
switch (parsed.subcommand) {
|
|
89
|
+
case 'phase':
|
|
90
|
+
return handlePhase(parsed, context);
|
|
91
|
+
default:
|
|
92
|
+
return {
|
|
93
|
+
exitCode: 1,
|
|
94
|
+
stderr: `${USAGE}\n`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return {
|
|
99
|
+
exitCode: 1,
|
|
100
|
+
stderr: `${error.message}\n`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse Node.js test runner output to extract pass/fail/skip counts.
|
|
7
|
+
* Handles the built-in `node --test` reporter format.
|
|
8
|
+
* @param {string} output - raw test runner stdout+stderr
|
|
9
|
+
* @returns {{ total: number, passed: number, failed: number, skipped: number }}
|
|
10
|
+
*/
|
|
11
|
+
export function parseTestOutput(output) {
|
|
12
|
+
const result = { total: 0, passed: 0, failed: 0, skipped: 0 };
|
|
13
|
+
|
|
14
|
+
if (!output) {
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Node built-in test runner summary lines:
|
|
19
|
+
// # tests 12
|
|
20
|
+
// # pass 10
|
|
21
|
+
// # fail 1
|
|
22
|
+
// # skipped 1
|
|
23
|
+
const totalMatch = output.match(/^# tests\s+(\d+)/m);
|
|
24
|
+
const passMatch = output.match(/^# pass\s+(\d+)/m);
|
|
25
|
+
const failMatch = output.match(/^# fail\s+(\d+)/m);
|
|
26
|
+
const skipMatch = output.match(/^# skipped\s+(\d+)/m);
|
|
27
|
+
|
|
28
|
+
if (totalMatch) {
|
|
29
|
+
result.total = Number.parseInt(totalMatch[1], 10);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (passMatch) {
|
|
33
|
+
result.passed = Number.parseInt(passMatch[1], 10);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (failMatch) {
|
|
37
|
+
result.failed = Number.parseInt(failMatch[1], 10);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (skipMatch) {
|
|
41
|
+
result.skipped = Number.parseInt(skipMatch[1], 10);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Fallback: if no summary lines found, try TAP-style counting
|
|
45
|
+
if (!totalMatch && !passMatch && !failMatch) {
|
|
46
|
+
const okLines = output.match(/^ok \d+/gm);
|
|
47
|
+
const notOkLines = output.match(/^not ok \d+/gm);
|
|
48
|
+
const skipLines = output.match(/^ok \d+ .+# skip/gim);
|
|
49
|
+
|
|
50
|
+
result.passed = (okLines?.length ?? 0) - (skipLines?.length ?? 0);
|
|
51
|
+
result.failed = notOkLines?.length ?? 0;
|
|
52
|
+
result.skipped = skipLines?.length ?? 0;
|
|
53
|
+
result.total = result.passed + result.failed + result.skipped;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Run tests and parse results, or return null on failure.
|
|
61
|
+
* @param {string} projectRoot
|
|
62
|
+
* @returns {{ total: number, passed: number, failed: number, skipped: number } | null}
|
|
63
|
+
*/
|
|
64
|
+
function collectTestResults(projectRoot) {
|
|
65
|
+
try {
|
|
66
|
+
const output = execFileSync(
|
|
67
|
+
'node',
|
|
68
|
+
['--test', '--experimental-test-snapshots'],
|
|
69
|
+
{ cwd: projectRoot, encoding: 'utf8', timeout: 120_000 },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return parseTestOutput(output);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// Test command may exit non-zero if tests fail — still parse output
|
|
75
|
+
if (error.stdout || error.stderr) {
|
|
76
|
+
return parseTestOutput(`${error.stdout ?? ''}${error.stderr ?? ''}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse `git diff --stat` output for files changed, insertions, deletions.
|
|
85
|
+
* @param {string} statOutput - output of git diff --stat
|
|
86
|
+
* @returns {{ files_changed: number, insertions: number, deletions: number }}
|
|
87
|
+
*/
|
|
88
|
+
export function parseDiffStat(statOutput) {
|
|
89
|
+
const result = { files_changed: 0, insertions: 0, deletions: 0 };
|
|
90
|
+
|
|
91
|
+
if (!statOutput) {
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Summary line: " 5 files changed, 120 insertions(+), 30 deletions(-)"
|
|
96
|
+
const summaryMatch = statOutput.match(
|
|
97
|
+
/(\d+) files? changed(?:,\s*(\d+) insertions?\(\+\))?(?:,\s*(\d+) deletions?\(-\))?/,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (summaryMatch) {
|
|
101
|
+
result.files_changed = Number.parseInt(summaryMatch[1], 10);
|
|
102
|
+
result.insertions = summaryMatch[2] ? Number.parseInt(summaryMatch[2], 10) : 0;
|
|
103
|
+
result.deletions = summaryMatch[3] ? Number.parseInt(summaryMatch[3], 10) : 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse `git diff --name-status` output into added/modified/deleted arrays.
|
|
111
|
+
* @param {string} nameStatusOutput
|
|
112
|
+
* @returns {{ added: string[], modified: string[], deleted: string[] }}
|
|
113
|
+
*/
|
|
114
|
+
export function parseNameStatus(nameStatusOutput) {
|
|
115
|
+
const result = { added: [], modified: [], deleted: [] };
|
|
116
|
+
|
|
117
|
+
if (!nameStatusOutput) {
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const line of nameStatusOutput.split('\n')) {
|
|
122
|
+
const trimmed = line.trim();
|
|
123
|
+
|
|
124
|
+
if (!trimmed) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const statusChar = trimmed[0];
|
|
129
|
+
const filePath = trimmed.slice(1).trim();
|
|
130
|
+
|
|
131
|
+
if (!filePath) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
switch (statusChar) {
|
|
136
|
+
case 'A':
|
|
137
|
+
result.added.push(filePath);
|
|
138
|
+
break;
|
|
139
|
+
case 'M':
|
|
140
|
+
result.modified.push(filePath);
|
|
141
|
+
break;
|
|
142
|
+
case 'D':
|
|
143
|
+
result.deleted.push(filePath);
|
|
144
|
+
break;
|
|
145
|
+
default:
|
|
146
|
+
// R (rename), C (copy), etc. — treat as modified
|
|
147
|
+
result.modified.push(filePath);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Collect diff stats from git.
|
|
157
|
+
* @param {string} projectRoot
|
|
158
|
+
* @param {string} baseBranch
|
|
159
|
+
* @returns {{ files_changed: number, insertions: number, deletions: number } | null}
|
|
160
|
+
*/
|
|
161
|
+
function collectDiffStats(projectRoot, baseBranch) {
|
|
162
|
+
try {
|
|
163
|
+
const output = execFileSync(
|
|
164
|
+
'git',
|
|
165
|
+
['diff', '--stat', `${baseBranch}...HEAD`],
|
|
166
|
+
{ cwd: projectRoot, encoding: 'utf8', timeout: 15_000 },
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return parseDiffStat(output);
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Collect file change summary from git.
|
|
177
|
+
* @param {string} projectRoot
|
|
178
|
+
* @param {string} baseBranch
|
|
179
|
+
* @returns {{ added: string[], modified: string[], deleted: string[] } | null}
|
|
180
|
+
*/
|
|
181
|
+
function collectFileChanges(projectRoot, baseBranch) {
|
|
182
|
+
try {
|
|
183
|
+
const output = execFileSync(
|
|
184
|
+
'git',
|
|
185
|
+
['diff', '--name-status', `${baseBranch}...HEAD`],
|
|
186
|
+
{ cwd: projectRoot, encoding: 'utf8', timeout: 15_000 },
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return parseNameStatus(output);
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* List artifact files in run directories.
|
|
197
|
+
* @param {string} stateRoot
|
|
198
|
+
* @param {string} runId
|
|
199
|
+
* @returns {string[]}
|
|
200
|
+
*/
|
|
201
|
+
function collectArtifacts(stateRoot, runId) {
|
|
202
|
+
const artifacts = [];
|
|
203
|
+
const clarifiedDir = path.join(stateRoot, 'runs', runId, 'clarified');
|
|
204
|
+
const artifactsDir = path.join(stateRoot, 'runs', runId, 'artifacts');
|
|
205
|
+
|
|
206
|
+
for (const dir of [clarifiedDir, artifactsDir]) {
|
|
207
|
+
if (fs.existsSync(dir)) {
|
|
208
|
+
try {
|
|
209
|
+
const entries = fs.readdirSync(dir);
|
|
210
|
+
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
artifacts.push(path.join(dir, entry));
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// ignore read errors
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return artifacts;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Compute phase duration from events.ndjson.
|
|
225
|
+
* Looks for phase_enter and phase_exit events matching the given phase.
|
|
226
|
+
* @param {string} stateRoot
|
|
227
|
+
* @param {string} runId
|
|
228
|
+
* @param {string} phase
|
|
229
|
+
* @returns {number | null} duration in seconds, or null if unavailable
|
|
230
|
+
*/
|
|
231
|
+
function collectDuration(stateRoot, runId, phase) {
|
|
232
|
+
const eventsPath = path.join(stateRoot, 'runs', runId, 'events.ndjson');
|
|
233
|
+
|
|
234
|
+
if (!fs.existsSync(eventsPath)) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const content = fs.readFileSync(eventsPath, 'utf8');
|
|
240
|
+
const lines = content.split('\n').filter(Boolean);
|
|
241
|
+
|
|
242
|
+
let enterTime = null;
|
|
243
|
+
let exitTime = null;
|
|
244
|
+
|
|
245
|
+
for (const line of lines) {
|
|
246
|
+
const event = JSON.parse(line);
|
|
247
|
+
|
|
248
|
+
if (event.phase !== phase) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (event.event === 'phase_enter' && event.created_at) {
|
|
253
|
+
enterTime = new Date(event.created_at).getTime();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (event.event === 'phase_exit' && event.created_at) {
|
|
257
|
+
exitTime = new Date(event.created_at).getTime();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (enterTime !== null && exitTime !== null && exitTime > enterTime) {
|
|
262
|
+
return Math.round((exitTime - enterTime) / 1000);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return null;
|
|
266
|
+
} catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Collect metrics for a phase report.
|
|
273
|
+
* @param {object} opts
|
|
274
|
+
* @param {string} opts.projectRoot - project root path
|
|
275
|
+
* @param {string} opts.stateRoot - state root path
|
|
276
|
+
* @param {string} opts.runId - current run ID
|
|
277
|
+
* @param {string} opts.phase - phase name (init|clarifier|executor|final_review)
|
|
278
|
+
* @param {string} [opts.baseBranch] - base branch for diff (default: 'main')
|
|
279
|
+
* @returns {object} structured metrics
|
|
280
|
+
*/
|
|
281
|
+
export function collectPhaseMetrics(opts) {
|
|
282
|
+
const { projectRoot, stateRoot, runId, phase } = opts;
|
|
283
|
+
const baseBranch = opts.baseBranch ?? 'main';
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
run_id: runId,
|
|
287
|
+
phase,
|
|
288
|
+
generated_at: new Date().toISOString(),
|
|
289
|
+
tests: collectTestResults(projectRoot),
|
|
290
|
+
diff: collectDiffStats(projectRoot, baseBranch),
|
|
291
|
+
files: collectFileChanges(projectRoot, baseBranch),
|
|
292
|
+
artifacts: collectArtifacts(stateRoot, runId),
|
|
293
|
+
duration_seconds: collectDuration(stateRoot, runId, phase),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Build a complete phase report JSON.
|
|
299
|
+
* Deterministic fields from code, qualitative fields left as placeholders for agent.
|
|
300
|
+
* @param {object} metrics - output from collectPhaseMetrics
|
|
301
|
+
* @param {object} [qualitative] - agent-provided qualitative fields
|
|
302
|
+
* @returns {object} complete report
|
|
303
|
+
*/
|
|
304
|
+
export function buildPhaseReport(metrics, qualitative = {}) {
|
|
305
|
+
return {
|
|
306
|
+
run_id: metrics.run_id,
|
|
307
|
+
phase: metrics.phase,
|
|
308
|
+
generated_at: metrics.generated_at,
|
|
309
|
+
metrics: {
|
|
310
|
+
tests: metrics.tests ?? { total: 0, passed: 0, failed: 0, skipped: 0 },
|
|
311
|
+
diff: metrics.diff ?? { files_changed: 0, insertions: 0, deletions: 0 },
|
|
312
|
+
files: metrics.files ?? { added: [], modified: [], deleted: [] },
|
|
313
|
+
artifacts: metrics.artifacts ?? [],
|
|
314
|
+
duration_seconds: metrics.duration_seconds ?? null,
|
|
315
|
+
},
|
|
316
|
+
qualitative: {
|
|
317
|
+
summary: qualitative.summary ?? '',
|
|
318
|
+
drift_analysis: qualitative.drift_analysis ?? '',
|
|
319
|
+
decisions: qualitative.decisions ?? [],
|
|
320
|
+
risks: qualitative.risks ?? [],
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { parseCommandOptions } from '../command-options.js';
|
|
4
|
+
import { readYamlFile } from '../loaders.js';
|
|
5
|
+
import { findProjectRoot } from '../project-root.js';
|
|
6
|
+
import { resolveStateRoot } from '../state-root.js';
|
|
7
|
+
import {
|
|
8
|
+
closeStateDb,
|
|
9
|
+
getAuditTrend,
|
|
10
|
+
getFindingsByRun,
|
|
11
|
+
getLearningsByScope,
|
|
12
|
+
getStateCounts,
|
|
13
|
+
getUsageSummary,
|
|
14
|
+
openStateDb,
|
|
15
|
+
} from './db.js';
|
|
16
|
+
|
|
17
|
+
function success(payload, options = {}) {
|
|
18
|
+
if (options.json) {
|
|
19
|
+
return {
|
|
20
|
+
exitCode: 0,
|
|
21
|
+
stdout: `${JSON.stringify(payload, null, 2)}\n`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
exitCode: 0,
|
|
27
|
+
stdout: `${options.formatText ? options.formatText(payload) : String(payload)}\n`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function failure(message, exitCode = 1) {
|
|
32
|
+
return {
|
|
33
|
+
exitCode,
|
|
34
|
+
stderr: `${message}\n`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadProjectContext(context, stateRootOverride) {
|
|
39
|
+
const projectRoot = findProjectRoot(context.cwd ?? process.cwd());
|
|
40
|
+
const manifest = readYamlFile(path.join(projectRoot, 'wazir.manifest.yaml'));
|
|
41
|
+
const stateRoot = resolveStateRoot(projectRoot, manifest, {
|
|
42
|
+
cwd: context.cwd ?? process.cwd(),
|
|
43
|
+
override: stateRootOverride,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
projectRoot,
|
|
48
|
+
manifest,
|
|
49
|
+
stateRoot,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function runStateCommand(parsed, context = {}) {
|
|
54
|
+
try {
|
|
55
|
+
const { positional, options } = parseCommandOptions(parsed.args, {
|
|
56
|
+
boolean: ['json'],
|
|
57
|
+
string: ['state-root', 'limit'],
|
|
58
|
+
});
|
|
59
|
+
const { stateRoot } = loadProjectContext(context, options.stateRoot);
|
|
60
|
+
|
|
61
|
+
switch (parsed.subcommand) {
|
|
62
|
+
case 'stats': {
|
|
63
|
+
const db = openStateDb(stateRoot);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const counts = getStateCounts(db);
|
|
67
|
+
const usage = getUsageSummary(db);
|
|
68
|
+
|
|
69
|
+
return success({ ...counts, usage }, {
|
|
70
|
+
json: options.json,
|
|
71
|
+
formatText: (value) => [
|
|
72
|
+
`Learnings: ${value.learning_count}`,
|
|
73
|
+
`Findings: ${value.finding_count}`,
|
|
74
|
+
`Audits: ${value.audit_count}`,
|
|
75
|
+
`Usage records: ${value.usage_count}`,
|
|
76
|
+
].join('\n'),
|
|
77
|
+
});
|
|
78
|
+
} finally {
|
|
79
|
+
closeStateDb(db);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case 'learnings': {
|
|
84
|
+
const db = openStateDb(stateRoot);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const limit = options.limit ? Number(options.limit) : undefined;
|
|
88
|
+
const learnings = getLearningsByScope(db, { limit });
|
|
89
|
+
|
|
90
|
+
return success(learnings, {
|
|
91
|
+
json: options.json,
|
|
92
|
+
formatText: (rows) => rows.length === 0
|
|
93
|
+
? 'No learnings recorded.'
|
|
94
|
+
: rows.map((row) => {
|
|
95
|
+
const scope = [row.scope_roles, row.scope_stacks, row.scope_concerns]
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.join(', ');
|
|
98
|
+
return `[${row.category}] ${row.content}${scope ? ` (${scope})` : ''} x${row.recurrence_count}`;
|
|
99
|
+
}).join('\n'),
|
|
100
|
+
});
|
|
101
|
+
} finally {
|
|
102
|
+
closeStateDb(db);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case 'findings': {
|
|
107
|
+
const runId = positional[0];
|
|
108
|
+
|
|
109
|
+
if (!runId) {
|
|
110
|
+
return failure('Usage: wazir state findings <run-id> [--state-root <path>] [--json]');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const db = openStateDb(stateRoot);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const findings = getFindingsByRun(db, runId);
|
|
117
|
+
|
|
118
|
+
return success(findings, {
|
|
119
|
+
json: options.json,
|
|
120
|
+
formatText: (rows) => rows.length === 0
|
|
121
|
+
? `No findings for run ${runId}.`
|
|
122
|
+
: rows.map((row) => {
|
|
123
|
+
const status = row.resolved ? 'RESOLVED' : 'OPEN';
|
|
124
|
+
return `[${row.severity}] [${status}] ${row.description} (${row.source}, ${row.phase})`;
|
|
125
|
+
}).join('\n'),
|
|
126
|
+
});
|
|
127
|
+
} finally {
|
|
128
|
+
closeStateDb(db);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case 'trend': {
|
|
133
|
+
const db = openStateDb(stateRoot);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const limit = options.limit ? Number(options.limit) : 10;
|
|
137
|
+
const trend = getAuditTrend(db, limit);
|
|
138
|
+
|
|
139
|
+
return success(trend, {
|
|
140
|
+
json: options.json,
|
|
141
|
+
formatText: (rows) => rows.length === 0
|
|
142
|
+
? 'No audit history.'
|
|
143
|
+
: rows.map((row) => {
|
|
144
|
+
const before = row.quality_score_before != null ? row.quality_score_before.toFixed(1) : '?';
|
|
145
|
+
const after = row.quality_score_after != null ? row.quality_score_after.toFixed(1) : '?';
|
|
146
|
+
return `${row.date} ${row.run_id} findings=${row.finding_count} fixed=${row.fix_count} manual=${row.manual_count} quality=${before}->${after}`;
|
|
147
|
+
}).join('\n'),
|
|
148
|
+
});
|
|
149
|
+
} finally {
|
|
150
|
+
closeStateDb(db);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
default:
|
|
155
|
+
return failure('Usage: wazir state <stats|learnings|findings|trend> [options]');
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return failure(error.message);
|
|
159
|
+
}
|
|
160
|
+
}
|