wogiflow 2.29.2 → 2.29.3
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/docs/intent-grounded-reasoning.md +1 -1
- package/.workflow/templates/partials/methodology-rules.hbs +30 -1
- package/lib/commands/team-connection.js +5 -28
- package/lib/utils.js +12 -26
- package/lib/wogi-claude +40 -1
- package/lib/workspace.js +6 -13
- package/package.json +2 -2
- package/scripts/flow +4 -0
- package/scripts/flow-autonomous-detector.js +29 -4
- package/scripts/flow-autonomous-mode.js +27 -7
- package/scripts/flow-completion-summary.js +2 -16
- package/scripts/flow-id.js +31 -0
- package/scripts/flow-io.js +78 -0
- package/scripts/flow-long-input-pending.js +110 -0
- package/scripts/flow-long-input-stories.js +8 -0
- package/scripts/flow-orchestrate.js +16 -10
- package/scripts/flow-question-queue.js +73 -7
- package/scripts/flow-scanner-base.js +77 -1
- package/scripts/flow-session-state.js +47 -0
- package/scripts/flow-source-fidelity.js +279 -0
- package/scripts/flow-time-format.js +42 -0
- package/scripts/flow-utils.js +3 -16
- package/scripts/flow-worker-mcp-strip.js +12 -11
- package/scripts/flow-workspace-summary.js +38 -19
- package/scripts/hooks/adapters/claude-code.js +7 -4
- package/scripts/hooks/core/long-input-enforcement.js +311 -0
- package/scripts/hooks/core/pre-tool-deps.js +185 -0
- package/scripts/hooks/core/pre-tool-orchestrator.js +22 -0
- package/scripts/hooks/core/session-context.js +26 -0
- package/scripts/hooks/core/task-boundary-reset.js +13 -0
- package/scripts/hooks/core/worker-boundary-gate.js +67 -16
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +21 -95
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +33 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Long-Input Enforcement Gate (P11.5 mechanical layer)
|
|
5
|
+
*
|
|
6
|
+
* The methodology rules + Adversary check (P11.5) are not enough on their
|
|
7
|
+
* own — text rules can be ignored. This gate makes the enforcement
|
|
8
|
+
* MECHANICAL by injecting a hard instruction when a long-form prompt
|
|
9
|
+
* arrives without a source-link, forcing the AI to run
|
|
10
|
+
* /wogi-extract-review before any work.
|
|
11
|
+
*
|
|
12
|
+
* Three triggers (in order of strictness):
|
|
13
|
+
*
|
|
14
|
+
* STRICT (worker-side channel-dispatch): when a workspace worker
|
|
15
|
+
* receives a channel message that's long-form without a
|
|
16
|
+
* source-link, inject a forcing instruction. This is the direct
|
|
17
|
+
* fix for the wogi-hub 2026-04-27 incident — manager compressed
|
|
18
|
+
* a 50-line prompt into a 5-bullet contract; worker had no
|
|
19
|
+
* mechanical reason to know the prompt was lossy. With this
|
|
20
|
+
* gate, worker auto-routes to /wogi-extract-review on receipt.
|
|
21
|
+
*
|
|
22
|
+
* FORCE (any long-form prompt without source-link): inject the
|
|
23
|
+
* same forcing instruction for all sessions, not just workers.
|
|
24
|
+
* The user typing a 50-line prompt directly into a manager
|
|
25
|
+
* session ALSO benefits from being routed to extraction.
|
|
26
|
+
*
|
|
27
|
+
* SUGGEST (ambiguous): below the strict threshold but still
|
|
28
|
+
* long-ish — surface the option without forcing.
|
|
29
|
+
*
|
|
30
|
+
* Detection heuristics:
|
|
31
|
+
* - >40 lines or ≥5 discrete items (same as long-input-gate)
|
|
32
|
+
* - Source-link patterns: `## Original Request (verbatim)`,
|
|
33
|
+
* `.workflow/changes/wf-XXXXXXXX.md` references, `spec: <path>`,
|
|
34
|
+
* `wf-XXXXXXXX` IDs in the message body.
|
|
35
|
+
*
|
|
36
|
+
* Public API:
|
|
37
|
+
* detectLongFormPrompt(text) → boolean
|
|
38
|
+
* hasSourceLink(text) → boolean
|
|
39
|
+
* shouldForceExtractReview({text, source, env}) → {forced, reason}
|
|
40
|
+
* buildEnforcementMessage(reason) → instruction text
|
|
41
|
+
* markLongInputPending(payload) → writes .workflow/state/long-input-pending.json
|
|
42
|
+
* clearLongInputPending() → clears the marker
|
|
43
|
+
* isLongInputPending() → boolean
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
const fs = require('node:fs');
|
|
47
|
+
const path = require('node:path');
|
|
48
|
+
const { PATHS } = require('../../flow-utils');
|
|
49
|
+
|
|
50
|
+
const PENDING_PATH = path.join(PATHS.state, 'long-input-pending.json');
|
|
51
|
+
|
|
52
|
+
const LONG_LINE_THRESHOLD = 40;
|
|
53
|
+
const LONG_ITEM_THRESHOLD = 5;
|
|
54
|
+
|
|
55
|
+
// Imperative verbs that suggest the prompt is task-like (vs. prose / log dump).
|
|
56
|
+
// When ≥2 imperatives are present alongside structural items, the prompt is
|
|
57
|
+
// almost certainly work-creating and the gate fires.
|
|
58
|
+
const TASK_IMPERATIVES = [
|
|
59
|
+
/\b(?:add|build|create|implement|fix|refactor|remove|delete|rename|move|extract|consolidate|migrate|update|enhance|integrate|connect|map|route|enforce|preserve|validate|check)\b/i
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// Patterns that indicate a source-link — at least one must be present for the
|
|
63
|
+
// gate to PASS a long-form prompt without forcing extract-review.
|
|
64
|
+
const SOURCE_LINK_PATTERNS = [
|
|
65
|
+
/^##\s+Original Request \(verbatim\)\s*$/m,
|
|
66
|
+
/\.workflow\/changes\/wf-[a-f0-9]{8}/i,
|
|
67
|
+
/\.workflow\/specs\/wf-[a-f0-9]{8}/i,
|
|
68
|
+
/^spec:\s*[^\s]+\.md/im,
|
|
69
|
+
/^source:\s*[^\s]+\.md/im,
|
|
70
|
+
/\bwf-[a-f0-9]{8}\b/i // bare wf-ID reference
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
function countDiscreteItems(text) {
|
|
74
|
+
if (typeof text !== 'string') return 0;
|
|
75
|
+
let count = 0;
|
|
76
|
+
for (const line of text.split('\n')) {
|
|
77
|
+
if (/^\s*[-*]\s+/.test(line)) count++;
|
|
78
|
+
else if (/^\s*\d+[.)]\s+/.test(line)) count++;
|
|
79
|
+
}
|
|
80
|
+
return count;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function detectLongFormPrompt(text) {
|
|
84
|
+
if (typeof text !== 'string' || !text.trim()) return false;
|
|
85
|
+
const lineCount = text.split('\n').filter(l => l.trim()).length;
|
|
86
|
+
if (lineCount > LONG_LINE_THRESHOLD) return true;
|
|
87
|
+
if (countDiscreteItems(text) >= LONG_ITEM_THRESHOLD) return true;
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function hasSourceLink(text) {
|
|
92
|
+
if (typeof text !== 'string') return false;
|
|
93
|
+
return SOURCE_LINK_PATTERNS.some(re => re.test(text));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function hasTaskSignals(text) {
|
|
97
|
+
if (typeof text !== 'string') return false;
|
|
98
|
+
let imperativeHits = 0;
|
|
99
|
+
for (const re of TASK_IMPERATIVES) {
|
|
100
|
+
const m = text.match(new RegExp(re.source, 'gi'));
|
|
101
|
+
if (m) imperativeHits += m.length;
|
|
102
|
+
}
|
|
103
|
+
return imperativeHits >= 2;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Detect whether the current prompt is a channel-dispatched message in
|
|
108
|
+
* worker mode. UserPromptSubmit gets `parsedInput.source` from Claude
|
|
109
|
+
* Code's hook payload — channel-dispatched prompts arrive with a
|
|
110
|
+
* channel-specific source identifier. We also check env vars to confirm
|
|
111
|
+
* worker context. Defensive: returns false in any edge case.
|
|
112
|
+
*/
|
|
113
|
+
function isChannelDispatchInWorker(source, env = process.env) {
|
|
114
|
+
if (!env.WOGI_WORKSPACE_ROOT) return false;
|
|
115
|
+
if (!env.WOGI_REPO_NAME || env.WOGI_REPO_NAME === 'manager') return false;
|
|
116
|
+
// Channel-dispatched prompts have specific source markers.
|
|
117
|
+
if (typeof source !== 'string') return false;
|
|
118
|
+
return /channel|notifications/i.test(source);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Decide whether the prompt should force-route to /wogi-extract-review.
|
|
123
|
+
*
|
|
124
|
+
* @param {object} input
|
|
125
|
+
* @param {string} input.text - prompt text
|
|
126
|
+
* @param {string} [input.source] - parsedInput.source from Claude Code
|
|
127
|
+
* @param {object} [input.env] - environment (for testing)
|
|
128
|
+
* @returns {{forced: boolean, level: 'strict'|'force'|'suggest'|'pass', reason: string}}
|
|
129
|
+
*/
|
|
130
|
+
function shouldForceExtractReview({ text, source, env = process.env } = {}) {
|
|
131
|
+
if (!detectLongFormPrompt(text)) {
|
|
132
|
+
return { forced: false, level: 'pass', reason: 'below-long-input-threshold' };
|
|
133
|
+
}
|
|
134
|
+
// If the prompt already has a source-link, trust it — the upstream
|
|
135
|
+
// already preserved the verbatim source and reconciled items.
|
|
136
|
+
if (hasSourceLink(text)) {
|
|
137
|
+
return { forced: false, level: 'pass', reason: 'source-link-present' };
|
|
138
|
+
}
|
|
139
|
+
// Check task-likeness — pure data dumps (log files, code pastes) shouldn't
|
|
140
|
+
// be forced through extract-review even if they're long.
|
|
141
|
+
if (!hasTaskSignals(text)) {
|
|
142
|
+
return { forced: false, level: 'suggest', reason: 'long-but-no-task-signals' };
|
|
143
|
+
}
|
|
144
|
+
// Worker receiving channel-dispatched long-form without source-link:
|
|
145
|
+
// STRICT — this is the wogi-hub 2026-04-27 failure mode.
|
|
146
|
+
if (isChannelDispatchInWorker(source, env)) {
|
|
147
|
+
return { forced: true, level: 'strict', reason: 'channel-dispatch-without-source-link' };
|
|
148
|
+
}
|
|
149
|
+
// Any other session: long-form + task-like + no source-link → force.
|
|
150
|
+
return { forced: true, level: 'force', reason: 'long-form-task-without-source-link' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildEnforcementMessage(reason, level) {
|
|
154
|
+
const header = level === 'strict'
|
|
155
|
+
? '🚨 STRICT P11.5 ENFORCEMENT — manager compression detected'
|
|
156
|
+
: '🚨 P11.5 ENFORCEMENT — long-form prompt without source-link';
|
|
157
|
+
const body = [];
|
|
158
|
+
body.push(header);
|
|
159
|
+
body.push('');
|
|
160
|
+
if (level === 'strict') {
|
|
161
|
+
body.push('This prompt arrived via channel-dispatch in worker mode and qualifies as');
|
|
162
|
+
body.push('long-form (>40 lines OR ≥5 discrete items) without a source-link. The');
|
|
163
|
+
body.push('manager that dispatched this message SHOULD have included a path to a spec');
|
|
164
|
+
body.push('with `## Original Request (verbatim)`. It did not. This is the exact failure');
|
|
165
|
+
body.push('shape that caused the wogi-hub 2026-04-27 Customers > Services regression.');
|
|
166
|
+
body.push('');
|
|
167
|
+
body.push('You MUST reverse the compression at this layer:');
|
|
168
|
+
} else {
|
|
169
|
+
body.push('This prompt qualifies as long-form (>40 lines OR ≥5 discrete items) AND');
|
|
170
|
+
body.push('contains task-creating signals (imperatives + structured items). Per P11.5,');
|
|
171
|
+
body.push('long-form work-creating prompts MUST go through /wogi-extract-review so');
|
|
172
|
+
body.push('every item is captured and reconciled.');
|
|
173
|
+
body.push('');
|
|
174
|
+
body.push('You MUST:');
|
|
175
|
+
}
|
|
176
|
+
body.push(' 1. Invoke `Skill(skill="wogi-extract-review")` BEFORE any other work.');
|
|
177
|
+
body.push(' 2. Let extract-review run its 6-phase pipeline (extract → review → topics →');
|
|
178
|
+
body.push(' map → clarify → stories) on this prompt.');
|
|
179
|
+
body.push(' 3. Use the resulting stories + item manifest as canonical source.');
|
|
180
|
+
body.push(' 4. Any spec or channel-dispatch you write next MUST link to the saved');
|
|
181
|
+
body.push(' spec file and include `## Original Request (verbatim)`.');
|
|
182
|
+
body.push('');
|
|
183
|
+
body.push(`Reason: ${reason}`);
|
|
184
|
+
body.push('');
|
|
185
|
+
body.push('Override (if you genuinely judge this prompt does NOT create work):');
|
|
186
|
+
body.push(' Run `flow long-input-pending dismiss --reason="<concrete reason>"`');
|
|
187
|
+
body.push(' Then proceed. The dismiss is logged for telemetry/learning.');
|
|
188
|
+
return body.join('\n');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function markLongInputPending(payload) {
|
|
192
|
+
try {
|
|
193
|
+
fs.mkdirSync(path.dirname(PENDING_PATH), { recursive: true });
|
|
194
|
+
fs.writeFileSync(PENDING_PATH, JSON.stringify({
|
|
195
|
+
markedAt: new Date().toISOString(),
|
|
196
|
+
...payload
|
|
197
|
+
}, null, 2));
|
|
198
|
+
return true;
|
|
199
|
+
} catch (_err) { return false; }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function clearLongInputPending() {
|
|
203
|
+
try { if (fs.existsSync(PENDING_PATH)) fs.unlinkSync(PENDING_PATH); }
|
|
204
|
+
catch (_err) { /* ignore */ }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isLongInputPending() {
|
|
208
|
+
try { return fs.existsSync(PENDING_PATH); }
|
|
209
|
+
catch (_err) { return false; }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function readLongInputPending() {
|
|
213
|
+
try {
|
|
214
|
+
if (!fs.existsSync(PENDING_PATH)) return null;
|
|
215
|
+
return JSON.parse(fs.readFileSync(PENDING_PATH, 'utf-8'));
|
|
216
|
+
} catch (_err) { return null; }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* PreToolUse gate consulting the long-input-pending marker.
|
|
221
|
+
* When the marker is present, blocks Edit/Write/Bash/Skill except for
|
|
222
|
+
* a small allowlist that's needed to either run extract-review or
|
|
223
|
+
* dismiss the marker. Returns the same `{blocked, reason?, message?}`
|
|
224
|
+
* shape as the other PreToolUse gates so it composes cleanly in
|
|
225
|
+
* pre-tool-orchestrator.
|
|
226
|
+
*
|
|
227
|
+
* Allowlist (these MUST stay reachable while the marker is present):
|
|
228
|
+
* - Skill calls to `wogi-extract-review` (the way out)
|
|
229
|
+
* - Skill calls to `wogi-start` only when args is `--bypass-long-input`
|
|
230
|
+
* or `wogi-extract-review` (escape hatch routes)
|
|
231
|
+
* - Bash calls invoking `flow long-input-pending dismiss` or the
|
|
232
|
+
* extract-review CLI
|
|
233
|
+
* - Read tool (no state changes)
|
|
234
|
+
*
|
|
235
|
+
* Everything else is blocked with a message redirecting to extract-review.
|
|
236
|
+
*/
|
|
237
|
+
function checkLongInputPendingGate(toolName, toolInput) {
|
|
238
|
+
if (!isLongInputPending()) return { blocked: false };
|
|
239
|
+
|
|
240
|
+
// Read tool is always allowed — investigation is fine while pending.
|
|
241
|
+
if (toolName === 'Read' || toolName === 'Glob' || toolName === 'Grep') {
|
|
242
|
+
return { blocked: false };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Skill tool — allow only the way-out skills
|
|
246
|
+
if (toolName === 'Skill') {
|
|
247
|
+
const skill = (toolInput && toolInput.skill) || '';
|
|
248
|
+
const args = (toolInput && toolInput.args) || '';
|
|
249
|
+
if (skill === 'wogi-extract-review') return { blocked: false };
|
|
250
|
+
if (skill === 'wogi-start' && /(?:^|\s)(?:wogi-extract-review|--bypass-long-input)\b/.test(args)) {
|
|
251
|
+
return { blocked: false };
|
|
252
|
+
}
|
|
253
|
+
// Falls through to block
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Bash — allow the dismiss / extract-review CLI commands
|
|
257
|
+
if (toolName === 'Bash') {
|
|
258
|
+
const cmd = (toolInput && toolInput.command) || '';
|
|
259
|
+
if (/flow\s+long-input-pending\s+dismiss/.test(cmd)) return { blocked: false };
|
|
260
|
+
if (/flow\s+extract-zero-loss/.test(cmd)) return { blocked: false };
|
|
261
|
+
if (/flow\s+long-input/.test(cmd)) return { blocked: false };
|
|
262
|
+
if (/flow-source-fidelity\.js/.test(cmd)) return { blocked: false };
|
|
263
|
+
// Falls through to block for everything else
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Block Edit / Write / NotebookEdit unconditionally while pending
|
|
267
|
+
const payload = readLongInputPending();
|
|
268
|
+
const level = payload?.level || 'unknown';
|
|
269
|
+
const reason = payload?.reason || 'long-form prompt without source-link';
|
|
270
|
+
return {
|
|
271
|
+
blocked: true,
|
|
272
|
+
reason: 'long-input-pending',
|
|
273
|
+
message: [
|
|
274
|
+
'🚨 BLOCKED: long-input-pending marker is set.',
|
|
275
|
+
'',
|
|
276
|
+
`A long-form prompt was detected (level: ${level}, reason: ${reason})`,
|
|
277
|
+
'and you have not yet run /wogi-extract-review on it.',
|
|
278
|
+
'',
|
|
279
|
+
'Per P11.6 (Temporal Source Coverage), every item in the user\'s prompt',
|
|
280
|
+
'must be captured before any work begins. Compressing the prompt into a',
|
|
281
|
+
'spec or channel-dispatch is the wogi-hub failure shape — it loses items.',
|
|
282
|
+
'',
|
|
283
|
+
'To unblock:',
|
|
284
|
+
' 1. (RECOMMENDED) Invoke `Skill(skill="wogi-extract-review")` to run',
|
|
285
|
+
' the 6-phase extraction pipeline. Marker auto-clears on completion.',
|
|
286
|
+
' 2. (ESCAPE HATCH) If this prompt genuinely does NOT create work',
|
|
287
|
+
' (e.g., it\'s a log dump or pure question), dismiss with:',
|
|
288
|
+
' `flow long-input-pending dismiss --reason="<concrete reason>"`',
|
|
289
|
+
'',
|
|
290
|
+
'Read/Glob/Grep tools remain available for investigation.'
|
|
291
|
+
].join('\n')
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
module.exports = {
|
|
296
|
+
PENDING_PATH,
|
|
297
|
+
LONG_LINE_THRESHOLD,
|
|
298
|
+
LONG_ITEM_THRESHOLD,
|
|
299
|
+
detectLongFormPrompt,
|
|
300
|
+
hasSourceLink,
|
|
301
|
+
hasTaskSignals,
|
|
302
|
+
isChannelDispatchInWorker,
|
|
303
|
+
shouldForceExtractReview,
|
|
304
|
+
buildEnforcementMessage,
|
|
305
|
+
markLongInputPending,
|
|
306
|
+
clearLongInputPending,
|
|
307
|
+
isLongInputPending,
|
|
308
|
+
readLongInputPending,
|
|
309
|
+
checkLongInputPendingGate,
|
|
310
|
+
countDiscreteItems
|
|
311
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — PreToolUse Gate Dependency Loader (audit Story 9 / wf-5e94e2c0)
|
|
5
|
+
*
|
|
6
|
+
* Extracted from scripts/hooks/entry/claude-code/pre-tool-use.js to
|
|
7
|
+
* comply with the hook three-layer rule (entry files ≤120 LOC). Owns
|
|
8
|
+
* the defensive lazy-load logic for every gate the orchestrator
|
|
9
|
+
* dispatches to — fail-open shims when modules are absent in older
|
|
10
|
+
* installs, stderr WARNING for the research-evidence-gate-specific
|
|
11
|
+
* deployment-issue case (CL-004).
|
|
12
|
+
*
|
|
13
|
+
* Architect plan: .workflow/changes/wf-5e94e2c0-architect.md
|
|
14
|
+
*
|
|
15
|
+
* Public surface (single function):
|
|
16
|
+
* loadGateDeps() → deps object for runPreToolGates(input, deps)
|
|
17
|
+
*
|
|
18
|
+
* Cross-Story Tier-3 Rule (P11.4) compliance: the upstream contract is
|
|
19
|
+
* `runPreToolGates`'s deps shape. Tests in
|
|
20
|
+
* tests/flow-hooks-pre-tool-deps.test.js pin the exact key set + shim
|
|
21
|
+
* shapes; the broader pre-tool-orchestrator.test.js exercises the
|
|
22
|
+
* dispatch path end-to-end as the Tier-3 integration check.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const _noop = () => ({ allowed: true, blocked: false });
|
|
26
|
+
const _phaseNoop = () => ({ blocked: false });
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build the dependency object expected by runPreToolGates(input, deps).
|
|
30
|
+
*
|
|
31
|
+
* Each gate is loaded inside its own try/catch so a missing module in an
|
|
32
|
+
* older install fails open (no-op shim) rather than crashing the entire
|
|
33
|
+
* PreToolUse pipeline. Behavior preserved verbatim from the prior inline
|
|
34
|
+
* implementation in pre-tool-use.js.
|
|
35
|
+
*
|
|
36
|
+
* @returns {Object} deps shape consumed by pre-tool-orchestrator
|
|
37
|
+
*/
|
|
38
|
+
function loadGateDeps() {
|
|
39
|
+
// Always-required core gates (these are part of the canonical install;
|
|
40
|
+
// crash on load failure is acceptable since the install itself is broken).
|
|
41
|
+
const { checkScopeGate } = require('./scope-gate');
|
|
42
|
+
const { checkComponentReuse } = require('./component-check');
|
|
43
|
+
const { checkTodoWriteGate } = require('./todowrite-gate');
|
|
44
|
+
const { checkRoutingGate, clearRoutingPending, hasActiveTask } = require('./routing-gate');
|
|
45
|
+
const { checkPhaseGate } = require('./phase-gate');
|
|
46
|
+
const { checkCommitLogGate } = require('./commit-log-gate');
|
|
47
|
+
|
|
48
|
+
// Defensive lazy-loaders — fail-open with shim if module absent
|
|
49
|
+
let recordPhaseRead = () => {};
|
|
50
|
+
let checkPhaseReadGate = _phaseNoop;
|
|
51
|
+
let clearPhaseReads = () => {};
|
|
52
|
+
try {
|
|
53
|
+
const prg = require('./phase-read-gate');
|
|
54
|
+
recordPhaseRead = prg.recordPhaseRead;
|
|
55
|
+
checkPhaseReadGate = prg.checkPhaseReadGate;
|
|
56
|
+
clearPhaseReads = prg.clearPhaseReads;
|
|
57
|
+
} catch (_err) {
|
|
58
|
+
if (process.env.DEBUG) console.error(`[Hook] Phase-read gate not loaded: ${_err.message}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let recordEvidenceRead = () => {};
|
|
62
|
+
let checkSpecWriteGate = _phaseNoop;
|
|
63
|
+
let clearResearchEvidence = () => {};
|
|
64
|
+
try {
|
|
65
|
+
const reg = require('./research-evidence-gate');
|
|
66
|
+
recordEvidenceRead = reg.recordEvidenceRead;
|
|
67
|
+
checkSpecWriteGate = reg.checkSpecWriteGate;
|
|
68
|
+
clearResearchEvidence = reg.clearResearchEvidence;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// CL-004: load failure for a gate file that SHOULD be present is a
|
|
71
|
+
// deployment issue worth surfacing without DEBUG. Silently shimming
|
|
72
|
+
// masks broken installs. Fail-open shims above keep the pipeline
|
|
73
|
+
// working; stderr makes the operator aware.
|
|
74
|
+
console.error(`[Hook] WARNING: Research-evidence gate failed to load — gate is disabled. ${err.message}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let checkDeployGate = _noop;
|
|
78
|
+
let checkWriteBlock = _noop;
|
|
79
|
+
try {
|
|
80
|
+
const dg = require('./deploy-gate');
|
|
81
|
+
checkDeployGate = dg.checkDeployGate;
|
|
82
|
+
checkWriteBlock = dg.checkWriteBlock;
|
|
83
|
+
} catch (_err) {
|
|
84
|
+
if (process.env.DEBUG) console.error(`[Hook] Deploy gate not loaded: ${_err.message}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let checkStrikeGate = _noop;
|
|
88
|
+
try {
|
|
89
|
+
checkStrikeGate = require('./strike-gate').checkStrikeGate;
|
|
90
|
+
} catch (_err) {
|
|
91
|
+
if (process.env.DEBUG) console.error(`[Hook] Strike gate not loaded: ${_err.message}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let checkBugfixScope = _noop;
|
|
95
|
+
try {
|
|
96
|
+
checkBugfixScope = require('./bugfix-scope-gate').checkBugfixScope;
|
|
97
|
+
} catch (_err) {
|
|
98
|
+
if (process.env.DEBUG) console.error(`[Hook] Bugfix scope gate not loaded: ${_err.message}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let checkScopeMutation = _noop;
|
|
102
|
+
try {
|
|
103
|
+
checkScopeMutation = require('./scope-mutation-gate').checkScopeMutation;
|
|
104
|
+
} catch (_err) {
|
|
105
|
+
if (process.env.DEBUG) console.error(`[Hook] Scope mutation gate not loaded: ${_err.message}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let checkGitSafety = _noop;
|
|
109
|
+
try {
|
|
110
|
+
checkGitSafety = require('./git-safety-gate').checkGitSafety;
|
|
111
|
+
} catch (_err) {
|
|
112
|
+
if (process.env.DEBUG) console.error(`[Hook] Git safety gate not loaded: ${_err.message}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let checkManagerBoundary = _noop;
|
|
116
|
+
try {
|
|
117
|
+
checkManagerBoundary = require('./manager-boundary-gate').checkManagerBoundary;
|
|
118
|
+
} catch (_err) {
|
|
119
|
+
if (process.env.DEBUG) console.error(`[Hook] Manager boundary gate not loaded: ${_err.message}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let checkWorkerBoundary = _noop;
|
|
123
|
+
let checkPathDiscipline = _noop;
|
|
124
|
+
try {
|
|
125
|
+
const wbg = require('./worker-boundary-gate');
|
|
126
|
+
checkWorkerBoundary = wbg.checkWorkerBoundary;
|
|
127
|
+
checkPathDiscipline = wbg.checkPathDiscipline;
|
|
128
|
+
} catch (_err) {
|
|
129
|
+
if (process.env.DEBUG) console.error(`[Hook] Worker boundary gate not loaded: ${_err.message}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Long-input-pending gate (P11.6 mechanical layer). Consults the marker
|
|
133
|
+
// file written by user-prompt-submit when a long-form prompt arrives
|
|
134
|
+
// without a source-link, and blocks mutating tools until the AI either
|
|
135
|
+
// runs /wogi-extract-review or dismisses the marker explicitly.
|
|
136
|
+
let checkLongInputPendingGate = _noop;
|
|
137
|
+
try {
|
|
138
|
+
checkLongInputPendingGate = require('./long-input-enforcement').checkLongInputPendingGate;
|
|
139
|
+
} catch (_err) {
|
|
140
|
+
if (process.env.DEBUG) console.error(`[Hook] Long-input-pending gate not loaded: ${_err.message}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// CLI-agnostic helpers (not gates per se but consumed by the orchestrator)
|
|
144
|
+
const { markSkillPending } = require('../../flow-durable-session');
|
|
145
|
+
const { getConfig } = require('../../flow-utils');
|
|
146
|
+
const { readHookStatus } = require('../../flow-hook-status');
|
|
147
|
+
|
|
148
|
+
// Strict adherence — lazy to avoid circular deps + startup cost
|
|
149
|
+
let _strictAdherence = null;
|
|
150
|
+
function getStrictAdherence() {
|
|
151
|
+
if (!_strictAdherence) {
|
|
152
|
+
try {
|
|
153
|
+
_strictAdherence = require('../../flow-strict-adherence');
|
|
154
|
+
} catch (_err) {
|
|
155
|
+
_strictAdherence = {
|
|
156
|
+
isEnabled: () => false,
|
|
157
|
+
validateCommand: () => ({ valid: true }),
|
|
158
|
+
validateFileName: () => ({ valid: true })
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return _strictAdherence;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
// Gates
|
|
167
|
+
checkScopeGate, checkComponentReuse, checkTodoWriteGate,
|
|
168
|
+
checkRoutingGate, clearRoutingPending, hasActiveTask,
|
|
169
|
+
checkPhaseGate, checkCommitLogGate,
|
|
170
|
+
recordPhaseRead, checkPhaseReadGate, clearPhaseReads,
|
|
171
|
+
recordEvidenceRead, checkSpecWriteGate, clearResearchEvidence,
|
|
172
|
+
checkDeployGate, checkWriteBlock,
|
|
173
|
+
checkStrikeGate, checkBugfixScope, checkScopeMutation,
|
|
174
|
+
checkGitSafety, checkManagerBoundary, checkWorkerBoundary, checkPathDiscipline,
|
|
175
|
+
checkLongInputPendingGate,
|
|
176
|
+
// Side-effect helpers
|
|
177
|
+
markSkillPending,
|
|
178
|
+
// Config + runtime
|
|
179
|
+
getConfig, readHookStatus, getStrictAdherence
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
loadGateDeps
|
|
185
|
+
};
|
|
@@ -41,6 +41,7 @@ const { parseSubagentContext, isAllGatesDisabled } = require('./pre-tool-helpers
|
|
|
41
41
|
* - checkDeployGate, checkWriteBlock
|
|
42
42
|
* - checkStrikeGate, checkBugfixScope, checkScopeMutation
|
|
43
43
|
* - checkGitSafety, checkManagerBoundary
|
|
44
|
+
* - checkLongInputPendingGate
|
|
44
45
|
* - markSkillPending, getConfig, readHookStatus
|
|
45
46
|
* - getStrictAdherence
|
|
46
47
|
* @returns {Object} coreResult
|
|
@@ -261,6 +262,27 @@ function runPreToolGates(ctx, deps) {
|
|
|
261
262
|
}
|
|
262
263
|
}
|
|
263
264
|
|
|
265
|
+
// Long-input-pending gate (P11.6 mechanical layer): if the prior
|
|
266
|
+
// UserPromptSubmit hook flagged this prompt as long-form-without-source-link
|
|
267
|
+
// and wrote the pending marker, block any mutating tool until extract-review
|
|
268
|
+
// runs or the user explicitly dismisses. This is the structural enforcement
|
|
269
|
+
// layer for the wogi-hub 2026-04-27 manager-compression failure.
|
|
270
|
+
if (typeof deps.checkLongInputPendingGate === 'function') {
|
|
271
|
+
try {
|
|
272
|
+
const liResult = deps.checkLongInputPendingGate(toolName, toolInput);
|
|
273
|
+
if (liResult.blocked) {
|
|
274
|
+
return {
|
|
275
|
+
allowed: false,
|
|
276
|
+
blocked: true,
|
|
277
|
+
reason: liResult.reason,
|
|
278
|
+
message: liResult.message,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
} catch (err) {
|
|
282
|
+
if (process.env.DEBUG) console.error(`[Hook] Long-input-pending gate error (fail-open): ${err.message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
264
286
|
// Path-discipline gate (Story B / wf-ab59f0e4): runs in BOTH manager and
|
|
265
287
|
// worker mode (different rules each side). Cross-process state writes are
|
|
266
288
|
// blocked fail-loud so file corruption is impossible to ignore.
|
|
@@ -852,6 +852,32 @@ function formatContextForInjection(context) {
|
|
|
852
852
|
const ctx = context.context;
|
|
853
853
|
let output = '## Wogi Flow Session Context\n\n';
|
|
854
854
|
|
|
855
|
+
// One-time prompt-cache flag tip (2026-04-26).
|
|
856
|
+
//
|
|
857
|
+
// Triggered when: ANTHROPIC_API_KEY is set (user is on API-key /
|
|
858
|
+
// Bedrock / Vertex / Foundry — NOT a Claude Pro/Max subscriber via
|
|
859
|
+
// OAuth) AND ENABLE_PROMPT_CACHING_1H is NOT set. These users default
|
|
860
|
+
// to a 5-min prompt-cache TTL — any pause >5min mid-session pays the
|
|
861
|
+
// full re-prompt cost on resume. Setting the env var bumps to 1h,
|
|
862
|
+
// matching what subscribers already get by default.
|
|
863
|
+
//
|
|
864
|
+
// Fires AT MOST ONCE per project (marker-cleared after first surface).
|
|
865
|
+
// ~40 tokens of one-time cost vs. potentially massive recurring savings
|
|
866
|
+
// for affected users. No surfacing for subscribers (the var is a no-op
|
|
867
|
+
// for them).
|
|
868
|
+
try {
|
|
869
|
+
const cacheTipMarker = path.join(PATHS.state, 'cache-tip-surfaced.json');
|
|
870
|
+
if (process.env.ANTHROPIC_API_KEY && !process.env.ENABLE_PROMPT_CACHING_1H && !fs.existsSync(cacheTipMarker)) {
|
|
871
|
+
output += `### 💡 Prompt-cache tip (one-time)\n`;
|
|
872
|
+
output += `Detected \`ANTHROPIC_API_KEY\` set without \`ENABLE_PROMPT_CACHING_1H=1\`. On API-key / Bedrock / Vertex / Foundry, default cache TTL is 5 min — any pause >5 min in this session pays the full re-prompt cost on resume. Subscribers (Claude Pro/Max via OAuth) already get 1h TTL by default.\n`;
|
|
873
|
+
output += `\nRecommended: \`export ENABLE_PROMPT_CACHING_1H=1\` in your shell profile. Materially reduces token cost on multi-hour WogiFlow sessions. Risk: none — silently ignored if not supported.\n\n`;
|
|
874
|
+
try {
|
|
875
|
+
fs.mkdirSync(path.dirname(cacheTipMarker), { recursive: true });
|
|
876
|
+
fs.writeFileSync(cacheTipMarker, JSON.stringify({ surfacedAt: new Date().toISOString() }));
|
|
877
|
+
} catch (_err) { /* non-critical — tip will surface again next session if marker write fails */ }
|
|
878
|
+
}
|
|
879
|
+
} catch (_err) { /* fail-open — never block session start over a tip */ }
|
|
880
|
+
|
|
855
881
|
// Post-restart continuity note (wf-39e9dc09 — Stop-hook triggered restart)
|
|
856
882
|
// If the most recent session in session-history.json was ended by
|
|
857
883
|
// task-boundary-restart and happened very recently, surface the resume
|
|
@@ -192,6 +192,19 @@ function checkPreconditions() {
|
|
|
192
192
|
return { ready: false, reason: 'no-parent-pid' };
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
// SEC-006 fix (2026-04-26): cross-check that WOGI_WRAPPER_PID actually
|
|
196
|
+
// refers to our parent. Without this, an attacker who can set the env
|
|
197
|
+
// var (e.g. via co-tenant on a shared CI runner, or a malicious
|
|
198
|
+
// CLAUDE.md that gets persisted into shell init) could cause the Stop
|
|
199
|
+
// hook to SIGTERM an arbitrary same-uid PID. Mismatch = abort.
|
|
200
|
+
const wrapperPidNum = parseInt(wrapperPid, 10);
|
|
201
|
+
if (!Number.isFinite(wrapperPidNum) || wrapperPidNum !== parentPid) {
|
|
202
|
+
return {
|
|
203
|
+
ready: false,
|
|
204
|
+
reason: `parent-pid-mismatch (env WOGI_WRAPPER_PID=${wrapperPid} ≠ ppid=${parentPid})`
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
195
208
|
return { ready: true, flagPath, parentPid };
|
|
196
209
|
} catch (err) {
|
|
197
210
|
return { ready: false, reason: `config-error: ${err.message}` };
|