@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
|
@@ -5,7 +5,7 @@ import { readJsonFile, readYamlFile } from '../loaders.js';
|
|
|
5
5
|
import { validateAgainstSchema } from '../schema-validator.js';
|
|
6
6
|
import { SUPPORTED_COMMAND_SUBJECTS } from './command-registry.js';
|
|
7
7
|
|
|
8
|
-
const EXCLUDED_DOC_DIRS = new Set(['
|
|
8
|
+
const EXCLUDED_DOC_DIRS = new Set(['plans', 'research', 'audit', 'decisions']);
|
|
9
9
|
|
|
10
10
|
function walkMarkdownFiles(dirPath, files = []) {
|
|
11
11
|
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
@@ -9,13 +9,9 @@ const EXCLUDED_DOC_FILES = new Set([
|
|
|
9
9
|
]);
|
|
10
10
|
|
|
11
11
|
const FORBIDDEN_TEXT_PATTERNS = [
|
|
12
|
-
{ label: '.agent-os path', regex: /\.agent-os\//g },
|
|
13
12
|
{ label: 'tasks/input path', regex: /\btasks\/input\//g },
|
|
14
13
|
{ label: 'tasks/clarified path', regex: /\btasks\/clarified\//g },
|
|
15
14
|
{ label: 'legacy run wrapper', regex: /\/run-(clarifier|orchestrator|opus-reviewer)\b/g },
|
|
16
|
-
{ label: 'legacy daemon binary', regex: /\bagent-os-(daemon|run|review|orchestrate)\b/g },
|
|
17
|
-
{ label: 'legacy npx invocation', regex: /\bnpx agent-os-[a-z-]+\b/g },
|
|
18
|
-
{ label: 'daemon workflow config', regex: /daemon\/WORKFLOW\.md/g },
|
|
19
15
|
];
|
|
20
16
|
|
|
21
17
|
const FORBIDDEN_DEPENDENCIES = new Set(['express', 'fastify', 'koa', 'socket.io']);
|
|
@@ -93,9 +89,9 @@ function collectRuntimeSurfaceFiles(projectRoot) {
|
|
|
93
89
|
|
|
94
90
|
function normalizeAllowedLegacyReferences(content) {
|
|
95
91
|
return content
|
|
96
|
-
.replace(/archive\/legacy-
|
|
97
|
-
.replace(/archive\/v5\.1-
|
|
98
|
-
.replace(/migration\/v5\.1-
|
|
92
|
+
.replace(/archive\/legacy-wazir\/[^\s)`]*/g, 'archive/<legacy>')
|
|
93
|
+
.replace(/archive\/v5\.1-wazir-daemon\/[^\s)`]*/g, 'archive/<legacy>')
|
|
94
|
+
.replace(/migration\/v5\.1-wazir-rename\.md/g, 'migration/<legacy>');
|
|
99
95
|
}
|
|
100
96
|
|
|
101
97
|
function assertGlobalPatternConfiguration() {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Known superpowers skill names that conflict with Wazir skill names.
|
|
6
|
+
* A Wazir skill may share a name with a superpowers skill ONLY if it uses
|
|
7
|
+
* the wz: prefix — the Augment tier (CONTEXT.md companion) is not
|
|
8
|
+
* implementable (concluded in Task 18 R2) and is no longer supported.
|
|
9
|
+
*/
|
|
10
|
+
const SUPERPOWERS_SKILL_NAMES = new Set([
|
|
11
|
+
'brainstorming',
|
|
12
|
+
'dispatching-parallel-agents',
|
|
13
|
+
'executing-plans',
|
|
14
|
+
'finishing-a-development-branch',
|
|
15
|
+
'receiving-code-review',
|
|
16
|
+
'requesting-code-review',
|
|
17
|
+
'subagent-driven-development',
|
|
18
|
+
'using-git-worktrees',
|
|
19
|
+
'verification',
|
|
20
|
+
'writing-plans',
|
|
21
|
+
'writing-skills',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function parseFrontmatter(content) {
|
|
25
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
26
|
+
if (!match) return null;
|
|
27
|
+
|
|
28
|
+
const fields = {};
|
|
29
|
+
for (const line of match[1].split('\n')) {
|
|
30
|
+
const colonIndex = line.indexOf(':');
|
|
31
|
+
if (colonIndex === -1) continue;
|
|
32
|
+
const key = line.slice(0, colonIndex).trim();
|
|
33
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
34
|
+
fields[key] = value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return fields;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function listSkillDirs(skillsDir) {
|
|
41
|
+
if (!fs.existsSync(skillsDir)) return [];
|
|
42
|
+
|
|
43
|
+
return fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
44
|
+
.filter((entry) => entry.isDirectory())
|
|
45
|
+
.map((entry) => entry.name)
|
|
46
|
+
.sort();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function validateSkillsAtProjectRoot(projectRoot) {
|
|
50
|
+
const skillsDir = path.join(projectRoot, 'skills');
|
|
51
|
+
const errors = [];
|
|
52
|
+
const skillDirs = listSkillDirs(skillsDir);
|
|
53
|
+
let checkedCount = 0;
|
|
54
|
+
|
|
55
|
+
for (const dirName of skillDirs) {
|
|
56
|
+
const skillMdPath = path.join(skillsDir, dirName, 'SKILL.md');
|
|
57
|
+
|
|
58
|
+
if (!fs.existsSync(skillMdPath)) continue;
|
|
59
|
+
|
|
60
|
+
checkedCount++;
|
|
61
|
+
const content = fs.readFileSync(skillMdPath, 'utf8');
|
|
62
|
+
const frontmatter = parseFrontmatter(content);
|
|
63
|
+
|
|
64
|
+
if (!frontmatter) {
|
|
65
|
+
errors.push(`${dirName}: SKILL.md missing YAML frontmatter`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!frontmatter.name) {
|
|
70
|
+
errors.push(`${dirName}: SKILL.md missing name field in frontmatter`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const skillName = frontmatter.name;
|
|
75
|
+
const baseName = skillName.startsWith('wz:') ? skillName.slice(3) : skillName;
|
|
76
|
+
const hasWzPrefix = skillName.startsWith('wz:');
|
|
77
|
+
const conflictsWithSuperpowers = SUPERPOWERS_SKILL_NAMES.has(baseName);
|
|
78
|
+
const contextMdPath = path.join(skillsDir, dirName, 'CONTEXT.md');
|
|
79
|
+
const hasContextMd = fs.existsSync(contextMdPath);
|
|
80
|
+
|
|
81
|
+
// Check: conflicting name without wz: prefix must be flagged.
|
|
82
|
+
// Augment tier is not supported — the only resolution is the wz: prefix.
|
|
83
|
+
if (conflictsWithSuperpowers && !hasWzPrefix) {
|
|
84
|
+
errors.push(
|
|
85
|
+
`${dirName}: skill name "${skillName}" conflicts with superpowers:${baseName} — ` +
|
|
86
|
+
'add wz: prefix to resolve the conflict',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check: CONTEXT.md files are stale — augment tier is not implementable
|
|
91
|
+
// (concluded in Task 18 R2). Flag any remaining CONTEXT.md as an error.
|
|
92
|
+
if (hasContextMd) {
|
|
93
|
+
errors.push(
|
|
94
|
+
`${dirName}: CONTEXT.md is not supported — augment tier was removed. ` +
|
|
95
|
+
'Delete CONTEXT.md; all Wazir skills use the Own tier with wz: prefix.',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (errors.length > 0) {
|
|
101
|
+
return {
|
|
102
|
+
exitCode: 1,
|
|
103
|
+
stderr: `Skill validation failed:\n- ${errors.join('\n- ')}\n`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
exitCode: 0,
|
|
109
|
+
stdout: `Skill validation passed. Checked ${checkedCount} skills.\n`,
|
|
110
|
+
};
|
|
111
|
+
}
|
package/tooling/src/cli.js
CHANGED
|
@@ -8,7 +8,11 @@ import { runValidateCommand } from './commands/validate.js';
|
|
|
8
8
|
import { runDoctorCommand } from './doctor/command.js';
|
|
9
9
|
import { runExportCommand as runGeneratedExportCommand } from './export/command.js';
|
|
10
10
|
import { runIndexCommand } from './index/command.js';
|
|
11
|
+
import { runInitCommand } from './init/command.js';
|
|
11
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';
|
|
12
16
|
import { runStatusCommand } from './status/command.js';
|
|
13
17
|
|
|
14
18
|
const COMMAND_FAMILIES = [
|
|
@@ -16,8 +20,12 @@ const COMMAND_FAMILIES = [
|
|
|
16
20
|
'validate',
|
|
17
21
|
'doctor',
|
|
18
22
|
'index',
|
|
23
|
+
'init',
|
|
19
24
|
'recall',
|
|
25
|
+
'report',
|
|
26
|
+
'state',
|
|
20
27
|
'status',
|
|
28
|
+
'stats',
|
|
21
29
|
'capture'
|
|
22
30
|
];
|
|
23
31
|
|
|
@@ -26,8 +34,12 @@ const COMMAND_HANDLERS = {
|
|
|
26
34
|
validate: runValidateCommand,
|
|
27
35
|
doctor: runDoctorCommand,
|
|
28
36
|
index: runIndexCommand,
|
|
37
|
+
init: runInitCommand,
|
|
29
38
|
recall: runRecallCommand,
|
|
39
|
+
report: runReportCommand,
|
|
40
|
+
state: runStateCommand,
|
|
30
41
|
status: runStatusCommand,
|
|
42
|
+
stats: runStatsCommand,
|
|
31
43
|
capture: runCaptureCommand,
|
|
32
44
|
};
|
|
33
45
|
|
|
@@ -63,7 +75,7 @@ export function renderHelp() {
|
|
|
63
75
|
].join('\n');
|
|
64
76
|
}
|
|
65
77
|
|
|
66
|
-
export function main(argv = process.argv.slice(2)) {
|
|
78
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
67
79
|
const parsed = parseArgs(argv);
|
|
68
80
|
|
|
69
81
|
if (parsed.help || !parsed.command) {
|
|
@@ -86,7 +98,7 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
86
98
|
let result;
|
|
87
99
|
|
|
88
100
|
try {
|
|
89
|
-
result = handler(parsed);
|
|
101
|
+
result = await handler(parsed);
|
|
90
102
|
} catch (error) {
|
|
91
103
|
console.error(error.message);
|
|
92
104
|
return 1;
|
|
@@ -112,5 +124,7 @@ function isDirectExecution() {
|
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
if (isDirectExecution()) {
|
|
115
|
-
|
|
127
|
+
main().then((code) => {
|
|
128
|
+
process.exitCode = code;
|
|
129
|
+
});
|
|
116
130
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { parseCommandOptions } from '../command-options.js';
|
|
5
|
+
import { readYamlFile } from '../loaders.js';
|
|
6
|
+
import { findProjectRoot } from '../project-root.js';
|
|
7
|
+
import { resolveStateRoot } from '../state-root.js';
|
|
8
|
+
import { getRunPaths } from '../capture/store.js';
|
|
9
|
+
import { readUsage, estimateTokens, consumeRoutingLog } from '../capture/usage.js';
|
|
10
|
+
|
|
11
|
+
function formatNumber(n) {
|
|
12
|
+
return n.toLocaleString('en-US');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildStatsPayload(usage) {
|
|
16
|
+
const savings = usage.savings ?? {};
|
|
17
|
+
const cr = savings.capture_routing ?? {};
|
|
18
|
+
const cm = savings.context_mode ?? {};
|
|
19
|
+
const co = savings.compaction ?? {};
|
|
20
|
+
const iq = savings.index_queries ?? {};
|
|
21
|
+
|
|
22
|
+
const totalQueriesFromPhases = Object.values(usage.phases)
|
|
23
|
+
.reduce((sum, p) => sum + (p.events_count ?? 0), 0);
|
|
24
|
+
const totalQueries = totalQueriesFromPhases + (iq.count ?? 0);
|
|
25
|
+
|
|
26
|
+
const crTokensSaved = cr.estimated_tokens_avoided ?? 0;
|
|
27
|
+
const cmRawTokens = estimateTokens(Math.round((cm.raw_kb ?? 0) * 1024));
|
|
28
|
+
const cmAfterTokens = estimateTokens(Math.round((cm.context_kb ?? 0) * 1024));
|
|
29
|
+
const cmTokensSaved = cmRawTokens - cmAfterTokens;
|
|
30
|
+
const coTokensSaved = (co.pre_compaction_tokens_est ?? 0) - (co.post_compaction_tokens_est ?? 0);
|
|
31
|
+
const iqTokensSaved = iq.estimated_tokens_saved ?? 0;
|
|
32
|
+
|
|
33
|
+
const totalEstimatedTokensSaved = crTokensSaved + cmTokensSaved + coTokensSaved + iqTokensSaved;
|
|
34
|
+
const totalBytesAvoided = (cr.raw_bytes ?? 0) - (cr.summary_bytes ?? 0) + (iq.bytes_avoided ?? 0);
|
|
35
|
+
|
|
36
|
+
const crRawTokens = crTokensSaved + estimateTokens(cr.summary_bytes ?? 0);
|
|
37
|
+
const iqRawTokens = estimateTokens(iq.total_raw_bytes ?? 0);
|
|
38
|
+
const iqAfterTokens = estimateTokens(iq.total_summary_bytes ?? 0);
|
|
39
|
+
const withoutSavings = crRawTokens + cmRawTokens + (co.pre_compaction_tokens_est ?? 0) + iqRawTokens;
|
|
40
|
+
const withAll = estimateTokens(cr.summary_bytes ?? 0) + cmAfterTokens + (co.post_compaction_tokens_est ?? 0) + iqAfterTokens;
|
|
41
|
+
const savingsRatio = withoutSavings > 0
|
|
42
|
+
? `${((1 - withAll / withoutSavings) * 100).toFixed(1)}%`
|
|
43
|
+
: '0.0%';
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
run_id: usage.run_id,
|
|
47
|
+
total_queries: totalQueries,
|
|
48
|
+
total_estimated_tokens_saved: totalEstimatedTokensSaved,
|
|
49
|
+
total_bytes_avoided: totalBytesAvoided,
|
|
50
|
+
savings_ratio: savingsRatio,
|
|
51
|
+
per_tool: {
|
|
52
|
+
capture_routing: {
|
|
53
|
+
tokens_saved: crTokensSaved,
|
|
54
|
+
raw_bytes: cr.raw_bytes ?? 0,
|
|
55
|
+
summary_bytes: cr.summary_bytes ?? 0,
|
|
56
|
+
},
|
|
57
|
+
context_mode: {
|
|
58
|
+
tokens_saved: cmTokensSaved,
|
|
59
|
+
raw_kb: cm.raw_kb ?? 0,
|
|
60
|
+
context_kb: cm.context_kb ?? 0,
|
|
61
|
+
},
|
|
62
|
+
compaction: {
|
|
63
|
+
tokens_saved: coTokensSaved,
|
|
64
|
+
compaction_count: co.compaction_count ?? 0,
|
|
65
|
+
},
|
|
66
|
+
index_queries: {
|
|
67
|
+
tokens_saved: iqTokensSaved,
|
|
68
|
+
query_count: iq.count ?? 0,
|
|
69
|
+
bytes_avoided: iq.bytes_avoided ?? 0,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatTextOutput(payload) {
|
|
76
|
+
const lines = [
|
|
77
|
+
`Stats: ${payload.run_id}`,
|
|
78
|
+
'',
|
|
79
|
+
`Total queries: ${formatNumber(payload.total_queries)}`,
|
|
80
|
+
`Total estimated tokens saved: ${formatNumber(payload.total_estimated_tokens_saved)}`,
|
|
81
|
+
`Total bytes avoided: ${formatNumber(payload.total_bytes_avoided)}`,
|
|
82
|
+
`Overall savings ratio: ${payload.savings_ratio}`,
|
|
83
|
+
'',
|
|
84
|
+
'Per-tool breakdown:',
|
|
85
|
+
` Capture routing: ${formatNumber(payload.per_tool.capture_routing.tokens_saved)} tokens saved`,
|
|
86
|
+
` Context-mode: ${formatNumber(payload.per_tool.context_mode.tokens_saved)} tokens saved`,
|
|
87
|
+
` Compaction: ${formatNumber(payload.per_tool.compaction.tokens_saved)} tokens saved (${payload.per_tool.compaction.compaction_count} compactions)`,
|
|
88
|
+
` Index queries: ${formatNumber(payload.per_tool.index_queries.tokens_saved)} tokens saved (${payload.per_tool.index_queries.query_count} queries)`,
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function runStatsCommand(parsed, context = {}) {
|
|
95
|
+
try {
|
|
96
|
+
if (parsed.subcommand) {
|
|
97
|
+
return {
|
|
98
|
+
exitCode: 1,
|
|
99
|
+
stderr: 'Usage: wazir stats --run <id> [--state-root <path>] [--json]\n',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { options } = parseCommandOptions(parsed.args, {
|
|
104
|
+
boolean: ['json', 'help'],
|
|
105
|
+
string: ['run', 'state-root'],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (options.help) {
|
|
109
|
+
return {
|
|
110
|
+
exitCode: 0,
|
|
111
|
+
stdout: 'Usage: wazir stats --run <id> [--state-root <path>] [--json]\n\nShow token savings statistics for a run.\n',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!options.run) {
|
|
116
|
+
return {
|
|
117
|
+
exitCode: 1,
|
|
118
|
+
stderr: 'wazir stats requires --run <id>\n',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const projectRoot = findProjectRoot(context.cwd ?? process.cwd());
|
|
123
|
+
const manifest = readYamlFile(path.join(projectRoot, 'wazir.manifest.yaml'));
|
|
124
|
+
const stateRoot = resolveStateRoot(projectRoot, manifest, {
|
|
125
|
+
cwd: context.cwd ?? process.cwd(),
|
|
126
|
+
override: options.stateRoot,
|
|
127
|
+
});
|
|
128
|
+
const runPaths = getRunPaths(stateRoot, options.run);
|
|
129
|
+
|
|
130
|
+
if (!fs.existsSync(runPaths.usagePath)) {
|
|
131
|
+
return {
|
|
132
|
+
exitCode: 1,
|
|
133
|
+
stderr: `Run usage data not found: ${runPaths.usagePath}\n`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Lazy aggregation: consume the routing log before computing stats
|
|
138
|
+
// so that routing decisions are reflected in the usage data.
|
|
139
|
+
consumeRoutingLog(runPaths);
|
|
140
|
+
|
|
141
|
+
const usage = readUsage(runPaths);
|
|
142
|
+
const payload = buildStatsPayload(usage);
|
|
143
|
+
|
|
144
|
+
if (options.json) {
|
|
145
|
+
return {
|
|
146
|
+
exitCode: 0,
|
|
147
|
+
stdout: `${JSON.stringify(payload, null, 2)}\n`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
exitCode: 0,
|
|
153
|
+
stdout: `${formatTextOutput(payload)}\n`,
|
|
154
|
+
};
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return {
|
|
157
|
+
exitCode: 1,
|
|
158
|
+
stderr: `${error.message}\n`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -12,6 +12,7 @@ import { validateBranchName } from '../checks/branches.js';
|
|
|
12
12
|
import { validateCommits } from '../checks/commits.js';
|
|
13
13
|
import { validateChangelog } from '../checks/changelog.js';
|
|
14
14
|
import { runDocsDriftCheck } from '../checks/docs-drift.js';
|
|
15
|
+
import { validateSkillsAtProjectRoot } from '../checks/skills.js';
|
|
15
16
|
|
|
16
17
|
function success(stdout) {
|
|
17
18
|
return { exitCode: 0, stdout: `${stdout}\n` };
|
|
@@ -254,9 +255,11 @@ export function runValidateCommand(parsed, context = {}) {
|
|
|
254
255
|
strict: hasFlag(parsed.args, '--strict'),
|
|
255
256
|
cwd: projectRoot,
|
|
256
257
|
});
|
|
258
|
+
case 'skills':
|
|
259
|
+
return validateSkillsAtProjectRoot(projectRoot);
|
|
257
260
|
default: {
|
|
258
261
|
if (parsed.subcommand != null) {
|
|
259
|
-
return failure(`Unknown validator: ${parsed.subcommand}\nUsage: wazir validate <manifest|hooks|docs|brand|runtime|branches|commits|changelog|docs-drift>`);
|
|
262
|
+
return failure(`Unknown validator: ${parsed.subcommand}\nUsage: wazir validate <manifest|hooks|docs|brand|runtime|branches|commits|changelog|docs-drift|skills>`);
|
|
260
263
|
}
|
|
261
264
|
|
|
262
265
|
if (parsed.args.length > 0) {
|
|
@@ -290,6 +293,7 @@ export function runValidateCommand(parsed, context = {}) {
|
|
|
290
293
|
{ name: 'branches', fn: () => validateBranchName(undefined, { cwd: projectRoot }), available: hasBranch },
|
|
291
294
|
{ name: 'commits', fn: () => validateCommits({ cwd: projectRoot }), available: hasGit },
|
|
292
295
|
{ name: 'changelog', fn: () => validateChangelog(projectRoot, {}), available: hasChangelog },
|
|
296
|
+
{ name: 'skills', fn: () => validateSkillsAtProjectRoot(projectRoot) },
|
|
293
297
|
];
|
|
294
298
|
|
|
295
299
|
const lines = [];
|
|
@@ -40,12 +40,18 @@ function listDeclaredWorkflowFiles(projectRoot, manifest) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function collectCanonicalSources(projectRoot, manifest) {
|
|
43
|
-
|
|
43
|
+
const sources = [
|
|
44
44
|
path.join(projectRoot, 'wazir.manifest.yaml'),
|
|
45
45
|
...listDeclaredRoleFiles(projectRoot, manifest),
|
|
46
46
|
...listDeclaredWorkflowFiles(projectRoot, manifest),
|
|
47
47
|
...listHookDefinitions(path.join(projectRoot, 'hooks', 'definitions')),
|
|
48
48
|
];
|
|
49
|
+
// hooks.json is a canonical source for Claude settings generation
|
|
50
|
+
const hooksJson = path.join(projectRoot, 'hooks', 'hooks.json');
|
|
51
|
+
if (fs.existsSync(hooksJson)) {
|
|
52
|
+
sources.push(hooksJson);
|
|
53
|
+
}
|
|
54
|
+
return sources;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
function toRelativeMap(projectRoot, filePaths) {
|
|
@@ -82,41 +88,27 @@ function renderCommonInstructions(host, manifest) {
|
|
|
82
88
|
].join('\n');
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
],
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
matcher: 'startup|resume|clear|compact',
|
|
110
|
-
hooks: [
|
|
111
|
-
{
|
|
112
|
-
type: 'command',
|
|
113
|
-
command: './hooks/session-start',
|
|
114
|
-
},
|
|
115
|
-
],
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
},
|
|
119
|
-
}, null, 2);
|
|
91
|
+
const DEFAULT_CLAUDE_HOOKS = {
|
|
92
|
+
hooks: {
|
|
93
|
+
PreToolUse: [
|
|
94
|
+
{ matcher: 'Write|Edit', hooks: [{ type: 'command', command: './hooks/protected-path-write-guard' }] },
|
|
95
|
+
{ matcher: 'Bash', hooks: [{ type: 'command', command: './hooks/context-mode-router' }] },
|
|
96
|
+
],
|
|
97
|
+
SessionStart: [
|
|
98
|
+
{ hooks: [{ type: 'command', command: './hooks/loop-cap-guard' }] },
|
|
99
|
+
{ matcher: 'startup|resume|clear|compact', hooks: [{ type: 'command', command: './hooks/session-start' }] },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function renderClaudeSettings(projectRoot) {
|
|
105
|
+
const hooksPath = path.join(projectRoot, 'hooks', 'hooks.json');
|
|
106
|
+
if (fs.existsSync(hooksPath)) {
|
|
107
|
+
const hooksContent = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
|
|
108
|
+
return JSON.stringify(hooksContent, null, 2);
|
|
109
|
+
}
|
|
110
|
+
// Fallback: default hooks when hooks.json doesn't exist (e.g., new projects)
|
|
111
|
+
return JSON.stringify(DEFAULT_CLAUDE_HOOKS, null, 2);
|
|
120
112
|
}
|
|
121
113
|
|
|
122
114
|
function renderCursorHooks() {
|
|
@@ -130,6 +122,10 @@ function renderCursorHooks() {
|
|
|
130
122
|
name: 'loop-cap-guard',
|
|
131
123
|
command: './hooks/loop-cap-guard',
|
|
132
124
|
},
|
|
125
|
+
{
|
|
126
|
+
name: 'context-mode-router',
|
|
127
|
+
command: './hooks/context-mode-router',
|
|
128
|
+
},
|
|
133
129
|
{
|
|
134
130
|
name: 'session-start',
|
|
135
131
|
command: './hooks/session-start',
|
|
@@ -146,7 +142,7 @@ function generateHostFiles(projectRoot, manifest, host) {
|
|
|
146
142
|
|
|
147
143
|
if (host === 'claude') {
|
|
148
144
|
files['CLAUDE.md'] = common;
|
|
149
|
-
files['.claude/settings.json'] = renderClaudeSettings();
|
|
145
|
+
files['.claude/settings.json'] = renderClaudeSettings(projectRoot);
|
|
150
146
|
|
|
151
147
|
for (const roleFile of roleFiles) {
|
|
152
148
|
files[path.join('.claude', 'agents', path.basename(roleFile))] = fs.readFileSync(roleFile, 'utf8');
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { readYamlFile } from '../loaders.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a dotted field path (e.g. "quality_metrics.test_fail_count") on an object.
|
|
5
|
+
* Returns undefined when any segment is missing.
|
|
6
|
+
*/
|
|
7
|
+
function resolvePath(obj, fieldPath) {
|
|
8
|
+
const segments = fieldPath.split('.');
|
|
9
|
+
let current = obj;
|
|
10
|
+
for (const seg of segments) {
|
|
11
|
+
if (current == null || typeof current !== 'object') return undefined;
|
|
12
|
+
current = current[seg];
|
|
13
|
+
}
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check whether a single condition holds against a report.
|
|
19
|
+
*/
|
|
20
|
+
function evaluateCondition(report, condition) {
|
|
21
|
+
const value = resolvePath(report, condition.field);
|
|
22
|
+
|
|
23
|
+
switch (condition.operator) {
|
|
24
|
+
case 'eq':
|
|
25
|
+
return value === condition.value;
|
|
26
|
+
|
|
27
|
+
case 'gt':
|
|
28
|
+
return typeof value === 'number' && value > condition.value;
|
|
29
|
+
|
|
30
|
+
case 'lt':
|
|
31
|
+
return typeof value === 'number' && value < condition.value;
|
|
32
|
+
|
|
33
|
+
case 'none_match': {
|
|
34
|
+
// field must be an array; none of its items may match all key/value
|
|
35
|
+
// pairs in condition.match
|
|
36
|
+
if (!Array.isArray(value)) return value === undefined || value === null;
|
|
37
|
+
const matchEntries = Object.entries(condition.match);
|
|
38
|
+
return value.every(
|
|
39
|
+
(item) => !matchEntries.every(([k, v]) => item[k] === v),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
default:
|
|
44
|
+
// Unknown operator — treat as ambiguous → condition fails
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract human-readable failure descriptions from the report for loop_back fixes.
|
|
51
|
+
*/
|
|
52
|
+
function extractFixes(report) {
|
|
53
|
+
const fixes = [];
|
|
54
|
+
|
|
55
|
+
const testFails = resolvePath(report, 'quality_metrics.test_fail_count');
|
|
56
|
+
if (typeof testFails === 'number' && testFails > 0) {
|
|
57
|
+
fixes.push(`fix ${testFails} failing test(s)`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const lintErrors = resolvePath(report, 'quality_metrics.lint_errors');
|
|
61
|
+
if (typeof lintErrors === 'number' && lintErrors > 0) {
|
|
62
|
+
fixes.push(`fix ${lintErrors} lint error(s)`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const typeErrors = resolvePath(report, 'quality_metrics.type_errors');
|
|
66
|
+
if (typeof typeErrors === 'number' && typeErrors > 0) {
|
|
67
|
+
fixes.push(`fix ${typeErrors} type error(s)`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return fixes;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Evaluate a phase report against gating rules and return a verdict.
|
|
75
|
+
*
|
|
76
|
+
* @param {object|null|undefined} report — parsed phase report JSON
|
|
77
|
+
* @param {string} rulesPath — path to gating-rules.yaml
|
|
78
|
+
* @param {object} [context={}] — additional context for the evaluation
|
|
79
|
+
* @param {string} [context.userInput] — optional user input that triggered the evaluation
|
|
80
|
+
* @param {object} [context.decisions] — optional prior decisions for multi-phase flows
|
|
81
|
+
* @returns {{ verdict: string, reason: string, fixes?: string[] }}
|
|
82
|
+
*/
|
|
83
|
+
export function evaluatePhaseReport(report, rulesPath, context = {}) {
|
|
84
|
+
// --- Load rules --------------------------------------------------------
|
|
85
|
+
let rules;
|
|
86
|
+
try {
|
|
87
|
+
rules = readYamlFile(rulesPath);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return {
|
|
90
|
+
verdict: 'escalate',
|
|
91
|
+
reason: `Failed to load gating rules: ${err.message}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- Guard: missing / empty report -------------------------------------
|
|
96
|
+
if (report == null || typeof report !== 'object' || Object.keys(report).length === 0) {
|
|
97
|
+
return {
|
|
98
|
+
verdict: rules.default_verdict ?? 'escalate',
|
|
99
|
+
reason: 'Report is empty or missing',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// --- Try "continue" rule -----------------------------------------------
|
|
104
|
+
const continueRule = rules.rules?.continue;
|
|
105
|
+
if (continueRule && Array.isArray(continueRule.conditions) && continueRule.conditions.length > 0) {
|
|
106
|
+
const allPass = continueRule.conditions.every((c) => evaluateCondition(report, c));
|
|
107
|
+
if (allPass) {
|
|
108
|
+
return { verdict: 'continue', reason: continueRule.description };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- Try "loop_back" rule ----------------------------------------------
|
|
113
|
+
// The loop_back rule fires when ANY deterministic failure exists.
|
|
114
|
+
// The YAML conditions encode test_fail_count > 0 explicitly; the agent
|
|
115
|
+
// also checks lint_errors > 0 and type_errors > 0 per the rule comments
|
|
116
|
+
// ("# OR lint_errors > 0 OR type_errors > 0").
|
|
117
|
+
const loopBackRule = rules.rules?.loop_back;
|
|
118
|
+
if (loopBackRule) {
|
|
119
|
+
const explicitMatch = Array.isArray(loopBackRule.conditions)
|
|
120
|
+
&& loopBackRule.conditions.length > 0
|
|
121
|
+
&& loopBackRule.conditions.some((c) => evaluateCondition(report, c));
|
|
122
|
+
|
|
123
|
+
const lintErrors = resolvePath(report, 'quality_metrics.lint_errors');
|
|
124
|
+
const typeErrors = resolvePath(report, 'quality_metrics.type_errors');
|
|
125
|
+
const implicitMatch =
|
|
126
|
+
(typeof lintErrors === 'number' && lintErrors > 0)
|
|
127
|
+
|| (typeof typeErrors === 'number' && typeErrors > 0);
|
|
128
|
+
|
|
129
|
+
if (explicitMatch || implicitMatch) {
|
|
130
|
+
const fixes = extractFixes(report);
|
|
131
|
+
return {
|
|
132
|
+
verdict: 'loop_back',
|
|
133
|
+
reason: loopBackRule.description,
|
|
134
|
+
fixes,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- Fallback to "escalate" --------------------------------------------
|
|
140
|
+
const escalateRule = rules.rules?.escalate;
|
|
141
|
+
return {
|
|
142
|
+
verdict: 'escalate',
|
|
143
|
+
reason: escalateRule?.description ?? 'Default escalation — no rule matched',
|
|
144
|
+
};
|
|
145
|
+
}
|