wogiflow 2.29.7 → 2.29.8

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.
@@ -78,6 +78,23 @@ flow parallel check # See available parallel tasks
78
78
  | 2.27.0+ | 2.1.116+ | Sandbox dangerous-path safety on auto-allow, agent frontmatter hooks for `--agent`, `/resume` large-session speedup, MCP stdio concurrent startup |
79
79
  | 2.27.0+ | 2.1.117+ | Native bfs/ugrep via Bash (hook audit documented), Opus 4.7 /context fix (estimator already percentage-based), Pro/Max effort default shift (advisory delta documented), agent frontmatter `mcpServers` for `--agent`, subagent model-mismatch malware-warning fix, managed-settings plugin marketplace enforcement |
80
80
  | 2.29.6+ | 2.1.132+ | Statusline `context_window` token-count accuracy fix (release notes: was reporting cumulative session totals — may have affected `wogi-statusline-setup` percentage presets if percentage was derived from cumulative tokens), Bedrock/Vertex `ENABLE_PROMPT_CACHING_1H` 400-error fix (recommendation now safe on those providers), `CLAUDE_CODE_SESSION_ID` available in Bash subprocess env |
81
+ | 2.29.7+ | 2.1.133+ | **Subagent skill discovery fix** (CRITICAL for IGR — Architect/Adversary/Skeptical-Evaluator are subagents; if they were missing skills, IGR was silently impaired across versions 2.1.128–2.1.132); **`worktree.baseRef` setting (fresh\|head) reverted to `origin/<default>` default** (was local HEAD since 2.1.128) — wogi-flow's `scripts/flow-worktree.js` users with unpushed local commits should set `worktree.baseRef: "head"` in `.claude/settings.json` to preserve prior behavior; **hooks now receive `effort.level` JSON field + `$CLAUDE_EFFORT` env var** (opportunity for wogi-flow gates to adjust thresholds based on effort — not yet wired); `/effort` no longer leaks across concurrent sessions (workspace-mode benefit); Edit/Write allow rules at drive-root fixed; `sandbox.bwrapPath`/`sandbox.socatPath` managed settings (Linux/WSL); `parentSettingsBehavior` admin-tier key |
82
+ | 2.29.7+ | 2.1.136+ | **`AskUserQuestion` multi-select array discard fixed** (wogi-flow uses AskUserQuestion extensively across skills — automatic fix); **`settings.autoMode.hard_deny`** new unconditional-block mechanism (potential future wogi-flow use for non-bypassable gates like routing/deferral); extended-thinking redacted-block API 400 fixed (wogi-flow's IGR Architect on Opus benefits); MCP servers no longer silently disappear after `/clear`; OAuth refresh-token races fixed (better stability for multi-MCP users); `--resume`/`--continue` no longer fails on underscored project paths; plan mode now correctly blocks file writes when matching Edit allow rule exists; subagent file pickers find files in dirs with >100 entries; many TUI cosmetic fixes (CJK rendering, autocomplete, color artifacts) |
83
+
84
+ ### Worktree-baseRef recommendation (2.1.133+)
85
+
86
+ If you use wogi-flow's worktree feature for parallel task execution AND have unpushed local commits you want available in new worktrees:
87
+
88
+ ```json
89
+ // .claude/settings.json
90
+ {
91
+ "worktree": {
92
+ "baseRef": "head"
93
+ }
94
+ }
95
+ ```
96
+
97
+ Without this, new worktrees branch from `origin/<default>` (the 2.1.133 default) and your unpushed commits won't be available in them. wogi-flow's `scripts/flow-worktree.js` doesn't currently configure this — it inherits whatever Claude Code's default is.
81
98
 
82
99
  ### Environment Variables (2.1.19+)
83
100
 
@@ -170,6 +170,6 @@
170
170
  },
171
171
  "_comment_dynamicHooks": "TaskCreated (2.1.84+) and PermissionDenied (2.1.88+) are added by postinstall.js when the CC version supports them. They must NOT be committed statically — CC rejects the entire settings file if it encounters an unknown hook event name.",
172
172
  "_wogiFlowManaged": true,
173
- "_wogiFlowVersion": "2.27.0",
173
+ "_wogiFlowVersion": "2.29.7",
174
174
  "_comment": "Shared WogiFlow hook configuration. Committed to repo for team use. User-specific overrides go in settings.local.json."
175
175
  }
@@ -79,7 +79,7 @@ const PEERS = parsePeers(PEERS_RAW);
79
79
  // Minimal MCP Protocol (JSON-RPC 2.0 over stdio)
80
80
  // ============================================================
81
81
 
82
- let initialized = false;
82
+ let _initialized = false;
83
83
 
84
84
  /**
85
85
  * Send a JSON-RPC message to Claude Code via stdout.
@@ -198,7 +198,7 @@ function handleRequest(msg) {
198
198
  }
199
199
 
200
200
  if (msg.method === 'notifications/initialized') {
201
- initialized = true;
201
+ _initialized = true;
202
202
  return;
203
203
  }
204
204
 
@@ -20,6 +20,11 @@ const fs = require('node:fs');
20
20
  const path = require('node:path');
21
21
  const crypto = require('node:crypto');
22
22
 
23
+ // wf-3c968989: use safeJsonParse for prototype-pollution protection on
24
+ // the workspace manifest read. Throw-on-failure contract preserved below
25
+ // via explicit null check (manifest is mandatory for workspace mode).
26
+ const { safeJsonParse } = require('../scripts/flow-io');
27
+
23
28
  const VALID_REPO_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
24
29
  const VALID_TASK_ID = /^wf-[0-9a-f]{8}$/i;
25
30
  const REQUIRED_FIELDS = ['id', 'title', 'type'];
@@ -42,11 +47,12 @@ function getWorkerReadyPath(workspaceRoot, repoName) {
42
47
  }
43
48
 
44
49
  const configPath = path.join(workspaceRoot, 'wogi-workspace.json');
45
- let manifest;
46
- try {
47
- manifest = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
48
- } catch (err) {
49
- throw new Error(`Cannot read workspace manifest at ${configPath}: ${err.message}`);
50
+ // wf-3c968989: safeJsonParse adds DANGEROUS_KEYS protection. It returns
51
+ // null on missing/corrupt/array-typed input — manifest is mandatory, so
52
+ // we preserve the original throw-on-failure contract via null check.
53
+ const manifest = safeJsonParse(configPath, null);
54
+ if (!manifest) {
55
+ throw new Error(`Cannot read workspace manifest at ${configPath}`);
50
56
  }
51
57
 
52
58
  const member = manifest.members?.[repoName];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.29.7",
3
+ "version": "2.29.8",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "flow": "./scripts/flow",
13
- "test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/workspace-ipc-sqlite.test.js tests/workspace-ipc-multi-worker.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js tests/auto-review.test.js tests/gate-telemetry-surface.test.js tests/agents-md-alias.test.js tests/flow-skill-manage.test.js tests/fuzzy-patch.test.js tests/mode-schema.test.js tests/flow-feature-dossier.test.js tests/flow-autonomous-mode.test.js tests/flow-epic-cascade.test.js tests/flow-workspace-summary.test.js tests/flow-hooks-research-evidence-gate.test.js tests/flow-worker-mcp-strip.test.js tests/flow-orchestrate-corrections.test.js tests/flow-source-fidelity.test.js tests/flow-hooks-long-input-enforcement.test.js tests/workspace-channel-tracking.test.js tests/flow-hooks-deletion-log.test.js tests/flow-task-boundary-reset.test.js tests/flow-deferral-gate.test.js tests/flow-research-required-gate.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
13
+ "test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-audit-gates.test.js tests/flow-standards-hook-three-layer.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/workspace-ipc-sqlite.test.js tests/workspace-ipc-multi-worker.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js tests/auto-review.test.js tests/gate-telemetry-surface.test.js tests/agents-md-alias.test.js tests/flow-skill-manage.test.js tests/fuzzy-patch.test.js tests/mode-schema.test.js tests/flow-feature-dossier.test.js tests/flow-autonomous-mode.test.js tests/flow-epic-cascade.test.js tests/flow-workspace-summary.test.js tests/flow-hooks-research-evidence-gate.test.js tests/flow-worker-mcp-strip.test.js tests/flow-orchestrate-corrections.test.js tests/flow-source-fidelity.test.js tests/flow-hooks-long-input-enforcement.test.js tests/workspace-channel-tracking.test.js tests/flow-hooks-deletion-log.test.js tests/flow-task-boundary-reset.test.js tests/flow-deferral-gate.test.js tests/flow-research-required-gate.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
14
14
  "test:syntax": "find scripts/ lib/ -name '*.js' -not -path '*/node_modules/*' -exec node --check {} +",
15
15
  "lint": "eslint scripts/ lib/ tests/",
16
16
  "lint:ci": "eslint scripts/ lib/ tests/ --max-warnings 0",
@@ -74,6 +74,9 @@
74
74
  "engines": {
75
75
  "node": ">=18.0.0"
76
76
  },
77
+ "overrides": {
78
+ "protobufjs": ">=7.5.5"
79
+ },
77
80
  "publishConfig": {
78
81
  "access": "public"
79
82
  }
@@ -282,19 +282,77 @@ function checkLintConfigIntegrity() {
282
282
  return result;
283
283
  }
284
284
 
285
+ /**
286
+ * Parse test failure count from Node test runner stdout.
287
+ *
288
+ * Bug fixed 2026-05-08 (wf-e111d850): previously inherited the generic
289
+ * runProjectScript regex `/error TS\d+|Error:|ERROR/gi` which matched the
290
+ * substring "error" in passing test descriptions (e.g., 'trimRetryErrors',
291
+ * 'classifier error path', 'returns null on git unavailable / error'),
292
+ * inflating errorCount even when 0 tests actually failed. Compounded by
293
+ * Node test runner v22 sometimes exiting non-zero on all-pass.
294
+ *
295
+ * Strategy:
296
+ * 1. Primary — parse Node test runner "Results: N passed, M failed" summary
297
+ * lines (one per suite when running multiple files). Sum the M values.
298
+ * 2. Fallback — count TAP "not ok N" lines if no summary present.
299
+ * 3. Default — 0 (graceful) if neither parser finds anything.
300
+ *
301
+ * @param {string} output — combined stdout+stderr from `npm run test`
302
+ * @returns {{ errorCount: number, source: 'summary' | 'tap' | 'default' }}
303
+ */
304
+ function parseTestErrorCount(output) {
305
+ if (typeof output !== 'string' || output.length === 0) {
306
+ return { errorCount: 0, source: 'default' };
307
+ }
308
+
309
+ // Primary: Node test runner summary line(s).
310
+ // Format: "Results: N passed, M failed" (color codes already stripped via
311
+ // FORCE_COLOR=0 / NO_COLOR=1 in runProjectScript).
312
+ const summaryRe = /Results:\s*\d+\s*passed,\s*(\d+)\s*failed/gi;
313
+ let summaryFound = false;
314
+ let total = 0;
315
+ for (const m of output.matchAll(summaryRe)) {
316
+ summaryFound = true;
317
+ total += parseInt(m[1], 10) || 0;
318
+ }
319
+ if (summaryFound) return { errorCount: total, source: 'summary' };
320
+
321
+ // Fallback: TAP "not ok N - ..." line count
322
+ const tap = (output.match(/^not ok \d+/gm) || []).length;
323
+ if (tap > 0) return { errorCount: tap, source: 'tap' };
324
+
325
+ return { errorCount: 0, source: 'default' };
326
+ }
327
+
285
328
  /**
286
329
  * Gate: Tests — do tests pass?
330
+ *
331
+ * Uses parseTestErrorCount() to override the generic regex from
332
+ * runProjectScript. See parseTestErrorCount() comment for bug history.
287
333
  */
288
334
  function checkTests() {
289
335
  const result = runProjectScript('test', 120000);
336
+ const parseSource = result.rawOutput || result.output || '';
337
+ const { errorCount, source: parserSource } = parseTestErrorCount(parseSource);
338
+
339
+ // Trust the parser over npm exit code: Node test runner v22 can exit
340
+ // non-zero in some configurations even when all tests pass. If the parser
341
+ // finds 0 failures via the summary line, that's authoritative.
342
+ const passed = errorCount === 0;
343
+
290
344
  return {
291
345
  gate: 'tests',
292
346
  ...result,
347
+ errorCount,
348
+ passed,
349
+ parserSource,
293
350
  scoreCap: 100, // Test failure doesn't cap, but is a HIGH finding
294
351
  severity: !result.exists ? 'info' :
295
- result.passed ? 'pass' : 'high',
352
+ passed ? 'pass' : 'high',
296
353
  message: !result.exists ? 'No test script defined' :
297
- result.passed ? 'Tests pass' : 'Tests FAIL'
354
+ passed ? 'Tests pass' :
355
+ `Tests FAIL: ${errorCount} failure(s)`
298
356
  };
299
357
  }
300
358
 
@@ -767,6 +825,7 @@ module.exports = {
767
825
  checkLint,
768
826
  checkLintConfigIntegrity,
769
827
  checkTests,
828
+ parseTestErrorCount, // wf-e111d850: exposed for unit testing
770
829
  checkScriptCompleteness,
771
830
 
772
831
  // Extended checks
@@ -1038,6 +1038,24 @@ const CONFIG_DEFAULTS = {
1038
1038
  claudeCode: { installPath: '.claude/settings.local.json' }
1039
1039
  },
1040
1040
 
1041
+ // --- Standards Check (wf-00c5067b) ---
1042
+ // Hook three-layer enforcement: entry files ≤120 LOC + ≤2 core/ imports.
1043
+ // Exemption list documents pre-extraction violators; clear each entry as
1044
+ // its corresponding Phase 2 task ships.
1045
+ standardsCheck: {
1046
+ hookThreeLayer: {
1047
+ enabled: true,
1048
+ maxLoc: 120,
1049
+ maxCoreImports: 2,
1050
+ exemptions: {
1051
+ 'scripts/hooks/entry/claude-code/stop.js': 'Phase 2 — wf-c1e892fa entry-file extraction (orchestration logic to core); remove exemption when extracted',
1052
+ 'scripts/hooks/entry/claude-code/session-start.js': 'Phase 2 — wf-c1e892fa entry-file extraction (orchestration logic to core); remove exemption when extracted',
1053
+ 'scripts/hooks/entry/claude-code/user-prompt-submit.js': 'Phase 2 — wf-c1e892fa entry-file extraction (orchestration logic to core); remove exemption when extracted',
1054
+ 'scripts/hooks/entry/claude-code/post-tool-use.js': 'Phase 2 — wf-c1e892fa entry-file extraction (orchestration logic to core); remove exemption when extracted'
1055
+ }
1056
+ }
1057
+ },
1058
+
1041
1059
  // --- Metrics ---
1042
1060
  metrics: { enabled: false },
1043
1061
 
@@ -14,7 +14,6 @@
14
14
  * flow defer-auth status
15
15
  */
16
16
 
17
- const path = require('node:path');
18
17
  const gate = require('./hooks/core/deferral-gate');
19
18
 
20
19
  function parseArgs(argv) {
@@ -20,7 +20,7 @@ const { PATHS, getConfig, readJson, success } = require('./flow-utils');
20
20
 
21
21
  // Default to PATHS.root from flow-utils, can be overridden via setProjectRoot() or CLI arg
22
22
  let PROJECT_ROOT = PATHS.root;
23
- let CONFIG_PATH = path.join(PROJECT_ROOT, '.workflow/config.json');
23
+ let _CONFIG_PATH = path.join(PROJECT_ROOT, '.workflow/config.json');
24
24
  let CACHE_PATH = path.join(PROJECT_ROOT, '.workflow/state/export-map.json');
25
25
  const CACHE_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
26
26
 
@@ -31,7 +31,7 @@ const CACHE_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
31
31
  */
32
32
  function setProjectRoot(root) {
33
33
  PROJECT_ROOT = path.resolve(root);
34
- CONFIG_PATH = path.join(PROJECT_ROOT, '.workflow/config.json');
34
+ _CONFIG_PATH = path.join(PROJECT_ROOT, '.workflow/config.json');
35
35
  CACHE_PATH = path.join(PROJECT_ROOT, '.workflow/state/export-map.json');
36
36
  }
37
37
 
@@ -700,7 +700,7 @@ function formatExportMapForTemplate(exportMap) {
700
700
  // Components
701
701
  if (Object.keys(exportMap.components).length > 0) {
702
702
  lines.push('#### Components');
703
- for (const [name, info] of Object.entries(exportMap.components)) {
703
+ for (const [_name, info] of Object.entries(exportMap.components)) {
704
704
  const exports = info.exports.join(', ') || (info.defaultExport ? `default: ${info.defaultExport}` : '');
705
705
  if (exports) {
706
706
  lines.push(`- \`import { ${info.exports.join(', ')} } from '${info.importPath}'\``);
@@ -712,7 +712,7 @@ function formatExportMapForTemplate(exportMap) {
712
712
  // Hooks
713
713
  if (Object.keys(exportMap.hooks).length > 0) {
714
714
  lines.push('#### Hooks');
715
- for (const [name, info] of Object.entries(exportMap.hooks)) {
715
+ for (const [_name, info] of Object.entries(exportMap.hooks)) {
716
716
  const exports = info.exports.join(', ');
717
717
  if (exports) {
718
718
  lines.push(`- \`import { ${exports} } from '${info.importPath}'\``);
@@ -724,7 +724,7 @@ function formatExportMapForTemplate(exportMap) {
724
724
  // Services
725
725
  if (Object.keys(exportMap.services).length > 0) {
726
726
  lines.push('#### Services');
727
- for (const [name, info] of Object.entries(exportMap.services)) {
727
+ for (const [_name, info] of Object.entries(exportMap.services)) {
728
728
  const exports = info.exports.join(', ');
729
729
  if (exports) {
730
730
  lines.push(`- \`import { ${exports} } from '${info.importPath}'\``);
@@ -736,7 +736,7 @@ function formatExportMapForTemplate(exportMap) {
736
736
  // Types
737
737
  if (Object.keys(exportMap.types).length > 0) {
738
738
  lines.push('#### Types');
739
- for (const [name, info] of Object.entries(exportMap.types)) {
739
+ for (const [_name, info] of Object.entries(exportMap.types)) {
740
740
  const types = info.types.join(', ');
741
741
  if (types) {
742
742
  lines.push(`- \`import type { ${types} } from '${info.importPath}'\``);
@@ -748,7 +748,7 @@ function formatExportMapForTemplate(exportMap) {
748
748
  // Utils
749
749
  if (Object.keys(exportMap.utils).length > 0) {
750
750
  lines.push('#### Utilities');
751
- for (const [name, info] of Object.entries(exportMap.utils)) {
751
+ for (const [_name, info] of Object.entries(exportMap.utils)) {
752
752
  const exports = info.exports.join(', ');
753
753
  if (exports) {
754
754
  lines.push(`- \`import { ${exports} } from '${info.importPath}'\``);
@@ -784,7 +784,7 @@ function validateComponentUsage(code, exportMap = null) {
784
784
 
785
785
  // Collect all array exports from components
786
786
  const arrayExports = new Set();
787
- for (const [name, info] of Object.entries(exportMap.components || {})) {
787
+ for (const [_name, info] of Object.entries(exportMap.components || {})) {
788
788
  if (info.arrayExports) {
789
789
  info.arrayExports.forEach(e => arrayExports.add(e));
790
790
  }
@@ -841,7 +841,7 @@ function validateComponentUsage(code, exportMap = null) {
841
841
  // Check if the actual export exists
842
842
  const wrongName = pattern.source.replace(/\\/g, '').replace(/\(\)/g, '');
843
843
  let found = false;
844
- for (const [name, info] of Object.entries(exportMap.hooks || {})) {
844
+ for (const [_name, info] of Object.entries(exportMap.hooks || {})) {
845
845
  if (info.exports?.includes(wrongName)) {
846
846
  found = true;
847
847
  break;
@@ -572,10 +572,12 @@ async function main() {
572
572
  const matcher = new SimilarityMatcher(registry);
573
573
 
574
574
  // Parse threshold argument
575
- let threshold = MATCH_CONFIG.thresholds.VARIANT_CANDIDATE;
575
+ // _threshold: parsed from --threshold CLI arg but not currently passed to
576
+ // the matcher (real bug; see audit notes; out of scope for lint cleanup).
577
+ let _threshold = MATCH_CONFIG.thresholds.VARIANT_CANDIDATE;
576
578
  const thresholdIndex = args.indexOf('--threshold');
577
579
  if (thresholdIndex !== -1 && args[thresholdIndex + 1]) {
578
- threshold = parseInt(args[thresholdIndex + 1]);
580
+ _threshold = parseInt(args[thresholdIndex + 1]);
579
581
  }
580
582
 
581
583
  if (input === '--stdin') {
@@ -32,12 +32,13 @@ try {
32
32
  adaptiveLearning = null;
33
33
  }
34
34
 
35
- // Import error recovery for integration
36
- let errorRecovery;
35
+ // Import error recovery for integration (currently loaded for side-effect /
36
+ // future-use; not yet referenced — _ prefix per naming convention).
37
+ let _errorRecovery;
37
38
  try {
38
- errorRecovery = require('./flow-error-recovery');
39
+ _errorRecovery = require('./flow-error-recovery');
39
40
  } catch (_err) {
40
- errorRecovery = null;
41
+ _errorRecovery = null;
41
42
  }
42
43
 
43
44
  // ============================================================
@@ -38,10 +38,10 @@ const { loadRegistry, loadStats } = require('./flow-model-types');
38
38
 
39
39
  // Smart Context System integration
40
40
  let contextGatherer = null;
41
- let instructionRichness = null;
41
+ let _instructionRichness = null;
42
42
  try {
43
43
  contextGatherer = require('./flow-context-gatherer');
44
- instructionRichness = require('./flow-instruction-richness');
44
+ _instructionRichness = require('./flow-instruction-richness');
45
45
  } catch (_err) {
46
46
  // Smart Context modules not available
47
47
  }
@@ -17,8 +17,7 @@ const fs = require('node:fs');
17
17
  const path = require('node:path');
18
18
  const {
19
19
  getConfig,
20
- PATHS,
21
- fileExists
20
+ PATHS
22
21
  } = require('./flow-utils');
23
22
 
24
23
  // ============================================================
@@ -59,7 +58,7 @@ function parseSimpleYaml(content) {
59
58
  const lines = content.split('\n');
60
59
  let currentKey = null;
61
60
  let currentSection = null;
62
- let currentList = null;
61
+ let _currentList = null;
63
62
  let indentLevel = 0;
64
63
  let multilineValue = '';
65
64
  let inMultiline = false;
@@ -100,7 +99,7 @@ function parseSimpleYaml(content) {
100
99
  if (BLOCKED_KEYS.has(key)) continue;
101
100
 
102
101
  currentSection = null;
103
- currentList = null;
102
+ _currentList = null;
104
103
 
105
104
  if (value === '' || value === '|') {
106
105
  // Start of nested section or multi-line
@@ -128,7 +127,7 @@ function parseSimpleYaml(content) {
128
127
 
129
128
  if (BLOCKED_KEYS.has(key)) continue;
130
129
 
131
- currentList = null;
130
+ _currentList = null;
132
131
  currentKey = key;
133
132
 
134
133
  if (value === '|') {
@@ -23,6 +23,7 @@ const { execFileSync } = require('node:child_process');
23
23
 
24
24
  const { PATHS } = require('./flow-paths');
25
25
  const { getConfig } = require('./flow-config-loader');
26
+ const { safeJsonParse } = require('./flow-io');
26
27
 
27
28
  const DEFAULT_BUDGET_BYTES = 16 * 1024; // ~4k tokens
28
29
  const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.workflow', '.worktrees', 'out']);
@@ -46,12 +47,15 @@ function resolveChangedFiles(opts = {}) {
46
47
  if (Array.isArray(opts.changedFiles)) return opts.changedFiles;
47
48
 
48
49
  // Try task-checkpoint.json
50
+ // wf-3c968989: safeJsonParse adds DANGEROUS_KEYS protection. Returns null
51
+ // (the explicit default) on missing/corrupt/array — preserves the
52
+ // original silent-fallthrough contract via the null check below.
49
53
  const checkpointPath = path.join(PATHS.state, 'task-checkpoint.json');
50
54
  if (fs.existsSync(checkpointPath)) {
51
- try {
52
- const cp = JSON.parse(fs.readFileSync(checkpointPath, 'utf8'));
53
- if (Array.isArray(cp.changedFiles) && cp.changedFiles.length > 0) return cp.changedFiles;
54
- } catch { /* fall through */ }
55
+ const cp = safeJsonParse(checkpointPath, null);
56
+ if (cp && Array.isArray(cp.changedFiles) && cp.changedFiles.length > 0) {
57
+ return cp.changedFiles;
58
+ }
55
59
  }
56
60
 
57
61
  // Git diff
@@ -39,7 +39,6 @@
39
39
  'use strict';
40
40
 
41
41
  const fs = require('node:fs');
42
- const path = require('node:path');
43
42
 
44
43
  const VERBATIM_HEADER_REGEX = /^##\s+Original Request \(verbatim\)\s*$/m;
45
44
  const MANIFEST_HEADER_REGEX = /^##\s+Item Manifest\s*$/m;
@@ -20,7 +20,8 @@ const {
20
20
  fileExists,
21
21
  readFile,
22
22
  safeJsonParse,
23
- color
23
+ color,
24
+ getConfig
24
25
  } = require('./flow-utils');
25
26
  const {
26
27
  calculateCombinedSimilarity,
@@ -76,19 +77,23 @@ const MATCH_LEVEL_SEVERITY = {
76
77
  };
77
78
 
78
79
  // Task type to check type mapping for smart scoping
80
+ // wf-00c5067b: 'hook-three-layer' added to all task types — entry-file LOC
81
+ // + import-count rule (per .claude/rules/architecture/hook-three-layer.md)
82
+ // is universally applicable; the exemption list in config covers known
83
+ // pre-extraction violators (see ARCH-001, ARCH-002 in .workflow/state/last-audit.json).
79
84
  const TASK_CHECK_MAP = {
80
- 'component': ['naming', 'components', 'security'],
81
- 'utility': ['naming', 'functions', 'security'],
82
- 'api': ['naming', 'api', 'security'],
83
- 'feature': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security'],
84
- 'bugfix': ['naming', 'security'],
85
- 'refactor': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security'],
86
- 'story': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security'],
87
- 'default': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security']
85
+ 'component': ['naming', 'components', 'security', 'hook-three-layer'],
86
+ 'utility': ['naming', 'functions', 'security', 'hook-three-layer'],
87
+ 'api': ['naming', 'api', 'security', 'hook-three-layer'],
88
+ 'feature': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security', 'hook-three-layer'],
89
+ 'bugfix': ['naming', 'security', 'hook-three-layer'],
90
+ 'refactor': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security', 'hook-three-layer'],
91
+ 'story': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security', 'hook-three-layer'],
92
+ 'default': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security', 'hook-three-layer']
88
93
  };
89
94
 
90
95
  // All available check types
91
- const ALL_CHECK_TYPES = ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security'];
96
+ const ALL_CHECK_TYPES = ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security', 'hook-three-layer'];
92
97
 
93
98
  // ============================================================================
94
99
  // Parse Standards Files
@@ -552,6 +557,88 @@ function checkSecurityPatterns(file, _securityRules) {
552
557
  * @param {Object} matchConfig - Semantic match config — optional, auto-loaded if omitted
553
558
  * @returns {Object[]} Array of violations
554
559
  */
560
+ /**
561
+ * wf-00c5067b — Hook Three-Layer enforcement.
562
+ *
563
+ * Per `.claude/rules/architecture/hook-three-layer.md`:
564
+ * - Entry files (`scripts/hooks/entry/<cli>/*.js`) must be ≤120 LOC and
565
+ * import from at most 2 `core/` modules (single-entry-point principle).
566
+ * - Core files (`scripts/hooks/core/*.js`) should be CLI-agnostic.
567
+ *
568
+ * This check enforces the LOC + import-count rules. Core CLI-identifier
569
+ * grep is intentionally NOT enforced here (false-positive prone — adversary
570
+ * critique 2026-05-08 found 1/4 supposed violations was actually config data).
571
+ *
572
+ * Exemptions: read from config.standardsCheck.hookThreeLayer.exemptions
573
+ * map of `{relativePath: reason}`. Each exemption MUST cite a rationale
574
+ * (typically a Phase 2 task ID for entries awaiting orchestrator extraction).
575
+ *
576
+ * @param {Object} file - File with path and content
577
+ * @param {Object} hookThreeLayerConfig - {enabled, exemptions, maxLoc, maxCoreImports}
578
+ * @returns {Object[]} Array of violations
579
+ */
580
+ function checkHookThreeLayer(file, hookThreeLayerConfig = {}) {
581
+ const violations = [];
582
+ const {
583
+ enabled = true,
584
+ exemptions = {},
585
+ maxLoc = 120,
586
+ maxCoreImports = 2
587
+ } = hookThreeLayerConfig;
588
+
589
+ if (!enabled) return violations;
590
+
591
+ // Normalize path to repo-root-relative form for exemption lookup
592
+ const relPath = file.path.startsWith('/')
593
+ ? path.relative(PATHS.root, file.path)
594
+ : file.path;
595
+
596
+ // Only apply to hook entry files
597
+ const isEntry = /^scripts\/hooks\/entry\/[^/]+\/[^/]+\.js$/.test(relPath);
598
+ if (!isEntry) return violations;
599
+
600
+ // Skip if exempted (with rationale)
601
+ if (Object.prototype.hasOwnProperty.call(exemptions, relPath)) return violations;
602
+
603
+ const content = file.content || '';
604
+ const lines = content.split('\n');
605
+
606
+ // Rule 1: LOC ceiling
607
+ if (lines.length > maxLoc) {
608
+ violations.push({
609
+ type: 'hook-three-layer',
610
+ severity: 'must-fix',
611
+ file: file.path,
612
+ line: null,
613
+ message: `Hook entry file exceeds ${maxLoc} LOC (${lines.length} lines). Extract orchestration logic to core/. Add to config.standardsCheck.hookThreeLayer.exemptions with rationale to defer.`,
614
+ rule: 'hook-three-layer.md'
615
+ });
616
+ }
617
+
618
+ // Rule 2: Core import count
619
+ // Match `require('../core/...')` or `require('../../core/...')` etc.
620
+ // Capture each core path; count distinct core modules imported.
621
+ const coreImportRegex = /require\(['"][^'"]*\/core\/([^'"/]+)['"]\)/g;
622
+ const coreModules = new Set();
623
+ let match;
624
+ while ((match = coreImportRegex.exec(content)) !== null) {
625
+ coreModules.add(match[1]);
626
+ }
627
+
628
+ if (coreModules.size > maxCoreImports) {
629
+ violations.push({
630
+ type: 'hook-three-layer',
631
+ severity: 'must-fix',
632
+ file: file.path,
633
+ line: null,
634
+ message: `Hook entry imports from ${coreModules.size} core/ modules (limit: ${maxCoreImports}). Single-entry-point principle violated. Refactor to dispatch through one orchestrator-core. Modules: ${[...coreModules].sort().join(', ')}`,
635
+ rule: 'hook-three-layer.md'
636
+ });
637
+ }
638
+
639
+ return violations;
640
+ }
641
+
555
642
  function checkApiDuplication(file, existingEndpoints, matchConfig) {
556
643
  const violations = [];
557
644
  const content = file.content || '';
@@ -1009,6 +1096,16 @@ function runStandardsCheck(files, options = {}) {
1009
1096
  const services = checksToRun.includes('services') ? parseServiceMap() : [];
1010
1097
 
1011
1098
  const allViolations = [];
1099
+
1100
+ // wf-00c5067b: load hook-three-layer config (with sensible defaults if unset)
1101
+ const config = getConfig();
1102
+ const hookThreeLayerConfig = (config?.standardsCheck?.hookThreeLayer) || {
1103
+ enabled: checksToRun.includes('hook-three-layer'),
1104
+ exemptions: {},
1105
+ maxLoc: 120,
1106
+ maxCoreImports: 2
1107
+ };
1108
+
1012
1109
  const checksSummary = {
1013
1110
  'decisions.md': { checked: true, violations: 0 },
1014
1111
  'app-map.md': { checked: checksToRun.includes('components') && components.length > 0, violations: 0 },
@@ -1017,7 +1114,8 @@ function runStandardsCheck(files, options = {}) {
1017
1114
  'schema-map.md': { checked: checksToRun.includes('schemas') && schemas.length > 0, violations: 0 },
1018
1115
  'service-map.md': { checked: checksToRun.includes('services') && services.length > 0, violations: 0 },
1019
1116
  'naming-conventions': { checked: checksToRun.includes('naming'), violations: 0 },
1020
- 'security-patterns': { checked: checksToRun.includes('security'), violations: 0 }
1117
+ 'security-patterns': { checked: checksToRun.includes('security'), violations: 0 },
1118
+ 'hook-three-layer': { checked: checksToRun.includes('hook-three-layer'), violations: 0 }
1021
1119
  };
1022
1120
 
1023
1121
  for (const file of files) {
@@ -1076,6 +1174,13 @@ function runStandardsCheck(files, options = {}) {
1076
1174
  allViolations.push(...securityViolations);
1077
1175
  checksSummary['security-patterns'].violations += securityViolations.length;
1078
1176
  }
1177
+
1178
+ // Hook three-layer architecture (wf-00c5067b)
1179
+ if (checksToRun.includes('hook-three-layer')) {
1180
+ const hookViolations = checkHookThreeLayer(file, hookThreeLayerConfig);
1181
+ allViolations.push(...hookViolations);
1182
+ checksSummary['hook-three-layer'].violations += hookViolations.length;
1183
+ }
1079
1184
  }
1080
1185
 
1081
1186
  // Count must-fix violations
@@ -1309,6 +1414,7 @@ module.exports = {
1309
1414
  checkApiDuplication,
1310
1415
  checkRegistryDuplication,
1311
1416
  checkSecurityPatterns,
1417
+ checkHookThreeLayer,
1312
1418
  extractDeclaredNames,
1313
1419
  discoverAllRegistries,
1314
1420
  collectReuseCandidates,
@@ -192,7 +192,7 @@ function isAuthorized(deferralChanges) {
192
192
  return { authorized: false, reason: 'auth-malformed-scope' };
193
193
  }
194
194
 
195
- function consumeAuth(deferralChanges) {
195
+ function consumeAuth(_deferralChanges) {
196
196
  // Auth is single-use: once a deferral write succeeds, the marker is removed
197
197
  // to prevent reuse on subsequent unrelated deferrals.
198
198
  clearAuth();
@@ -46,6 +46,7 @@
46
46
  const fs = require('node:fs');
47
47
  const path = require('node:path');
48
48
  const { PATHS } = require('../../flow-utils');
49
+ const { safeJsonParse } = require('../../flow-io');
49
50
 
50
51
  const PENDING_PATH = path.join(PATHS.state, 'long-input-pending.json');
51
52
 
@@ -210,10 +211,10 @@ function isLongInputPending() {
210
211
  }
211
212
 
212
213
  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; }
214
+ // wf-3c968989: safeJsonParse adds DANGEROUS_KEYS protection. Returns null
215
+ // on missing/corrupt/array input — exact behavior match for the prior
216
+ // try/catch + return-null contract.
217
+ return safeJsonParse(PENDING_PATH, null);
217
218
  }
218
219
 
219
220
  /**