docs(adr): add ADR 0167 - the .NET edition as a co-equal client stack - #3645
Conversation
Ratifies the co-equal .NET edition over one node - MAUI Blazor Hybrid host and client modes, hull parity pair, loopback-only in-process node hosting (G-R1 gates), a PWA-first mobile decision gate, and the W1-W8 wave DAG with a resume protocol. Folds all surviving findings of the six-perspective board (verdict GO-WITH-CHANGES) and narrows two of them on evidence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds ADR 0167 and a system-design note for a co-equal .NET client edition. The documents define MAUI and PWA modes, hull parity, loopback HTTP boundaries, execution waves, validation gates, parity targets, and acceptance criteria. Changes.NET edition architecture
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related issues
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Track the design note the ADR cites, add the implementation checklist for every introduced path, home Shipyard.Client in W2, reconcile the wave DAG with the normative gate table, correct the FounderWebAdmission fence polarity in G-R1(c), and repair the minor citation slips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the forward pointer on the design note, enumerate Shipyard.Client in D5's project row with its TFMs, and repair two citation slips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 13
🤖 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 `@docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md`:
- Line 282: The carrier-used coverage row in the ADR table has one missing cell,
causing MD056 and misplacing the gate value. Add the missing Today-column value
before the existing W6 exit entry, and keep “W6 exit: 100%” as the Gate-column
value, preserving the four-column table structure.
- Around line 240-250: Update the wave DAG under “D7 — BLOCKER M-1” to show W4
as an explicit predecessor of W5 alongside the existing W-PWA gate, matching the
predecessor contract in the W5 table row. Ensure the diagram and gate table
permit the same execution order under the single-source policy.
- Around line 406-408: Update the ADR’s “Independent verification” checklist to
record a separate independent review of this CP-touching change, including the
reviewer and review result, rather than relying only on the prior design board
review or self-assertion. Preserve the existing board-review context while
documenting that the independent reviewer assessed the security and audit-gate
changes before merge.
- Around line 242-250: Label both affected Markdown diagram fences with the text
language tag to satisfy MD040: update the wave DAG fence in
docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md lines
242-250 and the data-flow fence in
icm/working-notes/2026-08-04-dotnet-edition-system-design.md lines 80-88. No
diagram content changes are needed.
- Around line 153-159: Add a generated conformance contract or differential
fixture covering the five TypeScript hull faces and resolution round-trip cases,
then make its validation a required CI gate for the apps/hull and Shipyard.Hull
parity pair. Generate the shared expectations from a single source rather than
maintaining parallel hand-written copies, and integrate the check with the
existing W3 validation.
- Around line 202-208: Update the Acceptance criteria to require the E2E test to
verify that the accepted read model or audit store contains the same contact ID
after the contact creation and read requests complete, in addition to asserting
a non-empty audit envelope.
- Around line 202-208: Update the D5 E2E acceptance flow to send the valid
per-boot bearer token for the contact POST and GET round trip, then add checks
that requests with a missing token and an invalid token receive 401 or 403. Keep
the existing persisted-row and non-empty audit-envelope assertions, and anchor
the changes to the D5 E2E and per-boot authorization behavior described in the
acceptance section.
- Around line 256-267: Update the W8 CI edition gate entry so G-R1 is a required
check for every .NET-edition PR, in addition to the existing .NET E2E round trip
and story-parity lint requirements. Ensure the gate explicitly preserves the
full G-R1 requirement, including route-fence and caller-auth enforcement, rather
than relying only on W2’s completion criteria.
- Around line 155-156: Apply one consistent package-naming decision for
Shipyard.Hull and the related .NET package references across
docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md at
lines 83, 132, 155-156, 202-206, 325-326, and 365-367, and
icm/working-notes/2026-08-04-dotnet-edition-system-design.md at lines 28, 55,
72, 84, and 119; align every occurrence with the chosen naming convention,
including the parity-pair description and package references.
- Around line 202-205: Update the MAUI Workloads and TFMs entries in the ADR so
clean-checkout restore is deterministic: commit the workload-set installation
with an exact version, explicitly install the required iOS/Android targeting
packs with exact versions, and remove the conditional “verify at runtime”
language. Define the W1/W2 gate to fail when any pinned workload or
targeting-pack version is missing, rather than relying on runner state.
In `@icm/working-notes/2026-08-04-dotnet-edition-system-design.md`:
- Around line 47-50: Update the host mode entry in the mode/targets table and
the corresponding host-mode statement near line 87 to remove the optional LAN
listener or explicitly mark it disabled until W4 security gates pass; retain
loopback-only Kestrel behavior and client-mode details unchanged.
- Around line 33-34: Update SC5 to describe the existing React --mode webclient
served by the node, removing the unsupported Blazor web head claim; do not imply
multi-user support until the existing AUTH-1/CARR-1 gate is satisfied, and keep
the wording aligned with ADR 0167.
- Around line 4-6: Update the execution guidance in this working note to align
with ADR 0167: move the Shipyard.Client baseline out of W5, make the PWA the
default mobile gate before native client mode, reflect that the existing React
web head ships, and remove or mark historical the SC5 Blazor-web-served-by-node
and inverted fallback instructions.
🪄 Autofix
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: 16f1f802-98de-455f-8373-242955bc26b6
📒 Files selected for processing (2)
docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.mdicm/working-notes/2026-08-04-dotnet-edition-system-design.md
| ### D3 — The hull is a parity pair, scoped to the faces that exist (L-2) | ||
|
|
||
| `packages/hull-dotnet` → `Shipyard.Hull` joins `apps/hull` as a **parity pair under ADR 0014's | ||
| amendment**, on the same footing as `ui-react` ↔ `ui-adapters-blazor`. The parity obligation is | ||
| against the **five faces built in TypeScript** — `announce`, `negotiate`, `address`, `observe`, | ||
| `invoke` — plus `apps/hull/src/resolution/`, not against ADR 0124's full eight-face contract. When a | ||
| sixth face lands in TS it enters the pair; the .NET side is never obliged to lead. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
issue [blocking]: Add a conformance source for the hull parity pair.
D3 accepts two hand-maintained membrane implementations. The W3 gate checks five faces and five round-trip cases, but it does not prevent later semantic drift. Add a generated contract or differential fixture and make it a required CI check.
As per path instructions, avoid hand-parallel duplicate copies of single-source things (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 `@docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md`
around lines 153 - 159, Add a generated conformance contract or differential
fixture covering the five TypeScript hull faces and resolution round-trip cases,
then make its validation a required CI gate for the apps/hull and Shipyard.Hull
parity pair. Generate the shared expectations from a single source rather than
maintaining parallel hand-written copies, and integrate the check with the
existing W3 validation.
Source: Path instructions
| `packages/hull-dotnet` → `Shipyard.Hull` joins `apps/hull` as a **parity pair under ADR 0014's | ||
| amendment**, on the same footing as `ui-react` ↔ `ui-adapters-blazor`. The parity obligation is |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== matching files =="
git ls-files | rg '(^|/)0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node\.md$|^icm/working-notes/2026-08-04-dotnet-edition-system-design\.md$|docs/adrs|_shared/engineering/code-review-policy\.yaml' | sed -n '1,200p'
echo
echo "== shipyard namespace/project references in target docs =="
rg -n -C 2 'Shipyard\.(Foundation|Kernel|Client\.Dotnet|Container\.Dotnet|Car[ri]*|Hull|Carrier|Foundation|Kernel|Foundation\.|Kernel\.)|packages/(hull-dotnet|client-dotnet|.*dotnet)|apps/(.*dotnet|.*carrier)' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md icm/working-notes/2026-08-04-dotnet-edition-system-design.md || true
echo
echo "== package project files and Directory.Packages =="
fd -a 'Directory.Packages.props$|\.csproj$' . | sed -n '1,200p'
rg -n -C 2 '<ProjectName>|<RootNamespace>|<PackageId>|<AssemblyName>|<TargetFramework>.*11\.0|net11\.0|net9\.0|net8\.0|Microsoft\.NET\.Sdk\.Razor|UseMaui' --glob '*.csproj' --glob 'Directory.Packages.props' . || true
echo
echo "== existing foundation/kernel projects =="
rg -n '<ProjectName>|<PackageId>|<RootNamespace>|<ProjectReference>|<PackageReference>|Foundation|Kernel|Foundation\.|Kernel\.|shipyard' --glob '*.csproj' --glob 'Directory.Packages.props' packages apps docs _shared 2>/dev/null | sed -n '1,260p' || trueRepository: Harborline-Software/shipyard
Length of output: 50385
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== target doc relevant sections =="
sed -n '1,260p' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md
echo
echo
sed -n '1,120p' icm/working-notes/2026-08-04-dotnet-edition-system-design.mdRepository: Harborline-Software/shipyard
Length of output: 26427
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== deterministic package name and project namespace extraction =="
python3 - <<'PY'
from pathlib import Path
import re
files = [
"docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md",
"icm/working-notes/2026-08-04-dotnet-edition-system-design.md",
]
for file in files:
print(f"-- {file} --")
text = Path(file).read_text()
for i, name in enumerate(re.findall(r"(?:Shipyard\.[A-Za-z0-9_.]+|packages/\S+dotnet\S*|apps/\S*dotnet\S*|\.csproj)", text), 1):
line = text[:text.find(name, text.find(name) if i == 1 else 0)].count("\n") + 1
# find actual occurrence near text position
idx = text.find(name)
line = text[:idx].count("\n") + 1 if idx != -1 else "?"
print(f"{line}: {name}")
print()
print("-- csproj package identity ==")
for p in sorted(Path(".").rglob("*.csproj")):
if any(part.endswith("dotnet") for part in p.parts):
data = p.read_text(errors="ignore")
vals = {}
for tag in ["ProjectName","PackageId","RootNamespace","AssemblyName","TargetFramework","TargetFrameworks"]:
m = re.search(rf"<{tag}[^>]*>(.*?)</{tag}>|<{tag}>.*?</{tag}>", data, re.S)
if m:
vals[tag] = m.group(1).strip().replace("\n","")
print(f"{p}")
print(f" {vals}")
PYRepository: Harborline-Software/shipyard
Length of output: 1583
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate dotnet package files =="
git ls-files '*dotnet*.csproj' '*dotnet*' 'packages/*client*' 'packages/*hull*' 'apps/*carrier*' 'apps/*dotnet*' | sed -n '1,120p'
echo
echo "== all hull/client references and csproj identity =="
rg -n -C 2 'packages/(hull-dotnet|client-dotnet)|Shipyard\.(Hull|Client)|hull-dotnet|client-dotnet|HullDotnet|ClientDotnet' --glob '*.csproj' --glob 'Directory.Packages.props' --glob '*dotnet*' . || true
echo
echo "== namespace/project property scan for shipyard client or hull in dotnet paths =="
python3 - <<'PY'
from pathlib import Path
import re
for p in sorted(Path('.').rglob('*.csproj')):
s = p.read_text(errors='ignore')
if any(part.endswith('dotnet') for part in p.parts):
vals = {}
for tag in ['ProjectName','PackageId','RootNamespace','AssemblyName','TargetFramework','TargetFrameworks']:
m = re.search(rf'(<{tag}[^>]*(?:/>|>.*?</{tag}>))', s, re.S)
if m:
vals[tag]=m.group(0)
if s:
print(p, vals)
PYRepository: Harborline-Software/shipyard
Length of output: 20651
suggestion [blocking]: Apply one package-naming decision across both documents.
These packages are introduced by ADR 0167 and currently named inconsistently with the existing Shipyard.Foundation.* / Shipyard.Kernel.* conventions. If this exception is intended, apply it consistently across:
docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md#L83-L83,#L132-L132,#L155-L156,#L202-L206,#L325-L326,#L365-L367icm/working-notes/2026-08-04-dotnet-edition-system-design.md#L28-L28,#L55-L55,#L72-L72,#L84-L84,#L119-L119
📍 Affects 2 files
docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md#L155-L156(this comment)docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md#L202-L206icm/working-notes/2026-08-04-dotnet-edition-system-design.md#L55-L76
🤖 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 `@docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md`
around lines 155 - 156, Apply one consistent package-naming decision for
Shipyard.Hull and the related .NET package references across
docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md at
lines 83, 132, 155-156, 202-206, 325-326, and 365-367, and
icm/working-notes/2026-08-04-dotnet-edition-system-design.md at lines 28, 55,
72, 84, and 119; align every occurrence with the chosen naming convention,
including the parity-pair description and package references.
Source: Path instructions
| | Projects | `apps/carrier-dotnet/Shipyard.Carrier.Dotnet.csproj` (`Microsoft.NET.Sdk.Razor`, `<UseMaui>true</UseMaui>`) · `apps/carrier-dotnet/hosting/Shipyard.Carrier.NodeHosting.csproj` (`net11.0`, the sole node referrer) · `packages/client-dotnet/Shipyard.Client.csproj` (`net11.0` in W2; W5 multi-targets it `net11.0;net11.0-ios;net11.0-android` if native is authorized) | | ||
| | TFMs | W2: `net11.0-windows10.0.19041.0;net11.0-maccatalyst`. W5 adds `net11.0-ios;net11.0-android` | | ||
| | Packages | the two already pinned at `Directory.Packages.props:124-125`. No new central version without a CPM entry | | ||
| | Workloads | pin the MAUI workload set — `global.json` `sdk.workloadVersion` **if the preview SDK honours it** (verify with `dotnet workload --info`); otherwise a committed workload-set version in the CI install step. Unverified which applies at this preview; W2 must record which | |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== git status/stat =="
git diff --stat || true
echo "== relevant ADR lines =="
sed -n '180,270p' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md
echo "== repo dotnet workload/sdk pins =="
rg -n '"sdk\.workloadVersion"|workloadVersion|MAUI|maui|workload set|targeting pack|targeting-packs|global\.json|Dotnet|MAUI|workloadVersion|wasm|Targeting' -S --glob 'global.json' --glob 'Directory.Packages.props' --glob '*.csproj' --glob 'README.md' --glob 'docs/**' .Repository: Harborline-Software/shipyard
Length of output: 50386
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re, glob
adr = Path('docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md')
text = adr.read_text()
print('ADR exists:', adr.exists())
d5 = re.search(r'^### D5 — BLOCKER I-1:.*?(?=\n### D6|\Z)', text, re.S)
if d5:
block = d5.group(0)
print('D5 table contains "pin the MAUI workload set":', 'pin the MAUI workload set' in block)
print('D5 table contains "sdk.workloadVersion":', 'sdk.workloadVersion' in block)
print('D5 table contains "dotnet workload --info":', 'dotnet workload --info' in block)
print('D5 table contains "Workload install" or "workload-install":', bool(re.search(r'Workload install|workload-install', block)))
else:
print('D5 block not found')
gate = re.search(r'(?s)^|Gate G-R1 — .*?\n\]\n\n### D6', text)
table_start = text.find('| Wave | Predecessors | Binary exit gate | Review tier |')
if table_start != -1:
table = text[table_start:]
w1 = re.search(r'\|\s**W1** RCL split\s*\| W0 \| `tooling/ui-adapters-blazor-mobile-restore/restore\.sh` exits 0\s*\*\*on a runner with iOS/Android targeting packs\*\*;', table)
print('W1 gate mentions restore.sh with iOS/Android targeting packs check:', bool(w1))
ci_workload = re.search(r'CI install step|committe?ed workload-set|workload-set|dotnet workload.*install', table)
print('W8 gate doc contains workload install wording:', bool(ci_workload))
for path in ['global.json','Directory.Packages.props']:
p = Path(path)
if p.exists():
print(f'{path}: workloadVersion line count:', len([l for l in p.read_text().splitlines() if 'workloadVersion' in l]))
print(f'{path}: contains "workloadVersion":', 'workloadVersion' in p.read_text())
print(f'{path}: contains "sdk.workloadVersion":', 'sdk.workloadVersion' in p.read_text())
for p in glob.glob('tooling/**/*-restore/*.sh', recursive=True):
txt = Path(p).read_text(errors='replace')
if 'dotnet workload' in txt or 'targeting' in txt.lower() or 'pack' in txt.lower():
print('FOUND TOOLING SCRIPT:', p)
for i, line in enumerate(txt.splitlines(), 1):
if any(s in line for s in ['dotnet workload', 'targeting pack', 'targeting-pack', 'targeting-packs', 'ios', 'android']):
print(f'{p}:{i}:{line}')
PYRepository: Harborline-Software/shipyard
Length of output: 987
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== precise D5 occurrences =="
rg -n '^### D5|pin the MAUI workload set|sdk\.workloadVersion|dotnet workload --info|W2 must record' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md || true
echo "== W1 gate occurrences =="
rg -n '\\*\*W1\\*\* RCL split|tooling/ui-adapters-blazor-mobile-restore/restore\.sh|targeting pack|CI install step|workload-set|workload install' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md || true
echo "== project/workload files =="
git ls-files global.json Directory.Packages.props tooling apps/carrier-dotnet packages/client-dotnet | sed -n '1,120p'
for p in global.json Directory.Packages.props; do
if [ -f "$p" ]; then
echo "--- $p workload-related lines ---"
rg -n 'workloadVersion|MAUI|maui|Microsoft\.NET\.SDK|TargetFramework|net11\.0' "$p" || true
fi
done
echo "== restore scripts =="
git ls-files 'tooling/**/*restore*' | sed -n '1,100p'
for f in $(git ls-files 'tooling/**' | rg -i 'restore|workflow|ci|yaml|yml|sh|ps1'); do
if rg -q 'dotnet workload|MAUI|Maui|Maccatalyst|ios|android|targeting pack|targeting-pack' "$f"; then
echo "--- $f ---"
rg -n -C 2 'dotnet workload|MAUI|Maui|Maccatalyst|ios|android|targeting pack|targeting-pack' "$f" | sed -n '1,120p'
fi
doneRepository: Harborline-Software/shipyard
Length of output: 12929
issue [blocking]: Make MAUI restore reproducible from a clean checkout.
D5 makes the workload pin conditional and unverified, leaving W2 contingent on runtime discovery. W1 only passes on a runner with iOS/Android targeting packs. Require committed workload-set and targeting-pack installation with exact versions, and make the W1/W2 gate fail when those versions are absent.
🤖 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 `@docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md`
around lines 202 - 205, Update the MAUI Workloads and TFMs entries in the ADR so
clean-checkout restore is deterministic: commit the workload-set installation
with an exact version, explicitly install the required iOS/Android targeting
packs with exact versions, and remove the conditional “verify at runtime”
language. Define the W1/W2 gate to fail when any pinned workload or
targeting-pack version is missing, rather than relying on runner state.
Source: Path instructions
| | Projects | `apps/carrier-dotnet/Shipyard.Carrier.Dotnet.csproj` (`Microsoft.NET.Sdk.Razor`, `<UseMaui>true</UseMaui>`) · `apps/carrier-dotnet/hosting/Shipyard.Carrier.NodeHosting.csproj` (`net11.0`, the sole node referrer) · `packages/client-dotnet/Shipyard.Client.csproj` (`net11.0` in W2; W5 multi-targets it `net11.0;net11.0-ios;net11.0-android` if native is authorized) | | ||
| | TFMs | W2: `net11.0-windows10.0.19041.0;net11.0-maccatalyst`. W5 adds `net11.0-ios;net11.0-android` | | ||
| | Packages | the two already pinned at `Directory.Packages.props:124-125`. No new central version without a CPM entry | | ||
| | Workloads | pin the MAUI workload set — `global.json` `sdk.workloadVersion` **if the preview SDK honours it** (verify with `dotnet workload --info`); otherwise a committed workload-set version in the CI install step. Unverified which applies at this preview; W2 must record which | | ||
| | DI / lifecycle | `MauiProgram.CreateMauiApp()` registers `INodeHost` + `Shipyard.Client` + `Shipyard.UIAdapters.Blazor`. `OnStart` awaits `StartAsync` and only then navigates the `BlazorWebView`; `OnStop`/`OnDestroy` calls `StopAsync`. **`Shipyard.Client` is built IN W2** — the minimal loopback surface the shell needs (session bootstrap + the D5 acceptance endpoints); W5 extends it with the queue/cache client contract (I-4) if the D6 gate authorizes native | | ||
| | Storage ownership | the node owns the SQLCipher store. The shell owns nothing durable except the OS-keychain entry for the passphrase; the per-boot token is in-memory only and never persisted | | ||
| | Acceptance | `dotnet build` green on both W2 TFMs, **and** an E2E that creates a contact via `POST /api/local-node/contacts`, reads it back via `GET /api/local-node/contacts/{id}`, and asserts both the persisted row and a non-empty audit envelope | |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== repo files around ADR =="
git ls-files | rg '(^docs/adrs/0167|_shared/engineering/code-review-policy\.yaml|Directory\.Packages\.props|packages/client-dotnet|apps/carrier-dotnet|tests|test)' | sed -n '1,200p'
echo
echo "== ADR excerpt =="
sed -n '180,220p' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md || true
echo
echo "== audit mentions in ADR =="
rg -n "audit|Audit|envelope|acceptance|POST /api/local-node/contacts|GET /api/local-node/contacts" docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md || true
echo
echo "== repo audit/contacts mentions (limited) =="
rg -n "Audit|audit|Contact|contact|local-node/contacts" -g '!node_modules' -g '!dist' -g '!build' | sed -n '1,240p'
echo
echo "== docs/ADR frontmatter validation candidates =="
python3 - <<'PY'
from pathlib import Path
required={'id','concern','pipeline_variant','composes','superseded_by'}
for p in Path('docs/adrs').glob('*'):
if not p.name.endswith('.md'): continue
lines=p.read_text(errors='ignore').splitlines()
block=[]
in_fm=False
for line in lines:
if line.strip()=='---':
if not in_fm: in_fm=True
else: in_fm=False
elif in_fm: block.append(line.strip())
fm={}
for item in block:
if ':' not in item: continue
k,v=item.split(':',1)
fm[k.strip()]=v.strip()
print(f"{p}: missing={sorted(required-fm.keys())} extra_front={sorted(set(fm)-required)}")
PYRepository: Harborline-Software/shipyard
Length of output: 17717
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== ADR lines 1-120 =="
sed -n '1,120p' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md || true
echo
echo "== ADR lines 370-420 =="
sed -n '370,420p' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md || true
echo
echo "== docs/adrs list files =="
git ls-files docs/adrs | sed -n '1,80p'
echo
echo "== files mentioning audit persistence assertions =="
rg -n "persistent|persist|committed|retained|audit-store|read-model|audit.*record|audit.*entry|Audit.*record|audit.*link|audit.*contact|contact.*audit|audit.*contact" docs -g '*.md' || true
echo
echo "== files mentioning local-node/contacts tests =="
rg -n "POST /api/local-node/contacts|GET /api/local-node/contacts|local-node/contacts|contacts" apps packages -g '*.cs' -g '*.tsx' -g '*.ts' -g '*.js' -g '*.md' -g '*.mjs' || trueRepository: Harborline-Software/shipyard
Length of output: 50385
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== code-review policy relevant lines =="
rg -n "independent reviewer|self-assertion|CP|audit|financial|compliance|security|concurrency" _shared/engineering/code-review-policy.yaml || true
echo
echo "== ADR 0167 frontmatter and relevant sections =="
python3 - <<'PY'
from pathlib import Path
p=Path('docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md')
lines=p.read_text().splitlines()
in_fm=False
fm=[]
for line in lines[:120]:
if line.strip()=='---':
if not fm: in_fm=not in_fm
continue
if in_fm: fm.append(line.strip())
print('frontmatter:')
for l in fm: print(l)
print('\nlines 70-95:')
for i,l in enumerate(lines[69:95], start=70): print(f'{i}: {l}')
print('\nlines 120-215:')
for i,l in enumerate(lines[119:215], start=120): print(f'{i}: {l}')
print('\nlines 385-405:')
for i,l in enumerate(lines[384:405], start=385): print(f'{i}: {l}')
PY
echo
echo "== narrow audit/contact mentions in docs/adrs/0167 and docs specs =="
rg -n "Audit|audit|acceptance|POST /api/local-node/contacts|GET /api/local-node/contacts|persisted row|non-empty|envelope|Contact|contact" docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md docs -g 'adr*.md' -g '*.md' | head -n 120 || trueRepository: Harborline-Software/shipyard
Length of output: 50384
issue (blocking): Require an audit-store assertion in the D5 acceptance criteria.
non-empty audit envelope only proves emission. For this audit/CP gate, assert that the accepted read model or audit store contains the same contact ID after the request completes.
🤖 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 `@docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md`
around lines 202 - 208, Update the Acceptance criteria to require the E2E test
to verify that the accepted read model or audit store contains the same contact
ID after the contact creation and read requests complete, in addition to
asserting a non-empty audit envelope.
Source: Path instructions
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== repo files relevant =="
git ls-files | sed -n '1,220p' | rg '(^docs/adrs/0167|D5|acceptance|contacts|Shipyard\.Client|NodeHosting|Bearer|Authorization|Authentication|D5)' || true
echo
echo "== target doc excerpt =="
sed -n '180,220p' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md || true
echo
echo "== search for test/auth acceptance references =="
rg -n -S '\b(E2E|acceptance|D5|contacts|Authorization|Bearer|Bearer|JWT|invalid token|unauthenticated|missing (auth |token|credential)|token|credential|auth)\b' docs apps packages .github 2>/dev/null | sed -n '1,260p' || trueRepository: Harborline-Software/shipyard
Length of output: 50384
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== ADR 0167 relevant excerpts =="
sed -n '19,45p' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md
sed -n '100,145p' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md
sed -n '188,212p' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md
echo
echo "== KB query for ADR/acceptance specification =="
pnpm kb query "ADR 0167 Shipyard.Client acceptance contacts E2E bearer token authentication D5" || true
echo
echo "== focused auth token route specs in docs/specifications =="
rg -n -S -i '\b(/api/local-node/contacts|local-node|contacts|Authorization: Bearer|bearer|invalid token|unauthenticated|401|403)\b' docs/specifications docs/adrs docs/superpowers/plans -g '*.md' | sed -n '1,220p'Repository: Harborline-Software/shipyard
Length of output: 50384
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== ADR 0167 D1/D2/D5 section =="
sed -n '48,58p' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md
sed -n '100,122p' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md
sed -n '199,208p' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md
echo
echo "== focused D5 acceptance and D2 authority context in ADR 0167 =="
rg -n -S '(D5 is the board|D5) Blocker|Acceptance|per-boot bearer|enforced|Contact|contacts|Shipyard\.Client|loopback' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.mdRepository: Harborline-Software/shipyard
Length of output: 6878
Broken Authentication (CWE-287): Improper Authentication
Reachability: External
issue [blocking]: Require auth negative cases in the D5 E2E.
D5 acceptance currently defines a POST + GET data-flow round trip only. Because the host authorizes each dotnet carrier request with a per-boot bearer token, update the E2E to pass valid auth for the round trip and assert 401/403 for missing and invalid tokens. This prevents accepting the scaffold when only unauthenticated contact reads/writes are exercised.
🤖 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 `@docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md`
around lines 202 - 208, Update the D5 E2E acceptance flow to send the valid
per-boot bearer token for the contact POST and GET round trip, then add checks
that requests with a missing token and an invalid token receive 401 or 403. Keep
the existing persisted-row and non-empty audit-envelope assertions, and anchor
the changes to the D5 E2E and per-boot authorization behavior described in the
acceptance section.
Source: Path instructions
| ### D7 — BLOCKER M-1: wave DAG, binary gates, and the resume protocol | ||
|
|
||
| ``` | ||
| W0 discovery (DONE) ──▶ W1 ──┬──▶ W2 ──┬─────────────▶ W8 | ||
| ├──▶ W3 ──┤ | ||
| ├──▶ W6 ──┤ | ||
| └──▶ W7 ──┘ | ||
| W4-spec ──┬──▶ W4 ──┐ | ||
| W2 ───────┘ │ | ||
| W-PWA (D6 gate) ────┴──▶ W5 ──▶ W8 (only if the D6 gate authorizes native) | ||
| ``` |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
issue [blocking]: Add the W4 → W5 dependency to the DAG.
The W5 row lists W4 and W-PWA as predecessors. The diagram shows only W-PWA → W5. The table and diagram therefore permit different execution orders. Add the missing W4 → W5 edge.
As per path instructions, review the gate table and DAG as one cross-layer contract under the single-source policy.
Also applies to: 256-264
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 242-242: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 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 `@docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md`
around lines 240 - 250, Update the wave DAG under “D7 — BLOCKER M-1” to show W4
as an explicit predecessor of W5 alongside the existing W-PWA gate, matching the
predecessor contract in the W5 table row. Ensure the diagram and gate table
permit the same execution order under the single-source policy.
Source: Path instructions
| | Target | Command / source | Today | Gate | | ||
| |---|---|---|---| | ||
| | Component availability | `apps/docs/components/index.md:19` | Blazor 125 / 284 | informational; W6 updates it | | ||
| | **Carrier-used coverage** | `tooling/blazor-parity/carrier-used-components.txt`, regenerated by a committed generator script and diffed in CI. **Neither exists yet (§A0 — Introduced); authoring the generator is W6's FIRST obligation.** Exploratory measurement 2026-08-04 gave 57 (single-line import regex) to 110 (full parse, includes type-only imports) distinct `@harborline-software/ui-react` exports in `apps/carrier/src` — the committed script, not this range, is the contract | W6 exit: 100% | |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
issue [blocking]: Restore the missing table cell.
The header has four columns, but the carrier-used coverage row has three cells. W6 exit: 100% is currently placed in the Today column, and the Gate column is missing. Add a Today value and keep W6 exit: 100% in Gate.
Static analysis reports markdownlint MD056 for this row.
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 282-282: Table column count
Expected: 4; Actual: 3; Too few cells, row will be missing data
(MD056, table-column-count)
🤖 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 `@docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md`
at line 282, The carrier-used coverage row in the ADR table has one missing
cell, causing MD056 and misplacing the gate value. Add the missing Today-column
value before the existing W6 exit entry, and keep “W6 exit: 100%” as the
Gate-column value, preserving the four-column table structure.
Source: Linters/SAST tools
| - [x] **Independent verification.** A six-perspective adversarial board reviewed the design and returned | ||
| GO-WITH-CHANGES with three BLOCKERs; all three are answered by named decisions, and two board | ||
| findings were **narrowed after this ADR's own verification contradicted them** (§A0 correction (a)). |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
chore [blocking]: Record an independent review for this CP-touching change.
This ADR changes security and audit gates. The prior board review is design evidence, not a substitute for an independent reviewer on this PR. Record the independent review and its result before merge.
As per path instructions, CP-touching changes require an independent reviewer, not self-assertion (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 `@docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md`
around lines 406 - 408, Update the ADR’s “Independent verification” checklist to
record a separate independent review of this CP-touching change, including the
reviewer and review result, rather than relying only on the prior design board
review or self-assertion. Preserve the existing board-review context while
documenting that the independent reviewer assessed the security and audit-gate
changes before merge.
Source: Path instructions
| **Superseded in part by ADR 0167** — see its D5 (`Shipyard.Client` builds in W2, not W5), D6 (the PWA | ||
| is the DEFAULT mobile outcome; native must beat it — this note's §8 fallback framing is inverted), and | ||
| D7 (the normative wave DAG + resume protocol). |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^icm/working-notes/2026-08-04-dotnet-edition-system-design\.md$|ADR 0167|adr|A167|A168|0167)' || true
echo "== target file outline/size =="
wc -l icm/working-notes/2026-08-04-dotnet-edition-system-design.md
echo "== target file relevant sections =="
sed -n '1,170p' icm/working-notes/2026-08-04-dotnet-edition-system-design.md
echo "== search ADR 0167 references =="
rg -n "ADR 0167|A168|A167|W[25]|Shipyard\.Client|SC5|react|React|React web|web head|PWA|PWA|fallback" icm/working-notes/2026-08-04-dotnet-edition-system-design.md packages apps tooling _shared --glob '!coverage/**' --glob '!node_modules/**' || trueRepository: Harborline-Software/shipyard
Length of output: 50385
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Knowledge Base references for ADR 0167/W2/W5/Shipyard.Client/PWA/React =="
pnpm kb query "ADR 0167" || true
pnpm kb query "W2 Shipyard.Client" || true
pnpm kb query "W5 Shipyard.Client" || true
pnpm kb query "Shipyard.Client W2 W5" || true
pnpm kb query "PWA default mobile outcome" || true
pnpm kb query "ADRs naming A167" || trueRepository: Harborline-Software/shipyard
Length of output: 12058
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== target file relevant sections with line numbers =="
sed -n '1,30p' icm/working-notes/2026-08-04-dotnet-edition-system-design.md
sed -n '18,36p' icm/working-notes/2026-08-04-dotnet-edition-system-design.md
printf '\n--- Section 6 references ---\n'
rg -n '(^## Section 6|W5|ADR 0167|Shipyard\.Client|SDK|SC[0-9]+)' icm/working-notes/2026-08-04-dotnet-edition-system-design.md
printf '\n--- exact section/SC references ---\n'
python3 - <<'PY'
from pathlib import Path
p=Path('icm/working-notes/2026-08-04-dotnet-edition-system-design.md')
lines=p.read_text().splitlines()
for i,l in enumerate(lines,1):
if 'Section 6' in l or 'Section 8' in l or l.startswith('SC5') or 'W5' in l or 'Shipyard.Client' in l or 'React web head' in l or 'PWA' in l or 'fallback' in l.lower():
print(f"{i}: {l}")
PYRepository: Harborline-Software/shipyard
Length of output: 6317
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== docs/adrs/files containing 0167 or A167 =="
git ls-files | rg '(^docs/adrs/.*1[67][0-9]|^(icm|_shared|pkg)/).*[Aa]dr[AaDdRr].*[67]' || true
git ls-files | rg '0167|A167|A168' || true
echo "== references to ADR 0167/0168 in target and docs =="
rg -n --hidden --glob '!node_modules/**' --glob '!coverage/**' --glob '!dist/**' --glob '!build/**' '(ADR 016[78]|A16[78]|D5|D6|D7|Shipyard\.Client|PWA|PWA|fallback|react|React)' icm/working-notes/2026-08-04-dotnet-edition-system-design.md docs/adrs _shared engine packages apps 2>/dev/null | head -n 200
echo "== ADR files around 0150-0170 =="
git ls-files docs/adrs | awk 'NR>=144 && NR<=156' | while read f; do
echo "--- $f ---"
sed -n '1,120p' "$f"
doneRepository: Harborline-Software/shipyard
Length of output: 50384
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== target exact relevant sections =="
sed -n '1,145p' icm/working-notes/2026-08-04-dotnet-edition-system-design.md | nl -ba
echo "== ADR 0167 header and D decisions =="
sed -n '1,240p' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md | nl -ba | sed -n '1,220p'Repository: Harborline-Software/shipyard
Length of output: 243
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== target exact relevant sections =="
sed -n '1,145p' icm/working-notes/2026-08-04-dotnet-edition-system-design.md | awk '{printf "%6d %s\n", NR, $0}'
echo "== ADR 0167 header and D decisions =="
sed -n '1,240p' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md | awk '{printf "%6d %s\n", NR, $0}'Repository: Harborline-Software/shipyard
Length of output: 29657
issue [blocking]: Remove stale execution instructions from the working note.
ADR 0167 D5 moves Shipyard.Client baseline out of W5, D6 makes the PWA baseline the gate before native client mode, and D5 says the existing React web head ships. This note still assigns Shipyard.Client to W5, calls PWA fallback under failed conditions, and keeps SC5 as Blazor web served by the node. Mark these as historical, or align them with ADR 0167.
🤖 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 `@icm/working-notes/2026-08-04-dotnet-edition-system-design.md` around lines 4
- 6, Update the execution guidance in this working note to align with ADR 0167:
move the Shipyard.Client baseline out of W5, make the PWA the default mobile
gate before native client mode, reflect that the existing React web head ships,
and remove or mark historical the SC5 Blazor-web-served-by-node and inverted
fallback instructions.
Source: Path instructions
| - **SC5** — web head: Blazor web served by the node, **single-user posture only** until AUTH-1 / | ||
| CARR-1 land (ADR 0165 A1 — carrier server mode is NOT yet built; multi-user web is gated). |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== repo files =="
git ls-files | sed -n '1,200p'
echo "== target file =="
if [ -f icm/working-notes/2026-08-04-dotnet-edition-system-design.md ]; then
nl -ba icm/working-notes/2026-08-04-dotnet-edition-system-design.md | sed -n '1,120p'
else
echo "missing target file"
fi
echo "== ADR candidates =="
git ls-files | rg '(^|/)(ADR|adr|\.md)' | rg -i 'adr 0167|0167|webhead|web head|webclient|blazor' || true
echo "== relevant text search =="
rg -n -i "ADR 0167|0167|React .*webclient|webclient|Blazor|sc5|SC5|--mode webclient|web head|webhead" . --glob '!node_modules' --glob '!dist' --glob '!build' -SRepository: Harborline-Software/shipyard
Length of output: 14462
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== target file size and lines =="
if command -v nl >/dev/null 2>&1; then
nl -ba icm/working-notes/2026-08-04-dotnet-edition-system-design.md | sed -n '1,120p'
elif command -v awk >/dev/null 2>&1; then
awk '{printf "%7d\t%s\n", NR, $0}' icm/working-notes/2026-08-04-dotnet-edition-system-design.md | sed -n '1,140p'
else
cat -n icm/working-notes/2026-08-04-dotnet-edition-system-design.md | sed -n '1,140p'
fi
echo "== ADR 0167 candidates exact names =="
for f in $(git ls-files | rg -i 'adr.*167|167.*adr|0167' || true); do
echo "--- $f"
wc -l "$f"
rg -n -i 'ADR 0167|0167|webclient|mode|web|react|blazor|D1|D[0-9]' "$f" -S || true
done
echo "== relevant broader text search =="
rg -n -i 'ADR 0167|0167|React .*webclient|webclient|--mode webclient|Blazor|sc5|SC5|web head|webhead|carrier server mode|multi-user web' icm .wolf packages apps tooling _shared --glob '!node_modules' --glob '!dist' --glob '!build' -S || trueRepository: Harborline-Software/shipyard
Length of output: 50385
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== target file excerpt =="
awk '{printf "%7d\t%s\n", NR, $0}' icm/working-notes/2026-08-04-dotnet-edition-system-design.md 2>/dev/null | sed -n '1,120p'
echo "== ADR candidates by name pattern =="
git ls-files | awk 'tolower($0) ~ /(adr|adr-|\.adr|0167|\.167|167\.|0167|167)/' | sort -u | sed -n '1,200p'
echo "== precise ADR 0167 searches across repo (focused) =="
rg -n -i '(ADR\s+0167|0167\s+ADR|ADR-0167|0167|webclient|--mode webclient|React .*webclient|Blazor|SC5|SC 5|carrier server mode|multi-user web)' \
--glob '!node_modules' --glob '!dist' --glob '!build' --glob '!*.png' --glob '!*.jpg' --glob '!*.svg' \
| sed -n '1,240p'Repository: Harborline-Software/shipyard
Length of output: 20371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== ADR 0167 content =="
if [ -f docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md ]; then
awk '{printf "%7d\t%s\n", NR, $0}' docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md | sed -n '1,240p'
else
echo "missing docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md"
fi
echo "== ADR 0166 targeted excerpts =="
python3 - <<'PY'
from pathlib import Path
p=Path('docs/adrs/0166-carrier-ui-stack-react-remains-blazor-stays-a-maintained-target.md')
if p.exists():
s=p.read_text()
for n,line in enumerate(s.splitlines(),1):
if any(x in line for x in ['ADR 0166 D', 'React', 'Blazor', 'webclient', 'web', '0167', 'SC']):
print(f"{n:04d}\t{line}")
PY
echo "== precise web/head decision excerpts =="
rg -n -i '(^|[^A-Za-z])D[0-9]+[^A-Za-z]|ADR 0167|React .*web|webclient|--mode webclient|web head|Blazor web|node served|single-user|multi-user|carrier server mode|owner|responsibili' \
docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md \
docs/adrs/0166-carrier-ui-stack-react-remains-blazor-stays-a-maintained-target.md \
docs/adrs/0165-full-dotnet-viability-node-served-clients-and-the-mobile-client-boundary.md \
icm/working-notes/2026-08-04-dotnet-edition-system-design.md \
--glob '!node_modules' --glob '!dist' --glob '!build' -S || trueRepository: Harborline-Software/shipyard
Length of output: 50384
issue [blocking]: Keep SC5 aligned with ADR 0167.
ADR 0167 D1 defines the web head as the existing React --mode webclient served by the node. ADR 0167 does not authorize or define a Blazor web head. Either remove Blazor web from SC5 or update ADR 0167 with explicit ownership and a gate before implementing it.
🤖 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 `@icm/working-notes/2026-08-04-dotnet-edition-system-design.md` around lines 33
- 34, Update SC5 to describe the existing React --mode webclient served by the
node, removing the unsupported Blazor web head claim; do not imply multi-user
support until the existing AUTH-1/CARR-1 gate is satisfied, and keep the wording
aligned with ADR 0167.
Source: Path instructions
| | Mode | Targets | Node | Storage | | ||
| |---|---|---|---| | ||
| | **host** | net11.0 (WinUI), net11.0-maccatalyst | `local-node-host` in-process (Kestrel on loopback + optional LAN listener) | node's SQLCipher store | | ||
| | **client** | net11.0-ios, net11.0-android | none — HTTP client of a paired node | append-only event queue + read cache (SQLCipher via `bundle_e_sqlcipher` mobile natives, 0165 F1) | |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🌐 Web query:
pnpm kb query ADR 0167 host mode loopback LAN listener W4 security gate
💡 Result:
The terms in your query, specifically "ADR 0167," "W4 security gate," and "host mode loopback," relate to distinct technical contexts rather than a single unified pnpm feature. ADR 0167 is a document designator appearing in multiple independent software projects to manage tracking processes or security architectures [1][2][3][4]. For example, in the CorvinOS project, ADR-0167 governs the "Entangled License Ratchet" (ELR) and egress security gates for controlling network access [4][5][6]. W4 refers to a specific warning or linting code in the plan-forge project's gate command rules, which identifies the use of a require call on an.mjs file path, which will fail at runtime [7]. In the pnpm ecosystem, "security gates" typically refer to features like strict dependency build controls (e.g., --strict-dep-builds) that fail the install process if unapproved build scripts are present [8], but "W4" is not a standard pnpm security gate identifier [8][9]. There is no native pnpm feature matching "host mode loopback LAN listener" linked to these ADR or W4 identifiers. If this query relates to a specific local monorepo tool or an orchestration layer like klasp [10] or plan-forge [7], it is likely a project-specific implementation of network egress or security enforcement.
Citations:
- 1: https://github.com/accuser/bynk/blob/main/design/tracks/README.md
- 2: https://orbit-cli.com/architecture/design/user-interface/4_decisions/
- 3: https://orbit-cli.com/architecture/design/agent-families/4_decisions/
- 4: https://github.com/CorvinLabs/CorvinOS/blob/main/operator/license/elr_capabilities_m2.py
- 5: https://github.com/CorvinLabs/CorvinOS/blob/main/operator/bridges/shared/egress_gate.py
- 6: https://github.com/CorvinLabs/CorvinOS/blob/main/operator/license/elr.py
- 7: https://github.com/srnichols/plan-forge/blob/master/.github/instructions/plan-gate-command-rules.md
- 8: feat: add an option to fail on unapproved dependency builds pnpm/pnpm#9071
- 9: PNPM v10.3.0 new onlyBuiltDependencies does not work when shared-workspace-lockfile is set to false pnpm/pnpm#9082
- 10: https://libraries.io/cargo/klasp
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Relevant repo paths:"
git ls-files | rg '(^icm/working-notes/2026-08-04-dotnet-edition-system-design\.md$|ADR|0167|0067|security|gate|W4|route-allowlist|tls|pairing)' || true
echo
echo "Target file excerpt:"
if [ -f icm/working-notes/2026-08-04-dotnet-edition-system-design.md ]; then
nl -ba icm/working-notes/2026-08-04-dotnet-edition-system-design.md | sed -n '1,120p'
fi
echo
echo "Search for ADR 0167 and LAN/loopback security gate terms:"
rg -n "ADR 0167|A167|0167|W4|loopback|LAN listener|route-allowlist|security review|TLS|pairing" . --glob '!node_modules' --glob '!dist' --glob '!build' | head -200Repository: Harborline-Software/shipyard
Length of output: 16057
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Target file excerpt:"
python3 - <<'PY'
from pathlib import Path
p = Path("icm/working-notes/2026-08-04-dotnet-edition-system-design.md")
if p.exists():
lines = p.read_text(encoding="utf-8").splitlines()
for i in range(0, min(130, len(lines))):
print(f"{i+1:4}: {lines[i]}")
PY
echo
echo "ADR 0167 relevant excerpt:"
fd -a '0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node\.md$' . | while read -r f; do
echo "--- $f"
grep -nEi "loopback|LAN|listener|W4|TLS|pairing|route-allowlist|security|host mode|client|pair|allow" "$f" || true
done
echo
echo "Relevant security/gate terms in ADR and target note:"
rg -n -i "loopback|lan listener|LAN listener|optional LAN|wire|W4|TLS|pairing|route-allowlist|security review|security-review|route allow|allowlist|host mode|client mode" docs/adrs/0167-the-dotnet-edition-a-co-equal-client-stack-over-one-node.md icm/working-notes/2026-08-04-dotnet-edition-system-design.md || trueRepository: Harborline-Software/shipyard
Length of output: 25701
Security Misconfiguration (CWE-16)
issue [blocking]: Restrict the LAN listener to the W4 security gate.
ADR 0167 defines host mode as loopback only, while W4 is the gated LAN listening/pairing spec with TLS, pairing, LAN route allowlist, and deep security review. Remove the optional LAN listener from host mode in lines 47-50 and line 87, or state it is disabled until W4 gates pass.
🤖 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 `@icm/working-notes/2026-08-04-dotnet-edition-system-design.md` around lines 47
- 50, Update the host mode entry in the mode/targets table and the corresponding
host-mode statement near line 87 to remove the optional LAN listener or
explicitly mark it disabled until W4 security gates pass; retain loopback-only
Kestrel behavior and client-mode details unchanged.
Source: Path instructions
In plain terms: the decision record committing the fleet to build the .NET (Blazor/MAUI) version of the product as a second client of the same node, with the safety gates and wave plan that govern it.
Why it matters: it turns ADR 0166's funded option into an executable, gated program - and pins the one dangerous shortcut (direct in-process node calls would silently unfence the desktop plane) as forbidden with binary proof gates.
Done when: merged after the ADR 0069 pre-merge soundness council. CIC ratified the direction in session 2026-08-04.
Refs: #3642
🤖 Generated with Claude Code
Summary by CodeRabbit