wogiflow 2.30.0 → 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.
@@ -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}`);
@@ -148,6 +148,31 @@ function createMinimalStructure() {
148
148
  }, null, 2), { mode: FILE_MODE });
149
149
  }
150
150
  }
151
+
152
+ // wf-2eafdab0 (AC2/H2): Create forbidden-patterns.json from template on fresh
153
+ // installs. Idempotent — never overwrites an existing file. Without this, the
154
+ // declarative-forbidden-pattern feature ships as a silent no-op (the v2.30.0
155
+ // bug this commit fixes).
156
+ const forbiddenPatternsPath = path.join(STATE_DIR, 'forbidden-patterns.json');
157
+ if (!fs.existsSync(forbiddenPatternsPath)) {
158
+ const fpTemplate = path.join(STATE_DIR, 'forbidden-patterns.json.template');
159
+ try {
160
+ const rawContent = fs.existsSync(fpTemplate)
161
+ ? fs.readFileSync(fpTemplate, 'utf-8')
162
+ : '[]';
163
+ const parsed = safeJsonParseString(rawContent, []);
164
+ fs.writeFileSync(
165
+ forbiddenPatternsPath,
166
+ JSON.stringify(parsed, null, 2),
167
+ { mode: FILE_MODE }
168
+ );
169
+ } catch (err) {
170
+ if (process.env.DEBUG) {
171
+ console.error(`[postinstall] forbidden-patterns template error: ${err.message}`);
172
+ }
173
+ fs.writeFileSync(forbiddenPatternsPath, '[]\n', { mode: FILE_MODE });
174
+ }
175
+ }
151
176
  }
152
177
 
153
178
  /**