Investigate: noclickops v2 targets the new ADO repo layout
IMPLEMENTATION RULES: Before implementing the plans that come out of this investigation, read and follow:
- WORKFLOW.md — the implementation process
- PLANS.md — plan structure and best practices
Status: Completed — all child plans shipped (PLAN-A through PLAN-F)
Completed: 2026-05-29
Outcomes: lib/service-v2.sh (PLAN-A), bin/info.sh (PLAN-B), bin/deploy.sh with multi-pipeline orchestration (PLAN-C), bin/logs.sh + bin/shell.sh (PLAN-D), bin/clean-sample.sh (PLAN-E), bin/add-service.sh with two-PR auto-merge (PLAN-F). Version bumped to 1.6.0 (minor — keeps semver-major in reserve until v2 is proven on a live target repo end-to-end; the 2.0.0 cut happens once manual smoke confirms everything works). v1's lib/service.sh + lib/service.ps1 still in the tree pending cleanup (PLAN-G — separate follow-up).
Goal: Cut over noclickops from the FRT-shaped repo layout (v1.x) to the new layout used by copier-add-service-generated repos like ABC100001-myservice. v2 supports the new layout only. v1.x stays available for FRT users via the existing tag.
Last Updated: 2026-05-29
Surveyed target: /Users/terje.christensen/learn/redcross/ABC100001-myservice (ADO repo ExampleOrg/FrontendPlatform/ABC100001-myservice, one service services/postgrest/).
Design principle
The Azure engineer owns the target-repo layout. Repo layout, per-env variable names, pipeline names, container-app naming, RG naming, subscription assignment — all of that is set by the Azure engineer (via copier-add-service and Azure DevOps). noclickops's job is to discover what exists and trigger / call it. We:
- Do NOT clone
copier-add-serviceto read the canonical schema — the schema is whatever's deployed in the wild. - Do NOT invent naming conventions — we query Azure DevOps to find what's actually there.
- Do NOT modify the target repo's structure —
add-service,deploy, etc. trigger pipelines; we never bypass them. - Accept overrides (
--subscription,--resource-group,--app-name,--pipeline-name) for values that can't be discovered. - Degrade gracefully —
infoprints "not discoverable; pass--<flag>to override" rather than guessing.
This is the existing "wrap, never replicate" rule applied to layout: we wrap whatever the upstream pipeline definitions encode; we don't replicate authority over them.
The target layout (v2)
The new layout spans two ADO projects and two git repos per service. noclickops must orchestrate across all of them.
Project 1 — FrontendPlatform (the developer's source repo)
<repo>/ # e.g. ABC100001-myservice
.pipelines/
add-service.yaml # ADO pipeline definition (the request creator)
services/
<svc>/ # e.g. frontend
Dockerfile
app/ # service source code
config.test.yaml # per-env config — LIVES WITH THE SERVICE
config.prod.yaml
README.md
.pipelines/
service.yaml # BUILD pipeline (docker build, trivy scan, publish artifact)
deploy_service.yaml # DEPLOY pipeline (publishes deployment-package-{env})
Pipelines registered in FrontendPlatform per service:
<repo>-add-service— opens a PR with the service folder and triggers the IaC handoff<repo>-<svc>-build— builds the image, publishesdeploy-package<repo>-<svc>-deploy— packagesdeploy-package+ config → publishesdeployment-package-{env}(artifact only — does NOT touch Azure)
Project 2 — IaC (infrastructure / deployer; engineer-owned)
platform-infrastructure/ # git repo IaC owns
environments/
<PREFIX>/ # TCH / TST / HVI / FRT / CMS / etc. (from repo-name prefix)
<repo>/ # ABC100001-myservice
infrastructure/ # repo-level Bicep + ARM templates
services/
<svc>/ # frontend
.pipelines/
build_service.yaml # ACR push for this svc
test_deploy_service.yaml # ARM deploy → test sub
prod_deploy_service.yaml # ARM deploy → prod sub
<service-bicep + params>
Pipelines registered in IaC per service:
<repo>-infra-add-service— auto-triggered by FrontendPlatform's<repo>-add-service; opens a PR inplatform-infrastructureadding the YAML + Bicep files for the new service<repo>-<svc>-infra-build— pushes the docker image to ACR<repo>-<svc>-deploy-test— runs ARM template against the test subscription (the actual Azure deploy)<repo>-<svc>-deploy-prod— same for prod
How a service goes from "doesn't exist" to "live"
End-to-end flow with no auto-resource-trigger wiring (first-time path):
1. dev: noclickops add-service <svc>
2. ↳ trigger FrontendPlatform: <repo>-add-service
3. ↳ that opens PR-A in the source repo (service code; e.g. PR #4831)
4. ↳ that triggers IaC: <repo>-infra-add-service (auto-handoff)
5. ↳ that opens PR-B in platform-infrastructure (deploy YAML; e.g. PR #4832)
6. → merge BOTH PRs (PR-A and PR-B)
7. dev: noclickops deploy <svc> <env>
8. ↳ trigger FrontendPlatform: <repo>-<svc>-build → <repo>-<svc>-deploy
9. ↳ trigger IaC: <repo>-<svc>-infra-build → <repo>-<svc>-deploy-test
10. ARM deploys container app to rg-<env>-nrx-<repo-prefix-lc>
11. Front Door custom-domain validation + DNS propagation + managed cert (~30-90 min on first deploy)
12. <svc>.example.cloud serves 200
For subsequent (non-first) deploys, FrontendPlatform's -deploy succeeding fires a resource-trigger on IaC's deploy-test automatically — only steps 7-10 happen, in ~3-10 min. First-time path requires explicit -infra-build first because the resource trigger hasn't yet activated for the brand-new pipeline definition.
Generated by copier-add-service. The Azure engineer owns ALL of this — both repos, both projects, all the Bicep, all the pipeline YAML. noclickops's job is to discover what exists in each project and call what's already there.
Discovered facts (2026-05-29 — az queries against the test repo)
Pipeline naming (confirmed)
az pipelines list --repository <repo> for ABC100001-myservice returned exactly:
| ID | Name |
|---|---|
| 1125 | <repo>-add-service |
| 1128 | <repo>-<svc>-build |
| 1129 | <repo>-<svc>-deploy |
Convention: every service has a -build + -deploy pair. noclickops deploy <svc> <env> triggers <repo>-<svc>-deploy (not -build). The deploy pipeline's targetEnvironment parameter selects test vs prod.
Per-env config variables (confirmed)
Read from services/postgrest/config.test.yaml:
SERVICE_PORT,SERVICE_CPU,SERVICE_MEMORY,SERVICE_MIN_REPLICAS,SERVICE_MAX_REPLICAS,SERVICE_HEALTH_CHECK_PATH,SERVICE_HEALTH_PROBE_PORTENABLE_PUBLIC_ENDPOINT,PERSISTENT_STORAGE- App-specific (varies per service):
OKTA_ISSUER,APP_BASE_URL(postgrest-only).
SERVICE_NAME is set in the build pipeline YAML (services/<svc>/.pipelines/service.yaml), but it's identical to the folder name (services/<svc>/ → <svc>) — noclickops derives it from the path, doesn't read the YAML.
Subscription (discoverable via service connection)
ADO pipeline-level variables are empty for all three pipelines. Subscription is encoded in a service connection:
$ az devops service-endpoint list --query "[?type=='azurerm'].{name:name, sub:authorization.parameters.scope}"
myteam-frontend-test-sp /subscriptions/3aec5ff4-...-c36f2bf3ca2d
myteam-frontend-prod-sp /subscriptions/<prod-id>
myteam-frontend-acr-sp <ACR scope>
Convention: <org-prefix>-<env>-sp. noclickops parses authorization.parameters.scope to extract the subscription ID per env.
IaC project + cross-project pipelines (the deployer)
The actual Azure deploy lives in the IaC project (separate from FrontendPlatform). Discovered by listing pipelines per project:
az pipelines list --project IaC --query "[?contains(name, '<repo>')].name"
ABC100001-myservice-CD
ABC100001-myservice-infra-add-service
ABC100001-myservice-postgrest-infra-build
ABC100001-myservice-postgrest-deploy-test
ABC100001-myservice-postgrest-deploy-prod
ABC100001-myservice-frontend-infra-build
ABC100001-myservice-frontend-deploy-test
ABC100001-myservice-frontend-deploy-prod
Convention: <repo>-<svc>-infra-build, <repo>-<svc>-deploy-{test|prod} in the IaC project. The pipelines point at YAML files in the platform-infrastructure git repo (also in IaC).
Resource group naming: CONFIRMED
From the failed first-deploy log (run 28537): rg-test-myteam-abc100001. Convention: rg-<env>-nrx-<repo-prefix-lc> (repo-prefix = the part before the first - in the repo name, lowercased).
Container-app naming: CONFIRMED
Found by az containerapp list against peer NRX subscriptions (DEV / TEST / PROD INTEGRATIONS) that the user DOES have Reader on — same naming model as the FrontendPlatform sub:
| Convention | Pattern | Example |
|---|---|---|
| Container app name | ca-<repo-prefix>-<svc> | ca-abc100001-frontend (predicted), ca-xyz900005-backend (observed) |
| Container FQDN (internal) | <name>.<env-hash>.<region>.azurecontainerapps.io | ca-xyz900005-frontend.examplehill-deadbeef12.westeurope.azurecontainerapps.io |
| Image (ACR) | <CONTAINER_REGISTRY_NAME>.azurecr.io/<image-name>:<tag> | acrshareduw.azurecr.io/test-frontend-repo:latest |
| Public FQDN (Front Door) | <svc>.<DNS_ZONE_NAME> | frontend.example.cloud |
So our earlier <svc> hypothesis was incomplete — Front Door does route on <svc> for the hostname, but the backend container app name carries the repo-prefix too (ca-<repo-prefix>-<svc>). Front Door's routing rule maps <svc>.example.cloud → ca-<repo-prefix>-<svc> internally.
Permission boundary for live-state commands
Tested az containerapp calls against an accessible peer (ca-xyz900005-frontend in DEV INTEGRATIONS):
| Call | Permission needed | Result |
|---|---|---|
az containerapp show | Reader on sub | ✓ works — returns name, image, revision, fqdn, replicas, ingress |
az containerapp list | Reader on sub | ✓ works |
az containerapp logs show | Microsoft.App/containerApps/getAuthToken/action (Container Apps Logs Reader or higher) | ✗ AuthorizationFailed even with Reader |
az containerapp exec (shell) | same as logs | ✗ same |
Implication for v2: info works for anyone with sub Reader (good — degrades gracefully when even that's missing). logs and shell need elevated role; v2 should keep v1.5.x's behaviour of failing closed with a clear "ask your admin for Container Apps Logs Reader on subscription <id>" message.
Deploy mechanism: ARM templates
From the deploy-test pipeline log: deploys via the AzureResourceManagerTemplateDeployment@3 task. Inputs come from a <svc>-infra artifact (built by <svc>-infra-build) containing template.json + template.params.json. So Bicep is compiled into JSON ARM templates upstream and shipped as an artifact between the build and deploy pipelines.
add-service is a TWO-PR flow
Observed during the empirical test:
- FrontendPlatform's
<repo>-add-serviceruns (~10-30s, just publishes adeployment-requestartifact). - Downstream automation (in IaC) consumes the request → opens PR-A in the source repo (
<repo>) titled "Add service<svc>" containing the new service folder + Dockerfile +service.yaml. Source branch:add-service-<svc>. - Simultaneously triggers IaC's
<repo>-infra-add-service→ opens PR-B inplatform-infrastructuretitled "Add service<svc>" containing the IaC YAML + Bicep for the new service. - Both PRs must be merged. Branch policies on
platform-infrastructure/mainare empty — self-approval works.
Real evidence of how often this trips up developers: there are open PR-B's in platform-infrastructure going back to April 2026 (Add service CopierTest, Add service testingv3, Add service newprojectv2). Each represents a service whose deploy is silently blocked because nobody merged PR-B.
Deploy is a FOUR-pipeline orchestration (first-time)
For first-time deploys, the developer triggers and the pipelines fire in this order:
FrontendPlatform/<repo>-<svc>-build (~1-2 min, publishes deploy-package)
FrontendPlatform/<repo>-<svc>-deploy (~30s, publishes deployment-package-test)
IaC/<repo>-<svc>-infra-build (~3 min, pushes image to ACR) ← required for first time only
IaC/<repo>-<svc>-deploy-test (~6 min, ARM deploy creates container app)
For subsequent deploys, the FrontendPlatform -deploy succeeding fires a resource-trigger on IaC's deploy-test automatically (and infra-build typically isn't needed if the image tag hasn't moved). So same-day re-deploys are just 2 pipelines + auto-trigger.
The first-time gotcha: the resource-trigger on a freshly-registered IaC deploy-test pipeline doesn't auto-wire until that pipeline has run at least once manually. Combined with the missing infra-build, first deploys require manual triggering of both IaC pipelines.
Branch policies + auto-merge of PR-B
platform-infrastructure/main has empty branch policies (verified via az repos policy list). Self-approval via az repos pr set-vote --vote approve is sufficient; merge via az repos pr update --status completed --squash works.
This means noclickops can auto-merge PR-B if it has write access on platform-infrastructure — same way it auto-merges PR-A in the source repo today.
Public-endpoint architecture
All *.example.cloud services in FrontendPlatform route through a single Azure Front Door per environment:
<svc>.example.cloud
→ CNAME test-endpoint-frontend-<hash>.z02.azurefd.net (Azure Front Door, one endpoint for the env)
→ CNAME mr-z02.tm-azurefd.net
→ <Front Door IP>
Cert is a Front Door-managed DigiCert / GeoTrust SAN cert with CN=<svc>.example.cloud.
First-time deploy timing ("firewall registration" the engineer flagged): adding a new custom domain to the Front Door + DNS validation + managed-cert provisioning takes ~30–60 minutes. Subsequent deploys (same domain, new image) are fast — Front Door routing + cert are already in place.
Implication for noclickops:
- The public URL is always
<svc>.example.cloudfor any service withENABLE_PUBLIC_ENDPOINT: "true". Discoverable without Azure access — just readconfig.<env>.yamland apply the convention. - For first-time public deploys, see PLAN-watch-live-deploy for the layered polling that handles the ~60 min wait.
v1.5.2 against the new layout — observed failure modes
Captured 2026-05-29 against the live ABC100001-myservice repo. These are the exact messages a user sees today; v2 fixes each one.
| Command | Exact output |
|---|---|
noclickops | ✓ — works |
--help | ✓ — works |
update | ✓ — works |
status | ✓ — works (listed all 4 recent runs cleanly) |
status <run-id> | ✓ — works |
create-pr | ✓ — works |
merge-pr <id> | ✓ — works (merged PR-A #4831) |
add-service <svc> | Trigger pipeline ✓; PR-A appears ~1–2 min later; PR-B in platform-infrastructure also opens but v1.5.x doesn't know about it |
info <svc> <env> | ✗ Repo-level variables missing: <repo>/.pipelines/variables/common.yaml |
logs <svc> <env> | Same as info |
shell <svc> <env> | Same as info |
deploy <svc> <env> | ERROR: There were no build definitions matching name "<repo>-<svc>-CD" in project "<project-id>". |
clean-sample <svc> | ✗ services/<svc> doesn't look like the unmodified Next.js sample (missing components/control-panel.js). Refusing to auto-delete... — safe refusal on the new Express-shaped sample; not a regression |
sync-lovable --help | ✓ — works (full metadata renders); underlying command not exercised (no Lovable repo at hand) |
All 12 commands exercised against a live new-layout repo. No crashes; failures are explicit and informative.
Failures are fast and explicit — no hangs, no cryptic errors. v1.5.x against the new layout is safe to attempt; the user just gets a clear "doesn't apply here" signal.
Per-command impact
| Command | v2 change |
|---|---|
noclickops (lister) | None — layout-agnostic. |
update | None. |
status | None. az pipelines runs list is layout-agnostic. |
create-pr | None. |
merge-pr | None. |
info | Reads services/<svc>/config.<env>.yaml (not .pipelines/variables/<env>.yaml). Renders the new field set. Live container-app section requires --subscription / --resource-group / --app-name overrides OR best-effort discovery via az containerapp list (which needs the user's own Reader access on the deployed subscription — the SP creds in the service connection are the pipeline's, not the human's). Degrades gracefully when access / overrides are missing. |
deploy | Orchestrates 4 pipelines across 2 projects (FrontendPlatform + IaC) for first-time deploys; 2 pipelines + auto-trigger for subsequent. Detection: query IaC for the existence + last-run-status of <repo>-<svc>-deploy-test. If never run successfully → first-time path (trigger build → deploy → infra-build → deploy-test, sequentially with watch). If recently run successfully → trust resource trigger (just trigger <repo>-<svc>-deploy and watch both pipelines). --watch-live (PLAN-watch-live-deploy) then polls Front Door + DNS + cert + HTTPS for the additional 30-90 min on first-time public-endpoint services. |
logs | Same RG/app-name gap as info live state. Gating (no degrade) — requires --subscription + --resource-group + --app-name overrides if discovery fails. |
shell | Same as logs. |
add-service | Triggers <repo>-add-service (unchanged). BUT this opens TWO PRs — PR-A in the source repo (service code) AND PR-B in IaC/_git/platform-infrastructure (deploy YAML). v2 polls for PR-A by source branch (add-service-<svc>) AND for PR-B by title (Add service <svc>), then self-approves + merges both. Without PR-B merged, no subsequent deploy can succeed. v1.5.x didn't know PR-B exists; that's why the test repos accumulated unmerged PR-Bs going back months. |
clean-sample | New sample shape (Express + minimal app/, no Next.js). Rewrite to detect+strip the new sample. |
sync-lovable | Probably doesn't apply — the new layout's services have an app/ folder with Express, not a Vite/React PWA root. Defer until there's a concrete Lovable-into-new-layout use case. |
PLAN sequence
PLAN-A — lib/service.sh rewrite for the new layout
Centralises discovery for the new layout. Single source of truth used by every downstream command.
read_service_config <svc> <env>→ readsservices/<svc>/config.<env>.yaml(in the source repo) intoparsed_*globals.read_iac_variables <env>→ readscommon.yaml+<env>.yamlfromIaC/platform-infrastructure/environments/<TEAM>/<repo>/infrastructure/.pipelines/variables/. Same field names as FRT (SUBSCRIPTION_ID,APP_NAME,COMMON_RESOURCE_GROUP_NAME,CONTAINER_APPS_MANAGED_IDENTITY_NAME,DNS_ZONE_NAME, etc.). This is the structural FRT-equivalent in the new layout — small refactor, not a re-architecture.discover_iac_project→ typicallyIaC— acceptsNOCLICKOPS_IAC_PROJECTenv override; in practiceread_iac_variablesreadsIAC_PROJECTfromcommon.yamlso it self-discovers.discover_pipelines <svc>→ returns a struct with:frontend_build,frontend_deploy,iac_infra_build,iac_deploy_test,iac_deploy_prodpipeline ids. Empty fields when not found.discover_containerapp <svc>→ best-effortaz containerapp list -g <COMMON_RG-from-iac-vars>with<svc>filter; accepts--app-nameoverride.derive_rg <env> <repo>→rg-<env>-nrx-<repo-prefix-lc>(pure derivation; cross-check againstCOMMON_RESOURCE_GROUP_NAMEfrom iac vars — they may diverge for shared vs per-service RGs).public_url_for <svc> <env>→ ifENABLE_PUBLIC_ENDPOINT: "true"inconfig.<env>.yaml, returns<svc>.<DNS_ZONE_NAME>(whereDNS_ZONE_NAMEcomes from iac vars —example.cloudin test).
PLAN-B — info rewrite
Smaller refactor than the original sketch — most fields v1.5.x's info already shows come from the same variable names; just sourced from read_iac_variables instead of the source repo's .pipelines/variables/. The new service-level read_service_config adds the per-service overrides (SERVICE_PORT, SERVICE_CPU, etc.). Live container-app state via discover_containerapp + the RG from iac vars; degrades with a clear "pass --app-name <name> to specify backend" message if best-effort discovery comes up empty.
PLAN-C — deploy rewrite (multi-pipeline orchestration)
The big one. Two paths:
- Subsequent deploys (IaC's
deploy-testhas succeeded before for this svc/env): just trigger FrontendPlatform's<repo>-<svc>-deploy. The resource trigger fires IaC'sdeploy-testautomatically. With--watch, watches both pipelines. - First-time deploys (no successful
deploy-testrun yet): explicitly run in sequence —<repo>-<svc>-build→<repo>-<svc>-deploy→<repo>-<svc>-infra-build→<repo>-<svc>-deploy-test. With--watch-live, additionally polls Front Door / DNS / cert / HTTPS for the additional 30-90 min on first-time public-endpoint services (per PLAN-watch-live-deploy.md).
Detection: query IaC for <repo>-<svc>-deploy-test runs; if none succeeded → first-time.
PLAN-D — logs / shell rewrite
Same discover_containerapp + RG-from-iac-vars as info's live section. Gating: fail with --app-name / --resource-group override message if discovery comes up empty. With the iac-vars-reader in place, refactor is small.
PLAN-E — clean-sample for the new sample shape
Detect Express + app/ shape and strip it. v1's Next.js-sample logic is dropped along with FRT support.
PLAN-F — add-service two-PR flow
v1.5.x merges only PR-A (source repo). v2 waits for both PRs:
- Poll
az repos pr list --source-branch add-service-<svc>in the source repo until PR-A appears (~1-2 min); self-approve + squash-merge. - Poll
az repos pr list --project IaC --repository platform-infrastructure --status activefor a PR titledAdd service <svc>; self-approve + squash-merge. - Print a summary: "Source PR #X merged; infrastructure PR #Y merged. Run
noclickops deploy <svc> testto ship the first deploy."
--no-merge stays as the escape hatch (skips both PR merges; user is responsible).
Version bump
v1.5.x → v1.6.0 when PLAN-A through PLAN-F land (minor — semver-major reserved until v2 is proven on a live target repo end-to-end). Once manual smoke confirms every command works, a follow-up bumps to v2.0.0 (semver major; the target-repo contract change deserves a major when ready). v1 stays available via the v1.5.x tag for anyone still on FRT.
The --watch-live flag for first-time public-endpoint deploys is filed separately as PLAN-watch-live-deploy.md — ships as part of v2's PLAN-C --watch-live flag.
Risks
-
Container-app naming convention unconfirmed.
discover_containerappwill pattern-match by<svc>, but the downstream may use<svc>-<env>or<repo>-<svc>. Always offer--app-nameoverride; document it loudly. (Pending confirmation when the frontend test deploy reaches HTTPS 200 and we canaz containerapp list.) -
Subscription access mismatch. The pipeline's identity (the service connection SP, e.g.
myteam-frontend-test-sp) has Contributor on the deployed sub; the human developer may have nothing.infolive section,logs,shellneed the human's own access — failing closed with a clear message is the right behaviour. Don't try to use SP credentials from the service connection — that's a layer violation. -
Cross-project orchestration auth surface. v2 commands need read access on FrontendPlatform AND read/write on IaC (specifically platform-infrastructure for PR-B merging). Some developers may have one but not the other. Each
az pipelines runandaz repos pr updatein IaC could fail with 403. v2 must produce clear error messages naming the missing project + permission. -
Branch policies on
platform-infrastructurecould harden later. Today policies are empty — self-approval works. If the Azure engineer adds a required-reviewer policy, v2's auto-merge of PR-B breaks. Mitigation: try auto-merge; on policy-block, fall back to printing the PR URL and instructions (PR-B needs review at <url>; merge it, then re-run noclickops deploy <svc> test). -
First-deploy resource-trigger quirk. A freshly-registered IaC
deploy-testpipeline doesn't auto-fire from FrontendPlatform's-deployuntil it has run once manually. v2's first-time path triggers it explicitly; subsequent deploys rely on the trigger. If the trigger ever breaks (e.g. Azure engineer changes the YAML),deployshould detect "FrontendPlatform deploy succeeded but no IaC deploy fired within 60s" and fall back to manual triggering. -
Unmerged PR-B accumulation across the org. We observed PR-Bs in
platform-infrastructuregoing back to April with no merges. Today developers don't even know PR-B exists. Once v2 auto-merges them, expect cleanup pressure on those old PRs (manual decision per PR). Not noclickops's job to clean those up. -
copier-add-servicewill evolve. Each evolution may add / rename variables, change pipeline naming, restructure either repo's folder layout. Build noclickops around discovery + overrides + graceful degrade — not hardcoded names. The Bicep templates insideplatform-infrastructurearen't noclickops's concern; only the pipeline names and the PR titles are.
Out of scope
- FRT layout support. Dropped in v2. v1 users pin to
v1.5.x. - GitHub-target support. Future extension; not v2.
sync-lovablefor the new layout. Defer until a real use case exists.- Discovering the downstream deploy system. Not noclickops's concern — overrides handle the gap.
Next steps
- Draft PLAN-A (
lib/service.shfor the new layout). - Draft PLAN-B (info), PLAN-C (deploy), PLAN-D (logs/shell), PLAN-E (clean-sample).
- Per CLAUDE.md PR-per-investigation rule, all 6 plans accumulate on one branch; single PR opens after PLAN-F. Version → 1.6.0 (minor); 2.0.0 (major) cut reserved for after live-repo smoke confirms v2 works end-to-end.
- Move this INVESTIGATE to
completed/when PLAN-E merges. - Pin the current FRT-supporting state as
v1.5.xtag for users who haven't migrated.