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.
- package/.claude/docs/intent-grounded-reasoning.md +1 -1
- package/.workflow/templates/partials/methodology-rules.hbs +31 -0
- package/lib/mode-schema.js +2 -1
- package/lib/wogi-claude +70 -5
- package/lib/workspace-messages.js +2 -1
- package/lib/workspace.js +3 -3
- package/package.json +2 -2
- package/scripts/flow-config-loader.js +3 -2
- package/scripts/flow-correction-detector.js +3 -2
- package/scripts/flow-orchestrate-corrections.js +158 -0
- package/scripts/flow-orchestrate.js +16 -97
- package/scripts/flow-prompt-composer.js +3 -2
- package/scripts/flow-prompt-template.js +3 -2
- package/scripts/flow-worker-mcp-strip.js +122 -0
|
@@ -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
|
|
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:
|
package/lib/mode-schema.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
111
|
-
#
|
|
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-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
corrected = corrected
|
|
403
|
-
corrections.push(
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
+
}
|