wogiflow 2.30.3 → 2.30.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.30.3",
3
+ "version": "2.30.4",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -315,33 +315,100 @@ function checkWriteGate(filePath, newContentRaw, config) {
315
315
  }
316
316
 
317
317
  /**
318
- * Validate a Bash command against the deferral gate. Two-stage:
319
- * Stage 1: does the command mention any target file basename?
320
- * Stage 2: does the command also mention `deferred` literal substring?
321
- * If both fail SAFE (block) unless we can parse the content and prove auth.
318
+ * Strip quoted regions + heredoc bodies from a Bash command so the structural
319
+ * regex below only sees actual shell tokens. Released v2.30.3 over-triggered
320
+ * because the previous regex matched markdown blockquote `> "text"` inside
321
+ * heredoc bodies of `gh release create --notes "$(cat <<'EOF'...EOF)"`.
322
322
  *
323
- * For v1 we don't deep-parse the bash command; we conservatively block any
324
- * Bash that touches a target file AND contains a deferral status literal,
325
- * pointing the AI at the Write/Edit path (which can be properly inspected).
323
+ * Best-effort: handles single-quoted, double-quoted, backtick, and heredoc
324
+ * patterns. Doesn't attempt full shell parsing.
325
+ */
326
+ function stripQuotedContent(cmd) {
327
+ if (typeof cmd !== 'string') return '';
328
+ let stripped = cmd;
329
+ // Heredocs first (multiline) — replace body with a sentinel
330
+ stripped = stripped.replace(/<<-?\s*['"]?(\w+)['"]?[\s\S]*?\n\1\s*$/gm, ' <<HEREDOC>> ');
331
+ stripped = stripped.replace(/<<-?\s*['"]?(\w+)['"]?[\s\S]*?\n\1\b/g, ' <<HEREDOC>> ');
332
+ // Single-quoted strings
333
+ stripped = stripped.replace(/'[^']*'/g, "''");
334
+ // Backtick command substitution
335
+ stripped = stripped.replace(/`[^`]*`/g, '``');
336
+ // Double-quoted strings (allow escaped quotes inside)
337
+ stripped = stripped.replace(/"(?:[^"\\]|\\.)*"/g, '""');
338
+ return stripped;
339
+ }
340
+
341
+ /**
342
+ * Validate a Bash command against the deferral gate.
343
+ *
344
+ * wf-4a5b7a6f rewrite (2026-05-11): previously this used three independent
345
+ * regex checks AND'd together, which over-triggered on commands that merely
346
+ * REFERENCED the target file and the word "deferred" as text content
347
+ * (markdown blockquotes, commit messages, gh release notes). The
348
+ * `>\s*[^&|]` part of `mutates` matched markdown blockquote syntax inside
349
+ * heredocs. The bare-word `\bdeferred\b` part of `mentionsDeferral` matched
350
+ * any prose mention of "deferred".
351
+ *
352
+ * Fix:
353
+ * 1. Run the structural mutation check on a QUOTE-STRIPPED command —
354
+ * a `>` inside `"..."` or `'...'` is not a shell redirect.
355
+ * 2. Tighten the mutation check to require the target file be the WRITE
356
+ * DESTINATION, not merely mentioned anywhere.
357
+ * 3. Tighten deferral-content detection to the JSON-shape pattern only;
358
+ * drop the bare-word match.
359
+ *
360
+ * If the AI tries to actually mutate the file via Bash with deferred
361
+ * content, the gate still catches it. Prose mentions pass through.
326
362
  */
327
363
  function checkBashGate(command, config) {
328
364
  try {
329
365
  if (!isGateEnabled(config)) return { blocked: false };
330
366
  if (typeof command !== 'string' || !command) return { blocked: false };
331
367
 
332
- const mentionsTarget = /last-(review|audit)\.json/.test(command);
333
- if (!mentionsTarget) return { blocked: false };
334
-
335
- // Heuristic: only block when the command appears to MUTATE the file
336
- // (writeFileSync, redirection >, sed -i, etc.). Pure reads (cat, jq, grep)
337
- // are allowed.
338
- const mutates = /(?:writeFileSync|>\s*[^&|]|>>\s*[^&|]|sed\s+-i|tee\s+|fs\.write|rename(?:Sync)?)/.test(command);
339
- if (!mutates) return { blocked: false };
340
-
341
- const mentionsDeferral = /\bdeferred[-_a-zA-Z0-9]*\b|"status"\s*:\s*"(deferred|wont-?fix|skipped|dismissed)/i.test(command);
368
+ // Step 1: strip quoted/heredoc content for the SHELL-LEVEL structural
369
+ // check (catches `>`, `tee` in actual shell positions, not inside markdown).
370
+ const stripped = stripQuotedContent(command);
371
+
372
+ // Step 2: detect a mutation operation targeting the review/audit file
373
+ // SPECIFICALLY. The patterns require the target file to be the WRITE
374
+ // DESTINATION not merely mentioned. We test against BOTH the stripped
375
+ // command (catches shell-level redirects) AND the original command
376
+ // (catches in-language constructs like `node -e "fs.writeFileSync(...)"`
377
+ // where the JS payload is inside double-quotes and would be stripped).
378
+ // The patterns themselves are tight enough that running on the original
379
+ // doesn't re-introduce the prose-mention false positives — they require
380
+ // a write-verb token (writeFileSync, tee, etc.) IMMEDIATELY before the
381
+ // file path.
382
+ const writeToTargetPatterns = [
383
+ /(?:>>?|>\|)\s+['"]?[^\s'"`|&;]*last-(?:review|audit)\.json/,
384
+ /\btee\b(?:\s+-[a-zA-Z]+)*\s+['"]?[^\s'"`|&;]*last-(?:review|audit)\.json/,
385
+ /\b(?:fs\.)?writeFileSync\s*\(\s*[`'"][^`'"]*last-(?:review|audit)\.json/,
386
+ /\bfs\.write[A-Z][a-zA-Z]*\s*\(\s*[`'"][^`'"]*last-(?:review|audit)\.json/,
387
+ /\bsed\s+-i\b[^|;&]*\blast-(?:review|audit)\.json/,
388
+ /\b(?:mv|cp|rename(?:Sync)?)\s+\S+\s+['"]?[^\s'"`|&;]*last-(?:review|audit)\.json/
389
+ ];
390
+ const mutatesTarget = writeToTargetPatterns.some(re => re.test(stripped) || re.test(command));
391
+ if (!mutatesTarget) return { blocked: false };
392
+
393
+ // Step 3: check the ORIGINAL command for deferred-status content. We
394
+ // accept TWO signals:
395
+ // - Quoted value: "deferred" / 'deferred' / `deferred` — JSON, JS,
396
+ // template-literal styles.
397
+ // - Bare word `\bdeferred\b` (or wont-?fix, skipped, dismissed) — fallback
398
+ // for cases where escaping mangles the quote chars (e.g. shell-escaped
399
+ // `\"deferred\"` inside a `node -e` payload where the quote becomes
400
+ // non-adjacent to the word).
401
+ //
402
+ // The earlier false-positive case (prose mentions in release notes) is
403
+ // already closed by the tightened mutation check above — we only reach
404
+ // this step when the command demonstrably writes TO the target file.
405
+ // At that point, ANY mention of the deferral keyword is genuinely
406
+ // suspicious; the gate should err on the side of blocking.
407
+ const quotedDeferral = /['"`](deferred(?:[-_][a-zA-Z0-9]+)?|wont-?fix|won-?t-?fix|skipped|dismissed)['"`]/i;
408
+ const bareDeferral = /\b(deferred(?:[-_][a-zA-Z0-9]+)?|wont-?fix|won-?t-?fix|skipped|dismissed)\b/i;
409
+ const mentionsDeferral = quotedDeferral.test(command) || bareDeferral.test(command);
342
410
  if (!mentionsDeferral) return { blocked: false };
343
411
 
344
- // We can't easily extract and validate the new content from arbitrary bash.
345
412
  // Check auth: if the user has authorized deferrals, allow. Otherwise block.
346
413
  const authResult = isAuthorized([{ id: 'unspecified' }]);
347
414
  if (authResult.authorized) return { blocked: false };
@@ -393,6 +460,7 @@ module.exports = {
393
460
  // Core checks
394
461
  checkWriteGate,
395
462
  checkBashGate,
463
+ stripQuotedContent,
396
464
 
397
465
  // Auth API (used by classifier + CLI helper)
398
466
  loadAuth,