Skip to content

feat(scheduling): wire workshop appointment booking#2017

Merged
ctwoodwa merged 4 commits into
mainfrom
codex/scheduling-workshop-booking
Jul 12, 2026
Merged

feat(scheduling): wire workshop appointment booking#2017
ctwoodwa merged 4 commits into
mainfrom
codex/scheduling-workshop-booking

Conversation

@ctwoodwa

@ctwoodwa ctwoodwa commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

What changed

  • register Scheduling as a visible Workshop pillar
  • add a New appointment action to Calendar
  • let front-office users select a client, protocol, resource calendar, and requested start
  • add a dogfood-gated appointment endpoint backed by the existing availability and no-double-book booking service
  • persist successful appointments in the durable calendar event store

Authority boundary

This is the existing node-local, single-resource booking authority. It requires configured resource availability and rejects closed or occupied slots. It does not claim multi-resource ReservationSet atomicity, distributed authority, notifications, or protocol publication.

Validation

  • focused Carrier navigation, calendar, and scheduling tests: 26 passed
  • scheduling server tests: 18 passed
  • .NET build: passed with zero warnings/errors
  • Carrier lint: passed
  • dogfood web-client bundle: passed and emitted scheduling/calendar chunks
  • full Carrier suite: 1,630 passed; one unrelated real-TTS integration test failed on local runtime invocation

Summary by CodeRabbit

  • New Features
    • Added Scheduling to the navigation and workshop workspace.
    • Added appointment booking for rehearsals, including resource selection and booking confirmation.
    • Added a prominent option to create a new appointment from the Calendar view.
    • Added a restart-onboarding option with confirmation and page refresh.
  • Localization
    • Added Scheduling navigation labels across supported language bundles.
    • Added messaging for appointment errors, booking workflows, and onboarding restart guidance.
  • Bug Fixes
    • Improved handling of unavailable scheduling resources and invalid appointment details.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Scheduling navigation and localization are expanded, rehearsal appointments can be booked against selected resource calendars, calendar and onboarding entry points are updated, lazy routes are recognized by lint tooling, and dogfood deployment accepts a selected ref.

Changes

Scheduling experience

Layer / File(s) Summary
Scheduling navigation and localization
apps/carrier/src/navigation/*, apps/carrier/src/i18n/locales/*
Adds the scheduling navigation group, destination, permission, route labels, booking copy, rehearsal strings, and onboarding restart text.
Appointment booking API
apps/local-node-host/Health/*, apps/local-node-host/tests/Scheduling/*
Adds the appointment request contract and POST route, validates permissions and resources, invokes IBookingService, and tests invalid-resource and missing-permission responses.
Resource-based rehearsal booking
apps/carrier/src/scheduling/product/*
Loads resource calendars, requires a selected resource, posts booking requests, and renders booking progress and success state.
Calendar and onboarding entry points
apps/carrier/src/routes/calendar.tsx, apps/carrier/src/first-run/*, apps/carrier/src/scheduling/product/SchedulingHome.tsx
Adds a calendar appointment action and a confirmed onboarding restart that clears the genesis marker and reloads the page.

Lazy route lint support

Layer / File(s) Summary
Lazy route source mapping
tooling/label-continuity-lint/check.mjs
Maps lazy-loaded router paths to their dynamically imported route files.

Dogfood ref deployment

Layer / File(s) Summary
Selectable deployment ref
tooling/preview-host/deploy-dogfood.ps1
Uses DOGFOOD_REF to select the fetched worktree ref and build target, defaulting to origin/main.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant FrontOfficeRehearsal
  participant bookAppointment
  participant SchedulingDefinitionRoutes
  participant IBookingService
  User->>FrontOfficeRehearsal: select resource and review appointment
  FrontOfficeRehearsal->>bookAppointment: submit appointment details
  bookAppointment->>SchedulingDefinitionRoutes: POST scheduling appointment
  SchedulingDefinitionRoutes->>IBookingService: validate and book appointment
  IBookingService-->>SchedulingDefinitionRoutes: booking result
  SchedulingDefinitionRoutes-->>FrontOfficeRehearsal: eventId and booked status
  FrontOfficeRehearsal-->>User: show booking success
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description lacks the template's required Summary, ICM Stage, Affected Packages, and Checklist sections. Add the required template sections and checklist items, including the ICM stage, affected packages, and validation status.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: workshop scheduling now wires appointment booking.
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 codex/scheduling-workshop-booking

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.

@ctwoodwa ctwoodwa enabled auto-merge (squash) July 12, 2026 23:07
@ctwoodwa ctwoodwa merged commit 14c3ef7 into main Jul 12, 2026
35 of 36 checks passed
@ctwoodwa ctwoodwa deleted the codex/scheduling-workshop-booking branch July 12, 2026 23:47

@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: 8

🧹 Nitpick comments (8)
tooling/preview-host/deploy-dogfood.ps1 (1)

66-69: 🩺 Stability & Availability | 🔵 Trivial

chore (blocking): Add clean-checkout coverage for ref selection

Please add or link CI/operational coverage that exercises the script with DOGFOOD_REF unset and set to an explicit fetched ref, verifies the resolved SHA, and proves failed fetch/checkout paths cannot reach Stop-Service. The real deployment path and its offline/alternative test path should not be skip-gated, and this high-blast change needs an independent reviewer.

As per path instructions, CI must exercise modified paths, verification must be clean-checkout reproducible, real and offline paths must be covered, and high-blast changes must not rely on self-assertion.

Also applies to: 113-131

🤖 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 `@tooling/preview-host/deploy-dogfood.ps1` around lines 66 - 69, Add
clean-checkout CI or operational coverage for deploy-dogfood.ps1 that runs with
DOGFOOD_REF unset and set to an explicit fetched ref, verifies the selected
commit SHA, and confirms fetch or checkout failures cannot reach Stop-Service.
Ensure both the real deployment path and offline/alternative test path execute
without skip gating, make the verification reproducible from a clean checkout,
and require an independent reviewer for this high-blast change.

Source: Path instructions

apps/carrier/src/navigation/permissions.tsx (1)

92-92: 🔒 Security & Privacy | 🔵 Trivial

chore [blocking]: Require independent review for this booking permission grant.

This permission exposes the scheduling surface, which is part of the CP-bearing booking flow. Please obtain independent reviewer sign-off and verify the builder-specific permission path before merge; the fully privileged navigation test does not prove this role mapping.

As per path instructions, CP-touching changes require an independent reviewer rather than self-assertion (PROC-A2).

🤖 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 `@apps/carrier/src/navigation/permissions.tsx` at line 92, Obtain independent
reviewer sign-off for the scheduling:design permission change and verify the
builder-specific permission path and its role-mapping test; do not rely solely
on the fully privileged navigation test or self-assertion.

Source: Path instructions

apps/carrier/src/i18n/locales/ru-RU.ts (1)

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

suggestion [non-blocking]: Keep one canonical scheduling label key.

everything-pack.ts:116 and routes/scheduling.tsx:5-8 use scheduling.route.title; this new nav.scheduling key is not the contract for the added Workshop group. Remove it or update consumers to avoid label drift.

As per path instructions, avoid hand-parallel duplicate copies of single-source things (PROC-A4).

🤖 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 `@apps/carrier/src/i18n/locales/ru-RU.ts` at line 874, Remove the duplicate
nav.scheduling translation entry from the locale, and keep the existing
scheduling.route.title key as the canonical label used by everything-pack.ts and
routes/scheduling.tsx. Do not add or update consumers to use the duplicate key.

Source: Path instructions

apps/carrier/src/i18n/locales/nl-NL.ts (1)

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

suggestion [non-blocking]: Keep one canonical scheduling label key.

everything-pack.ts:116 and routes/scheduling.tsx:5-8 use scheduling.route.title; this new nav.scheduling key is not the contract for the added Workshop group. Remove it or update consumers to avoid label drift.

As per path instructions, avoid hand-parallel duplicate copies of single-source things (PROC-A4).

🤖 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 `@apps/carrier/src/i18n/locales/nl-NL.ts` at line 874, Remove the duplicate
nav.scheduling translation entry in the nl-NL locale and retain the canonical
scheduling.route.title key used by everything-pack and routes/scheduling. Do not
add or update consumers for nav.scheduling.

Source: Path instructions

apps/carrier/src/navigation/__tests__/resolveNav.test.ts (1)

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

suggestion [non-blocking]: Test the changed builder-role path.

This test resolves navigation with FULL, so it only proves that scheduling appears for fully privileged users. Add a focused assertion using NAV_ROLE_PERMISSIONS.builder to verify that scheduling:design actually makes the scheduling item visible.

As per path instructions, CI must exercise modified code paths (PROC-A1), with clean-checkout reproducible verification (PROC-A7).

🤖 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 `@apps/carrier/src/navigation/__tests__/resolveNav.test.ts` at line 58, Add a
focused navigation test using NAV_ROLE_PERMISSIONS.builder, asserting that the
scheduling:design permission makes the scheduling item visible. Keep the
existing FULL-role coverage intact and ensure the new assertion exercises the
modified builder-role path.

Source: Path instructions

apps/carrier/src/i18n/locales/pt-PT.ts (1)

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

suggestion [non-blocking]: Keep one canonical scheduling label key.

everything-pack.ts:116 and routes/scheduling.tsx:5-8 use scheduling.route.title; this new nav.scheduling key is not the contract for the added Workshop group. Remove it or update consumers to avoid label drift.

As per path instructions, avoid hand-parallel duplicate copies of single-source things (PROC-A4).

🤖 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 `@apps/carrier/src/i18n/locales/pt-PT.ts` at line 874, Remove the newly added
nav.scheduling translation from the pt-PT locale and keep scheduling.route.title
as the canonical label used by everything-pack.ts and routes/scheduling.tsx. Do
not add alternate consumer keys or duplicate scheduling labels.

Source: Path instructions

apps/carrier/src/i18n/locales/pt-BR.ts (1)

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

suggestion [non-blocking]: Keep one canonical scheduling label key.

everything-pack.ts:116 and routes/scheduling.tsx:5-8 use scheduling.route.title; this new nav.scheduling key is not the contract for the added Workshop group. Remove it or update consumers to avoid label drift.

As per path instructions, avoid hand-parallel duplicate copies of single-source things (PROC-A4).

🤖 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 `@apps/carrier/src/i18n/locales/pt-BR.ts` at line 874, Remove the newly added
nav.scheduling label from the locale and keep scheduling.route.title as the
single canonical scheduling label used by everything-pack.ts and
routes/scheduling.tsx. Do not add parallel scheduling label keys or update
consumers to use the redundant key.

Source: Path instructions

apps/carrier/src/scheduling/product/client.ts (1)

139-150: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

suggestion: surface the server's rejection code instead of only the HTTP status

SchedulingRequestError only carries response.status; the JSON body's code (e.g. scheduling.appointment.slot_conflict, scheduling.appointment.no_availability, scheduling.appointment.resource_invalid) is discarded. The UI currently can only show one generic error message for every rejection reason, even though the booking service returns a specific, actionable reason.

♻️ Suggested enhancement
 export async function bookAppointment(input: {
   subjectId: string
   resource: string
   title: string
   startUtc: string
   endUtc: string
 }): Promise<{ eventId: string; status: 'booked' }> {
   const node = await requireNode()
   const response = await nodePost(node, '/api/local-node/scheduling/appointments', input)
-  if (!response.ok) throw new SchedulingRequestError(response.status)
+  if (!response.ok) {
+    const body = await response.json().catch(() => null) as { code?: string } | null
+    throw new SchedulingRequestError(response.status, body?.code)
+  }
   return response.json() as Promise<{ eventId: string; status: 'booked' }>
 }
🤖 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 `@apps/carrier/src/scheduling/product/client.ts` around lines 139 - 150, Update
bookAppointment to read the rejected response JSON and pass its server-provided
code into SchedulingRequestError, while preserving the HTTP status and existing
successful response behavior. Ensure rejection handling supports codes such as
scheduling.appointment.slot_conflict, scheduling.appointment.no_availability,
and scheduling.appointment.resource_invalid.
🤖 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 `@apps/carrier/src/scheduling/product/FrontOfficeRehearsal.tsx`:
- Around line 88-108: The booking flow in book must reset booked to false before
starting a new booking attempt, so a prior successful appointment does not keep
the primary button disabled. Preserve setting booked to true after success and
the existing disabled condition.
- Around line 88-101: Reset or invalidate the checked result when subjectId
changes, matching the existing definitionId change behavior, so book() cannot
use stale result.contactName with the current subjectId. Ensure the review and
booking flow require a fresh check before calling bookAppointment.
- Around line 88-101: Update book() to parse startsAt as a wall-clock time in
selectedDefinition.timezone, matching check(), instead of constructing it with
new Date(startsAt). Use the timezone-aware parsed instant to calculate the
duration and populate startUtc and endUtc.

In `@apps/local-node-host/Health/SchedulingDefinitionRoutes.cs`:
- Around line 92-109: Update the request validation in the AppointmentRoute
handler to reject null, empty, or whitespace request.SubjectId alongside the
existing Title and time checks. Return the existing
scheduling.appointment.invalid 400 response before calling ParticipantRef.Party.

In `@apps/local-node-host/tests/Scheduling/SchedulingDefinitionRouteTests.cs`:
- Around line 233-255: The SchedulingDefinitionRouteTests coverage only verifies
denied and invalid appointment requests, not successful booking or overlap
rejection. Add tests alongside Appointment_booking_requires_scheduling_operate
and Appointment_booking_rejects_an_invalid_resource_without_writing that grant
SchedulingOperate, assert an available appointment returns 200 with status
"booked", then submit the same resource and overlapping slot again and assert
409 with code "scheduling.appointment.slot_conflict".

In `@tooling/label-continuity-lint/check.mjs`:
- Around line 102-107: Update the route scanning logic around lazyPathRe so each
path is paired only with its own lazy import, rather than searching an arbitrary
500-character slice that can reach the next route. Use a single route-entry
pattern that captures the path and its direct import together, or otherwise
parse route-object boundaries structurally before updating segToFile.

In `@tooling/preview-host/deploy-dogfood.ps1`:
- Around line 66-69: Update the config-copy logic near the dogfood deployment
flow to source dogfood-appsettings.Production.json from $Worktree instead of
$RepoRoot, keeping the template aligned with the selected $DeployRef and
deployed worktree revision.
- Around line 113-131: Update the worktree preparation flow around git fetch,
git checkout, and git worktree add to explicitly validate each command’s exit
status and terminate on failure. Ensure the script never proceeds to rev-parse
or logs $headSha when selecting $DeployRef fails, preventing a stale worktree
commit from being treated as current.

---

Nitpick comments:
In `@apps/carrier/src/i18n/locales/nl-NL.ts`:
- Line 874: Remove the duplicate nav.scheduling translation entry in the nl-NL
locale and retain the canonical scheduling.route.title key used by
everything-pack and routes/scheduling. Do not add or update consumers for
nav.scheduling.

In `@apps/carrier/src/i18n/locales/pt-BR.ts`:
- Line 874: Remove the newly added nav.scheduling label from the locale and keep
scheduling.route.title as the single canonical scheduling label used by
everything-pack.ts and routes/scheduling.tsx. Do not add parallel scheduling
label keys or update consumers to use the redundant key.

In `@apps/carrier/src/i18n/locales/pt-PT.ts`:
- Line 874: Remove the newly added nav.scheduling translation from the pt-PT
locale and keep scheduling.route.title as the canonical label used by
everything-pack.ts and routes/scheduling.tsx. Do not add alternate consumer keys
or duplicate scheduling labels.

In `@apps/carrier/src/i18n/locales/ru-RU.ts`:
- Line 874: Remove the duplicate nav.scheduling translation entry from the
locale, and keep the existing scheduling.route.title key as the canonical label
used by everything-pack.ts and routes/scheduling.tsx. Do not add or update
consumers to use the duplicate key.

In `@apps/carrier/src/navigation/__tests__/resolveNav.test.ts`:
- Line 58: Add a focused navigation test using NAV_ROLE_PERMISSIONS.builder,
asserting that the scheduling:design permission makes the scheduling item
visible. Keep the existing FULL-role coverage intact and ensure the new
assertion exercises the modified builder-role path.

In `@apps/carrier/src/navigation/permissions.tsx`:
- Line 92: Obtain independent reviewer sign-off for the scheduling:design
permission change and verify the builder-specific permission path and its
role-mapping test; do not rely solely on the fully privileged navigation test or
self-assertion.

In `@apps/carrier/src/scheduling/product/client.ts`:
- Around line 139-150: Update bookAppointment to read the rejected response JSON
and pass its server-provided code into SchedulingRequestError, while preserving
the HTTP status and existing successful response behavior. Ensure rejection
handling supports codes such as scheduling.appointment.slot_conflict,
scheduling.appointment.no_availability, and
scheduling.appointment.resource_invalid.

In `@tooling/preview-host/deploy-dogfood.ps1`:
- Around line 66-69: Add clean-checkout CI or operational coverage for
deploy-dogfood.ps1 that runs with DOGFOOD_REF unset and set to an explicit
fetched ref, verifies the selected commit SHA, and confirms fetch or checkout
failures cannot reach Stop-Service. Ensure both the real deployment path and
offline/alternative test path execute without skip gating, make the verification
reproducible from a clean checkout, and require an independent reviewer for this
high-blast change.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b68b4ee4-0359-47a8-9726-21cd3a7e2e1e

📥 Commits

Reviewing files that changed from the base of the PR and between 8090277 and 82c5896.

📒 Files selected for processing (37)
  • apps/carrier/src/first-run/genesisState.test.ts
  • apps/carrier/src/first-run/genesisState.ts
  • apps/carrier/src/i18n/locales/ar-AE.ts
  • apps/carrier/src/i18n/locales/ar.ts
  • apps/carrier/src/i18n/locales/bn.ts
  • apps/carrier/src/i18n/locales/de-DE.ts
  • apps/carrier/src/i18n/locales/en-GB.ts
  • apps/carrier/src/i18n/locales/en-US.ts
  • apps/carrier/src/i18n/locales/es-419.ts
  • apps/carrier/src/i18n/locales/es-ES.ts
  • apps/carrier/src/i18n/locales/fr-CA.ts
  • apps/carrier/src/i18n/locales/fr-FR.ts
  • apps/carrier/src/i18n/locales/hi-IN.ts
  • apps/carrier/src/i18n/locales/it-IT.ts
  • apps/carrier/src/i18n/locales/ja-JP.ts
  • apps/carrier/src/i18n/locales/nl-NL.ts
  • apps/carrier/src/i18n/locales/pt-BR.ts
  • apps/carrier/src/i18n/locales/pt-PT.ts
  • apps/carrier/src/i18n/locales/ru-RU.ts
  • apps/carrier/src/i18n/locales/ur-PK.ts
  • apps/carrier/src/i18n/locales/ur.ts
  • apps/carrier/src/i18n/locales/zh-CN.ts
  • apps/carrier/src/i18n/locales/zh-TW.ts
  • apps/carrier/src/navigation/__tests__/resolveNav.test.ts
  • apps/carrier/src/navigation/everything-pack.ts
  • apps/carrier/src/navigation/fragments.tsx
  • apps/carrier/src/navigation/permissions.tsx
  • apps/carrier/src/routes/calendar.tsx
  • apps/carrier/src/scheduling/product/FrontOfficeRehearsal.test.tsx
  • apps/carrier/src/scheduling/product/FrontOfficeRehearsal.tsx
  • apps/carrier/src/scheduling/product/SchedulingHome.tsx
  • apps/carrier/src/scheduling/product/client.ts
  • apps/local-node-host/Health/HostedSchedulingApiEndpoint.cs
  • apps/local-node-host/Health/SchedulingDefinitionRoutes.cs
  • apps/local-node-host/tests/Scheduling/SchedulingDefinitionRouteTests.cs
  • tooling/label-continuity-lint/check.mjs
  • tooling/preview-host/deploy-dogfood.ps1

Comment on lines +88 to +101
async function book() {
if (!result || !selectedDefinition || !resource) return
setBooking(true)
setError(null)
try {
const start = new Date(startsAt)
const end = new Date(start.getTime() + selectedDefinition.durationMinutes * 60_000)
await bookAppointment({
subjectId,
resource,
title: `${selectedDefinition.name} — ${result.contactName}`,
startUtc: start.toISOString(),
endUtc: end.toISOString(),
})

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 | 🟡 Minor | ⚡ Quick win

thought: booking uses live subjectId/resource but a stale result.contactName

If the user edits the contact selection after check() but before clicking Book, book()'s title still reads result.contactName from the earlier check, while the actual subjectId sent to bookAppointment reflects the new selection — the booked appointment's subject and the displayed review can diverge. Resetting result (as already done for definitionId) when subjectId changes would close this gap.

🤖 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 `@apps/carrier/src/scheduling/product/FrontOfficeRehearsal.tsx` around lines 88
- 101, Reset or invalidate the checked result when subjectId changes, matching
the existing definitionId change behavior, so book() cannot use stale
result.contactName with the current subjectId. Ensure the review and booking
flow require a fresh check before calling bookAppointment.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -A10 'function validateLocalDateTime' apps/carrier/src
rg -n -A15 'export function DateTimeField' apps/carrier/src

Repository: Harborline-Software/shipyard

Length of output: 1374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- apps/carrier/src/scheduling/product/rehearsal.ts ---'
cat -n apps/carrier/src/scheduling/product/rehearsal.ts | sed -n '1,220p'

echo
echo '--- apps/carrier/src/scheduling/product/FrontOfficeRehearsal.tsx ---'
cat -n apps/carrier/src/scheduling/product/FrontOfficeRehearsal.tsx | sed -n '1,220p'

Repository: Harborline-Software/shipyard

Length of output: 12544


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- apps/carrier/src/scheduling/product/rehearsal.ts ---'
cat -n apps/carrier/src/scheduling/product/rehearsal.ts | sed -n '1,220p'

echo
echo '--- apps/carrier/src/scheduling/product/FrontOfficeRehearsal.tsx ---'
cat -n apps/carrier/src/scheduling/product/FrontOfficeRehearsal.tsx | sed -n '1,260p'

Repository: Harborline-Software/shipyard

Length of output: 12544


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' apps/carrier/src/scheduling/product/rehearsal.ts | nl -ba
printf '\n---\n'
sed -n '1,260p' apps/carrier/src/scheduling/product/FrontOfficeRehearsal.tsx | nl -ba

Repository: Harborline-Software/shipyard

Length of output: 206


issue [blocking]: parse startsAt in the protocol timezone

check() treats the input as a wall-clock in selectedDefinition.timezone, but book() still does new Date(startsAt). A bare YYYY-MM-DDTHH:mm will be interpreted in the browser’s local timezone, so startUtc/endUtc can land on the wrong slot. Convert it with a timezone-aware parse before sending the appointment.

🤖 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 `@apps/carrier/src/scheduling/product/FrontOfficeRehearsal.tsx` around lines 88
- 101, Update book() to parse startsAt as a wall-clock time in
selectedDefinition.timezone, matching check(), instead of constructing it with
new Date(startsAt). Use the timezone-aware parsed instant to calculate the
duration and populate startUtc and endUtc.

Comment on lines +88 to +108
async function book() {
if (!result || !selectedDefinition || !resource) return
setBooking(true)
setError(null)
try {
const start = new Date(startsAt)
const end = new Date(start.getTime() + selectedDefinition.durationMinutes * 60_000)
await bookAppointment({
subjectId,
resource,
title: `${selectedDefinition.name} — ${result.contactName}`,
startUtc: start.toISOString(),
endUtc: end.toISOString(),
})
setBooked(true)
} catch (cause: unknown) {
setError(cause instanceof Error ? cause.message : t('scheduling.errors.appointmentBook'))
} finally {
setBooking(false)
}
}

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

issue: booking is permanently disabled after the first success

booked is set to true on a successful booking but is never reset back to false. The primary button's disabled condition (disabled={booking || booked || !resource}, line 146) means that once any booking succeeds, the Book button stays disabled for the rest of the session — the user cannot book a second appointment without leaving and re-entering this screen.

♻️ Suggested fix
   function check() {
     setChecking(true)
     setResult(null)
+    setBooked(false)
     const contact = contacts.find((item) => item.id === subjectId)

Also applies to: 145-149

🤖 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 `@apps/carrier/src/scheduling/product/FrontOfficeRehearsal.tsx` around lines 88
- 108, The booking flow in book must reset booked to false before starting a new
booking attempt, so a prior successful appointment does not keep the primary
button disabled. Preserve setting booked to true after success and the existing
disabled condition.

Comment on lines +92 to +109

app.MapPost(AppointmentRoute, async (SchedulingAppointmentRequest request, CancellationToken ct) =>
{
if (!authz.HasPermission(Permission.SchedulingOperate))
return Denied(Permission.SchedulingOperate);
if (string.IsNullOrWhiteSpace(request.Title) || request.EndUtc <= request.StartUtc)
return Results.BadRequest(new { code = "scheduling.appointment.invalid" });
var resource = ParseParticipant(request.Resource);
if (resource is null)
return Results.BadRequest(new { code = "scheduling.appointment.resource_invalid" });

var outcome = await bookingService.Book(
NodeTenant.Resolve(activeTeam), resource, request.Title.Trim(), request.StartUtc,
request.EndUtc, LocalSchedulingActor, ParticipantRef.Party(request.SubjectId), ct: ct)
.ConfigureAwait(false);
return outcome.Success
? Results.Ok(new { eventId = outcome.Event!.Id.Value, status = "booked" })
: Results.Conflict(new { code = $"scheduling.appointment.{outcome.RejectionReason!.ToLowerInvariant()}" });

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep run --pattern 'static ParticipantRef Party($ARG) { $$$ }' --lang csharp packages/blocks-calendar
rg -n -A5 'static ParticipantRef Party' packages/blocks-calendar

Repository: Harborline-Software/shipyard

Length of output: 826


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' packages/blocks-calendar/Models/ParticipantRef.cs
printf '\n---\n'
rg -n -A6 -B6 'class PartyRef|record PartyRef|PartyRef\(' packages/blocks-calendar/Models

Repository: Harborline-Software/shipyard

Length of output: 8995


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A8 -B8 'class SchedulingAppointmentRequest|record SchedulingAppointmentRequest|SubjectId' apps/local-node-host packages/blocks-calendar
printf '\n---\n'
rg -n -A6 -B6 'UseExceptionHandler|app.Use|MapPost\(AppointmentRoute|Results\.BadRequest' apps/local-node-host

Repository: Harborline-Software/shipyard

Length of output: 50385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A20 -B10 'record SchedulingAppointmentRequest|class SchedulingAppointmentRequest|SchedulingAppointmentRequest\s*\(' apps/local-node-host/Health apps/local-node-host -g '*.cs'
printf '\n---\n'
rg -n -A8 -B8 'UseExceptionHandler|AddExceptionHandler|app\.UseExceptionHandler|exception handler|MapPost\(AppointmentRoute' apps/local-node-host -g '*.cs'

Repository: Harborline-Software/shipyard

Length of output: 4649


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A8 -B8 'UseExceptionHandler|AddExceptionHandler|UseDeveloperExceptionPage|exception handler|ProblemDetails|MapGroup\(".*Health|app\.MapPost\(AppointmentRoute' apps/local-node-host -g '*.cs'

Repository: Harborline-Software/shipyard

Length of output: 2071


🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -a 'Program.cs' apps/local-node-host
printf '\n---\n'
rg -n -A12 -B12 'UseExceptionHandler|AddExceptionHandler|UseDeveloperExceptionPage|app\.Use|MapPost\(AppointmentRoute|ArgumentException' apps/local-node-host/Program.cs apps/local-node-host -g '*.cs'

Repository: Harborline-Software/shipyard

Length of output: 50385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

program=$(fd -a '^Program\.cs$' apps/local-node-host | head -n 1)
echo "$program"
printf '\n---\n'
cat -n "$program" | sed -n '1,260p'

Repository: Harborline-Software/shipyard

Length of output: 16242


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'UseExceptionHandler|AddExceptionHandler|UseDeveloperExceptionPage|DeveloperExceptionPage|ExceptionHandler|UseStatusCodePages|UseMiddleware<.*Exception' apps/local-node-host -g '*.cs'

Repository: Harborline-Software/shipyard

Length of output: 166


issue [blocking]: validate SubjectId before calling ParticipantRef.Party

ParticipantRef.Party already rejects empty/whitespace ids, so a bad SubjectId here raises an ArgumentException and bubbles as a 500. Add it to the existing request validation and return the same 400 as the other invalid fields.

🤖 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 `@apps/local-node-host/Health/SchedulingDefinitionRoutes.cs` around lines 92 -
109, Update the request validation in the AppointmentRoute handler to reject
null, empty, or whitespace request.SubjectId alongside the existing Title and
time checks. Return the existing scheduling.appointment.invalid 400 response
before calling ParticipantRef.Party.

Comment on lines +233 to +255
[Fact]
public async Task Appointment_booking_requires_scheduling_operate()
{
var response = await _client.PostAsJsonAsync(SchedulingDefinitionRoutes.AppointmentRoute,
new { subjectId = "subject-1", resource = "party:resource-1", title = "Visit",
startUtc = "2026-07-13T13:00:00Z", endUtc = "2026-07-13T13:30:00Z" });

Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}

[Fact]
public async Task Appointment_booking_rejects_an_invalid_resource_without_writing()
{
_principal.Grant(Permission.SchedulingOperate);
var response = await _client.PostAsJsonAsync(SchedulingDefinitionRoutes.AppointmentRoute,
new { subjectId = "subject-1", resource = "unknown:resource-1", title = "Visit",
startUtc = "2026-07-13T13:00:00Z", endUtc = "2026-07-13T13:30:00Z" });

Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("scheduling.appointment.resource_invalid", body.GetProperty("code").GetString());
}

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

issue: no test covers a successful booking or the no-double-booking rejection

The two new tests only cover permission-denial and invalid-resource inputs. The actual booking behavior this route introduces — a successful 200 booking, and rejection of a conflicting/overlapping slot (SLOT_CONFLICT → 409) — is never exercised.

As per path instructions, "Ensure CI actually exercises the modified code paths (PROC-A1)" and "Treat booking/concurrency/slotting as CP-5—preserve single-winner/uniqueness guarantees (no double-booking...)". Without a test that books once and then attempts a second overlapping booking, the no-double-booking guarantee for this new endpoint is unverified by CI.

Consider adding:

  • A happy-path test asserting 200 OK with { status: "booked" } when availability exists.
  • A conflict test that books the same resource/slot twice and asserts the second call returns 409 with a scheduling.appointment.slot_conflict code.
🤖 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 `@apps/local-node-host/tests/Scheduling/SchedulingDefinitionRouteTests.cs`
around lines 233 - 255, The SchedulingDefinitionRouteTests coverage only
verifies denied and invalid appointment requests, not successful booking or
overlap rejection. Add tests alongside
Appointment_booking_requires_scheduling_operate and
Appointment_booking_rejects_an_invalid_resource_without_writing that grant
SchedulingOperate, assert an available appointment returns 200 with status
"booked", then submit the same resource and overlapping slot again and assert
409 with code "scheduling.appointment.slot_conflict".

Source: Path instructions

Comment on lines +102 to +107
const lazyPathRe = /path:\s*'([^']+)'\s*,\s*lazy:/g;
while ((rm = lazyPathRe.exec(src)) !== null) {
const routeBody = src.slice(rm.index, rm.index + 500);
const dynamicImport = routeBody.match(/import\(\s*'(\.\/routes\/[^']+)'\s*\)/);
if (dynamicImport) {
segToFile.set(rm[1], `apps/carrier/src/${dynamicImport[1].replace(/^\.\//, '')}.tsx`);

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

issue (blocking): Bind the import to the current route entry

The 500-character slice is not a route boundary. If a lazy entry uses a helper or lacks a direct import, this loop can consume the next route’s import() and map the wrong file to the current path. Capture the path and import from the same route pattern, or parse route-object boundaries structurally.

Proposed fix
-  const lazyPathRe = /path:\s*'([^']+)'\s*,\s*lazy:/g;
-  while ((rm = lazyPathRe.exec(src)) !== null) {
-    const routeBody = src.slice(rm.index, rm.index + 500);
-    const dynamicImport = routeBody.match(/import\(\s*'(\.\/routes\/[^']+)'\s*\)/);
-    if (dynamicImport) {
-      segToFile.set(rm[1], `apps/carrier/src/${dynamicImport[1].replace(/^\.\//, '')}.tsx`);
-    }
-  }
+  const lazyRouteRe =
+    /\{\s*path:\s*'([^']+)'\s*,\s*lazy:\s*(?:async\s*)?\(\s*\)\s*=>\s*import\(\s*'(\.\/routes\/[^']+)'\s*\)/g;
+  let lm;
+  while ((lm = lazyRouteRe.exec(src)) !== null) {
+    segToFile.set(lm[1], `apps/carrier/src/${lm[2].replace(/^\.\//, '')}.tsx`);
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const lazyPathRe = /path:\s*'([^']+)'\s*,\s*lazy:/g;
while ((rm = lazyPathRe.exec(src)) !== null) {
const routeBody = src.slice(rm.index, rm.index + 500);
const dynamicImport = routeBody.match(/import\(\s*'(\.\/routes\/[^']+)'\s*\)/);
if (dynamicImport) {
segToFile.set(rm[1], `apps/carrier/src/${dynamicImport[1].replace(/^\.\//, '')}.tsx`);
const lazyRouteRe =
/\{\s*path:\s*'([^']+)'\s*,\s*lazy:\s*(?:async\s*)?\(\s*\)\s*=>\s*import\(\s*'(\.\/routes\/[^']+)'\s*\)/g;
let lm;
while ((lm = lazyRouteRe.exec(src)) !== null) {
segToFile.set(lm[1], `apps/carrier/src/${lm[2].replace(/^\.\//, '')}.tsx`);
}
🤖 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 `@tooling/label-continuity-lint/check.mjs` around lines 102 - 107, Update the
route scanning logic around lazyPathRe so each path is paired only with its own
lazy import, rather than searching an arbitrary 500-character slice that can
reach the next route. Use a single route-entry pattern that captures the path
and its direct import together, or otherwise parse route-object boundaries
structurally before updating segToFile.

Comment on lines +66 to +69
# Internal dogfood may intentionally exercise an unmerged branch. Production-shaped use keeps the
# default origin/main; an operator can set DOGFOOD_REF to an explicit fetched remote ref.
$DeployRef = if ($env:DOGFOOD_REF) { $env:DOGFOOD_REF } else { 'origin/main' }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the deployment script and inspect the relevant sections with line numbers.
git ls-files tooling/preview-host/deploy-dogfood.ps1
wc -l tooling/preview-host/deploy-dogfood.ps1
sed -n '1,220p' tooling/preview-host/deploy-dogfood.ps1 | cat -n

# Find where the copied config file is sourced from and how $Worktree/$RepoRoot are used.
rg -n "\$Worktree|\$RepoRoot|dogfood-appsettings\.Production\.json|DOGFOOD_REF|origin/main" tooling/preview-host/deploy-dogfood.ps1 tooling/preview-host -S

Repository: Harborline-Software/shipyard

Length of output: 18619


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect nearby docs or scripts that describe dogfood deployment behavior.
git ls-files tooling/preview-host | sed -n '1,120p'
rg -n "dogfood-appsettings\.Production\.json|DOGFOOD_REF|preview-host|dogfood" -S README.md tooling _shared 2>/dev/null || true

Repository: Harborline-Software/shipyard

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the config template and the documented deployment contract around it.
cat -n tooling/preview-host/dogfood-appsettings.Production.json | sed -n '1,220p'
printf '\n--- DOGFOOD.md excerpt ---\n'
sed -n '170,190p;440,452p;476,486p' tooling/preview-host/DOGFOOD.md | cat -n

Repository: Harborline-Software/shipyard

Length of output: 5492


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether any script copies appsettings from the worktree instead of the repo root.
rg -n "appsettings\.Production\.json|Copy-Item .*dogfood-appsettings|Join-Path .*appsettings" tooling/preview-host -S

Repository: Harborline-Software/shipyard

Length of output: 1238


issue (blocking): keep config on the same revision as the worktree
tooling/preview-host/deploy-dogfood.ps1:149-150 still copies dogfood-appsettings.Production.json from $RepoRoot. With a custom DOGFOOD_REF, that mixes binaries/web assets from one commit with config from another checkout. Copy the template from $Worktree, or pin the config source to the same ref explicitly, so deploys stay reproducible.

🤖 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 `@tooling/preview-host/deploy-dogfood.ps1` around lines 66 - 69, Update the
config-copy logic near the dogfood deployment flow to source
dogfood-appsettings.Production.json from $Worktree instead of $RepoRoot, keeping
the template aligned with the selected $DeployRef and deployed worktree
revision.

Source: Path instructions

Comment on lines +113 to +131
Write-Host "[deploy-dogfood] Fetching remote refs for $DeployRef..."
git fetch origin

if (Test-Path $Worktree) {
Write-Host "[deploy-dogfood] Updating existing worktree to origin/main..."
Write-Host "[deploy-dogfood] Updating existing worktree to $DeployRef..."
Push-Location $Worktree
git checkout --detach origin/main
git checkout --detach $DeployRef
Pop-Location
} else {
Write-Host "[deploy-dogfood] Creating worktree at $Worktree pinned to origin/main..."
git worktree add $Worktree origin/main
Write-Host "[deploy-dogfood] Creating worktree at $Worktree pinned to $DeployRef..."
git worktree add $Worktree $DeployRef
}
}
finally {
Pop-Location
}

$headSha = (git -C $Worktree rev-parse HEAD)
Write-Host "[deploy-dogfood] Building from origin/main @ $headSha"
Write-Host "[deploy-dogfood] Building from $DeployRef @ $headSha"

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and surrounding logic.
git ls-files tooling/preview-host/deploy-dogfood.ps1
wc -l tooling/preview-host/deploy-dogfood.ps1
sed -n '1,220p' tooling/preview-host/deploy-dogfood.ps1

Repository: Harborline-Software/shipyard

Length of output: 13726


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check for native-command error handling settings and any HEAD/ref validation.
rg -n "\$PSNativeCommandUseErrorActionPreference|\$ErrorActionPreference|git fetch|git checkout|git worktree add|rev-parse" tooling/preview-host/deploy-dogfood.ps1

Repository: Harborline-Software/shipyard

Length of output: 473


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the rest of the deployment flow around the git selection to see whether
# failures or mismatches are handled later.
sed -n '130,260p' tooling/preview-host/deploy-dogfood.ps1

Repository: Harborline-Software/shipyard

Length of output: 9151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '130,260p' tooling/preview-host/deploy-dogfood.ps1

Repository: Harborline-Software/shipyard

Length of output: 9151


🌐 Web query:

PowerShell 7 native command non-zero exit codes ErrorActionPreference Stop PSNativeCommandUseErrorActionPreference documentation

💡 Result:

In PowerShell 7, the variable $PSNativeCommandUseErrorActionPreference allows you to control how PowerShell handles non-zero exit codes from native (external) commands [1][2][3]. When this variable is set to $true, PowerShell will treat a non-zero exit code from a native command as a PowerShell error [2][4]. This error is then subject to the behavior defined by your current $ErrorActionPreference [2][3]. If you set $ErrorActionPreference to 'Stop', any native command that returns a non-zero exit code will trigger a script-terminating error, effectively halting execution [2][4]. Key implementation details: - Default Value: The variable defaults to $false [2]. - Scope: You can modify this setting locally within a script block or function to avoid affecting the global state [2][3]. - Specific Exceptions: Some native commands (e.g., robocopy) use non-zero exit codes for informational purposes rather than indicating failures [2][3]. In such cases, you should explicitly set the variable to $false before running those commands to prevent unintended script termination [2][3]. - Error Type: When triggered, PowerShell generates a NativeCommandExitException [5][6][4]. Note that because this error is subject to $ErrorActionPreference, using 'Stop' elevates it to a terminating error that can be handled using try/catch blocks [5][6][4]. Example Usage: $PSNativeCommandUseErrorActionPreference = $true $ErrorActionPreference = 'Stop' try { # This will throw a NativeCommandExitException if the command fails some-native-command.exe } catch { Write-Host "Command failed with exit code: $LASTEXITCODE" }

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for any repo-wide PowerShell setting that would change native-command
# failure behavior, plus any invocation hints for pwsh/Windows PowerShell.
rg -n "\$PSNativeCommandUseErrorActionPreference|pwsh|powershell\.exe|PowerShell 7|PSNativeCommand" .

Repository: Harborline-Software/shipyard

Length of output: 1156


issue (blocking): fail closed when Git cannot select the requested ref

git fetch, git checkout, and git worktree add still need explicit exit-code checks here; this script does not enable native-command failures as terminating errors. If one of those steps fails, $Worktree can stay on the previous commit and the later rev-parse HEAD will log stale code as if it matched $DeployRef.

🤖 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 `@tooling/preview-host/deploy-dogfood.ps1` around lines 113 - 131, Update the
worktree preparation flow around git fetch, git checkout, and git worktree add
to explicitly validate each command’s exit status and terminate on failure.
Ensure the script never proceeds to rev-parse or logs $headSha when selecting
$DeployRef fails, preventing a stale worktree commit from being treated as
current.

Sources: Path instructions, MCP tools

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