wogiflow 2.29.0 → 2.29.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,7 +18,7 @@ Research finding: across 1,309 user messages mined, first-pass agent output was
18
18
  | 1 | **Intent Bootstrap** — scaffolds product/domain/glossary/user-journeys artifacts; agnostic trap-zone detector finds structural ambiguities | `scripts/flow-intent-bootstrap.js` + `scripts/flow-trap-zone.js` |
19
19
  | 2 | **Intent Framing Pass** — per-task reasoning step; produces a Framing Artifact resolving ambiguities before any other work | `scripts/flow-intent-framing.js` |
20
20
  | 3 | **Architect Pass** — read-only sub-agent produces an 8-section pre-spec plan | `scripts/flow-architect-pass.js` + persona `.workflow/agents/architect.md` |
21
- | 4 | **Logic Adversary** — separate sub-agent on a different model critiques the plan against the 11-principle Logic Constitution (v2 adds Principle 11 Platform Capability Grounding) | `scripts/flow-logic-adversary.js` + rubric `.workflow/rubrics/logic-constitution-v2.md` |
21
+ | 4 | **Logic Adversary** — separate sub-agent on a different model critiques the plan against the 11-principle Logic Constitution (v2: P11 + sub-principles 11.1–11.4 covering observed-behavior, project rules, sibling features, and stacked-story integration) | `scripts/flow-logic-adversary.js` + rubric `.workflow/rubrics/logic-constitution-v2.md` |
22
22
  | 5 | **Session Correction Memory** — detects user corrections during a session and cross-references back to gates that passed the contradicted work | extensions in `scripts/flow-correction-detector.js` |
23
23
  | 6 | **Completion Truth Gate** — audits "done" claims against Tier 0–4 evidence; downgrades language when evidence is insufficient | `scripts/flow-completion-truth-gate.js` |
24
24
  | 7 | **Pipeline wiring + rollout** — integrates all above into `/wogi-start`, the gate registry, the eval framework | (this story) |
@@ -135,6 +135,37 @@ If artifacts don't exist yet, run `node scripts/flow-intent-bootstrap.js bootstr
135
135
 
136
136
  ---
137
137
 
138
+ ### Cross-Story Integration Tier-3 Rule
139
+
140
+ When Story B layers behavior on top of infrastructure shipped by Story A (or any prior commit), Story B's IGR pass MUST treat that infrastructure as an audited dependency, not as a given. Within-module unit tests that pin Story B's local behavior do NOT verify that Story A's contract holds for Story B's usage.
141
+
142
+ **Mandatory for every layering story:**
143
+
144
+ 1. **Architect output names upstream dependencies.** A "Dependencies" section lists prior stories/commits + the specific contract relied on (interface signature, file format, transport, invariant). "I'm reusing Story A's X" is not enough; quote the contract.
145
+
146
+ 2. **Adversary challenges the dependency.** "What if Story A's invariant doesn't hold? What's the failure mode? What evidence proves Story A's contract is intact for THIS usage?" The adversary's job is finding the assumption Story B silently inherits.
147
+
148
+ 3. **At least one Tier-3 integration test exercises the chain end-to-end.** Not a unit test of Story B in isolation — a test that simulates a real run through both stories' code paths. If Story A's output flows into Story B's input, the test feeds a real Story-A output through Story B and asserts the output. Mark the test `// regression-tier3` so future readers know its purpose.
149
+
150
+ 4. **Pre-release gate verifies stacked coverage.** Before tagging a release, identify any commits that layer on prior commits in the same release. For each, confirm a Tier-3 integration test exists. Missing Tier-3 + stacked stories → block release.
151
+
152
+ **Why:** unit tests within a story boundary catch the story's own bugs but miss every regression where the story's correct behavior depends on a broken upstream. The 2026-04-26 incident (audit-channel-transport-001) was caused by exactly this gap: Story A stripped MCP servers including the workspace-channel transport itself; Story B layered task-completion routing on top; both stories' tests passed; manager dispatch silently failed in production. Self-IGR caught Story B's local correctness but missed that the upstream contract was broken.
153
+
154
+ **Anti-rationalization:**
155
+ - *"The upstream story has its own tests"* → WRONG. Their tests pin THEIR contract. Your Tier-3 test pins YOUR usage of their contract.
156
+ - *"It's expensive to set up an integration test"* → WRONG. The 2026-04-26 incident cost a v2.29.1 hot-fix release. Set up time amortizes; regression cost compounds.
157
+ - *"Self-IGR is enough; we don't need the actual adversary subagent"* → WRONG. Self-IGR pattern-matches on the same model that wrote the plan; the cross-story dependency is exactly the blind spot a different-model adversary catches.
158
+
159
+ **How to apply** (concrete checks for any layering story):
160
+ - `git log --oneline <prior-N-commits>` — which earlier work does this story sit on?
161
+ - For each, write the contract you're relying on: "Story A delivers X via Y."
162
+ - `grep -r "<Story A's interface>"` — is the contract still intact in HEAD?
163
+ - Write the Tier-3 test BEFORE writing Story B's code. If the test cannot be written without first standing up infrastructure that makes the integration verifiable, that's a signal the architecture needs that infrastructure too.
164
+
165
+ Enforced by: Logic Constitution v2 sub-principle 11.4 (Stacked-story integration verification). Pre-release gate consumes this signal before tagging.
166
+
167
+ ---
168
+
138
169
  ### Autonomous Walk-Away Mode
139
170
 
140
171
  The user can dump N items, say "go until you finish" / "autonomous mode" / "run this autonomously" / "don't bother me, just do it" (or similar phrases — see `flow-autonomous-detector.js`), and walk away. While the run is active:
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
+ const { DANGEROUS_KEYS } = require('../scripts/flow-io');
5
6
 
6
7
  const MODES_DIR = path.join(process.cwd(), '.workflow', 'modes');
7
8
 
@@ -17,7 +18,7 @@ const REQUIRED_FIELDS = ['name', 'roleDefinition', 'whenToUse'];
17
18
  const OPTIONAL_FIELDS = ['customInstructions', 'allowedToolGroups'];
18
19
  const ALL_FIELDS = new Set([...REQUIRED_FIELDS, ...OPTIONAL_FIELDS]);
19
20
 
20
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
21
+ // DANGEROUS_KEYS imported from scripts/flow-io canonical (audit dup-002 / wf-9fc4970b).
21
22
 
22
23
  function parseModeYaml(content, sourceLabel = '<inline>') {
23
24
  const result = Object.create(null);
package/lib/wogi-claude CHANGED
@@ -107,13 +107,78 @@ if [ "$__wogi_is_worker" -eq 1 ]; then
107
107
  fi
108
108
  fi
109
109
 
110
- # Resolve the empty-MCP config used for stripping. Persistent path so we don't
111
- # regenerate a tmpfile on every restart; living in .workflow/state/ keeps it
112
- # alongside other worker state.
110
+ # Resolve the channel-only MCP config used for stripping. Persistent path
111
+ # so we don't regenerate on every restart; living in .workflow/state/ keeps
112
+ # it alongside other worker state.
113
+ #
114
+ # IMPORTANT — REGRESSION FIX (audit-channel-transport-001):
115
+ #
116
+ # The original Story A (wf-8294d960) wrote `{"mcpServers":{}}` — fully
117
+ # empty — under the (unverified) assumption that workers don't need any
118
+ # MCP servers in worker mode. That assumption was WRONG: it stripped
119
+ # `wogi-workspace-channel` which IS the transport that the manager uses
120
+ # to dispatch tasks to workers via `workspace_send_message` (the manager
121
+ # HTTP-POSTs to the worker's channel-server port; with no MCP server,
122
+ # there's no listener, so dispatches silently fail with "connection
123
+ # refused"). Tier-3 evidence (end-to-end manager→worker dispatch) was
124
+ # never collected; only boot-latency was measured. Story B
125
+ # (wf-ab59f0e4) layered COMPLETION-SUMMARY routing on top of this
126
+ # broken transport without auditing the dependency.
127
+ #
128
+ # The proper fix: extract ONLY the `wogi-workspace-channel` entry from
129
+ # the worker's real `.mcp.json` and write a channel-only config. This
130
+ # preserves Story A's boot-speed win (claude.ai MCP integrations stay
131
+ # stripped) while keeping the workspace transport active. If the
132
+ # worker's `.mcp.json` doesn't define `wogi-workspace-channel` (e.g.
133
+ # this is not a workspace member), fall back to the empty MCP config
134
+ # (the strip is harmless in non-workspace contexts).
113
135
  __wogi_empty_mcp_config=""
114
136
  if [ "$__wogi_strip_mcp" -eq 1 ]; then
115
- __wogi_empty_mcp_config="${WOGI_WORKSPACE_ROOT:-$(pwd)}/.workflow/state/worker-empty-mcp.json"
116
- if [ ! -f "$__wogi_empty_mcp_config" ]; then
137
+ __wogi_empty_mcp_config="${WOGI_WORKSPACE_ROOT:-$(pwd)}/.workflow/state/worker-channel-only-mcp.json"
138
+ __wogi_member_mcp_path="$(pwd)/.mcp.json"
139
+ if command -v node >/dev/null 2>&1; then
140
+ # Use the dedicated helper (testable; see tests/flow-worker-mcp-strip.test.js).
141
+ # Extracts ONLY the wogi-workspace-channel entry from the worker's real
142
+ # .mcp.json so manager-side workspace_send_message dispatch keeps working.
143
+ __wogi_strip_helper=""
144
+ for __wogi_candidate in \
145
+ "$(dirname "$0")/../scripts/flow-worker-mcp-strip.js" \
146
+ "$(npm root -g 2>/dev/null)/wogiflow/scripts/flow-worker-mcp-strip.js" \
147
+ "$(pwd)/node_modules/wogiflow/scripts/flow-worker-mcp-strip.js"; do
148
+ if [ -f "$__wogi_candidate" ]; then
149
+ __wogi_strip_helper="$__wogi_candidate"
150
+ break
151
+ fi
152
+ done
153
+ if [ -n "$__wogi_strip_helper" ]; then
154
+ node "$__wogi_strip_helper" "$__wogi_member_mcp_path" "$__wogi_empty_mcp_config" 2>/dev/null || __wogi_strip_mcp=0
155
+ else
156
+ # Helper not found — fall back to inline extraction (legacy code path
157
+ # for installs that pre-date the helper script).
158
+ node -e '
159
+ const fs = require("fs");
160
+ const path = require("path");
161
+ const [src, out] = process.argv.slice(1);
162
+ let channelEntry = null;
163
+ try {
164
+ if (fs.existsSync(src)) {
165
+ const cfg = JSON.parse(fs.readFileSync(src, "utf-8"));
166
+ const ws = cfg && cfg.mcpServers && cfg.mcpServers["wogi-workspace-channel"];
167
+ if (ws) channelEntry = ws;
168
+ }
169
+ } catch (_e) {}
170
+ const payload = channelEntry
171
+ ? { mcpServers: { "wogi-workspace-channel": channelEntry } }
172
+ : { mcpServers: {} };
173
+ fs.mkdirSync(path.dirname(out), { recursive: true });
174
+ const tmp = out + ".tmp." + process.pid;
175
+ fs.writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n");
176
+ fs.renameSync(tmp, out);
177
+ ' "$__wogi_member_mcp_path" "$__wogi_empty_mcp_config" 2>/dev/null || __wogi_strip_mcp=0
178
+ fi
179
+ else
180
+ # node missing — last-resort fallback. Empty config = manager dispatch
181
+ # will fail, but worker boot will still succeed.
117
182
  mkdir -p "$(dirname "$__wogi_empty_mcp_config")" 2>/dev/null
118
183
  printf '{"mcpServers":{}}\n' > "$__wogi_empty_mcp_config" 2>/dev/null || __wogi_strip_mcp=0
119
184
  fi
@@ -11,6 +11,7 @@
11
11
  const fs = require('node:fs');
12
12
  const path = require('node:path');
13
13
  const { safeReadJson } = require('./utils');
14
+ const { DANGEROUS_KEYS } = require('../scripts/flow-io');
14
15
  const crypto = require('node:crypto');
15
16
 
16
17
  // ============================================================
@@ -170,7 +171,7 @@ function updateMessageStatus(workspaceRoot, messageId, newStatus, extra = {}) {
170
171
  message.updatedAt = new Date().toISOString();
171
172
  // Safe merge: filter dangerous keys to prevent prototype pollution
172
173
  if (extra && typeof extra === 'object') {
173
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
174
+ // DANGEROUS_KEYS imported from scripts/flow-io canonical (audit dup-002 / wf-9fc4970b).
174
175
  for (const [key, value] of Object.entries(extra)) {
175
176
  if (!DANGEROUS_KEYS.has(key)) {
176
177
  message[key] = value;
package/lib/workspace.js CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  const fs = require('node:fs');
19
19
  const path = require('node:path');
20
- const { safeJsonParse } = require('../scripts/flow-io');
20
+ const { safeJsonParse, DANGEROUS_KEYS } = require('../scripts/flow-io');
21
21
 
22
22
  /**
23
23
  * wf-f747f993 — resolve the claude-spawning command for workspace sessions.
@@ -61,8 +61,8 @@ function resolveClaudeSpawnCommand(role, flags) {
61
61
  // Constants
62
62
  // ============================================================
63
63
 
64
- // Proto-pollution safe JSON parse (finding-007)
65
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
64
+ // Proto-pollution safe JSON parse (finding-007).
65
+ // DANGEROUS_KEYS imported from scripts/flow-io canonical (audit dup-002 / wf-9fc4970b).
66
66
  function safeParseJson(str, fallback) {
67
67
  try {
68
68
  const obj = JSON.parse(str);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.29.0",
3
+ "version": "2.29.2",
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 && 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-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 && 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",
@@ -368,8 +368,9 @@ function invalidateConfigCache() {
368
368
  // Config Value Access
369
369
  // ============================================================
370
370
 
371
- // Dangerous property names that could lead to prototype pollution
372
- const DANGEROUS_CONFIG_PROPS = new Set(['__proto__', 'constructor', 'prototype']);
371
+ // Dangerous property names that could lead to prototype pollution.
372
+ // Consolidated to flow-io canonical (audit dup-002 / wf-9fc4970b).
373
+ const { DANGEROUS_KEYS: DANGEROUS_CONFIG_PROPS } = require('./flow-io');
373
374
 
374
375
  /**
375
376
  * Validate config path doesn't contain dangerous property names
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  const path = require('node:path');
19
+ const { DANGEROUS_KEYS } = require('./flow-io');
19
20
  const {
20
21
  PATHS,
21
22
  safeJsonParse,
@@ -695,11 +696,11 @@ function loadPendingCorrections() {
695
696
  // SEC-005 fix (2026-04-13): recursive prototype-pollution check for
696
697
  // array-rooted JSON. Returns true if __proto__/constructor/prototype found.
697
698
  function hasDangerousKeys(value) {
698
- const dangerous = new Set(['__proto__', 'constructor', 'prototype']);
699
+ // Consolidated to flow-io canonical DANGEROUS_KEYS (audit dup-002 / wf-9fc4970b).
699
700
  const visit = (node, depth) => {
700
701
  if (depth > 8 || node === null || typeof node !== 'object') return false;
701
702
  for (const key of Object.getOwnPropertyNames(node)) {
702
- if (dangerous.has(key)) return true;
703
+ if (DANGEROUS_KEYS.has(key)) return true;
703
704
  if (visit(node[key], depth + 1)) return true;
704
705
  }
705
706
  return false;
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow — Auto-Correction Helpers (Story 14 / wf-d0937c83)
5
+ *
6
+ * Splits flow-orchestrate's autoCorrectCode into focused helpers:
7
+ *
8
+ * - fixForbiddenImports (section 1: doNotImport, default/combined/namespace forms)
9
+ * - fixComponentPaths (section 2: shadcn @/components/ui/X mapping)
10
+ * - fixFeatureTypePaths (section 3: ../types in /features/ files)
11
+ * - fixNoExternalUtils (section 4: @/lib/utils removal + formatCurrency inline + cn() unwrap)
12
+ * - normalizeQuotes (section 5: double-quote → single-quote when single dominates)
13
+ * - cleanupEmptyImports (section 6: empty `import {} from "..."` cleanup)
14
+ * - collapseBlankLines (section 7: 3+ blanks → 2)
15
+ *
16
+ * Each helper takes a `(code, ...args)` shape and returns
17
+ * `{ corrected, corrections }`. The orchestrator (flow-orchestrate.js
18
+ * `autoCorrectCode`) chains them in section order.
19
+ *
20
+ * Behavior is preserved verbatim from the pre-extraction implementation;
21
+ * pinned by characterization tests in
22
+ * `tests/flow-orchestrate-corrections.test.js` (Tier-3 integration test
23
+ * per the Cross-Story Integration Tier-3 Rule — feeds real input through
24
+ * the public `autoCorrectCode` API and asserts the output).
25
+ *
26
+ * Programmatic:
27
+ * const c = require('./flow-orchestrate-corrections');
28
+ * const { corrected, corrections } = c.fixForbiddenImports(code, ['React']);
29
+ */
30
+
31
+ 'use strict';
32
+
33
+ function fixForbiddenImports(code, doNotImport) {
34
+ let corrected = code;
35
+ const corrections = [];
36
+ const list = Array.isArray(doNotImport) && doNotImport.length ? doNotImport : ['React'];
37
+ for (const forbidden of list) {
38
+ const defaultImportRegex = new RegExp(`^import ${forbidden} from ['"][^'"]+['"];?\\s*\\n?`, 'gm');
39
+ if (defaultImportRegex.test(corrected)) {
40
+ corrected = corrected.replace(defaultImportRegex, '');
41
+ corrections.push(`Removed forbidden import: ${forbidden}`);
42
+ }
43
+
44
+ const combinedImportRegex = new RegExp(`^import ${forbidden},\\s*(\\{[^}]+\\})\\s+from\\s+(['"][^'"]+['"])`, 'gm');
45
+ if (combinedImportRegex.test(corrected)) {
46
+ corrected = corrected.replace(combinedImportRegex, 'import $1 from $2');
47
+ corrections.push(`Removed ${forbidden} from combined import`);
48
+ }
49
+
50
+ const namespaceImportRegex = new RegExp(`^import \\* as ${forbidden} from ['"][^'"]+['"];?\\s*\\n?`, 'gm');
51
+ if (namespaceImportRegex.test(corrected)) {
52
+ corrected = corrected.replace(namespaceImportRegex, '');
53
+ corrections.push(`Removed namespace import: ${forbidden}`);
54
+ }
55
+ }
56
+ return { corrected, corrections };
57
+ }
58
+
59
+ function fixComponentPaths(code, componentPaths) {
60
+ let corrected = code;
61
+ const corrections = [];
62
+ const map = (componentPaths && typeof componentPaths === 'object') ? componentPaths : {};
63
+ const shadcnPattern = /@\/components\/ui\/(\w+)/g;
64
+ corrected = corrected.replace(shadcnPattern, (match, component) => {
65
+ const capitalName = component.charAt(0).toUpperCase() + component.slice(1);
66
+ const configPath = map[capitalName];
67
+ if (configPath) {
68
+ corrections.push(`Fixed import: ${match} → ${configPath}`);
69
+ return configPath;
70
+ }
71
+ return match;
72
+ });
73
+ return { corrected, corrections };
74
+ }
75
+
76
+ function fixFeatureTypePaths(code, filePath, typePaths) {
77
+ let corrected = code;
78
+ const corrections = [];
79
+ const paths = (typePaths && typeof typePaths === 'object') ? typePaths : { features: '../api/types' };
80
+ if (filePath && filePath.includes('/features/') && paths.features) {
81
+ const wrongPaths = ["'../types'", '"../types"', "'./types'", '"./types"'];
82
+ for (const wrong of wrongPaths) {
83
+ if (corrected.includes(wrong)) {
84
+ corrected = corrected.replace(new RegExp(wrong.replace(/['"]/g, '[\'"]'), 'g'), `'${paths.features}'`);
85
+ corrections.push('Fixed type import path');
86
+ }
87
+ }
88
+ }
89
+ return { corrected, corrections };
90
+ }
91
+
92
+ function fixNoExternalUtils(code, ctx) {
93
+ let corrected = code;
94
+ const corrections = [];
95
+ if (!(ctx && ctx.noExternalUtils && corrected.includes('@/lib/utils'))) {
96
+ return { corrected, corrections };
97
+ }
98
+
99
+ const hadFormatCurrency = corrected.includes('formatCurrency');
100
+ const hadCn = corrected.includes(' cn(') || corrected.includes(' cn`');
101
+
102
+ corrected = corrected.replace(/^import.*from ['"]@\/lib\/utils['"];?\s*\n?/gm, '');
103
+ corrections.push('Removed @/lib/utils import');
104
+
105
+ if (hadFormatCurrency) {
106
+ const formatCurrencyFn = `\nconst formatCurrency = (amount: number) =>\n new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);\n`;
107
+ const lastImportMatch = corrected.match(/^import[^;]+;?\s*\n/gm);
108
+ if (lastImportMatch) {
109
+ const lastImport = lastImportMatch[lastImportMatch.length - 1];
110
+ const insertPos = corrected.lastIndexOf(lastImport) + lastImport.length;
111
+ corrected = corrected.slice(0, insertPos) + formatCurrencyFn + corrected.slice(insertPos);
112
+ }
113
+ corrections.push('Inlined formatCurrency');
114
+ }
115
+
116
+ if (hadCn) {
117
+ corrected = corrected.replace(/cn\((['"`][^'"`]+['"`])\)/g, '$1');
118
+ corrections.push('Removed cn() wrapper');
119
+ }
120
+
121
+ return { corrected, corrections };
122
+ }
123
+
124
+ function normalizeQuotes(code) {
125
+ let corrected = code;
126
+ const corrections = [];
127
+ const singleQuoteCount = (corrected.match(/from '/g) || []).length;
128
+ const doubleQuoteCount = (corrected.match(/from "/g) || []).length;
129
+ if (singleQuoteCount > doubleQuoteCount && doubleQuoteCount > 0) {
130
+ corrected = corrected.replace(/from "([^"]+)"/g, "from '$1'");
131
+ corrections.push('Normalized import quotes to single quotes');
132
+ }
133
+ return { corrected, corrections };
134
+ }
135
+
136
+ function cleanupEmptyImports(code) {
137
+ return {
138
+ corrected: code.replace(/^import\s*\{\s*\}\s*from\s*['"][^'"]+['"];?\s*\n?/gm, ''),
139
+ corrections: []
140
+ };
141
+ }
142
+
143
+ function collapseBlankLines(code) {
144
+ return {
145
+ corrected: code.replace(/\n{3,}/g, '\n\n'),
146
+ corrections: []
147
+ };
148
+ }
149
+
150
+ module.exports = {
151
+ fixForbiddenImports,
152
+ fixComponentPaths,
153
+ fixFeatureTypePaths,
154
+ fixNoExternalUtils,
155
+ normalizeQuotes,
156
+ cleanupEmptyImports,
157
+ collapseBlankLines
158
+ };
@@ -307,109 +307,28 @@ function autoCorrectCode(code, filePath, projectConfig = null) {
307
307
  return { corrected: code, corrections: [] };
308
308
  }
309
309
 
310
- // Load project context from config if not provided
311
310
  const ctx = projectConfig?.projectContext ?? getProjectContext();
312
-
313
- let corrected = code;
314
311
  const corrections = [];
315
312
 
316
- // 1. Remove forbidden imports (from config, defaults to ['React'])
317
- const doNotImport = ctx.doNotImport || ['React'];
318
- for (const forbidden of doNotImport) {
319
- // Case A: Default import - "import X from '...'"
320
- const defaultImportRegex = new RegExp(`^import ${forbidden} from ['"][^'"]+['"];?\\s*\\n?`, 'gm');
321
- if (defaultImportRegex.test(corrected)) {
322
- corrected = corrected.replace(defaultImportRegex, '');
323
- corrections.push(`Removed forbidden import: ${forbidden}`);
324
- }
325
-
326
- // Case B: Combined with named imports - "import X, { y, z } from '...'"
327
- const combinedImportRegex = new RegExp(`^import ${forbidden},\\s*(\\{[^}]+\\})\\s+from\\s+(['"][^'"]+['"])`, 'gm');
328
- if (combinedImportRegex.test(corrected)) {
329
- corrected = corrected.replace(combinedImportRegex, 'import $1 from $2');
330
- corrections.push(`Removed ${forbidden} from combined import`);
331
- }
332
-
333
- // Case C: Namespace import - "import * as X from '...'"
334
- const namespaceImportRegex = new RegExp(`^import \\* as ${forbidden} from ['"][^'"]+['"];?\\s*\\n?`, 'gm');
335
- if (namespaceImportRegex.test(corrected)) {
336
- corrected = corrected.replace(namespaceImportRegex, '');
337
- corrections.push(`Removed namespace import: ${forbidden}`);
338
- }
339
- }
340
-
341
- // 2. Fix component paths based on config mappings
342
- const componentPaths = ctx.componentPaths ?? {};
343
-
344
- // Build reverse mapping from shadcn-style to project paths
345
- // @/components/ui/button → project's Button path
346
- const shadcnPattern = /@\/components\/ui\/(\w+)/g;
347
- corrected = corrected.replace(shadcnPattern, (match, component) => {
348
- const capitalName = component.charAt(0).toUpperCase() + component.slice(1);
349
- const configPath = componentPaths[capitalName];
350
- if (configPath) {
351
- corrections.push(`Fixed import: ${match} → ${configPath}`);
352
- return configPath;
353
- }
354
- return match; // Leave as-is if no mapping
355
- });
356
-
357
- // 3. Fix type paths for features (from config)
358
- const typePaths = ctx.typePaths || { features: '../api/types' };
359
- if (filePath && filePath.includes('/features/') && typePaths.features) {
360
- const wrongPaths = ["'../types'", '"../types"', "'./types'", '"./types"'];
361
- for (const wrong of wrongPaths) {
362
- if (corrected.includes(wrong)) {
363
- corrected = corrected.replace(new RegExp(wrong.replace(/['"]/g, '[\'"]'), 'g'), `'${typePaths.features}'`);
364
- corrections.push('Fixed type import path');
365
- }
366
- }
367
- }
368
-
369
- // 4. Remove external utils if configured (noExternalUtils: true)
370
- if (ctx.noExternalUtils && corrected.includes('@/lib/utils')) {
371
- const hadFormatCurrency = corrected.includes('formatCurrency');
372
- const hadCn = corrected.includes(' cn(') || corrected.includes(' cn`');
373
-
374
- // Remove the import
375
- corrected = corrected.replace(/^import.*from ['"]@\/lib\/utils['"];?\s*\n?/gm, '');
376
- corrections.push('Removed @/lib/utils import');
377
-
378
- // Inline formatCurrency if it was used
379
- if (hadFormatCurrency) {
380
- const formatCurrencyFn = `\nconst formatCurrency = (amount: number) =>\n new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);\n`;
381
- // Insert after imports
382
- const lastImportMatch = corrected.match(/^import[^;]+;?\s*\n/gm);
383
- if (lastImportMatch) {
384
- const lastImport = lastImportMatch[lastImportMatch.length - 1];
385
- const insertPos = corrected.lastIndexOf(lastImport) + lastImport.length;
386
- corrected = corrected.slice(0, insertPos) + formatCurrencyFn + corrected.slice(insertPos);
387
- }
388
- corrections.push('Inlined formatCurrency');
389
- }
313
+ // Lazy-load the helpers avoids module-load-order issues with the
314
+ // legacy CLI bootstrap at the bottom of this file.
315
+ const c = require('./flow-orchestrate-corrections');
390
316
 
391
- // Remove cn() usage - just use template literals or className directly
392
- if (hadCn) {
393
- corrected = corrected.replace(/cn\((['"`][^'"`]+['"`])\)/g, '$1');
394
- corrections.push('Removed cn() wrapper');
395
- }
396
- }
397
-
398
- // 5. Fix double-quoted imports to single quotes (style consistency)
399
- const singleQuoteCount = (corrected.match(/from '/g) || []).length;
400
- const doubleQuoteCount = (corrected.match(/from "/g) || []).length;
401
- if (singleQuoteCount > doubleQuoteCount && doubleQuoteCount > 0) {
402
- corrected = corrected.replace(/from "([^"]+)"/g, "from '$1'");
403
- corrections.push('Normalized import quotes to single quotes');
317
+ let corrected = code;
318
+ for (const step of [
319
+ () => c.fixForbiddenImports(corrected, ctx.doNotImport),
320
+ () => c.fixComponentPaths(corrected, ctx.componentPaths),
321
+ () => c.fixFeatureTypePaths(corrected, filePath, ctx.typePaths),
322
+ () => c.fixNoExternalUtils(corrected, ctx),
323
+ () => c.normalizeQuotes(corrected),
324
+ () => c.cleanupEmptyImports(corrected),
325
+ () => c.collapseBlankLines(corrected)
326
+ ]) {
327
+ const r = step();
328
+ corrected = r.corrected;
329
+ if (r.corrections.length) corrections.push(...r.corrections);
404
330
  }
405
331
 
406
- // 6. Remove empty import statements (artifact of removing imports)
407
- corrected = corrected.replace(/^import\s*\{\s*\}\s*from\s*['"][^'"]+['"];?\s*\n?/gm, '');
408
-
409
- // 7. Fix multiple consecutive blank lines (cleanup)
410
- corrected = corrected.replace(/\n{3,}/g, '\n\n');
411
-
412
- // Log corrections if any
413
332
  if (corrections.length > 0 && typeof log === 'function') {
414
333
  log('dim', ` 🔧 Auto-corrected: ${corrections.join(', ')}`);
415
334
  }
@@ -16,6 +16,7 @@
16
16
 
17
17
  const fs = require('node:fs');
18
18
  const path = require('node:path');
19
+ const { DANGEROUS_KEYS } = require('./flow-io');
19
20
  const {
20
21
  PROJECT_ROOT,
21
22
  parseFlags,
@@ -341,7 +342,7 @@ function applyTemplate(template, data) {
341
342
  }
342
343
 
343
344
  // Forbidden keys to prevent prototype pollution (case-insensitive)
344
- const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
345
+ // Consolidated to flow-io canonical (audit dup-002 / wf-9fc4970b).
345
346
 
346
347
  // Simple substitution: {{key}} or {{object.key}}
347
348
  return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
@@ -351,7 +352,7 @@ function applyTemplate(template, data) {
351
352
  for (const key of keys) {
352
353
  // Prevent prototype pollution attacks (case-insensitive check)
353
354
  const keyLower = key.toLowerCase();
354
- if (FORBIDDEN_KEYS.has(keyLower)) return match;
355
+ if (DANGEROUS_KEYS.has(keyLower)) return match;
355
356
  if (value === undefined || value === null) return match;
356
357
  // Only access own properties
357
358
  if (!Object.hasOwn(value, key)) return match;
@@ -38,8 +38,9 @@ const MODEL_TEMPLATE_MAP = {
38
38
  'gemini-2-flash': 'gemini-flash.yaml'
39
39
  };
40
40
 
41
- // Blocked keys for security (prototype pollution prevention)
42
- const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
41
+ // Blocked keys for security (prototype pollution prevention).
42
+ // Consolidated to flow-io canonical (audit dup-002 / wf-9fc4970b).
43
+ const { DANGEROUS_KEYS: BLOCKED_KEYS } = require('./flow-io');
43
44
 
44
45
  // ============================================================
45
46
  // YAML Parser (lightweight, no dependency)
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow — Worker MCP Strip Helper
5
+ *
6
+ * Generates a channel-only MCP config for worker boot. This is the proper
7
+ * fix for the audit-channel-transport-001 regression: Story A originally
8
+ * wrote `{"mcpServers":{}}` (fully empty) for boot speed, which silently
9
+ * stripped the `wogi-workspace-channel` MCP server — leaving manager-side
10
+ * `workspace_send_message` HTTP-POSTs unable to reach the worker.
11
+ *
12
+ * This script reads the worker member-repo's real `.mcp.json`, extracts
13
+ * ONLY the `wogi-workspace-channel` entry, and writes a channel-only
14
+ * config to a destination path. Result:
15
+ * - claude.ai MCP integrations remain stripped (Story A's boot-speed win)
16
+ * - The workspace transport remains active (manager dispatch works)
17
+ *
18
+ * Fallback: if the source `.mcp.json` doesn't define
19
+ * `wogi-workspace-channel` (e.g. the worker isn't a workspace member),
20
+ * the destination is written with `{"mcpServers":{}}` — harmless in
21
+ * non-workspace contexts.
22
+ *
23
+ * Usage:
24
+ * node flow-worker-mcp-strip.js <source-mcp.json> <dest-mcp.json>
25
+ *
26
+ * Programmatic:
27
+ * const { extractChannelOnlyConfig, writeChannelOnlyConfig } =
28
+ * require('./flow-worker-mcp-strip');
29
+ * const cfg = extractChannelOnlyConfig(srcPath);
30
+ * writeChannelOnlyConfig(destPath, cfg);
31
+ *
32
+ * Exit codes:
33
+ * 0 — success (channel-only or empty config written)
34
+ * 1 — write failure (caller should fall back to no-strip)
35
+ */
36
+
37
+ 'use strict';
38
+
39
+ const fs = require('node:fs');
40
+ const path = require('node:path');
41
+
42
+ const CHANNEL_SERVER_NAME = 'wogi-workspace-channel';
43
+
44
+ /**
45
+ * Read the source `.mcp.json` and return the channel-only config object.
46
+ * Never throws; returns the empty-config fallback on any failure.
47
+ */
48
+ function extractChannelOnlyConfig(sourcePath) {
49
+ const empty = { mcpServers: {} };
50
+ if (!sourcePath || typeof sourcePath !== 'string') return empty;
51
+ try {
52
+ if (!fs.existsSync(sourcePath)) return empty;
53
+ const raw = fs.readFileSync(sourcePath, 'utf-8');
54
+ const parsed = JSON.parse(raw);
55
+ if (!parsed || typeof parsed !== 'object' || !parsed.mcpServers) return empty;
56
+ const entry = parsed.mcpServers[CHANNEL_SERVER_NAME];
57
+ if (!entry || typeof entry !== 'object') return empty;
58
+ return { mcpServers: { [CHANNEL_SERVER_NAME]: entry } };
59
+ } catch (_err) {
60
+ return empty;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Atomically write the channel-only config to destPath. Returns true on
66
+ * success, false on failure (caller should fall back).
67
+ */
68
+ function writeChannelOnlyConfig(destPath, config) {
69
+ if (!destPath || typeof destPath !== 'string') return false;
70
+ try {
71
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
72
+ const tmp = `${destPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
73
+ fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n');
74
+ fs.renameSync(tmp, destPath);
75
+ return true;
76
+ } catch (_err) {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Whether the resulting config preserves the channel transport (i.e. the
83
+ * worker will be reachable from the manager). Useful for callers that want
84
+ * to log a warning if dispatch will silently fail.
85
+ */
86
+ function preservesChannelTransport(config) {
87
+ return Boolean(
88
+ config &&
89
+ config.mcpServers &&
90
+ config.mcpServers[CHANNEL_SERVER_NAME] &&
91
+ typeof config.mcpServers[CHANNEL_SERVER_NAME] === 'object'
92
+ );
93
+ }
94
+
95
+ module.exports = {
96
+ CHANNEL_SERVER_NAME,
97
+ extractChannelOnlyConfig,
98
+ writeChannelOnlyConfig,
99
+ preservesChannelTransport
100
+ };
101
+
102
+ if (require.main === module) {
103
+ const [src, dest] = process.argv.slice(2);
104
+ if (!src || !dest) {
105
+ process.stderr.write('Usage: flow-worker-mcp-strip <source-mcp.json> <dest-mcp.json>\n');
106
+ process.exit(1);
107
+ }
108
+ const cfg = extractChannelOnlyConfig(src);
109
+ const ok = writeChannelOnlyConfig(dest, cfg);
110
+ if (!ok) {
111
+ process.stderr.write(`[flow-worker-mcp-strip] failed to write ${dest}\n`);
112
+ process.exit(1);
113
+ }
114
+ if (!preservesChannelTransport(cfg)) {
115
+ process.stderr.write(
116
+ `[flow-worker-mcp-strip] WARNING: ${src} did not define ${CHANNEL_SERVER_NAME} — ` +
117
+ `worker will boot but manager dispatch will fail. ` +
118
+ `Run "flow workspace init" in the workspace root to regenerate .mcp.json.\n`
119
+ );
120
+ }
121
+ process.exit(0);
122
+ }