@wazir-dev/cli 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +74 -10
  2. package/README.md +15 -15
  3. package/assets/demo.cast +47 -0
  4. package/assets/demo.gif +0 -0
  5. package/docs/anti-patterns/AP-23-skipping-enabled-workflows.md +28 -0
  6. package/docs/anti-patterns/AP-24-clarifier-deciding-scope.md +34 -0
  7. package/docs/concepts/architecture.md +1 -1
  8. package/docs/concepts/roles-and-workflows.md +2 -0
  9. package/docs/concepts/why-wazir.md +59 -0
  10. package/docs/decisions/2026-03-19-deferred-items.md +564 -0
  11. package/docs/decisions/2026-03-19-enhancement-decisions.md +300 -0
  12. package/docs/readmes/INDEX.md +21 -5
  13. package/docs/readmes/features/expertise/README.md +2 -2
  14. package/docs/readmes/features/exports/README.md +2 -2
  15. package/docs/readmes/features/hooks/pre-compact-summary.md +1 -1
  16. package/docs/readmes/features/schemas/README.md +3 -0
  17. package/docs/readmes/features/skills/README.md +17 -0
  18. package/docs/readmes/features/skills/clarifier.md +5 -0
  19. package/docs/readmes/features/skills/claude-cli.md +5 -0
  20. package/docs/readmes/features/skills/codex-cli.md +5 -0
  21. package/docs/readmes/features/skills/dispatching-parallel-agents.md +5 -0
  22. package/docs/readmes/features/skills/executing-plans.md +5 -0
  23. package/docs/readmes/features/skills/executor.md +5 -0
  24. package/docs/readmes/features/skills/finishing-a-development-branch.md +5 -0
  25. package/docs/readmes/features/skills/gemini-cli.md +5 -0
  26. package/docs/readmes/features/skills/humanize.md +5 -0
  27. package/docs/readmes/features/skills/init-pipeline.md +5 -0
  28. package/docs/readmes/features/skills/receiving-code-review.md +5 -0
  29. package/docs/readmes/features/skills/requesting-code-review.md +5 -0
  30. package/docs/readmes/features/skills/reviewer.md +5 -0
  31. package/docs/readmes/features/skills/subagent-driven-development.md +5 -0
  32. package/docs/readmes/features/skills/using-git-worktrees.md +5 -0
  33. package/docs/readmes/features/skills/wazir.md +5 -0
  34. package/docs/readmes/features/skills/writing-skills.md +5 -0
  35. package/docs/readmes/features/workflows/prepare-next.md +1 -1
  36. package/docs/reference/configuration-reference.md +47 -6
  37. package/docs/reference/hooks.md +1 -0
  38. package/docs/reference/launch-checklist.md +4 -4
  39. package/docs/reference/review-loop-pattern.md +119 -9
  40. package/docs/reference/roles-reference.md +1 -0
  41. package/docs/reference/skill-tiers.md +147 -0
  42. package/docs/reference/tooling-cli.md +3 -1
  43. package/docs/truth-claims.yaml +12 -0
  44. package/expertise/antipatterns/process/ai-coding-antipatterns.md +214 -1
  45. package/exports/hosts/claude/.claude/commands/plan-review.md +3 -1
  46. package/exports/hosts/claude/.claude/commands/verify.md +30 -1
  47. package/exports/hosts/claude/.claude/settings.json +9 -0
  48. package/exports/hosts/claude/CLAUDE.md +1 -1
  49. package/exports/hosts/claude/export.manifest.json +6 -4
  50. package/exports/hosts/claude/host-package.json +3 -1
  51. package/exports/hosts/codex/AGENTS.md +1 -1
  52. package/exports/hosts/codex/export.manifest.json +6 -4
  53. package/exports/hosts/codex/host-package.json +3 -1
  54. package/exports/hosts/cursor/.cursor/hooks.json +4 -0
  55. package/exports/hosts/cursor/.cursor/rules/wazir-core.mdc +1 -1
  56. package/exports/hosts/cursor/export.manifest.json +6 -4
  57. package/exports/hosts/cursor/host-package.json +3 -1
  58. package/exports/hosts/gemini/GEMINI.md +1 -1
  59. package/exports/hosts/gemini/export.manifest.json +6 -4
  60. package/exports/hosts/gemini/host-package.json +3 -1
  61. package/hooks/context-mode-router +191 -0
  62. package/hooks/definitions/context_mode_router.yaml +19 -0
  63. package/hooks/hooks.json +31 -6
  64. package/hooks/protected-path-write-guard +8 -0
  65. package/hooks/routing-matrix.json +45 -0
  66. package/hooks/session-start +62 -1
  67. package/llms-full.txt +937 -134
  68. package/package.json +2 -4
  69. package/schemas/hook.schema.json +2 -1
  70. package/schemas/phase-report.schema.json +89 -0
  71. package/schemas/usage.schema.json +25 -1
  72. package/schemas/wazir-manifest.schema.json +19 -0
  73. package/skills/brainstorming/SKILL.md +32 -157
  74. package/skills/clarifier/SKILL.md +289 -111
  75. package/skills/claude-cli/SKILL.md +320 -0
  76. package/skills/codex-cli/SKILL.md +260 -0
  77. package/skills/debugging/SKILL.md +13 -0
  78. package/skills/design/SKILL.md +13 -0
  79. package/skills/dispatching-parallel-agents/SKILL.md +13 -0
  80. package/skills/executing-plans/SKILL.md +13 -0
  81. package/skills/executor/SKILL.md +139 -19
  82. package/skills/finishing-a-development-branch/SKILL.md +13 -0
  83. package/skills/gemini-cli/SKILL.md +260 -0
  84. package/skills/humanize/SKILL.md +13 -0
  85. package/skills/init-pipeline/SKILL.md +72 -164
  86. package/skills/prepare-next/SKILL.md +81 -10
  87. package/skills/receiving-code-review/SKILL.md +13 -0
  88. package/skills/requesting-code-review/SKILL.md +13 -0
  89. package/skills/reviewer/SKILL.md +369 -24
  90. package/skills/run-audit/SKILL.md +13 -0
  91. package/skills/scan-project/SKILL.md +13 -0
  92. package/skills/self-audit/SKILL.md +217 -16
  93. package/skills/skill-research/SKILL.md +188 -0
  94. package/skills/subagent-driven-development/SKILL.md +13 -0
  95. package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +2 -0
  96. package/skills/subagent-driven-development/implementer-prompt.md +8 -0
  97. package/skills/subagent-driven-development/spec-reviewer-prompt.md +7 -0
  98. package/skills/tdd/SKILL.md +13 -0
  99. package/skills/using-git-worktrees/SKILL.md +13 -0
  100. package/skills/using-skills/SKILL.md +13 -0
  101. package/skills/verification/SKILL.md +54 -3
  102. package/skills/wazir/SKILL.md +464 -381
  103. package/skills/writing-plans/SKILL.md +14 -1
  104. package/skills/writing-skills/SKILL.md +13 -0
  105. package/templates/artifacts/implementation-plan.md +3 -0
  106. package/templates/artifacts/tasks-template.md +133 -0
  107. package/templates/examples/phase-report.example.json +48 -0
  108. package/tooling/src/adapters/composition-engine.js +256 -0
  109. package/tooling/src/adapters/model-router.js +84 -0
  110. package/tooling/src/capture/command.js +41 -2
  111. package/tooling/src/capture/run-config.js +3 -1
  112. package/tooling/src/capture/store.js +56 -0
  113. package/tooling/src/capture/usage.js +106 -0
  114. package/tooling/src/capture/user-input.js +66 -0
  115. package/tooling/src/checks/ac-matrix.js +256 -0
  116. package/tooling/src/checks/command-registry.js +12 -0
  117. package/tooling/src/checks/docs-truth.js +1 -1
  118. package/tooling/src/checks/security-sensitivity.js +69 -0
  119. package/tooling/src/checks/skills.js +111 -0
  120. package/tooling/src/cli.js +31 -20
  121. package/tooling/src/commands/stats.js +161 -0
  122. package/tooling/src/commands/validate.js +5 -1
  123. package/tooling/src/export/compiler.js +33 -37
  124. package/tooling/src/gating/agent.js +145 -0
  125. package/tooling/src/guards/phase-prerequisite-guard.js +185 -0
  126. package/tooling/src/hooks/routing-logic.js +69 -0
  127. package/tooling/src/init/auto-detect.js +258 -0
  128. package/tooling/src/init/command.js +38 -170
  129. package/tooling/src/input/scanner.js +46 -0
  130. package/tooling/src/reports/command.js +103 -0
  131. package/tooling/src/reports/phase-report.js +323 -0
  132. package/tooling/src/state/command.js +160 -0
  133. package/tooling/src/state/db.js +287 -0
  134. package/tooling/src/status/command.js +58 -1
  135. package/tooling/src/verify/proof-collector.js +299 -0
  136. package/wazir.manifest.yaml +26 -14
  137. package/workflows/plan-review.md +3 -1
  138. package/workflows/verify.md +30 -1
@@ -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
+ }
@@ -1,17 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Suppress Node.js ExperimentalWarning for built-in SQLite (node:sqlite).
4
+ // Must run before any module that transitively imports node:sqlite loads,
5
+ // so command handlers are lazy-imported below instead of using static imports.
6
+ const _originalEmit = process.emit;
7
+ process.emit = function (event, ...args) {
8
+ if (event === 'warning' && args[0]?.name === 'ExperimentalWarning' &&
9
+ args[0]?.message?.includes('SQLite')) {
10
+ return false;
11
+ }
12
+ return _originalEmit.apply(this, [event, ...args]);
13
+ };
14
+
3
15
  import fs from 'node:fs';
4
16
  import { fileURLToPath } from 'node:url';
5
17
 
6
- import { runCaptureCommand } from './capture/command.js';
7
- import { runValidateCommand } from './commands/validate.js';
8
- import { runDoctorCommand } from './doctor/command.js';
9
- import { runExportCommand as runGeneratedExportCommand } from './export/command.js';
10
- import { runIndexCommand } from './index/command.js';
11
- import { runInitCommand } from './init/command.js';
12
- import { runRecallCommand } from './recall/command.js';
13
- import { runStatusCommand } from './status/command.js';
14
-
15
18
  const COMMAND_FAMILIES = [
16
19
  'export',
17
20
  'validate',
@@ -19,19 +22,26 @@ const COMMAND_FAMILIES = [
19
22
  'index',
20
23
  'init',
21
24
  'recall',
25
+ 'report',
26
+ 'state',
22
27
  'status',
28
+ 'stats',
23
29
  'capture'
24
30
  ];
25
31
 
26
- const COMMAND_HANDLERS = {
27
- export: runGeneratedExportCommand,
28
- validate: runValidateCommand,
29
- doctor: runDoctorCommand,
30
- index: runIndexCommand,
31
- init: runInitCommand,
32
- recall: runRecallCommand,
33
- status: runStatusCommand,
34
- capture: runCaptureCommand,
32
+ // Lazy-load command handlers so the warning filter is active before node:sqlite loads
33
+ const COMMAND_LOADERS = {
34
+ export: () => import('./export/command.js').then(m => m.runExportCommand),
35
+ validate: () => import('./commands/validate.js').then(m => m.runValidateCommand),
36
+ doctor: () => import('./doctor/command.js').then(m => m.runDoctorCommand),
37
+ index: () => import('./index/command.js').then(m => m.runIndexCommand),
38
+ init: () => import('./init/command.js').then(m => m.runInitCommand),
39
+ recall: () => import('./recall/command.js').then(m => m.runRecallCommand),
40
+ report: () => import('./reports/command.js').then(m => m.runReportCommand),
41
+ state: () => import('./state/command.js').then(m => m.runStateCommand),
42
+ status: () => import('./status/command.js').then(m => m.runStatusCommand),
43
+ stats: () => import('./commands/stats.js').then(m => m.runStatsCommand),
44
+ capture: () => import('./capture/command.js').then(m => m.runCaptureCommand),
35
45
  };
36
46
 
37
47
  export function parseArgs(argv) {
@@ -79,9 +89,9 @@ export async function main(argv = process.argv.slice(2)) {
79
89
  return 1;
80
90
  }
81
91
 
82
- const handler = COMMAND_HANDLERS[parsed.command];
92
+ const loader = COMMAND_LOADERS[parsed.command];
83
93
 
84
- if (!handler) {
94
+ if (!loader) {
85
95
  console.error(`wazir ${parsed.command} is not implemented yet`);
86
96
  return 2;
87
97
  }
@@ -89,6 +99,7 @@ export async function main(argv = process.argv.slice(2)) {
89
99
  let result;
90
100
 
91
101
  try {
102
+ const handler = await loader();
92
103
  result = await handler(parsed);
93
104
  } catch (error) {
94
105
  console.error(error.message);
@@ -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
+ }