@wazir-dev/cli 1.1.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 (124) hide show
  1. package/CHANGELOG.md +73 -4
  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/readmes/INDEX.md +21 -5
  9. package/docs/readmes/features/expertise/README.md +2 -2
  10. package/docs/readmes/features/exports/README.md +2 -2
  11. package/docs/readmes/features/schemas/README.md +3 -0
  12. package/docs/readmes/features/skills/README.md +17 -0
  13. package/docs/readmes/features/skills/clarifier.md +5 -0
  14. package/docs/readmes/features/skills/claude-cli.md +5 -0
  15. package/docs/readmes/features/skills/codex-cli.md +5 -0
  16. package/docs/readmes/features/skills/dispatching-parallel-agents.md +5 -0
  17. package/docs/readmes/features/skills/executing-plans.md +5 -0
  18. package/docs/readmes/features/skills/executor.md +5 -0
  19. package/docs/readmes/features/skills/finishing-a-development-branch.md +5 -0
  20. package/docs/readmes/features/skills/gemini-cli.md +5 -0
  21. package/docs/readmes/features/skills/humanize.md +5 -0
  22. package/docs/readmes/features/skills/init-pipeline.md +5 -0
  23. package/docs/readmes/features/skills/receiving-code-review.md +5 -0
  24. package/docs/readmes/features/skills/requesting-code-review.md +5 -0
  25. package/docs/readmes/features/skills/reviewer.md +5 -0
  26. package/docs/readmes/features/skills/subagent-driven-development.md +5 -0
  27. package/docs/readmes/features/skills/using-git-worktrees.md +5 -0
  28. package/docs/readmes/features/skills/wazir.md +5 -0
  29. package/docs/readmes/features/skills/writing-skills.md +5 -0
  30. package/docs/readmes/features/workflows/prepare-next.md +1 -1
  31. package/docs/reference/configuration-reference.md +47 -6
  32. package/docs/reference/launch-checklist.md +4 -4
  33. package/docs/reference/review-loop-pattern.md +117 -8
  34. package/docs/reference/roles-reference.md +1 -0
  35. package/docs/reference/skill-tiers.md +147 -0
  36. package/docs/reference/tooling-cli.md +3 -1
  37. package/docs/truth-claims.yaml +12 -0
  38. package/expertise/antipatterns/process/ai-coding-antipatterns.md +97 -1
  39. package/exports/hosts/claude/.claude/settings.json +9 -0
  40. package/exports/hosts/claude/CLAUDE.md +1 -1
  41. package/exports/hosts/claude/export.manifest.json +4 -2
  42. package/exports/hosts/claude/host-package.json +3 -1
  43. package/exports/hosts/codex/AGENTS.md +1 -1
  44. package/exports/hosts/codex/export.manifest.json +4 -2
  45. package/exports/hosts/codex/host-package.json +3 -1
  46. package/exports/hosts/cursor/.cursor/hooks.json +4 -0
  47. package/exports/hosts/cursor/.cursor/rules/wazir-core.mdc +1 -1
  48. package/exports/hosts/cursor/export.manifest.json +4 -2
  49. package/exports/hosts/cursor/host-package.json +3 -1
  50. package/exports/hosts/gemini/GEMINI.md +1 -1
  51. package/exports/hosts/gemini/export.manifest.json +4 -2
  52. package/exports/hosts/gemini/host-package.json +3 -1
  53. package/hooks/context-mode-router +191 -0
  54. package/hooks/definitions/context_mode_router.yaml +19 -0
  55. package/hooks/hooks.json +31 -6
  56. package/hooks/protected-path-write-guard +8 -0
  57. package/hooks/routing-matrix.json +45 -0
  58. package/hooks/session-start +62 -1
  59. package/llms-full.txt +905 -132
  60. package/package.json +2 -3
  61. package/schemas/hook.schema.json +2 -1
  62. package/schemas/phase-report.schema.json +80 -0
  63. package/schemas/usage.schema.json +25 -1
  64. package/schemas/wazir-manifest.schema.json +19 -0
  65. package/skills/brainstorming/SKILL.md +18 -155
  66. package/skills/clarifier/SKILL.md +122 -98
  67. package/skills/claude-cli/SKILL.md +320 -0
  68. package/skills/codex-cli/SKILL.md +260 -0
  69. package/skills/debugging/SKILL.md +13 -0
  70. package/skills/design/SKILL.md +13 -0
  71. package/skills/dispatching-parallel-agents/SKILL.md +13 -0
  72. package/skills/executing-plans/SKILL.md +13 -0
  73. package/skills/executor/SKILL.md +72 -19
  74. package/skills/finishing-a-development-branch/SKILL.md +13 -0
  75. package/skills/gemini-cli/SKILL.md +260 -0
  76. package/skills/humanize/SKILL.md +13 -0
  77. package/skills/init-pipeline/SKILL.md +73 -164
  78. package/skills/prepare-next/SKILL.md +81 -10
  79. package/skills/receiving-code-review/SKILL.md +13 -0
  80. package/skills/requesting-code-review/SKILL.md +13 -0
  81. package/skills/reviewer/SKILL.md +287 -15
  82. package/skills/run-audit/SKILL.md +13 -0
  83. package/skills/scan-project/SKILL.md +13 -0
  84. package/skills/self-audit/SKILL.md +197 -16
  85. package/skills/subagent-driven-development/SKILL.md +13 -0
  86. package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +2 -0
  87. package/skills/subagent-driven-development/implementer-prompt.md +8 -0
  88. package/skills/subagent-driven-development/spec-reviewer-prompt.md +7 -0
  89. package/skills/tdd/SKILL.md +13 -0
  90. package/skills/using-git-worktrees/SKILL.md +13 -0
  91. package/skills/using-skills/SKILL.md +13 -0
  92. package/skills/verification/SKILL.md +13 -0
  93. package/skills/wazir/SKILL.md +194 -377
  94. package/skills/writing-plans/SKILL.md +14 -1
  95. package/skills/writing-skills/SKILL.md +13 -0
  96. package/templates/artifacts/implementation-plan.md +3 -0
  97. package/templates/artifacts/tasks-template.md +133 -0
  98. package/templates/examples/phase-report.example.json +48 -0
  99. package/tooling/src/adapters/composition-engine.js +256 -0
  100. package/tooling/src/adapters/model-router.js +84 -0
  101. package/tooling/src/capture/command.js +24 -1
  102. package/tooling/src/capture/run-config.js +3 -1
  103. package/tooling/src/capture/store.js +24 -0
  104. package/tooling/src/capture/usage.js +106 -0
  105. package/tooling/src/checks/ac-matrix.js +256 -0
  106. package/tooling/src/checks/command-registry.js +12 -0
  107. package/tooling/src/checks/docs-truth.js +1 -1
  108. package/tooling/src/checks/skills.js +111 -0
  109. package/tooling/src/cli.js +9 -0
  110. package/tooling/src/commands/stats.js +161 -0
  111. package/tooling/src/commands/validate.js +5 -1
  112. package/tooling/src/export/compiler.js +33 -37
  113. package/tooling/src/gating/agent.js +145 -0
  114. package/tooling/src/guards/phase-prerequisite-guard.js +127 -0
  115. package/tooling/src/hooks/routing-logic.js +69 -0
  116. package/tooling/src/init/auto-detect.js +260 -0
  117. package/tooling/src/init/command.js +95 -135
  118. package/tooling/src/input/scanner.js +46 -0
  119. package/tooling/src/reports/command.js +103 -0
  120. package/tooling/src/reports/phase-report.js +323 -0
  121. package/tooling/src/state/command.js +160 -0
  122. package/tooling/src/state/db.js +287 -0
  123. package/tooling/src/status/command.js +53 -1
  124. package/wazir.manifest.yaml +26 -14
@@ -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
+ }
@@ -0,0 +1,127 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { readYamlFile } from '../loaders.js';
5
+ import { getRunPaths, readPhaseExitEvents } from '../capture/store.js';
6
+
7
+ export function evaluateScopeCoverageGuard(payload) {
8
+ const { input_item_count: inputCount, plan_task_count: planCount, user_approved_reduction: userApproved } = payload;
9
+
10
+ const safeInputCount = inputCount ?? 0;
11
+ const safePlanCount = planCount ?? 0;
12
+
13
+ if (safeInputCount === 0) {
14
+ return {
15
+ allowed: true,
16
+ reason: 'No input items to check against.',
17
+ input_count: safeInputCount,
18
+ plan_count: safePlanCount,
19
+ };
20
+ }
21
+
22
+ if (safePlanCount >= safeInputCount) {
23
+ return {
24
+ allowed: true,
25
+ reason: `Plan covers all input items (${safePlanCount} tasks >= ${safeInputCount} items).`,
26
+ input_count: safeInputCount,
27
+ plan_count: safePlanCount,
28
+ };
29
+ }
30
+
31
+ if (userApproved === true) {
32
+ return {
33
+ allowed: true,
34
+ reason: `User explicitly approved scope reduction (${safePlanCount} tasks < ${safeInputCount} items).`,
35
+ input_count: safeInputCount,
36
+ plan_count: safePlanCount,
37
+ };
38
+ }
39
+
40
+ return {
41
+ allowed: false,
42
+ reason: `Scope reduction detected: plan has ${safePlanCount} tasks but input has ${safeInputCount} items. User approval required.`,
43
+ input_count: safeInputCount,
44
+ plan_count: safePlanCount,
45
+ };
46
+ }
47
+
48
+ export function evaluatePhasePrerequisiteGuard(payload) {
49
+ const { run_id: runId, phase, state_root: stateRoot, project_root: projectRoot } = payload;
50
+
51
+ if (!runId) {
52
+ throw new Error('run_id is required');
53
+ }
54
+
55
+ if (!phase) {
56
+ throw new Error('phase is required');
57
+ }
58
+
59
+ if (!stateRoot) {
60
+ throw new Error('state_root is required');
61
+ }
62
+
63
+ if (!projectRoot) {
64
+ throw new Error('project_root is required');
65
+ }
66
+
67
+ const runPaths = getRunPaths(stateRoot, runId);
68
+
69
+ if (!fs.existsSync(runPaths.statusPath)) {
70
+ throw new Error(`status.json not found for run ${runId}`);
71
+ }
72
+
73
+ const manifestPath = path.join(projectRoot, 'wazir.manifest.yaml');
74
+ const manifest = readYamlFile(manifestPath);
75
+ const prerequisites = manifest.phase_prerequisites?.[phase];
76
+
77
+ if (!prerequisites || (Object.keys(prerequisites).length === 0)) {
78
+ return {
79
+ allowed: true,
80
+ reason: `No prerequisites defined for phase ${phase}.`,
81
+ };
82
+ }
83
+
84
+ const requiredArtifacts = prerequisites.required_artifacts ?? [];
85
+ const requiredPhaseExits = prerequisites.required_phase_exits ?? [];
86
+
87
+ const missingArtifacts = [];
88
+ for (const artifact of requiredArtifacts) {
89
+ const artifactPath = path.join(runPaths.runRoot, artifact);
90
+ if (!fs.existsSync(artifactPath)) {
91
+ missingArtifacts.push(artifact);
92
+ }
93
+ }
94
+
95
+ const completedPhases = readPhaseExitEvents(runPaths);
96
+ const missingPhaseExits = [];
97
+ for (const requiredPhase of requiredPhaseExits) {
98
+ if (!completedPhases.includes(requiredPhase)) {
99
+ missingPhaseExits.push(requiredPhase);
100
+ }
101
+ }
102
+
103
+ // OR-logic for resumed runs: if all artifacts exist, pass even without phase_exit events.
104
+ // Artifacts are the hard evidence; phase_exits are supplementary.
105
+ // But if artifacts are missing, phase_exits alone are not sufficient.
106
+ if (missingArtifacts.length === 0) {
107
+ return {
108
+ allowed: true,
109
+ reason: `All prerequisite artifacts present for phase ${phase}.`,
110
+ };
111
+ }
112
+
113
+ const reasons = [];
114
+ if (missingArtifacts.length > 0) {
115
+ reasons.push(`Missing artifacts: ${missingArtifacts.join(', ')}`);
116
+ }
117
+ if (missingPhaseExits.length > 0) {
118
+ reasons.push(`Missing phase exits: ${missingPhaseExits.join(', ')}`);
119
+ }
120
+
121
+ return {
122
+ allowed: false,
123
+ reason: reasons.join('. '),
124
+ missing_artifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
125
+ missing_phase_exits: missingPhaseExits.length > 0 ? missingPhaseExits : undefined,
126
+ };
127
+ }
@@ -0,0 +1,69 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ function firstToken(cmd) {
5
+ return cmd.split(/\s+/)[0] || '';
6
+ }
7
+
8
+ function hasPipe(cmd) {
9
+ return /(?<![\\])\|/.test(cmd);
10
+ }
11
+
12
+ function hasRedirect(cmd) {
13
+ return /(?<![\\])[>]/.test(cmd);
14
+ }
15
+
16
+ export function loadRoutingMatrix(projectRoot) {
17
+ const matrixPath = join(projectRoot, 'hooks', 'routing-matrix.json');
18
+ return JSON.parse(readFileSync(matrixPath, 'utf8'));
19
+ }
20
+
21
+ export function classifyCommand(cmd, matrix) {
22
+ const command = (cmd || '').trim();
23
+
24
+ if (!matrix) {
25
+ return { route: 'small', reason: 'matrix missing — safe fallback' };
26
+ }
27
+
28
+ // 1. Explicit context-mode marker always wins
29
+ if (command.includes('# wazir:context-mode')) {
30
+ return { route: 'large', reason: 'explicit context-mode marker' };
31
+ }
32
+
33
+ // 2. Check large patterns FIRST — large commands are never downgraded
34
+ for (const pattern of matrix.large) {
35
+ if (command === pattern || command.startsWith(pattern + ' ') || command.startsWith(pattern + '\t')) {
36
+ return { route: 'large', reason: `matched large pattern: ${pattern}` };
37
+ }
38
+ }
39
+
40
+ // 3. Passthrough marker — only honoured when command is NOT large
41
+ if (command.includes('# wazir:passthrough')) {
42
+ return { route: 'small', reason: 'explicit passthrough marker' };
43
+ }
44
+
45
+ // 4. Check small patterns
46
+ for (const pattern of matrix.small) {
47
+ if (command === pattern || command.startsWith(pattern + ' ') || command.startsWith(pattern + '\t')) {
48
+ return { route: 'small', reason: `matched small pattern: ${pattern}` };
49
+ }
50
+ }
51
+
52
+ // 5. Ambiguous heuristics
53
+ const heuristic = matrix.ambiguous_heuristic || {};
54
+
55
+ if (heuristic.pipe_detected && hasPipe(command)) {
56
+ return { route: 'ambiguous', reason: 'pipe detected' };
57
+ }
58
+ if (heuristic.redirect_detected && hasRedirect(command)) {
59
+ return { route: 'ambiguous', reason: 'redirect detected' };
60
+ }
61
+
62
+ const bin = firstToken(command);
63
+ if (Array.isArray(heuristic.verbose_binaries) && heuristic.verbose_binaries.includes(bin)) {
64
+ return { route: 'ambiguous', reason: `verbose binary: ${bin}` };
65
+ }
66
+
67
+ // Default: unknown commands pass through
68
+ return { route: 'small', reason: 'no pattern matched — default passthrough' };
69
+ }