@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.
Files changed (163) hide show
  1. package/CHANGELOG.md +100 -2
  2. package/README.md +6 -6
  3. package/docs/concepts/architecture.md +1 -1
  4. package/docs/concepts/roles-and-workflows.md +2 -0
  5. package/docs/concepts/why-wazir.md +59 -0
  6. package/docs/decisions/2026-03-19-deferred-items.md +564 -0
  7. package/docs/decisions/2026-03-19-enhancement-decisions.md +300 -0
  8. package/docs/plans/2026-03-15-cli-pipeline-integration-plan.md +1 -1
  9. package/docs/readmes/INDEX.md +21 -5
  10. package/docs/readmes/features/expertise/README.md +2 -2
  11. package/docs/readmes/features/exports/README.md +2 -2
  12. package/docs/readmes/features/schemas/README.md +3 -0
  13. package/docs/readmes/features/skills/README.md +17 -0
  14. package/docs/readmes/features/skills/clarifier.md +5 -0
  15. package/docs/readmes/features/skills/claude-cli.md +5 -0
  16. package/docs/readmes/features/skills/codex-cli.md +5 -0
  17. package/docs/readmes/features/skills/dispatching-parallel-agents.md +5 -0
  18. package/docs/readmes/features/skills/executing-plans.md +5 -0
  19. package/docs/readmes/features/skills/executor.md +5 -0
  20. package/docs/readmes/features/skills/finishing-a-development-branch.md +5 -0
  21. package/docs/readmes/features/skills/gemini-cli.md +5 -0
  22. package/docs/readmes/features/skills/humanize.md +5 -0
  23. package/docs/readmes/features/skills/init-pipeline.md +5 -0
  24. package/docs/readmes/features/skills/receiving-code-review.md +5 -0
  25. package/docs/readmes/features/skills/requesting-code-review.md +5 -0
  26. package/docs/readmes/features/skills/reviewer.md +5 -0
  27. package/docs/readmes/features/skills/subagent-driven-development.md +5 -0
  28. package/docs/readmes/features/skills/using-git-worktrees.md +5 -0
  29. package/docs/readmes/features/skills/wazir.md +5 -0
  30. package/docs/readmes/features/skills/writing-skills.md +5 -0
  31. package/docs/readmes/features/workflows/prepare-next.md +1 -1
  32. package/docs/reference/configuration-reference.md +47 -6
  33. package/docs/reference/launch-checklist.md +4 -4
  34. package/docs/reference/review-loop-pattern.md +538 -0
  35. package/docs/reference/roles-reference.md +1 -0
  36. package/docs/reference/skill-tiers.md +147 -0
  37. package/docs/reference/tooling-cli.md +5 -1
  38. package/docs/truth-claims.yaml +18 -0
  39. package/expertise/antipatterns/process/ai-coding-antipatterns.md +97 -1
  40. package/exports/hosts/claude/.claude/agents/clarifier.md +3 -0
  41. package/exports/hosts/claude/.claude/agents/designer.md +3 -0
  42. package/exports/hosts/claude/.claude/agents/executor.md +2 -0
  43. package/exports/hosts/claude/.claude/agents/planner.md +3 -0
  44. package/exports/hosts/claude/.claude/agents/researcher.md +2 -0
  45. package/exports/hosts/claude/.claude/agents/reviewer.md +5 -1
  46. package/exports/hosts/claude/.claude/agents/specifier.md +3 -0
  47. package/exports/hosts/claude/.claude/commands/clarify.md +4 -0
  48. package/exports/hosts/claude/.claude/commands/design-review.md +4 -0
  49. package/exports/hosts/claude/.claude/commands/design.md +4 -0
  50. package/exports/hosts/claude/.claude/commands/discover.md +4 -0
  51. package/exports/hosts/claude/.claude/commands/execute.md +4 -0
  52. package/exports/hosts/claude/.claude/commands/plan-review.md +4 -0
  53. package/exports/hosts/claude/.claude/commands/plan.md +4 -0
  54. package/exports/hosts/claude/.claude/commands/spec-challenge.md +4 -0
  55. package/exports/hosts/claude/.claude/commands/specify.md +4 -0
  56. package/exports/hosts/claude/.claude/commands/verify.md +4 -0
  57. package/exports/hosts/claude/.claude/settings.json +9 -0
  58. package/exports/hosts/claude/CLAUDE.md +1 -1
  59. package/exports/hosts/claude/export.manifest.json +22 -20
  60. package/exports/hosts/claude/host-package.json +3 -1
  61. package/exports/hosts/codex/AGENTS.md +1 -1
  62. package/exports/hosts/codex/export.manifest.json +22 -20
  63. package/exports/hosts/codex/host-package.json +3 -1
  64. package/exports/hosts/cursor/.cursor/hooks.json +4 -0
  65. package/exports/hosts/cursor/.cursor/rules/wazir-core.mdc +1 -1
  66. package/exports/hosts/cursor/export.manifest.json +22 -20
  67. package/exports/hosts/cursor/host-package.json +3 -1
  68. package/exports/hosts/gemini/GEMINI.md +1 -1
  69. package/exports/hosts/gemini/export.manifest.json +22 -20
  70. package/exports/hosts/gemini/host-package.json +3 -1
  71. package/hooks/context-mode-router +191 -0
  72. package/hooks/definitions/context_mode_router.yaml +19 -0
  73. package/hooks/definitions/loop_cap_guard.yaml +1 -1
  74. package/hooks/hooks.json +43 -0
  75. package/hooks/protected-path-write-guard +8 -0
  76. package/hooks/routing-matrix.json +45 -0
  77. package/hooks/session-start +62 -1
  78. package/llms-full.txt +905 -132
  79. package/package.json +3 -3
  80. package/roles/clarifier.md +3 -0
  81. package/roles/designer.md +3 -0
  82. package/roles/executor.md +2 -0
  83. package/roles/planner.md +3 -0
  84. package/roles/researcher.md +2 -0
  85. package/roles/reviewer.md +5 -1
  86. package/roles/specifier.md +3 -0
  87. package/schemas/hook.schema.json +2 -1
  88. package/schemas/phase-report.schema.json +80 -0
  89. package/schemas/usage.schema.json +25 -1
  90. package/schemas/wazir-manifest.schema.json +19 -0
  91. package/skills/brainstorming/SKILL.md +20 -56
  92. package/skills/clarifier/SKILL.md +243 -0
  93. package/skills/claude-cli/SKILL.md +320 -0
  94. package/skills/codex-cli/SKILL.md +260 -0
  95. package/skills/debugging/SKILL.md +24 -1
  96. package/skills/design/SKILL.md +13 -0
  97. package/skills/dispatching-parallel-agents/SKILL.md +13 -0
  98. package/skills/executing-plans/SKILL.md +28 -2
  99. package/skills/executor/SKILL.md +129 -0
  100. package/skills/finishing-a-development-branch/SKILL.md +13 -0
  101. package/skills/gemini-cli/SKILL.md +260 -0
  102. package/skills/humanize/SKILL.md +13 -0
  103. package/skills/init-pipeline/SKILL.md +76 -78
  104. package/skills/prepare-next/SKILL.md +81 -10
  105. package/skills/receiving-code-review/SKILL.md +21 -0
  106. package/skills/requesting-code-review/SKILL.md +38 -5
  107. package/skills/reviewer/SKILL.md +423 -0
  108. package/skills/run-audit/SKILL.md +13 -0
  109. package/skills/scan-project/SKILL.md +13 -0
  110. package/skills/self-audit/SKILL.md +197 -16
  111. package/skills/subagent-driven-development/SKILL.md +38 -2
  112. package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +2 -0
  113. package/skills/subagent-driven-development/implementer-prompt.md +8 -0
  114. package/skills/subagent-driven-development/spec-reviewer-prompt.md +7 -0
  115. package/skills/tdd/SKILL.md +21 -0
  116. package/skills/using-git-worktrees/SKILL.md +13 -0
  117. package/skills/using-skills/SKILL.md +13 -0
  118. package/skills/verification/SKILL.md +13 -0
  119. package/skills/wazir/SKILL.md +286 -262
  120. package/skills/writing-plans/SKILL.md +44 -4
  121. package/skills/writing-skills/SKILL.md +13 -0
  122. package/templates/artifacts/implementation-plan.md +3 -0
  123. package/templates/artifacts/tasks-template.md +133 -0
  124. package/templates/examples/phase-report.example.json +48 -0
  125. package/templates/examples/wazir-manifest.example.yaml +1 -1
  126. package/tooling/src/adapters/composition-engine.js +256 -0
  127. package/tooling/src/adapters/model-router.js +84 -0
  128. package/tooling/src/capture/command.js +111 -2
  129. package/tooling/src/capture/run-config.js +23 -0
  130. package/tooling/src/capture/store.js +24 -0
  131. package/tooling/src/capture/usage.js +106 -0
  132. package/tooling/src/checks/ac-matrix.js +256 -0
  133. package/tooling/src/checks/brand-truth.js +3 -6
  134. package/tooling/src/checks/command-registry.js +13 -0
  135. package/tooling/src/checks/docs-truth.js +1 -1
  136. package/tooling/src/checks/runtime-surface.js +3 -7
  137. package/tooling/src/checks/skills.js +111 -0
  138. package/tooling/src/cli.js +17 -3
  139. package/tooling/src/commands/stats.js +161 -0
  140. package/tooling/src/commands/validate.js +5 -1
  141. package/tooling/src/export/compiler.js +33 -37
  142. package/tooling/src/gating/agent.js +145 -0
  143. package/tooling/src/guards/phase-prerequisite-guard.js +127 -0
  144. package/tooling/src/hooks/routing-logic.js +69 -0
  145. package/tooling/src/init/auto-detect.js +260 -0
  146. package/tooling/src/init/command.js +161 -0
  147. package/tooling/src/input/scanner.js +46 -0
  148. package/tooling/src/reports/command.js +103 -0
  149. package/tooling/src/reports/phase-report.js +323 -0
  150. package/tooling/src/state/command.js +160 -0
  151. package/tooling/src/state/db.js +287 -0
  152. package/tooling/src/status/command.js +53 -1
  153. package/wazir.manifest.yaml +26 -17
  154. package/workflows/clarify.md +4 -0
  155. package/workflows/design-review.md +4 -0
  156. package/workflows/design.md +4 -0
  157. package/workflows/discover.md +4 -0
  158. package/workflows/execute.md +4 -0
  159. package/workflows/plan-review.md +4 -0
  160. package/workflows/plan.md +4 -0
  161. package/workflows/spec-challenge.md +4 -0
  162. package/workflows/specify.md +4 -0
  163. 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(['daemon', 'plans', 'research', 'audit']);
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-agent-os\/[^\s)`]*/g, 'archive/<legacy>')
97
- .replace(/archive\/v5\.1-agent-os-daemon\/[^\s)`]*/g, 'archive/<legacy>')
98
- .replace(/migration\/v5\.1-agent-os-to-wazir\.md/g, 'migration/<legacy>');
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
+ }
@@ -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
- process.exitCode = main();
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
- return [
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
- function renderClaudeSettings() {
86
- return JSON.stringify({
87
- hooks: {
88
- PreToolUse: [
89
- {
90
- matcher: 'Write|Edit',
91
- hooks: [
92
- {
93
- type: 'command',
94
- command: './hooks/protected-path-write-guard',
95
- },
96
- ],
97
- },
98
- ],
99
- SessionStart: [
100
- {
101
- hooks: [
102
- {
103
- type: 'command',
104
- command: './hooks/loop-cap-guard',
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
+ }