Skip to content

FE-761: Petri-net semantic alignment for Petrinaut visualization#156

Open
kostandinang wants to merge 10 commits into
ka/fe-755-cook-codebase-modefrom
ka/fe-761-petri-petrinaut-semantics
Open

FE-761: Petri-net semantic alignment for Petrinaut visualization#156
kostandinang wants to merge 10 commits into
ka/fe-755-cook-codebase-modefrom
ka/fe-761-petri-petrinaut-semantics

Conversation

@kostandinang
Copy link
Copy Markdown
Contributor

@kostandinang kostandinang commented May 27, 2026

Summary

  • Sibling transitions for conditional branchingevaluate, run-tests, assess-semantic, and verify-epic no longer select between output sets; each producer emits a report-bearing token to a single :reported intermediate place, and a pair of sibling-passthrough transitions with complementary EnablingGuards does the routing.
  • Halt as a structural placeslice:<sid>:halted / epic:<eid>:halted places replace ctx.halted / ctx.haltReason; the engine derives halt status from net.hasHaltToken() and reason from the halt token's haltReason field.
  • Async handler dispatch + explicit dispatch/running/complete topology — every long-running producer compiles as <step>:dispatch (sync, kind dispatch) → <step>:running (sentinel place) → <step>:complete (existing handler descriptor, scheduled deferred). Handler invocation no longer blocks the petri-net step loop.

Context

  • Blocker for the rest of the Orchestrator ↔ Petrinaut sub-track (FE-762 blueprint export, FE-763 event stream, FE-764 sync server). Without this, any blueprint shipped to Petrinaut is structurally unrenderable: Petrinaut requires every transition to emit to all declared outputs (fan-out, not selection) and treats transitions as instantaneous events.
  • Stacks on FE-747 declarative routing. That frontier introduced HandlerDescriptor output-set selection (onTrue / onFalse / onPass / onFail / onSatisfied / onRejected) which this frontier dismantles in favor of sibling transitions.
  • Token enrichment in the transition kernel is preserved — sibling siblings carry the producer's report via an attached reportId, consistent with the cross-team-agreed payload shape.

What changed

  • net-blueprint.ts: new EnablingGuard type (peek-time predicate over the input token's attached report); new SiblingPassthroughDescriptor (with optional onFire hook for epic-completion mark / halt-reason stamp); new DispatchDescriptor; enumerateCandidateOutputs updated for the new variants. The producer descriptors (action / run-tests / assess-semantic / verify-epic) lost their branching fields and now each emit one fixed output set.
  • net-compiler.ts: per slice, every former producer compiles as two transitions (:dispatch:complete) around a new :running sentinel place, plus the existing :reported intermediate + sibling-passthrough pair. wireHandlers got a dispatch case (forwards the work token to the running place, stashing retryCount / reworkCount from the companion budget token so the complete-phase handler can read it back). run-tests / assess-semantic complete handlers updated accordingly.
  • petri-net.ts: TransitionDef.guard enforces peek-time EnablingGuard mutual exclusion in isEnabled. Token gains optional haltReason. PetriNet gains hasHaltToken() / getHaltTokens() introspection and scheduleDeferred(transitionId, contract, consumedPlaces, work); runSerial / runParallel await in-flight deferred completions before declaring deadlock.
  • engine.ts / types.ts: RunCtx.halted / RunCtx.haltReason removed; engine's halt signal is net.hasHaltToken() and reason is derived from halt tokens.
  • Topology deltas (counts pinned in adapter tests):
    • simplePlan (1 slice): 17 → 22 places, 14 → 19 transitions.
    • depPlan (2 slices): 32 → 42 places, 27 → 37 transitions.
    • Per verified epic: +1 verify:running place, +1 :halted place, +1 :dispatch transition.

Verification

  • 99 orchestrator tests pass (was 95 at frontier start). Topology adapter tests pin the new sibling decomposition, halted places, and dispatch/running/complete shape per fixture.
  • Engine-contract suite green end-to-end on fixtures/txt/ semantics; deferred-fire substrate proven by serial-policy concurrency oracle (maxConcurrent > 1 under serial when handler-bound).
  • Full npm run verify (check + test + build) green.
  • Outer-loop cook smoke on real pi is deferred to FE-762 integration validation.

Out of scope

  • Petrinaut blueprint export wire format (petri-blueprint-export, FE-762).
  • Petrinaut event stream payload shape (petri-event-stream, FE-763).
  • Sync transport (petri-sync-server, FE-764).
  • Read-arc concurrency semantics in Petrinaut — pending Petrinaut team confirmation. Today's pool / budget tokens still use consume+return to preserve capacity-bounding.

Traceability

  • Linear FE-761 (parent FE-760); SPEC §Substrate; frontier petri-petrinaut-semantics in memory/PLAN.md; scope cards 1–4 in memory/CARDS.md; stacks on FE-747.

@kostandinang kostandinang changed the title FE-761: Plan reconciliation — Petrinaut sub-track frontier definitions FE-761: Petri-net semantic alignment for Petrinaut visualization May 27, 2026
@kostandinang kostandinang changed the base branch from ka/fe-747-petri-declarative-routing to graphite-base/156 May 27, 2026 22:48
@kostandinang kostandinang force-pushed the ka/fe-761-petri-petrinaut-semantics branch from 778f22f to 8ddb5c1 Compare May 27, 2026 22:48
@kostandinang kostandinang changed the base branch from graphite-base/156 to ka/fe-755-cook-codebase-mode May 27, 2026 22:48
@kostandinang kostandinang force-pushed the ka/fe-755-cook-codebase-mode branch from e549e84 to ecc2cc5 Compare May 27, 2026 23:16
@kostandinang kostandinang force-pushed the ka/fe-761-petri-petrinaut-semantics branch from 8ddb5c1 to e4be997 Compare May 27, 2026 23:16
@kostandinang kostandinang marked this pull request as ready for review May 27, 2026 23:17
@cursor
Copy link
Copy Markdown

cursor Bot commented May 27, 2026

PR Summary

Medium Risk
Large structural changes to net compilation, firing, halt detection, and concurrency semantics in the core orchestrator; mitigated by expanded contract/topology tests but outer-loop cook smoke is deferred.

Overview
Refactors the orchestrator Petri net so compiled topology matches Petrinaut semantics (FE-761), unblocking blueprint export and event streaming.

Branching no longer picks output sets inside HandlerDescriptor (onTrue/onPass/etc.). Producers emit to a single :reported place; sibling passthrough transitions with complementary **EnablingGuard**s route outcomes. Halt is structural: tokens on slice:*:halted / epic:*:halted with haltReason on the token; RunCtx.halted is removed and the engine uses net.hasHaltToken().

Long-running steps use scheduleDeferred so handlers do not block the step loop (serial policy can still overlap handlers within the agent pool). Each former producer is split into :dispatchrunning:*:complete so in-flight work is visible in the blueprint.

Topology counts rise (e.g. simplePlan: 17→22 places, 14→19 transitions). 99 orchestrator tests updated; memory/PLAN.md / memory/CARDS.md document the four landed slices.

Reviewed by Cursor Bugbot for commit 94351f0. Bugbot is set up for automated code reviews on this repo. Configure here.

kostandinang and others added 10 commits May 28, 2026 01:20
Re-apply the four Petrinaut sub-track frontiers committed during the
2026-05-26 cross-team alignment:

- petri-petrinaut-semantics (FE-761) — Petri-net-faithful refactor of
  FE-747 conditional outputs + start/end pairs for agent dispatch.
- petri-blueprint-export (FE-762) — Petrinaut-format JSON of NetBlueprint
  per cook run.
- petri-event-stream (FE-763) — initial markings + transition firings in
  the cross-team-agreed payload shape.
- petri-sync-server (FE-764) — transport, brunch -> Petrinaut, one-way v1.

Plus Sequencing -> Active item 3 referencing umbrella FE-760 and the
Dependencies -> TRACK F branch under petri-declarative-routing.

Adds memory/CARDS.md with the FE-761 prepared scope-card queue (Slice 1:
sibling transitions for conditional branching; Slice 2 to be scoped after
Slice 1 lands). Retires HANDOFF.md per its retirement rule.

Co-authored-by: Amp <amp@ampcode.com>
…lingGuard infra

Establishes the producer + sibling-passthrough pattern for Petri-net-faithful
conditional branching. Each conditional action-transition will eventually
decompose into:

  1. one producer (kind: action) that runs the work synchronously, attaches
     the resulting reportId to the output token, and emits to a single new
     intermediate place 'slice:<sliceId>:<step>:reported'
  2. N sibling passthroughs that consume from the intermediate place,
     evaluate an EnablingGuard against the token's attached report, and
     emit to a single fixed output set

Sibling mutual exclusion is enforced at peek time: PetriNet.isEnabled now
evaluates an optional TransitionDef.guard against the first token in each
input place before considering the transition enabled. Complementary guards
across siblings (truthy / falsy on the same report field) ensure exactly
one sibling fires per intermediate token.

This commit applies the pattern to the 'evaluate' transition only. The
remaining branching transitions (run-tests, assess-semantic, verify-epic)
will be decomposed in subsequent Slice 1 commits — each carries stateful
budget / halt concerns that the scope card's risk note authorized to keep
in fire closures for Slice 1.

Topology delta per slice (so far): +1 place (evaluate:reported), +2
transitions (evaluate:done, evaluate:more).

Engine-contract suite (43 tests) green — runtime equivalence preserved.

Co-authored-by: Amp <amp@ampcode.com>
…ss-semantic, verify-epic

Extends Slice 1a's producer + sibling-passthrough pattern to the remaining conditional transitions:

- run-tests: producer emits to slice:<sid>:run-tests:reported with retry budget; siblings run-tests:pass / run-tests:fail branch via enabling guards. Fail-side halt/budget exhaustion still in fire closure (deferred to Slice 2).
- assess-semantic: producer emits to slice:<sid>:assess-semantic:reported; siblings assess-semantic:satisfied / assess-semantic:rejected branch via guards. Rework budget handling remains in fire closure.
- verify-epic: producer emits to epic:<epicId>:verify:reported; siblings for pass/fail branch via guards. SiblingPassthroughDescriptor gains optional onFire; verify-epic siblings use onFire to mark epic completed or halted in ctx.

All 95 orchestrator tests pass; topology goldens updated for new intermediate places and sibling transitions. Full verify gate green (fmt + lint + build).

Refs FE-761

Co-authored-by: Amp <amp@ampcode.com>
…tion)

Slice 1 (sibling-transition decomposition for conditional branching) landed
across commits 3b7b860 (1a: evaluate + EnablingGuard infra) and 8b76629
(1b: run-tests + assess-semantic + verify-epic). All 95 orchestrator tests
green; full verify gate passes.

Scope Slice 2 as full scope card: split each producer transition into
dispatch:<step> + running:<step>:<scopeId> + complete:<step>:<outcome>
sibling pair; retire ctx.halted in favor of a halted:<scopeId> place. This
extends the Slice 1 report-bearing-token contract with async completion,
unblocking handler invocation from the petri-net step loop.

Refs FE-761

Co-authored-by: Amp <amp@ampcode.com>
First tracer of Slice 2 (dispatch/complete decomposition). Makes halt
observable at the topology level without yet retiring ctx.halted or
introducing async dispatch — those follow in Slice 2b.

- New places: slice:<sid>:halted for every slice (retry / semantic-rework
  exhaustion sink); epic:<eid>:halted for every epic with verification
  (verify-epic-fail sink).
- run-tests producer: on retry exhaustion, emits halt token to
  slice:<sid>:halted (in addition to legacy ctx.halted mutation).
- assess-semantic producer: on rework exhaustion, emits halt token to
  slice:<sid>:halted similarly.
- verify-epic fail sibling: outputs now [epic:<eid>:halted] instead of [];
  onFire halt-epic still mutates ctx for now.
- petri-net.ts BENIGN_RESIDUAL_PLACES: 'halted' added so halt tokens do
  not trigger spurious net_deadlocked events.

Topology tests pin the new halted-place declarations; engine-contract
adapter counts updated (simplePlan 16→17 places, depPlan 30→32). All 98
orchestrator tests pass; full verify gate green.

Refs FE-761

Co-authored-by: Amp <amp@ampcode.com>
Completes the halted-as-place refactor begun in Slice 2a. Halt is now
purely structural: the engine's halt signal is net.hasHaltToken() reading
tokens on :halted places, and the halt reason is carried on the halt
token itself (Token.haltReason).

Changes:
- petri-net.ts: Token gains optional haltReason. PetriNet gains
  hasHaltToken() and getHaltTokens() introspection that scan all places
  whose suffix is ':halted'.
- types.ts: RunCtx loses halted and haltReason fields; doc comment
  explains the place-token derivation that replaces them.
- engine.ts: shouldHalt uses net.hasHaltToken(); post-run derives
  result.reason from net.getHaltTokens() (first halt token's haltReason)
  and falls back to 'Some slices or epics were never reached' when
  outcomes are missing.
- net-compiler.ts: run-tests and assess-semantic producers emit a halt
  token carrying haltReason on budget exhaustion instead of mutating ctx.
  Sibling-passthrough fire honors a new attach-halt-reason onFire variant
  by stamping haltReason on the forwarded token; verify-epic fail sibling
  switched to that variant.
- net-blueprint.ts: SiblingPassthroughDescriptor.onFire halt-variant
  renamed halt-epic → attach-halt-reason; producer doc comments updated to
  reflect halted-as-place final state.
- engine-contract.test.ts: two RunCtx fixtures drop halted: false; the
  net_halted retry-exhaustion test now uses net.hasHaltToken() as the
  shouldHalt callback (and is renamed accordingly).

All 98 orchestrator tests pass; full verify gate green.

Refs FE-761

Co-authored-by: Amp <amp@ampcode.com>
…tch/complete async)

Slice 2 landed as two tracers — Slice 2a (commit d2878f9) added the
halted:<scopeId> places and emitted halt tokens to them; Slice 2b (commit
c58ee62) retired ctx.halted/haltReason entirely, with the engine reading
halt status from net.hasHaltToken() and halt reason from the halt token's
haltReason field.

Original scope card bundled halted-as-place with the dispatch/complete
async refactor. In practice those are independent: structural place
addition + ctx retirement vs runtime-loop architectural lift. Splitting
them shipped a clean structural win without taking on async risk in the
same commit window. Dispatch/complete is now Slice 3, still next in queue.

Refs FE-761

Co-authored-by: Amp <amp@ampcode.com>
Decouples handler invocation from synchronous fire(), letting the engine
step other independent transitions while handlers are in flight. Chosen
shape: PetriNet exposes scheduleDeferred() that producer fire closures
use to enqueue asynchronous follow-up work, rather than restructuring the
topology into explicit dispatch+complete sibling pairs.

Rationale for the scope adjustment: the original Slice 3 card called for
a topology split (running:* places + complete:<outcome> sibling pairs per
producer). In practice that would have entangled ~6 existing tests that
hardcode <slice>:<step> transition ids while delivering no new
observable runtime behavior beyond what the deferred-fire pattern
already provides. The deferred-fire approach ships the runtime
acceptance criterion (async-completion-ordering) with zero test churn
and preserves all existing topology assertions. The topology split
remains a possible future refinement if richer in-flight observability
is needed.

Changes:
- petri-net.ts: PetriNet.scheduleDeferred(transitionId, contract,
  consumedPlaces, work) enqueues a Promise whose resolved output tokens
  are deposited into the net when it settles (and a transition_fired
  event is emitted for the deferred completion). runSerial and runParallel
  await an in-flight deferred completion before declaring deadlock when
  no transition is immediately enabled. Errors from deferred work
  surface on the next loop iteration via this.deferredError.

- net-compiler.ts: all four producer fire closures (action, run-tests,
  assess-semantic, verify-epic) restructured to schedule their handler
  invocation as deferred work and return [] synchronously. Agent and
  budget tokens stay 'checked out' for the duration of the handler,
  preserving pool-size = handler-concurrency-limit invariants.

- engine-contract.test.ts: two test expectations updated to reflect the
  new async-dispatch semantics:
    * Serial policy now allows concurrent handlers (bounded by agent
      pool); test renamed and assertion changed from maxConcurrent === 1
      to maxConcurrent > 1.
    * Parallel-vs-serial wall-clock test renamed; assertion relaxed to
      'parallel no slower than serial + small slack' since both policies
      now enable handler overlap.

All 98 orchestrator tests pass; full verify gate green.

Refs FE-761

Co-authored-by: Amp <amp@ampcode.com>
…Slice 4 (Petrinaut topology split)

Slice 3 landed via deferred-fire pattern (commit 3f5358d) rather than the
original topology split scoped in the card. That delivers the runtime
async-completion-ordering acceptance criterion at low cost but does not
satisfy FE-761 acceptance criterion (3) — Petrinaut's blueprint export
(FE-762) still needs the dispatch + running:* + complete shape to render
in-flight state. Scope Slice 4 to deliver that topology piece on top of
Slice 3's runtime async substrate.

Refs FE-761

Co-authored-by: Amp <amp@ampcode.com>
Petrinaut (FE-762) needs the in-flight phase of every long-running producer
to be visible at the net-shape level. Slice 3 delivered async runtime via
the deferred-fire substrate but kept the topology unchanged; this slice
reifies that runtime split as explicit topology.

For each of the 5 per-slice producers (evaluate, write-tests, write-code,
run-tests, assess-semantic) and the per-verified-epic verify-epic producer:

  inputs -> [<step>:dispatch] -> <step>:running -> [<step>:complete] -> outputs

The :dispatch transition is synchronous (kind 'dispatch', new descriptor
variant): it forwards the work token to the running:* sentinel place,
stashing retryCount / reworkCount from the companion budget token so the
complete-phase handler can read it back without an extra input arc.

The :complete transition keeps the original handler descriptor
(action / run-tests / assess-semantic / verify-epic) and continues to
schedule the deferred handler invocation. Outcome routing still happens
in the existing slice-1 sibling-passthroughs off the :reported
intermediate, so no new complete:*:<outcome> siblings were needed.

Topology deltas:
- simplePlan: +5 places, +5 transitions (17->22 / 14->19)
- depPlan:    +10 places, +10 transitions (32->42 / 27->37)
- verifyPlan: +6 places, +6 transitions (per verified epic)

Tests:
- topology.test.ts: producer lookups migrated to <sid>:<step>:complete;
  new golden pinning dispatch-> running:* shape for all 5 producers (+1 test).
- engine-contract.test.ts: count goldens updated; descriptor-kinds includes
  'dispatch'; event-vocab asserts both :dispatch and :complete fire.
- 99 orchestrator tests pass (was 98); npm run fix / check / build all green.

Co-authored-by: Amp <amp@ampcode.com>
@kostandinang kostandinang force-pushed the ka/fe-755-cook-codebase-mode branch from ecc2cc5 to 3cc97a2 Compare May 27, 2026 23:21
@kostandinang kostandinang force-pushed the ka/fe-761-petri-petrinaut-semantics branch from e4be997 to 94351f0 Compare May 27, 2026 23:21
@augmentcode
Copy link
Copy Markdown

augmentcode Bot commented May 27, 2026

🤖 Augment PR Summary

Summary: Refactors the orchestrator Petri-net compiler/runtime to better match Petrinaut’s visualization semantics (fixed-output transitions + instantaneous firings).

Changes:

  • Introduces EnablingGuard + sibling-passthrough transitions to express conditional routing (evaluate / run-tests / assess-semantic / verify-epic) via mutually-exclusive enabled siblings instead of output-set selection.
  • Makes halt a structural concept by adding slice:<sid>:halted / epic:<eid>:halted places and removing RunCtx.halted/haltReason mutation.
  • Adds dispatch/running/complete topology for long-running producers: :dispatch emits to *:running sentinel places, then :complete performs the handler work.
  • Extends PetriNet with peek-time guards, halt-token introspection (hasHaltToken/getHaltTokens), and a deferred-completion mechanism (scheduleDeferred) so handler execution doesn’t block the step loop.
  • Updates topology and engine-contract tests to pin new place/transition counts, handler kinds (including dispatch), and updated event vocabulary expectations.

Technical Notes: Deferred completions now unblock deadlock detection by awaiting in-flight work when no transitions are enabled; halt reason is derived from Token.haltReason on halted-place tokens.

🤖 Was this summary useful? React with 👍 or 👎

Copy link
Copy Markdown

@augmentcode augmentcode Bot left a comment

Choose a reason for hiding this comment

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

Review completed. 3 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

haltReason = 'Some slices or epics were never reached';
}

const halted = haltReason !== undefined;
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 27, 2026

Choose a reason for hiding this comment

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

src/orchestrator/src/engine.ts:71 — halted is derived solely from haltReason, so a halt token without a haltReason would currently report status: 'completed' even though net.hasHaltToken() was true during the run. It seems safer to derive halted from the structural halt signal (net.hasHaltToken() / getHaltTokens().length) and treat the reason as optional metadata.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

this.addToken(place, token);
producedPlaces.push(place);
}
this.deferredEventSink?.emit({
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 27, 2026

Choose a reason for hiding this comment

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

src/orchestrator/src/petri-net.ts:164 — scheduleDeferred() emits a second transition_fired event for the same transitionId that already emitted in depositClaim, which means deferred transitions will appear to “fire” twice in the event stream. If downstream consumers (e.g., Petrinaut/FE-763) assume 1 event per transition firing, this could be a correctness issue.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

return out;
})();
net.scheduleDeferred(skel.id, skel.contract, skel.inputs, deferred);
return [];
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 27, 2026

Choose a reason for hiding this comment

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

src/orchestrator/src/net-compiler.ts:642 — Returning [] from these :complete transitions means they consume the running:* token immediately, so the running:* place won’t stay marked for the duration of the async handler (reducing in-flight observability for Petrinaut). This same pattern appears in the other producer completes as well.

Severity: medium

Other Locations
  • src/orchestrator/src/net-compiler.ts:739
  • src/orchestrator/src/net-compiler.ts:794
  • src/orchestrator/src/net-compiler.ts:876

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 94351f0. Configure here.

case 'assess-semantic':
for (const p of h.onSatisfied) out.add(p);
for (const p of h.onRejected) out.add(p);
out.add(h.intermediatePlace);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Halt place missing from topology output enumerator

Medium Severity

enumerateCandidateOutputs for run-tests and assess-semantic only returns intermediatePlace and budgetPlace, but the fire closures in wireHandlers can also emit to p(sliceId, 'halted') on budget exhaustion. Previously the halt path returned [] so the enumerator was correct, but now that halt emits a token to a real place, this output is invisible to the function documented as the "topology-only enumeration of reachable output places" for "static analyzers (reachability, deadlock detection, simulation)."

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 94351f0. Configure here.

sliceIdsForEpicVerifyMerge,
} from './epic-sandbox-merge.js';
import { evalRouteGuard } from './net-blueprint.js';
import { evalEnablingGuard, evalRouteGuard } from './net-blueprint.js';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unused evalRouteGuard import in net-compiler

Low Severity

evalRouteGuard is imported but no longer used anywhere in net-compiler.ts. All former call sites were replaced by direct field checks or by evalEnablingGuard in the sibling-passthrough refactor.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 94351f0. Configure here.

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.

1 participant