Task Breakdown: App runtime on Kubernetes
Ordered tasks derived from
plan.md. Each is small enough to land in one PR and ships with tests per Constitution VI. Schema tasks ship their migration in the same PR per Constitution V.
Epic ID: APW-06-app-runtime
Spec: ./spec.md · Plan: ./plan.md
Status: Draft
Last updated: 2026-09-17
How to use
- Tasks are sequential by default.
(parallel)means it may run alongside its predecessor. - Every task names the exact files to create or modify;
_(new)_files do not exist yet; every other Modify path exists ondevelop@ee45946e5. - Every task has a Test line (a file and what it asserts, or the command that is the test) and a Done when
line that is checkable without reading the diff.
(ACC-06-nn)tags name the spec §8 criteria a Test line proves. - Add new tasks at the bottom rather than renumbering. T47 and T48 were moved from P2 into P1.8 by Resolution R-16 and keep their numbers.
- Phase boundaries are ship boundaries:
developmust be green and deployable at the end of each phase. - Commands run from the monorepo root; migrations are authored from
apps/api/. Package filters:@ever-works/contracts,@ever-works/plugin,@ever-works/k8s-plugin,@ever-works/agent,ever-works-api,ever-works-web,@ever-works/trigger-tasks. - API behaviour is tested under
apps/api/src/**orapps/web/e2e/— neverapps/api/test/(Resolution R-22). - Additive guard for every task: the existing suites
packages/plugins/k8s/src/__tests__/*.spec.ts,apps/api/src/plugins-capabilities/deploy/*.spec.tsandapps/web/e2e/flow-work-deploy-*.spec.tspass with no edits to existing assertions. - Resolutions that shape these tasks (CONTRACTS §0):
R-2 (Activity families), R-3 (eligibility read from APW-03, no attestation stored here), R-5 (Ever Works Apps only
through
AppsTierPolicyand theapps-tierdeployment plugin), R-10 (verification targets), R-12 (target None), R-15 (deleting an App Work), R-16 (managed subdomain on Your cluster in Wave 1), R-24 (sandboxed runtime from Wave 2).
Phase P1 — Your cluster (Wave 1)
Delivers spec FR-1…FR-6, FR-9…FR-21, FR-23…FR-51 (managed subdomain on Your cluster included), FR-54…FR-62.
P1.1 — Contracts and ports
-
T1. Runtime constants. Create
packages/contracts/src/apps/app-runtime.ts(new) with every constant in plan §5.3 plusAPP_DEPLOYMENT_STATES,APP_RUNTIME_HEALTH,APP_DEPLOY_TARGETS(none,your-cluster,ever-works-apps— no "not yet" value, R-12),APP_PRECONDITION_CODES(plan §5.1, incl.managed_ineligible,managed_sandbox_unavailable,app_work_deleting) andAPP_FAILURE_CODES(plan §5.4, §11). Modifypackages/contracts/src/apps/index.ts(APW-03's barrel — appendexport * from './app-runtime.js'; create it and Modifypackages/contracts/src/index.tsonly if APW-03 has not landed — R-1). Test:packages/contracts/src/apps/__tests__/app-runtime.spec.tspins every numeric value and every union;APP_DEPLOY_TARGETSis exactly the three values. Done when:pnpm --filter @ever-works/contracts testis green andimport { APP_ROLLOUT_MAX_S } from '@ever-works/contracts'resolves fromapps/api. -
T2 (parallel with T1). Plugin App contract. Create
packages/plugin/src/contracts/capabilities/app-deployment.types.ts(new) with the types of plan §3 and §3.1 (the normative field reference) (AppDeployTarget,AppDeployPhase,AppTargetRef,AppRenderInputincl.purposeandttlMinutes,AppComponentInput,AppJobInputincl.http.authScheme,AppCronInput,AppSmokeInput,AppDeployHooks,AppDeployResultincl.cancelReasonandimage,AppScaleResult,AppStatusSnapshot,AppStatusSpec,AppJobRunRequestincl. therunner: 'smoke'variant,AppJobResult,AppLogRequest,AppLogTail,AppLogRef,AppClusterCheckRequest,AppClusterCheck,AppDestroyResultincl.kept,AppQuotaInput,AppLimitRangeInput,CheckResult,AppSmokeRun,AppSmokeResult,AppComponentStatus,AppFailureCode,AppRuntimeState). Modifypackages/plugin/src/contracts/capabilities/deployment.interface.ts— add the ten optional members (supportsApps,deployApp,getAppStatus,runAppJob,destroyApp,scaleApp,getAppLogs,checkAppCluster,prepareAppNamespace,publishAppHosts) andisAppDeploymentPlugin(plugin)guard (supportsApps === true && typeof deployApp === 'function');scaleAppreturnsPromise<AppScaleResult>and takes the optionalresumeChecksargument. Modifypackages/plugin/src/contracts/capabilities/index.ts— export the new file. Test:packages/plugin/src/contracts/__tests__/app-deployment.types.spec.ts(new) — a type-level test that a plugin implementing only the pre-existing members still satisfiesIDeploymentPlugin(and that a plugin implementing only the eight earlier App members still satisfies it, so the two new members stay optional);isAppDeploymentPlugintrue/false cases. Done when:pnpm --filter @ever-works/plugin testis green andpackages/plugins/vercelbuilds unchanged. -
T3. Ports. Create
packages/agent/src/app-runtime/ports.ts(new) exactly as plan §9.6 (incl.AppsTierPolicy.eligibility,AppRuntimeEnvSource.resolveEphemeral,target: AppDeployTargetin both resolve contexts,AppRuntimeTargetPort/APP_RUNTIME_TARGET,AppRuntimeEventSink/APP_RUNTIME_EVENT_SINK,AppVerificationSink/AppVerificationSinkandAppVerificationUpdate), andpackages/agent/src/app-runtime/default-ports.ts(new):DisabledAppsTierPolicy(isOpen() = false,eligibility→{ eligible: false, reasons: ['managedTierDisabled'] }),UnavailablePullCredentialSource,UnavailableRuntimeEnvSourceandUnavailableRuntimeTargetthat throwAppPortUnavailableError(code), and anUnavailableVerificationSinkthat throwsverification_sink_unavailable. Createpackages/agent/src/app-runtime/index.ts(new) barrel. Modifypackages/agent/package.json— add the./app-runtimesubpath export next to./deployment-context. Test:packages/agent/src/app-runtime/__tests__/default-ports.spec.ts— disabled policy never reports open; unavailable sources throw with codespull_credential_unavailable/env_source_unavailable;UnavailableRuntimeTarget.prepareDependencyTargetresolves{ unavailable: 'target_none' }rather than throwing, so a caller reports a precondition; the verification sink refuses before any namespace exists; a grep assertion finds noEVER_WORKS_APPS_MANAGED_ENABLEDinpackages/agent/src/app-runtime/(R-5). Done when:pnpm --filter @ever-works/agent test -- default-portsis green and the symbols resolve from@ever-works/agent/app-runtime.
P1.2 — The App renderer (k8s plugin)
-
T4. Names and labels. Create
packages/plugins/k8s/src/app/app-names.ts(new) per plan §4.1:appNamespaceName(slug, workId),previewNamespaceName(ns, pr),verificationNamespaceName(ns, provisioningId, attempt), object name helpers,appLabels(...),componentSelector(name),internalUrl(component, namespace). Test:packages/plugins/k8s/src/app/__tests__/app-names.spec.ts— 63-char cap, slug truncation at 30, deterministic 8-hex suffix, the verification suffix-v<first 6 hex of provisioningId>-<attempt ≤ 9>stays ≤ 63 characters and ≤ 52 in practice, is deterministic and gives two provisionings different names (APW06-G09); labels never containever-works.io/managedorapp.kubernetes.io/name, Job name ≤ 45 and CronJob name ≤ 37 for a 32-charName. Done when:pnpm --filter @ever-works/k8s-plugin test -- app-namesis green and the helpers are pure and 100 % branch-covered. -
T5. Security context. Create
packages/plugins/k8s/src/app/app-security.ts(new) —podSecurityContext(input, component),containerSecurityContext(...),tmpVolume(...),namespacePodSecurityLabels(policy)per plan §4.4. Test:packages/plugins/k8s/src/app/__tests__/app-security.spec.ts— oneitper cell of the plan §4.4 table: no privilege escalation,drop: [ALL],RuntimeDefaultseccomp,runAsNonRoot, read-only root unless declared (ACC-06-07);runAsNonRoot: falseonly withallowRootonyour-clusterand never forever-works-apps(ACC-06-08);privileged_portrefusal onever-works-apps;NET_BIND_SERVICEonly withallowRootand port < 1024. Done when:pnpm --filter @ever-works/k8s-plugin test -- app-securityis green and no rendered container lacksallowPrivilegeEscalation: falseandcapabilities.drop: [ALL]. -
T6. Workloads, services, volumes, secrets, ingress. Create
packages/plugins/k8s/src/app/app-manifest.renderer.ts(new) —renderNamespace,renderServiceAccount,renderLimitRange,renderResourceQuota,renderEnvSecret(immutable, keys = env names),renderPlatformConfigMap,renderPullSecret(reusebuildImagePullSecretshape without editing it),renderPvc(emptyDirsubstitute forpurpose: 'verification'),renderComponentDeployment,renderComponentService,renderIngress(reuseIngressStrategyRegistry; TLS only forcert-manager; none for verification),componentDeadlineSeconds(component)(plan §5.3 formula), andvalidateRenderInputreturningvolume_replicas,volume_shrink,privileged_port; export the pure entry points as a library (R-5). Test:packages/plugins/k8s/src/app/__tests__/app-manifest.renderer.spec.tswith golden JSON fixtures inpackages/plugins/k8s/src/app/__tests__/fixtures/built from APW-03schema.md§24 examples: digest image;Recreatewith volumes;envFromnot optional; no env value in any pod spec and no pull credential other than the render input's (ACC-06-16);automountServiceAccountToken: false;enableServiceLinks: false(ACC-06-07); the pod template'sever-works.io/env-checksumchanges when one env value changes and is byte-identical otherwise (ACC-06-15); rendering twice with differentdeploymentId/deploymentShortbut the same Build, env and spec yields byte-identical componentspec.templates and identicalenvFromobject names, putsever-works.io/deployment-idonly under the Deployment'smetadata.annotations, and leaves noEVER_WORKS_DEPLOYMENT_IDkey in the platform ConfigMap (ACC-06-58, APW06-G07); Ingress only for the primary web component; strict host validation; deadline clamp at 300 and 2400; a component with a volume andreplicas: 2→volume_replicas(ACC-06-18); theprepare-namespacesubset renders the namespace, the ServiceAccount and theLimitRangeand — withisolation: true— exactlyew-default-deny,ew-allow-same-namespaceandew-allow-egress, neverew-allow-ingress/ew-allow-deps(ACC-06-54, plan §4.2). Done when:pnpm --filter @ever-works/k8s-plugin test -- app-manifest.renderer manifest.rendereris green andmanifest.renderer.spec.ts(existing) is untouched. -
T7 (parallel with T6). Network policies. Create
packages/plugins/k8s/src/app/app-network-policy.renderer.ts(new) per plan §4.10. Test:packages/plugins/k8s/src/app/__tests__/app-network-policy.spec.ts— the five default policies render and every excepted IPv4/IPv6 CIDR is present (ACC-06-17); controller-namespace and fallback variants;extraEgressand hairpin rules;isolation: falserenders zero policies and returns the five names to delete — and never adep-<kind>name (APW07-G01, ACC-06-54). Done when:pnpm --filter @ever-works/k8s-plugin test -- app-network-policyis green and the fixture diff is reviewed against the plan table. -
T8. Jobs, CronJobs and the runner. Create
packages/plugins/k8s/src/app/app-runner.script.ts(new) (the runner source as a string constant +APP_RUNNER_IMAGEdigest constant),packages/plugins/k8s/src/app/app-jobs.renderer.ts(new) —renderCommandJob,renderRunnerJob(kind: 'http-job'|'smoke'|'hairpin'|'isolation-probe'),renderRunnerConfigMap,renderCronJob; requests withredirect: 'manual',authSchemebearer/raw,{{env.NAME}}fromsecretKeyRef,foundcapped at 200 chars and secret-scrubbed;cron_too_frequentcheck. Test:packages/plugins/k8s/src/app/__tests__/app-jobs.renderer.spec.ts— backoff/deadline/TTL/Never; the ConfigMap contains paths with$(, backticks and quotes verbatim as JSON data and no command string contains them; cron auth viasecretKeyRef;concurrencyPolicymapping;suspend: truewhen paused (ACC-06-35).packages/plugins/k8s/src/app/__tests__/app-runner.script.spec.tsruns the script in-process against a local HTTP server: status,bodyContains,bodyNotContainsfailure quoting the found string (ACC-06-12) with the 1 MiB cap, latency, 307 not followed, bearer vs raw header. Done when:pnpm --filter @ever-works/k8s-plugin test -- app-jobs.renderer app-runner.scriptis green and no renderedcommand/argscontains a value read from the App spec'shttpblock. -
T9. Rollout predicate and classifier. Create
packages/plugins/k8s/src/app/app-rollout.ts(new) per plan §5.4 (isComponentRolledOut,classifyPodFailure,workerStable). Do not changepackages/plugins/k8s/src/status.mapper.ts. Test:packages/plugins/k8s/src/app/__tests__/app-rollout.spec.ts—metadata.generationvsobservedGeneration; old ReplicaSet with ready pods blocks success; 3 restarts;ImagePullBackOfffor 179 s vs 180 s;OOMKilled; the two root-user kubelet messages classifyimage_runs_as_root/image_user_unverifiablewithin 180 s (ACC-06-08). Done when:pnpm --filter @ever-works/k8s-plugin test -- app-rolloutis green and eachAppFailureCodein plan §5.4 has at least one test. -
T10. API wrapper additions. Modify
packages/plugins/k8s/src/k8s-api.service.ts— addapplyObject,readObject,listObjects(apiVersion, kind, namespace, labelSelector),deleteObject(…, propagationPolicy),readPodLog(ns, pod, container, { tailLines, limitBytes, previous }),createSelfSubjectAccessReview, andauthorizationV1ApionKubernetesClientFactory+defaultClientFactory. Existing methods unchanged. Test: extendpackages/plugins/k8s/src/__tests__/k8s-api.service.spec.tswith mocked-factory cases for each new method, including 404 →nullon reads. Done when:pnpm --filter @ever-works/k8s-plugin test -- k8s-api.serviceis green with existing assertions unchanged. -
T11. Kubeconfig guard. Create
packages/plugins/k8s/src/app/app-kubeconfig.guard.ts(new) per plan §6.1:assertSupportedKubeconfig,isPublicAddress(ip, allowlist),pinKubeconfigServer(yaml, resolver)→ rewritten YAML withtls-server-name. Modifypackages/plugins/k8s/src/errors.ts— add codeKUBECONFIG_UNSUPPORTEDandCLUSTER_ADDRESS_NOT_PUBLIC. Test:packages/plugins/k8s/src/app/__tests__/app-kubeconfig.guard.spec.ts—exec,auth-provider,tokenFile, file certificate paths,proxy-url,insecure-skip-tls-verifyand missing CA data each refused before any resolver or client call (ACC-06-02); every deny CIDR incl.::ffff:10.0.0.1,64:ff9b::a00:1; a hostname resolving to one public + one private address is refused; an operator allow-list range is accepted (ACC-06-03); DNS timeout 10 s; resulting YAML has the IP server and originaltls-server-name; a mocked 307 from/versionis not followed. Done when:pnpm --filter @ever-works/k8s-plugin test -- app-kubeconfig.guardis green and a spec spying on the factory proves no code path insrc/app/callsKubeConfig.loadFromStringwithout the guard. -
T12. Deployer: phase machine and rollback. Create
packages/plugins/k8s/src/app/app-deployer.ts(new) per plan §5.5: capture → prepare → pre-deploy jobs → rollout → first-deploy jobs → in-cluster smoke → isolation probe → publish →hooks.verifyPublic→ hairpin (T61) → post-deploy jobs → CronJobs → GC (env Secrets/ConfigMaps beyond 3, Jobs beyond 3 per name); rollback from capture; first-Deployment failure handling (scaleFailedFirstDeployToZero); cancellation between phases and polls; 2-hour overall deadline;purpose: 'verification'path (T60). Test:packages/plugins/k8s/src/app/__tests__/app-deployer.spec.tswith a fake API — phase order; a failing pre-deploy job endsfailedwith zero Deployment writes (ACC-06-09); a crash-looping component re-applies captured templates and hosts →rolled-back(ACC-06-10); the Ingress apply happens after the first-deploy job completes andisFirstDeploymentOnCluster: falserenders no first-deploy Job (ACC-06-11); in-clusterbodyNotContainsfailure →rolled-backwith the found string (ACC-06-12); publicdns_not_pointing→succeeded-with-warningsand no rollback (ACC-06-13); rollback restores the previousenvFromsecret name (ACC-06-15); cancel before change →cancelled, after change →rolled-backwithcancelled(ACC-06-22);skipPreDeployJobson a manual rollback input renders no pre-deploy Job and applies the captured build's image (ACC-06-23); rollback that does not become ready →rollback-failed(ACC-06-24); publish failure rolls back. Done when:pnpm --filter @ever-works/k8s-plugin test -- app-deployeris green and every row of spec FR-26's table has a test named after it. -
T13. Status, scale, logs, destroy, cluster check. Create
packages/plugins/k8s/src/app/app-status.reader.ts,app-lifecycle.ts,app-cluster-check.ts(new):getAppStatus(components, restarts, lastOOMKilledwithin 24 h, jobs, cron last schedule/success),scaleApp(replicas + CronJobsuspend; onresumeit takes the optionalresumeChecks{ smoke, deadlines }, waits withisComponentRolledOut/componentDeadlineSecondsfor the phase-3 rollout and then runs the phase-5 in-cluster smoke, resolvingAppScaleResult— APW06-G03),getAppLogs(≤ 500 lines, 262 144 bytes, secret redaction by value ≥ 8 chars),runAppJob(live Deployment's image andenvFrom; refuses while a Job of the same name is active; therunner: 'smoke'variant runs the App spec's checks through the runner Job),prepareAppNamespace(plan §4.2 subset: namespace + pod-security labels + ownership check, ServiceAccount, LimitRange with thelimitrange_forbiddenwarning, and the three baseline policies whenisolationis true; idempotent; never drawsew-allow-ingress,ew-allow-depsor adep-*policy — GAP-06),publishAppHosts(re-applies only theIngressby reusingrenderIngressand returns the observedingressAddress; zero other applies — APW06-G03),destroyApp(never PVCs,ever-works.io/dependencyobjects or — while such objects remain —ew-default-denyunlessdeleteVolumes; namespace deleted only whendeleteVolumes; verification namespaces deleted whole),checkAppCluster(plan §6.3 permission list, ingress classes, controller namespace, issuers, storage classes, the ingress controller Service's address asingressAddress, and the fingerprint inside the result — GAP-09 / APW06-G03). Test:packages/plugins/k8s/src/app/__tests__/app-status.reader.spec.ts— every FR-46 field is filled from a fake cluster (ACC-06-31).packages/plugins/k8s/src/app/__tests__/app-lifecycle.spec.ts—scaleApp('pause')sets replicas 0 andsuspend: true(ACC-06-35);scaleApp('resume', …)resolves anAppScaleResultcarrying components and smoke, and reportsfailure.codewithout rolling back;destroyAppwithdeleteVolumes: falseissues zero PVC deletes, zero dependency deletes, keepsew-default-denyand zero namespace deletes (ACC-06-18, ACC-06-36);getAppLogsreplaces a secret value appearing mid-line with its name and returns no value (ACC-06-34);runAppJobuses the live image digest and a second call while active is refused (ACC-06-37);prepareAppNamespacepersists nothing itself but renders the §4.2 subset and is idempotent on a second call;publishAppHostsapplies oneIngressand zero Deployments (ACC-06-25).packages/plugins/k8s/src/app/__tests__/app-cluster-check.spec.ts— each missing required permission is named, withrequired: trueblocking Save (ACC-06-05); optional permissions listed as optional; the result carriesfingerprintandingressAddressand never writes the runtime-stateclusterFingerprint. Done when:pnpm --filter @ever-works/k8s-plugin test -- app-status.reader app-lifecycle app-cluster-checkis green and results contain no secret values (asserted with a sentinel value). -
T14. Plugin wiring. Modify
packages/plugins/k8s/src/k8s.plugin.ts—readonly supportsApps = trueand ten methods delegating tosrc/app/*(deployApp,getAppStatus,runAppJob,destroyApp,scaleApp,getAppLogs,checkAppCluster,prepareAppNamespace,publishAppHosts), each runningassertSupportedKubeconfig+pinKubeconfigServeron every credential (thek8splugin never servesever-works-apps; R-5).deploy()and every existing method are untouched. Modifypackages/plugins/k8s/src/index.ts— export the App renderer (pure functions, for APW-10's in-zone controller) and guard entry points. Test: extendpackages/plugins/k8s/src/__tests__/k8s.plugin.spec.ts—supportsApps, delegation, the guard runs on every App method, anAppTargetRefwith targetever-works-appsis refused, and a snapshot provingdeploy()renders the same manifests as before for the existing fixture (ACC-06-43). Done when:pnpm --filter @ever-works/k8s-plugin testis green. -
T15. Kind e2e. Create
packages/plugins/k8s/src/__tests__/e2e/app-runtime.e2e.spec.ts(new) per plan §12.1 using a non-root nginx image asweb, a busyboxworker, amigratecommand job, a first-deployhttpjob, anhttpcron every minute, a 100Mi PVC, smoke checks; second Deployment with a crashing command → rolled back; isolation reported not enforced; App Work deletion without data; a verification namespace. Allow-list127.0.0.1/32viaEVER_WORKS_APPS_CLUSTER_PRIVATE_ALLOWLISTfor kind. Modify.github/workflows/k8s-e2e.ymlonly if the new spec needs a longer job timeout (≤ 30 min). Test:pnpm --filter @ever-works/k8s-plugin test:e2eon kind — first Deployment Live with web, worker, migrate, first-deploy job, cron and volume (ACC-06-06); the loopback allow-list admits the kind API (ACC-06-03); the crashing Deployment rolls back and the Service still answers with the previous version (ACC-06-10); first-deploy Job completion precedes the Ingress creation and no first-deploy Job exists after the second Deployment (ACC-06-11); a changed env value restarts pods and an unchanged one does not (ACC-06-15); policies exist and isolation readsfalseon kindnet (ACC-06-17); the PVC survives redeploy, pause and remove-without-data (ACC-06-18); pause scales to 0 within 120 s (ACC-06-35); remove keeps the PVC (ACC-06-36); App Work deletion keeps the PVC andew-default-denyand removes every workload (ACC-06-45); the verification namespace has no Ingress or PVC and is gone after destroy (ACC-06-48). Done when: thek8s-e2e.ymlworkflow is green andcluster.e2e.spec.tsis unchanged.
P1.3 — Data model
-
T16.
WorkDeploymentcolumns and states. Modifypackages/agent/src/entities/work-deployment.entity.ts— appendbuildId,componentStatuses,smokeResult,appTarget,appRender(plan §7.1); extendisTerminal()withROLLED_BACK,SUPERSEDED. Test:packages/agent/src/entities/__tests__/work-deployment.entity.spec.ts(create if absent) — columns nullable, no pre-existing column changed,isTerminaltruth table. Done when:pnpm --filter @ever-works/agent test -- work-deployment.entityis green andpnpm --filter @ever-works/agent buildis clean. -
T17.
WorkAppRuntimeStateentity and repository. Createpackages/agent/src/entities/work-app-runtime-state.entity.ts(new) (plan §7.2 — nolicenseAttestationcolumn, R-3; deletion columns for R-15; scope columns without relation decorators) andpackages/agent/src/database/repositories/work-app-runtime-state.repository.ts(new):getOrCreate(workId),claimDeployLock(workId, deploymentId, staleAfterS),releaseDeployLock(workId, deploymentId),setQueued(...),selectForHealthPoll(limit),recordHealth(...),saveSnapshot(...),claimDeletion(workId, opts),recordDeletionAttempt(workId). Added by the 2026-09-17 fix pass (plan §7.2). The entity also carriescancelRequestedAt/cancelRequestedByUserId(the flaghooks.isCancelled()reads, cleared byreleaseDeployLockin the same UPDATE),pendingDomainRebuildBuildId(with an atomicclearPendingDomainRebuild(workId, buildId)) andupstreamSyncJudgedToSha.claimDeployLockrequirespaused = falseanddeletionRequestedAt IS NULLin addition to the null lock.prepare-namespace(T69) and §5.6 both persistnamespace/clusterFingerprint; acluster-checknever writes them. FR-63 — derive the target on first read (this closes APW-01's recorded cross-epic requirement; without it a Work created for Your cluster staysnoneand refuses to deploy).getOrCreate(workId)must settargetfrom the Work's creation-time choice:your-clusterwhen the Work's persisteddeployProvidernames a deployment plugin withsupportsApps === true, the managed target when it isever-works-apps, elsenone. Never leave the column's default in place for a Work that was created with a target, and never overwrite a target the owner has since changed. Modifypackages/agent/src/entities/index.ts,packages/agent/src/database/_entity-names.ts,packages/agent/src/database/_entities-inventory.ts— register the entity next toWorkDeployment. Test:packages/agent/src/database/repositories/__tests__/work-app-runtime-state.repository.spec.ts(new) — lock claim is atomic under two concurrent claims (one wins), stale lock reclaimed after 7 260 s, release only by the holder;claimDeletionrefuses while a deploy lock is held and succeeds once; the entity metadata has no column namedlicenseAttestation(ACC-06-39); andgetOrCreatederivesyour-clusterfor a Work whosedeployProvideris asupportsAppsplugin,ever-works-appsfor the managed provider,noneotherwise, and does not clobber a target that was already set. Done when:pnpm --filter @ever-works/agent test -- work-app-runtime-state.repositoryis green and the database drift specs pass without editing their counts by hand beyond the new entity. -
T18. Migrations. Create
apps/api/src/migrations/1792060000000-ExtendWorkDeploymentsForApps.tsandapps/api/src/migrations/1792060100000-CreateWorkAppRuntimeStates.ts(new) (plan §7.3), generated withcd apps/api && pnpm typeorm migration:generate …and reviewed by hand. Test:apps/api/src/migrations/__tests__/ExtendWorkDeploymentsForApps.spec.tsandapps/api/src/migrations/__tests__/CreateWorkAppRuntimeStates.spec.ts—up()has noDROP/rename of pre-existing columns;down()drops only whatup()created; existing rows readbuildId = null; the runtime-state table carriescancelRequestedAt,cancelRequestedByUserId,pendingDomainRebuildBuildIdandupstreamSyncJudgedToSha, and rows written before the column existed readnullfor all four. Done when:pnpm --filter ever-works-api test -- ExtendWorkDeploymentsForApps CreateWorkAppRuntimeStatesis green and a fresh database and one with existingwork_deploymentsrows both migrate.
P1.4 — Agent services
-
T19. Config. Modify
packages/agent/src/config/index.ts— addeverWorks.apps(getDomain,getMaxPerUserdefault 3,getDnsZoneId,getDnsApiToken,isClusterWorkerIsolated,getClusterPrivateAllowlist) with the apps-domain relation validation of plan §8.3 (P1 per R-16). Test: extendpackages/agent/src/config/config.spec.ts— apps domain equal to / under / parent ofEVER_WORKS_DOMAIN→getDomain() === null(ACC-06-27); invalid CIDR entries dropped with a warning and a valid one returned (ACC-06-03). Done when:pnpm --filter @ever-works/agent test -- config.specis green and unset env keeps every getter at its documented default. -
T20.
AppRuntimeFacadeService. Createpackages/agent/src/facades/app-runtime.facade.ts(new) — resolves, throughPluginRegistryServiceand by capability only (R-5): foryour-clusterthe deployment plugin for the Work'sdeployProviderwithisAppDeploymentPluginand withoutapps-tier; forever-works-appsthe enabled deployment plugin withisAppDeploymentPluginandapps-tier, only whileAppsTierPolicy.isOpen(). Resolves the credential per plan §5.6 step 3 (custom-kubeconfigonly foryour-cluster;AppsTierPolicy.resolveClusterCredentialonly forever-works-apps). Added (APW06-G02). It is constructed in every process that importsFacadesModule, so it cannot refuse at construction: every method call throwsAPP_CLUSTER_IO_IN_APIunlessisAppClusterWorkerContext()is true. Createpackages/agent/src/app-runtime/worker-context.ts(new) exportingmarkAppClusterWorkerContext()andisAppClusterWorkerContext()— a process-level flag, not an env var. Only theTriggerAppRuntimeModulebootstrap provider (T71) calls the marker, inonModuleInit. Modifypackages/agent/src/facades/facades.module.tsandpackages/agent/src/facades/index.tsto provide it. Test:packages/agent/src/facades/__tests__/app-runtime.facade.spec.ts(new) — constructing it outside the worker is allowed and the first call throwsAPP_CLUSTER_IO_IN_API(ACC-06-04); the API module graph never imports the marker provider (a static import scan overapps/api/src); never readsEVER_WORKS_K8S_WORKS_KUBECONFIG,EVER_WORKS_K8S_WORKS_SHARED_KUBECONFIGorEVER_WORKS_APPS_MANAGED_ENABLED(env spies); refusesk8s-works-sharedsettings for kindapp;validateClusterSourceForOwnercalled with the data-repository owner; forever-works-appstheapps-tierplugin receives the tier credential and thek8splugin spy receives nothing (ACC-06-49). Done when:pnpm --filter @ever-works/agent test -- app-runtime.facadeis green and no'k8s'string literal is added outsidepackages/plugins/k8s/. -
T21. Preconditions and license gate. Status 2026-09-26: Status notes. Create
packages/agent/src/app-runtime/app-deploy-preconditions.service.tsandpackages/agent/src/app-runtime/app-license-gate.ts(new) per plan §5.1–§5.2 — the license gate readsAppLicenseService.getHostingEligibility(workId)(APW-03; typed fake until it lands) and stores nothing (R-3). Test:packages/agent/src/app-runtime/__tests__/app-deploy-preconditions.service.spec.ts— one test per precondition code; two unset required values and a provisioning dependency produce three named entries and no dispatch; no green Build for the head returnsno_green_build_for_headwithlatestGreenBuildId(ACC-06-19); the App spec is read at the Build's commit, not the latest applied commit (ACC-06-20); targetnone→target_none(ACC-06-01); a pending dependency gives exactly onedependency_not_readyentry naming it and exactly oneAppDependenciesService.ensureReadyForDeploycall, which is what dispatches provisioning (GAP-05, ACC-06-54); strategydockerfile/autorequires a green Build while strategyimagedoes not and never yieldsno_green_build*,nothing_to_deployis returned for strategynone, andimage_not_pinnedis returned only onever-works-appsfor a tag-only reference (FR-64, ACC-06-52/-53); an entry sourced fromdomains.primary.*with no primary host yieldsprimary_domain_missingnaming it, with theprimary_url_inclusterwarning rather than a refusal (GAP-09); a dispatcher that resolvesnullor lacksdispatchApp*yieldsworker_not_isolatedwith noWorkDeploymentrow (APW06-G02, ACC-06-55).packages/agent/src/app-runtime/__tests__/app-license-gate.spec.ts—yourCluster: 'attestationRequired'→license_attestation_missing;managedreason (amber without agreement, red) →license_blocks_target; a changed eligibility after a license change re-requires attestation; the gate never writes to any repository (ACC-06-39). Done when:pnpm --filter @ever-works/agent test -- app-deploy-preconditions.service app-license-gateis green and the service never throws for an unmet precondition. -
T22. Render input builder. Create
packages/agent/src/app-runtime/app-render-input.builder.ts(new) — spec at Build commit, env viaAppRuntimeEnvSource(withbuildCommitSha,internalUrls, primary URL/host), pull credential viaAppImagePullCredentialSource, dependency egress resolved to/32or/128CIDRs, hosts from T26, policy. Added (APW06-G08 / APW06-G04). It passestarget: AppDeployTargetintoAppRuntimeEnvSource.resolve; forbuild.strategy: imageit takes the image from the spec atspecCommitSha, passesbuildCommitSha: nulland never callsAppImagePullCredentialSource; for every other strategy it passes the Build's commit and resolves the pull credential. Test:packages/agent/src/app-runtime/__tests__/app-render-input.builder.spec.ts(new) — never includesGH_TOKEN,PLATFORM_API_SECRET_TOKEN,PLATFORM_SYNC_SECRETor any key the env source did not return; the image pull block equals exactly the port's credential and the owner's Git token sentinel appears nowhere in the input (ACC-06-16); image reference andspecCommitShacome from the same Build (ACC-06-20);targetreaches the env source for every target; under strategyimagethe pull-credential port records zero calls,buildCommitShaisnulland the reference is the spec's own (ACC-06-52);DeployService.collectServerSideRuntimeEnvandresolveGhcrReadTokenare not called (spies). Done when:pnpm --filter @ever-works/agent test -- app-render-input.builderis green and the builder has no dependency onapps/api. -
T23. Public smoke service. Create
packages/agent/src/app-runtime/app-public-smoke.service.ts(new) — requests with manual redirects, 1 MiB body cap, classificationdns_not_pointing/tls_not_ready/unreachable/check_failed, windows 600 s / 180 s, retry every 10 s. Test:packages/agent/src/app-runtime/__tests__/app-public-smoke.service.spec.ts(new) — local HTTPS server with a mismatched certificate →tls_not_ready; resolver returning another address →dns_not_pointing, both reported as warnings not failures (ACC-06-13); body mismatch →check_failedwith the found string ≤ 200 chars. Done when:pnpm --filter @ever-works/agent test -- app-public-smoke.serviceis green and no response body beyond 200 characters leaves the service. -
T24. Deploy request service. Create
packages/agent/src/app-runtime/app-deploy-request.service.ts(new) — preconditions, lock claim, latest-wins queue (SUPERSEDED),WorkDeploymentcreation, dispatch; manual vs Build-triggered vsspec-appliedvs domain-change vs rollback (skipPreDeployJobsdefault true); cluster-change confirmation; refuses whiledeletionRequestedAtis set. Added (APW06-G02, APW06-G04). The request body accepts{ buildId?, specCommitSha?, confirmClusterChange? }; sendingbuildIdfor strategyimagereturns400 build_not_applicable; a strategy-imagerequest creates the row withbuildId: nulland the spec commit, and its queue entry keepsqueuedBuildIdnull; a queued request from the other strategy isSUPERSEDED. The dispatcher is checked before the lock claim: when it resolvesnull,isEnabled()is false or the active runtime lacksdispatchApp*, the request returns422 worker_not_isolatedand creates no row (APW06-G02). Test:packages/agent/src/app-runtime/__tests__/app-deploy-request.service.spec.ts(new) — second manual request →APP_DEPLOY_IN_PROGRESS; three Build-triggered requests during one run → one queued, oneSUPERSEDED(ACC-06-21); a rollback request carries the old Build (or the recorded digest and spec commit under strategyimage) and its commit andskipPreDeployJobs: true(ACC-06-23); request budget ≤ 2 s with a slow dispatcher mocked at 5 s; deleting App Work →app_work_deleting; a missing dispatcher →worker_not_isolated, zero rows, zero cache entries (ACC-06-55). Done when:pnpm --filter @ever-works/agent test -- app-deploy-request.serviceis green and the service never calls the plugin. -
T25.
app-deployorchestrator. Createpackages/agent/src/app-runtime/app-deploy.orchestrator.ts(new) per plan §5.6 (states, hooks, snapshot, first-deploy bookkeeping per cluster fingerprint, lock release, dequeue, event emission order). Added by the 2026-09-17 fix pass. It resolves its credential throughAppRuntimeTargetResolver(T69), emits throughAppRuntimeEventSink(T70) rather thanEventEmitter2, resolves animage-strategy Deployment throughAppImageReferenceResolver(T72) beforeprepare, calls APW-07'sonAppRemovedon both removal paths (plan §5.6 step 8, T70'sremoveop), and runs the upstream-sync verdict of plan §5.6 step 9 (APW06-G10) after the terminal andapp.smoke.*events:APP_PROVISION_EVENTS_PORT@Optional(),upstreamSyncJudgedToShaset whether the Deployment passed or failed,smokeFailedAfterUpstreamSynccalled only when the Deployment emittedapp.smoke.failed(in-cluster failure endingROLLED_BACK/ERROR, or a publiccheck_failed— neverdns_not_pointing,tls_not_readyorunreachable), rollback Deployments skipped, and a throwing port never delaying lock release. Test:packages/agent/src/app-runtime/__tests__/app-deploy.orchestrator.spec.ts(new) — outcome → state mapping table (rolled-back→ROLLED_BACK,succeeded-with-warnings→READY+ warnings); lock released on every outcome including thrown errors; queued Build requested after release; event orderstarted → job.* → terminal → smoke.*;rollback-failedtriggers the urgent notification producer (ACC-06-24); a thrown plugin error endsERROR (worker_failed); a cancelled or quarantined result is storedCANCELEDwithappRender.cancelledBy: 'quarantined'(APW06-G05, ACC-06-55); the upstream verdict's eight cases (fast-forward range plus failing smoke → one call; the sametoShatwice → one call; pass-then-fail → none; publiccheck_failedon aREADY+ warnings Deployment → one;tls_not_readyalone → none; rollback → none;isAncestorCommitnull and commit ≠toSha→ none; an unbound or throwing port leaves state and lock release unchanged) (APW06-G10); under strategyimagethe resolver runs once beforeprepareand a 404/401/timeout endsERRORwith its own code (ACC-06-52). Done when:pnpm --filter @ever-works/agent test -- app-deploy.orchestratoris green and no outcome leaves a held lock. -
T26. Hosts and domains. Create
packages/agent/src/app-runtime/app-hosts.service.tsandpackages/agent/src/app-runtime/app-domains.service.ts(new) per plan §8.1, §8.2, §8.4 (custom domains, primary order custom-then-managed, URL scheme per TLS mode). Modifypackages/agent/src/facades/deploy.facade.ts— early kind-appbranch ingetDomains,addDomain,removeDomain,verifyDomaindelegating toAppDomainsService. Test:packages/agent/src/app-runtime/__tests__/app-hosts.service.spec.ts(new) — unverified domain never inhosts; verify →ingress-reconciledispatched with no Deployment requested (ACC-06-25); primary change withrestartrequests a Deployment of the current Build and withrebuildcallsAppBuildsService.requestRebuildand stores the returned id inpendingDomainRebuildBuildId, while a rate-limited or blocked request stores nothing and requests no Deployment (ACC-06-26, APW06-G11); under strategyimagerebuildbehaves asrestartwith therebuild_not_applicablewarning (ACC-06-52). The custom-domain verify path usesruntimeState.ingressAddress, whichcheckAppClusternow records before the first Deployment (GAP-09).packages/agent/src/app-runtime/__tests__/app-domains.service.spec.ts(new) — DNS guidanceAfor an IP andCNAMEfor a hostname.packages/agent/src/facades/__tests__/deploy.facade.spec.tsstays green unchanged. Done when:pnpm --filter @ever-works/agent test -- app-hosts.service app-domains.service deploy.facadeis green andmergeCustomDomainHostsis untouched. -
T27. Health service. Create
packages/agent/src/app-runtime/app-health.service.ts(new) per plan §9.3. Test:packages/agent/src/app-runtime/__tests__/app-health.service.spec.ts(new) — 4 failing polls → no notification, 5th → one; failures for 7 h → 2 notifications; 3 passes → recovery only after a failure notification (ACC-06-32); 10 unreachable →unreachablenotdownwith one notification (ACC-06-33); paused and deleting App Works skipped; per-cluster concurrency 5. Done when:pnpm --filter @ever-works/agent test -- app-health.serviceis green and a single poll never exceeds 20 s (fake timers). -
T28. Events and Activity. Create
packages/agent/src/events/app-runtime.events.ts(new); Modifypackages/agent/src/events/index.ts. Createapps/api/src/app-runtime/app-runtime-event-relay.service.ts(new) and its worker-facing proxy (APW06-G02):emit(name, payload)accepts only names in theapp.*catalogue, runs the ACC-06-41 forbidden-key check and re-emits throughEventEmitter2, so the existing listener writes Activity unchanged; an unknown name is refused. Register it inapps/api/src/trigger/trigger-internal.controller.tsand inpackages/tasks/src/trigger/worker/modules/trigger-internal.module.ts(createRemoteProxy). Modifypackages/agent/src/entities/activity-log.types.ts—APP_DEPLOY = 'app_deploy',APP_JOB = 'app_job',APP_SMOKE = 'app_smoke',APP_HEALTH = 'app_health'(R-2). Modifyapps/api/src/activity-log/activity-log.listener.ts— one@OnEventper event in plan §9.4 withaction= the dotted name andactionType= the family. Test:packages/agent/src/app-runtime/__tests__/app-runtime.events.spec.ts(new) — payload types have novalue/env/log/kubeconfig/tokenfields (compile-time + runtime key check with sentinel values) (ACC-06-41); the relay refuses an unknown name and writes one Activity row for a known one (APW06-G02); extendapps/api/src/activity-log/activity-log.listener.spec.ts— each event writes its familyactionType, neverdeployment; switching isolation off records a warning row (ACC-06-17). Done when:pnpm --filter @ever-works/agent test -- app-runtime.eventsandpnpm --filter ever-works-api test -- activity-log.listenerare green; Activity summaries name components/jobs/checks only. -
T29. Notifications. Modify
packages/agent/src/notifications/core-event-catalogue.ts—app_deploy_failed,app_unhealthy,app_recovered,app_cluster_unreachable(plan §9.4). Modifypackages/agent/src/notifications/notification.service.ts— the four producers withdeduplicationKeyapp-health:<workId>/app-deploy:<deploymentId>andactionUrlto the Deploy tab. Test:packages/agent/src/notifications/__tests__/event-registry-coverage.spec.tsgreen;packages/agent/src/notifications/__tests__/app-runtime-notifications.spec.ts(new) — a rollback-failed producer call creates an urgent notification with the Deploy tab link (ACC-06-24); dedupe keys as stated. Done when:pnpm --filter @ever-works/agent test -- event-registry-coverage app-runtime-notificationsis green and urgent rows ship in-app + email by the catalogue's defaults rule. -
T30 (parallel with T29). Source offer. Create
packages/agent/src/app-runtime/app-source-offer.ts(new) —requiredand the URL from APW-03'sgetHostingEligibility(workId).sourceOffer(the shared C3 condition: obligation and (link or ahead > 0)), the deployed commit substituted into the URL,license.sourceOfferUrlwhen private;privateWithoutUrlwarning. Test:packages/agent/src/app-runtime/__tests__/app-source-offer.spec.ts(new) — truth table of obligation × link × ahead × private × url; the URL targets the deployed commit, not the branch head (ACC-06-30). Done when:pnpm --filter @ever-works/agent test -- app-source-offeris green andEVER_WORKS_SOURCE_URLis set only when the offer applies.
P1.5 — Background jobs
-
T31. Dispatchers. Create
packages/agent/src/tasks/app-deploy-dispatcher.ts,app-deploy.types.ts,app-smoke-dispatcher.ts,app-smoke.types.ts,app-cluster-op-dispatcher.ts,app-cluster-op.types.ts(new) (ops incl.prepare-namespace,delete-app-work,verification-deploy,verification-status,verification-destroy, with the typed verification payloads of plan §9.2). Modifypackages/agent/src/tasks/index.tsandpackages/agent/src/tasks/_tasks-symbols.ts(alphabetical). Modifypackages/agent/src/tasks/job-runtime.providers.tsandpackages/tasks/src/trigger/trigger.module.ts— bind the three dispatchers to the active runtime. Added (APW06-G02).TriggerServicegainsdispatchAppDeploy,dispatchAppSmokeanddispatchAppClusterOpthat propagate errors and never returnnullon a throw;dispatchersFromTenantClientmirrors them with the propagate shapekb-reembed-workuses, notsoftDispatch; their ids joinTASK_IDS, the three symbols joinDISPATCHER_SYMBOLS, and the arity pin inpackages/agent/src/tasks/__tests__/job-runtime.providers.spec.ts:134-143is updated — count it off the merged list (14 at HEAD after AW-22), not by adding branch numbers. Test:packages/agent/src/tasks/tasks.spec.tsgreen;packages/agent/src/tasks/__tests__/app-cluster-op-dispatcher.spec.ts(new) — dispatchers refuse in production whenisClusterWorkerIsolated()is false (worker_not_isolated) (ACC-06-04); a registry that resolvesnullreturnsworker_not_isolatedwith noWorkDeploymentrow and no in-process call, in every environment (APW06-G02); the tenant dispatcher propagates a thrown dispatch error; the verification payloads round-trip through the dispatcher. Done when:pnpm --filter @ever-works/agent test -- tasks.spec app-cluster-op-dispatcheris green, the arity spec is updated rather than loosened, and no call site imports@trigger.dev/sdk. -
T32. Trigger tasks. Create
packages/tasks/src/tasks/trigger/app-deploy.task.ts(maxDuration: 7200,retry.maxAttempts: 1,onFailure),app-smoke.task.ts(maxDuration: 900),app-cluster-op.task.ts(maxDuration: 900; the dispatcher passesmaxDuration: 3600forverification-deploy),app-health-poll.task.ts(schedules.task,cron: '* * * * *', guarded byDistributedTaskLockService) — all on queueapp-cluster-io(concurrencyLimit: 20). Every one of them bootsTriggerAppRuntimeModule(T71), which is what sets the worker-context flag T20 checks;app-cluster-op.task.tsdelegates to the router (T70). Createpackages/tasks/src/tasks/trigger/app-runtime-local-worker.ts(new) — theapp-runtime:local-workerentry point (an npm script inpackages/tasks/package.json) that bootsTriggerAppRuntimeModuleand drains the same exported task run functions from a local queue. It refuses to start whenNODE_ENV=production. Modifypackages/tasks/src/tasks/trigger/index.ts. Test:packages/tasks/src/__tests__/app-deploy.task.spec.tsandapp-health-poll.task.spec.ts(new) with mocked orchestrators —onFailuremarksERROR (worker_failed)and releases the lock; the poll task exits when the lock is held; every task declares queueapp-cluster-ioand bootsTriggerAppRuntimeModule; the local-worker entry exits non-zero underNODE_ENV=production. Done when:pnpm --filter @ever-works/trigger-tasks testis green and the tasks package builds and lists the four ids.
P1.6 — API
-
T33. App runtime module and controller. Create
apps/api/src/app-runtime/app-runtime.module.ts,app-runtime.controller.ts,dto/*.dto.ts(new) with every route of plan §9.1 (incl.GET :id/app-deletion-preview, thePOST :id/app-target/checkbody and the response shapes of plan §9.1's "Response shapes" block);AppWorkAccessService.resolve(view for GET, edit for the rest), called before the kind check; throttles; 202 shapes; 422APP_DEPLOY_PRECONDITIONSandAPP_TARGET_REFUSED; typed-slug check fordeleteData; 409APP_WORK_DELETING. Createapps/api/src/app-runtime/app-runtime-ports.module.ts(new) — the@Global()module of plan §9.8 that provides and exportsAPPS_TIER_POLICY,APP_IMAGE_PULL_CREDENTIAL_SOURCE,APP_RUNTIME_ENV_SOURCE,APP_RUNTIME_TARGET,APP_RUNTIME_EVENT_SINK,APP_VERIFICATION_SINKandAPP_WORK_DELETION_PORTwith their disabled/unavailable defaults (APW06-G12). It is the single provider for each token; the owning epics replace the binding there (T44 replaces the tier policy binding in this file, never inapp-runtime.module.ts). Modifyapps/api/src/api.module.ts— import both modules. Test:apps/api/src/app-runtime/app-runtime.controller.spec.ts— every route × {202/200, 404 foreign, 403 viewer on actions, 409, 422, 429} (ACC-06-40); unmet preconditions → 422 listing every code and nothing dispatched (ACC-06-19); manual deploy during a run → 409 (ACC-06-21);GET app-statusreturns every FR-46 field,stale: trueafter 180 s, the in-flightops[]entries and refresh 429 inside 15 s (ACC-06-31, ACC-06-55);app-lifecycle removewithdeleteDataand a wrong slug → 422, withoutdeleteData→ 202 keeping data (ACC-06-36);app-jobs/:name/runwhile active → 409 (ACC-06-37);app-deletion-previewlists names and sizes only; kind ≠app→ 400 onPOST :id/deploy;app-target/checkwith a kubeconfig performs one settings write and onecluster-checkdispatch with no kubeconfig in the payload, while spies onPluginValidationService.tryValidateConnectionand on the plugin'svalidateConnectionrecord zero calls (ACC-06-02, ACC-06-56); aPUT app-targetthat changes the target or fingerprint callsreconcileonce and dispatchesprepare-namespaceonce, while an unchanged PUT calls neither (GAP-05, ACC-06-54); the access cases of ACC-06-57 with the realAppWorkAccessServiceover stubbed repositories.apps/api/src/app-runtime/__tests__/app-runtime-ports.module.spec.ts(new) — the module is global and exports all seven tokens; the deletion-port provider hasuseExistinganduseClassundefined andinjectequal to[ModuleRef]; a reduced graph that does not import the module still injects a defined@Optional() @Inject(APP_WORK_DELETION_PORT)whoserequestDeletiondelegates toAppRuntimeDeletionService(APW06-G12). Done when:pnpm --filter ever-works-api test -- app-runtime.controller app-runtime-ports.moduleis green and no controller method awaits a plugin call. -
T34. Delegation from existing routes. Modify
apps/api/src/plugins-capabilities/deploy/deploy.service.ts— kind-appbranch next to thereporefusal delegating toAppDeployRequestService.request()before any website-repository work. Modifyapps/api/src/plugins-capabilities/deploy/deploy.controller.ts—deployandrollbackdelegate for kindappand skipdeploymentVerifier.startVerification. Modifyapps/api/src/plugins-capabilities/deploy/managed-subdomain.service.ts— kindappdelegates to T48's branch. Modifyapps/api/src/plugins/plugins.controller.ts(added, APW06-G01) — when the Work's kind isappand the plugin has thedeploymentcapability,updateWorkPluginSettingsskipsPluginValidationService.tryValidateConnectionand returnsvalidation: null, because that call reachesKubernetesPlugin.validateConnectionand dials the pasted cluster from the API process before the §6.1 guard. Non-app Works are byte-identical to today. Test: extendapps/api/src/plugins-capabilities/deploy/deploy.service.spec.tsanddeploy.controller.spec.ts— kindappperforms zero Actions secret pushes and zero workflow dispatches; every existing test unchanged (ACC-06-43). Extendapps/api/src/plugins/plugins.controller.spec.ts— PATCH settings on a kind-appWork callstryValidateConnection0 times andKubeConfig.loadFromString0 times; on a website Work it is called once, unchanged (ACC-06-56). Done when:pnpm --filter ever-works-api test -- deploy.service deploy.controller plugins.controller deploy.e2eis green withdeploy.e2e.spec.tsunchanged. -
T35. Build-succeeded trigger and spec-applied trigger. Create
apps/api/src/app-runtime/app-build-succeeded.listener.ts(new) — consumes APW-05'sAppBuildFinishedEvent(CONTRACTS §2A) onapp.build.succeededfor the spec's deploy branch, whenautoDeploy(or the Build named bypendingDomainRebuildBuildId) and target ≠none. It requests a Build-triggered Deployment whendeployable: trueandbranchis the deploy branch, or adomain-changeDeployment whenbuildId === pendingDomainRebuildBuildId(then clearing the marker).deployable: falsenever requests a Deployment, andapp.build.failed/app.build.cancelledfor the marked Build clears the marker and requests nothing (APW06-G11). Createapps/api/src/app-runtime/app-spec-applied.listener.ts(new) andpackages/agent/src/app-runtime/app-image-reference.resolver.ts(new) (added, APW06-G04): the listener starts aspec-appliedDeployment when the strategy isimage,autoDeployis on, the target is notnoneand the changed blocks intersectAPP_IMAGE_REDEPLOY_BLOCKS; the resolver performs the anonymous registryHEAD, resolves a tag to a digest once, and returns theimage_not_found/image_private_unsupported/image_unresolvablecode. Test:apps/api/src/app-runtime/app-build-succeeded.listener.spec.ts(new) — other branches ignored;autoDeploy: falseignored unless a rebuild is pending; targetnonemakes zero deploy requests and zero facade calls, so Builds still complete with no cluster call (ACC-06-01); a non-deployable succeeded event on the deploy branch requests nothing.apps/api/src/app-runtime/app-spec-applied.listener.spec.ts(new) — changed blocks that change nothing that runs,autoDeployoff, targetnoneand strategydockerfileeach request nothing;packages/agent/src/app-runtime/__tests__/app-image-reference.resolver.spec.ts(new) — digest existence, tag resolution recorded asresolvedFromTag, and one case per registry answer (ACC-06-52, ACC-06-53). Done when:pnpm --filter ever-works-api test -- app-build-succeeded.listener app-spec-applied.listenerandpnpm --filter @ever-works/agent test -- app-image-reference.resolverare green and a green Build on the deploy branch produces exactly oneapp.deploy.started.
P1.7 — Web
-
T36. Client, actions, BFF. Create
apps/web/src/lib/api/app-runtime.ts(server-only),apps/web/src/app/actions/dashboard/app-runtime.ts,apps/web/src/app/api/works/[id]/app-status/route.ts(new; cookie auth like the existing deploy status route). Added (APW06-G01).apps/web/src/lib/api/app-runtime.tsgainscheckAppTarget(workId, { kubeconfig, kubeContext }), and the client types carry no kubeconfig or env value. Test:apps/web/src/app/api/works/[id]/app-status/route.unit.spec.ts(new) — 401 without session; passes throughstaleand every FR-46 field (ACC-06-31). Done when:pnpm --filter ever-works-web test -- app-status/routeis green and no kubeconfig or env value type exists in the web client types. -
T37. Deploy page branch, target card, connect dialog. Modify
apps/web/src/app/[locale]/(dashboard)/works/[id]/deploy/page.tsx— kindapprendersAppDeployPagebefore the website-repository redirect. Createapps/web/src/components/works/detail/deploy/app/AppDeployPage.tsx,AppTargetCard.tsx,ConnectClusterDialog.tsx,ClusterCheckResult.tsx,AppLicenseAttestationDialog.tsx(new) (spec §6.1–§6.2; target label None — don't deploy yet, R-12; Deploy now checkbox default on; attestation through APW-03'sPOST /api/works/:id/app-license/attest, R-3). Test:apps/web/src/components/works/detail/deploy/app/AppTargetCard.unit.spec.tsx(new) — target None renders the S1 copy and the label "None — don't deploy yet" (ACC-06-01); Ever Works Apps disabled with its reason when off or ineligible (ACC-06-38).ConnectClusterDialog.unit.spec.tsx(new) — Save disabled while a required permission is missing (ACC-06-05); refusal reasons render (ACC-06-02); switching isolation off shows the warning (ACC-06-17); it sends the kubeconfig only throughcheckAppTarget(T36) and the unit spec asserts the generic work-plugin-settings action is never called (APW06-G01, ACC-06-56).AppLicenseAttestationDialog.unit.spec.tsx(new) — a403renders "Only the App Work's owner can attest." and the request body is APW-03's (ACC-06-39). Done when:pnpm --filter ever-works-web test -- AppTargetCard ConnectClusterDialog AppLicenseAttestationDialogis green and non-app Works render the Deploy tab byte-identically (snapshot). -
T38. Live card, progress, components, smoke, jobs, cron. Create
apps/web/src/components/works/detail/deploy/app/AppLiveCard.tsx,AppDeployButton.tsx,AppDeployProgress.tsx,AppComponentsTable.tsx,AppSmokeResults.tsx,AppJobsList.tsx,AppCronList.tsx,AppSourceOffer.tsx(new). Test:apps/web/src/components/works/detail/deploy/app/AppDeployProgress.unit.spec.tsx(new) — polls every 3 s during a Deployment and 30 s otherwise and stops on unmount; the precondition list renders one row per code with its fix link (ACC-06-19).AppLiveCard.unit.spec.tsx(new) — "Last checkedminutes ago" after 180 s (ACC-06-31); Source link only when sourceOffer.required(ACC-06-30). Done when:pnpm --filter ever-works-web test -- AppDeployProgress AppLiveCardis green and every state and phase key renders translated text. -
T39. History, rollback, logs, danger zone. Create
apps/web/src/components/works/detail/deploy/app/AppHistoryTable.tsx,AppRollbackDialog.tsx,AppLogsDrawer.tsx,AppDangerZone.tsx(new). Test:apps/web/src/components/works/detail/deploy/app/AppHistoryTable.unit.spec.tsx(new) — Rollback shown only on Live rows among the last 20 with an existing image, and the dialog shows the disclaimer (ACC-06-23); animage-strategy row shows the short digest instead of a Build link (ACC-06-52).AppDangerZone.unit.spec.tsx(new) — remove with data enables only on the exact slug (ACC-06-36); pause disabled during a Deployment with the S27 copy (ACC-06-35).AppLogsDrawer.unit.spec.tsx(new) — nothing written to local storage. Done when:pnpm --filter ever-works-web test -- AppHistoryTable AppDangerZone AppLogsDraweris green. -
T40 (parallel with T39). Overview health card. Create
apps/web/src/components/works/detail/overview/AppHealthCard.tsx(new); Modifyapps/web/src/app/[locale]/(dashboard)/works/[id]/page.tsxto render it for kindapp. Test:apps/web/src/components/works/detail/overview/AppHealthCard.unit.spec.tsx(new) — out-of-memory line only within 24 h; Source link only when the offer applies (ACC-06-30). Done when:pnpm --filter ever-works-web test -- AppHealthCardis green and other kinds' Overview is unchanged. -
T41. i18n. Modify
apps/web/messages/en.json— sub-trees of plan §10.3; mirror keys into the 20 other locale files inapps/web/messages/. Test:node apps/web/scripts/sync-locale-parity.mjs && git diff --exit-code apps/web/messagesadds zero keys (ACC-06-44); a grep over the new leaves finds no.. Done when: both commands exit 0 and no string literal remains in the new components. -
T42. Playwright. Create
apps/web/e2e/flow-app-deploy-target.spec.ts,apps/web/e2e/flow-app-deploy-lifecycle.spec.ts,apps/web/e2e/flow-app-deploy-domains.spec.ts(new) (BFF-mocked). Wire the kind-cluster scenarios into APW-13'sapps/web/e2e/flow-app-works-kind-runtime.spec.tsrows for ACC-06-06, -10, -11, -12, -15, -18, -25, -35, -36, -45, -47, -48 (APW-13 owns that file; this task adds rows through its owner). Modifyapps/web/e2e/flow-app-deploy-target.spec.tsto cover the published-image strategy's two refusals and the dependency-before-first-Deployment ordering of ACC-06-54 through mocks (APW06-G04, GAP-06). Test:pnpm --filter ever-works-web test:e2e flow-app-deploy-— target: None copy (ACC-06-01), refused kubeconfig (ACC-06-02), missing permission blocks Save (ACC-06-05), Ever Works Apps refused while off (ACC-06-38); lifecycle: rollback dialog (ACC-06-23), pause/resume and refused pause during a Deployment (ACC-06-35), remove with typed slug (ACC-06-36); domains: verify publishes without a restart request (ACC-06-25), primary change restart/rebuild (ACC-06-26). Done when: the three specs pass locally and ine2e.yml, and the existingflow-work-deploy-*.spec.tssuites pass unchanged. -
T43. P1 docs and ship gate. Create
docs/features/app-runtime.md(new) — targets, service-account recipe with the plan §6.3 permission list, what gets applied, TLS modes, the managed subdomain, smoke, rollback, health, pause/remove, deleting an App Work, source offer. Modifyapps/docs/sidebarsPlatform.ts. Modifydocs/specs/features/app-works/TRACKER.mdAPW-06 row and, through its owner, the APW-06 table indocs/specs/features/app-works/ACCEPTANCE.mdwith ACC-06-01…49. Test:pnpm --filter ever-works-docs build; rootpnpm format:check,pnpm lint,pnpm type-check,pnpm test,pnpm build. Done when: every command exits 0 and spec ACC-06-01…49 except the P2 rows (-38 managed half, -49) and the P3 row (-42) are walked on a kind cluster.
P1.8 — Managed subdomain on Your cluster (moved from P2 by Resolution R-16)
-
T47. Apps-domain DNS. Create
packages/agent/src/ever-works-providers/apps-domain-dns.service.ts(new) (plan §8.3). Test:packages/agent/src/ever-works-providers/__tests__/apps-domain-dns.service.spec.ts(new) — apex unset → the platform domain is used as the default root and managed subdomains ARE offered (ACC-06-27, owner decision 2026-09-17); a malformed apex →nullprovider and managed subdomains hidden; a dedicated apex equal to, under or above the platform domain →nullwhile the shared default does not trip this check (ACC-06-27); configured →rootDomain()equals the apps domain; the apps-domain DNS configuration (EVER_WORKS_APPS_DNS_ZONE_ID/EVER_WORKS_APPS_DNS_API_TOKEN) is the only DNS configuration it reads — on the shared default it resolves the zone for the platform domain, and it never silently falls back to the platform's own DNS provider instance. Done when:pnpm --filter @ever-works/agent test -- apps-domain-dns.serviceis green andEverWorksDnsServiceis untouched. -
T48. Managed subdomain for App Works. Modify
apps/api/src/plugins-capabilities/deploy/managed-subdomain.service.ts— kind-appbranch: allocator with apps DNS ops, label ≥ 3,editableper target settings, rename → onChange (T26). Modifypackages/agent/src/app-runtime/app-hosts.service.ts— the managed subdomain as a host (primary only when no custom domain is primary, plan §8.1);dns-reconcileop inapp-cluster-opwritesA/CNAMEunproxied only for a public ingress address (guard from T11) onyour-cluster; the health poll re-validates every 10th poll and withdraws a non-public target. Create (added, APW06-G14).packages/agent/src/app-runtime/app-managed-host-root.resolver.ts(new) withAppManagedHostRootResolver implements ManagedHostRootResolver: for kindappit returnsconfig.everWorks.apps.getDomain()(null when unset or rejected by the plan §8.3 boot validation, in which case the launcher skips the managed-subdomain candidate and falls back to the latest READY Deployment's address); for any other kind it delegates to APW-11'sDefaultManagedHostRootResolver. Modifypackages/agent/src/app-runtime/index.ts(export) andapps/api/src/app-runtime/app-runtime-ports.module.ts/app-runtime.module.ts— provideMANAGED_HOST_ROOT_RESOLVERwithuseClass: AppManagedHostRootResolver, visible to APW-11'sAppLauncherService(a typed fake of the token until APW-11 lands). This is a P1.8 deliverable (R-16 moved the apps-domain subdomain to Wave 1); APW-11 tasks.md line 401 is updated by its owner from "APW-06 P2" to "APW-06 T48 (P1.8)". Test: extendapps/api/src/plugins-capabilities/deploy/managed-subdomain.service.spec.ts— kindappallocates under the configured apps domain, which defaults toEVER_WORKS_DOMAIN(so<slug>.ever.worksis the expected default and asserting it is the point of the case), and never under another Ever product's domain (ACC-06-27). Extendpackages/agent/src/app-runtime/__tests__/app-hosts.service.spec.ts— a private ingress address never produces a record, a changed public address updates it and a non-public one withdraws it (ACC-06-28); a firstyour-clusterDeployment's hosts include<slug>.<apps-domain>on the default apex and with a dedicated apex, while only a switched-off or invalid managed shape leaves custom domains alone (ACC-06-47).packages/agent/src/app-runtime/__tests__/app-managed-host-root.resolver.spec.ts(new) — kindappwith the apps domain set returns it; unset or invalid returns null; for kindappthe result never equals or ends withEVER_WORKS_DOMAIN; kindwebsitereturnsEVER_WORKS_DOMAIN(APW06-G14). Done when:pnpm --filter ever-works-api test -- managed-subdomain.serviceandpnpm --filter @ever-works/agent test -- app-hosts.service app-managed-host-root.resolverare green, every allocated label sits under the configured apps domain — the platform domain when it is the default, a dedicated apex when one is configured — and no App Work launcher address resolves underEVER_WORKS_DOMAIN.
Phase P2 — Ever Works Apps for verified Blueprints (Wave 2)
Delivers FR-7, FR-8, FR-22 and FR-40 on the managed target. Starts only after APW-10's P2 gate provides an
AppsTierPolicy and the apps-tier deployment plugin (R-5). T47 and T48 moved to P1.8 (R-16).
-
T44. Tier policy binding. Modify
apps/api/src/app-runtime/app-runtime.module.ts— bindAPPS_TIER_POLICYto APW-10's implementation when present; keepDisabledAppsTierPolicyotherwise. Test: extendapps/api/src/app-runtime/app-runtime.controller.spec.ts— with the disabled policyPUT app-target { target: 'ever-works-apps' }→ 422managed_disabled(ACCEPTANCE NEG-03); with scopeverified-blueprintsa provisioned App Work →managed_scope_unverified_blueprint;eligibilityfalse →managed_ineligible;podPolicy().runtimeClassName === null→managed_sandbox_unavailable(R-24) (ACC-06-38); the fake tier plugin receives zero calls in each refused case. Done when:pnpm --filter ever-works-api test -- app-runtime.controlleris green. -
T45. Quota. Create
packages/agent/src/ever-works-providers/ever-works-apps-quota.service.ts(new) + counter token (plan §9.5); call fromPUT app-targetand inside the lock claim. Test:packages/agent/src/ever-works-providers/__tests__/ever-works-apps-quota.service.spec.ts(new) — 4th App Work refused with the three counted Works named; two concurrent claims for the 3rd and 4th → exactly one succeeds; fails closed when the policy is open and the counter missing (ACC-06-38); paused App Works count, removed ones do not. Done when:pnpm --filter @ever-works/agent test -- ever-works-apps-quota.serviceis green. -
T46. Managed target through the tier (re-scoped by Resolution R-5). Modify
packages/agent/src/app-runtime/app-render-input.builder.ts— forever-works-appsfillpolicyfromAppsTierPolicy.podPolicy()/ingress()(restricted labels, quota,runtimeClassName,cpuLimitdefault, cron ≥ 5 minutes,requireIsolationEnforced,scaleFailedFirstDeployToZero,tls: 'edge') as desired state for the tier — the platform applies none of it. Modifypackages/agent/src/app-runtime/app-deploy.orchestrator.ts— hand the render input to theapps-tierdeployment plugin selected in T20 with the credential fromAppsTierPolicy.resolveClusterCredential; map tier refusals (isolation_not_enforced, admission refusals) toERRORwith codes. Modifypackages/plugins/k8s/src/app/app-manifest.renderer.tsandapp-security.tsonly to keep the managed variant as pure library output for APW-10's controller (no apply path). Createpackages/agent/src/app-runtime/app-image-config.reader.ts(new) (plan §4.4): image configUserread in the worker before handing over →managed_root_forbidden/image_user_unverifiableonever-works-apps. Test: extendpackages/plugins/k8s/src/app/__tests__/app-manifest.renderer.spec.tswith managed-variant fixtures (restricted labels, quota, runtime class). Extendpackages/agent/src/app-runtime/__tests__/app-deploy.orchestrator.spec.ts—ever-works-appscalls the tier plugin'sdeployApponce, thek8splugin fake records zero calls and zero applied objects, and a tierisolation_not_enforcedrefusal endsERROR(ACC-06-49).packages/agent/src/app-runtime/__tests__/app-image-config.reader.spec.ts(new) — OCI index, single manifest,Userempty /0/root/nextjs/10001, registry timeout; root refused before any tier call (ACC-06-08). Done when:pnpm --filter @ever-works/agent test -- app-deploy.orchestrator app-image-config.readerandpnpm --filter @ever-works/k8s-plugin test -- app-manifest.rendererare green, and ACCEPTANCE E2E-10 (b) assertions pass against the fake tier. -
T49. Web for P2. Modify
apps/web/src/components/works/detail/deploy/app/AppTargetCard.tsx,ConnectClusterDialog.tsx,apps/web/src/components/works/detail/deploy/SubdomainManagement.tsx(App branch copy for edge-owned managed hostnames), messages in all 21 files underapps/web/messages/. Test: extendapps/web/src/components/works/detail/deploy/app/AppTargetCard.unit.spec.tsx— managed target states disabled, scope refused, ineligible, sandbox unavailable, quota reached, available (ACC-06-38); extendapps/web/e2e/flow-app-deploy-target.spec.tswith the managed path against mocks. Done when:pnpm --filter ever-works-web test -- AppTargetCardandpnpm --filter ever-works-web test:e2e flow-app-deploy-targetare green. -
T50. P2 ship gate. Modify
docs/specs/features/app-works/TRACKER.md— tick APW-06 P2. Test: ACC-06-38 (managed half) and ACC-06-49 walked on stage behind APW-10's gate; rootpnpm format:check,pnpm lint,pnpm type-check,pnpm test,pnpm build. Done when: both criteria are recorded green and every root command exits 0.
Phase P3 — Any App Work on the managed tier, preview Deployments (Wave 3)
Delivers FR-7 (scope any), FR-52, FR-53.
-
T51. Scope
any. Modifypackages/agent/src/app-runtime/app-deploy-preconditions.service.ts— honourmanagedScope() === 'any'(the sandboxed-runtime precondition from T44 already applies since Wave 2, R-24). Test: extendpackages/agent/src/app-runtime/__tests__/app-deploy-preconditions.service.spec.ts— a provisioned App Work is accepted when scope isanyand the tier reports a runtime class, refused when it reports none (ACC-06-38). Done when:pnpm --filter @ever-works/agent test -- app-deploy-preconditions.serviceis green and scopeverified-blueprintsbehaviour is unchanged. -
T52. Preview Deployments. Modify
packages/agent/src/app-runtime/app-deploy-request.service.ts,app-render-input.builder.ts,app-hosts.service.ts— trigger on a green Build of a same-repository pull request head whentargetSettings.previewsand flagworks-app-previews;environment = preview,prNumber, namespace<ns>-pr<n>, hostpr-<n>-<label>.<apps-domain>, replicas 1, no CronJobs,emptyDirvolumes, env contextpreview; refuse whenAppRuntimeEnvSourcecannot provision preview dependencies (preview_dependencies_unavailable) and comment the reason on the pull request through the Git facade. Test: extendpackages/agent/src/app-runtime/__tests__/app-deploy-request.service.spec.ts— fork pull requests ignored; 4th concurrent preview refused; production dependencies never referenced (sentinel check on env values) (ACC-06-42). Done when:pnpm --filter @ever-works/agent test -- app-deploy-request.serviceis green and a preview never shares a namespace, Secret or dependency with the live app. -
T53. Preview garbage collection. Create
packages/tasks/src/tasks/trigger/app-preview-gc.task.ts(new) (*/5 * * * *) andpackages/agent/src/app-runtime/app-preview-gc.service.ts(new) — remove within 10 minutes of close/merge and after 72 h without a push;destroyApp(…, { deleteVolumes: true })for previews only, after APW-07 deprovisions preview dependencies. Test:packages/agent/src/app-runtime/__tests__/app-preview-gc.service.spec.ts(new) — closed 9 min ago kept, 11 min ago removed (tick cadence 5 min ⇒ ≤ 10 min); idle 71 h kept, 73 h removed (ACC-06-42); a query-level test proves GC never selects a non-preview Deployment. Done when:pnpm --filter @ever-works/agent test -- app-preview-gc.serviceis green. -
T54. Preview UI and ship gate. Modify
apps/web/src/components/works/detail/deploy/app/AppTargetCard.tsx(Previews toggle),AppHistoryTable.tsx(preview rows with pull request link), messages in all 21 locale files;docs/specs/features/app-works/TRACKER.md(tick P3). Createapps/web/e2e/flow-app-deploy-previews.spec.ts(new). Test:pnpm --filter ever-works-web test:e2e flow-app-deploy-previews— a preview row links its pull request and disappears after close (mocked) (ACC-06-42); ACC-06-42 walked on stage; root checks. Done when: the spec is green, ACC-06-42 is recorded and rootformat / lint / type-check / test / buildpass.
Cross-phase closing tasks
-
T55. Telemetry. Create
packages/agent/src/app-runtime/app-runtime.telemetry.ts(new) emitting, through the existing monitoring package, deploy requested/started/outcome (with phase and code), rollback outcome, smoke outcome by classification, health transitions, cluster check outcome, lifecycle actions, App Work deletion outcome. Modifypackages/agent/src/app-runtime/app-deploy.orchestrator.ts,app-health.service.ts,app-runtime-deletion.service.tsto call it. Test:packages/agent/src/app-runtime/__tests__/app-runtime.telemetry.spec.ts(new) — no payload contains a hostname, URL, namespace, env name, env value, kubeconfig fragment or log text. Done when:pnpm --filter @ever-works/agent test -- app-runtime.telemetryis green. -
T56. Security review pass. Modify
docs/specs/features/app-works/APW-06-app-runtime/plan.md§14 "Known gaps" if the review finds one. Test: a reviewer outside the epic walks plan §6, §4.4, §9.7 and §4.12 against the merged code; findings are recorded in the private operations repository, not in this public repository (program rule 10, R-14). Done when: every item in plan §14 is re-confirmed or a gap is added to plan §14 "Known gaps". -
T57. Statuses. Modify
docs/specs/features/app-works/APW-06-app-runtime/spec.md,plan.md, this file anddocs/specs/features/app-works/TRACKER.md—Implemented/Done. Test:rg -n "Status\*\*: \Implemented`|Status**: `Done`" docs/specs/features/app-works/APW-06-app-runtime` lists all three files. Done when: the three files and the TRACKER row show the shipped status.
Program audit follow-ups (added 2026-09-17)
-
T58. Deleting an App Work — runtime removal (Resolution R-15). Create
packages/agent/src/app-runtime/app-runtime-deletion.service.ts(new) per plan §9.7:preview(workId),requestDeletion({ workId, userId, deleteStoredData })returning{ status, target, reason? }with statuspendingordone— the binding of APW-01'sAPP_WORK_DELETION_PORT(packages/agent/src/app-works/app-work-deletion.port.ts) — and thedelete-app-workop handler (APW-07onAppWorkDeletingfirst, thendestroyAppwithdeleteVolumesequal todeleteStoredData; on Ever Works Apps theapps-tierplugin maps it to APW-10'sremoveWork(workId, { deleteData })— managed DNS record removal, Activityapp.deploy.removedwithkept[]/mayRemain[], 3 attempts over 15 minutes, then APW-01'scompleteAppWorkDeletion(workId)). Modifypackages/agent/src/app-runtime/index.ts(export),apps/api/src/app-runtime/app-runtime-ports.module.ts(provideAPP_WORK_DELETION_PORTwith a lazyuseFactory+inject: [ModuleRef]resolvingAppRuntimeDeletionService— neveruseExisting, because the port andWorkLifecycleServiceform a cycle) andpackages/tasks/src/tasks/trigger/app-cluster-op.task.ts(route the op through T70's router). APW-01 callsrequestDeletionfromWorkLifecycleService.deleteWork(its task T39; typed fake here). Added (APW06-G08, APW06-G12). Thedelete-app-workop gets its cluster access foronAppWorkDeletingfrom T69'sAppRuntimeTargetResolver, deprovisions APW-07 before deleting volumes on the Remove-with-data path, ends withmayRemain[]and deletes no volume when APW-07 reportsremaining, and finishes by calling a remote proxy ofAppRuntimeDeletionService.finishDeletion(workId, { mayRemain })— a method that runs in the API, resolvesWorkLifecycleServicethroughModuleRefand callscompleteAppWorkDeletion(workId). Add thefinishDeletionentry toapps/api/src/trigger/trigger-internal.controller.tsand the matchingcreateRemoteProxyinpackages/tasks/src/trigger/worker/modules/trigger-internal.module.ts. Test:packages/agent/src/app-runtime/__tests__/app-runtime-deletion.service.spec.ts(new) — targetnoneor never deployed →{ status: 'done' }and zero dispatches; a live App Work → claim + onedelete-app-workdispatch and{ status: 'pending' }; a second request while pending →pendingand zero new dispatches; the op calls APW-07 beforedestroyApp, passesdeleteVolumes: falseby default, and callscompleteAppWorkDeletion(workId)only after removal (ACC-06-45); withdeleteStoredData: truedependencies are deprovisioned beforedestroyApp(…, { deleteVolumes: true })(ACC-06-46); three unreachable attempts spaced 5 minutes → completion withmayRemain[](ACC-06-46); Activity rows carry names only; the keep path callsonAppRemoved(workId, { deleteData: false })afterdestroyApp, the data path callsonAppRemoved(workId, { deleteData: true })before it and aborts the volume delete when it reportsremaining(APW06-G08); the port resolves toAppRuntimeDeletionServicefrom a module graph that does not import the ports module (APW06-G12). Done when:pnpm --filter @ever-works/agent test -- app-runtime-deletion.serviceis green. -
T59. Deleting an App Work — API preview and dialog section (R-15). Modify
apps/api/src/app-runtime/app-runtime.controller.ts—GET :id/app-deletion-preview; every action route answers409 APP_WORK_DELETINGwhile deletion is in progress. Createapps/web/src/components/works/detail/deploy/app/AppDeleteStoredDataSection.tsx(new) — kept list, the Also delete stored data checkbox (unticked), typed slug, generated-values warning. Modifyapps/web/src/components/works/detail/settings/DeleteComponent.tsx(created for kindappby APW-01 T39) — mount this section in place of APW-01's inline stored-data checkbox; the payload rule stays APW-01's (delete_stored_data: trueonly on the exact slug). Test: extendapps/api/src/app-runtime/app-runtime.controller.spec.ts— preview returns names and sizes only; deploy during deletion → 409 (ACC-06-45).apps/web/src/components/works/detail/deploy/app/AppDeleteStoredDataSection.unit.spec.tsx(new) — checkbox unticked by default; the confirmation input appears only when ticked; the section reportsdeleteStoredData: trueonly on the exact slug (ACC-06-46). Done when:pnpm --filter ever-works-api test -- app-runtime.controllerandpnpm --filter ever-works-web test -- AppDeleteStoredDataSectionare green. -
T60. Verification targets —
purpose: 'verification'(Resolution R-10). Modifypackages/plugins/k8s/src/app/app-deployer.ts,app-manifest.renderer.ts,app-lifecycle.ts,app-status.reader.ts— plan §4.12 (namespaceverificationNamespaceName(ns, provisioningId, attempt)with the purpose label and expiry annotation, no Ingress, TLS, CronJob, DNS or PVC;emptyDirvolumes; in-namespace smoke only; destroy deletes the namespace). Createpackages/agent/src/app-runtime/app-verification-target.service.ts(new) — handlers forverification-deploy,verification-status,verification-destroyonapp-cluster-op(registered in T70's router), env viaAppRuntimeEnvSource.resolveEphemeral({ target: 'cluster' }), dependencies via APW-07AppDependenciesService.provisionEphemeral(workId, <verification namespace>, kinds)applied after the namespace and its policies and before the workloads (APW06-G08), results reported throughAppVerificationSink(APW06-G09); noWorkDeploymentrow and no runtime-state write. Test: extendpackages/plugins/k8s/src/app/__tests__/app-deployer.spec.tsandapp-manifest.renderer.spec.ts— the verification variant renders zero Ingress, CronJob and PVC objects, sets the expiry annotation fromttlMinutes, runs smoke withHost: <component>.<ns>.svc, anddestroyAppon it issues one namespace delete (ACC-06-48).packages/agent/src/app-runtime/__tests__/app-verification-target.service.spec.ts(new) — zerowork_deploymentsinserts, zero calls to the stored-value env path, and the returned status has components, jobs and smoke only (ACC-06-48); exactly oneprovisionEphemeralcall with the verification namespace and the declared kind set, zerowork_app_dependencieswrites, and oneAppVerificationSink.reportper phase carrying components, jobs and smoke only (APW06-G08, APW06-G09); a Work whose name would collide with a leftover attempt namespace from an earlier provisioning gets a different name (ACC-06-48). Done when:pnpm --filter @ever-works/k8s-plugin test -- app-deployer app-manifest.rendererandpnpm --filter @ever-works/agent test -- app-verification-target.serviceare green. -
T61. Self-address (hairpin) check (spec FR-37, ACC-06-14). Modify
packages/plugins/k8s/src/app/app-deployer.tsandapp-jobs.renderer.ts— plan §4.11 hairpin Job after publish whennetwork.needsHairpinand a primary host exist; result intosmokeResult.hairpin; warninghairpin_unreachable, never a rollback. Modifyapps/web/src/components/works/detail/deploy/app/AppSmokeResults.tsx— hairpin row. Test: extendpackages/plugins/k8s/src/app/__tests__/app-jobs.renderer.spec.ts—renderRunnerJob('hairpin')is created in the app namespace, targets<scheme>://<primary host><path>without aHostoverride (ACC-06-14). Extendpackages/plugins/k8s/src/app/__tests__/app-deployer.spec.ts—needsHairpin: truerenders one hairpin Job after the Ingress apply and a failing one yieldssucceeded-with-warnings;needsHairpin: falserenders none (ACC-06-14).apps/web/src/components/works/detail/deploy/app/AppSmokeResults.unit.spec.tsx(new) — the hairpin warning copy. Done when:pnpm --filter @ever-works/k8s-plugin test -- app-jobs.renderer app-deployerandpnpm --filter ever-works-web test -- AppSmokeResultsare green. -
T62. TLS modes and URL scheme (spec FR-41, FR-42, ACC-06-29). Modify
packages/agent/src/app-runtime/app-hosts.service.ts—appUrlScheme(tls, hostKind)(plan §4.11) used forEVER_WORKS_APP_URL,domains.primary.url, the Deploy tab URL and public smoke URLs. Modifypackages/plugins/k8s/src/app/app-manifest.renderer.ts— TLS block lists every published host with the issuer annotation only forcert-manager. Test: extendpackages/agent/src/app-runtime/__tests__/app-hosts.service.spec.ts—cert-manager→httpsfor custom and managed hosts;external→httpsfor custom,httpfor managed;none→httpwith warningtls_disabled;edge→https(ACC-06-29). Extendpackages/plugins/k8s/src/app/__tests__/app-manifest.renderer.spec.ts—cert-managerrenders a TLS block and issuer annotation;externalandnonerender neither (ACC-06-29). Done when:pnpm --filter @ever-works/agent test -- app-hosts.serviceandpnpm --filter @ever-works/k8s-plugin test -- app-manifest.rendererare green. -
T63. Deploy tab accessibility and locale parity (ACC-06-44). Create
apps/web/e2e/flow-app-deploy-a11y.spec.ts(new) — axe (as inapps/web/e2e/accessibility-axe-deep.spec.ts) on the App Deploy tab in the None, live and failed states and on the Connect, Rollback, Remove and Stored data dialogs; keyboard: every action reachable byTab,Esccloses dialogs and returns focus. Test:pnpm --filter ever-works-web test:e2e flow-app-deploy-a11yreports no new violations, andnode apps/web/scripts/sync-locale-parity.mjs && git diff --exit-code apps/web/messagesadds zero keys (ACC-06-44). Done when: both commands exit 0 in the PR. -
T64. Managed subdomain on Your cluster — web (R-16). Modify
apps/web/src/components/works/detail/deploy/SubdomainManagement.tsx(App branch: apps-domain suffix, on/off toggle,httpwarning when the TLS choice is not the issuer) andapps/web/src/components/works/detail/deploy/app/AppLiveCard.tsx(shows the managed URL when primary). Test:apps/web/src/components/works/detail/deploy/SubdomainManagement.unit.spec.tsx(new) — kindappwith an apps domain shows<slug>.<apps-domain>; without one the managed section is absent and custom domains remain; thehttpwarning shows outside issuer mode (ACC-06-47). Extendapps/web/e2e/flow-app-deploy-domains.spec.tswith the managed-subdomain row (ACC-06-47). Done when:pnpm --filter ever-works-web test -- SubdomainManagementandpnpm --filter ever-works-web test:e2e flow-app-deploy-domainsare green and non-app Works renderSubdomainManagementunchanged. -
T65 (P1, lands with T16–T18). Classify new tables for workspace backup (R-25). Modify
packages/agent/src/account-transfer/backup/collectors/domain-specs.ts— append to theworksdomain:{ file: 'app-runtime-states.jsonl', entity: 'WorkAppRuntimeState', scope: { by: 'parent', column: 'workId', from: 'workIds' } }.packages/agent/src/account-transfer/backup/redaction.tsis not modified:WorkAppRuntimeStateholds no credential (clusterFingerprinthashes the API server address and CA;clusterCheckis secret-free andstatusSnapshotcarries no log text, plan §7.2), and T16'sWorkDeploymentcolumns (buildId,componentStatuses,smokeResult,appTarget,appRender) ride the existingworks/deployments.jsonlfile with no secret-shaped name. Test: extendpackages/agent/src/account-transfer/backup/collectors/collectors.spec.ts—WorkAppRuntimeStateis referenced exactly once, inworks, scopedparentonworkIdfromworkIds, not dropped;WorkDeploymentis still referenced once, bydeployments.jsonl, and a fixture row with anappRenderobject is yielded unchanged. The newWorkAppRuntimeStatecolumns (cancelRequestedByUserId,pendingDomainRebuildBuildId,upstreamSyncJudgedToSha) ride that file and need no redaction entry — none is a credential — and the fixture asserts they survive verbatim. Done when:pnpm --filter @ever-works/agent test -- collectors redactionis green and a backup of a workspace with one deployed App Work listsdata/works/app-runtime-states.jsonlwith one record.
P1.11 — The deploy-shape family stays whole (R-27, added 2026-09-17)
-
T66. Prove no shipped cluster source was narrowed. Modify nothing in
apps/api/src/plugins-capabilities/deploy/cluster-source-matrix.ts— this task is a regression proof for the owner's B-01 answer: the matrix must still returnk8s-worksfor a platform admin (admin-only org),k8s-works-sharedalways, andcustom-kubeconfigfor every owner outside the shared orgs, in that UI order, with the three labels and descriptions unchanged. Test: extendapps/api/src/plugins-capabilities/deploy/cluster-source-matrix.spec.ts— the three sources and their exact labels/descriptions are asserted as a snapshot; a platform admin on a non-ever-worksowner gets['k8s-works-shared', 'custom-kubeconfig']; a non-admin on anever-worksowner gets['k8s-works-shared']; no code path returns an empty list (ACC-06-50). Done when:pnpm --filter ever-works-api test -- cluster-source-matrixis green and the diff touches only the spec file. -
T67. Prove one runtime, two configurations. Create
packages/agent/src/facades/__tests__/deployment-context.resolver.spec.ts(new) — the resolver has no unit suite of its own today, only indirect coverage throughdeploy.facade.spec.ts. Test: for each shipped provider id (k8s,ever-works) crossed with eachClusterSource(k8s-works-shared,custom-kubeconfig), assert the resolved context differs only in credential/namespace handling and never in shape; a non-Kubernetes provider id (vercel) passes the token through untouched; acustom-kubeconfigwith an empty kubeconfig fails withDEPLOY_MATRIX_VIOLATION/PLATFORM_KUBECONFIG_MISSINGas today; the platform-managed sentinel resolves without an owner credential (ACC-06-51). Done when:pnpm --filter @ever-works/agent test -- deployment-context.resolveris green anddeploy.facade.spec.tsis untouched. -
T68. Record the shapes that are extension points, not shipped paths. Modify
plan.md— add a §8.4 pointer todeploy-shapes.mdand state that the connected-node deploy executor (shape F) and the SSH provider (shape G) are recorded additions, not deliverables of this epic, and that neither may be removed from the taxonomy when they are scheduled. Test: none (documentation). The file is cited fromCONTRACTS.mdR-27,spec.md§4.1 and the program README. Done when:deploy-shapes.mdis linked fromplan.md,spec.mdand the README's artifact table, and every claim in it names the file and line it was verified against.
P1.12 — Task edits for the medium findings of the 2026-09-17 fix pass
These are edits to tasks that already exist, not new work: each one is the task-side half of a plan change above. Apply them in place; do not renumber.
- T11 (APW06-G20). Create
packages/plugin/src/helpers/cluster-address-policy.tsand its specpackages/plugin/src/helpers/__tests__/cluster-address-policy.spec.ts, which takes over the deny-CIDR cases (every CIDR,::ffff:10.0.0.1,64:ff9b::a00:1, a mixed public/private resolution, an allow-listed range accepted, the 10 s timeout, andinvalidentries reported) — ACC-06-03.pinKubeconfigServer(yaml, { allowlist, resolver?, timeoutMs? })importsisPublicAddressandresolvePublicAddressesfrom that helper, and the guard spec keeps the refusal, pinning and mocked-307 cases. - T14 (APW06-G20). Each App method reads the allow-list once per call from
parsePrivateAllowlist(process.env.EVER_WORKS_APPS_CLUSTER_PRIVATE_ALLOWLIST)and passes it topinKubeconfigServer; a test asserts an allow-listed private API is accepted and a non-listed one refused, and that the plugin imports no agent config. - T15 (APW06-G19). Use a non-root image for the worker (for example the nginx-unprivileged image with a sleep
command) — a root
busyboxworker failsimage_runs_as_rootunder FR-13 and the spec can never reach Live; make the Ingress-order and public-smoke assertions conditional on the ingress matrix leg (withingress: falseno Ingress is rendered andno_ingress_controlleris the correct outcome); replace the fixedisolationEnforced === falsewith the §4.10 probe result and name the CNI the workflow actually runs in the spec instead of asserting that kindnet does not enforce NetworkPolicy. - T16 (APW06-G16). Widen
DeploymentTriggerSourceinpackages/agent/src/entities/work-deployment.entity.tswithbuild,target-saved,domain-changeandrollback(manualandscheduledunchanged), and addwork_deployments.appTrigger. Test that the two pre-existing values still behave as before. - T17 (APW06-G16, APW06-G15). Every date column in the new entity is a nullable
TimestampColumn(nevertimestamp/timestamptz), nullability and defaults are exactly as plan §7 states, and the entity metadata test asserts notimestamp/timestamptzcolumn.getOrCreate(workId, initialTarget)is an insert-if-absent that takes T69'sderiveInitialAppTargetvalue and never overwrites an existing row; addpackages/agent/src/app-runtime/app-initial-target.tsand its spec (provider null ⇒none; apps-capable cluster plugin ⇒your-cluster; website-only plugin ⇒none; managed value with the policy closed or unbound ⇒none, open ⇒ever-works-apps; a row already set byPUT app-targetis not re-derived; two concurrent first reads create one row). - T18 (APW06-G16). Both migrations are hand-written, idempotent
Table/TableColumn/TableIndexDDL withhasTable/hasColumn/index-name guards in the style of1791240000000-AddSafetyRailsCore.ts; a generated draft is a starting point only and its dialect SQL is not committed. Testup()twice (the second is a no-op) thendown()in the in-memory better-sqlite3 harness the sibling specs use (for exampleapps/api/src/migrations/__tests__/AddAgentHaltReason.spec.ts), plus the opt-in Postgres run with existingwork_deploymentsrows; everyTimestampColumnmaps to abigintcolumn. - T29 (APW06-G17). Create the five catalogue rows of plan §9.4 with their category, title, description, channels and
alternativeSurface, and update the pinned counts as an intentional edit:event-registry-coverage.spec.tsneedsYou 11 → 14, routine →['agent_run_finished', 'app_recovered', 'work_generation_finished'], signals 10 → 11 (digest unchanged, the default quiet-hours list unchanged, plus an assertion that the three new urgent rows require the opt-in), andapps/api/src/notifications/notification-matrix.service.spec.tslength 24 → 29 at both call sites with its title updated. Addapp-runtime-notifications.spec.tscases for the rollback-failed urgent notification, a second down streak creating a new notification, andapp_recoveredcreated while the down notification is still undismissed. - T34 (APW06-G15). Bind
APP_DEPLOY_ROUTE_PORT(useExisting: AppDeployRequestService) in T73's global ports module, visible toDeployModulewithout a cycle — do not add a second kind-appbranch todeploy.service.ts; APW-01's branch delegates once the token is bound. TheDeployControllerrollback, 202-shape and verifier-skip changes stay as written. With the binding, kindappnever yields 409app_runtime_unavailableand callsAppDeployRequestService.requestexactly once; with the binding removed, the 409 returns. - T24/T25 (APW06-G16). A
buildordomain-changerequest that finds the lock held creates itsWorkDeploymentimmediately (INITIALIZING, UI Queued, withbuildId,appTarget,appTrigger), setsqueuedDeploymentIdandqueuedBuildIdin the same transaction and marks any previously queued rowSUPERSEDEDwithappRender.supersededBy. §5.6 step 7 becomes one compare-and-set that adopts the queued row's id and clears both queue columns, then dispatches that row — no new row is created.manual,rollbackandtarget-savedare refused with 409 while the lock is held;buildanddomain-changeare queued. Tests: three Build-triggered requests during one run leave two rows (oneSUPERSEDED, oneINITIALIZING), and after release the samedeploymentIdis dispatched with the row count unchanged.
P1.13 — Namespace preparation, the op router, the isolated worker and published images (added 2026-09-17)
Closes GAP-06 / APW07-G01 (the ordering cycle), APW06-G02, APW06-G03, APW06-G04 and APW06-G12. All P1; T69–T71 also unblock APW-07 P1. Numbers continue from T68.
-
T69 (P1, lands with T6–T7 and T20). Namespace preparation and the runtime target resolver (GAP-06, APW07-G01, APW06-G08). Create
packages/agent/src/app-runtime/app-runtime-target.resolver.ts(new) implementingAppRuntimeTargetPort(T3) per plan §9.9 —resolve(workId)→{ target: 'your-cluster', kubeconfig, context, namespace, appLabels, clusterFingerprint }or{ target: 'ever-works-apps' | 'none', cluster: null }, andprepareDependencyTarget(workId)→{ ref, podLabels }or{ unavailable: 'target_none' | 'target_not_checked' | 'namespace_owned_elsewhere' | 'cluster_unreachable' }. It is worker-only (APP_CLUSTER_IO_IN_API), resolves the credential as plan §5.6 step 3, computes or reads the namespace and persists it with a compare-and-set, and callsprepareAppNamespacebefore returning ayour-clustertarget. Modifypackages/agent/src/app-runtime/index.ts(export) andapps/api/src/app-runtime/app-runtime-ports.module.ts(bindAPP_RUNTIME_TARGET, T73). Register theprepare-namespaceop on T70's router, dispatched fromPUT :id/app-target(T33) and fromprepareDependencyTarget; Modifypackages/agent/src/tasks/app-cluster-op.types.tsfor its payload. Test:packages/agent/src/app-runtime/__tests__/app-runtime-target.resolver.spec.ts(new) — worker-only; the §6.1 guard runs on the kubeconfig; the namespace is persisted once and reused; a foreign-owned namespace is refusednamespace_owned_elsewhere;ever-works-appsandnonereturncluster: null; no env kubeconfig is read (spies).packages/agent/src/app-runtime/__tests__/app-prepare-namespace.spec.ts(new) — a Work with no Deployment gets its namespace,LimitRangeand the three baseline policies and hasnamespaceandclusterFingerprintpersisted; a second call is a no-op; with isolation off the namespace andLimitRangeare applied with zeroew-*policies; a laterdeployAppreuses the frozen name and addsew-allow-ingress/ew-allow-deps(ACC-06-54). Done when:pnpm --filter @ever-works/agent test -- app-runtime-target.resolver app-prepare-namespaceis green, and APW-07'sapp-dependencies.service.spec.tscase "a Work with a declared Postgres and no Deployment reaches ready with zeroapp-deploydispatches" passes against the typed fake. -
T70 (P1, lands with T31–T32). The
app-cluster-oprouter, the nine op handlers andapp-smoke(APW06-G03). Createpackages/agent/src/app-runtime/app-cluster-op.router.ts(new) —handle(payload)routes byop; T48, T58, T60 and T69 register their ops here andapp-cluster-op.task.tsdelegates to it. Createpackages/agent/src/app-runtime/app-lifecycle-ops.service.ts(new) — the nine handlers of plan §9.10 (status-refresh,logs,pause,resume,remove,cancel-deploy,job-run,cluster-check,ingress-reconcile), each writingCACHE_MANAGERkeyapp-op:<workId>:<requestId>(TTL 300 000 ms) and, where the plan says so, the runtime-state row; thelogshandler writesapp-logs:<workId>:<requestId>instead and nothing towork_deployments, runtime state or Activity. Createpackages/agent/src/app-runtime/app-smoke.service.ts(new) — plan §5.7. Modifypackages/agent/src/app-runtime/ports.ts(theAppRuntimeEventSinkbinding the handlers emit through),packages/agent/src/app-runtime/index.ts,packages/tasks/src/tasks/trigger/app-cluster-op.task.tsandpackages/tasks/src/tasks/trigger/app-smoke.task.ts(delegate to the router / the service). Test:packages/agent/src/app-runtime/__tests__/app-lifecycle-ops.service.spec.ts(new) andapp-smoke.service.spec.ts(new) — refresh saves the snapshot and an unreachable cluster keeps the old one withfailed(ACC-06-31); logs are cached under the Work-scoped key with TTL 300 000, write nothing to a repository or Activity, never leak a sentinel secret, and a foreignrequestIdmisses (ACC-06-34); pause is refused while the lock is held and otherwise setspausedand emitsapp.deploy.paused; resume takes the lock, runs the rollout and smoke checks, then clearspausedand emitsapp.deploy.resumed, re-dispatching when the wait exceeds the budget (ACC-06-35, FR-49); remove calls APW-07 beforedestroyApp, defaultsdeleteVolumes: falseand setsremovedAt(ACC-06-36); cancel is honoured only for the matchingdeployLockId(ACC-06-22); job-run uses the live image, refuses a concurrent run and re-dispatches a long one (ACC-06-37, FR-51); cluster-check writesclusterCheck.fingerprintand the observedingressAddressand leavesclusterFingerprintuntouched (ACC-06-05, ACC-06-54); ingress-reconcile callspublishAppHostsonly, with zerodeployAppcalls (ACC-06-25); every op refusesapp_work_deleting; forever-works-appsthek8splugin fake gets zero calls (R-5);app-smokewritessmokeResultand emitsapp.smoke.*with no rollback (ACC-06-55). Done when:pnpm --filter @ever-works/agent test -- app-lifecycle-ops.service app-smoke.serviceandpnpm --filter @ever-works/trigger-tasks testare green, and every op in plan §9.2 has a handler or a named registering task. -
T71 (P1, lands with T20 and T32). The isolated App runtime worker (APW06-G02). Status (2026-09-25, wave 2, worker-deploy-sources):
TriggerAppRuntimeModulebindsAPP_DEPLOY_SPEC_SOURCEtocreateRemoteProxy(api, 'AppSpecService')andAPP_DEPLOY_BUILD_SOURCEtocreateRemoteProxy(api, 'AppDeployBuildSourceAdapter')— plan §6.4's "Proxied" rows for APW-05'sWorkBuildreads and the spec read. API side:AppDeployRequestModuleexports theAppDeployBuildSourceAdapterclass,TriggerInternalModuleimportsAppDeployRequestModule, andTriggerInternalControllerregistersremoteMap.AppDeployBuildSourceAdapter(allow-list:getBuild,listDeployableBuilds). The worker's render-input builder,AppHostsServiceandAppHealthServicenow read the spec and the Build over the internal RPC hop. Still open before a worker deploy can pass §5.6 step 1: (a)APP_DEPLOY_DISPATCHER_AVAILABILITYis unbound in the worker, soAppDeployPreconditionsService.evaluateanswersworker_not_isolatedat step 1 for every dequeued Deployment, before any spec or Build read — this needs a §5.1/§5.6 decision on how the re-check treats the dispatcher gate inside the isolated worker (for example, skip it when the request carries the lock-holdingdeploymentId); (b)WORK_APP_RUNTIME_STATESis unbound in the worker (runtime-state warning); (c)APP_RUNTIME_ENV_SOURCE,APP_IMAGE_PULL_CREDENTIAL_SOURCE,APP_RUNTIME_TARGETandAPPS_TIER_POLICYare still default-ports fail-closed stubs (T73/T44). Later status: Status notes. Createpackages/tasks/src/trigger/worker/modules/trigger-app-runtime.module.ts(new) exactly as plan §6.4, modelled ontrigger-workflow-run.module.tsbut importing noDatabaseModule, no TypeORMDataSourceand no Redis client. It provides the local services of plan §6.4's table, proxies the rest throughTriggerInternalApiClient, and includes the one bootstrap provider that callsmarkAppClusterWorkerContext()inonModuleInit. Modifyapps/api/src/trigger/trigger-internal.controller.ts— add every new name toremoteMap, with its constructor dependency appended last as@Optional()(the existing arity rule);apps/api/src/trigger/trigger-internal.module.ts— import the owning modules;packages/tasks/src/tasks/trigger/app-runtime-local-worker.tsandpackages/tasks/package.json— theapp-runtime:local-workerscript (T32). Test:packages/tasks/src/trigger/worker/modules/__tests__/trigger-app-runtime.module.spec.ts(new) — creates the application context with a stubbed API client, resolves the orchestrator and the facade, asserts noDataSourceis in the container, and asserts the worker-context flag is set only by this module's bootstrap; extendapps/api/src/trigger/trigger-internal.controller.spec.ts— every newremoteMapname resolves and its dependency is optional (APW06-G02, ACC-06-04). Done when:pnpm --filter @ever-works/trigger-tasks testandpnpm --filter ever-works-api test -- trigger-internal.controllerare green, and the local worker starts on a dev machine and refuses to start underNODE_ENV=production. -
T72 (P1, lands with T21–T25). Published images —
build.strategy: image(APW06-G04). Createpackages/agent/src/app-runtime/app-image-reference.resolver.ts(new) andapps/api/src/app-runtime/app-spec-applied.listener.ts(new) per plan §5.8; Modifypackages/agent/src/app-runtime/app-render-input.builder.ts(spec-commit image,buildCommitSha: null, no pull credential),app-deploy.orchestrator.ts(resolve beforeprepare, recordappRender.image), and the T1 constants (APP_IMAGE_REDEPLOY_BLOCKS). Test:packages/agent/src/app-runtime/__tests__/app-image-reference.resolver.spec.ts(new) —@sha256:existence check, tag → digest withresolvedFromTag, 404 →image_not_found, 401/403 →image_private_unsupported, timeout →image_unresolvable, and the registry host passing the §6.1 guard (ACC-06-52);apps/api/src/app-runtime/app-spec-applied.listener.spec.ts(new) — the changed-block matrix, with blocks that change nothing that runs,autoDeploy: false, targetnoneand strategydockerfileeach requesting nothing (ACC-06-52, ACC-06-53). Done when:pnpm --filter @ever-works/agent test -- app-image-reference.resolverandpnpm --filter ever-works-api test -- app-spec-applied.listenerare green, and APW-13's cluster-half fixture (profiles/published-image.works.yml, noFIXTURE_GIT_SHA) deploys with no Build and serves the image tag at/marker.sha(ACC-E2E-05's PR-cluster half). -
T73 (P1, lands with T33). Port publication and wiring (APW06-G12). Create
apps/api/src/app-runtime/app-runtime-ports.module.ts(new) — the@Global()module of plan §9.8, the single provider forAPPS_TIER_POLICY,APP_IMAGE_PULL_CREDENTIAL_SOURCE,APP_RUNTIME_ENV_SOURCE,APP_RUNTIME_TARGET,APP_RUNTIME_EVENT_SINK,APP_VERIFICATION_SINK,APP_WORK_DELETION_PORTandAPP_DEPLOY_ROUTE_PORT(APW-01's token, bound here withuseExisting: AppDeployRequestService— APW06-G15), with the deletion port as a lazyModuleReffactory. Modifyapps/api/src/api.module.ts(import it once) andapps/api/src/app-runtime/app-runtime.module.ts(import it; provide nothing for the same tokens). Test:apps/api/src/app-runtime/__tests__/app-runtime-ports.module.spec.ts(new) — global; exports all seven tokens; the deletion-port provider hasuseExistinganduseClassundefined andinject: [ModuleRef]; a reduced graph that does not import the module still injects a defined@Optional() @Inject(APP_WORK_DELETION_PORT)whoserequestDeletiondelegates toAppRuntimeDeletionService(APW06-G12). Done when: the spec is green andWorkModulecontains no import added for any App runtime token.
Definition of Done
- Every checkbox above is ticked for the phase being shipped.
pnpm format:check,pnpm lint,pnpm type-check,pnpm testandpnpm buildare green from the repo root.- The k8s plugin kind e2e workflow is green, including the unchanged
cluster.e2e.spec.ts. - Existing deploy suites (
packages/plugins/k8s/src/__tests__,apps/api/src/plugins-capabilities/deploy,apps/web/e2e/flow-work-deploy-*) pass unchanged. - Every acceptance box in spec §8 for the phase has been walked against a running build, each ACC-06 id
appears in at least one Test line above, and the matching rows in
../ACCEPTANCE.mdpoint at real test files. - No new string literal
'k8s'outsidepackages/plugins/k8s/; no read ofEVER_WORKS_APPS_MANAGED_ENABLEDin this epic; no kubeconfig, env value, token or log text in Activity, telemetry, API responses orwork_deployments. docs/plugin-system/built-in-plugins.mduntouched (no plugin added — Constitution VIII).
Status notes
Dated status for the tasks above. It is kept here, not in the task bodies, so the task text keeps the line numbers that code comments and specs cite.
- T21 (2026-09-26,
3a956180e): the preconditions are wired in the API graph.AppDeployRequestModulenow imports APW-07'sAppRuntimeEnvModule(APP_RUNTIME_ENV_SOURCE) andAppDependenciesModule(APP_DEPENDENCIES_SERVICE), which were in no API graph, so every Deploy past the dispatcher gate had been refusedenv_source_unavailable. It also providesAppLicenseGate, so the licence preconditions run on the API request path; with APW-03'sAPP_LICENSE_SERVICEunwritten the gate answers its documented unreadable verdict (license_blocks_targeton the managed target, alicense_eligibility_unavailablewarning on your cluster). The dependency step reuses the readiness the env step already fetched (oneensureReadyForDeploycall per evaluation), and while APW-07'sAPP_DEPENDENCY_SPEC_SOURCEis unbound it refuses (dependency_not_ready) only a spec that declares a dependency; a spec that declares none gets thedependencies_unavailablewarning. Pinned bypackages/agent/src/app-runtime/__tests__/app-deploy-request.graph.spec.tsandapp-deploy-preconditions.service.spec.ts; guarded byapps/api/src/app-works-di-reachability.spec.ts. - T71 (2026-09-26,
3a956180e):apps/api/src/app-works-di-reachability.spec.tswalks the worker contexts theapp-*tasks boot. ItsEXPECTED_WORKER_UNBOUNDlist cites T71's 2026-09-25 status line for items (a) and (b). Besides those gaps it measured 13 more unbound worker bindings, kept in itsOPEN_WORKER_GAPSlist rather than called intended: six inAppDependencyProvisionWorkerModule(WorkAppDependencyRepositorytwice, by class and by name,APP_DEPENDENCY_CLUSTER_ACCESS,APP_DEPENDENCY_CONFIG_CIPHER,APP_DEPENDENCY_PROVISION_DISPATCHER,AppDependencyFacadeService) and seven inTriggerAppRuntimeModule(APP_CUSTOM_DOMAIN_STORE,APP_DEPENDENCIES_SERVICE,APP_DEPLOY_DEPLOYMENT_STORE,APP_HOSTS_APPS_DOMAIN,APP_HOSTS_DEPLOYMENT_STORE,APP_HOSTS_DEPLOY_REQUESTER,APP_HOSTS_WORK_STORE). Owner decision: add them to T71's status line (they then move toEXPECTED_WORKER_UNBOUND, citing it) or cut a T71 slice (with APW-07 T17) that binds them. - T71 (2026-10-01, owner ruling on C44, 2026-09-30): the 13 worker bindings named above are added to T71's
status line as expected-unbound until a T71 slice binds them: in
AppDependencyProvisionWorkerModuleWorkAppDependencyRepository(by class and by name),APP_DEPENDENCY_CLUSTER_ACCESS,APP_DEPENDENCY_CONFIG_CIPHER,APP_DEPENDENCY_PROVISION_DISPATCHER,AppDependencyFacadeService; inTriggerAppRuntimeModuleAPP_CUSTOM_DOMAIN_STORE,APP_DEPENDENCIES_SERVICE,APP_DEPLOY_DEPLOYMENT_STORE,APP_HOSTS_APPS_DOMAIN,APP_HOSTS_DEPLOYMENT_STORE,APP_HOSTS_DEPLOY_REQUESTER,APP_HOSTS_WORK_STORE. The DI reachability spec moved them fromOPEN_WORKER_GAPStoEXPECTED_WORKER_UNBOUND, each citing this note; the list stays exact both ways, so the slice that binds one also deletes its entry. Unbound, each keeps its documented fail-closed answer.