Skip to content

refactor: split the run orchestrator god-function into phase modules#154

Merged
jithin23-kv merged 1 commit into
masterfrom
refactor-run-engine
Jul 2, 2026
Merged

refactor: split the run orchestrator god-function into phase modules#154
jithin23-kv merged 1 commit into
masterfrom
refactor-run-engine

Conversation

@prasanth-nair-kv

@prasanth-nair-kv prasanth-nair-kv commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

What & why

runAll was a ~275-line function (depth-10 nesting, a labeled break) fusing five
jobs. This dissolves the god-function into a phase pipelinerunAll is now a
thin orchestrator (resolve → preflight → execute → report), dropping
843 → 278 lines.

  • baselineScanner.ts (new) — the MCP pre-flight scans as a Chain of
    Responsibility
    : runBaselineScans + resource / tool-description / rug-pull
    scanners; each contributes an EvaluatorResult or nothing, pushed in order.
  • evaluatorLoop.ts (new) — runEvaluatorAttacks, the evaluator attack loop.
    The labeled break evaluatorLoop becomes a return; captureSessionContext
    moves in.

Backward-compatible

runAll(config, options) keeps its exact signature — CLI, SDK, MCP, and
browser callers are untouched. The RunEngine/RunRequest boundary from the plan
is intentionally deferred: it would rewire the front-ends and change the public
API, conflicting with the backward-compat requirement. The phase pipeline delivers
the SRP/clean-code win without touching the API.

Behavior-preserving

evaluatorResults ordering (scans then evaluators), partial-report-on-stop,
mcpTarget.close() in a finally, topo-sort order, dependency-skip, and
upstream-session threading are all reproduced. Guarded by the runAll.smoke +
orchestrator.equivalence drift tests (18/18 green).

Clean-code / DRY

The two verbatim-duplicated stop-error branches (isStopError vs
TargetStopError) and the thrice-repeated evaluatorMeta object are consolidated;
makeFailedResult now uses errorJudge(). New baselineScanner test covers the
chain (the MCP pre-flight had no coverage before).

Deferred (logged in the plan; pre-existing, out of a behavior-preserving refactor)

Three pre-existing baseline-scanner bugs surfaced by the review (kept out, need
their own PR + tests): the "ERROR: " read-failure sentinel collision (a
security false-negative), a zero-byte baseline treated as "first run", and an
unguarded JSON.parse of the on-disk baseline. Also: consolidating the
runAllBrowser twin loop, and minor nits.

Verification

full suite 121 tests, 0 fail (3 skips) · typecheck 0 · lint 0 · prettier clean ·
full build (incl. browser bundle).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added automatic pre-flight checks for MCP targets before evaluator attacks begin.
    • Improved execution flow so evaluators now run in a more structured, sequential way with clearer progress handling.
    • Added baseline tracking for tool definitions, with change detection when a target’s tools differ from the saved snapshot.
  • Tests

    • Added coverage for baseline scanning when no resources or tools are available.

runAll was a ~275-line function (depth-10 nesting, a labeled break) fusing five
jobs. Extract the two big inline blocks so runAll becomes a thin orchestrator —
resolve -> preflight -> execute -> report — dropping 843 -> 278 lines:

- baselineScanner.ts: the MCP pre-flight scans as a Chain of Responsibility
  (runBaselineScans + the resource/tool-description/rug-pull scanners). Each
  scanner contributes an EvaluatorResult or nothing; runAll pushes them in order.
- evaluatorLoop.ts: runEvaluatorAttacks — the evaluator attack loop. The labeled
  `break evaluatorLoop` becomes a return; captureSessionContext moves in. The two
  verbatim-duplicated stop-error branches (isStopError vs TargetStopError) and the
  thrice-repeated evaluatorMeta object are consolidated (DRY), and makeFailedResult
  now uses errorJudge().

Backward-compatible: runAll(config, options) keeps its exact signature, so all
callers (CLI, SDK, MCP, browser) are untouched. The RunEngine/RunRequest boundary
from the plan is intentionally deferred — it would rewire the front-ends and
change the public API. Behavior-preserving: evaluatorResults ordering (scans then
evaluators), partial-report-on-stop, mcpTarget.close() in a finally, topo-sort,
dependency-skip, and upstream-session threading are all reproduced; the
runAll.smoke and orchestrator.equivalence drift guards stay green.

New baselineScanner test covers the chain (the MCP pre-flight had no coverage).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR extracts MCP baseline pre-flight scanning and the evaluator attack loop from runAll.ts into two new modules, baselineScanner.ts and evaluatorLoop.ts, and refactors runAll.ts to delegate to them. A new test covers baseline scanning behavior on empty targets.

Changes

Baseline Scan and Evaluator Loop Extraction

Layer / File(s) Summary
Baseline scan chain
core/src/execute/baselineScanner.ts
Adds runBaselineScans orchestrating sequential resource-exposure, tool-description-scan, and rug-pull-detection scanners, plus scanResources for judging MCP resource contents against OWASP-MCP MCP01.
Tool description scanning
core/src/execute/baselineScanner.ts
Adds scanToolDescriptions to judge tool descriptions for hidden directives/exfiltration/override phrases (OWASP-MCP MCP03).
Rug-pull detection and diffing
core/src/execute/baselineScanner.ts
Adds scanRugPull to hash and compare tool snapshots against a stored baseline JSON, and computeToolDiffs to produce removed/added/changed tool evidence.
Baseline scanner tests
core/tests/baselineScanner.test.ts
Adds a test verifying runBaselineScans on an empty target returns a single PASS rug-pull-detection result with baseline-recorded reasoning.
Evaluator loop context and execution
core/src/execute/evaluatorLoop.ts
Adds EvaluatorLoopContext and runEvaluatorAttacks, iterating evaluators, checking dependsOn prerequisites, generating and running attacks via MCP/agent runners, and handling stop conditions.
Session context capture
core/src/execute/evaluatorLoop.ts
Adds captureSessionContext to build history/verdict metadata for dependent evaluators.
runAll delegation
core/src/execute/runAll.ts
Removes inline scan/loop implementations and unused imports, delegating baseline scans and evaluator attacks to the new modules while preserving stopReason handling in the returned report.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related PRs

  • KeyValueSoftwareSystems/agent-opfor#152: Both PRs refactor the execute pipeline so runAll/the new evaluator loop dispatches MCP attacks through the shared MCP runner extracted in the related PR.

Suggested reviewers: arunSunnyKVS

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main refactor of splitting the orchestrator into phase modules.
Description check ✅ Passed The description covers the problem, solution, changes, testing, and compatibility, though it doesn't use the template's exact headings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor-run-engine

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
core/src/execute/evaluatorLoop.ts (3)

200-244: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated turn-formatting logic between the multi-turn and single-turn fallback branches.

The t.kind === "agent" vs. tool-call formatting (lines 210-219) is re-implemented almost identically in the r.kind === "agent"/r.kind === "mcp" fallback branches (lines 221-230). A small helper (e.g. formatTurnPair(kind, prompt/toolName/toolArguments, response)) would remove the duplication and prevent the two paths from drifting if the formatting ever changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/execute/evaluatorLoop.ts` around lines 200 - 244, The session
history formatting in captureSessionContext is duplicated between the multi-turn
loop and the single-turn fallback branches, so refactor the shared
user/assistant pair construction into a small helper near captureSessionContext
and reuse it for both the t.kind === "agent" / tool-call path and the r.kind ===
"agent" / r.kind === "mcp" path. Keep the helper responsible for formatting
prompt versus [tool:...] content so the behavior stays consistent and only one
place needs updates if the history format changes.

133-144: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Non-null/type assertions bypass the invariant they depend on.

mcpTarget! (line 135) and config.target as AgentTargetConfig (line 142) both assume attack.kind correctly predicts target shape/availability, but nothing in this function enforces that. If that invariant is ever violated (e.g. a future evaluator misconfiguration), the failure surfaces as a generic null-dereference/type error deep in runMcpAttack/createAgentTarget rather than a message telling the caller what's wrong.

🛡️ Suggested guard
       result =
         attack.kind === "mcp"
-          ? await runMcpAttack(attack, mcpTarget!, attackModel, judgeLlmConfig)
+          ? await runMcpAttack(
+              attack,
+              mcpTarget ?? (() => { throw new Error(`MCP attack "${attack.id}" has no connected mcpTarget`); })(),
+              attackModel,
+              judgeLlmConfig
+            )
           : await runAgentAttack(...)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/execute/evaluatorLoop.ts` around lines 133 - 144, The evaluatorLoop
execution path is relying on unsafe non-null/type assertions for the target
selection logic. Update the attack.kind branch in evaluatorLoop to explicitly
validate that the MCP path has a real mcpTarget and that the agent path has a
valid AgentTargetConfig before calling runMcpAttack or createAgentTarget. If the
expected target shape is missing or mismatched, fail early with a clear,
descriptive error that names the attack kind and the missing/invalid target
instead of using mcpTarget! or config.target as AgentTargetConfig.

19-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type-only import creates a reverse dependency on the orchestrator.

ProgressEvent is imported from ./runAll.js (the file that now imports runEvaluatorAttacks from this module per the runAll.ts snippet). Since it's import type, it's erased at compile time so there's no runtime cycle, but it still couples this extracted module back to the orchestrator it was pulled out of, undermining the stated goal of keeping runAll.ts a thin orchestrator on top of lower-level modules. Consider hoisting ProgressEvent into types.ts (alongside RunConfig, SessionContext, etc.) so evaluatorLoop.ts, baselineScanner.ts, and runAll.ts all import it from a shared, dependency-free location.

♻️ Suggested fix
-import type { ProgressEvent } from "./runAll.js";
+import type { ProgressEvent } from "./types.js";

And move the ProgressEvent interface definition from runAll.ts into types.ts, updating runAll.ts's own import accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/execute/evaluatorLoop.ts` around lines 19 - 28, The evaluatorLoop.ts
imports ProgressEvent from runAll.ts, which creates an unnecessary dependency
back to the orchestrator. Move the ProgressEvent type definition into the shared
types.ts module alongside RunConfig and SessionContext, then update
evaluatorLoop.ts, baselineScanner.ts, and runAll.ts to import it from that
shared location so runAll.ts stays a thin top-level coordinator.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/src/execute/baselineScanner.ts`:
- Around line 295-296: The baseline update flow in baselineScanner should not
overwrite the trusted baseline immediately after detecting drift or a rug pull;
keep the existing baseline unchanged and store the new snapshot as a separate
candidate instead. Update the logic around writeFile in the baselineScanner flow
so currentJson is written only to a candidate/temporary artifact, and reserve
any baselinePath overwrite for an explicit accept path. Apply the same change to
the other update site referenced in the scanner logic so both paths use the same
approved-baseline behavior.
- Around line 221-227: The drift check in baselineScanner currently fails on raw
JSON hash mismatches even when computeToolDiffs returns no real changes, so
reorder-only tools/list responses can incorrectly fail. Update the drift path
around currentSnapshot/currentHash and the FAIL decision to use semantic
comparison first: either canonicalize the tool snapshot before hashing or, at
minimum, treat computeToolDiffs(...).length === 0 as PASS before marking drift.
Keep the logic in baselineScanner aligned with the parsing/diffing flow so
snapshot order does not trigger false failures.
- Around line 286-290: The baseline parsing in baselineScanner should not assume
the stored JSON is valid. In the logic that reads baselineJson and builds
baselineSnapshot, first parse as unknown, validate the result with the existing
Zod schema, and if validation fails return an ERROR result with a clear
invalid-baseline message instead of letting JSON.parse or shape mismatches abort
the scan. Keep the fix localized around the baselineSnapshot creation path so
attack_done still gets emitted and progress state is not left hanging after
attack_start.

---

Nitpick comments:
In `@core/src/execute/evaluatorLoop.ts`:
- Around line 200-244: The session history formatting in captureSessionContext
is duplicated between the multi-turn loop and the single-turn fallback branches,
so refactor the shared user/assistant pair construction into a small helper near
captureSessionContext and reuse it for both the t.kind === "agent" / tool-call
path and the r.kind === "agent" / r.kind === "mcp" path. Keep the helper
responsible for formatting prompt versus [tool:...] content so the behavior
stays consistent and only one place needs updates if the history format changes.
- Around line 133-144: The evaluatorLoop execution path is relying on unsafe
non-null/type assertions for the target selection logic. Update the attack.kind
branch in evaluatorLoop to explicitly validate that the MCP path has a real
mcpTarget and that the agent path has a valid AgentTargetConfig before calling
runMcpAttack or createAgentTarget. If the expected target shape is missing or
mismatched, fail early with a clear, descriptive error that names the attack
kind and the missing/invalid target instead of using mcpTarget! or config.target
as AgentTargetConfig.
- Around line 19-28: The evaluatorLoop.ts imports ProgressEvent from runAll.ts,
which creates an unnecessary dependency back to the orchestrator. Move the
ProgressEvent type definition into the shared types.ts module alongside
RunConfig and SessionContext, then update evaluatorLoop.ts, baselineScanner.ts,
and runAll.ts to import it from that shared location so runAll.ts stays a thin
top-level coordinator.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dd7d43c8-1c1c-4661-a8b1-75f4091f3c2b

📥 Commits

Reviewing files that changed from the base of the PR and between 682c466 and 2e6806d.

📒 Files selected for processing (4)
  • core/src/execute/baselineScanner.ts
  • core/src/execute/evaluatorLoop.ts
  • core/src/execute/runAll.ts
  • core/tests/baselineScanner.test.ts

Comment on lines +221 to +227
const currentSnapshot = tools.map((t) => ({
name: t.name,
description: t.description ?? "",
inputSchema: t.inputSchema ?? null,
}));
const currentJson = JSON.stringify(currentSnapshot, null, 2);
const currentHash = createHash("sha256").update(currentJson).digest("hex");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use semantic diffs before failing drift.

Line 267 compares raw JSON hashes before parsing/diffing, so a different tools/list order can produce a FAIL with 0 actual diffs. Canonicalize snapshots or treat computeToolDiffs(...).length === 0 as PASS.

Also applies to: 266-291, 344-346

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/execute/baselineScanner.ts` around lines 221 - 227, The drift check
in baselineScanner currently fails on raw JSON hash mismatches even when
computeToolDiffs returns no real changes, so reorder-only tools/list responses
can incorrectly fail. Update the drift path around currentSnapshot/currentHash
and the FAIL decision to use semantic comparison first: either canonicalize the
tool snapshot before hashing or, at minimum, treat computeToolDiffs(...).length
=== 0 as PASS before marking drift. Keep the logic in baselineScanner aligned
with the parsing/diffing flow so snapshot order does not trigger false failures.

Comment on lines +286 to +290
const baselineSnapshot = JSON.parse(baselineJson) as Array<{
name: string;
description: string;
inputSchema: unknown;
}>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file around the cited lines
nl -ba core/src/execute/baselineScanner.ts | sed -n '240,340p'

printf '\n--- search baselineJson usage ---\n'
rg -n "baselineJson|attack_start|JSON\.parse|Zod|z\." core/src/execute/baselineScanner.ts

printf '\n--- outline of file ---\n'
ast-grep outline core/src/execute/baselineScanner.ts --view expanded

Repository: KeyValueSoftwareSystems/agent-opfor

Length of output: 213


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path("core/src/execute/baselineScanner.ts")
lines = path.read_text().splitlines()
for start, end in [(240, 340)]:
    for i in range(start, min(end, len(lines)) + 1):
        print(f"{i:4d}  {lines[i-1]}")
PY

printf '\n--- search baselineJson usage ---\n'
rg -n "baselineJson|attack_start|JSON\.parse|Zod|z\." core/src/execute/baselineScanner.ts

printf '\n--- outline of file ---\n'
ast-grep outline core/src/execute/baselineScanner.ts --view expanded

Repository: KeyValueSoftwareSystems/agent-opfor

Length of output: 5806


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path("core/src/execute/baselineScanner.ts")
lines = path.read_text().splitlines()
for start, end in [(90, 180), (180, 240), (240, 330), (330, 420)]:
    print(f"\n--- lines {start}-{end} ---")
    for i in range(start, min(end, len(lines)) + 1):
        print(f"{i:4d}  {lines[i-1]}")
PY

printf '\n--- search for catch/finally in baselineScanner.ts ---\n'
rg -n "try|catch|finally|attack_done|notify\\(" core/src/execute/baselineScanner.ts

printf '\n--- search for baseline file validation elsewhere ---\n'
rg -n "baseline.*schema|Zod|zod|safeParse|JSON.parse\\(baselineJson\\)|computeToolDiffs|tool-description-drift" core/src -g '!**/dist/**'

Repository: KeyValueSoftwareSystems/agent-opfor

Length of output: 15062


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path("core/src/execute/baselineScanner.ts")
lines = path.read_text().splitlines()
for start, end in [(1, 90), (60, 120), (120, 240)]:
    print(f"\n--- lines {start}-{end} ---")
    for i in range(start, min(end, len(lines)) + 1):
        print(f"{i:4d}  {lines[i-1]}")
PY

printf '\n--- search for runBaselineScans callers / error handling ---\n'
rg -n "runBaselineScans|Promise\\.all|catch \\(|notify\\({ type: \"attack_done\"|attack_start" core/src -g '!**/dist/**'

Repository: KeyValueSoftwareSystems/agent-opfor

Length of output: 11565


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for rel in ["core/src/run/runAll.ts", "core/src/execute/baselineScanner.ts"]:
    path = Path(rel)
    if path.exists():
        lines = path.read_text().splitlines()
        print(f"\n--- {rel} ---")
        for i, line in enumerate(lines, 1):
            if "runBaselineScans" in line or "attack_start" in line or "attack_done" in line or "catch" in line or "finally" in line:
                print(f"{i:4d}  {line}")
PY

printf '\n--- search baseline scan callers ---\n'
rg -n "runBaselineScans\\(" core/src

Repository: KeyValueSoftwareSystems/agent-opfor

Length of output: 1180


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path("core/src/execute/runAll.ts")
lines = path.read_text().splitlines()
for start, end in [(1, 140), (140, 240)]:
    print(f"\n--- lines {start}-{end} ---")
    for i in range(start, min(end, len(lines)) + 1):
        print(f"{i:4d}  {lines[i-1]}")
PY

Repository: KeyValueSoftwareSystems/agent-opfor

Length of output: 9940


Validate the stored baseline before diffing it.
JSON.parse(baselineJson) can throw on a malformed baseline file, which aborts the scan before attack_done is emitted and leaves the progress state hanging after attack_start. Parse as unknown, validate with a Zod schema, and return an ERROR result with a clear message when the baseline is invalid.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/execute/baselineScanner.ts` around lines 286 - 290, The baseline
parsing in baselineScanner should not assume the stored JSON is valid. In the
logic that reads baselineJson and builds baselineSnapshot, first parse as
unknown, validate the result with the existing Zod schema, and if validation
fails return an ERROR result with a clear invalid-baseline message instead of
letting JSON.parse or shape mismatches abort the scan. Keep the fix localized
around the baselineSnapshot creation path so attack_done still gets emitted and
progress state is not left hanging after attack_start.

Source: Coding guidelines

Comment on lines +295 to +296
await mkdir(baselinesDir, { recursive: true });
await writeFile(baselinePath, currentJson, "utf8");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not auto-accept detected drift as the new baseline.

After detecting a rug pull, Line 296 overwrites the trusted baseline with the changed snapshot. A second run will then PASS without explicit approval. Keep the original baseline and write the new snapshot as a candidate, or only update it through an explicit accept flow.

Also applies to: 313-313

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/execute/baselineScanner.ts` around lines 295 - 296, The baseline
update flow in baselineScanner should not overwrite the trusted baseline
immediately after detecting drift or a rug pull; keep the existing baseline
unchanged and store the new snapshot as a separate candidate instead. Update the
logic around writeFile in the baselineScanner flow so currentJson is written
only to a candidate/temporary artifact, and reserve any baselinePath overwrite
for an explicit accept path. Apply the same change to the other update site
referenced in the scanner logic so both paths use the same approved-baseline
behavior.

@jithin23-kv
jithin23-kv merged commit 36fa8af into master Jul 2, 2026
7 of 8 checks passed
@jithin23-kv
jithin23-kv deleted the refactor-run-engine branch July 2, 2026 09:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants