@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
@@ -92,6 +92,62 @@ export function writeCaptureOutput(targetPath, content) {
92
92
  fs.writeFileSync(targetPath, content);
93
93
  }
94
94
 
95
+ export function readPhaseExitEvents(runPaths) {
96
+ if (!fs.existsSync(runPaths.eventsPath)) {
97
+ return [];
98
+ }
99
+
100
+ const content = fs.readFileSync(runPaths.eventsPath, 'utf8');
101
+ const completedPhases = [];
102
+
103
+ for (const line of content.split('\n')) {
104
+ const trimmed = line.trim();
105
+ if (!trimmed) continue;
106
+ try {
107
+ const event = JSON.parse(trimmed);
108
+ if (event.event === 'phase_exit' && event.status === 'completed' && event.phase) {
109
+ completedPhases.push(event.phase);
110
+ }
111
+ } catch {
112
+ // Skip malformed lines
113
+ }
114
+ }
115
+
116
+ return completedPhases;
117
+ }
118
+
119
+ /**
120
+ * Read phase exit events with full two-level detail (parent_phase + workflow).
121
+ */
122
+ export function readPhaseExitEventsDetailed(runPaths) {
123
+ if (!fs.existsSync(runPaths.eventsPath)) {
124
+ return [];
125
+ }
126
+
127
+ const content = fs.readFileSync(runPaths.eventsPath, 'utf8');
128
+ const events = [];
129
+
130
+ for (const line of content.split('\n')) {
131
+ const trimmed = line.trim();
132
+ if (!trimmed) continue;
133
+ try {
134
+ const event = JSON.parse(trimmed);
135
+ if (event.event === 'phase_exit' && event.phase) {
136
+ events.push({
137
+ phase: event.phase,
138
+ parent_phase: event.parent_phase ?? event.phase,
139
+ workflow: event.workflow ?? event.phase,
140
+ status: event.status,
141
+ });
142
+ }
143
+ } catch {
144
+ // Skip malformed lines
145
+ }
146
+ }
147
+
148
+ return events;
149
+ }
150
+
95
151
  export function writeSummary(runPaths, content) {
96
152
  ensureRunDirectories(runPaths);
97
153
  fs.writeFileSync(runPaths.summaryPath, content);
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
 
3
4
  export function estimateTokens(bytes) {
4
5
  if (bytes < 0) {
@@ -37,6 +38,19 @@ function createDefaultUsage(runId) {
37
38
  pre_compaction_tokens_est: 0,
38
39
  post_compaction_tokens_est: 0,
39
40
  },
41
+ index_queries: {
42
+ count: 0,
43
+ total_raw_bytes: 0,
44
+ total_summary_bytes: 0,
45
+ estimated_tokens_saved: 0,
46
+ bytes_avoided: 0,
47
+ },
48
+ },
49
+ routing: {
50
+ total_commands: 0,
51
+ context_mode_routed: 0,
52
+ passthrough: 0,
53
+ by_category: {},
40
54
  },
41
55
  totals: {
42
56
  total_events: 0,
@@ -184,6 +198,98 @@ export function recordCompaction(runPaths, preTokens, postTokens) {
184
198
  writeUsageAtomic(runPaths, usage);
185
199
  }
186
200
 
201
+ /**
202
+ * Record a single index query's savings.
203
+ * Called by the pipeline's capture hooks during phase execution
204
+ * (e.g. via `wazir capture index-query`), not by the CLI directly.
205
+ */
206
+ export function recordIndexQuery(runPaths, { query, file_count_in_results, median_file_size, summary_bytes }) {
207
+ const usage = readUsage(runPaths);
208
+ const rawBytes = file_count_in_results * median_file_size;
209
+ const iq = usage.savings.index_queries;
210
+
211
+ iq.count += 1;
212
+ iq.total_raw_bytes += rawBytes;
213
+ iq.total_summary_bytes += summary_bytes;
214
+ iq.bytes_avoided = iq.total_raw_bytes - iq.total_summary_bytes;
215
+ iq.estimated_tokens_saved = estimateTokens(iq.bytes_avoided);
216
+
217
+ writeUsageAtomic(runPaths, usage);
218
+ }
219
+
220
+ export function consumeRoutingLog(runPaths) {
221
+ // Derive stateRoot from runPaths.runRoot (stateRoot/runs/runId -> stateRoot)
222
+ const stateRoot = path.resolve(runPaths.runRoot, '..', '..');
223
+ const logPath = path.join(stateRoot, 'logs', 'routing.ndjson');
224
+
225
+ if (!fs.existsSync(logPath)) {
226
+ return;
227
+ }
228
+
229
+ const raw = fs.readFileSync(logPath, 'utf8').trim();
230
+ if (!raw) {
231
+ return;
232
+ }
233
+
234
+ const usage = readUsage(runPaths);
235
+
236
+ // Determine run start time for scoping log entries to this run
237
+ let runStartTime = null;
238
+ try {
239
+ const configPath = path.join(runPaths.runRoot, 'run-config.yaml');
240
+ if (fs.existsSync(configPath)) {
241
+ const configRaw = fs.readFileSync(configPath, 'utf8');
242
+ const match = configRaw.match(/created_at:\s*["']?([^"'\n]+)/);
243
+ if (match) runStartTime = new Date(match[1].trim()).toISOString();
244
+ }
245
+ } catch { /* fall through — include all entries if no config */ }
246
+
247
+ // Ensure routing section exists (for older usage.json files)
248
+ if (!usage.routing) {
249
+ usage.routing = {
250
+ total_commands: 0,
251
+ context_mode_routed: 0,
252
+ passthrough: 0,
253
+ by_category: {},
254
+ };
255
+ }
256
+
257
+ // Reset counts before re-aggregating (scoped to this run)
258
+ usage.routing.total_commands = 0;
259
+ usage.routing.context_mode_routed = 0;
260
+ usage.routing.passthrough = 0;
261
+ usage.routing.by_category = {};
262
+
263
+ const lines = raw.split('\n');
264
+ for (const line of lines) {
265
+ if (!line.trim()) continue;
266
+
267
+ let entry;
268
+ try {
269
+ entry = JSON.parse(line);
270
+ } catch {
271
+ continue; // skip malformed lines
272
+ }
273
+
274
+ // Scope to current run: skip entries before this run started
275
+ if (runStartTime && entry.ts && entry.ts < runStartTime) continue;
276
+
277
+ usage.routing.total_commands += 1;
278
+
279
+ const route = entry.route || 'passthrough';
280
+ if (route === 'context-mode') {
281
+ usage.routing.context_mode_routed += 1;
282
+ } else {
283
+ usage.routing.passthrough += 1;
284
+ }
285
+
286
+ const category = entry.category || 'unknown';
287
+ usage.routing.by_category[category] = (usage.routing.by_category[category] || 0) + 1;
288
+ }
289
+
290
+ writeUsageAtomic(runPaths, usage);
291
+ }
292
+
187
293
  function formatNumber(n) {
188
294
  return n.toLocaleString('en-US');
189
295
  }
@@ -0,0 +1,66 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * Append a user input entry to the run's NDJSON log.
6
+ *
7
+ * @param {string} runDir - Absolute path to the run directory
8
+ * @param {object} entry - { phase, type, content, context }
9
+ * type: 'instruction' | 'approval' | 'correction' | 'rejection' | 'redirect'
10
+ */
11
+ export function captureUserInput(runDir, { phase, type, content, context }) {
12
+ const logPath = path.join(runDir, 'user-input-log.ndjson');
13
+ const record = {
14
+ timestamp: new Date().toISOString(),
15
+ phase: phase ?? 'unknown',
16
+ type: type ?? 'instruction',
17
+ content: content ?? '',
18
+ context: context ?? '',
19
+ };
20
+ fs.appendFileSync(logPath, JSON.stringify(record) + '\n');
21
+ return logPath;
22
+ }
23
+
24
+ /**
25
+ * Read all entries from a run's user input log.
26
+ */
27
+ export function readUserInputLog(runDir) {
28
+ const logPath = path.join(runDir, 'user-input-log.ndjson');
29
+ if (!fs.existsSync(logPath)) return [];
30
+
31
+ return fs.readFileSync(logPath, 'utf8')
32
+ .split('\n')
33
+ .filter(line => line.trim())
34
+ .map(line => {
35
+ try { return JSON.parse(line); }
36
+ catch { return null; }
37
+ })
38
+ .filter(Boolean);
39
+ }
40
+
41
+ /**
42
+ * Prune old user-input-log.ndjson files, keeping the most recent `keep` runs.
43
+ *
44
+ * @param {string} stateRoot - Absolute path to the state root (e.g. ~/.wazir/projects/foo)
45
+ * @param {number} keep - Number of recent runs to keep (default 10)
46
+ */
47
+ export function pruneOldInputLogs(stateRoot, keep = 10) {
48
+ const runsDir = path.join(stateRoot, 'runs');
49
+ if (!fs.existsSync(runsDir)) return { pruned: 0 };
50
+
51
+ const entries = fs.readdirSync(runsDir)
52
+ .filter(name => name.startsWith('run-') && fs.statSync(path.join(runsDir, name)).isDirectory())
53
+ .sort()
54
+ .reverse();
55
+
56
+ let pruned = 0;
57
+ for (let i = keep; i < entries.length; i++) {
58
+ const logPath = path.join(runsDir, entries[i], 'user-input-log.ndjson');
59
+ if (fs.existsSync(logPath)) {
60
+ fs.unlinkSync(logPath);
61
+ pruned++;
62
+ }
63
+ }
64
+
65
+ return { pruned };
66
+ }
@@ -0,0 +1,256 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const ROOT = path.resolve(import.meta.dirname, '..', '..', '..');
5
+
6
+ function resolve(rel) {
7
+ return path.join(ROOT, rel);
8
+ }
9
+
10
+ function fileExists(rel) {
11
+ return fs.existsSync(resolve(rel));
12
+ }
13
+
14
+ function fileContains(rel, pattern) {
15
+ if (!fileExists(rel)) return false;
16
+ const content = fs.readFileSync(resolve(rel), 'utf8');
17
+ if (typeof pattern === 'string') return content.includes(pattern);
18
+ return pattern.test(content);
19
+ }
20
+
21
+ function sectionContains(rel, sectionHeading, pattern) {
22
+ if (!fileExists(rel)) return false;
23
+ const content = fs.readFileSync(resolve(rel), 'utf8');
24
+ const lines = content.split('\n');
25
+ let inSection = false;
26
+ let sectionContent = '';
27
+ const headingLevel = sectionHeading.match(/^#+/)?.[0]?.length || 2;
28
+ for (const line of lines) {
29
+ // Match exact heading: "## Step 2:" should NOT match "## Step 2.5:" or "## Step 2.6:"
30
+ // Use word boundary check: heading text must be followed by end-of-line, colon, or space
31
+ const trimmed = line.replace(/^#+\s*/, '');
32
+ const probe = sectionHeading.replace(/^#+\s*/, '');
33
+ if (trimmed === probe || trimmed.startsWith(probe + ':') || trimmed.startsWith(probe + ' ')) {
34
+ inSection = true;
35
+ sectionContent = '';
36
+ continue;
37
+ }
38
+ if (inSection) {
39
+ const match = line.match(/^(#{1,6})\s/);
40
+ if (match && match[1].length <= headingLevel) break;
41
+ sectionContent += line + '\n';
42
+ }
43
+ }
44
+ if (typeof pattern === 'string') return sectionContent.includes(pattern);
45
+ return pattern.test(sectionContent);
46
+ }
47
+
48
+ function countMatches(rel, pattern) {
49
+ if (!fileExists(rel)) return 0;
50
+ const content = fs.readFileSync(resolve(rel), 'utf8');
51
+ if (typeof pattern === 'string') {
52
+ return content.split(pattern).length - 1;
53
+ }
54
+ return (content.match(pattern) || []).length;
55
+ }
56
+
57
+ const CHECKS = [
58
+ // Item 8: Spec-kit task template
59
+ { id: 'AC8.1', item: 8, tier: 1, desc: 'tasks-template.md exists', check: () => fileExists('templates/artifacts/tasks-template.md') },
60
+ { id: 'AC8.2', item: 8, tier: 1, desc: 'Contains T001', check: () => fileContains('templates/artifacts/tasks-template.md', 'T001') },
61
+ { id: 'AC8.3', item: 8, tier: 1, desc: 'Contains [P]', check: () => fileContains('templates/artifacts/tasks-template.md', '[P]') },
62
+ { id: 'AC8.4', item: 8, tier: 1, desc: 'Contains Phase 1: Setup', check: () => fileContains('templates/artifacts/tasks-template.md', /Phase\s*1.*Setup|Setup.*Phase\s*1/i) },
63
+ { id: 'AC8.5', item: 8, tier: 1, desc: 'Contains Phase 2: Foundational', check: () => fileContains('templates/artifacts/tasks-template.md', /Phase\s*2.*Foundational|Foundational.*Phase\s*2/i) },
64
+ { id: 'AC8.6', item: 8, tier: 1, desc: 'Contains MVP', check: () => fileContains('templates/artifacts/tasks-template.md', /mvp/i) },
65
+ { id: 'AC8.7', item: 8, tier: 1, desc: 'Contains dependency', check: () => fileContains('templates/artifacts/tasks-template.md', /dependenc|depend/i) },
66
+ { id: 'AC8.8', item: 8, tier: 1, desc: 'implementation-plan.md references tasks-template', check: () => fileContains('templates/artifacts/implementation-plan.md', 'tasks-template') },
67
+ { id: 'AC8.9', item: 8, tier: 1, desc: 'Contains [US', check: () => fileContains('templates/artifacts/tasks-template.md', '[US') },
68
+ { id: 'AC8.10', item: 8, tier: 2, desc: 'Story phase has goal, test criteria, tasks', check: () => fileContains('templates/artifacts/tasks-template.md', /goal/i) && fileContains('templates/artifacts/tasks-template.md', /test.*criteria|criteria.*test/i) },
69
+
70
+ // Item 12: Fix-and-loop
71
+ { id: 'AC12.1', item: 12, tier: 1, desc: 'Describes re-submission after fixes', check: () => fileContains('docs/reference/review-loop-pattern.md', /re-s(end|ubmit)|loop/i) },
72
+ { id: 'AC12.2', item: 12, tier: 1, desc: 'Pass caps per depth', check: () => fileContains('docs/reference/review-loop-pattern.md', /quick.*3|standard.*5|deep.*7/) },
73
+ { id: 'AC12.3', item: 12, tier: 1, desc: 'At cap user chooses', check: () => fileContains('docs/reference/review-loop-pattern.md', /user|approve|escalat/i) },
74
+ { id: 'AC12.4', item: 12, tier: 1, desc: 'No fix-and-continue path', check: () => !fileContains('docs/reference/review-loop-pattern.md', /fix and continue without/i) || fileContains('docs/reference/review-loop-pattern.md', /prohibit|must not|never.*fix and continue/i) },
75
+ { id: 'AC12.5', item: 12, tier: 1, desc: 'Clarifier references review-loop-pattern.md', check: () => fileContains('skills/clarifier/SKILL.md', 'review-loop-pattern') },
76
+ { id: 'AC12.6', item: 12, tier: 1, desc: 'Escalation offers 3 options', check: () => fileContains('docs/reference/review-loop-pattern.md', /approve.*issues|fix.*manually|abort/i) },
77
+
78
+ // Item 13: Reviewer skill invocation
79
+ { id: 'AC13.1', item: 13, tier: 1, desc: 'Clarifier references wz:reviewer', check: () => fileContains('skills/clarifier/SKILL.md', /wz:reviewer|reviewer skill/i) },
80
+ { id: 'AC13.2', item: 13, tier: 2, desc: 'Clarifier no bare codex exec/review', check: () => { /* Manual: check codex exec/review only in code blocks */ return null; } },
81
+ { id: 'AC13.3', item: 13, tier: 1, desc: 'writing-plans references wz:reviewer --mode plan-review', check: () => fileContains('skills/writing-plans/SKILL.md', /wz:reviewer.*plan-review|plan-review.*wz:reviewer/) },
82
+ { id: 'AC13.4', item: 13, tier: 2, desc: 'writing-plans no bare codex exec/review', check: () => { return null; } },
83
+ { id: 'AC13.5', item: 13, tier: 1, desc: 'Clarifier uses all 3 modes', check: () => fileContains('skills/clarifier/SKILL.md', 'clarification-review') && fileContains('skills/clarifier/SKILL.md', 'spec-challenge') && fileContains('skills/clarifier/SKILL.md', 'plan-review') },
84
+ { id: 'AC13.6', item: 13, tier: 1, desc: 'Reviewer documents 4 responsibilities', check: () => fileContains('skills/reviewer/SKILL.md', /Codex.*integration|integration.*Codex/i) && fileContains('skills/reviewer/SKILL.md', /dimension/i) },
85
+
86
+ // Item 1: Input preservation
87
+ { id: 'AC1.1', item: 1, tier: 2, desc: 'wc -l plan >= wc -l input', check: () => null },
88
+ { id: 'AC1.2', item: 1, tier: 2, desc: 'Criteria in corresponding task description', check: () => null },
89
+ { id: 'AC1.3', item: 1, tier: 2, desc: 'Endpoints/hex/dimensions in relevant section', check: () => null },
90
+ { id: 'AC1.4', item: 1, tier: 2, desc: 'Works with empty input/tasks/', check: () => null },
91
+
92
+ // Item 2: Spec-kit plan format
93
+ { id: 'AC2.1', item: 2, tier: 1, desc: 'Clarifier produces execution-plan.md', check: () => fileContains('skills/clarifier/SKILL.md', 'execution-plan.md') },
94
+ { id: 'AC2.2', item: 2, tier: 1, desc: 'Plan has T0XX checkboxes', check: () => fileContains('skills/clarifier/SKILL.md', /T\d{3}|T0XX/) },
95
+ { id: 'AC2.3', item: 2, tier: 1, desc: 'Plan has [P] markers', check: () => fileContains('skills/clarifier/SKILL.md', '[P]') || fileContains('skills/writing-plans/SKILL.md', '[P]') },
96
+ { id: 'AC2.4', item: 2, tier: 1, desc: 'Phase headings Setup/Foundational', check: () => fileContains('skills/clarifier/SKILL.md', /Setup|Foundational/) || fileContains('skills/writing-plans/SKILL.md', /Setup|Foundational/) },
97
+ { id: 'AC2.5', item: 2, tier: 2, desc: 'Every task has file path', check: () => null },
98
+ { id: 'AC2.6', item: 2, tier: 1, desc: 'No tasks/task-NNN/spec.md created', check: () => !fileExists('.wazir/runs/latest/tasks/task-001/spec.md') },
99
+ { id: 'AC2.7', item: 2, tier: 1, desc: 'Contains [US] labels', check: () => fileContains('skills/clarifier/SKILL.md', '[US') || fileContains('skills/writing-plans/SKILL.md', '[US') },
100
+ { id: 'AC2.8', item: 2, tier: 2, desc: 'Story phases have goal, criteria, tasks', check: () => null },
101
+
102
+ // Item 3: Gap analysis
103
+ { id: 'AC3.1', item: 3, tier: 2, desc: 'plan-review-pass-N.md files exist', check: () => null },
104
+ { id: 'AC3.2', item: 3, tier: 1, desc: 'Gap analysis reads input/ AND user-feedback.md', check: () => fileContains('skills/clarifier/SKILL.md', 'input/') && fileContains('skills/clarifier/SKILL.md', 'user-feedback.md') },
105
+ { id: 'AC3.3', item: 3, tier: 1, desc: 'Uses wz:reviewer --mode plan-review', check: () => fileContains('skills/clarifier/SKILL.md', /wz:reviewer.*plan-review/) },
106
+ { id: 'AC3.4', item: 3, tier: 2, desc: 'Terminal state CLEAN or user-approved-with-issues', check: () => null },
107
+ { id: 'AC3.5', item: 3, tier: 2, desc: 'Clarifier doesnt produce review files', check: () => null },
108
+
109
+ // Item 6: Context-mode
110
+ { id: 'AC6.1', item: 6, tier: 1, desc: 'init-pipeline references context_mode', check: () => fileContains('skills/init-pipeline/SKILL.md', /context.mode|context-mode/) },
111
+ { id: 'AC6.2', item: 6, tier: 1, desc: 'Config stores as object', check: () => fileContains('skills/init-pipeline/SKILL.md', /enabled.*true|\{.*enabled/) },
112
+ { id: 'AC6.3', item: 6, tier: 1, desc: 'Clarifier references fetch_and_index', check: () => fileContains('skills/clarifier/SKILL.md', 'fetch_and_index') },
113
+ { id: 'AC6.4', item: 6, tier: 1, desc: 'Clarifier has WebFetch fallback', check: () => fileContains('skills/clarifier/SKILL.md', 'WebFetch') },
114
+ { id: 'AC6.5', item: 6, tier: 1, desc: 'Detection checks tools under correct prefix', check: () => fileContains('skills/init-pipeline/SKILL.md', 'mcp__plugin_context-mode_context-mode__') || fileContains('skills/init-pipeline/SKILL.md', /execute.*fetch_and_index.*search/) },
115
+ { id: 'AC6.6', item: 6, tier: 1, desc: 'command.js references context_mode', check: () => fileContains('tooling/src/init/command.js', 'context_mode') || fileContains('tooling/src/init/command.js', 'context-mode') },
116
+ { id: 'AC6.7', item: 6, tier: 1, desc: 'Clarifier references execute/execute_file for large outputs', check: () => fileContains('skills/clarifier/SKILL.md', /execute_file|execute.*large/i) },
117
+ { id: 'AC6.8', item: 6, tier: 1, desc: 'Bash fallback documented', check: () => fileContains('skills/clarifier/SKILL.md', 'Bash') },
118
+ { id: 'AC6.9', item: 6, tier: 1, desc: 'Wazir records context-mode in run-config', check: () => fileContains('skills/wazir/SKILL.md', /context.mode|context-mode/) },
119
+
120
+ // Item 9: Online research
121
+ { id: 'AC9.1', item: 9, tier: 1, desc: 'Phase 0 keyword extraction', check: () => fileContains('skills/clarifier/SKILL.md', /extract.*keyword|keyword.*extract|extract.*concept|concept.*extract/i) },
122
+ { id: 'AC9.2', item: 9, tier: 1, desc: 'Decision criteria documented', check: () => fileContains('skills/clarifier/SKILL.md', /when to research|decision.*criteria/i) },
123
+ { id: 'AC9.3', item: 9, tier: 1, desc: 'Both fetch_and_index and WebFetch', check: () => fileContains('skills/clarifier/SKILL.md', 'fetch_and_index') && fileContains('skills/clarifier/SKILL.md', 'WebFetch') },
124
+ { id: 'AC9.4', item: 9, tier: 1, desc: 'Error handling for 404, rate limit, no URL', check: () => fileContains('skills/clarifier/SKILL.md', /404|failed.*fetch|error.*handling/i) },
125
+ { id: 'AC9.5', item: 9, tier: 1, desc: 'Content saved to sources/', check: () => fileContains('skills/clarifier/SKILL.md', 'sources/') },
126
+ { id: 'AC9.6', item: 9, tier: 1, desc: 'Manifest tracks status', check: () => fileContains('skills/clarifier/SKILL.md', /manifest|status.*track/i) },
127
+
128
+ // Item 17: Codex output protection
129
+ { id: 'AC17.1', item: 17, tier: 1, desc: 'Pattern doc describes tee + execute_file', check: () => fileContains('docs/reference/review-loop-pattern.md', 'tee') && fileContains('docs/reference/review-loop-pattern.md', 'execute_file') },
130
+ { id: 'AC17.2', item: 17, tier: 1, desc: 'Reviewer shows execute_file after Codex', check: () => fileContains('skills/reviewer/SKILL.md', 'execute_file') },
131
+ { id: 'AC17.3', item: 17, tier: 1, desc: 'Fallback uses tac-based extraction', check: () => fileContains('docs/reference/review-loop-pattern.md', 'tac') || fileContains('skills/reviewer/SKILL.md', 'tac') },
132
+ { id: 'AC17.4', item: 17, tier: 2, desc: 'Raw trace preserved in file only', check: () => null },
133
+ { id: 'AC17.5', item: 17, tier: 1, desc: 'Clarifier doesnt call codex directly', check: () => true /* covered by AC13.2 */ },
134
+
135
+ // Item 4: Resume
136
+ { id: 'AC4.1', item: 4, tier: 1, desc: 'Resume copies clarified/ except user-feedback', check: () => fileContains('skills/wazir/SKILL.md', /clarified.*user-feedback|user-feedback.*exclude/i) },
137
+ { id: 'AC4.2', item: 4, tier: 2, desc: 'Start-fresh creates empty clarified/', check: () => null },
138
+ { id: 'AC4.3', item: 4, tier: 1, desc: 'tasks/ NOT copied', check: () => fileContains('skills/wazir/SKILL.md', /tasks.*not.*cop|do not.*copy.*tasks/i) || !fileContains('skills/wazir/SKILL.md', /copy.*tasks\//) },
139
+ { id: 'AC4.4', item: 4, tier: 1, desc: 'Staleness checks ALL input/ files', check: () => fileContains('skills/wazir/SKILL.md', /stale|input.*newer|mtime/i) },
140
+ { id: 'AC4.5', item: 4, tier: 1, desc: 'User explicitly chooses Resume', check: () => fileContains('skills/wazir/SKILL.md', /Resume.*Start fresh|choose.*resume/i) },
141
+ { id: 'AC4.6', item: 4, tier: 2, desc: 'Staleness warning with file names + interactive', check: () => null },
142
+ { id: 'AC4.7', item: 4, tier: 1, desc: 'Resume resumes from last completed phase', check: () => fileContains('skills/wazir/SKILL.md', /last.*completed.*phase|resume.*phase/i) },
143
+
144
+ // Item 5: CHANGELOG + gitflow
145
+ { id: 'AC5.1', item: 5, tier: 1, desc: 'wazir has validate changelog --require-entries', check: () => fileContains('skills/wazir/SKILL.md', 'validate changelog') && fileContains('skills/wazir/SKILL.md', '--require-entries') },
146
+ { id: 'AC5.2', item: 5, tier: 1, desc: 'wazir has validate commits', check: () => fileContains('skills/wazir/SKILL.md', 'validate commits') },
147
+ { id: 'AC5.3', item: 5, tier: 1, desc: 'Executor references CHANGELOG + [Unreleased]', check: () => fileContains('skills/executor/SKILL.md', 'CHANGELOG') && fileContains('skills/executor/SKILL.md', 'Unreleased') },
148
+ { id: 'AC5.4', item: 5, tier: 1, desc: 'Reviewer flags CHANGELOG as [warning]', check: () => fileContains('skills/reviewer/SKILL.md', 'CHANGELOG') && fileContains('skills/reviewer/SKILL.md', /warning/i) },
149
+ { id: 'AC5.5', item: 5, tier: 1, desc: 'Hard gate stops pipeline', check: () => fileContains('skills/wazir/SKILL.md', /hard gate|must fix before/i) },
150
+ { id: 'AC5.6', item: 5, tier: 1, desc: 'All 6 keepachangelog types', check: () => ['Added', 'Changed', 'Fixed', 'Removed', 'Deprecated', 'Security'].every(t => fileContains('skills/executor/SKILL.md', t)) },
151
+ { id: 'AC5.7', item: 5, tier: 1, desc: 'Reviewer binds to task-review AND final', check: () => fileContains('skills/reviewer/SKILL.md', 'task-review') && fileContains('skills/reviewer/SKILL.md', 'final') },
152
+
153
+ // Item 7: Usage reports
154
+ { id: 'AC7.1', item: 7, tier: 1, desc: 'wazir contains capture usage', check: () => fileContains('skills/wazir/SKILL.md', 'capture usage') },
155
+ { id: 'AC7.2', item: 7, tier: 2, desc: 'Present at EVERY phase_exit block', check: () => null },
156
+ { id: 'AC7.3', item: 7, tier: 1, desc: 'Output path includes run-id and phase', check: () => fileContains('skills/wazir/SKILL.md', /usage.*phase|phase.*usage/i) },
157
+
158
+ // Item 10: Interactive
159
+ { id: 'AC10.1', item: 10, tier: 2, desc: 'Clarifier Checkpoint 0 has pattern', check: () => sectionContains('skills/clarifier/SKILL.md', 'Checkpoint 0', '(Recommended)') },
160
+ { id: 'AC10.2', item: 10, tier: 2, desc: 'Clarifier Checkpoint 1A has pattern', check: () => sectionContains('skills/clarifier/SKILL.md', 'Checkpoint 1A', '(Recommended)') },
161
+ { id: 'AC10.3', item: 10, tier: 2, desc: 'Clarifier Checkpoint 1A+ has pattern', check: () => sectionContains('skills/clarifier/SKILL.md', 'Checkpoint 1A+', '(Recommended)') },
162
+ { id: 'AC10.4', item: 10, tier: 2, desc: 'Clarifier Checkpoint 1B has pattern', check: () => sectionContains('skills/clarifier/SKILL.md', 'Checkpoint 1B', '(Recommended)') },
163
+ { id: 'AC10.5', item: 10, tier: 2, desc: 'Clarifier Checkpoint 1C has pattern', check: () => sectionContains('skills/clarifier/SKILL.md', 'Checkpoint 1C', '(Recommended)') },
164
+ { id: 'AC10.6a', item: 10, tier: 2, desc: 'wazir Step 2 has pattern', check: () => sectionContains('skills/wazir/SKILL.md', 'Step 2', '(Recommended)') },
165
+ { id: 'AC10.6b', item: 10, tier: 2, desc: 'wazir Step 3 has 3 option blocks', check: () => null },
166
+ { id: 'AC10.6c', item: 10, tier: 2, desc: 'wazir Step 4 has pattern', check: () => sectionContains('skills/wazir/SKILL.md', 'Step 4', '(Recommended)') },
167
+ { id: 'AC10.6d', item: 10, tier: 2, desc: 'wazir Step 5 has pattern', check: () => sectionContains('skills/wazir/SKILL.md', 'Step 5', '(Recommended)') },
168
+ { id: 'AC10.7', item: 10, tier: 2, desc: 'ALL executor prompts use numbered options', check: () => null },
169
+ { id: 'AC10.8', item: 10, tier: 2, desc: 'ALL reviewer prompts use numbered options', check: () => null },
170
+ { id: 'AC10.9', item: 10, tier: 1, desc: 'No AskUserQuestion in 4 skills', check: () => !fileContains('skills/clarifier/SKILL.md', 'AskUserQuestion') && !fileContains('skills/wazir/SKILL.md', 'AskUserQuestion') && !fileContains('skills/executor/SKILL.md', 'AskUserQuestion') && !fileContains('skills/reviewer/SKILL.md', 'AskUserQuestion') },
171
+ { id: 'AC10.10', item: 10, tier: 2, desc: 'No open-ended questions', check: () => null },
172
+
173
+ // Item 11: User feedback
174
+ { id: 'AC11.1', item: 11, tier: 1, desc: 'Clarifier references user-feedback.md at runs/ path', check: () => fileContains('skills/clarifier/SKILL.md', 'user-feedback.md') && fileContains('skills/clarifier/SKILL.md', /runs\//) },
175
+ { id: 'AC11.2', item: 11, tier: 2, desc: 'Checkpoint routes corrections', check: () => null },
176
+ { id: 'AC11.3', item: 11, tier: 2, desc: 'File starts empty on new runs', check: () => null },
177
+ { id: 'AC11.4', item: 11, tier: 1, desc: 'Feedback is timestamped', check: () => fileContains('skills/clarifier/SKILL.md', /timestamp/i) },
178
+
179
+ // Item 14: Briefing updates
180
+ { id: 'AC14.1', item: 14, tier: 1, desc: 'Clarifier references User Additions', check: () => fileContains('skills/clarifier/SKILL.md', 'User Additions') },
181
+ { id: 'AC14.2', item: 14, tier: 2, desc: 'Checkpoint distinguishes scope vs correction', check: () => null },
182
+ { id: 'AC14.3', item: 14, tier: 1, desc: 'Gap analysis reads input/ and user-feedback.md', check: () => true /* covered by AC3.2 */ },
183
+ { id: 'AC14.4', item: 14, tier: 2, desc: 'Routing question uses numbered options', check: () => null },
184
+
185
+ // Item 15: Phase scoring
186
+ { id: 'AC15.1', item: 15, tier: 1, desc: 'Pattern doc defines canonical dimension sets', check: () => fileContains('docs/reference/review-loop-pattern.md', /canonical.*dimension|dimension.*set.*per.*phase/i) },
187
+ { id: 'AC15.2', item: 15, tier: 1, desc: 'Same dimensions + delta', check: () => fileContains('docs/reference/review-loop-pattern.md', /same.*dimension|delta/i) },
188
+ { id: 'AC15.3', item: 15, tier: 1, desc: 'Report includes Quality Delta', check: () => fileContains('docs/reference/review-loop-pattern.md', 'Quality Delta') || fileContains('skills/wazir/SKILL.md', 'Quality Delta') },
189
+ { id: 'AC15.4', item: 15, tier: 1, desc: 'Delta format with arrow', check: () => fileContains('docs/reference/review-loop-pattern.md', /\d+\/10.*→|→.*\d+\/10/) || fileContains('docs/reference/review-loop-pattern.md', /\+\d+\)/) },
190
+ { id: 'AC15.5', item: 15, tier: 1, desc: 'Reviewer pass files record dimension set', check: () => fileContains('skills/reviewer/SKILL.md', /dimension.*set|record.*dimension/i) },
191
+
192
+ // Item 16: Full report
193
+ { id: 'AC16.1', item: 16, tier: 1, desc: 'Report path reviews/<phase>-report.md', check: () => fileContains('skills/wazir/SKILL.md', /report\.md|phase.*report/) },
194
+ { id: 'AC16.2', item: 16, tier: 1, desc: 'Contains ## Summary', check: () => fileContains('skills/wazir/SKILL.md', 'Summary') || fileContains('docs/reference/review-loop-pattern.md', 'Summary') },
195
+ { id: 'AC16.3', item: 16, tier: 1, desc: 'Contains Key Changes', check: () => fileContains('skills/wazir/SKILL.md', 'Key Changes') || fileContains('docs/reference/review-loop-pattern.md', 'Key Changes') },
196
+ { id: 'AC16.4', item: 16, tier: 1, desc: 'Contains Quality Delta', check: () => fileContains('skills/wazir/SKILL.md', 'Quality Delta') || fileContains('docs/reference/review-loop-pattern.md', 'Quality Delta') },
197
+ { id: 'AC16.5', item: 16, tier: 2, desc: 'Findings Log with per-pass severity', check: () => null },
198
+ { id: 'AC16.6', item: 16, tier: 1, desc: 'Contains Usage', check: () => fileContains('skills/wazir/SKILL.md', /Usage|capture usage/) },
199
+ { id: 'AC16.7', item: 16, tier: 1, desc: 'Context Savings conditional', check: () => fileContains('skills/wazir/SKILL.md', /context.*sav|context.mode/i) || fileContains('docs/reference/review-loop-pattern.md', /context.*sav/i) },
200
+ { id: 'AC16.8', item: 16, tier: 1, desc: 'Time Spent section', check: () => fileContains('skills/wazir/SKILL.md', /time.*spent|time.*phase/i) || fileContains('docs/reference/review-loop-pattern.md', /time.*spent/i) },
201
+ { id: 'AC16.9', item: 16, tier: 2, desc: 'Bound to EVERY phase_exit', check: () => null },
202
+ ];
203
+
204
+ export function runAcMatrix() {
205
+ let pass = 0;
206
+ let fail = 0;
207
+ let manual = 0;
208
+ const results = [];
209
+
210
+ for (const ac of CHECKS) {
211
+ let result;
212
+ try {
213
+ result = ac.check();
214
+ } catch {
215
+ result = false;
216
+ }
217
+
218
+ if (result === null) {
219
+ manual++;
220
+ results.push({ ...ac, status: 'MANUAL' });
221
+ } else if (result) {
222
+ pass++;
223
+ results.push({ ...ac, status: 'PASS' });
224
+ } else {
225
+ fail++;
226
+ results.push({ ...ac, status: 'FAIL' });
227
+ }
228
+ }
229
+
230
+ return { pass, fail, manual, total: CHECKS.length, results };
231
+ }
232
+
233
+ // CLI entry point
234
+ if (import.meta.url === `file://${process.argv[1]}`) {
235
+ const { pass, fail, manual, total, results } = runAcMatrix();
236
+
237
+ console.log(`\nAC Matrix: ${pass} PASS / ${fail} FAIL / ${manual} MANUAL / ${total} total\n`);
238
+
239
+ const failing = results.filter(r => r.status === 'FAIL');
240
+ if (failing.length > 0) {
241
+ console.log('FAILING:');
242
+ for (const f of failing) {
243
+ console.log(` ${f.id} (Item ${f.item}): ${f.desc}`);
244
+ }
245
+ }
246
+
247
+ const manualChecks = results.filter(r => r.status === 'MANUAL');
248
+ if (manualChecks.length > 0) {
249
+ console.log('\nMANUAL VERIFICATION NEEDED:');
250
+ for (const m of manualChecks) {
251
+ console.log(` ${m.id} (Item ${m.item}): ${m.desc}`);
252
+ }
253
+ }
254
+
255
+ process.exit(fail > 0 ? 1 : 0);
256
+ }
@@ -10,6 +10,7 @@ export const SUPPORTED_COMMAND_SUBJECTS = new Set([
10
10
  'wazir validate commits',
11
11
  'wazir validate changelog',
12
12
  'wazir validate docs-drift',
13
+ 'wazir validate skills',
13
14
  'wazir export',
14
15
  'wazir export build',
15
16
  'wazir export --check',
@@ -26,6 +27,7 @@ export const SUPPORTED_COMMAND_SUBJECTS = new Set([
26
27
  'wazir recall symbol',
27
28
  'wazir doctor',
28
29
  'wazir status',
30
+ 'wazir stats',
29
31
  'wazir capture',
30
32
  'wazir capture init',
31
33
  'wazir capture event',
@@ -34,4 +36,14 @@ export const SUPPORTED_COMMAND_SUBJECTS = new Set([
34
36
  'wazir capture summary',
35
37
  'wazir capture usage',
36
38
  'wazir capture loop-check',
39
+ 'wazir init',
40
+ 'wazir config',
41
+ 'wazir config set',
42
+ 'wazir report',
43
+ 'wazir report phase',
44
+ 'wazir state',
45
+ 'wazir state stats',
46
+ 'wazir state learnings',
47
+ 'wazir state findings',
48
+ 'wazir state trend',
37
49
  ]);
@@ -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(['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 })) {
@@ -0,0 +1,69 @@
1
+ const SECURITY_PATTERNS = [
2
+ 'auth', 'password', 'passwd', 'token', 'query', 'sql',
3
+ 'fetch', 'upload', 'secret', 'env', 'api[._-]?key',
4
+ 'session', 'cookie', 'cors', 'csrf', 'jwt', 'oauth',
5
+ 'encrypt', 'decrypt', 'hash', 'salt', 'credential',
6
+ 'private[._-]?key', 'access[._-]?token', 'refresh[._-]?token',
7
+ ];
8
+
9
+ const PATTERN_REGEX = new RegExp(
10
+ `\\b(${SECURITY_PATTERNS.join('|')})\\b`,
11
+ 'gi'
12
+ );
13
+
14
+ /**
15
+ * Scan diff text for security-sensitive patterns.
16
+ * Returns which patterns were found and in which files.
17
+ */
18
+ export function detectSecurityPatterns(diffText) {
19
+ if (!diffText || typeof diffText !== 'string') {
20
+ return { triggered: false, patterns: [], files: [] };
21
+ }
22
+
23
+ const matchedPatterns = new Set();
24
+ const matchedFiles = new Set();
25
+ let currentFile = null;
26
+
27
+ for (const line of diffText.split('\n')) {
28
+ // Track current file from diff headers
29
+ const fileMatch = line.match(/^(?:diff --git a\/\S+ b\/|[+]{3} b\/)(.+)/);
30
+ if (fileMatch) {
31
+ currentFile = fileMatch[1];
32
+ continue;
33
+ }
34
+
35
+ // Only scan added/modified lines (starting with +, not +++)
36
+ if (!line.startsWith('+') || line.startsWith('+++')) continue;
37
+
38
+ const lineMatches = line.match(PATTERN_REGEX);
39
+ if (lineMatches) {
40
+ for (const m of lineMatches) {
41
+ matchedPatterns.add(m.toLowerCase());
42
+ }
43
+ if (currentFile) {
44
+ matchedFiles.add(currentFile);
45
+ }
46
+ }
47
+ }
48
+
49
+ const patterns = [...matchedPatterns].sort();
50
+ const files = [...matchedFiles].sort();
51
+
52
+ return {
53
+ triggered: patterns.length > 0,
54
+ patterns,
55
+ files,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Security review dimensions to add when patterns are detected.
61
+ */
62
+ export const SECURITY_REVIEW_DIMENSIONS = [
63
+ 'Injection (SQL, command, template, header)',
64
+ 'Authentication bypass',
65
+ 'Data exposure (PII, secrets, tokens in logs/responses)',
66
+ 'CSRF / SSRF',
67
+ 'XSS (stored, reflected, DOM)',
68
+ 'Secrets leakage (hardcoded keys, env vars in client code)',
69
+ ];