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.
- package/.claude/commands/wogi-audit.md +212 -17
- package/.claude/commands/wogi-research.md +37 -0
- package/.claude/commands/wogi-review.md +200 -22
- package/.claude/commands/wogi-start.md +45 -0
- package/.claude/docs/claude-code-compatibility.md +46 -1
- package/.claude/docs/intent-grounded-review.md +209 -0
- package/.claude/settings.json +34 -1
- package/.workflow/agents/logic-adversary.md +8 -0
- package/.workflow/templates/claude-md.hbs +18 -0
- package/lib/installer.js +22 -0
- package/lib/utils.js +29 -3
- package/lib/workspace-changelog.js +2 -1
- package/lib/workspace-channel-server.js +4 -6
- package/lib/workspace-contracts.js +5 -4
- package/lib/workspace-events.js +8 -7
- package/lib/workspace-gates.js +4 -3
- package/lib/workspace-integration-tests.js +2 -1
- package/lib/workspace-intelligence.js +3 -2
- package/lib/workspace-locks.js +2 -1
- package/lib/workspace-messages.js +7 -6
- package/lib/workspace-routing.js +14 -26
- package/lib/workspace-session.js +7 -6
- package/lib/workspace-sync.js +9 -8
- package/package.json +4 -2
- package/scripts/base-workflow-step.js +1 -1
- package/scripts/flow +19 -0
- package/scripts/flow-adaptive-learning.js +1 -1
- package/scripts/flow-aggregate.js +2 -1
- package/scripts/flow-architect-pass.js +3 -3
- package/scripts/flow-archive-runs.js +372 -0
- package/scripts/flow-ask.js +1 -1
- package/scripts/flow-ast-grep.js +216 -0
- package/scripts/flow-audit-gates.js +1 -1
- package/scripts/flow-auto-learn.js +8 -11
- package/scripts/flow-bug.js +2 -2
- package/scripts/flow-capture-gate.js +644 -0
- package/scripts/flow-capture.js +4 -3
- package/scripts/flow-cli-flags.js +95 -0
- package/scripts/flow-community-sync.js +2 -1
- package/scripts/flow-community.js +6 -6
- package/scripts/flow-conclusion-classifier.js +310 -0
- package/scripts/flow-config-defaults.js +3 -3
- package/scripts/flow-constants.js +8 -11
- package/scripts/flow-context-scoring.js +1 -0
- package/scripts/flow-correction-detector.js +344 -3
- package/scripts/flow-damage-control.js +1 -1
- package/scripts/flow-decisions-merge.js +1 -0
- package/scripts/flow-done-gates.js +20 -0
- package/scripts/flow-done-report.js +2 -2
- package/scripts/flow-done.js +4 -4
- package/scripts/flow-epics.js +5 -11
- package/scripts/flow-health.js +145 -1
- package/scripts/flow-id.js +92 -0
- package/scripts/flow-io.js +15 -5
- package/scripts/flow-knowledge-router.js +2 -1
- package/scripts/flow-links.js +1 -1
- package/scripts/flow-log-manager.js +2 -1
- package/scripts/flow-logic-adversary.js +4 -4
- package/scripts/flow-long-input-cli.js +6 -0
- package/scripts/flow-long-input-stories.js +1 -1
- package/scripts/flow-loop-retry-learning.js +1 -1
- package/scripts/flow-mcp-capabilities.js +2 -3
- package/scripts/flow-mcp-docs.js +2 -1
- package/scripts/flow-memory-blocks.js +2 -1
- package/scripts/flow-memory-sync.js +1 -1
- package/scripts/flow-memory.js +767 -0
- package/scripts/flow-migrate-igr.js +1 -1
- package/scripts/flow-migrate.js +2 -1
- package/scripts/flow-model-adapter.js +1 -1
- package/scripts/flow-model-config.js +5 -1
- package/scripts/flow-model-profile.js +2 -1
- package/scripts/flow-orchestrate.js +3 -3
- package/scripts/flow-output.js +29 -0
- package/scripts/flow-parallel.js +10 -9
- package/scripts/flow-pattern-enforcer.js +2 -1
- package/scripts/flow-permissions-audit.js +124 -0
- package/scripts/flow-plugin-registry.js +2 -2
- package/scripts/flow-progress.js +5 -1
- package/scripts/flow-project-analyzer.js +1 -1
- package/scripts/flow-promote.js +510 -0
- package/scripts/flow-registries.js +86 -0
- package/scripts/flow-request-log.js +133 -0
- package/scripts/flow-research-protocol.js +0 -1
- package/scripts/flow-revision-tracker.js +2 -1
- package/scripts/flow-roadmap.js +2 -1
- package/scripts/flow-rules-sync.js +3 -7
- package/scripts/flow-session-end.js +3 -1
- package/scripts/flow-session-learning.js +6 -13
- package/scripts/flow-session-state.js +2 -2
- package/scripts/flow-setup-hooks.js +2 -1
- package/scripts/flow-skill-create.js +1 -1
- package/scripts/flow-skill-freshness.js +6 -7
- package/scripts/flow-skill-learn.js +1 -1
- package/scripts/flow-step-coverage.js +1 -1
- package/scripts/flow-step-security.js +1 -1
- package/scripts/flow-story.js +58 -10
- package/scripts/flow-sys.js +204 -0
- package/scripts/flow-task-hierarchy.js +88 -0
- package/scripts/flow-tech-debt.js +2 -1
- package/scripts/flow-test-api.js +1 -1
- package/scripts/flow-utils.js +60 -890
- package/scripts/hooks/core/bugfix-scope-gate.js +5 -4
- package/scripts/hooks/core/deploy-gate.js +1 -1
- package/scripts/hooks/core/pre-tool-helpers.js +72 -0
- package/scripts/hooks/core/pre-tool-orchestrator.js +442 -0
- package/scripts/hooks/core/routing-gate.js +8 -0
- package/scripts/hooks/core/session-end.js +28 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +48 -492
- package/scripts/hooks/entry/shared/hook-runner.js +1 -1
- package/scripts/registries/schema-registry.js +1 -1
- 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
|
-
|
|
336
|
-
|
|
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 (
|
|
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
|
}
|