wogiflow 2.29.7 → 2.29.9
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/.claude/docs/claude-code-compatibility.md +17 -0
- package/.claude/settings.json +1 -1
- package/lib/workspace-channel-server.js +2 -2
- package/lib/workspace-task-injector.js +11 -5
- package/package.json +5 -2
- package/scripts/flow-audit-gates.js +180 -2
- package/scripts/flow-config-defaults.js +18 -0
- package/scripts/flow-correction-backfill.js +148 -0
- package/scripts/flow-correction-detector.js +117 -7
- package/scripts/flow-defer-auth.js +0 -1
- package/scripts/flow-export-scanner.js +9 -9
- package/scripts/flow-figma-match.js +4 -2
- package/scripts/flow-hypothesis-generator.js +5 -4
- package/scripts/flow-model-router.js +2 -2
- package/scripts/flow-prompt-template.js +4 -5
- package/scripts/flow-repo-map.js +8 -4
- package/scripts/flow-source-fidelity.js +0 -1
- package/scripts/flow-standards-checker.js +117 -11
- package/scripts/hooks/core/deferral-gate.js +1 -1
- package/scripts/hooks/core/long-input-enforcement.js +5 -4
|
@@ -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
|
|
package/.claude/settings.json
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
3
|
+
"version": "2.29.9",
|
|
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-correction-detector-reconcile.test.js tests/flow-correction-backfill.test.js tests/flow-audit-gates-feature-output-health.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
|
}
|
|
@@ -34,6 +34,7 @@ const fs = require('node:fs');
|
|
|
34
34
|
const path = require('node:path');
|
|
35
35
|
|
|
36
36
|
const { PATHS, safeJsonParse } = require('./flow-utils');
|
|
37
|
+
const { safeJsonParseString } = require('./flow-io');
|
|
37
38
|
|
|
38
39
|
// ============================================================
|
|
39
40
|
// Score Cap Thresholds
|
|
@@ -282,19 +283,77 @@ function checkLintConfigIntegrity() {
|
|
|
282
283
|
return result;
|
|
283
284
|
}
|
|
284
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Parse test failure count from Node test runner stdout.
|
|
288
|
+
*
|
|
289
|
+
* Bug fixed 2026-05-08 (wf-e111d850): previously inherited the generic
|
|
290
|
+
* runProjectScript regex `/error TS\d+|Error:|ERROR/gi` which matched the
|
|
291
|
+
* substring "error" in passing test descriptions (e.g., 'trimRetryErrors',
|
|
292
|
+
* 'classifier error path', 'returns null on git unavailable / error'),
|
|
293
|
+
* inflating errorCount even when 0 tests actually failed. Compounded by
|
|
294
|
+
* Node test runner v22 sometimes exiting non-zero on all-pass.
|
|
295
|
+
*
|
|
296
|
+
* Strategy:
|
|
297
|
+
* 1. Primary — parse Node test runner "Results: N passed, M failed" summary
|
|
298
|
+
* lines (one per suite when running multiple files). Sum the M values.
|
|
299
|
+
* 2. Fallback — count TAP "not ok N" lines if no summary present.
|
|
300
|
+
* 3. Default — 0 (graceful) if neither parser finds anything.
|
|
301
|
+
*
|
|
302
|
+
* @param {string} output — combined stdout+stderr from `npm run test`
|
|
303
|
+
* @returns {{ errorCount: number, source: 'summary' | 'tap' | 'default' }}
|
|
304
|
+
*/
|
|
305
|
+
function parseTestErrorCount(output) {
|
|
306
|
+
if (typeof output !== 'string' || output.length === 0) {
|
|
307
|
+
return { errorCount: 0, source: 'default' };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Primary: Node test runner summary line(s).
|
|
311
|
+
// Format: "Results: N passed, M failed" (color codes already stripped via
|
|
312
|
+
// FORCE_COLOR=0 / NO_COLOR=1 in runProjectScript).
|
|
313
|
+
const summaryRe = /Results:\s*\d+\s*passed,\s*(\d+)\s*failed/gi;
|
|
314
|
+
let summaryFound = false;
|
|
315
|
+
let total = 0;
|
|
316
|
+
for (const m of output.matchAll(summaryRe)) {
|
|
317
|
+
summaryFound = true;
|
|
318
|
+
total += parseInt(m[1], 10) || 0;
|
|
319
|
+
}
|
|
320
|
+
if (summaryFound) return { errorCount: total, source: 'summary' };
|
|
321
|
+
|
|
322
|
+
// Fallback: TAP "not ok N - ..." line count
|
|
323
|
+
const tap = (output.match(/^not ok \d+/gm) || []).length;
|
|
324
|
+
if (tap > 0) return { errorCount: tap, source: 'tap' };
|
|
325
|
+
|
|
326
|
+
return { errorCount: 0, source: 'default' };
|
|
327
|
+
}
|
|
328
|
+
|
|
285
329
|
/**
|
|
286
330
|
* Gate: Tests — do tests pass?
|
|
331
|
+
*
|
|
332
|
+
* Uses parseTestErrorCount() to override the generic regex from
|
|
333
|
+
* runProjectScript. See parseTestErrorCount() comment for bug history.
|
|
287
334
|
*/
|
|
288
335
|
function checkTests() {
|
|
289
336
|
const result = runProjectScript('test', 120000);
|
|
337
|
+
const parseSource = result.rawOutput || result.output || '';
|
|
338
|
+
const { errorCount, source: parserSource } = parseTestErrorCount(parseSource);
|
|
339
|
+
|
|
340
|
+
// Trust the parser over npm exit code: Node test runner v22 can exit
|
|
341
|
+
// non-zero in some configurations even when all tests pass. If the parser
|
|
342
|
+
// finds 0 failures via the summary line, that's authoritative.
|
|
343
|
+
const passed = errorCount === 0;
|
|
344
|
+
|
|
290
345
|
return {
|
|
291
346
|
gate: 'tests',
|
|
292
347
|
...result,
|
|
348
|
+
errorCount,
|
|
349
|
+
passed,
|
|
350
|
+
parserSource,
|
|
293
351
|
scoreCap: 100, // Test failure doesn't cap, but is a HIGH finding
|
|
294
352
|
severity: !result.exists ? 'info' :
|
|
295
|
-
|
|
353
|
+
passed ? 'pass' : 'high',
|
|
296
354
|
message: !result.exists ? 'No test script defined' :
|
|
297
|
-
|
|
355
|
+
passed ? 'Tests pass' :
|
|
356
|
+
`Tests FAIL: ${errorCount} failure(s)`
|
|
298
357
|
};
|
|
299
358
|
}
|
|
300
359
|
|
|
@@ -583,6 +642,114 @@ function compareTrend(currentResults, previousAudit) {
|
|
|
583
642
|
// Main: Run All Gates
|
|
584
643
|
// ============================================================
|
|
585
644
|
|
|
645
|
+
/**
|
|
646
|
+
* Gate: Feature Output Health (wf-6c58953a)
|
|
647
|
+
*
|
|
648
|
+
* Inspects DATA produced by features, not just CODE that produces it.
|
|
649
|
+
* Catches "silent feature no-op" — feature runs without errors, persists
|
|
650
|
+
* data, but the persisted data has all-null structured fields. This class
|
|
651
|
+
* is invisible to traditional code review/lint/typecheck/tests.
|
|
652
|
+
*
|
|
653
|
+
* Discovered 2026-05-09 when wogiflow-cli investigation found the
|
|
654
|
+
* correction-extractor was capturing user frustration but writing null
|
|
655
|
+
* structured fields. The /wogi-audit ran B+ and missed it because every
|
|
656
|
+
* agent inspects code, not output.
|
|
657
|
+
*
|
|
658
|
+
* Rule registry — explicit per-file checks, NOT a generic walker (per
|
|
659
|
+
* challenge round: blanket "all-null is bug" is false-positive city).
|
|
660
|
+
*
|
|
661
|
+
* @param {string} [projectRoot=PATHS.root] — project to inspect (default: current)
|
|
662
|
+
* @returns {Object} gate result with severity + findings
|
|
663
|
+
*/
|
|
664
|
+
function checkFeatureOutputHealth(projectRoot = PATHS.root) {
|
|
665
|
+
const findings = [];
|
|
666
|
+
const stateDir = path.join(projectRoot, '.workflow', 'state');
|
|
667
|
+
const corrDir = path.join(projectRoot, '.workflow', 'corrections');
|
|
668
|
+
|
|
669
|
+
// ---- Rule 1: pending-corrections.json null-fields ratio ----
|
|
670
|
+
// Note: pending-corrections.json is a top-level ARRAY, so safeJsonParse
|
|
671
|
+
// (which rejects arrays) won't work. Use file-read + safeJsonParseString.
|
|
672
|
+
const pcPath = path.join(stateDir, 'pending-corrections.json');
|
|
673
|
+
if (fs.existsSync(pcPath)) {
|
|
674
|
+
let records = [];
|
|
675
|
+
try {
|
|
676
|
+
const content = fs.readFileSync(pcPath, 'utf-8');
|
|
677
|
+
records = safeJsonParseString(content, []);
|
|
678
|
+
} catch (_err) { /* fail-open */ }
|
|
679
|
+
const arr = Array.isArray(records) ? records : [];
|
|
680
|
+
if (arr.length > 0) {
|
|
681
|
+
const nullCount = arr.filter(r =>
|
|
682
|
+
r && typeof r === 'object' &&
|
|
683
|
+
(r.whatWasWrong == null) &&
|
|
684
|
+
(r.whatUserWants == null)
|
|
685
|
+
).length;
|
|
686
|
+
const ratio = nullCount / arr.length;
|
|
687
|
+
if (ratio >= 0.5) {
|
|
688
|
+
findings.push({
|
|
689
|
+
rule: 'pending-corrections-null-fields',
|
|
690
|
+
severity: ratio === 1 ? 'high' : 'medium',
|
|
691
|
+
message: `${nullCount}/${arr.length} (${Math.round(ratio * 100)}%) pending-corrections records have null structured fields. Likely correction-detector extraction failure. Run \`flow-correction-backfill\` or restore via Layer 2 enrichment.`,
|
|
692
|
+
evidence: `${path.relative(projectRoot, pcPath)}: ${arr.length} records analyzed; ${nullCount} fully null`
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// ---- Rule 2: prompt-history × corrections cross-reference ----
|
|
699
|
+
// prompt-history.json is also typically a top-level array.
|
|
700
|
+
const phPath = path.join(stateDir, 'prompt-history.json');
|
|
701
|
+
if (fs.existsSync(phPath)) {
|
|
702
|
+
let ph = [];
|
|
703
|
+
try {
|
|
704
|
+
const content = fs.readFileSync(phPath, 'utf-8');
|
|
705
|
+
ph = safeJsonParseString(content, []);
|
|
706
|
+
} catch (_err) { /* fail-open */ }
|
|
707
|
+
const phArr = Array.isArray(ph) ? ph : (ph && Array.isArray(ph.prompts) ? ph.prompts : []);
|
|
708
|
+
|
|
709
|
+
// Frustration markers (regex per known-pattern set)
|
|
710
|
+
const frustrationRe = /\b(don'?t|stop|wait|actually|why did|why is|you keep|you always|fucking|seriously)\b/i;
|
|
711
|
+
let frustrationCount = 0;
|
|
712
|
+
for (const entry of phArr) {
|
|
713
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
714
|
+
const text = entry.prompt || entry.text || entry.userMessage || '';
|
|
715
|
+
if (typeof text === 'string' && frustrationRe.test(text)) frustrationCount++;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
let corrCount = 0;
|
|
719
|
+
if (fs.existsSync(corrDir)) {
|
|
720
|
+
try {
|
|
721
|
+
corrCount = fs.readdirSync(corrDir).filter(f => f.endsWith('.md')).length;
|
|
722
|
+
} catch (_err) { /* fail-open */ }
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (frustrationCount >= 3 && corrCount === 0) {
|
|
726
|
+
findings.push({
|
|
727
|
+
rule: 'prompt-history-vs-corrections-mismatch',
|
|
728
|
+
severity: 'high',
|
|
729
|
+
message: `prompt-history.json has ${frustrationCount} frustration markers but corrections/ is empty. Correction-extractor pipeline appears non-functional (captures input, fails to materialize records).`,
|
|
730
|
+
evidence: `prompt-history: ${frustrationCount} matches across ${phArr.length} entries; corrections/: ${corrCount} files`
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Determine overall gate severity
|
|
736
|
+
const hasHigh = findings.some(f => f.severity === 'high');
|
|
737
|
+
const hasMed = findings.some(f => f.severity === 'medium');
|
|
738
|
+
const severity = hasHigh ? 'high' : hasMed ? 'medium' : 'pass';
|
|
739
|
+
|
|
740
|
+
return {
|
|
741
|
+
gate: 'feature-output-health',
|
|
742
|
+
exists: true,
|
|
743
|
+
passed: findings.length === 0,
|
|
744
|
+
findings,
|
|
745
|
+
severity,
|
|
746
|
+
scoreCap: 100, // doesn't cap score directly; surfaces as audit findings
|
|
747
|
+
message: findings.length === 0
|
|
748
|
+
? 'Feature output health: no issues detected'
|
|
749
|
+
: `Feature output health: ${findings.length} finding(s) — ${findings.map(f => f.rule).join(', ')}`
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
586
753
|
/**
|
|
587
754
|
* Run all Gate 0 checks and return consolidated results.
|
|
588
755
|
* @returns {Object} gate results with score cap
|
|
@@ -596,6 +763,7 @@ function runAllGates() {
|
|
|
596
763
|
gates.push(checkLintConfigIntegrity());
|
|
597
764
|
gates.push(checkTests());
|
|
598
765
|
gates.push(checkScriptCompleteness());
|
|
766
|
+
gates.push(checkFeatureOutputHealth());
|
|
599
767
|
|
|
600
768
|
const cap = calculateScoreCap(gates);
|
|
601
769
|
const framework = detectFramework();
|
|
@@ -658,6 +826,14 @@ function main() {
|
|
|
658
826
|
console.log(JSON.stringify(checkScriptCompleteness(), null, 2));
|
|
659
827
|
break;
|
|
660
828
|
|
|
829
|
+
case 'feature-output-health': {
|
|
830
|
+
// Optional --project=<path> argument for cross-project audit
|
|
831
|
+
const projArg = process.argv.find(a => a.startsWith('--project='));
|
|
832
|
+
const projectRoot = projArg ? projArg.slice('--project='.length) : PATHS.root;
|
|
833
|
+
console.log(JSON.stringify(checkFeatureOutputHealth(projectRoot), null, 2));
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
|
|
661
837
|
case 'eslint-disable':
|
|
662
838
|
console.log(JSON.stringify(countEslintDisables(), null, 2));
|
|
663
839
|
break;
|
|
@@ -767,7 +943,9 @@ module.exports = {
|
|
|
767
943
|
checkLint,
|
|
768
944
|
checkLintConfigIntegrity,
|
|
769
945
|
checkTests,
|
|
946
|
+
parseTestErrorCount, // wf-e111d850: exposed for unit testing
|
|
770
947
|
checkScriptCompleteness,
|
|
948
|
+
checkFeatureOutputHealth, // wf-6c58953a: feature output health gate
|
|
771
949
|
|
|
772
950
|
// Extended checks
|
|
773
951
|
countEslintDisables,
|
|
@@ -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
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Pending-Corrections Backfill (wf-6c58953a)
|
|
5
|
+
*
|
|
6
|
+
* Backfills records in `.workflow/state/pending-corrections.json` that have
|
|
7
|
+
* null `whatWasWrong` / `whatUserWants` fields. The fix lands at code level
|
|
8
|
+
* (flow-correction-detector.js Layer 1+2 reconciliation), but historical
|
|
9
|
+
* records persisted before the fix already have null fields. This tool
|
|
10
|
+
* applies the same deterministic-fallback extraction retroactively.
|
|
11
|
+
*
|
|
12
|
+
* Strategy:
|
|
13
|
+
* - Read pending-corrections.json
|
|
14
|
+
* - For each record where userMessage is populated AND
|
|
15
|
+
* (whatWasWrong is null OR whatUserWants is null)
|
|
16
|
+
* - Apply deterministic extraction: whatWasWrong = first 200 chars of
|
|
17
|
+
* userMessage; whatUserWants stays null (intent inference is an LLM job
|
|
18
|
+
* — honest null > wrong guess; live extractor will populate going forward)
|
|
19
|
+
* - Mark `enrichmentSource: "backfill-<date>"` so consumers can distinguish
|
|
20
|
+
* backfilled from live extractions
|
|
21
|
+
* - Atomic write: write-temp + rename
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* node scripts/flow-correction-backfill.js # current project
|
|
25
|
+
* node scripts/flow-correction-backfill.js --project=<path> # explicit project
|
|
26
|
+
* node scripts/flow-correction-backfill.js --dry-run # report only
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
|
|
34
|
+
const { PATHS } = require('./flow-utils');
|
|
35
|
+
const { safeJsonParseString } = require('./flow-io');
|
|
36
|
+
const { deterministicWhatWasWrong } = require('./flow-correction-detector');
|
|
37
|
+
|
|
38
|
+
const BACKFILL_DATE = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Backfill a single project's pending-corrections.json.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} projectRoot — project directory containing .workflow/
|
|
44
|
+
* @param {Object} [opts]
|
|
45
|
+
* @param {boolean} [opts.dryRun=false] — if true, return what WOULD change without writing
|
|
46
|
+
* @returns {{ found: number, backfilled: number, alreadyPopulated: number, written: boolean, path: string|null, dryRun: boolean }}
|
|
47
|
+
*/
|
|
48
|
+
function backfillPendingCorrections(projectRoot, opts = {}) {
|
|
49
|
+
const { dryRun = false } = opts;
|
|
50
|
+
const pcPath = path.join(projectRoot, '.workflow', 'state', 'pending-corrections.json');
|
|
51
|
+
|
|
52
|
+
const result = {
|
|
53
|
+
found: 0,
|
|
54
|
+
backfilled: 0,
|
|
55
|
+
alreadyPopulated: 0,
|
|
56
|
+
written: false,
|
|
57
|
+
path: null,
|
|
58
|
+
dryRun
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (!fs.existsSync(pcPath)) {
|
|
62
|
+
result.path = pcPath;
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let content;
|
|
67
|
+
try {
|
|
68
|
+
content = fs.readFileSync(pcPath, 'utf-8');
|
|
69
|
+
} catch (err) {
|
|
70
|
+
throw new Error(`Cannot read pending-corrections at ${pcPath}: ${err.message}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const records = safeJsonParseString(content, []);
|
|
74
|
+
if (!Array.isArray(records)) {
|
|
75
|
+
throw new Error(`Expected array at ${pcPath}; got ${typeof records}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
result.found = records.length;
|
|
79
|
+
result.path = pcPath;
|
|
80
|
+
|
|
81
|
+
let changed = false;
|
|
82
|
+
for (const r of records) {
|
|
83
|
+
if (!r || typeof r !== 'object') continue;
|
|
84
|
+
const userMsg = r.userMessage;
|
|
85
|
+
if (typeof userMsg !== 'string' || !userMsg.trim()) continue;
|
|
86
|
+
|
|
87
|
+
const needsFill = (r.whatWasWrong == null) && (r.whatUserWants == null);
|
|
88
|
+
if (!needsFill) {
|
|
89
|
+
result.alreadyPopulated += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Apply deterministic extraction (whatWasWrong only — whatUserWants
|
|
94
|
+
// stays null; intent inference is the live extractor's job going forward)
|
|
95
|
+
r.whatWasWrong = deterministicWhatWasWrong(userMsg);
|
|
96
|
+
r.enrichmentSource = `backfill-${BACKFILL_DATE}`;
|
|
97
|
+
result.backfilled += 1;
|
|
98
|
+
changed = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (changed && !dryRun) {
|
|
102
|
+
// Atomic write: write-temp + rename
|
|
103
|
+
const tmpPath = `${pcPath}.tmp-${process.pid}`;
|
|
104
|
+
fs.writeFileSync(tmpPath, JSON.stringify(records, null, 2) + '\n');
|
|
105
|
+
fs.renameSync(tmpPath, pcPath);
|
|
106
|
+
result.written = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============================================================
|
|
113
|
+
// CLI
|
|
114
|
+
// ============================================================
|
|
115
|
+
|
|
116
|
+
function main() {
|
|
117
|
+
const argv = process.argv.slice(2);
|
|
118
|
+
const projArg = argv.find(a => a.startsWith('--project='));
|
|
119
|
+
const dryRun = argv.includes('--dry-run');
|
|
120
|
+
|
|
121
|
+
const projectRoot = projArg ? projArg.slice('--project='.length) : PATHS.root;
|
|
122
|
+
|
|
123
|
+
let result;
|
|
124
|
+
try {
|
|
125
|
+
result = backfillPendingCorrections(projectRoot, { dryRun });
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error(`Error: ${err.message}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(JSON.stringify({
|
|
132
|
+
project: projectRoot,
|
|
133
|
+
pendingCorrectionsPath: result.path,
|
|
134
|
+
found: result.found,
|
|
135
|
+
backfilled: result.backfilled,
|
|
136
|
+
alreadyPopulated: result.alreadyPopulated,
|
|
137
|
+
written: result.written,
|
|
138
|
+
dryRun: result.dryRun
|
|
139
|
+
}, null, 2));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
backfillPendingCorrections
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (require.main === module) {
|
|
147
|
+
main();
|
|
148
|
+
}
|
|
@@ -329,14 +329,106 @@ function recordHybridTelemetry(verdict, runCtx = {}) {
|
|
|
329
329
|
}
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
// ============================================================================
|
|
333
|
+
// Layer 1 + Layer 2 Reconciliation (wf-6c58953a)
|
|
334
|
+
// ============================================================================
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Deterministic fallback for `whatWasWrong` — preserves the user's literal
|
|
338
|
+
* frustration text when LLM extraction is unavailable or fails. Better than
|
|
339
|
+
* null: a 200-char excerpt is honest signal; null is data loss.
|
|
340
|
+
*
|
|
341
|
+
* @param {string} message — user message
|
|
342
|
+
* @returns {string|null}
|
|
343
|
+
*/
|
|
344
|
+
function deterministicWhatWasWrong(message) {
|
|
345
|
+
if (typeof message !== 'string') return null;
|
|
346
|
+
const trimmed = message.trim();
|
|
347
|
+
if (!trimmed) return null;
|
|
348
|
+
return trimmed.slice(0, 200);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Reconcile Layer 1 (keyword classifier) + Layer 2 (Haiku LLM) results.
|
|
353
|
+
*
|
|
354
|
+
* Pre-fix bug: Layer 1 returned `{whatWasWrong: null, whatUserWants: null}`
|
|
355
|
+
* when keyword matched, never calling Layer 2. The user's actual frustration
|
|
356
|
+
* was captured but structured fields stayed null — silent feature no-op.
|
|
357
|
+
*
|
|
358
|
+
* Post-fix design:
|
|
359
|
+
* - Layer 1 hit + Layer 2 success: trust Layer 1's classification (high-
|
|
360
|
+
* precision keyword match), use Layer 2's strings if non-null else
|
|
361
|
+
* deterministic fallback. Record `llmDisagreed` if Layer 2 said
|
|
362
|
+
* `isCorrection: false` (e.g., user said "I'm just asking a question").
|
|
363
|
+
* - Layer 1 hit + Layer 2 fail/skip: deterministic fallback for `whatWasWrong`
|
|
364
|
+
* (first 200 chars). `whatUserWants` stays null (intent inference is an
|
|
365
|
+
* LLM job; honest null > wrong guess).
|
|
366
|
+
* - Layer 1 miss + Layer 2 success: Layer 2 is primary classifier (existing path).
|
|
367
|
+
* - Both miss: not a correction.
|
|
368
|
+
*
|
|
369
|
+
* Pure function — testable in isolation, no LLM mock needed.
|
|
370
|
+
*
|
|
371
|
+
* @param {Object|null} layer1 — Layer 1 result {isCorrection, confidence, correctionType, method, matchedPattern}
|
|
372
|
+
* @param {Object|null} layer2 — Layer 2 (LLM) result {isCorrection, confidence, correctionType, whatWasWrong, whatUserWants}
|
|
373
|
+
* @param {string} trimmed — trimmed user message (for deterministic fallback)
|
|
374
|
+
* @returns {Object|null} reconciled record OR null if not a correction
|
|
375
|
+
*/
|
|
376
|
+
function reconcileExtraction(layer1, layer2, trimmed) {
|
|
377
|
+
// Both layers ran
|
|
378
|
+
if (layer1 && layer2) {
|
|
379
|
+
const what = layer2.whatWasWrong || deterministicWhatWasWrong(trimmed);
|
|
380
|
+
const wants = layer2.whatUserWants || null;
|
|
381
|
+
return {
|
|
382
|
+
isCorrection: true, // Layer 1 high-precision keyword match wins binary
|
|
383
|
+
confidence: layer1.confidence,
|
|
384
|
+
correctionType: layer1.correctionType || layer2.correctionType || 'behavior',
|
|
385
|
+
whatWasWrong: what,
|
|
386
|
+
whatUserWants: wants,
|
|
387
|
+
method: 'keyword+ai',
|
|
388
|
+
matchedPattern: layer1.matchedPattern,
|
|
389
|
+
enrichmentSource: layer2.whatWasWrong ? 'haiku' : 'deterministic-fallback',
|
|
390
|
+
llmDisagreed: layer2.isCorrection === false,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
// Layer 1 only (Layer 2 unavailable: no API key, network error, etc.)
|
|
394
|
+
if (layer1) {
|
|
395
|
+
return {
|
|
396
|
+
isCorrection: true,
|
|
397
|
+
confidence: layer1.confidence,
|
|
398
|
+
correctionType: layer1.correctionType || 'behavior',
|
|
399
|
+
whatWasWrong: deterministicWhatWasWrong(trimmed),
|
|
400
|
+
whatUserWants: null,
|
|
401
|
+
method: layer1.method,
|
|
402
|
+
matchedPattern: layer1.matchedPattern,
|
|
403
|
+
enrichmentSource: 'deterministic-fallback',
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
// Layer 2 only (Layer 1 missed)
|
|
407
|
+
if (layer2 && layer2.isCorrection) {
|
|
408
|
+
return {
|
|
409
|
+
isCorrection: true,
|
|
410
|
+
confidence: layer2.confidence,
|
|
411
|
+
correctionType: layer2.correctionType || null,
|
|
412
|
+
whatWasWrong: layer2.whatWasWrong || null,
|
|
413
|
+
whatUserWants: layer2.whatUserWants || null,
|
|
414
|
+
method: 'ai',
|
|
415
|
+
enrichmentSource: 'haiku',
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
// Both missed → not a correction
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
332
422
|
// ============================================================================
|
|
333
423
|
// AI-Based Detection (Haiku — language-agnostic)
|
|
334
424
|
// ============================================================================
|
|
335
425
|
|
|
336
426
|
/**
|
|
337
427
|
* Detect if a message is a correction using Claude Haiku.
|
|
338
|
-
*
|
|
339
|
-
*
|
|
428
|
+
* Hybrid: Layer 1 keyword classifier (fast) + Layer 2 Haiku enrichment.
|
|
429
|
+
*
|
|
430
|
+
* wf-6c58953a (2026-05-09): Layer 1 hit no longer short-circuits structured
|
|
431
|
+
* extraction. See reconcileExtraction() for the post-fix design rationale.
|
|
340
432
|
*
|
|
341
433
|
* @param {string} userMessage - The user's message
|
|
342
434
|
* @param {string} previousContext - Summary of what the AI was doing
|
|
@@ -354,8 +446,12 @@ async function detectCorrection(userMessage, previousContext = '') {
|
|
|
354
446
|
return { isCorrection: false, confidence: 0, method: 'skipped', reason: 'length-filter' };
|
|
355
447
|
}
|
|
356
448
|
|
|
357
|
-
// Layer 1 (wf-e6d65edf) — keyword pre-classifier.
|
|
449
|
+
// Layer 1 (wf-e6d65edf) — keyword pre-classifier.
|
|
450
|
+
// wf-6c58953a: NO longer short-circuits structured extraction. Layer 1's
|
|
451
|
+
// classification is captured; reconcile with Layer 2 (or deterministic
|
|
452
|
+
// fallback when Layer 2 unavailable) at end.
|
|
358
453
|
const hybridCfg = getHybridConfig();
|
|
454
|
+
let layer1Result = null;
|
|
359
455
|
if (hybridCfg.hybridEnabled) {
|
|
360
456
|
const matched = findKeywordMatch(trimmed);
|
|
361
457
|
if (matched) {
|
|
@@ -368,12 +464,10 @@ async function detectCorrection(userMessage, previousContext = '') {
|
|
|
368
464
|
confidence: conf,
|
|
369
465
|
durationMs: Date.now() - start,
|
|
370
466
|
});
|
|
371
|
-
|
|
467
|
+
layer1Result = {
|
|
372
468
|
isCorrection: true,
|
|
373
469
|
confidence: conf,
|
|
374
470
|
correctionType: 'behavior',
|
|
375
|
-
whatWasWrong: null,
|
|
376
|
-
whatUserWants: null,
|
|
377
471
|
method: 'keyword',
|
|
378
472
|
matchedPattern: matched.phrase,
|
|
379
473
|
};
|
|
@@ -383,6 +477,10 @@ async function detectCorrection(userMessage, previousContext = '') {
|
|
|
383
477
|
// Check if API key is available
|
|
384
478
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
385
479
|
if (!apiKey) {
|
|
480
|
+
// wf-6c58953a: Layer 1 hit + no API key → deterministic fallback (not null)
|
|
481
|
+
if (layer1Result) {
|
|
482
|
+
return reconcileExtraction(layer1Result, null, trimmed);
|
|
483
|
+
}
|
|
386
484
|
return { isCorrection: false, confidence: 0, method: 'skipped', reason: 'no-api-key' };
|
|
387
485
|
}
|
|
388
486
|
|
|
@@ -492,11 +590,19 @@ Respond with JSON only (no markdown, no explanation):
|
|
|
492
590
|
durationMs: Date.now() - start,
|
|
493
591
|
});
|
|
494
592
|
|
|
593
|
+
// wf-6c58953a: reconcile Layer 1 + Layer 2 (or just Layer 2 if Layer 1 missed)
|
|
594
|
+
const reconciled = reconcileExtraction(layer1Result, aiResult, trimmed);
|
|
595
|
+
if (reconciled) return reconciled;
|
|
596
|
+
// Both layers say no-correction
|
|
495
597
|
return aiResult;
|
|
496
598
|
} catch (err) {
|
|
497
599
|
if (process.env.DEBUG) {
|
|
498
600
|
console.error(`[DEBUG] AI correction detection failed: ${err.message}`);
|
|
499
601
|
}
|
|
602
|
+
// wf-6c58953a: Layer 2 failure with Layer 1 hit → deterministic fallback
|
|
603
|
+
if (layer1Result) {
|
|
604
|
+
return reconcileExtraction(layer1Result, null, trimmed);
|
|
605
|
+
}
|
|
500
606
|
return { isCorrection: false, confidence: 0, method: 'ai', reason: err.message };
|
|
501
607
|
}
|
|
502
608
|
}
|
|
@@ -1295,11 +1401,15 @@ function correlateWithPriorGates(correction) {
|
|
|
1295
1401
|
// ============================================================================
|
|
1296
1402
|
|
|
1297
1403
|
module.exports = {
|
|
1298
|
-
// Detection (
|
|
1404
|
+
// Detection (hybrid Layer 1 + Layer 2)
|
|
1299
1405
|
detectCorrection,
|
|
1300
1406
|
batchAnalyzePrompts,
|
|
1301
1407
|
spawnBackgroundDetection,
|
|
1302
1408
|
|
|
1409
|
+
// wf-6c58953a: reconciliation helpers exposed for unit testing + backfill
|
|
1410
|
+
reconcileExtraction,
|
|
1411
|
+
deterministicWhatWasWrong,
|
|
1412
|
+
|
|
1303
1413
|
// Queue management
|
|
1304
1414
|
loadPendingCorrections,
|
|
1305
1415
|
queuePendingCorrection,
|
|
@@ -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
|
|
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
|
-
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
+
_errorRecovery = require('./flow-error-recovery');
|
|
39
40
|
} catch (_err) {
|
|
40
|
-
|
|
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
|
|
41
|
+
let _instructionRichness = null;
|
|
42
42
|
try {
|
|
43
43
|
contextGatherer = require('./flow-context-gatherer');
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
130
|
+
_currentList = null;
|
|
132
131
|
currentKey = key;
|
|
133
132
|
|
|
134
133
|
if (value === '|') {
|
package/scripts/flow-repo-map.js
CHANGED
|
@@ -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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
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
|
|
@@ -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(
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
/**
|