wogiflow 2.17.0 → 2.18.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 (111) hide show
  1. package/.claude/commands/wogi-audit.md +212 -17
  2. package/.claude/commands/wogi-research.md +37 -0
  3. package/.claude/commands/wogi-review.md +200 -22
  4. package/.claude/commands/wogi-start.md +45 -0
  5. package/.claude/docs/claude-code-compatibility.md +46 -1
  6. package/.claude/docs/intent-grounded-review.md +209 -0
  7. package/.claude/settings.json +34 -1
  8. package/.workflow/agents/logic-adversary.md +8 -0
  9. package/.workflow/templates/claude-md.hbs +18 -0
  10. package/lib/installer.js +22 -0
  11. package/lib/utils.js +29 -3
  12. package/lib/workspace-changelog.js +2 -1
  13. package/lib/workspace-channel-server.js +4 -6
  14. package/lib/workspace-contracts.js +5 -4
  15. package/lib/workspace-events.js +8 -7
  16. package/lib/workspace-gates.js +4 -3
  17. package/lib/workspace-integration-tests.js +2 -1
  18. package/lib/workspace-intelligence.js +3 -2
  19. package/lib/workspace-locks.js +2 -1
  20. package/lib/workspace-messages.js +7 -6
  21. package/lib/workspace-routing.js +14 -26
  22. package/lib/workspace-session.js +7 -6
  23. package/lib/workspace-sync.js +9 -8
  24. package/package.json +4 -2
  25. package/scripts/base-workflow-step.js +1 -1
  26. package/scripts/flow +19 -0
  27. package/scripts/flow-adaptive-learning.js +1 -1
  28. package/scripts/flow-aggregate.js +2 -1
  29. package/scripts/flow-architect-pass.js +3 -3
  30. package/scripts/flow-archive-runs.js +372 -0
  31. package/scripts/flow-ask.js +1 -1
  32. package/scripts/flow-ast-grep.js +216 -0
  33. package/scripts/flow-audit-gates.js +1 -1
  34. package/scripts/flow-auto-learn.js +8 -11
  35. package/scripts/flow-bug.js +2 -2
  36. package/scripts/flow-capture-gate.js +644 -0
  37. package/scripts/flow-capture.js +4 -3
  38. package/scripts/flow-cli-flags.js +95 -0
  39. package/scripts/flow-community-sync.js +2 -1
  40. package/scripts/flow-community.js +6 -6
  41. package/scripts/flow-conclusion-classifier.js +310 -0
  42. package/scripts/flow-config-defaults.js +3 -3
  43. package/scripts/flow-constants.js +8 -11
  44. package/scripts/flow-context-scoring.js +1 -0
  45. package/scripts/flow-correction-detector.js +344 -3
  46. package/scripts/flow-damage-control.js +1 -1
  47. package/scripts/flow-decisions-merge.js +1 -0
  48. package/scripts/flow-done-gates.js +20 -0
  49. package/scripts/flow-done-report.js +2 -2
  50. package/scripts/flow-done.js +4 -4
  51. package/scripts/flow-epics.js +5 -11
  52. package/scripts/flow-health.js +145 -1
  53. package/scripts/flow-id.js +92 -0
  54. package/scripts/flow-io.js +15 -5
  55. package/scripts/flow-knowledge-router.js +2 -1
  56. package/scripts/flow-links.js +1 -1
  57. package/scripts/flow-log-manager.js +2 -1
  58. package/scripts/flow-logic-adversary.js +4 -4
  59. package/scripts/flow-long-input-cli.js +6 -0
  60. package/scripts/flow-long-input-stories.js +1 -1
  61. package/scripts/flow-loop-retry-learning.js +1 -1
  62. package/scripts/flow-mcp-capabilities.js +2 -3
  63. package/scripts/flow-mcp-docs.js +2 -1
  64. package/scripts/flow-memory-blocks.js +2 -1
  65. package/scripts/flow-memory-sync.js +1 -1
  66. package/scripts/flow-memory.js +767 -0
  67. package/scripts/flow-migrate-igr.js +1 -1
  68. package/scripts/flow-migrate.js +2 -1
  69. package/scripts/flow-model-adapter.js +1 -1
  70. package/scripts/flow-model-config.js +5 -1
  71. package/scripts/flow-model-profile.js +2 -1
  72. package/scripts/flow-orchestrate.js +3 -3
  73. package/scripts/flow-output.js +29 -0
  74. package/scripts/flow-parallel.js +10 -9
  75. package/scripts/flow-pattern-enforcer.js +2 -1
  76. package/scripts/flow-permissions-audit.js +124 -0
  77. package/scripts/flow-plugin-registry.js +2 -2
  78. package/scripts/flow-progress.js +5 -1
  79. package/scripts/flow-project-analyzer.js +1 -1
  80. package/scripts/flow-promote.js +510 -0
  81. package/scripts/flow-registries.js +86 -0
  82. package/scripts/flow-request-log.js +133 -0
  83. package/scripts/flow-research-protocol.js +0 -1
  84. package/scripts/flow-revision-tracker.js +2 -1
  85. package/scripts/flow-roadmap.js +2 -1
  86. package/scripts/flow-rules-sync.js +3 -7
  87. package/scripts/flow-session-end.js +3 -1
  88. package/scripts/flow-session-learning.js +6 -13
  89. package/scripts/flow-session-state.js +2 -2
  90. package/scripts/flow-setup-hooks.js +2 -1
  91. package/scripts/flow-skill-create.js +1 -1
  92. package/scripts/flow-skill-freshness.js +6 -7
  93. package/scripts/flow-skill-learn.js +1 -1
  94. package/scripts/flow-step-coverage.js +1 -1
  95. package/scripts/flow-step-security.js +1 -1
  96. package/scripts/flow-story.js +58 -10
  97. package/scripts/flow-sys.js +204 -0
  98. package/scripts/flow-task-hierarchy.js +88 -0
  99. package/scripts/flow-tech-debt.js +2 -1
  100. package/scripts/flow-test-api.js +1 -1
  101. package/scripts/flow-utils.js +60 -890
  102. package/scripts/hooks/core/bugfix-scope-gate.js +5 -4
  103. package/scripts/hooks/core/deploy-gate.js +1 -1
  104. package/scripts/hooks/core/pre-tool-helpers.js +72 -0
  105. package/scripts/hooks/core/pre-tool-orchestrator.js +442 -0
  106. package/scripts/hooks/core/routing-gate.js +8 -0
  107. package/scripts/hooks/core/session-end.js +28 -0
  108. package/scripts/hooks/entry/claude-code/pre-tool-use.js +48 -492
  109. package/scripts/hooks/entry/shared/hook-runner.js +1 -1
  110. package/scripts/registries/schema-registry.js +1 -1
  111. package/scripts/registries/service-registry.js +1 -1
@@ -21,7 +21,7 @@
21
21
 
22
22
  const fs = require('node:fs');
23
23
  const path = require('node:path');
24
- const { getConfig, PATHS, safeJsonParse, writeJson } = require('../../flow-utils');
24
+ const { getConfig, PATHS, safeJsonParse, writeJson, getReadyData } = require('../../flow-utils');
25
25
 
26
26
  // ============================================================
27
27
  // Constants
@@ -331,9 +331,10 @@ function checkBugfixScope(toolName, config) {
331
331
  return { allowed: true, blocked: false };
332
332
  }
333
333
 
334
- // Get active task from ready.json
335
- const readyPath = path.join(PATHS.state, 'ready.json');
336
- const ready = safeJsonParse(readyPath, { inProgress: [] });
334
+ // Get active task from ready.json — use getReadyData for 200ms TTL cache
335
+ // instead of direct safeJsonParse (perf-006 fix, wf-7c36aaed). On L3 bugfix
336
+ // tasks during Edit/Write this avoids a 2nd disk read per hook invocation.
337
+ const ready = getReadyData();
337
338
  if (!ready.inProgress || ready.inProgress.length === 0) {
338
339
  return { allowed: true, blocked: false };
339
340
  }
@@ -170,7 +170,7 @@ function computeSourceHash(changedFiles) {
170
170
  // Fallback: use git HEAD (less precise but functional)
171
171
  try {
172
172
  return execSync('git rev-parse HEAD', { encoding: 'utf-8', cwd: PATHS.root }).trim();
173
- } catch (_err2) {
173
+ } catch (_err) {
174
174
  return 'unknown';
175
175
  }
176
176
  }
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - PreToolUse Helpers (core module)
5
+ *
6
+ * Shared helper functions extracted from scripts/hooks/entry/claude-code/pre-tool-use.js
7
+ * to improve three-layer compliance (entry = parse + dispatch, core = logic).
8
+ *
9
+ * Story: wf-93b48ca1 (epic wf-94cc3b72, arch-001 fix — partial).
10
+ *
11
+ * Scope: safe, pure helpers only. The 480-line gate-orchestration body
12
+ * remains in the entry file until hook coverage (wf-e9e31c7c) lands — refactoring
13
+ * it blind is high-risk given 0 unit tests on the orchestration path.
14
+ */
15
+
16
+ const VALID_AGENT_TYPES = new Set([
17
+ 'general-purpose', 'Explore', 'Plan', 'code-reviewer', 'bug-analyzer',
18
+ 'statusline-setup', 'claude-code-guide', 'ui-sketcher',
19
+ ]);
20
+
21
+ const READ_ONLY_AGENT_TYPES = new Set(['Explore', 'Plan', 'code-reviewer', 'bug-analyzer']);
22
+
23
+ /**
24
+ * Parse + validate the subagent context from hook input.
25
+ *
26
+ * @param {Object} input - Parsed hook input
27
+ * @returns {{ agentId: string|null, agentType: string|null, isSubagent: boolean, subagentReadOnly: boolean }}
28
+ */
29
+ function parseSubagentContext(input) {
30
+ const rawAgentId = input && input.agent_id ? input.agent_id : null;
31
+ const rawAgentType = input && input.agent_type ? input.agent_type : null;
32
+ const agentId = typeof rawAgentId === 'string' && /^[a-zA-Z0-9_-]{1,128}$/.test(rawAgentId) ? rawAgentId : null;
33
+ const agentType = typeof rawAgentType === 'string' && VALID_AGENT_TYPES.has(rawAgentType) ? rawAgentType : null;
34
+ const isSubagent = !!agentId;
35
+ const subagentReadOnly = isSubagent && agentType ? READ_ONLY_AGENT_TYPES.has(agentType) : false;
36
+ return { agentId, agentType, isSubagent, subagentReadOnly };
37
+ }
38
+
39
+ /**
40
+ * Check if the hook-status fast path applies — all gates disabled, skip
41
+ * full orchestration.
42
+ *
43
+ * @param {Object|null} hookStatus - Result of readHookStatus()
44
+ * @returns {boolean}
45
+ */
46
+ function isAllGatesDisabled(hookStatus) {
47
+ if (!hookStatus || !hookStatus.enforcement) return false;
48
+ const enf = hookStatus.enforcement;
49
+ return (
50
+ enf.taskGating === false &&
51
+ enf.scopeGating === false &&
52
+ enf.routingGate === false &&
53
+ enf.commitLogGate === false &&
54
+ enf.todoWriteGate === false &&
55
+ enf.loopEnforcement === false &&
56
+ enf.deployGate === false &&
57
+ enf.strikeEscalation === false &&
58
+ enf.bugfixScope === false &&
59
+ enf.scopeMutation === false &&
60
+ enf.gitSafety === false &&
61
+ hookStatus.componentReuse === false &&
62
+ hookStatus.phaseGate === false &&
63
+ hookStatus.phaseReadGate === false
64
+ );
65
+ }
66
+
67
+ module.exports = {
68
+ VALID_AGENT_TYPES,
69
+ READ_ONLY_AGENT_TYPES,
70
+ parseSubagentContext,
71
+ isAllGatesDisabled,
72
+ };
@@ -0,0 +1,442 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - PreToolUse Orchestrator (Core Module)
5
+ *
6
+ * Gate-cascade logic extracted from scripts/hooks/entry/claude-code/pre-tool-use.js
7
+ * (wf-94cc3b72 epic / TD-002). Entry file shrinks from 538 LOC to a thin
8
+ * wiring layer; this module owns the check sequence.
9
+ *
10
+ * CLI-agnostic: all side-effect and gate dependencies are injected via
11
+ * the `deps` argument so this module can be unit-tested in isolation and
12
+ * reused by future (non-Claude-Code) CLIs.
13
+ *
14
+ * Contract:
15
+ * runPreToolGates({ input, parsedInput }, deps) → coreResult
16
+ * where coreResult = { allowed, blocked, reason?, message?, ...extras }
17
+ *
18
+ * Special sentinel field `_fastPath: true` signals that the all-gates-disabled
19
+ * fast path was taken — the entry-layer should bypass the adapter transform
20
+ * and return an allow response directly. Always paired with allowed=true.
21
+ *
22
+ * Short-circuit: the first gate that blocks wins — subsequent gates skip.
23
+ * Fail-open everywhere except routing gate (fail-closed) and top-level
24
+ * try/catch boundaries.
25
+ */
26
+
27
+ 'use strict';
28
+
29
+ const path = require('node:path');
30
+ const { parseSubagentContext, isAllGatesDisabled } = require('./pre-tool-helpers');
31
+
32
+ /**
33
+ * Run the PreToolUse gate cascade.
34
+ *
35
+ * @param {Object} ctx - { input, parsedInput }
36
+ * @param {Object} deps - Injected gate functions + side-effect helpers
37
+ * - checkScopeGate, checkComponentReuse, checkTodoWriteGate
38
+ * - checkRoutingGate, clearRoutingPending, hasActiveTask
39
+ * - checkPhaseGate, checkCommitLogGate
40
+ * - recordPhaseRead, checkPhaseReadGate, clearPhaseReads
41
+ * - checkDeployGate, checkWriteBlock
42
+ * - checkStrikeGate, checkBugfixScope, checkScopeMutation
43
+ * - checkGitSafety, checkManagerBoundary
44
+ * - markSkillPending, getConfig, readHookStatus
45
+ * - getStrictAdherence
46
+ * @returns {Object} coreResult
47
+ */
48
+ function runPreToolGates(ctx, deps) {
49
+ const { input, parsedInput } = ctx;
50
+
51
+ // Handle empty/invalid input gracefully (caller also handles, belt-and-braces)
52
+ if (!input || Object.keys(input).length === 0) {
53
+ return { allowed: true, blocked: false };
54
+ }
55
+
56
+ const toolName = parsedInput.toolName;
57
+ const toolInput = parsedInput.toolInput || {};
58
+ const filePath = toolInput.file_path;
59
+
60
+ // Agent-aware gating
61
+ const { isSubagent, subagentReadOnly } = parseSubagentContext(input);
62
+
63
+ // Fast path: pre-computed hook status
64
+ const hookStatus = deps.readHookStatus();
65
+ if (isAllGatesDisabled(hookStatus)) {
66
+ return { allowed: true, blocked: false, _fastPath: true };
67
+ }
68
+
69
+ // Load config once
70
+ let config;
71
+ try {
72
+ config = deps.getConfig();
73
+ } catch (err) {
74
+ if (process.env.DEBUG) console.error(`[Hook] Config load error: ${err.message}`);
75
+ config = null;
76
+ }
77
+
78
+ let coreResult = { allowed: true, blocked: false };
79
+
80
+ // Phase-read recording (side effect)
81
+ if (toolName === 'Read' && filePath) {
82
+ try { deps.recordPhaseRead(filePath); } catch (_err) { /* fail-open */ }
83
+ }
84
+
85
+ // Phase gate
86
+ const isReadTool = ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch'].includes(toolName);
87
+ const skipPhaseGateForSubagent = isSubagent && subagentReadOnly && isReadTool;
88
+
89
+ if (!skipPhaseGateForSubagent) {
90
+ try {
91
+ const phaseResult = deps.checkPhaseGate(toolName, toolInput, config);
92
+ if (phaseResult.blocked) {
93
+ return { allowed: false, blocked: true, reason: phaseResult.reason, message: phaseResult.message };
94
+ }
95
+ } catch (err) {
96
+ if (process.env.DEBUG) console.error(`[Hook] Phase gate error (fail-open): ${err.message}`);
97
+ }
98
+ }
99
+
100
+ // Phase-read gate
101
+ if (toolName === 'Edit' || toolName === 'Write' || toolName === 'Bash') {
102
+ try {
103
+ const readGateResult = deps.checkPhaseReadGate(toolName, config);
104
+ if (readGateResult.blocked) {
105
+ return {
106
+ allowed: false,
107
+ blocked: true,
108
+ reason: 'Phase-read gate: phase file not read',
109
+ message: readGateResult.message,
110
+ };
111
+ }
112
+ } catch (_err) {
113
+ if (process.env.DEBUG) console.error(`[Hook] Phase-read gate error (fail-open): ${_err.message}`);
114
+ }
115
+ }
116
+
117
+ // Scope gate (Edit/Write only)
118
+ if (toolName === 'Edit' || toolName === 'Write') {
119
+ coreResult = deps.checkScopeGate({ filePath, operation: toolName.toLowerCase() }, config);
120
+ if (coreResult.blocked) return coreResult;
121
+ }
122
+
123
+ // TodoWrite gate
124
+ if (toolName === 'TodoWrite') {
125
+ const todos = toolInput.todos || [];
126
+ coreResult = deps.checkTodoWriteGate({ todos }, config);
127
+ if (coreResult.blocked) return coreResult;
128
+ }
129
+
130
+ // Skill tool tracking (side effects)
131
+ if (toolName === 'Skill') {
132
+ const skillName = toolInput.skill;
133
+ if (typeof skillName === 'string' && /^wogi-(bulk|start)$/i.test(skillName)) {
134
+ deps.markSkillPending(skillName.toLowerCase(), { args: toolInput.args });
135
+ try { deps.clearPhaseReads(); } catch (_err) { /* fail-open */ }
136
+ if (process.env.DEBUG) {
137
+ console.error(`[Hook] Marked skill ${skillName} as pending (via Skill tool)`);
138
+ }
139
+ }
140
+ if (typeof skillName === 'string' && /^wogi-/i.test(skillName)) {
141
+ try {
142
+ deps.clearRoutingPending();
143
+ if (process.env.DEBUG) {
144
+ console.error(`[Hook] Cleared routing-pending flag (Skill: ${skillName})`);
145
+ }
146
+ } catch (err) {
147
+ if (process.env.DEBUG) console.error(`[Hook] Failed to clear routing flag: ${err.message}`);
148
+ }
149
+ }
150
+ }
151
+
152
+ // Routing gate
153
+ const skipRoutingGateForSubagent = isSubagent && deps.hasActiveTask();
154
+
155
+ let skipRoutingGateForReadOnlyGit = false;
156
+ if (toolName === 'Bash' && typeof toolInput.command === 'string') {
157
+ const cmd = toolInput.command.trim();
158
+ const READ_ONLY_GIT_PREFIXES = [
159
+ 'git status', 'git log', 'git diff', 'git branch',
160
+ 'git show', 'git rev-parse', 'git remote -v', 'git tag -l',
161
+ 'git ls-files', 'git describe',
162
+ ];
163
+ const SHELL_CHAIN_OPERATORS = /[;&|`$()\n\r\\]/;
164
+ const DESTRUCTIVE_GIT_FLAGS = /\s-[dD]\b|\s--delete\b|\s--force\b|\s--hard\b|\s--prune\b/;
165
+ if (
166
+ READ_ONLY_GIT_PREFIXES.some(prefix => cmd.startsWith(prefix)) &&
167
+ !SHELL_CHAIN_OPERATORS.test(cmd) &&
168
+ !DESTRUCTIVE_GIT_FLAGS.test(cmd)
169
+ ) {
170
+ skipRoutingGateForReadOnlyGit = true;
171
+ }
172
+ }
173
+
174
+ const GATED_TOOLS = new Set([
175
+ 'Bash', 'EnterPlanMode', 'Read', 'Glob', 'Grep',
176
+ 'Edit', 'Write', 'NotebookEdit', 'Agent', 'WebSearch', 'WebFetch',
177
+ ]);
178
+ if (!skipRoutingGateForSubagent && !skipRoutingGateForReadOnlyGit && GATED_TOOLS.has(toolName)) {
179
+ try {
180
+ const routingResult = deps.checkRoutingGate(toolName, config);
181
+ if (routingResult.blocked) {
182
+ return {
183
+ allowed: false,
184
+ blocked: true,
185
+ reason: `Routing gate: ${routingResult.reason}`,
186
+ message: routingResult.message,
187
+ };
188
+ }
189
+ } catch (err) {
190
+ // Fail-CLOSED for routing gate
191
+ if (process.env.DEBUG) console.error(`[Hook] Routing gate error (fail-closed): ${err.message}`);
192
+ return {
193
+ allowed: false,
194
+ blocked: true,
195
+ reason: `Routing gate error: ${err.message}`,
196
+ message: 'Routing gate check failed. Please invoke /wogi-start first. Use Skill(skill="wogi-start", args="<your request>").',
197
+ };
198
+ }
199
+ }
200
+
201
+ // Manager boundary
202
+ if (process.env.WOGI_REPO_NAME === 'manager') {
203
+ try {
204
+ const boundaryResult = deps.checkManagerBoundary(toolName, toolInput);
205
+ if (boundaryResult.blocked) {
206
+ return {
207
+ allowed: false,
208
+ blocked: true,
209
+ reason: boundaryResult.reason,
210
+ message: boundaryResult.message,
211
+ };
212
+ }
213
+ } catch (err) {
214
+ if (process.env.DEBUG) console.error(`[Hook] Manager boundary gate error (fail-open): ${err.message}`);
215
+ }
216
+ }
217
+
218
+ // Commit log gate
219
+ if (toolName === 'Bash' && toolInput.command) {
220
+ try {
221
+ const commitLogResult = deps.checkCommitLogGate(toolInput.command, config);
222
+ if (commitLogResult.blocked) {
223
+ return {
224
+ allowed: false,
225
+ blocked: true,
226
+ reason: `Commit log gate: ${commitLogResult.reason}`,
227
+ message: commitLogResult.message,
228
+ };
229
+ }
230
+ } catch (err) {
231
+ if (process.env.DEBUG) console.error(`[Hook] Commit log gate error (fail-open): ${err.message}`);
232
+ }
233
+ }
234
+
235
+ // Deploy gate (Bash)
236
+ if (toolName === 'Bash' && toolInput.command) {
237
+ try {
238
+ const deployResult = deps.checkDeployGate(toolInput.command, config);
239
+ if (deployResult.blocked) {
240
+ return {
241
+ allowed: false,
242
+ blocked: true,
243
+ reason: `Deploy gate: ${deployResult.reason}`,
244
+ message: deployResult.message,
245
+ };
246
+ }
247
+ } catch (err) {
248
+ if (process.env.DEBUG) console.error(`[Hook] Deploy gate error (fail-open): ${err.message}`);
249
+ }
250
+ }
251
+
252
+ // Deploy gate (Write anti-forgery)
253
+ if ((toolName === 'Write' || toolName === 'Edit') && filePath) {
254
+ try {
255
+ const writeBlockResult = deps.checkWriteBlock(filePath, config);
256
+ if (writeBlockResult.blocked) {
257
+ return {
258
+ allowed: false,
259
+ blocked: true,
260
+ reason: `Deploy gate: ${writeBlockResult.reason}`,
261
+ message: writeBlockResult.message,
262
+ };
263
+ }
264
+ } catch (err) {
265
+ if (process.env.DEBUG) console.error(`[Hook] Deploy gate write-block error (fail-open): ${err.message}`);
266
+ }
267
+ }
268
+
269
+ // Scope mutation guard
270
+ if (toolName === 'Write' || toolName === 'Bash') {
271
+ try {
272
+ const scopeMutResult = deps.checkScopeMutation(toolName, toolInput, config);
273
+ if (scopeMutResult.blocked) {
274
+ return {
275
+ allowed: false,
276
+ blocked: true,
277
+ reason: `Scope mutation: ${scopeMutResult.reason}`,
278
+ message: scopeMutResult.message,
279
+ };
280
+ }
281
+ } catch (err) {
282
+ if (process.env.DEBUG) console.error(`[Hook] Scope mutation gate error (fail-open): ${err.message}`);
283
+ }
284
+ }
285
+
286
+ // Git safety
287
+ if (toolName === 'Bash' && toolInput.command && /git\s+(reset|checkout\s+(--\s+)?[\.\-]|restore\s+.*\.|clean\s+.*-f)/.test(toolInput.command)) {
288
+ try {
289
+ const gitResult = deps.checkGitSafety(toolInput.command, config);
290
+ if (gitResult.blocked) {
291
+ return {
292
+ allowed: false,
293
+ blocked: true,
294
+ reason: `Git safety: ${gitResult.reason}`,
295
+ message: gitResult.message,
296
+ };
297
+ }
298
+ } catch (err) {
299
+ if (process.env.DEBUG) console.error(`[Hook] Git safety gate error (fail-open): ${err.message}`);
300
+ }
301
+ }
302
+
303
+ // Bugfix scope
304
+ if (toolName === 'Edit' || toolName === 'Write') {
305
+ try {
306
+ const scopeResult = deps.checkBugfixScope(toolName, config);
307
+ if (scopeResult.blocked) {
308
+ return {
309
+ allowed: false,
310
+ blocked: true,
311
+ reason: `Bugfix scope: ${scopeResult.reason}`,
312
+ message: scopeResult.message,
313
+ };
314
+ }
315
+ } catch (err) {
316
+ if (process.env.DEBUG) console.error(`[Hook] Bugfix scope gate error (fail-open): ${err.message}`);
317
+ }
318
+ }
319
+
320
+ // Strike gate
321
+ if (toolName === 'Edit' || toolName === 'Write' || toolName === 'Bash') {
322
+ try {
323
+ const strikeResult = deps.checkStrikeGate(toolName, config);
324
+ if (strikeResult.blocked) {
325
+ return {
326
+ allowed: false,
327
+ blocked: true,
328
+ reason: `Strike gate: ${strikeResult.reason}`,
329
+ message: strikeResult.message,
330
+ };
331
+ }
332
+ } catch (err) {
333
+ if (process.env.DEBUG) console.error(`[Hook] Strike gate error (fail-open): ${err.message}`);
334
+ }
335
+ }
336
+
337
+ // Strict adherence (Bash)
338
+ if (toolName === 'Bash') {
339
+ const command = toolInput.command;
340
+ if (command) {
341
+ const strictAdherence = deps.getStrictAdherence();
342
+ if (strictAdherence.isEnabled()) {
343
+ const cmdResult = strictAdherence.validateCommand(command);
344
+ if (cmdResult.blocked) {
345
+ return {
346
+ allowed: false,
347
+ blocked: true,
348
+ reason: `Strict adherence: ${cmdResult.reason}`,
349
+ message: cmdResult.autoCorrect
350
+ ? `⚠️ BLOCKED: ${cmdResult.reason}\n\n✅ Auto-correcting to: ${cmdResult.autoCorrect}`
351
+ : `⚠️ BLOCKED: ${cmdResult.reason}\n\n💡 ${cmdResult.suggestion || 'Please use the correct pattern.'}`,
352
+ };
353
+ }
354
+ }
355
+ }
356
+ }
357
+
358
+ // Damage control (Bash)
359
+ if (toolName === 'Bash' && toolInput.command && config?.damageControl?.enabled) {
360
+ try {
361
+ const dc = require('../../flow-damage-control');
362
+ const dcResult = dc.checkBashEvent(toolInput.command);
363
+ if (dcResult && dcResult.action === 'block') {
364
+ return {
365
+ allowed: false,
366
+ blocked: true,
367
+ reason: `Damage control: ${dcResult.reason || dcResult.message || 'blocked by rule'}`,
368
+ message: `\u26d4 BLOCKED by damage control: ${dcResult.message || dcResult.reason || 'This command matches a blocked pattern.'}\n\nRule: ${dcResult.rule || 'unknown'}`,
369
+ };
370
+ }
371
+ if (dcResult && dcResult.action === 'ask') {
372
+ return {
373
+ allowed: true,
374
+ blocked: false,
375
+ reason: `Damage control warning: ${dcResult.reason || dcResult.message || 'requires confirmation'}`,
376
+ message: `\u26a0\ufe0f Damage control: ${dcResult.message || dcResult.reason || 'This command matches an ask-before-execute pattern.'}`,
377
+ };
378
+ }
379
+ } catch (err) {
380
+ if (process.env.DEBUG) console.error(`[Hook] Damage control error (fail-open): ${err.message}`);
381
+ }
382
+ }
383
+
384
+ // Damage control (file ops)
385
+ if ((toolName === 'Edit' || toolName === 'Write') && filePath && config?.damageControl?.enabled && config?.damageControl?.events?.file) {
386
+ try {
387
+ const dc = require('../../flow-damage-control');
388
+ const dcResult = dc.checkFileEvent(filePath, toolName.toLowerCase());
389
+ if (dcResult && dcResult.action === 'block') {
390
+ return {
391
+ allowed: false,
392
+ blocked: true,
393
+ reason: `Damage control: ${dcResult.reason || 'file access blocked'}`,
394
+ message: `\u26d4 BLOCKED by damage control: ${dcResult.message || 'This file is protected.'}`,
395
+ };
396
+ }
397
+ } catch (err) {
398
+ if (process.env.DEBUG) console.error(`[Hook] Damage control file check error (fail-open): ${err.message}`);
399
+ }
400
+ }
401
+
402
+ // Component reuse (Write only)
403
+ if (toolName === 'Write' && filePath) {
404
+ const componentResult = deps.checkComponentReuse({ filePath, content: toolInput.content }, config);
405
+
406
+ if (componentResult.blocked || componentResult.warning) {
407
+ coreResult = {
408
+ ...coreResult,
409
+ ...componentResult,
410
+ allowed: !componentResult.blocked,
411
+ blocked: componentResult.blocked,
412
+ };
413
+ }
414
+
415
+ // Strict adherence — file naming
416
+ if (!coreResult.blocked) {
417
+ const strictAdherence = deps.getStrictAdherence();
418
+ if (strictAdherence.isEnabled()) {
419
+ const isComponent = /\/(components?|ui)\//i.test(filePath) && /\.(tsx|jsx)$/i.test(filePath);
420
+ const isApi = /\/(api|routes)\//i.test(filePath);
421
+ const fileType = isComponent ? 'component' : isApi ? 'api' : 'generic';
422
+
423
+ const fileName = path.basename(filePath);
424
+ const fileResult = strictAdherence.validateFileName(fileName, fileType);
425
+ if (fileResult.blocked) {
426
+ coreResult = {
427
+ allowed: false,
428
+ blocked: true,
429
+ reason: `Strict adherence: ${fileResult.reason}`,
430
+ message: `⚠️ BLOCKED: ${fileResult.reason}\n\n💡 ${fileResult.suggestion || 'Please use the correct naming convention.'}`,
431
+ };
432
+ }
433
+ }
434
+ }
435
+ }
436
+
437
+ return coreResult;
438
+ }
439
+
440
+ module.exports = {
441
+ runPreToolGates,
442
+ };
@@ -151,6 +151,14 @@ function setRoutingPending() {
151
151
  pid: process.pid
152
152
  }), 'utf-8');
153
153
 
154
+ // Keep hook-status aggregator in sync with the flag file (wf-7c36aaed prep
155
+ // for future perf-003 — once synced, routing-gate reads can come from
156
+ // hook-status cache instead of direct file reads).
157
+ try {
158
+ const { setRouting } = require('../../flow-hook-status');
159
+ setRouting({ pending: true, cleared: false });
160
+ } catch (_err) { /* non-critical */ }
161
+
154
162
  if (process.env.DEBUG) {
155
163
  console.error('[routing-gate] Set routing-pending flag');
156
164
  }
@@ -147,6 +147,34 @@ function handleSessionEnd(input) {
147
147
  } catch (err) {
148
148
  // Non-critical — memory DB may not be available
149
149
  }
150
+
151
+ // Promotion pipeline (wf-6a352aae) — scan adversary-runs + correction-patterns
152
+ // for content that should be promoted to feedback-patterns.md. Writes to
153
+ // pending-promotions.json; user reviews via `flow promote apply`.
154
+ // Fire-and-forget, gated by config.promotion.autoAtSessionEnd.
155
+ try {
156
+ const cfg = getConfig();
157
+ if (cfg.promotion?.autoAtSessionEnd !== false) {
158
+ const promote = require('../../flow-promote');
159
+ promote.promoteAll(promote.getPromotionConfig()).then((r) => {
160
+ if (r.proposed > 0) {
161
+ result.pendingPromotions = {
162
+ count: r.proposed,
163
+ message: `${r.proposed} promotion(s) ready. Run \`flow promote apply\` to write to feedback-patterns.md.`,
164
+ };
165
+ }
166
+ }).catch((err) => {
167
+ if (process.env.DEBUG) {
168
+ console.error(`[Session End] Promotion pipeline failed: ${err.message}`);
169
+ }
170
+ });
171
+ }
172
+ } catch (err) {
173
+ // Non-critical — promotion module may not be available
174
+ if (process.env.DEBUG) {
175
+ console.error(`[Session End] Promotion module unavailable: ${err.message}`);
176
+ }
177
+ }
150
178
  } catch (err) {
151
179
  result.warning = `Session end handler error: ${err.message}`;
152
180
  }