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.
Files changed (33) hide show
  1. package/.claude/docs/intent-grounded-reasoning.md +1 -1
  2. package/.workflow/templates/partials/methodology-rules.hbs +30 -1
  3. package/lib/commands/team-connection.js +5 -28
  4. package/lib/utils.js +12 -26
  5. package/lib/wogi-claude +40 -1
  6. package/lib/workspace.js +6 -13
  7. package/package.json +2 -2
  8. package/scripts/flow +4 -0
  9. package/scripts/flow-autonomous-detector.js +29 -4
  10. package/scripts/flow-autonomous-mode.js +27 -7
  11. package/scripts/flow-completion-summary.js +2 -16
  12. package/scripts/flow-id.js +31 -0
  13. package/scripts/flow-io.js +78 -0
  14. package/scripts/flow-long-input-pending.js +110 -0
  15. package/scripts/flow-long-input-stories.js +8 -0
  16. package/scripts/flow-orchestrate.js +16 -10
  17. package/scripts/flow-question-queue.js +73 -7
  18. package/scripts/flow-scanner-base.js +77 -1
  19. package/scripts/flow-session-state.js +47 -0
  20. package/scripts/flow-source-fidelity.js +279 -0
  21. package/scripts/flow-time-format.js +42 -0
  22. package/scripts/flow-utils.js +3 -16
  23. package/scripts/flow-worker-mcp-strip.js +12 -11
  24. package/scripts/flow-workspace-summary.js +38 -19
  25. package/scripts/hooks/adapters/claude-code.js +7 -4
  26. package/scripts/hooks/core/long-input-enforcement.js +311 -0
  27. package/scripts/hooks/core/pre-tool-deps.js +185 -0
  28. package/scripts/hooks/core/pre-tool-orchestrator.js +22 -0
  29. package/scripts/hooks/core/session-context.js +26 -0
  30. package/scripts/hooks/core/task-boundary-reset.js +13 -0
  31. package/scripts/hooks/core/worker-boundary-gate.js +67 -16
  32. package/scripts/hooks/entry/claude-code/pre-tool-use.js +21 -95
  33. 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}` };