Skip to content

chore(#470): C10 — the flip: delete the Dev Platform from core (~49k LOC) - #804

Merged
Weegy merged 15 commits into
mainfrom
feat/470-c10-delete-dev-platform
Aug 21, 2026
Merged

chore(#470): C10 — the flip: delete the Dev Platform from core (~49k LOC)#804
Weegy merged 15 commits into
mainfrom
feat/470-c10-delete-dev-platform

Conversation

@Weegy

@Weegy Weegy commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

epic #470 C10 — the flip: delete the Dev Platform from core

The Dev Platform now lives in byte5ai/omadia-dev-platform and installs via the Hub (or a ZIP upload). This is core's side of the move.

One PR by necessity, not preference. A partial delete leaves tsc red — index.ts, config.ts, the routers and the SPA are one dependency knot. It is split into 7 commits by area so it stays reviewable and revertible.

243 files changed · +149 / −59,356 · 217 files deleted.


Zone table — ratchet before → after

node scripts/check-core-decoupling.mjs --report

Zone Before After Note
middleware/src 1576 19 6 are C12's publicPaths; rest are comments
middleware/test 1031 62 fixture strings + the legacy-key regression test → C13
middleware/packages 89 17 plugin-api CHANGELOG recording the removal → C13
middleware/scripts 8 CLEAN
middleware/sidecars 195 CLEAN
middleware/migrations 69 69 C1100220030 stay, deliberately
middleware/package.json 0 CLEAN
middleware/env-example 19 1 the plugin repo URL, which cannot be reworded
web-ui/app 229 19 nav test fixtures + two comments → C13
web-ui/messages 4 CLEAN
web-ui/config 0 CLEAN
ci-workflows 16 CLEAN
scripts 30 27 the ratchet's own pattern list — see Note on zero
compose 36 CLEAN
TOTAL 3302 214 baseline updated 3300 → 214

Nine of fourteen zones read CLEAN. The baseline is updated consistently in all three places the README requires: decoupling-baseline.json, the README "Baseline 214" line, and the acceptance.md guard row.

main was already +2 over its own baseline when this branch was cut (middleware/test 1031 vs 1030). Pre-existing; the deletion absorbs it.

Note on zero. scripts/check-core-decoupling.mjs counts itself — 27 of the remaining 214 are its own literal pattern list. The total can never reach the 0 the README defines as done until the script excludes its own path. That is a one-line fix and belongs to C13; flagging it because "ratchet reads 0" is the stated definition of done and it is currently unreachable by construction.


What was deleted

Area Files LOC
middleware/src/devplatform/ 62 18,022
middleware/test/devplatform/ 58 16,878
middleware/sidecars/ (dev-runner, dev-runner-daemon, dev-dind) 34 12,990
web-ui/app/admin/dev-platform/ 29 4,348
middleware/packages/dev-runner-shim/ 23 4,077
i18n keys (en + de + allowlist) 3 782
web-ui/app/_components/devjobs/ 6 771
middleware/src/{index,config}.ts 2 565
docker-compose.dev-platform.yaml 1 321
CI workflows 3 72

Plus scripts/dev-transcript.ts, web-ui/app/_lib/useDevJobEvents.ts, the /admin grid card, and all 43 DEV_* config keys.

i18n: 299 leaf keys per locale (adminDevPlatform.* 288, chat.devJob.* 9, admin.index.cards.devPlatform.* 2) = 598 across en+de, plus the 4 orphaned i18n-identical-allowlist.json entries the validator flagged. npm run i18n:check → OK, 3560 keys.


KEPT — and why

Every item below was traced to a live importer or an explicit spec instruction before being left in place.

Kept Reason
auth/publicPaths.ts — both exemptions C12. Deleting them now 401s every in-flight runner phone-home; runners carry no session cookie. Single revertible commit, on its own.
migrations 00220030 C11. Contiguous slot range in the core ledger; the handoff seeds by filename against a per-file schema witness and never deletes donor rows. Core still applies them, so no operator data moves in this PR.
services/githubAppJwt.ts implementation.md §5. It was moved into core to close the one core→devplatform reverse dependency. Sending it out recreates that leak across a repo boundary. The plugin uses a capability-first local RS256 fallback (SEAMS S2).
express, pg, zod §5. Plugin peerDependencies, resolved through the host node_modules symlink. Removing them breaks every installed plugin at runtime with no compile-time signal here. Comment added to middleware/package.json as §5 asked.
DEV_ENDPOINTS_ENABLED, DEV_ENDPOINTS_LOOPBACK_ONLY Core dev-graph (/api/dev/*). Shares the prefix, nothing else.
FLY_APP_NAME Describes the host, not the feature — CORE_DEV_PREFIXED_KEYS already said so in prose.
devFlag() The checklist called all three call sites dev-platform; that is stale. Two live call sites are PUBLIC_MCP_*.
uiRouteCatalog / requiresNavFrom Generic mechanisms. Dev Platform was their only caller, not their owner. They now have no in-core caller, which is the design intent.
C1–C8 platform capabilities The whole point of Phase A. Untouched.
The missing-Dockerfile CI guard Also computes the version stamp every remaining image consumes. Only its dev-runner-daemon justification comment is gone.

Traced importers from outside the delete set

Four, all resolved:

  1. middleware/scripts/dev-transcript.ts → deleted (in scope).
  2. middleware/src/config.ts → the DevPlatformConfig type import; namespace collapsed.
  3. middleware/src/index.ts → the assembly; deleted.
  4. middleware/test/adversarial/adversarialModel.tscomposeBrief — see below. This is the one that needed a judgement call.

⚠️ One capability reduction, recorded rather than absorbed

The adversarial injection eval's brief_delimiter Tier A probe ran the real composeBrief out of src/devplatform/. That is 5 of 12 deterministic scenarios, and the only deterministic coverage for the direct_injection and indirect_injection vectors.

It leaves with the code it measured. A probe against a module core no longer ships measures a library, not a deployed defense — and keeping a core copy would be dead code the eval then reports green against, which is worse than an honest gap. The plugin repo already owns briefComposer.ts and its unit test.

Removed together (the harness throws on a corpus row naming an unknown probe, so these cannot drift apart): the probe, direct-injection.jsonl, indirect-injection.jsonl, the negative-control test, and 5 baseline.json rows. Deterministic corpus 12 → 7.

middleware/test/adversarial/README.md now states the gap and both ways to close it — they are not alternatives:

  • (a) the plugin repo ports the probe next to the composeBrief it owns;
  • (b) core re-establishes a Tier A probe against whichever core surface frames untrusted text into a prompt.

exfiltration and tool_output deterministic coverage is unchanged, and sanitizeIssueBody (core's builder) keeps its probe.


Two things that fell out of the deletion

A latent ordering bug, fixed. The /api/v1/dev-runner/llm/ express.json carve-out sat before the conductor's inbound webhook router. That router uses a route-level express.raw(), which body-parser short-circuits once an earlier parser has marked the request _body — so its HMAC path was reading a parsed object, not a Buffer. Deleting the carve-out restores the order the surrounding comments already promised: pluginRawBodyMount → conductor raw router → global express.json.

Four core config keys had to be rescued (commit af131b3d) — tsc caught them, which is precisely why this is one PR. Three LONG_RUNNING_* keys (the generic W2-2 / #543 task seam) sat interleaved with the dev-platform keys inside the same schema block, and FLY_APP_NAME went with them despite the prose saying it must survive. All four are restored under an explicit heading stating why they are core.


H3 — resolved by omission

chat/page.tsx no longer hardcodes tool.name === 'dev_job_start'. The plugin's start tool falls through to the generic long-running-task card (isTaskStartToolName / TaskChatCard). That is the accepted-degradation branch of D2, not the declarative card schema — a plugin wanting a richer card goes through the C8 UI contract. The README's "decisions before code" entry for H3 is struck through.


Supply chain

dev-runner and dev-runner-daemon are no longer built, signed or published here; id-token: write is dropped from publish-images.yml and both callers (auto-release.yml, release.yml), since keyless cosign was its only user. The if: false npm-provenance job keeps its own grant.

⚠️ Per implementation.md §2.4 the cosign certificate identity binds to repo + workflow + ref. A deployed daemon pinning the old exact identity will refuse images from the new signer until the transition --certificate-identity-regexp lands — that is P4 in the plugin repo, and it must land before the first image publishes from there.


Verification

Gate Result
middleware build green
middleware typecheck (+ golden + adversarial tsconfigs) green
middleware npm test 6925 pass / 0 fail / 12 skipped (6937 total)
middleware typecheck:test ratchet 406 → 371, baseline lowered
middleware lint 0 problems
web-ui typecheck green
web-ui npm test (vitest) 717 pass / 0 fail (85 files)
web-ui lint 0 errors (43 pre-existing warnings)
web-ui i18n:check OK — 3560 keys, en + de
web-ui i18n:literals translate = 0
decoupling ratchet 3302 → 214
package-lock.json regenerated; npm ci verified clean

npm left packages/dev-runner-shim in the lockfile as an extraneous: true stanza rather than dropping it; removed explicitly and re-verified with npm ci.


Follow-ups this PR does not do

  1. C11 — migration ledger handoff (own PR, own rollback, dryRun first).
  2. C12 — delete the two publicPaths exemptions. Nothing else in that PR.
  3. C13 — residue: comments, nav test fixture strings, the plugin-api CHANGELOG, and the ratchet's self-exclusion without which the count cannot reach 0.
  4. Plugin repo — port the brief_delimiter probe; land the cosign identity regexp before publishing.
  5. Core capabilities the plugin is waiting on (SEAMS.md): conductorRoles@1 (role-principal gates are unapprovable without it — the largest functional gap), githubAppJwt@1, usageTelemetry@1.

Do not merge before C11/C12 sequencing is agreed.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Cross-family review (Forge)

Independent review by an OpenAI-family model (GPT-5.4, reasoning=high), run against the
code rather than the PR description. Focus for a deletion of this size is collateral:
what left that should have stayed, and what stayed that no longer resolves.

Verdict: MERGE — after three fixes, now in 2c6cc20c

Gate results (all run on the branch, not inferred)

Gate Result
middleware build / typecheck / lint pass
middleware typecheck:test 371 known errors, no regressions (baseline 371)
middleware full suite 6940 tests, 0 fail, 12 skipped (was 6937 — +3 from this review)
web-ui lint / typecheck / build pass
web-ui vitest 85 files, 717 tests, 0 fail
i18n-validate / i18n-literal-scan exit 0 · 3560 keys, locales en, de · 0 orphaned devPlatform/devJob keys
check-core-decoupling.mjs Dev Platform references held at 214
eval:adversarial 7/7 defenses held (100%), 0 breached
actionlint reduced to exactly main's four pre-existing findings
docker compose config every remaining file behaves identically to main (the four "undefined network omadia" / missing-.env complaints are pre-existing and unrelated)

Collateral check — clean

Every deletion outside a dev-platform-named path traces to the checklist:
scripts/dev-transcript.ts (Zone 11), sidecars/dev-dind/**, the devjobs/ chat surface
and useDevJobEvents.ts (Zone 15), and the two adversarial corpus files (documented below).
No survivor imports them — statically or dynamically. Checked beyond what tsc can see:
route tables, plugin manifests, CI YAML, Dockerfiles, all eight compose files, render.yaml,
fly/, package.json workspaces/scripts, tsconfig references. workspaces is
["packages/*"] and packages/dev-runner-shim is gone, so nothing dangles.

The kept-list verifies: migrations 00220030 all present (C11), both publicPaths
exemptions present (C12), services/githubAppJwt.ts present, express/pg/zod still in
dependencies. Config keys: 43 removed, all DEV_*, none interleavedDEV_ENDPOINTS_ENABLED
and DEV_ENDPOINTS_LOOPBACK_ONLY correctly survive despite the prefix, and the four
LONG_RUNNING_* / FLY_APP_NAME keys the author restored are the complete set. Ratchet is
consistent in all three places (JSON 214, README Baseline **214**, acceptance Baseline **214**).

The one thing that needed real work

The express.json carve-out removal changes behaviour, correctly, and nothing locked it.

On main the mount order was pluginRawBodyMount → carve-out-wrapped express.json
conductorWebhooksInbound → global express.json. The conductor router therefore sat
behind a JSON parser. Since body-parser stamps the request _body, the router's own
route-level express.raw() short-circuits and the handler falls to
Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0) — it HMACs an empty buffer.
Measured with a standalone probe on both orders:

MAIN order   (express.json BEFORE conductor router): expectedRawLen 24, observed rawLen 0
BRANCH order (conductor router BEFORE express.json): expectedRawLen 24, observed rawLen 24

So on main every correctly-signed inbound Conductor webhook answers 401. This PR fixes
that as a side effect
— the answer to the review question is yes, the router now gets the
raw body.

But no test could see it. conductorWebhookInbound.test.ts builds const app = express(); app.use(createConductorWebhooksInboundRouter(...)) — the router alone on a bare app, which
passes under both orders. That is precisely how the broken ordering shipped green, and
reintroducing any parser above that mount would break production silently again.

Added, all mutation-checked:

  • two behavioural tests sending the same correctly-signed request under both mount
    orders (202 started vs 401, nothing claimed or emitted);
  • a source-order assertion in 778RouteMounts.wiring.test.ts pinning the real index.ts
    order, which fails loudly if either anchor is renamed rather than passing vacuously.

Mutation evidence: forcing the parser first fails 8 tests; removing it fails exactly the one
new test that depends on it; reordering index.ts fails the wiring test on the ordering
message; renaming the anchor fails it on the anchor message.

Also fixed

  • publish-images.yml referenced matrix.sha_on_release, undefined since the
    dev-runner and dev-runner-daemon matrix entries were deleted. actionlint errors on
    this branch and is clean on main. Dropping the clause also restores what the adjacent
    comment already claims (sha tags on the edge build only). The steps.guard block was left
    alone — it still computes the stamp that build-args consumes, so it is not dead.
  • middleware/package.json — the new _dependencyNote was indented four spaces and
    pushed "dependencies" to column 0. Valid JSON, but off-style and any rewrite would
    reflow it. Text and ordering unchanged.
  • Spec README file count — claimed 213 files deleted, measured 217. Corrected. The
    adjacent 43 config keys claim was checked and is correct; left alone.

The ratchet caught my own first patch (214 → 215, one comment naming a dev-runner path).
Reworded rather than baseline-raised, per the spec's own rule.

Adversarial corpus reduction — honest, and the follow-up is real

12 → 7 is a genuine coverage loss, not an accounting trick: brief_delimiter ran the real
composeBrief out of src/devplatform/, so it went with the module. direct_injection and
indirect_injection now have no Tier A probe at all. The README says exactly that, in
those words, and names two non-alternative remedies (plugin repo ports the probe; core
re-establishes a Tier A delimiter probe against a surface it still ships). The gate still
functions on what remains — 7/7 held, baseline rows match the surviving scenario ids
one-for-one, and the behavioral scenario is correctly treated as expected-absent without a
key. Accepting the reduction is reasonable; the follow-up should be tracked as an issue,
not left to the README.

Nits, not blockers

  • publish-images.yml's "skip a matrix image whose Dockerfile is not in the tree yet" guard
    now protects three Dockerfiles that all exist. Harmless, and it does double duty computing
    stamp, so it was left in place.
  • actionlint is not wired into CI, so the sha_on_release error would not have blocked the
    merge. Worth wiring up separately — it caught a real regression here for free.

Weegy added 8 commits August 20, 2026 20:21
…(C10)

Epic #470 C10 — the flip. The Dev Platform now lives in
byte5ai/omadia-dev-platform and installs via Hub/ZIP.

Removed:
- middleware/src/devplatform/ (62 files) — stores, worker, backends, LLM
  proxy, pipeline/gates, GitHub App, triggers, routers, wireDevPlatform
- middleware/test/devplatform/ (58 files)
- middleware/scripts/dev-transcript.ts

Adversarial eval (#498): the `brief_delimiter` Tier A probe ran the real
`composeBrief` out of src/devplatform/. A probe against a module core no
longer ships measures a library, not a deployed defense — so the probe, its
five corpus scenarios (direct-injection.jsonl, indirect-injection.jsonl) and
their baseline rows leave with it. This is a real coverage reduction and is
recorded as such in test/adversarial/README.md, with both ways to close it.
Deterministic corpus: 12 scenarios -> 7.
…y (C10)

Removed:
- middleware/packages/dev-runner-shim/ (23 files) — the in-container agent
  shim. Never built by `npm run build`, yet index.ts resolved its dist/ at
  runtime; the extraction removes that inconsistency with it.
- middleware/sidecars/dev-runner/ (2), dev-runner-daemon/ (30, dockerode),
  dev-dind/ (2)
- docker-compose.dev-platform.yaml

All four now live in byte5ai/omadia-dev-platform, which owns their GHCR
publishing, SBOM and signing pipeline.
index.ts (-238 lines):
- 15 devplatform imports + the now-dead DeviceFlowStore / ConductorRoleStore
- the GitHub-webhook block mounted before express.json
- the whole assembly block: assembleDevPlatform / mountDevPlatform, the
  GitHub-App routers, the three chat orchestrator tools, worker start +
  SIGTERM/SIGINT hooks, the uiRouteCatalog nav registration, the retention
  cron and the no-graphPool warning

Side effect worth calling out: the `/api/v1/dev-runner/llm/` carve-out sat
BEFORE the conductor inbound webhook router, so an express.json ran ahead of
that router's route-level express.raw() and short-circuited it (body-parser
marks the request `_body`). Deleting the carve-out restores the order the
surrounding comments already document: pluginRawBodyMount -> conductor raw
router -> global express.json.

config.ts (-305 lines): all 43 DEV_*/FLY_* dev-platform schema keys, the
`devPlatform` namespace, `buildDevPlatformConfig`, `csvList`, and the
CORE_DEV_PREFIXED_KEYS / DevPlatformEnvKey / isDevPlatformEnvKey machinery
that existed only to hold those keys out of the top level — `Config` is now
`ParsedConfig`. `devPlatformBootRefusals` goes too: the plugin owns both
interlocks (pluginConfig.ts, verified), so they became activation refusals
rather than vanishing. KEPT: devFlag() (two PUBLIC_MCP_* call sites),
DEV_ENDPOINTS_ENABLED, DEV_ENDPOINTS_LOOPBACK_ONLY, FLY_APP_NAME.

.env.example: the 15-key block replaced with a pointer to the plugin repo.

middleware/package.json: the §5 note — express/pg/zod stay as plugin
peerDependencies resolved through the host node_modules symlink.
… (C10)

- web-ui/app/admin/dev-platform/ (29 files) — the operator SPA. It ships from
  the plugin now as a compiled Vite bundle served through the C8 static host
  at /p/<pluginId>/ui/.
- web-ui/app/_components/devjobs/ (6 files) + app/_lib/useDevJobEvents.ts
- app/admin/page.tsx — the grid card. The generic `requiresNavFrom` mechanism
  stays; this was its only user.
- app/_lib/i18n-structural.test.ts — the GateInbox.tsx path entry.

H3 resolved by omission, as the plugin's ACCEPTANCE-RUN records. chat/page.tsx
no longer special-cases `tool.name === 'dev_job_start'`; a dev-job start from
the installed plugin now falls through to the generic long-running-task card
(`isTaskStartToolName` / TaskChatCard), which is the accepted degradation.

i18n: 299 leaf keys per locale removed (adminDevPlatform.* 288, chat.devJob.* 9,
admin.index.cards.devPlatform.* 2) = 598 across en+de, plus the four orphaned
`i18n-identical-allowlist.json` entries the validator flagged.
`npm run i18n:check` OK — 3560 keys; `i18n:literals` translate=0.
… (C10)

publish-images.yml:
- the `dev-runner` and `dev-runner-daemon` matrix entries
- the runner-image supply chain: cosign install, syft SPDX-JSON SBOM,
  keyless sign + attest (all guarded on `matrix.name == 'dev-runner'`)
- `id-token: write`, which existed solely for that keyless signing

auto-release.yml / release.yml: the matching caller-side `id-token: write`
grants, which a reusable workflow cannot self-grant and nothing else needs.
The `if: false` npm-provenance job in release.yml keeps its own grant.

byte5ai/omadia-dev-platform owns runner GHCR publishing, SBOM and signing
now. Per implementation.md §2.4 the cosign certificate identity binds to
repo + workflow + ref, so the new signer needs the transition
`--certificate-identity-regexp` landed before it publishes — that is P4's
job in the plugin repo, not core's.

The missing-Dockerfile guard stays: it also computes the version `stamp`
every remaining image consumes; only its dev-runner-daemon justification
comment is gone.

scripts/wave-{implement,verify}.workflow.mjs: the generic wave prompts baked
in "terminal transitions go through finalizeDevJob" — a dev-platform domain
rule in a domain-agnostic prompt, naming a function core no longer ships.
Genericised to "the subsystem's single finalizer"; the rule survives, the
implementor name does not. Same for the `docs/dev-platform/` example path.
`tsc` caught these, which is exactly why C10 has to be one PR.

Three LONG_RUNNING_* keys sat INTERLEAVED with the dev-platform keys in
config.ts, between DEV_PLATFORM_RUNNER_BASE_URL and DEV_PLATFORM_CLI_BIN, and
went out with the block:

  LONG_RUNNING_SUBAGENT_TOOLS   — W2-2 / issue #543, the generic
  LONG_RUNNING_TASK_STALE_MS      `<tool>_start/_status/_list` seam that any
  LONG_RUNNING_TASK_RETAIN_MS     slow tool opts into. Not dev-platform.

FLY_APP_NAME went too, despite CORE_DEV_PREFIXED_KEYS documenting in prose that
it "describes the host, not the feature, so it must survive the extraction".

All four are restored under an explicit heading that says why they are core, so
the next person reading config.ts does not have to re-derive it. Also drops the
`node:os` import, now unused (it backed the workspace-dir default), and rewords
the three comments this PR itself introduced that named the extracted subsystem
— C10 must not add coupling references while removing them.

Verified: middleware build + typecheck + adversarial/golden tsconfigs green;
6925 pass / 0 fail; typecheck:test ratchet 406 -> 371 (baseline lowered);
lint 0 problems. web-ui typecheck green, 717 pass / 0 fail, i18n 3560 keys OK.
…(C10)

Ratchet: **3,300 → 214**, updated consistently in all three places the README
says must agree — `decoupling-baseline.json`, the README "Baseline **214**"
line, and the `acceptance.md` guard row.

Nine of fourteen zones read CLEAN. Every survivor is scheduled, not stranded:

  migrations       69  C11 — 0022-0030 stay; core still applies them
  middleware/test  62  C13 — fixture strings + the legacy-key regression test
  scripts          27  C13 — the ratchet's own pattern list (needs a
                            self-exclusion before the total can reach 0)
  middleware/src   19  C12 — publicPaths (6); rest are comments
  web-ui/app       19  C13 — nav test fixtures, two comments
  packages         17  C13 — plugin-api CHANGELOG recording the removal
  env-example       1  the plugin repo URL, which cannot be reworded

Also: `test-typecheck-baseline.json` 406 → 371, and `package-lock.json`
regenerated (npm left `packages/dev-runner-shim` as an `extraneous: true`
stanza rather than dropping it; `npm ci` verified clean afterwards).

Docs:
- README gains a "C10 — the flip" status section: what left, what was kept and
  why, H3 resolved by omission, the adversarial coverage reduction, and the
  express.json ordering bug the deletion fixed. The H3 "decision before code"
  is struck through — it is answered now.
- plan.md §4.2 gains a C10 note (and flags that its `devJobStepEffect.ts` line
  went stale in the other direction: C5 deleted that port as dead code). P4 row
  marked shipped.
- docs/CHANGELOG.md: "Dev Platform moved to byte5ai/omadia-dev-platform
  (install via Hub/ZIP)" — written for operators, so it leads with how to get
  it back, states that no data is touched, and that in-flight jobs survive the
  upgrade because C12 has not run yet.
…sha_on_release matrix ref (C10 review)

Cross-family review of PR #804. Three defects, no scope change to the deletion itself.

1. `.github/workflows/publish-images.yml` referenced `matrix.sha_on_release`, which
   nothing defines any more: the only two matrix entries that set it (dev-runner,
   dev-runner-daemon) left with the Dev Platform. actionlint errors on this branch and
   is clean on main. Dropping the clause also restores the behaviour the adjacent
   comment already claims — sha tags on the edge build only.

2. `middleware/package.json` — the `_dependencyNote` added by this PR was indented four
   spaces and pushed `"dependencies"` to column 0. Valid JSON, but the rest of the file
   is 2-space and any tool that rewrites it would reflow the diff. Text unchanged.

3. Removing the LLM-proxy carve-out moved `createConductorWebhooksInboundRouter` from
   *after* the global `express.json` to *before* it. That is correct — and it silently
   fixes a live bug: on main the router HMAC'd `Buffer.alloc(0)`, because body-parser
   stamps `_body` and the route-level `express.raw()` then short-circuits, so every
   correctly-signed inbound webhook answered 401. Measured on a standalone probe:
   json-first yields rawLen 0, router-first yields the full 24 bytes.

   Nothing locked that order. `conductorWebhookInbound.test.ts` mounts the router alone
   on a bare app, which passes under BOTH orders — which is exactly how the broken
   ordering shipped green. Added two behavioural tests (same signed request, both mount
   orders: 202 vs 401) plus a source-order assertion in `778RouteMounts.wiring.test.ts`
   that fails loudly if either anchor goes missing rather than passing vacuously.

   All four assertions mutation-checked: reordering the mount fails the wiring test,
   renaming the anchor fails it for the anchor reason, and forcing/removing the parser
   in the harness kills exactly the intended behavioural tests.

Also corrects the C10 file count in the spec README (213 -> 217 measured deletions).
The adjacent "43 config keys" claim was verified correct and left alone. Ratchet stays
at 214 — the one comment that would have raised it was reworded, not excused.
@Weegy
Weegy merged commit c93f867 into main Aug 21, 2026
9 checks passed
Weegy added a commit that referenced this pull request Aug 21, 2026
…racted subsystem (epic #470 C12) (#807)

* chore(#470): delete the dev-platform backend tree, routers and tests (C10)

Epic #470 C10 — the flip. The Dev Platform now lives in
byte5ai/omadia-dev-platform and installs via Hub/ZIP.

Removed:
- middleware/src/devplatform/ (62 files) — stores, worker, backends, LLM
  proxy, pipeline/gates, GitHub App, triggers, routers, wireDevPlatform
- middleware/test/devplatform/ (58 files)
- middleware/scripts/dev-transcript.ts

Adversarial eval (#498): the `brief_delimiter` Tier A probe ran the real
`composeBrief` out of src/devplatform/. A probe against a module core no
longer ships measures a library, not a deployed defense — so the probe, its
five corpus scenarios (direct-injection.jsonl, indirect-injection.jsonl) and
their baseline rows leave with it. This is a real coverage reduction and is
recorded as such in test/adversarial/README.md, with both ways to close it.
Deterministic corpus: 12 scenarios -> 7.

* chore(#470): delete the dev-runner shim, sidecars and compose topology (C10)

Removed:
- middleware/packages/dev-runner-shim/ (23 files) — the in-container agent
  shim. Never built by `npm run build`, yet index.ts resolved its dist/ at
  runtime; the extraction removes that inconsistency with it.
- middleware/sidecars/dev-runner/ (2), dev-runner-daemon/ (30, dockerode),
  dev-dind/ (2)
- docker-compose.dev-platform.yaml

All four now live in byte5ai/omadia-dev-platform, which owns their GHCR
publishing, SBOM and signing pipeline.

* chore(#470): unwire the dev platform from index.ts, config and env (C10)

index.ts (-238 lines):
- 15 devplatform imports + the now-dead DeviceFlowStore / ConductorRoleStore
- the GitHub-webhook block mounted before express.json
- the whole assembly block: assembleDevPlatform / mountDevPlatform, the
  GitHub-App routers, the three chat orchestrator tools, worker start +
  SIGTERM/SIGINT hooks, the uiRouteCatalog nav registration, the retention
  cron and the no-graphPool warning

Side effect worth calling out: the `/api/v1/dev-runner/llm/` carve-out sat
BEFORE the conductor inbound webhook router, so an express.json ran ahead of
that router's route-level express.raw() and short-circuited it (body-parser
marks the request `_body`). Deleting the carve-out restores the order the
surrounding comments already document: pluginRawBodyMount -> conductor raw
router -> global express.json.

config.ts (-305 lines): all 43 DEV_*/FLY_* dev-platform schema keys, the
`devPlatform` namespace, `buildDevPlatformConfig`, `csvList`, and the
CORE_DEV_PREFIXED_KEYS / DevPlatformEnvKey / isDevPlatformEnvKey machinery
that existed only to hold those keys out of the top level — `Config` is now
`ParsedConfig`. `devPlatformBootRefusals` goes too: the plugin owns both
interlocks (pluginConfig.ts, verified), so they became activation refusals
rather than vanishing. KEPT: devFlag() (two PUBLIC_MCP_* call sites),
DEV_ENDPOINTS_ENABLED, DEV_ENDPOINTS_LOOPBACK_ONLY, FLY_APP_NAME.

.env.example: the 15-key block replaced with a pointer to the plugin repo.

middleware/package.json: the §5 note — express/pg/zod stay as plugin
peerDependencies resolved through the host node_modules symlink.

* chore(#470): delete the dev-platform web-ui surface and its i18n keys (C10)

- web-ui/app/admin/dev-platform/ (29 files) — the operator SPA. It ships from
  the plugin now as a compiled Vite bundle served through the C8 static host
  at /p/<pluginId>/ui/.
- web-ui/app/_components/devjobs/ (6 files) + app/_lib/useDevJobEvents.ts
- app/admin/page.tsx — the grid card. The generic `requiresNavFrom` mechanism
  stays; this was its only user.
- app/_lib/i18n-structural.test.ts — the GateInbox.tsx path entry.

H3 resolved by omission, as the plugin's ACCEPTANCE-RUN records. chat/page.tsx
no longer special-cases `tool.name === 'dev_job_start'`; a dev-job start from
the installed plugin now falls through to the generic long-running-task card
(`isTaskStartToolName` / TaskChatCard), which is the accepted degradation.

i18n: 299 leaf keys per locale removed (adminDevPlatform.* 288, chat.devJob.* 9,
admin.index.cards.devPlatform.* 2) = 598 across en+de, plus the four orphaned
`i18n-identical-allowlist.json` entries the validator flagged.
`npm run i18n:check` OK — 3560 keys; `i18n:literals` translate=0.

* chore(#470): drop the dev-runner CI matrix and supply chain from core (C10)

publish-images.yml:
- the `dev-runner` and `dev-runner-daemon` matrix entries
- the runner-image supply chain: cosign install, syft SPDX-JSON SBOM,
  keyless sign + attest (all guarded on `matrix.name == 'dev-runner'`)
- `id-token: write`, which existed solely for that keyless signing

auto-release.yml / release.yml: the matching caller-side `id-token: write`
grants, which a reusable workflow cannot self-grant and nothing else needs.
The `if: false` npm-provenance job in release.yml keeps its own grant.

byte5ai/omadia-dev-platform owns runner GHCR publishing, SBOM and signing
now. Per implementation.md §2.4 the cosign certificate identity binds to
repo + workflow + ref, so the new signer needs the transition
`--certificate-identity-regexp` landed before it publishes — that is P4's
job in the plugin repo, not core's.

The missing-Dockerfile guard stays: it also computes the version `stamp`
every remaining image consumes; only its dev-runner-daemon justification
comment is gone.

scripts/wave-{implement,verify}.workflow.mjs: the generic wave prompts baked
in "terminal transitions go through finalizeDevJob" — a dev-platform domain
rule in a domain-agnostic prompt, naming a function core no longer ships.
Genericised to "the subsystem's single finalizer"; the rule survives, the
implementor name does not. Same for the `docs/dev-platform/` example path.

* fix(#470): restore four core config keys the C10 cut swallowed (C10)

`tsc` caught these, which is exactly why C10 has to be one PR.

Three LONG_RUNNING_* keys sat INTERLEAVED with the dev-platform keys in
config.ts, between DEV_PLATFORM_RUNNER_BASE_URL and DEV_PLATFORM_CLI_BIN, and
went out with the block:

  LONG_RUNNING_SUBAGENT_TOOLS   — W2-2 / issue #543, the generic
  LONG_RUNNING_TASK_STALE_MS      `<tool>_start/_status/_list` seam that any
  LONG_RUNNING_TASK_RETAIN_MS     slow tool opts into. Not dev-platform.

FLY_APP_NAME went too, despite CORE_DEV_PREFIXED_KEYS documenting in prose that
it "describes the host, not the feature, so it must survive the extraction".

All four are restored under an explicit heading that says why they are core, so
the next person reading config.ts does not have to re-derive it. Also drops the
`node:os` import, now unused (it backed the workspace-dir default), and rewords
the three comments this PR itself introduced that named the extracted subsystem
— C10 must not add coupling references while removing them.

Verified: middleware build + typecheck + adversarial/golden tsconfigs green;
6925 pass / 0 fail; typecheck:test ratchet 406 -> 371 (baseline lowered);
lint 0 problems. web-ui typecheck green, 717 pass / 0 fail, i18n 3560 keys OK.

* docs(#470): record the C10 flip — ratchet baseline, specs, changelog (C10)

Ratchet: **3,300 → 214**, updated consistently in all three places the README
says must agree — `decoupling-baseline.json`, the README "Baseline **214**"
line, and the `acceptance.md` guard row.

Nine of fourteen zones read CLEAN. Every survivor is scheduled, not stranded:

  migrations       69  C11 — 0022-0030 stay; core still applies them
  middleware/test  62  C13 — fixture strings + the legacy-key regression test
  scripts          27  C13 — the ratchet's own pattern list (needs a
                            self-exclusion before the total can reach 0)
  middleware/src   19  C12 — publicPaths (6); rest are comments
  web-ui/app       19  C13 — nav test fixtures, two comments
  packages         17  C13 — plugin-api CHANGELOG recording the removal
  env-example       1  the plugin repo URL, which cannot be reworded

Also: `test-typecheck-baseline.json` 406 → 371, and `package-lock.json`
regenerated (npm left `packages/dev-runner-shim` as an `extraneous: true`
stanza rather than dropping it; `npm ci` verified clean afterwards).

Docs:
- README gains a "C10 — the flip" status section: what left, what was kept and
  why, H3 resolved by omission, the adversarial coverage reduction, and the
  express.json ordering bug the deletion fixed. The H3 "decision before code"
  is struck through — it is answered now.
- plan.md §4.2 gains a C10 note (and flags that its `devJobStepEffect.ts` line
  went stale in the other direction: C5 deleted that port as dead code). P4 row
  marked shipped.
- docs/CHANGELOG.md: "Dev Platform moved to byte5ai/omadia-dev-platform
  (install via Hub/ZIP)" — written for operators, so it leads with how to get
  it back, states that no data is touched, and that in-flight jobs survive the
  upgrade because C12 has not run yet.

* fix(#470): lock the conductor raw-body mount order and drop the dead sha_on_release matrix ref (C10 review)

Cross-family review of PR #804. Three defects, no scope change to the deletion itself.

1. `.github/workflows/publish-images.yml` referenced `matrix.sha_on_release`, which
   nothing defines any more: the only two matrix entries that set it (dev-runner,
   dev-runner-daemon) left with the Dev Platform. actionlint errors on this branch and
   is clean on main. Dropping the clause also restores the behaviour the adjacent
   comment already claims — sha tags on the edge build only.

2. `middleware/package.json` — the `_dependencyNote` added by this PR was indented four
   spaces and pushed `"dependencies"` to column 0. Valid JSON, but the rest of the file
   is 2-space and any tool that rewrites it would reflow the diff. Text unchanged.

3. Removing the LLM-proxy carve-out moved `createConductorWebhooksInboundRouter` from
   *after* the global `express.json` to *before* it. That is correct — and it silently
   fixes a live bug: on main the router HMAC'd `Buffer.alloc(0)`, because body-parser
   stamps `_body` and the route-level `express.raw()` then short-circuits, so every
   correctly-signed inbound webhook answered 401. Measured on a standalone probe:
   json-first yields rawLen 0, router-first yields the full 24 bytes.

   Nothing locked that order. `conductorWebhookInbound.test.ts` mounts the router alone
   on a bare app, which passes under BOTH orders — which is exactly how the broken
   ordering shipped green. Added two behavioural tests (same signed request, both mount
   orders: 202 vs 401) plus a source-order assertion in `778RouteMounts.wiring.test.ts`
   that fails loudly if either anchor goes missing rather than passing vacuously.

   All four assertions mutation-checked: reordering the mount fails the wiring test,
   renaming the anchor fails it for the anchor reason, and forcing/removing the parser
   in the harness kills exactly the intended behavioural tests.

Also corrects the C10 file count in the spec README (213 -> 217 measured deletions).
The adjacent "43 config keys" claim was verified correct and left alone. Ratchet stays
at 214 — the one comment that would have raised it was reworded, not excused.

* feat(#470): delete the two dev-platform publicPaths exemptions (C12)

Core stops exempting two URLs on a plugin's behalf. Since C4 the plugin
declares them itself in `permissions.public_paths`, and the operator consents
per prefix (`platform/publicPathGrants.ts`).

The deletion is the load-bearing half of that handover, not tidy-up.
`validateDeclaredPublicPath` REJECTS a declaration that collides with a live
static exemption — "is already a static core public path … remove the core
exemption first, or drop the declaration" — because two mechanisms claiming
one URL is ambiguous and only one of them terminates. So while these entries
existed the plugin's own grant could never validate. With them gone it does.

Guarded by a new suite that pins `STATIC_PUBLIC_PATHS` as a CLOSED set: every
entry must be accounted for by a named, justified core-owned exemption.
Restore either deleted entry and the suite goes red on the unaccounted entry,
which is what makes this commit cleanly revertible.

The suite deliberately does not name the two deleted paths. Hardcoding a
plugin's wire paths into core is a reference to that plugin whichever
direction it asserts in — the coupling this epic removes and the thing
`scripts/check-core-decoupling.mjs` counts — and it goes stale-but-green the
day the plugin renames them. The closed set is also the stronger guard: it
catches any unauthenticated surface being added, not just these two. The 401
follows as a corollary, since `requireAuth` runs on the `/api` mount before
routing, and is asserted end-to-end against the same chain `src/index.ts`
assembles.

Ratchet: 214 → 209. Nothing raised.

* fix(#470): fail loud in CI when the closed-set 401 half cannot bind loopback (C12 review)

The suite self-skips its five unauthenticated 401 assertions when a
sandbox refuses a loopback listener. Locally that is right — the closed-set
assertion still runs. In CI it is not: a runner that denied bind(127.0.0.1:0)
would delete the entire 401 half of the guard while the job stayed green,
which is the same permanently-green failure family the decoupling job's own
timeout comment warns about.

Keep the local skip, but throw with a named reason when CI is set.

* chore(470): lower decoupling baseline 211 → 206 after C12
Weegy added a commit that referenced this pull request Aug 21, 2026
…d is pinned there (epic #470 C13) (#808)

* chore(#470): delete the dev-platform backend tree, routers and tests (C10)

Epic #470 C10 — the flip. The Dev Platform now lives in
byte5ai/omadia-dev-platform and installs via Hub/ZIP.

Removed:
- middleware/src/devplatform/ (62 files) — stores, worker, backends, LLM
  proxy, pipeline/gates, GitHub App, triggers, routers, wireDevPlatform
- middleware/test/devplatform/ (58 files)
- middleware/scripts/dev-transcript.ts

Adversarial eval (#498): the `brief_delimiter` Tier A probe ran the real
`composeBrief` out of src/devplatform/. A probe against a module core no
longer ships measures a library, not a deployed defense — so the probe, its
five corpus scenarios (direct-injection.jsonl, indirect-injection.jsonl) and
their baseline rows leave with it. This is a real coverage reduction and is
recorded as such in test/adversarial/README.md, with both ways to close it.
Deterministic corpus: 12 scenarios -> 7.

* chore(#470): delete the dev-runner shim, sidecars and compose topology (C10)

Removed:
- middleware/packages/dev-runner-shim/ (23 files) — the in-container agent
  shim. Never built by `npm run build`, yet index.ts resolved its dist/ at
  runtime; the extraction removes that inconsistency with it.
- middleware/sidecars/dev-runner/ (2), dev-runner-daemon/ (30, dockerode),
  dev-dind/ (2)
- docker-compose.dev-platform.yaml

All four now live in byte5ai/omadia-dev-platform, which owns their GHCR
publishing, SBOM and signing pipeline.

* chore(#470): unwire the dev platform from index.ts, config and env (C10)

index.ts (-238 lines):
- 15 devplatform imports + the now-dead DeviceFlowStore / ConductorRoleStore
- the GitHub-webhook block mounted before express.json
- the whole assembly block: assembleDevPlatform / mountDevPlatform, the
  GitHub-App routers, the three chat orchestrator tools, worker start +
  SIGTERM/SIGINT hooks, the uiRouteCatalog nav registration, the retention
  cron and the no-graphPool warning

Side effect worth calling out: the `/api/v1/dev-runner/llm/` carve-out sat
BEFORE the conductor inbound webhook router, so an express.json ran ahead of
that router's route-level express.raw() and short-circuited it (body-parser
marks the request `_body`). Deleting the carve-out restores the order the
surrounding comments already document: pluginRawBodyMount -> conductor raw
router -> global express.json.

config.ts (-305 lines): all 43 DEV_*/FLY_* dev-platform schema keys, the
`devPlatform` namespace, `buildDevPlatformConfig`, `csvList`, and the
CORE_DEV_PREFIXED_KEYS / DevPlatformEnvKey / isDevPlatformEnvKey machinery
that existed only to hold those keys out of the top level — `Config` is now
`ParsedConfig`. `devPlatformBootRefusals` goes too: the plugin owns both
interlocks (pluginConfig.ts, verified), so they became activation refusals
rather than vanishing. KEPT: devFlag() (two PUBLIC_MCP_* call sites),
DEV_ENDPOINTS_ENABLED, DEV_ENDPOINTS_LOOPBACK_ONLY, FLY_APP_NAME.

.env.example: the 15-key block replaced with a pointer to the plugin repo.

middleware/package.json: the §5 note — express/pg/zod stay as plugin
peerDependencies resolved through the host node_modules symlink.

* chore(#470): delete the dev-platform web-ui surface and its i18n keys (C10)

- web-ui/app/admin/dev-platform/ (29 files) — the operator SPA. It ships from
  the plugin now as a compiled Vite bundle served through the C8 static host
  at /p/<pluginId>/ui/.
- web-ui/app/_components/devjobs/ (6 files) + app/_lib/useDevJobEvents.ts
- app/admin/page.tsx — the grid card. The generic `requiresNavFrom` mechanism
  stays; this was its only user.
- app/_lib/i18n-structural.test.ts — the GateInbox.tsx path entry.

H3 resolved by omission, as the plugin's ACCEPTANCE-RUN records. chat/page.tsx
no longer special-cases `tool.name === 'dev_job_start'`; a dev-job start from
the installed plugin now falls through to the generic long-running-task card
(`isTaskStartToolName` / TaskChatCard), which is the accepted degradation.

i18n: 299 leaf keys per locale removed (adminDevPlatform.* 288, chat.devJob.* 9,
admin.index.cards.devPlatform.* 2) = 598 across en+de, plus the four orphaned
`i18n-identical-allowlist.json` entries the validator flagged.
`npm run i18n:check` OK — 3560 keys; `i18n:literals` translate=0.

* chore(#470): drop the dev-runner CI matrix and supply chain from core (C10)

publish-images.yml:
- the `dev-runner` and `dev-runner-daemon` matrix entries
- the runner-image supply chain: cosign install, syft SPDX-JSON SBOM,
  keyless sign + attest (all guarded on `matrix.name == 'dev-runner'`)
- `id-token: write`, which existed solely for that keyless signing

auto-release.yml / release.yml: the matching caller-side `id-token: write`
grants, which a reusable workflow cannot self-grant and nothing else needs.
The `if: false` npm-provenance job in release.yml keeps its own grant.

byte5ai/omadia-dev-platform owns runner GHCR publishing, SBOM and signing
now. Per implementation.md §2.4 the cosign certificate identity binds to
repo + workflow + ref, so the new signer needs the transition
`--certificate-identity-regexp` landed before it publishes — that is P4's
job in the plugin repo, not core's.

The missing-Dockerfile guard stays: it also computes the version `stamp`
every remaining image consumes; only its dev-runner-daemon justification
comment is gone.

scripts/wave-{implement,verify}.workflow.mjs: the generic wave prompts baked
in "terminal transitions go through finalizeDevJob" — a dev-platform domain
rule in a domain-agnostic prompt, naming a function core no longer ships.
Genericised to "the subsystem's single finalizer"; the rule survives, the
implementor name does not. Same for the `docs/dev-platform/` example path.

* fix(#470): restore four core config keys the C10 cut swallowed (C10)

`tsc` caught these, which is exactly why C10 has to be one PR.

Three LONG_RUNNING_* keys sat INTERLEAVED with the dev-platform keys in
config.ts, between DEV_PLATFORM_RUNNER_BASE_URL and DEV_PLATFORM_CLI_BIN, and
went out with the block:

  LONG_RUNNING_SUBAGENT_TOOLS   — W2-2 / issue #543, the generic
  LONG_RUNNING_TASK_STALE_MS      `<tool>_start/_status/_list` seam that any
  LONG_RUNNING_TASK_RETAIN_MS     slow tool opts into. Not dev-platform.

FLY_APP_NAME went too, despite CORE_DEV_PREFIXED_KEYS documenting in prose that
it "describes the host, not the feature, so it must survive the extraction".

All four are restored under an explicit heading that says why they are core, so
the next person reading config.ts does not have to re-derive it. Also drops the
`node:os` import, now unused (it backed the workspace-dir default), and rewords
the three comments this PR itself introduced that named the extracted subsystem
— C10 must not add coupling references while removing them.

Verified: middleware build + typecheck + adversarial/golden tsconfigs green;
6925 pass / 0 fail; typecheck:test ratchet 406 -> 371 (baseline lowered);
lint 0 problems. web-ui typecheck green, 717 pass / 0 fail, i18n 3560 keys OK.

* docs(#470): record the C10 flip — ratchet baseline, specs, changelog (C10)

Ratchet: **3,300 → 214**, updated consistently in all three places the README
says must agree — `decoupling-baseline.json`, the README "Baseline **214**"
line, and the `acceptance.md` guard row.

Nine of fourteen zones read CLEAN. Every survivor is scheduled, not stranded:

  migrations       69  C11 — 0022-0030 stay; core still applies them
  middleware/test  62  C13 — fixture strings + the legacy-key regression test
  scripts          27  C13 — the ratchet's own pattern list (needs a
                            self-exclusion before the total can reach 0)
  middleware/src   19  C12 — publicPaths (6); rest are comments
  web-ui/app       19  C13 — nav test fixtures, two comments
  packages         17  C13 — plugin-api CHANGELOG recording the removal
  env-example       1  the plugin repo URL, which cannot be reworded

Also: `test-typecheck-baseline.json` 406 → 371, and `package-lock.json`
regenerated (npm left `packages/dev-runner-shim` as an `extraneous: true`
stanza rather than dropping it; `npm ci` verified clean afterwards).

Docs:
- README gains a "C10 — the flip" status section: what left, what was kept and
  why, H3 resolved by omission, the adversarial coverage reduction, and the
  express.json ordering bug the deletion fixed. The H3 "decision before code"
  is struck through — it is answered now.
- plan.md §4.2 gains a C10 note (and flags that its `devJobStepEffect.ts` line
  went stale in the other direction: C5 deleted that port as dead code). P4 row
  marked shipped.
- docs/CHANGELOG.md: "Dev Platform moved to byte5ai/omadia-dev-platform
  (install via Hub/ZIP)" — written for operators, so it leads with how to get
  it back, states that no data is touched, and that in-flight jobs survive the
  upgrade because C12 has not run yet.

* fix(#470): lock the conductor raw-body mount order and drop the dead sha_on_release matrix ref (C10 review)

Cross-family review of PR #804. Three defects, no scope change to the deletion itself.

1. `.github/workflows/publish-images.yml` referenced `matrix.sha_on_release`, which
   nothing defines any more: the only two matrix entries that set it (dev-runner,
   dev-runner-daemon) left with the Dev Platform. actionlint errors on this branch and
   is clean on main. Dropping the clause also restores the behaviour the adjacent
   comment already claims — sha tags on the edge build only.

2. `middleware/package.json` — the `_dependencyNote` added by this PR was indented four
   spaces and pushed `"dependencies"` to column 0. Valid JSON, but the rest of the file
   is 2-space and any tool that rewrites it would reflow the diff. Text unchanged.

3. Removing the LLM-proxy carve-out moved `createConductorWebhooksInboundRouter` from
   *after* the global `express.json` to *before* it. That is correct — and it silently
   fixes a live bug: on main the router HMAC'd `Buffer.alloc(0)`, because body-parser
   stamps `_body` and the route-level `express.raw()` then short-circuits, so every
   correctly-signed inbound webhook answered 401. Measured on a standalone probe:
   json-first yields rawLen 0, router-first yields the full 24 bytes.

   Nothing locked that order. `conductorWebhookInbound.test.ts` mounts the router alone
   on a bare app, which passes under BOTH orders — which is exactly how the broken
   ordering shipped green. Added two behavioural tests (same signed request, both mount
   orders: 202 vs 401) plus a source-order assertion in `778RouteMounts.wiring.test.ts`
   that fails loudly if either anchor goes missing rather than passing vacuously.

   All four assertions mutation-checked: reordering the mount fails the wiring test,
   renaming the anchor fails it for the anchor reason, and forcing/removing the parser
   in the harness kills exactly the intended behavioural tests.

Also corrects the C10 file count in the spec README (213 -> 217 measured deletions).
The adjacent "43 config keys" claim was verified correct and left alone. Ratchet stays
at 214 — the one comment that would have raised it was reworded, not excused.

* feat(#470): delete the two dev-platform publicPaths exemptions (C12)

Core stops exempting two URLs on a plugin's behalf. Since C4 the plugin
declares them itself in `permissions.public_paths`, and the operator consents
per prefix (`platform/publicPathGrants.ts`).

The deletion is the load-bearing half of that handover, not tidy-up.
`validateDeclaredPublicPath` REJECTS a declaration that collides with a live
static exemption — "is already a static core public path … remove the core
exemption first, or drop the declaration" — because two mechanisms claiming
one URL is ambiguous and only one of them terminates. So while these entries
existed the plugin's own grant could never validate. With them gone it does.

Guarded by a new suite that pins `STATIC_PUBLIC_PATHS` as a CLOSED set: every
entry must be accounted for by a named, justified core-owned exemption.
Restore either deleted entry and the suite goes red on the unaccounted entry,
which is what makes this commit cleanly revertible.

The suite deliberately does not name the two deleted paths. Hardcoding a
plugin's wire paths into core is a reference to that plugin whichever
direction it asserts in — the coupling this epic removes and the thing
`scripts/check-core-decoupling.mjs` counts — and it goes stale-but-green the
day the plugin renames them. The closed set is also the stronger guard: it
catches any unauthenticated surface being added, not just these two. The 401
follows as a corollary, since `requireAuth` runs on the `/api` mount before
routing, and is asserted end-to-end against the same chain `src/index.ts`
assembles.

Ratchet: 214 → 209. Nothing raised.

* feat(#470): drive the core-decoupling count to 0 and pin it there (C13)

Core carries no Dev Platform reference at all: no code path, config key,
i18n key, fixture string or comment. All 14 zones CLEAN.

  214 -> 0    (3,300 at the epic's start, 214 after C10, 209 after C12)

WHAT MOVED

- middleware/src (14 -> 0), packages (17 -> 0), env-example (1 -> 0):
  comments and one operator-facing pointer. Where the reference was to a
  dead path (`devplatform/routes/devWebhooks.ts`, `src/devplatform/
  devJobTypes.ts`) it was a stale link and is now corrected; where it named
  the product it stays named, in prose.
- middleware/test (62 -> 0): fixture route prefixes, nav ids and channel
  types now use a neutral example plugin. `manifestDevJobsLegacyKey.test.ts`
  becomes `manifestRetiredPermissionKey.test.ts` and asserts the guarantee
  that actually exists — unknown permission keys are ignored, not rejected —
  which is key-agnostic, so pinning one historical key made it stale-but-
  green the next time a different capability is retired.
- web-ui/app (19 -> 0): the same, plus a genuinely stale comment claiming
  the nav entry appears "while DEV_PLATFORM_ENABLED" — an env var C10
  deleted.
- scripts (27 -> 0): the ratchet was counting its own `PATTERNS` array. It
  now excludes itself. A detector that counts its own definitions can never
  reach zero, and "zero except for the 27 that are the tool" is not a
  property anyone can check at a glance.

WHAT IS ALLOWLISTED, AND WHY IT IS NOT A DODGE

Two path-anchored entries, both historical record rather than coupling.
A record you are allowed to edit is not a record.

- `middleware/migrations/00{22..30}_*` — every deployment that ran them has
  those FILENAMES in its `schema_migrations` ledger, and C11's plugin-side
  migrator seeds from exactly those donor rows. Rename one and the handoff
  stops matching on the installations that need it most.
- `packages/plugin-api/CHANGELOG.md` — a published entry for a released
  version, written to be found by a consumer grepping for a removed type.

`services/githubAppJwt.ts` stays in core and needed no allowlist: it is
generic GitHub App auth and carries no matching identifier.

THE FLOOR IS NOW ABSOLUTE

A ratchet parked at zero still reads its floor out of a JSON file, and a
JSON file is editable. `EXTRACTION_COMPLETE` makes the script assert 0
outright and ignore the baseline for pass/fail; `--update` refuses to write
a non-zero baseline. The CI job additionally greps the report for `TOTAL 0`,
so a future refactor that made the script exit 0 while still counting hits
still fails. Bringing the Dev Platform back into core is an architectural
decision to argue in review, not a number to edit.

Zero counts IDENTIFIERS, not the English name: a comment may still say "the
dev platform plugin". Prose describes history; identifiers create coupling.

* fix(#470): fail loud in CI when the closed-set 401 half cannot bind loopback (C12 review)

The suite self-skips its five unauthenticated 401 assertions when a
sandbox refuses a loopback listener. Locally that is right — the closed-set
assertion still runs. In CI it is not: a runner that denied bind(127.0.0.1:0)
would delete the entire 401 half of the guard while the job stayed green,
which is the same permanently-green failure family the decoupling job's own
timeout comment warns about.

Keep the local skip, but throw with a named reason when CI is set.

* fix(#470): anchor the detector self-exclusion and finish the neutral fixture rename (C13 review)

Three defects from cross-family review of the zero floor.

1. The self-exclusion was a BASENAME wildcard. `!**/check-core-decoupling.mjs`
   hides every file of that name anywhere in a scanned zone, not just the one
   detector. A probe at middleware/src/__probe/check-core-decoupling.mjs
   carrying three real identifiers scanned as "Core is free of Dev Platform
   references", exit 0. Anchored to the exact repo-relative path; the scripts
   zone still reports CLEAN, and the probe now trips the check.

2. The mutation test is now committed, not just performed. It plants that
   probe and asserts the checker goes red. Its own tokens are assembled at
   runtime so the test file cannot trip the detector it tests.

3. The C13 identifier rename left the plugin's HUMAN-READABLE label behind in
   four fixtures. PATTERNS are identifier-shaped, so `Dev Platform` and
   `Dev-Plattform` are invisible to the zero floor while the sweep claimed no
   fixture string survives. Renamed with their assertions, preserving the
   en/de/de-AT/fr fallback each test exercises. Deliberate prose mentions are
   untouched.

The regression test is wired into the decoupling job. scripts/ has no
package.json and no job globs *.test.mjs, so it would otherwise have been a
test nobody runs — green because nothing executed it.

* chore(470): lower decoupling baseline 211 → 206 after C12

* chore(470): keep C13's neutral tombstone wording after merging C12/main

* docs(470): drop the superseded in-flight ratchet row
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