wogiflow 2.30.1 → 2.30.2

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/lib/installer.js CHANGED
@@ -583,6 +583,36 @@ function createWorkflowStructure(projectRoot, config) {
583
583
  }
584
584
  }
585
585
 
586
+ // wf-d5fcb880 (H2): scaffold forbidden-patterns.json from its template.
587
+ // The template ships in the npm package (per package.json `files` →
588
+ // `.workflow/state/*.template`). Without this step, the standards-checker's
589
+ // forbidden-patterns feature is a silent no-op on fresh installs — the loader
590
+ // returns [] because no rule pack exists on disk. The previous review flagged
591
+ // this as a HIGH finding (H2 in last-review.json).
592
+ const forbiddenPath = path.join(workflowDir, 'state', 'forbidden-patterns.json');
593
+ if (!fs.existsSync(forbiddenPath)) {
594
+ const templatePath = path.join(workflowDir, 'state', 'forbidden-patterns.json.template');
595
+ if (fs.existsSync(templatePath)) {
596
+ try {
597
+ const templateContent = fs.readFileSync(templatePath, 'utf-8');
598
+ fs.writeFileSync(forbiddenPath, templateContent);
599
+ } catch (err) {
600
+ // Fall back to an empty array so the file exists and is parseable.
601
+ // safeJsonParse on this returns [] (valid empty pack); loader behaves
602
+ // identically to the no-file path but the user no longer sees the
603
+ // "forbidden-patterns.json not found" stderr warning.
604
+ if (process.env.DEBUG) {
605
+ console.error(`[installer] forbidden-patterns template copy failed: ${err.message}`);
606
+ }
607
+ fs.writeFileSync(forbiddenPath, '[]\n');
608
+ }
609
+ } else {
610
+ // Template not shipped (unusual install layout) — still scaffold an
611
+ // empty pack so loader is silent.
612
+ fs.writeFileSync(forbiddenPath, '[]\n');
613
+ }
614
+ }
615
+
586
616
  // Create registry manifest (dynamic registry discovery)
587
617
  const registryManifestPath = path.join(workflowDir, 'state', 'registry-manifest.json');
588
618
  if (!fs.existsSync(registryManifestPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.30.1",
3
+ "version": "2.30.2",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -22,12 +22,17 @@ if (command === 'transition') {
22
22
  console.error('Usage: flow-phase.js transition <from> <to> [taskId]');
23
23
  process.exit(1);
24
24
  }
25
- if (!isPhaseGateEnabled()) {
26
- process.exit(0); // Silent no-op when disabled
27
- }
25
+ // wf-88a08fd4: previously this exited silently when `phaseGate.enabled` was
26
+ // false, which is the default. The CLI is an explicit caller action — honor
27
+ // it even when gate enforcement is off. State tracking (workflow-phase.json)
28
+ // is independent of gate enforcement (blocking Edit/Write until phase file
29
+ // is read). Callers that depend on phase state always need the write; the
30
+ // gate flag only controls whether PreToolUse blocks tools.
31
+ const gateActive = isPhaseGateEnabled();
28
32
  const success = transitionPhase(from, to, taskId || null);
29
33
  if (success) {
30
- console.log(`Phase: ${from} ${to}`);
34
+ const suffix = gateActive ? '' : ' (gate enforcement disabled — state updated only)';
35
+ console.log(`Phase: ${from} → ${to}${suffix}`);
31
36
  // wf-8d635d0e / E1: fire background auto-review on coding → validating.
32
37
  // Fails open — any error here must not fail the primary transition.
33
38
  try {
@@ -169,6 +169,21 @@ function inferTaskType(taskType, changedFiles) {
169
169
  * @returns {Object} Check results with feedback for retry loop
170
170
  */
171
171
  function runTaskStandardsCheck(taskContext, files, options = {}) {
172
+ // Defensive normalization: callers (flow-done-gates.js → ctx.getModifiedFiles())
173
+ // pass a string[] of file paths; this function documents Object[] with
174
+ // {path, content}. Pre-fix, `files.map(f => f.path)` returned [undefined,...]
175
+ // which then crashed downstream `.includes()` on undefined. The crash was
176
+ // swallowed by the caller's try/catch and surfaced as "checker error —
177
+ // degraded to manual check" on every `flow done`. Normalize here so both
178
+ // shapes work; content-dependent checks skip strings gracefully (downstream
179
+ // already does `if (!file.content) continue;`).
180
+ if (!Array.isArray(files)) {
181
+ files = [];
182
+ } else {
183
+ files = files.map(f => (typeof f === 'string' ? { path: f, content: undefined } : f))
184
+ .filter(f => f && typeof f.path === 'string');
185
+ }
186
+
172
187
  const config = getConfig();
173
188
  const standardsConfig = config.standardsCompliance || {};
174
189
  const gateStartMs = Date.now();
@@ -421,16 +421,31 @@ Run: /wogi-start ${coreResult.nextTaskId}`;
421
421
  };
422
422
  }
423
423
 
424
- // Compose additionalContext from up to five pieces:
425
- // 1. longInputEnforcement (P11.5 — placed FIRST so AI sees the
426
- // forcing instruction before anything else)
427
- // 2. systemReminder (research protocol) OR message (warning)
428
- // 3. phasePrompt (phase-specific context)
429
- // 4. overduePrompt (wf-d3e67abe silent-halt surfacing, manager-only)
424
+ // wf-35742353 Gate-orchestrator integration.
425
+ //
426
+ // Previously this concatenated up to five pieces blindly, producing a
427
+ // wall of competing system-reminder text ("invoke /wogi-extract-review"
428
+ // + "research protocol" + phase context, etc.) that the AI/user could
429
+ // not triage. Now: REMEDIATIONS (gates demanding action) go through
430
+ // the gate-orchestrator which picks the highest-priority one and lists
431
+ // the rest as a one-line "queued" footer. INFO pieces (phase context,
432
+ // overdue dispatches) pass through unchanged alongside the top
433
+ // remediation. Fail-open: any error falls back to the prior concat.
430
434
  const pieces = [];
431
- if (coreResult.longInputEnforcement) pieces.push(coreResult.longInputEnforcement);
432
- if (coreResult.systemReminder) pieces.push(coreResult.systemReminder);
433
- else if (coreResult.message) pieces.push(coreResult.message);
435
+ try {
436
+ const { selectAndRender } = require('../core/gate-orchestrator');
437
+ const remediation = selectAndRender({
438
+ 'long-input-pending': coreResult.longInputEnforcement,
439
+ 'research-required': coreResult.systemReminder || coreResult.message
440
+ });
441
+ if (remediation) pieces.push(remediation);
442
+ } catch (_err) {
443
+ // Fallback to prior concat shape if orchestrator misfires.
444
+ if (coreResult.longInputEnforcement) pieces.push(coreResult.longInputEnforcement);
445
+ if (coreResult.systemReminder) pieces.push(coreResult.systemReminder);
446
+ else if (coreResult.message) pieces.push(coreResult.message);
447
+ }
448
+ // Info pieces always pass through.
434
449
  if (coreResult.phasePrompt) pieces.push(coreResult.phasePrompt);
435
450
  if (coreResult.overduePrompt) pieces.push(coreResult.overduePrompt);
436
451
 
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Wogi Flow — Gate Orchestrator (wf-35742353)
5
+ *
6
+ * Cross-gate priority and remediation surfacing. Each gate (long-input-pending,
7
+ * routing, research-required, phase-context, overdue-dispatches, etc.) was
8
+ * designed assuming it was the only voice in the room. At the integration
9
+ * point (UserPromptSubmit additionalContext and Stop hook stopReason) they
10
+ * collide and produce conflicting "do this NOW" instructions in the same turn.
11
+ *
12
+ * This module owns the priority order and the picker. The hook entry files
13
+ * and adapters call it instead of stacking messages.
14
+ *
15
+ * Priority (highest first):
16
+ * 1. long-input-pending — user's prompt isn't captured; downstream is
17
+ * suspect. Resolve before anything else.
18
+ * 2. routing — no task assigned; work would be untracked.
19
+ * 3. research-required — diagnostic prompt needs evidence-reading.
20
+ * 4. workspace-overdue — silent worker death surfacing (manager-only).
21
+ * 5. phase-context — informational phase-prompt injection (not a demand).
22
+ *
23
+ * Categories:
24
+ * - "remediation": the AI must take a specific action (resolve before
25
+ * proceeding). At most ONE remediation is surfaced per turn — the
26
+ * highest-priority active one wins; others get a one-line "queued"
27
+ * footer so the AI knows more work follows.
28
+ * - "info": informational, always passes through alongside the top
29
+ * remediation. Examples: phase-context, dossier injection.
30
+ *
31
+ * Fail-open philosophy: if classification or formatting errors, return
32
+ * the original message stack unchanged (caller fallback).
33
+ */
34
+
35
+ const REMEDIATION_PRIORITY = Object.freeze([
36
+ 'long-input-pending',
37
+ 'routing',
38
+ 'research-required',
39
+ 'workspace-overdue'
40
+ ]);
41
+
42
+ const REMEDIATION_LABELS = Object.freeze({
43
+ 'long-input-pending': 'long-input-pending (invoke /wogi-extract-review or `flow long-input-pending dismiss`)',
44
+ 'routing': 'routing (invoke /wogi-start)',
45
+ 'research-required': 'research-required (read evidence before answering)',
46
+ 'workspace-overdue': 'workspace-overdue (a worker dispatch is past its deadline)'
47
+ });
48
+
49
+ /**
50
+ * Pick the top-priority active remediation from a set of (gateId, message) pairs.
51
+ *
52
+ * @param {Array<{id: string, message: string}>} active - gates currently demanding action
53
+ * @returns {{ top: {id, message}|null, queued: string[] }}
54
+ * top: the highest-priority active gate (null if none)
55
+ * queued: gateIds of the others (in priority order), for footer rendering
56
+ */
57
+ function pickTopRemediation(active) {
58
+ if (!Array.isArray(active) || active.length === 0) {
59
+ return { top: null, queued: [] };
60
+ }
61
+ // Filter to valid entries and sort by priority index.
62
+ const valid = active.filter(g => g && typeof g.id === 'string' && typeof g.message === 'string' && g.message.trim().length > 0);
63
+ if (valid.length === 0) return { top: null, queued: [] };
64
+
65
+ const indexed = valid.map(g => ({ ...g, idx: REMEDIATION_PRIORITY.indexOf(g.id) }))
66
+ .map(g => ({ ...g, idx: g.idx === -1 ? Number.POSITIVE_INFINITY : g.idx }));
67
+ indexed.sort((a, b) => a.idx - b.idx);
68
+
69
+ const top = { id: indexed[0].id, message: indexed[0].message };
70
+ const queued = indexed.slice(1).map(g => g.id);
71
+ return { top, queued };
72
+ }
73
+
74
+ /**
75
+ * Render the top remediation message with a one-line footer listing queued
76
+ * remediations. If no others are queued, returns the top message unchanged.
77
+ */
78
+ function renderRemediation(top, queued) {
79
+ if (!top || typeof top.message !== 'string') return '';
80
+ if (!Array.isArray(queued) || queued.length === 0) return top.message;
81
+ const labels = queued.map(id => REMEDIATION_LABELS[id] || id).join('; ');
82
+ return `${top.message}\n\n[gate-orchestrator] Also queued (resolve after the above): ${labels}`;
83
+ }
84
+
85
+ /**
86
+ * Convenience: take a map of {gateId: message-or-null} and return the rendered
87
+ * top remediation (or empty string when nothing is active).
88
+ */
89
+ function selectAndRender(gateMap) {
90
+ if (!gateMap || typeof gateMap !== 'object') return '';
91
+ const active = Object.entries(gateMap)
92
+ .filter(([_id, msg]) => typeof msg === 'string' && msg.trim().length > 0)
93
+ .map(([id, message]) => ({ id, message }));
94
+ const { top, queued } = pickTopRemediation(active);
95
+ return renderRemediation(top, queued);
96
+ }
97
+
98
+ module.exports = {
99
+ REMEDIATION_PRIORITY,
100
+ REMEDIATION_LABELS,
101
+ pickTopRemediation,
102
+ renderRemediation,
103
+ selectAndRender
104
+ };
@@ -94,6 +94,47 @@ function hasSourceLink(text) {
94
94
  return SOURCE_LINK_PATTERNS.some(re => re.test(text));
95
95
  }
96
96
 
97
+ // Known system-content tag prefixes that arrive via UserPromptSubmit but are
98
+ // NOT user-typed input. Sub-agent task-notifications, system reminders, and
99
+ // slash-command framings all flow through the same hook. Treating them as
100
+ // user prompts is the wf-f7d58760 false-positive shape — sub-agent
101
+ // completions tripped the long-input gate and force-blocked the parent
102
+ // session with no recoverable path (catch-22 documented in the bug).
103
+ const SYSTEM_CONTENT_PREFIXES = [
104
+ '<task-notification>',
105
+ '<system-reminder>',
106
+ '<command-message>',
107
+ '<command-name>',
108
+ '<command-args>',
109
+ '<command-output>',
110
+ '<command-stderr>',
111
+ '<local-command-stdout>',
112
+ '<local-command-stderr>',
113
+ '<user-prompt-submit-hook>',
114
+ '<bash-input>',
115
+ '<bash-stdout>',
116
+ '<bash-stderr>'
117
+ ];
118
+
119
+ /**
120
+ * Detect content that originates from the system (tool results, sub-agent
121
+ * notifications, slash-command framings) rather than user typing. These
122
+ * arrive via UserPromptSubmit in some Claude Code paths but should never
123
+ * trip the long-input gate — they aren't requests, and the user can't
124
+ * "preserve their verbatim source" because the user didn't author them.
125
+ *
126
+ * Detection: leading non-whitespace begins with a known system tag prefix.
127
+ * Conservative — only matches if the tag is the FIRST thing in the text.
128
+ */
129
+ function isSystemOriginatedContent(text) {
130
+ if (typeof text !== 'string') return false;
131
+ const lead = text.trimStart().slice(0, 64);
132
+ for (const prefix of SYSTEM_CONTENT_PREFIXES) {
133
+ if (lead.startsWith(prefix)) return true;
134
+ }
135
+ return false;
136
+ }
137
+
97
138
  function hasTaskSignals(text) {
98
139
  if (typeof text !== 'string') return false;
99
140
  let imperativeHits = 0;
@@ -129,6 +170,12 @@ function isChannelDispatchInWorker(source, env = process.env) {
129
170
  * @returns {{forced: boolean, level: 'strict'|'force'|'suggest'|'pass', reason: string}}
130
171
  */
131
172
  function shouldForceExtractReview({ text, source, env = process.env } = {}) {
173
+ // wf-f7d58760: system-originated content (sub-agent task-notifications,
174
+ // tool-result echoes, slash-command framings) flows through the same
175
+ // UserPromptSubmit pipe but is NOT a user prompt. Skip the gate entirely.
176
+ if (isSystemOriginatedContent(text)) {
177
+ return { forced: false, level: 'pass', reason: 'system-originated-content' };
178
+ }
132
179
  if (!detectLongFormPrompt(text)) {
133
180
  return { forced: false, level: 'pass', reason: 'below-long-input-threshold' };
134
181
  }
@@ -297,9 +344,11 @@ module.exports = {
297
344
  PENDING_PATH,
298
345
  LONG_LINE_THRESHOLD,
299
346
  LONG_ITEM_THRESHOLD,
347
+ SYSTEM_CONTENT_PREFIXES,
300
348
  detectLongFormPrompt,
301
349
  hasSourceLink,
302
350
  hasTaskSignals,
351
+ isSystemOriginatedContent,
303
352
  isChannelDispatchInWorker,
304
353
  shouldForceExtractReview,
305
354
  buildEnforcementMessage,
@@ -47,7 +47,11 @@ const DIAGNOSTIC_PATTERNS = [
47
47
  /\bwhich\s+(approach|option|way|one)\s+(is\s+better|should|do\s+you)\b/i,
48
48
  /\bis\s+it\s+better\s+to\b/i,
49
49
  /\bwhat'?s?\s+the\s+(right|best|correct)\s+(approach|way)\b/i,
50
- /\brecommend\b/i,
50
+ // Imperative "recommend" — anchored to prompt start or after sentence end.
51
+ // Prior version `/\brecommend\b/i` matched ANY occurrence and false-fired on
52
+ // "the recommendation system is broken" / "I recommend doing X" (statements,
53
+ // not questions). See wf-12271e82.
54
+ /(?:^|[.?!]\s+)\s*(please\s+|can\s+you\s+|could\s+you\s+|would\s+you\s+)?recommend\b/i,
51
55
  /\bdid\s+you\s+(fix|address|verify|check|test|handle)\b/i,
52
56
  /\bdo\s+you\s+(think|recommend|suggest)\b/i,
53
57
  ];
@@ -7,6 +7,45 @@
7
7
  * Checks if there's an active task before allowing implementation actions.
8
8
  *
9
9
  * Returns a standardized result that adapters transform for specific CLIs.
10
+ *
11
+ * --------------------------------------------------------------------------
12
+ * "Active task" state-source contract (wf-c573961f, 2026-05-11)
13
+ * --------------------------------------------------------------------------
14
+ *
15
+ * The task-gate consults FOUR state sources to determine if a task is active.
16
+ * External tooling that mutates task state MUST satisfy this contract or the
17
+ * gate will reject Edits/Writes with `no_active_task` or `task_missing_routing_proof`.
18
+ *
19
+ * 1. `.workflow/state/ready.json` → `inProgress[]`
20
+ * The task object must be the first element of inProgress, with a valid
21
+ * `wf-XXXXXXXX` id (validateTaskId).
22
+ *
23
+ * 2. `.workflow/state/durable-session.json` → `{ taskId, status: 'active' }`
24
+ * Fallback path when inProgress is empty. Also requires routing proof.
25
+ *
26
+ * 3. Task object `routedAt` field (ISO timestamp)
27
+ * Set automatically by `flow start` / `createQuickTask` / `moveTaskAsync`
28
+ * when a task legitimately enters inProgress.
29
+ *
30
+ * 4. `.workflow/state/.routing-receipt-<taskId>` (dotfile, JSON)
31
+ * Anti-bypass receipt written by the same callers that set `routedAt`.
32
+ * The dual mechanism is defense-in-depth: AI editing ready.json directly
33
+ * can spoof `routedAt`, but the receipt file is harder to forge silently.
34
+ *
35
+ * The gate accepts the task if EITHER (3) routedAt is set, OR (4) the
36
+ * receipt file exists, OR the task has a legacy `startedAt` field (pre-
37
+ * routedAt migration). One of these MUST be true; otherwise the task is
38
+ * rejected as "manually inserted".
39
+ *
40
+ * Documented helpers for external tooling:
41
+ * - `writeRoutingReceipt(taskId, options)` — produces a valid receipt for
42
+ * a task that's already in inProgress (e.g., after `flow bug` then a
43
+ * manual move). This is the documented "I know what I'm doing" escape
44
+ * hatch that closes the 2026-05-10 wogiflow-cli bug-report issue.
45
+ *
46
+ * Single source of truth in the future: see the deferred follow-up epic
47
+ * referenced in wf-c573961f's bug spec ("Approach 1 / consolidate to
48
+ * .workflow/state/active-task.json"). That work is out of L2 scope.
10
49
  */
11
50
 
12
51
  const fs = require('node:fs');
@@ -314,7 +353,7 @@ function checkTaskGate(options = {}, config) {
314
353
  return {
315
354
  allowed: false,
316
355
  blocked: true,
317
- message: `Task ${taskId} is in inProgress but has no routing proof (missing routedAt and no routing receipt file).\n\nThis usually means the task was inserted into ready.json manually instead of through /wogi-start.\n\nTo fix:\n1. Use /wogi-start ${taskId} to properly route this task\n2. Or remove it from inProgress and start fresh: /wogi-ready`,
356
+ message: `Task ${taskId} is in inProgress but has no routing proof (missing routedAt and no routing receipt file).\n\nThis usually means the task was inserted into ready.json manually instead of through /wogi-start.\n\nTo fix:\n1. RECOMMENDED — Use /wogi-start ${taskId} to properly route this task.\n2. Or remove it from inProgress and start fresh: /wogi-ready\n3. Or, if you know what you're doing (e.g. after \`flow bug\` + manual move), write a routing receipt:\n node -e "require('./scripts/hooks/core/task-gate').writeRoutingReceipt('${taskId}', { via: 'manual' })"\n\nSee scripts/hooks/core/task-gate.js header for the "active task" state-source contract.`,
318
357
  reason: 'task_missing_routing_proof'
319
358
  };
320
359
  }
@@ -410,6 +449,42 @@ function checkTaskGate(options = {}, config) {
410
449
  };
411
450
  }
412
451
 
452
+ /**
453
+ * Write a routing receipt for a task. This is the documented external API
454
+ * for satisfying the routing-proof requirement (state source #4) when the
455
+ * task is in inProgress but was placed there by a caller that didn't write
456
+ * a receipt (e.g., manual edit, or `flow bug` then move).
457
+ *
458
+ * Returns `{ ok: true, path }` on success or `{ ok: false, reason }` on
459
+ * failure. Validates taskId format before writing.
460
+ *
461
+ * wf-c573961f: previously this functionality was inlined in createQuickTask
462
+ * and not exported, leaving external tooling no documented way to satisfy
463
+ * the gate. The bug-report author hit exactly this — they updated four
464
+ * state files but couldn't discover the receipt-file requirement.
465
+ *
466
+ * @param {string} taskId - wf-XXXXXXXX format
467
+ * @param {Object} [options]
468
+ * @param {string} [options.via='external'] - audit field stored in the receipt
469
+ * @returns {{ok: boolean, path?: string, reason?: string}}
470
+ */
471
+ function writeRoutingReceipt(taskId, options = {}) {
472
+ if (typeof taskId !== 'string' || !validateTaskId(taskId).valid) {
473
+ return { ok: false, reason: `invalid taskId format: ${taskId}` };
474
+ }
475
+ try {
476
+ const receiptPath = path.join(PATHS.state, `.routing-receipt-${taskId}`);
477
+ fs.writeFileSync(receiptPath, JSON.stringify({
478
+ taskId,
479
+ routedAt: new Date().toISOString(),
480
+ via: options.via || 'external'
481
+ }, null, 2));
482
+ return { ok: true, path: receiptPath };
483
+ } catch (err) {
484
+ return { ok: false, reason: err.message };
485
+ }
486
+ }
487
+
413
488
  /**
414
489
  * Generate warning message (when not blocking)
415
490
  */
@@ -523,6 +598,7 @@ module.exports = {
523
598
  getActiveTask,
524
599
  checkTaskGate,
525
600
  createQuickTask,
601
+ writeRoutingReceipt,
526
602
  generateBlockMessage,
527
603
  generateWarningMessage
528
604
  };
@@ -17,12 +17,26 @@ const { isRoutingPending, incrementStopAttempts } = require('../../core/routing-
17
17
  const { runHook } = require('../shared/hook-runner');
18
18
 
19
19
  runHook('Stop', async ({ parsedInput }) => {
20
+ // wf-35742353 — Gate priority: if long-input-pending is active, it is the
21
+ // top-priority remediation. The UserPromptSubmit hook already surfaced the
22
+ // full long-input message on prompt arrival; firing routing-enforcement
23
+ // and research-required gates now would issue conflicting "do this NOW"
24
+ // instructions in the same turn. Defer the lower-priority Stop-hook gates
25
+ // until long-input-pending is resolved.
26
+ //
27
+ // Fail-open: any error reading the marker falls through to normal gate flow.
28
+ let longInputActive = false;
29
+ try {
30
+ const { isLongInputPending } = require('../../core/long-input-enforcement');
31
+ longInputActive = isLongInputPending();
32
+ } catch (_err) { /* fail-open */ }
33
+
20
34
  // v6.2: Routing enforcement check — catches text-only response bypass
21
35
  // If routing-pending flag is still set when the AI tries to stop, it means
22
36
  // the AI responded to the user's message without ever invoking a /wogi-* command.
23
37
  // This is the exact bypass we need to prevent (especially after context compaction).
24
38
  try {
25
- if (isRoutingPending()) {
39
+ if (isRoutingPending() && !longInputActive) {
26
40
  // Use counter-based approach instead of clearing immediately.
27
41
  // This gives the AI multiple chances to comply before giving up.
28
42
  // Gap 4 fix: clearing immediately made this single-shot protection.
@@ -260,7 +274,15 @@ runHook('Stop', async ({ parsedInput }) => {
260
274
  // turn was classified as diagnostic (Tier 2/3 from CLAUDE.md), check that
261
275
  // the AI made enough Read calls against evidence paths before answering.
262
276
  // If not, re-prompt with a violation message forcing a redo. Fail-open.
277
+ //
278
+ // wf-35742353 — Skip this gate when long-input-pending is active. The user's
279
+ // prompt isn't yet captured, so demanding evidence-reading would issue a
280
+ // conflicting remediation. The diagnostic marker will still be present when
281
+ // long-input resolves; the gate fires correctly then.
263
282
  try {
283
+ if (longInputActive) {
284
+ // skip — defer to long-input remediation
285
+ } else {
264
286
  const { checkResearchRequiredGate } = require('../../core/research-required-gate');
265
287
  const { getConfig } = require('../../../flow-utils');
266
288
  const config = getConfig();
@@ -276,6 +298,7 @@ runHook('Stop', async ({ parsedInput }) => {
276
298
  // Soft re-prompt: force the AI to redo with reads
277
299
  return { __raw: true, continue: true, stopReason: result.message };
278
300
  }
301
+ } // end else (wf-35742353 long-input-active skip)
279
302
  } catch (err) {
280
303
  if (process.env.DEBUG) {
281
304
  console.error(`[Stop] Research-required gate error (fail-open): ${err.message}`);