Task Breakdown: Builds
Ordered tasks derived from
plan.md. Each is small enough to land in one PR and ships with tests per Constitution VI. The schema tasks ship their migrations in the same PR per Constitution V.
Epic ID: APW-05-builds
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. An implementer should never have to guess a path. Paths not
marked "(create if absent)" under Modify exist on
develop@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-05-nn)tags name the spec §8 criteria a Test line proves. - Add new tasks at the bottom rather than renumbering.
- Phase boundaries are ship boundaries:
developmust be green and deployable at the end of each phase. - Repo commands run from the monorepo root unless a task says otherwise. Migrations are authored from
apps/api/. Package filters:@ever-works/contracts,@ever-works/plugin,@ever-works/agent,ever-works-api,ever-works-web,@ever-works/trigger-tasks,@ever-works/github-actions-build-plugin(new, T7). - API behaviour is tested by controller/service specs under
apps/api/src/**or Playwright specs underapps/web/e2e/— neverapps/api/test/(Resolution R-22). - Cross-epic prerequisites (do not implement here): APW-03's effective App spec +
WorkAppSpecStateandapp.spec.applied; APW-07'sAppEnvResolver.resolveForBuild,AppEnvService.buildRedactorand the ephemeral recipe ofAppRuntimeEnvSource(R-10); APW-02'ssetActionsPermissions?; APW-01'sWorkCapabilities.builds(R-7); APW-13's fixture branches (R-23). Added 2026-09-17 (APW05-G06): APW-03 T22'sIGitProviderPlugin.commitFiles?and itsGitFacadeServicewrapper (the only clone-free write — T9, T16, T19), and APW-06 T3'spackages/agent/src/app-runtime/ports.ts(AppImagePullCredentialSource/APP_IMAGE_PULL_CREDENTIAL_SOURCEfor T16;AppRuntimeEnvSource/APP_RUNTIME_ENV_SOURCEfor T43). Both merge in the Wave 1 foundations (TRACKER), ahead of this epic's T9/T16/T19. Unit tests use a fakeRepositoryWriterand fake port objects; production bindings in T16, T19 and T43 are not merged, and the port files are neither re-declared nor copied, until APW-03 T22 and APW-06 T3 are ondevelop. This epic never modifiesgit.facade.tsorports.ts. Until they land, T16–T22 and T43 run against the typed fakes named in each task. - Cross-epic prerequisite claimed by this epic (added 2026-09-17,
APW05-G01/GAP-07): APW-05 is the only caller of APW-02'screateWebhook?/deleteWebhook?. T19a installs theworkflow_runhook and T19a's release path removes it; no other epic calls either method.
Phase P1 — Builds on GitHub-hosted runners
Delivers spec FR-1…FR-54 and FR-60…FR-70: the workflow file, build values as secrets, build services, runner selection, digests, deployability, diagnosis, receipts, pull tokens, runner verification, App checks, the Builds tab.
P1.1 — Contracts
-
T1. App build contracts. Create
packages/contracts/src/apps/builds.tswith the unionsAPP_BUILD_STATUSES,APP_BUILD_TRIGGERS,APP_BUILD_FAILURE_CLASSES(14),APP_BUILD_BLOCKED_REASONS(15),APP_BUILD_NOT_DEPLOYABLE_REASONS(9),APP_BUILD_STRATEGIES(dockerfile,image,auto,none— Resolution R-13), their types,AppBuildSummary,AppBuildDetail(receipt incl.checksBillableMinutes),AppBuildVerificationResult, and every constant in plan §3.2 with exactly those values, includingAPP_BUILD_CHECK_NAME_PREFIX,APP_BUILD_CHECKS_MAX,APP_BUILD_CHECKS_MAX_PARALLELandAPP_BUILD_VERIFY_PROMPTED_SECRET. Modify APW-03's barrelpackages/contracts/src/apps/index.tstoexport * from './builds.js';(create it, and its export frompackages/contracts/src/index.ts, only if APW-03 has not landed — Resolution R-1). Test:packages/contracts/src/apps/__tests__/builds.spec.ts— pins each union (a member cannot be added without editing the test);APP_BUILD_STRATEGIEScontains no builder product name; asserts every numeric constant (50,48_000,8,10_000,10,60_000,90_000,200,300_000,30,20,300,2_097_152,8_192,2,16,7,30,12,60_000,14,20,100, checks20and5), the workflow path string and the check name prefixEver Works check:. Done when:pnpm --filter @ever-works/contracts testis green,pnpm --filter @ever-works/contracts buildemits declarations andapps/apiimportsAppBuildSummaryfrom@ever-works/contracts. -
T2 (parallel with T1).
buildcapability and category. Createpackages/plugin/src/contracts/capabilities/build.interface.tsexactly as plan §4.1, includingBuildStrategywithauto,PrepareRepositoryInput.checks,VerificationPlan.promptedNames,BuildSnapshot.checksBillableMinutes,isBuildPluginand the optionalcheckImageAccess?. Modifypackages/plugin/src/contracts/capabilities/index.ts—export * from './build.interface.js';. Modifypackages/plugin/src/contracts/facade-capabilities.ts— addBUILD: 'build'with a comment citing APW-05. Modifypackages/plugin/src/contracts/plugin-manifest.types.ts— append'build'toPLUGIN_CATEGORIES. Test:packages/plugin/src/contracts/__tests__/build-capability.spec.ts—isValidPluginCapability('build'),isPluginCategory('build'),isBuildPlugintrue/false, and that no existing capability or category was removed (snapshot of the previous members). Done when:pnpm --filter @ever-works/plugin testis green. -
T3 (parallel with T1).
buildsWork capability — consume only. No change topackages/contracts/src/domain/work-capabilities.ts: APW-01 T3 addsreadonly builds: boolean(trueforapp,falsefor every other kind) and its spec pins (Resolution R-7). This epic only readsgetWorkCapabilities(work.kind).builds(T25's Builds tab) and never declares or defaults the field. Test:git grep -n "builds" packages/contracts/src/domain/work-capabilities.tsshows APW-01's field, and no APW-05 change touches that file or its spec. Done when: APW-01 T3 is merged andpnpm type-checkpasses for web and API against it.
P1.2 — Entity, table, migration
-
T4.
WorkBuildentity. Createpackages/agent/src/entities/work-build.entity.tswith every column of plan §3.1 (incl.checksBillableMinutes,verifySecretNamesand thesyncOrigin/syncFromSha/syncToShatrio APW-04 and APW-06 read —APW04-G06),TimestampColumnfrompackages/agent/src/entities/_types.tsfor timestamps,ManyToOne(() => Work, { onDelete: 'CASCADE' }), Tier AtenantId/organizationIdwithout relations, and the five indexes. Create alsopackages/agent/src/entities/work-build-preparation.entity.ts— every column of plan §3.1b, the unique(workId)indexuq_work_build_preparations_work, the sameTimestampColumn/ManyToOne(Work, CASCADE)/Tier A treatment and no API route that writes it (APW05-G03). Modifypackages/agent/src/entities/index.ts,packages/agent/src/database/_entity-names.ts('WorkBuild','WorkBuildPreparation'),packages/agent/src/database/_entities-inventory.ts(import +ENTITIES). Test:packages/agent/src/entities/__tests__/work-build.entity.spec.ts— index names, uniqueness flags, the plainUNIQUEon(buildPluginId, providerRunId, runAttempt)with no partialWHERE(so the same DDL works on Postgres, SQLite, MySQL and MariaDB —APW05-G10), both scope columns present; andpackages/agent/src/entities/__tests__/work-build-preparation.entity.spec.ts— the uniqueworkIdindex, thewebhookState/workflowStatedefaults, both scope columns. Done when:pnpm --filter @ever-works/agent test -- work-build.entity work-build-preparation.entityis green and the drift specspackages/agent/src/database/database.module.spec.tsandpackages/agent/src/database/database.config.spec.tspass without editing a magic number. -
T5. Migration. Create
apps/api/src/migrations/1792050000000-CreateWorkBuilds.ts(generate the skeleton withcd apps/api && pnpm typeorm migration:generate -d typeorm.config.ts src/migrations/CreateWorkBuilds, then re-stamp the class name/timestamp to the APW-05 block). It creates bothwork_buildsandwork_build_preparations(plan §3.1b) with their FKs and all six indexes, declared through TypeORMTableIndex— no raw double-quoted SQL and no partial index anywhere (APW05-G10).down()drops only those two tables. Test:apps/api/src/migrations/__tests__/CreateWorkBuilds.spec.ts—up()creates both tables, their FKs and the six indexes;down()removes only them; the file contains noALTER TABLEon a pre-existing table and no driver branch. Done when:pnpm --filter ever-works-api test -- CreateWorkBuildsis green, a fresh Postgres, a fresh SQLite and a fresh MySQL/MariaDB database all migrate up and down cleanly, and the timestamp is above the newest migration ondevelop. -
T6.
AppBuildRepository. Createpackages/agent/src/database/repositories/app-build.repository.tswithinsertWithNextNumber(workId, data, { stampFromPreparation })(transaction; the Work-row pessimistic lock on postgres/mysql/mariadb withloadEagerRelations: false, skipped on the SQLite family;MAX(number)+1through the query builder with noFOR UPDATE; 3 retries on a unique violation, detected across drivers asCreditLedgerRepository.isUniqueViolationdoes — plan §3.1,APW05-G10),upsertByProviderRun,findPage(workId, filters, page, pageSize),findByIdForWork,findRecentForCommit(workId, sha, sinceMs),claimWatchLease(id, ms)through the query builder with a parameterised:now/:until,findSilentNonTerminal(now, silenceMs, limit),findWithOrphanedVerifySecrets(now, limit),markLost(ids). Create alsopackages/agent/src/database/repositories/app-build-preparation.repository.tswithfindByWorkandupsertAfterPrepare(APW05-G03). Modifypackages/agent/src/database/index.tsto export both. Test:packages/agent/src/database/repositories/__tests__/app-build.repository.spec.ts— 20 concurrentinsertWithNextNumbercalls yield numbers 1…20 with no gap or duplicate;stampFromPreparationcopiesbuildInputsHash,buildSecretNamesandsecretsSyncedAtfrom the preparation row, and an absent row leaves all three NULL; lease claim returns 0 rows for a live lease; silent-build query honours the 90 s silence and the 200 limit, oldest first; orphaned-verify-secret query selects only verification Builds older than 40 minutes with a non-emptyverifySecretNames.apps/api/src/migrations/__tests__/query-shape.spec.ts(or the existing query-shape precedent) asserts no double-quoted raw SQL and nointerval 'literal in either repository, so the MySQL/MariaDB rule ofb5a7d6857is enforced by a test rather than by review (APW05-G10). Done when:pnpm --filter @ever-works/agent test -- app-build.repository app-build-preparation.repositoryis green on SQLite and on the Postgres test container.
P1.3 — The github-actions-build plugin
-
T7. Package scaffold and settings. Create
packages/plugins/github-actions-build/—package.json(name@ever-works/github-actions-build-plugin,everworks.pluginblock from plan §4.3, depsoctokit,libsodium-wrappers,fflate;tsup,vitestscripts copied frompackages/plugins/k8s/package.json),tsconfig.json,tsup.config.ts,vitest.config.ts,src/index.ts,src/settings.schema.ts(plan §4.4,pullTokenwithx-secret: trueandx-platformManaged: true,pullTokenExpiresAtx-platformManaged: true, and the two new Work-scope booleansallowBuildValuesOnPullRequests(defaultfalse) andverificationPromptedValuesRequireApproval(defaulttrue) —XC-01),src/github-actions-build.plugin.ts(id, categorybuild, capabilitybuild,buildKind: 'github-actions',supportedStrategies: ['dockerfile']—autois not supported, R-13 — method stubs throwingnot implemented; novalidateSettingsforpullToken— the check isAppBuildPullTokenService, plan §4.12,APW05-G07). Test:packages/plugins/github-actions-build/src/__tests__/plugin.manifest.spec.ts— manifest id/category/capabilities;pullTokenisx-secretandx-platformManaged;attestationsdefaultsfalse;reclaimDiskdefaultstrue;supportedStrategiesexcludesauto; the schema declares no preparation key (workflowSha256,workflowPullRequestNumber,webhookId,runsEtag,repositoryBlockare absent —APW05-G03), andvalidateSettingsScoperefusespullTokenandpullTokenExpiresAtat every scope (APW05-G07). Done when:pnpm --filter @ever-works/github-actions-build-plugin testis green and plugin discovery listsgithub-actions-buildat API boot in development. -
T8. Workflow generator and action pins. Create
packages/plugins/github-actions-build/src/workflow/generator.ts,src/workflow/inputs-hash.ts,src/workflow/action-pins.ts(checkout,setupBuildx,login,buildPush,uploadArtifact— 40-hex commit hashes resolved at implementation time, release tag in a trailing comment) implementing plan §2.4 and §4.5: header lines, triggers,permissions: {}, concurrency expression, pull-request guard, services with health options, check-values step, checkout, disk reclaim, buildx (network host only with services), login, build (load: true,push: false,provenance: false), secret check, push + digest capture, verify step, result write, upload. (Thechecksjob is T41.) Test:packages/plugins/github-actions-build/src/__tests__/generator.spec.ts— golden files undersrc/__tests__/golden/for 6 fixtures (minimal; args withvalue+fromEnv; services postgres + redis; private repo with larger runner; attestations on; branchfeature/xslug); byte equality on two runs (ACC-05-03); LF endings; for each fixture every build value string supplied to the test is absent from the output and the build job readsEW_secrets only as${{ secrets.EW_* }}references (ACC-05-05); the build job'sif:refuses pull requests whose head repository differs and excludesinputs.ew_mode == 'verify'(APW05-G02), and no job that referencessecrets.or pushes runs for them (ACC-05-06); tagssha-${{ env.EW_SHA }}+branch-<slug>for push andpr-<n>for pull requests, neverlatest(ACC-05-07);cache-toonly on push; the concurrency block setscancel-in-progressonly forpull_request, groups the tracked branch by ref, and gives a verification its ownverify-<buildId>group (APW05-G02); the services golden declares thepostgresservice with theBUILD_SERVICE_DEFAULTSenv (POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB), the published5432port andpg_isreadyhealth, and a build-arg resolving to127.0.0.1(ACC-05-12,APW05-G08); thebuildjob'stimeout-minutesequalsbuild.resources.timeoutMinuteswith no verification bonus, and only theverifyjob adds 30 (APW05-G22). Also add four goldens (APW05-G02,G08,XC-01):verify-bootstrap(dispatch-only: nobuildjob, nochecksjob, nosecrets.EW_exceptEW_VERIFY__PROMPTED, nopushorpull_requesttrigger);verify-job(permissionsexactlycontents: read, packages: read, nodocker push,--pushorcache-to, theew_reuse_digestinput, theverify-<buildId>concurrency group);services-postgres-defaults(an undeclaredenvstill yields the three defaults and the published container port); andrestricted-values(a same-repository pull request'sfromEnvargument is the throwaway marker with nosecrets.EW_reference, while the same file's push path keeps${{ secrets.EW_<NAME> }}— ACC-05-31).packages/plugins/github-actions-build/src/__tests__/action-pins.spec.ts— every pin matches^[0-9a-f]{40}$(ACC-05-05). Done when:pnpm --filter @ever-works/github-actions-build-plugin test -- generator action-pinsis green and the golden workflow for the minimal fixture passesactionlintlocally. -
T9. Branch protection and workflow writer. Create
packages/plugins/github-actions-build/src/repo/branch-protection.ts(the classic endpoint: 404 → unprotected; 200 with reviews or status checks → protected; 403 → protected; plusGET /repos/{o}/{r}/rules/branches/{branch}, where an activepull_requestorrequired_status_checksrule also means protected, so a ruleset-only branch is not mistaken for an open one —APW05-G16) andsrc/repo/workflow-writer.ts(through theRepositoryWriterparameter only — APW-03'scommitFiles?via the facade; pull-request path onever-works/build-workflowwith one reused pull request; read-back sha256 compare with one retry; hand-edit detection againstlastWrittenWorkflowSha256;nonFastForwardretries ≤ 3;refRejectedByRuleswitches once to the pull request path and never loops — plan §4.6 step 2,APW05-G16;createdByAppWork: false(Link) always takes the pull request path — Resolution R-4; the bootstrap file of plan §4.6 step 0, which is delivered by these same rules and setsworkflowStatewithout ever looking like a hand edit —APW05-G02). Test:packages/plugins/github-actions-build/src/__tests__/workflow-writer.spec.tswith a fakeRepositoryWriter— no clone call exists; direct commit on an unprotected fork changes exactly the one workflow path (ACC-05-01); pull request on a protected branch and oncreatedByAppWork: false; a ruleset-protected branch whose legacy endpoint answers 404 takes the pull request path, and a direct write refused withrefRejectedByRulefalls back to the pull request path exactly once (APW05-G16); a second and third preparation update the same pull request so one stays open (ACC-05-02); read-back mismatch → retry →workflowWriteFailed; hand-edited file →editedByHand+ pull request, never an overwrite on the tracked branch (ACC-05-04); unchanged content →unchangedand zero write calls; a branch with no workflow at all gets exactly one bootstrap commit whose content carries only theverifyjob, and a Link-relation App Work getsworkflowPendingplus the pull request URL and zero commits (APW05-G02, ACC-05-02). Done when:pnpm --filter @ever-works/github-actions-build-plugin test -- workflow-writeris green and no code path calls a force update on the tracked branch. -
T10. Secret sync. Create
packages/plugins/github-actions-build/src/repo/secret-sync.ts— refuse > 50 values (tooManyBuildValues) or a value > 48,000 bytes (buildValueTooLarge, name only) or an env name mapping ontoAPP_BUILD_VERIFY_PROMPTED_SECRET(buildValueNameReserved); one public-key fetch; libsodium sealed box per value (same sequence and name validation aspackages/plugins/github/src/github-actions.service.ts); delete onlypreviouslyWrittenSecretNamesno longer referenced; map the repository secret-limit error tosecretLimitReached; computebuildInputsHash;writeVerifyPromptedSecret(values)/deleteVerifyPromptedSecret()for §4.10. Test:packages/plugins/github-actions-build/src/__tests__/secret-sync.spec.ts— the three limits and the reserved name; removal set arithmetic (never deletes a name not previously written, even if it starts withEW_); everyfromEnvname isPUTbeforestartBuildwould be called (ACC-05-13); hash stable under value order; the verify secret is one JSON secret ≤ 48 KB and is deleted by name; no value appears in any thrown error message or logger call (spy on the logger). Done when:pnpm --filter @ever-works/github-actions-build-plugin test -- secret-syncis green. -
T11 (parallel with T10). Runner selector. Create
packages/plugins/github-actions-build/src/runner/runner-selector.ts— public →githubPublic; private →largerRunnerLabelwhen set (memory fromlargerRunnerMemoryGiB) elsegithubPrivate; blockrunnerTooSmallwith{ needed, max }whenmemoryGiB > memory − 2; CPU above vCPU → warning flag only. Test:packages/plugins/github-actions-build/src/__tests__/runner-selector.spec.ts— 14 GiB public allowed, 15 GiB public blocked, 5 GiB private allowed, 6 GiB private blocked, 12 GiB private with no larger runner blocked with{ needed: 12, max: 5 }(ACC-05-22), 12 GiB private with a 32 GiB larger runner allowed, label without memory rejected by settings validation. Done when:pnpm --filter @ever-works/github-actions-build-plugin test -- runner-selectoris green. -
T12. Run correlation, observation and the result artifact. Create
packages/plugins/github-actions-build/src/runs/run-correlator.ts(dispatch-time window −5 s,display_titlecontains the Build id, adoption window 5 minutes),src/runs/run-observer.ts(run + jobs →BuildSnapshot; minutes = Σ ceil per job;checksBillableMinutes= the subset whose job name starts with the check prefix; only thebuildjob decides status; pull-request head sha; failing step name/number),src/runs/result-artifact.ts(artifact lookup by name, zip ≤ 64 KB refused above,fflate.unzipSync, JSON ≤ 8 KB, strict schema, digest regex), andsrc/runs/run-lister.tsimplementingIBuildPlugin.listRecentRuns?(APW05-G01):GET /repos/{o}/{r}/actions/workflows/{file}/runs?per_page=20withIf-None-Matchfrom the stored ETag, newest first, mapping each run toBuildRunRef; 304 →notModified: true. Modifypackages/plugins/github-actions-build/src/github-actions-build.plugin.ts— implementstartBuild(workflow dispatch on the tracked branch withew_build_id,ew_sha,ew_mode,ew_verify_plan≤ 60,000 chars andew_reuse_digest, retrying a 404/422 for up to 60 s after a bootstrap commit so a just-added workflow file is dispatchable —APW05-G02),getBuild,cancelBuild,getLogsUrl,listRecentRuns. Test:packages/plugins/github-actions-build/src/__tests__/run-correlator.spec.ts(adoption bydisplay_titleinside the window, none outside it);run-observer.spec.ts(two jobs 61 s + 30 s → 3 minutes and a receipt payload carrying them — ACC-05-20; a failedEver Works check: lintjob with a succeededbuildjob → snapshotsucceeded,checksBillableMinutes1 — ACC-05-29; pull request run reports head sha not merge sha; a run that staysin_progressacross two polls and thencompletedmaps each status — ACC-05-11;cancelBuildcalls the cancel endpoint and acancelledconclusion maps tocancelled— ACC-05-09; tags reported withoutlatest— ACC-05-07);result-artifact.spec.ts(oversize zip, malformed digest, extra keys rejected);run-lister.spec.ts(per_page≤ 20, a 304 maps tonotModified, fields map toBuildRunRef, a fork pull request's head repository is reported —APW05-G01). Done when:pnpm --filter @ever-works/github-actions-build-plugin test -- run-correlator run-observer result-artifactis green. -
T13. Failure classifier and excerpt. Create
packages/plugins/github-actions-build/src/runs/failure-classifier.ts(the 11-row ordered table in plan §4.9) andsrc/runs/log-tail.ts(last 2 MiB viaRange, split, apply the facade'sredact, then mask secret-shaped strings, cut to 20 lines × 300 chars ending at the matched line). Test:packages/plugins/github-actions-build/src/__tests__/failure-classifier.spec.ts— one fixture log per class undersrc/__tests__/fixtures/logs/;outOfMemory(exit 137),dockerfileError(step, total, 120-char command),missingBuildValue(fromEW_MISSING:<NAME>in the first step, ACC-05-14),secretInImage(fromEW_SECRET_IN_IMAGE:<NAME>, ACC-05-15),timeoutanddiskFulleach classify with theirfailureDetail(ACC-05-17); precedence (a log with bothexit code: 137and a Dockerfile step classifiesoutOfMemory); excerpt ≤ 20 lines × 300 chars and a known value inside the log becomes***(ACC-05-18). Done when:pnpm --filter @ever-works/github-actions-build-plugin test -- failure-classifieris green. -
T14. Registry access and the pull token. Status (2026-09-25, wave 2): the GHCR token exchange is in
ghcr-access.ts(anonymous/tokenthenHEADwith the registry bearer; for a pull token,/tokenwith Basicx-access-token:<PAT>), and the confirming half is implemented and review-hardened:AppBuildsService.finalizeconfirms the digest through the facade binding'scheckImageAccess(bounded byAPP_BUILD_DIGEST_READ_TIMEOUT_MS, 15 s), confirmation survives repeated observations, a later confirmation clears adigestMismatch, amanual/verificationBuild keeps its dispatched commit, and plan §4.8's no-token fallback readsBuildSnapshot.image.pushLogDigest. Still open: a caller forreconfirmDigest(the §7.4 sweep recheck and the pull-token-save recheck), and the private path verified with a real classicread:packagestoken (operator). Createpackages/plugins/github-actions-build/src/registry/ghcr-access.ts— anonymous and token-authenticated manifestHEADforsha-<sha>; token check viaGET /userrequiringx-oauth-scopesexactlyread:packages; absent header → refused (fine-grained); expiry header parsed. Modifypackages/plugins/github-actions-build/src/github-actions-build.plugin.ts— implementcheckImageAccessand asyncvalidateSettingsforpullToken(refusal codespullTokenTooBroad,pullTokenFineGrained,pullTokenCannotRead), writingpullTokenExpiresAt. Test:packages/plugins/github-actions-build/src/__tests__/ghcr-access.spec.ts— scopesread:packagesok;read:packages, reporefused; header absent refused; 401 on manifest →cannotRead; public manifest anonymous 200 →visibility: 'public'; private manifest without token →readable: false(ACC-05-21); no request ever sends the token to a host other thanapi.github.comorghcr.io. Done when:pnpm --filter @ever-works/github-actions-build-plugin test -- ghcr-accessis green. -
T15. Secret-in-image check and runner verification scripts. Create
packages/plugins/github-actions-build/src/workflow/secret-check.sh.ts(the plan §4.11 script as a template literal) andsrc/workflow/verify-runner.sh.ts(plan §4.10: plan schema check withversion == 1refused otherwise (APW05-G11), 12 GiB summed memory refusal, throwawaypostgres/redis/miniocontainers pinned by digest with no volumes, recipe materialisation withopenssl rand/openssl genpkeyinto a0600env file, prompted values from the per-run secret, jobs, readiness waits, smoke viacurl --max-time 30 --max-redirs 0, per-job and per-smoke result rows, env file shredded,set +x; and the image it verifies:--loadfrom the plan'sbuildsection with--cache-fromthebuildcachetag only, ordocker pullofew_reuse_digest, never a push and nevercache-to—APW05-G02). Test:packages/plugins/github-actions-build/src/__tests__/secret-check.script.spec.ts— runs the script underbashwith a stubdockeronPATH: match → exit 79 andEW_SECRET_IN_IMAGE:<NAME>on stdout without the value, and the push step is never reached (ACC-05-15); no match → 0; 7-char value skipped.packages/plugins/github-actions-build/src/__tests__/verify-runner.script.spec.ts— plan over 60,000 chars refused; summed memory 13 GiB refused; a stub smoke server returning 200/500 produces one passing and one failing smoke row, a stub job exiting 3 producesexitCode: 3, andcomponentsReadyreflects the stub probe (ACC-05-23); generatedbase6424 bytes → 32 characters; the env file does not exist after the script ends; no generated value appears in captured stdout/stderr. Done when: both specs pass on Linux CI (skipped with a reason on Windows developer machines).
P1.4 — Facade, services, jobs
-
T16.
BuildFacadeService. Status 2026-09-26: Status notes. Createpackages/agent/src/facades/build.facade.tsextendingBaseFacadeServicewithCAPABILITY = PLUGIN_CAPABILITIES.BUILD:resolve(workId, userId)→{ plugin, auth, settings, repository }(token throughGitFacadeService.getAccessToken),getPullCredential(workId)(public →null; private →{ server: 'ghcr.io', username: 'x-access-token', password }from the Work-scopedpullToken; never any fallback),redactorFor(workId)(APW-07AppEnvService.buildRedactor; typed fake until APW-07 lands),repositoryWriter(workId)(bindsgetFileContent, APW-03'scommitFiles?,createBranch,createPullRequest). Createpackages/agent/src/app-builds/app-build-pull-credential.source.tsimplementing APW-06'sAppImagePullCredentialSource.resolve(workId, buildId)overgetPullCredential, bound toAPP_IMAGE_PULL_CREDENTIAL_SOURCEfrompackages/agent/src/app-runtime/ports.ts. Modifypackages/agent/src/facades/facades.module.tsandpackages/agent/src/facades/index.tsto provide/export it. Test:packages/agent/src/facades/__tests__/build.facade.spec.ts— resolution through a mockbuildplugin; no stringgithub-actions-buildin the facade source (grep assertion);getPullCredentialreturnsnullfor public and never returns the git token whenpullTokenis unset (ACC-05-21); the port returns the same object as the facade. Done when:pnpm --filter @ever-works/agent test -- build.facadeis green. -
T17.
AppBuildsServiceand the deployable verdict. Status 2026-09-26: Status notes. Status (2026-09-25, wave 2):finalize's digest confirmation (T14's confirming half) andreconfirmDigest(buildId, { pushLogDigest? }), which re-settles adigestUnconfirmedBuild without publishing an event, are implemented; nothing callsreconfirmDigestyet (the §7.4 sweep recheck and the pull-token-save recheck).requestRebuildnow goes throughrequestPrepare, so theprepareSeqbump lands before the dispatch, and the in-process fallback re-runs a prepare requested while one is in flight once, ascoalesced(plan §7.1). Createpackages/agent/src/app-builds/app-builds.service.ts(requestPreparewith theprepareSeqmarker of plan §7.2,recordProviderRun(workId, run, source)— the shared accept rules of plan §7.5,requestRebuildwith 10 s dedupe and 10/hour limit,cancel,startVerification(workId, { ref, sha, reuseImageDigest? }) → { buildId },getDetail(workId, buildId),applySnapshot(buildId, snapshot),finalize(buildId)— digest confirmation, verdict, receipt, Activity, events — andpublish(build, event), the single Activity + event writer of plan §7.8),deployable-verdict.ts(plan §5.1, first failing clause wins,computeBuildInputsHashfrom@ever-works/contracts),app-build-failure-copy.ts(class → i18n key + params, reused for the agent hand-off in T44),app-build-pull-token.service.ts(save(workId, userId, token)— plan §4.12,APW05-G07),packages/agent/src/app-builds/app-builds.module.ts,index.ts. Create alsopackages/agent/src/events/app-build.events.ts(the five classes,AppBuildEventPayloadand the explicit status → event map of plan §7.8 —APW05-G05) and Modifypackages/agent/src/events/index.tsto export them. Modifypackages/agent/src/entities/plugin-usage-event.entity.ts— addBUILD = 'build'toPluginUsageCapability(varchar; no migration). Modifypackages/agent/src/entities/activity-log.types.ts— addAPP_BUILD = 'app_build'toActivityActionType;actionstringsapp.build.queued|started|succeeded|failed|cancelled(Resolution R-2). Modifypackages/contracts/src/apps/builds.ts— exportAPP_BUILD_SWEEP_CRON,AppVerificationPlanand the purecomputeBuildInputsHash(APW05-G03,G11,G20). Test:packages/agent/src/app-builds/__tests__/deployable-verdict.spec.ts— one case per clause in order: a pull-request Build →pullRequest, a verification Build →verification(ACC-05-23),specValidAtCommit: false→specInvalid, a Build whosebuildInputsHashdiffers from the current hash after a rotation →staleInputs(ACC-05-16), a NULLbuildInputsHashorsecretsSyncedAt→staleInputs, a preparation that synced zero values (the hash of the empty list) → passes that clause, secret check failed →secretCheckFailed, unconfirmed digest →digestUnconfirmed(APW05-G03).packages/agent/src/app-builds/__tests__/app-builds.service.spec.ts— a Rebuild returns within 2 s with a slow dispatcher mocked (dispatch not awaited past the insert) and, with a null-returning prepare dispatcher, the prepare runner is still invoked in-process exactly once and the 2 s budget holds (APW05-G20); dedupe inside/outside 10 s returns the same/new Build; 11th rebuild →rebuildRateLimitedwith minutes (ACC-05-08);cancelon a running Build callscancelBuildand the next snapshot finalisescancelled(ACC-05-09); receipt recorded once withunits, payerworkspace, operationbuild.run,costCents: 0and no credit ledger call (ACC-05-20); Activity rows carryactionType: 'app_build'and metadata with no value and no excerpt line;requestRebuildandstartVerificationpublish exactly oneapp.build.queuedwith the plan §7.8 payload,applySnapshotpublishesapp.build.startedexactly once across repeatedrunningsnapshots, a snapshot first seen ascompletedstill publishesstartedbeforesucceeded, and ablockedBuild publishes nothing (APW05-G05); the plan JSON is built from the fixture spec atsha, validates againstverify-plan.schema.json, is refused above 60,000 characters or 12 GiB before dispatch, contains no value from the env-source fake, and a verification Build's every transition callsAPP_PROVISION_EVENTS_PORT.buildUpdated— 3 times for queued → running → succeeded, once for a pre-dispatchmissingBuildValuesblock, zero times for a push Build, and a throwing port never fails the job (APW05-G11);AppBuildPullTokenService.savemaps the three refusal codes, returnspullTokenNoImageYetwith no Build, and writespullTokenExpiresAtthroughwritePlatformManagedWorkSettings(APW05-G07);packages/agent/src/events/__tests__/events.spec.ts— the five names are unique and dotted (APW05-G05). Done when:pnpm --filter @ever-works/agent test -- deployable-verdict app-builds.service app-build-pull-token.serviceis green. -
T18. Dispatchers. Create
packages/agent/src/tasks/app-build-prepare-dispatcher.ts,app-build-prepare.types.ts,app-build-watch-dispatcher.ts,app-build-watch.types.ts(plan §7.1); both interfaces returnPromise<string | null>and are modelled onwork-import-dispatcher.ts, not on the throwingkb-reembed-work-dispatcher.ts(APW05-G20). Modifypackages/agent/src/tasks/index.ts(exports) andpackages/agent/src/tasks/_tasks-symbols.ts(APP_BUILD_PREPARE_DISPATCHER,APP_BUILD_WATCH_DISPATCHER, alphabetical). Modifypackages/agent/src/tasks/job-runtime.providers.ts— add both symbols toDISPATCHER_SYMBOLSand update its arity JSDoc, counted off the merged array. Modifypackages/agent/src/tasks/__tests__/job-runtime.providers.spec.ts— raise thetoHaveLengthpin by two (14 ondevelop@ 873274c9f; recount at merge after APW-02/03/04) and add both symbols to the expectedSet. Modifypackages/tasks/src/trigger/trigger.service.ts—dispatchAppBuildPrepare(payload)anddispatchAppBuildWatch(payload), bothPromise<string | null>: returnnullwhenensureConfigured()is false ortrigger()throws, the same shape asdispatchWorkspaceBackup; tagswork:<workId>/build:<buildId>,concurrencyKeyapp-build-prepare:<workId>/app-build-watch:<buildId>.trigger.module.tsneeds no edit —buildJobRuntimeProviders()binds everyDISPATCHER_SYMBOLSentry. Test:packages/agent/src/tasks/tasks.spec.tspasses with the two new symbols counted automatically;job-runtime.providers.spec.ts(arity andSet);packages/tasks/src/__tests__/trigger.service.spec.ts(unconfigured →null; a throwntrigger→null; a configured call passes tags andconcurrencyKey). Done when:pnpm --filter @ever-works/agent test -- tasks.spec job-runtime.providersandpnpm --filter @ever-works/tasks test -- trigger.serviceare green. -
T19.
app-build-preparejob. Createpackages/agent/src/app-builds/app-build-prepare.runner.ts(plan §7.2: strategy gate, build values through APW-07, runner selection (an absentbuild.resources.memorymeans the runner's maximum and never blocks —APW05-G14),prepareRepository(incl. checks), the single-transaction preparation-row upsert of plan §3.1b (APW05-G03), the verification bootstrap of plan §4.6 step 0 (APW05-G02), the blocked-Build retry of plan §7.2 step 7 withactionsEnabledhandled aftersetActionsPermissions?(APW05-G15), andstartBuildfor requested Builds), theprepareSeqcoalescing loop of plan §7.2 (APW05-G17) andpackages/tasks/src/tasks/trigger/app-build-prepare.task.ts; Modifypackages/tasks/src/tasks/trigger/index.ts. When the prepare dispatcher returnsnull, run the runner in-process, unawaited, under the same lock (plan §7.1,APW05-G20). Test:packages/agent/src/app-builds/__tests__/app-build-prepare.runner.spec.ts—image/nonewithout checks: no Build and no plugin call;autoblocks a requested Build withstrategyNotSupported(R-13); a missing required build value blocks the requested Build naming it and nothing is dispatched (ACC-05-14); runner too small blocks with both numbers (ACC-05-22) and an absent memory does not block (APW05-G14); concurrent dispatch runs the passes the coalescing loop allows and re-runs whileprepareSeqkeeps moving, and a dispatch that cannot take the lock exits asskippedwithout losing the request (APW05-G17); aspecAppliedprepare with no Build upserts the preparation row with the hash,secretsSyncedAt, the names andworkflowState: 'committed'; a second prepare after an env entry was removed passes that name inpreviouslyWrittenSecretNamesand drops it from the row; a checks-only prepare leaves the three secret fields untouched (APW05-G03); a verification request on an App Work with no workflow and no applied spec delivers exactly one bootstrap commit and then dispatches on the tracked branch, and on a Link App Work it blocks withworkflowPendingand dispatches nothing (APW05-G02); amanualBuild blocked formissingBuildValuesbecomesqueuedwith a clearedblockedReasonand publishesapp.build.queuedonce the value exists, while an older blocked manual Build is cancelled assuperseded(APW05-G15). Done when:pnpm --filter @ever-works/agent test -- app-build-prepare.runneris green. -
T19a. Webhook installation — the optional latency path (
APW05-G01,GAP-07). Modifypackages/agent/src/app-builds/app-build-prepare.runner.ts— afterprepareRepositorysucceeds and the workflow exists on the tracked branch, install or update theworkflow_runhook through APW-02'sGitFacadeService.createWebhook?with{ url: <config.webAppUrl() + '/api/ingest/github/events'>, secret: <the owner's github-plugin webhookSecret>, events: ['workflow_run'] }, and persistwebhookId+webhookState(installed|skipped|permissionMissing) on the preparation row (plan §7.7). Create alsoAppBuildsService.releaseRepository(workId)— best-effortdeleteWebhook?, warning-logged, never fatal (GAP-07's removal half). Test:packages/agent/src/app-builds/__tests__/app-build-webhook.spec.ts— the call uses the owner'swebhookSecretand exactly['workflow_run']; it is skipped when the secret is unset, when the receiver URL failsisSafeWebhookUrl, when the token is an App installation token, and for an upstream relation; apermission_missingresult never blocks a Build and never changes a status; the secret appears in no log, error, Activity row or telemetry payload;releaseRepositoryswallows adeleteWebhook?failure. Done when:pnpm --filter @ever-works/agent test -- app-build-webhookis green. -
T20.
app-build-watchjob. Createpackages/agent/src/app-builds/app-build-watch.runner.ts(plan §7.3, including the preparation-row re-stamp whenstartedAtis first set —APW05-G03) andpackages/tasks/src/tasks/trigger/app-build-watch.task.ts. When the watch dispatcher returnsnull, run the runner in-process, unawaited, capped at 10 concurrent runs per API process (plan §7.1,APW05-G20). Test:packages/agent/src/app-builds/__tests__/app-build-watch.runner.spec.ts— lease prevents a second concurrent observation; terminal transition finalises exactly once across 3 deliveries; the ordered sequenceapp.build.queued → app.build.started → app.build.succeededpublishes exactly once each, a snapshot first seen ascompletedstill yieldsstartedbeforesucceeded, andapp.build.succeededcarries branch, trigger,deployableandimageDigest(APW05-G05);app.build.succeededis emitted only when deployable is computed (with the flag in the payload); a succeeded push Build with a confirmed digest and thesha-<40>andbranch-<slug>tags — and nolatest— isdeployable: true(ACC-05-07); a push Build created by the consumer after aspecAppliedprepare, whose run started aftersecretsSyncedAt, is likewisedeployable: true, and that same Build isstaleInputswhen a sync finished afterstartedAt(APW05-G03); a verification Build's per-run secret is deleted on its terminal transition; a verification Build going queued → running → succeeded callsAPP_PROVISION_EVENTS_PORT.buildUpdatedthree times, and a throwing port does not fail the watch (APW05-G11). Done when:pnpm --filter @ever-works/agent test -- app-build-watch.runneris green. -
T21.
app-build-sweepjob. Createpackages/agent/src/app-builds/app-build-sweep.service.tsandpackages/tasks/src/tasks/trigger/app-build-sweep.task.ts(schedules.task({ id: 'app-build-sweep', cron: APP_BUILD_SWEEP_CRON })from@ever-works/contracts, same shape aspackages/tasks/src/tasks/trigger/deploy-ready-poller.task.ts), and run discovery before the silent pass by calling T21a'sAppBuildRunDiscoveryService(plan §7.4a,APW05-G01). Create alsoapps/api/src/app-builds/app-build-sweep-cron.service.ts— the same pass from the API process when Trigger.dev is not the configured runtime, gated onconfig.trigger.shouldUseTrigger(), likeSkillReadinessSweepCronService: the cron callsAppBuildSweepService.runSweep(), which takesapp-builds:sweepitself (ttl 90 s); the cron does not wrap it in a secondrunExclusive, because the Trigger task reaches the service over RPC, where a lock callback cannot cross (corrected 2026-09-25). Register it inapps/api/src/app-builds/app-builds.module.ts(new, created by T21; T23 and T24 extend it) and importAppBuildsModuleinapps/api/src/api.module.ts(plan §7.4,APW05-G20). Status (2026-09-25, first slice):AppBuildSweepServiceruns two passes underapp-builds:sweep(90 s lease, 5 min hard lifetime): the §9.2 re-drive (a queued manual or verification Build withdispatchedAt IS NULLand a queue age in [90 s, 450 s) getsrequestPrepare(workId, 'sweep')once per Work) and the never-adopted half of §7.4's lost rule (providerRunId IS NULL, open, pastmax(queuedAt, dispatchedAt) + 5 min + timeoutMinutes + 30→markLost, thenfinalizeonly for rows the pass moved). The Trigger task and the API cron fallback both callrunSweep(). Still open in T21: the silent-Build watch dispatch, the adopted half of the lost rule (startedAt + timeoutMinutes + 30), thedigestUnconfirmedrecheck, deleting orphaned verification secrets and T21a's discovery — all in the same service. Dormant in production until a Work can be prepared (the facade answerspluginUnavailable). Test:packages/agent/src/app-builds/__tests__/app-build-sweep.service.spec.ts— 250 silent Builds → 200 dispatched, oldest first; a Build silent for 91 s is dispatched so a terminal status lands within the next 2-minute tick (≤ 3 min, ACC-05-11); lost thresholds for adopted and never-adopted Builds;digestUnconfirmedrechecked after a pull token is saved; an orphaned verification secret is deleted.apps/api/src/app-builds/app-build-sweep-cron.service.spec.ts— skipped whenshouldUseTrigger()is true; runsrunSweep()once when false (the service takes the lock); a held lock means skip; a sweep error is logged, not thrown; a watch dispatch returningnullruns the watch runner in-process with at most 10 concurrent (APW05-G20). Done when:pnpm --filter @ever-works/agent test -- app-build-sweep.serviceandpnpm --filter ever-works-api test -- app-build-sweep-cron.serviceare green. -
T21a. Run discovery — Builds without a delivery (
APW05-G01,GAP-07). Createpackages/agent/src/app-builds/app-build-run-discovery.service.ts(plan §7.4a): up to 100 App Works whose applied strategy isdockerfileand whoseworkflowSha256is set, stalestrunsCheckedAtfirst;BuildFacadeService→IBuildPlugin.listRecentRuns?with the storedrunsEtag;notModifiedonly stampsrunsCheckedAt; every run withcreatedAt ≥ workflowWrittenAtgoes toAppBuildsService.recordProviderRun(workId, run, 'poll'); the cursor (runsEtag,runsCheckedAt) is stored on the preparation row (plan §3.1b), not in a plugin setting. Test:packages/agent/src/app-builds/__tests__/app-build-run-discovery.service.spec.ts— with delivery disabled, a push run is recorded as Build #n on the next tick and its latercompletedis visible within 3 minutes (ACC-05-11); a delivery plus a poll of the same run yields one Build and oneapp.build.queued; a fork pull-request run → no Build;image/none/auto→ none; a run created beforeworkflowWrittenAt→ none; a 304 → no row writes; 150 eligible App Works → 100 checked, stalest first; a plugin without the method → skipped; a discovery failure does not stop the silent pass. Done when:pnpm --filter @ever-works/agent test -- app-build-run-discovery.serviceis green. -
T22. Event listeners. Create
packages/agent/src/app-builds/app-builds.listener.ts—app.spec.appliedwithchangedBlocksincludingbuildorchecks→ prepare;app.env.changedwith a build-phase name → prepare, debounced 10 s per Work; pull token saved → prepare (reasonpullTokenSaved); a save of the Work-scoped settings of the resolved build plugin → prepare (reasonsettingsChanged), which is what clearsrunnerTooSmall(APW05-G15). Test:packages/agent/src/app-builds/__tests__/app-builds.listener.spec.ts— runtime-only names do not dispatch; 5 changes within 10 s dispatch once; dispatch happens within 60 s of the first change, soEW_secrets re-sync inside the SLA (ACC-05-13); achecks-only change dispatches prepare; a settings change dispatches prepare and the newest blocked manual Build becomesqueued(APW05-G15). Done when:pnpm --filter @ever-works/agent test -- app-builds.listeneris green.
P1.5 — API and webhook intake
-
T23. Builds controller. Create
apps/api/src/app-builds/app-builds.controller.ts,apps/api/src/app-builds/dto/app-builds.dto.ts(ListAppBuildsQueryDtowithpage,pageSize1–100,status,trigger,branch,pullRequest;CreateAppBuildDtowith optional 40-hexcommitSha;SaveAppBuildPullTokenDtowith atokenstring, for the newPUT /api/works/:id/builds/pull-tokenroute of plan §5 —APW05-G07). Modifyapps/api/src/app-builds/app-builds.module.ts— it already exists (created by T21 for the sweep cron, corrected 2026-09-25); add the controller to it. Routes and codes exactly as plan §5;ensureCanView/ensureCanEditfrompackages/agent/src/services/work-ownership.service.ts; non-appkind → 404. The list response'sworkflowfield reads the preparation row ({ state: 'none', pullRequestUrl: null }when there is none —APW05-G03), and the pull-token route delegates toAppBuildPullTokenService, returning its stable codes and never the token (APW05-G07).apps/api/src/api.module.tsalready importsAppBuildsModule(T21), so nothing is added there. Test:apps/api/src/app-builds/app-builds.controller.spec.ts— foreign id 404 on read, Rebuild and Cancel (ACC-05-24); viewer 403 on POST routes with code; 202 shape withdedupedand a second POST inside 10 s returning the same Build (ACC-05-08); 429rebuildRateLimitedwithretryAfterMinutes; 202 on cancel of a running Build and 409notCancellableon a terminal one (ACC-05-09);nothingToBuildforimagestrategy; no response containspullTokenor its value (ACC-05-21). Done when:pnpm --filter ever-works-api test -- app-builds.controlleris green and Swagger lists the four routes under aBuildstag. -
T24.
workflow_runconsumer. Createapps/api/src/app-builds/app-build-workflow-run.consumer.ts(plan §7.5), registered onGitHubWebhookDispatcherServiceinonModuleInit. The consumer maps the delivery to aBuildRunRefand callsAppBuildsService.recordProviderRun(workId, run, 'event')— the same entry point run discovery uses — so the accept rules live in one place (APW05-G01). Modifyapps/api/src/app-builds/app-builds.module.tsto import the ingest module's dispatcher, and eitherapps/api/src/ingest/ingest.module.tsto addGitHubWebhookDispatcherServicetoexports(with its spec extended) or register this consumer as a provider insideIngestModule— the module exports only[EventIngestModule]today (apps/api/src/ingest/ingest.module.ts:131-149), so the "import the exported dispatcher" instruction cannot work as written (APW05-G21). Test:apps/api/src/app-builds/app-build-workflow-run.consumer.spec.ts— other workflow paths ignored; repository not an App Work ignored; runs of an App Work whose applied strategy isimage,noneorautoignored (ACC-05-30); apull_requestrun whose head repository differs creates no Build (ACC-05-06);requestedcreates Build #nqueuedand publishesapp.build.queuedonce, while a duplicate delivery publishes nothing (APW05-G05); manual run adopted bydisplay_title; a push or pull-request insert stampsbuildInputsHash,buildSecretNamesandsecretsSyncedAtfrom the preparation row in the same transaction, and a push run whileworkflowState = 'pullRequestOpen'dispatchesapp-build-prepare { reason: 'workflowMerged' }(APW05-G03); a commit inside a completed upstream-sync range stampssyncOrigin: 'upstreamSync'withsyncFromSha/syncToSha, and any other commit stampsnonewith both NULL (APW04-G06); a null watch dispatch runs the watch runner in-process without being awaited (APW05-G20);apps/api/src/ingest/github/github-check-intake.service.spec.tsstill passes unchanged. Done when:pnpm --filter ever-works-api test -- app-build-workflow-run.consumer github-check-intakeis green and the consumer performs zero outbound HTTP calls (asserted). Done when:pnpm --filter ever-works-api test -- app-build-workflow-run.consumer github-check-intakeis green and the consumer performs zero outbound HTTP calls (asserted).
P1.6 — Web
-
T25. Client, actions and route. Create
apps/web/src/lib/api/app-builds.ts,apps/web/src/app/actions/dashboard/app-builds.ts(six actions in plan §6.2),apps/web/src/app/[locale]/(dashboard)/works/[id]/builds/page.tsx. Modifyapps/web/src/lib/constants.ts—DASHBOARD_WORK_BUILDS. Modifyapps/web/src/components/works/detail/WorkTabs.tsx— Builds tab after Pull requests, visible whengetWorkCapabilities(work.kind).builds. Test:apps/web/src/components/works/detail/WorkTabs.unit.spec.tsx— Builds tab hidden fordirectory, shown for a kind withbuilds: true. Done when:pnpm --filter ever-works-web test -- WorkTabsis green and a non-app Work shows no new tab. -
T26. Builds list. Create
apps/web/src/components/works/detail/builds/BuildsPageClient.tsx,BuildRow.tsx,BuildStatusChip.tsx,BuildBlockedNotice.tsx— URL-backed filters and page, 10 s polling while any row is queued/running (paused when hidden, stops after 60 minutes), empty/loading/error states, viewer-disabled actions. Test:apps/web/src/components/works/detail/builds/BuildsPageClient.unit.spec.tsx— poll starts/stops; filters round-trip through the URL; each blocked reason renders its action; amissingBuildValuesnotice names the value and links Set it (ACC-05-14); a viewer sees Rebuild and Cancel disabled with the edit-access copy (ACC-05-24). Done when:pnpm --filter ever-works-web test -- BuildsPageClientis green. -
T27. Detail drawer and failure panel. Create
apps/web/src/components/works/detail/builds/BuildDetailDrawer.tsx(?build=<number>, copy digest, receipt with the checks-minutes line, verification results, and the verification approval notice of plan §4.7b when prompted values were withheld) andBuildFailurePanel.tsx(title + suggestion per class with params; excerpt in<pre>; Ask an agent to fix this opening APW-08'sRequestChangeDialogwithbuildIdpreset throughrequestAppChangeAction({ workId, buildId, request? })→POST /api/works/:id/evolve, hidden when APW-08 is absent or the viewer lacks edit access —APW05-G12). Test:apps/web/src/components/works/detail/builds/BuildFailurePanel.unit.spec.tsx— all 14 classes render translated title and suggestion with params (ACC-05-17);BuildDetailDrawer.unit.spec.tsx—Escreturns focus to the row;Ccopies the image reference; the receipt reads payer "your GitHub account" and no credits (ACC-05-20); verification rows render one line per job and smoke result (ACC-05-23). Done when:pnpm --filter ever-works-web test -- BuildFailurePanel BuildDetailDraweris green. -
T28 (parallel with T27). Overview card, pull token and settings dialogs. Create
apps/web/src/components/works/detail/overview/LatestBuildCard.tsx,apps/web/src/components/works/detail/builds/PullTokenDialog.tsx,BuildSettingsDialog.tsx(plugin id taken from the list response'sprovider.pluginId). Modifyapps/web/src/components/works/detail/overview/WorkInfo.tsxto render the card whenbuildsis true. Test:apps/web/src/components/works/detail/builds/PullTokenDialog.unit.spec.tsx— the three refusal codes map to copy; the input is cleared after save and never re-populated (ACC-05-21);apps/web/src/components/works/detail/overview/LatestBuildCard.unit.spec.tsx— deployable same vs other commit. Done when:pnpm --filter ever-works-web test -- PullTokenDialog LatestBuildCardis green and no web source file contains the literalgithub-actions-build.
P1.7 — i18n, tests, docs
-
T29. i18n. Modify
apps/web/messages/en.json— thedashboard.workDetail.tabs.buildskeys and thedashboard.workDetail.buildstree from plan §8; mirror into the 20 sibling locale files inapps/web/messages/. Test: runnode apps/web/scripts/sync-locale-parity.mjsthengit diff --exit-code apps/web/messages— the script adds zero keys because all 21 files already carry every new key (ACC-05-25); a grep over the new leaves finds no.. Done when: both commands exit 0 in the PR. -
T30. E2E. Seeding recipe (rewritten 2026-09-17,
APW05-G18). The PR lane runs withEVER_WORKS_E2E_FAKES=1, and no route or environment variable is added to seed Builds — every Build in these specs is produced by the realapp-build-preparepath against APW-13's fake GitHub and stops before any build-provider call: 1. register the owner withregisterUserViaAPI(APW-13 T6); 2. seed the fake GitHub through/_control/seed(APW-13 T2) with a repository whose.works/works.ymlis either APW-13'smissing-value.works.ymlprofile (→missingBuildValues) or adockerfilespec withbuild.resources.memoryGiB: 12on a private repository and no larger runner set (→runnerTooSmall); 3. create the App Work with APW-13'screateAppWorkhelper (T6); 4.POST /api/works/:id/buildswithoutcommitSha; 5.expect.pollonGET /api/works/:id/builds/:buildIduntilstatusisblockedandblockedReasonis the expected one. These Builds stop inapp-build-preparesteps 3–4 (plan §7.2), beforeprepareRepositoryorstartBuild, so the detail showsproviderRunId: null. Nowork_buildsrow is inserted directly, and no seeding route exists. Createapps/web/e2e/app-builds-tab.spec.ts(list, URL-backed filters, drawer on a blocked Build, viewer sees Rebuild and Cancel disabled, another account's Build id answers 404 on read, Rebuild and Cancel — ACC-05-24),apps/web/e2e/app-builds-failure.spec.ts(the blockedmissingBuildValuesnotice names the value — ACC-05-14;runnerTooSmallshows both numbers — ACC-05-22),apps/web/e2e/app-builds-a11y.spec.ts(axe on the tab, on a blocked Build's drawer, and onBuildSettingsDialogandPullTokenDialogopened without submitting; the keys↑↓,Enter,EscandR— ACC-05-25). Recorded move (APW-13 plan §8.3 — a path the fake switch cannot see leaves the PR lane). The build plugin has its own Octokit and GHCR clients, and T14 keeps them onapi.github.comandghcr.io, so provider-set failure classes (ACC-05-17), copying the digest withC, pull-token validation and "Deploy blocked for a private image without a token" (ACC-05-21) are not PR-lane e2e and noapp-builds-pull-token.spec.tsis created. They are covered by T13 and T27 (all 14 classes' copy;C), by T14, T16, T23 and T28 (pull token), and by APW-13 T59 live on dev (failure classes against real Builds). Test:pnpm --filter ever-works-web test:e2e app-builds-(the three specs above are the test). Done when: all three pass locally and in thee2e.ymllane, every Build they create isblockedwithproviderRunId: null, and rows usegetByTestId. -
T31. Live acceptance wiring. Modify
docs/specs/features/app-works/ACCEPTANCE.md(the APW-05 table in §3 — coordinate with the file's owner) — mapACC-05-01…23,29,30, 31 and 32 to the APW-13 harness scenario names and to APW-13's fixture branches ofever-works/app-fixture-hello— the branchesvariant/<name>:variant/build-oom,variant/services-postgres,variant/missing-value,variant/secret-in-image,variant/dockerfile-error,variant/build-timeoutandvariant/disk-full, which APW-13 T58 also creates for ACC-05-17'stimeoutanddiskFullcases (short names used elsewhere in this epic map tovariant/<name>; Resolution R-23: this epic references them and creates none —APW05-G23). The failure class name in that mapping isoutOfMemory, matching spec §6.3 and ACC-05-17, neverout_of_memory. Test:rg -n "ACC-05-(0[1-9]|1[0-9]|2[0-3]|29|30|31|32)" docs/specs/features/app-works/ACCEPTANCE.mdlists every id with a scenario name, andgit ls-remote https://github.com/ever-works/app-fixture-helloshows each referenced branch once APW-13 T58 has landed. Done when: every P1 ACC-05 id names a scenario and no APW-05 task creates a fixture branch. -
T32. Docs. Create
docs/features/app-builds.md— what the workflow file is, build values, runners and sizes, App checks on pull requests, failure classes, pull tokens, receipts. Modifyapps/docs/sidebarsPlatform.ts(manual sidebar),docs/plugin-system/built-in-plugins.md(addgithub-actions-build),docs/plugin-system/plugin-categories.md(addbuild). Test:pnpm --filter ever-works-docs build. Done when: the build reports no broken-link warnings. -
T33. P1 ship gate. Modify
docs/specs/features/app-works/TRACKER.md— tick APW-05 P1. Test: rootpnpm format:check,pnpm lint,pnpm type-check,pnpm test,pnpm build; ACC-05-01…25, 29 and 30 walked on the fixture repository. Done when: every command exits 0 and each walked criterion is recorded against its scenario.
Phase P3 — Ever Works Apps builder
Delivers spec FR-55…FR-59 and ACC-05-26…28. Starts only after APW-10 publishes the isolated build controller contract and its launch gate passes (Resolution R-24: sandboxed in-zone builds are Wave 3, gate item LG-24). Infrastructure specifics stay in the private operations repository.
-
T34. Supply-chain columns. Create
apps/api/src/migrations/1792050100000-AddWorkBuildSupplyChain.ts(scanSummary,signatureState,blockedEgressHosts); Modifypackages/agent/src/entities/work-build.entity.ts; Modifypackages/contracts/src/apps/builds.ts(AppBuildDetail.supplyChain). Test:apps/api/src/migrations/__tests__/AddWorkBuildSupplyChain.spec.ts— additive only (threeADD COLUMN, no other statement);down()drops only those columns. Done when:pnpm --filter ever-works-api test -- AddWorkBuildSupplyChainis green and the migration runs up/down on both databases. -
T35.
apps-builderplugin. Createpackages/plugins/apps-builder/(package@ever-works/apps-builder-plugin; manifestcategory: build,buildKind: 'apps-builder';prepareRepository= no repository writes;startBuild→IAppsTierProvider.submitBuild?with anAppBuildrequest (APW-10 plan §3.2: sealed build values,sealedSourceToken, caps);getBuild→getBuild?; caps fromAPP_BUILD_MANAGED_*; concurrency 1 per App Work and 3 per account →managedConcurrencyLimit). No builder endpoint or credential setting exists in this plugin. Modifypackages/agent/src/facades/build.facade.ts— resolveapps-builderonly whenAppsTierPolicy.isOpen()andmanagedScope() === 'any'and the target is Ever Works Apps; otherwise never (Resolution R-5 — no read ofEVER_WORKS_APPS_MANAGED_ENABLED). Test:packages/plugins/apps-builder/src/__tests__/apps-builder.plugin.spec.tsagainst anIAppsTierProviderfake — caps clamped to maxima; token lifetimes requested (≤ 1 h source, ≤ timeout + 60 min push); a snapshot whoseblockedEgressHostslists hosts maps to failure classegressBlockedwith at most 10 hosts (ACC-05-27); the request never asks for a privileged workload. Extendpackages/agent/src/facades/__tests__/build.facade.spec.ts— refused for Your cluster, for scopeverified-blueprints, and when the policy is closed; a spy proves the env var is never read. Done when:pnpm --filter @ever-works/apps-builder-plugin testandpnpm --filter @ever-works/agent test -- build.facadeare green. -
T36. Verdict, receipts and UI for the managed tier. Modify
packages/agent/src/app-builds/deployable-verdict.ts(signature and fixable-critical clauses),packages/agent/src/app-builds/app-builds.service.ts(receipt payerplatform),apps/web/src/components/works/detail/builds/BuildDetailDrawer.tsx(scan counts, Signed), i18n (dashboard.workDetail.builds.supplyChain.*,failure.egressBlocked.*) in all 21 files underapps/web/messages/. Test: extendpackages/agent/src/app-builds/__tests__/deployable-verdict.spec.tswithunsigned(unsigned and foreign-signed) andcriticalVulnerability(ACC-05-28); extendapps/web/src/components/works/detail/builds/BuildDetailDrawer.unit.spec.tsx— scan counts and Signed render, and a Build row withcompletedAtshows no running workload (ACC-05-26). Done when:pnpm --filter @ever-works/agent test -- deployable-verdictandpnpm --filter ever-works-web test -- BuildDetailDrawerare green. -
T37. P3 acceptance and ship gate. Modify
docs/specs/features/app-works/ACCEPTANCE.md(APW-05 rows ACC-05-26…28, with the owner's agreement) anddocs/specs/features/app-works/TRACKER.md(tick P3). Test: ACC-05-26…28 walked in APW-10's gated environment (manual, evidence in the private operations repository); rootpnpm format:check,pnpm lint,pnpm type-check,pnpm test,pnpm build. Done when: the three criteria are recorded green and every root command exits 0.
Cross-phase closing tasks
-
T38. Telemetry. Create
packages/agent/src/app-builds/app-builds.telemetry.tsemitting the events in plan §9.1 through APW-01 T36's pattern: an@Optional()sink token bound by the API — reuseAPP_WORKS_TELEMETRY_SINK/AppWorksTelemetryService(packages/agent/src/app-works/app-works-telemetry.service.ts) rather than importing the monitoring package, on whichpackages/agenttakes no dependency (corrected 2026-09-25); Modifypackages/agent/src/app-builds/app-builds.service.ts,app-build-sweep.service.tsandpackages/plugins/github-actions-build/src/repo/workflow-writer.ts(via a callback) to call it. Test:packages/agent/src/app-builds/__tests__/app-builds.telemetry.spec.ts— no event payload contains an env name, a check name or command, a repository name, a commit message, a log line or a token. Done when:pnpm --filter @ever-works/agent test -- app-builds.telemetryis green. -
T39. Contracts confirmation. Modify
docs/specs/features/app-works/TRACKER.md(APW-05 row) and, through its owner,CONTRACTS.mdif the merged code differs from the rows this epic added (jobsapp-build-prepare,app-build-sweep; routePOST /api/works/:id/builds/:buildId/cancel;checkImageAccess?and verification inputs onIBuildPlugin; the §3 checks-job row as a matrix job (R-9); the §9 repository conventions incl.EW_VERIFY__PROMPTED). Test:rg -n "app-build-prepare|app-build-sweep|builds/:buildId/cancel|checkImageAccess|Ever Works check|EW_VERIFY__PROMPTED" apps packagesfinds each name in merged code. Done when: every CONTRACTS row this epic owns matches a hit, or the row was corrected. -
T40. Update statuses. Modify
docs/specs/features/app-works/APW-05-builds/spec.md,plan.mdand this file —Implemented/Done. Test: re-check every gate in plan §12 against the merged code with a reviewer. Done when: each gate is ticked with a link to the code or test that proves it, and known gaps remain listed.
Program audit follow-ups (added 2026-09-17)
-
T41. The
checksmatrix job (Resolution R-9). Createpackages/plugins/github-actions-build/src/workflow/checks-job.ts(plan §2.4, §4.14): one matrix row perspec.checks[]entry in declared order (name,required,timeoutMinutes = ceil(timeoutSeconds / 60),commandB64), job-levelname: "Ever Works check: ${{ matrix.check.name }}",if:pull request from the same repository,permissions: { contents: read },continue-on-error: ${{ !matrix.check.required }},fail-fast: false,max-parallel: 5, checkout of the pull request head withpersist-credentials: false, and the base64 run step. Modifypackages/plugins/github-actions-build/src/workflow/generator.tsandsrc/workflow/inputs-hash.ts— emit the job afterbuildwhenchecksis non-empty; addchecks(withcommandSha256) to the canonical inputs. Ownership (APW05-G04). T41 and T42 are the only implementers of thechecksjob, and the single golden issrc/__tests__/golden/checks.yml; APW-08 T15 consumes the check runs and does not edit this generator, and no other epic uses the nameapp-checks-two.yml. The trigger is same-repository pull requests into the tracked branch only — nopush, noworkflow_dispatch(CONTRACTS §3's row is corrected to match FR-65 by this fix pass). Test:packages/plugins/github-actions-build/src/__tests__/checks-job.spec.ts— two checks (one advisory) → two matrix rows and the exact job name expression; the job's YAML containscontents: readand nothing else underpermissions, nosecrets.and noEW_token other thanEW_CHECK_COMMAND_B64, nocache-key, noneeds:; the advisory row setsrequired: falsesocontinue-on-erroris true; the same-repository guard is present (ACC-05-29); a command containing${{ secrets.X }}, a backtick and both quote kinds appears nowhere in clear text and decodes back byte-identical;timeoutSeconds61 → 2 minutes; a golden filesrc/__tests__/golden/checks.ymlstays byte-stable. Done when:pnpm --filter @ever-works/github-actions-build-plugin test -- checks-job generatoris green and the golden passesactionlintlocally. -
T42. Checks-only workflow, observation and receipts for checks (R-9). Modify
packages/plugins/github-actions-build/src/workflow/generator.ts(checks-only file forimage/nonestrategies:on: pull_requestonly,permissions: {}, concurrency,checksjob),src/repo/workflow-writer.ts(write checks-only files through the same delivery rules; propose removal by pull request when neither a build nor a check remains),packages/agent/src/app-builds/app-build-prepare.runner.ts(runprepareRepositorywithout secret sync forimage/none/autowhen checks exist),packages/agent/src/app-builds/app-builds.service.ts(receipt metadatachecksBillableMinutes). Test: extendpackages/plugins/github-actions-build/src/__tests__/generator.spec.ts—image+ one check → a file with nobuildjob, nopushorworkflow_dispatchtrigger and nosecrets.reference; removing the check yields no workflow content (ACC-05-30). Extendpackages/agent/src/app-builds/__tests__/app-build-prepare.runner.spec.ts—image+ checks callsprepareRepositorywith zero values and creates no Build (ACC-05-30). Extendpackages/agent/src/app-builds/__tests__/app-builds.service.spec.ts— a pull request Build with 3 check minutes recordschecksBillableMinutes: 3insideunitsand the Build's status anddeployableare identical with checks green or red (ACC-05-29). Done when: the three specs are green through their package commands. -
T43. Verification inputs from APW-07's ephemeral mode (Resolution R-10). Modify
packages/agent/src/app-builds/app-builds.service.ts—startVerification(workId, { ref, sha, reuseImageDigest? }) → { buildId }asksAPP_RUNTIME_ENV_SOURCE's ephemeral mode for the value-free runner recipe (APW-07 plan §4.6.1; typed fake until APW-07 lands), buildscomponents,dependencies,jobsandsmokefromAppSpecService.getEffectiveSpec(workId, sha), validates the plan againstverify-plan.schema.jsonwith ajv before dispatch (APW05-G11), refuses withmissingBuildValueswhen a required prompted name is unset, writes the per-runEW_VERIFY__PROMPTEDsecret through the plugin only when T46's approval gate passes (XC-01), recordsverifySecretNames, passesreuseImageDigestwhen a succeeded Build of the same commit has a confirmed digest, dispatches on the tracked branch after the bootstrap file of plan §4.6 step 0 when the App Work has no workflow (APW05-G02), and dispatchesstartBuild({ mode: 'verify', verification, reuseImageDigest }). Modifypackages/agent/src/app-builds/app-build-watch.runner.tsandapp-build-sweep.service.ts— delete the per-run secret. Modifypackages/plugins/github-actions-build/src/github-actions-build.plugin.ts—getBuildfillsBuildSnapshot.verification{ jobs[], componentsReady, smoke[] }from the result artifact. Test: extendpackages/agent/src/app-builds/__tests__/app-builds.service.spec.ts— the plan JSON contains the recipe and no value from the env source fake (sentinel search); every generated plan validates againstverify-plan.schema.jsonand a plan over 60,000 characters or 12 GiB is refused before dispatch; an unset required prompted name blocks before dispatch; a reused digest setsew_reuse_digestand leaves the plan with nobuildsection; an App Work with no workflow and no applied spec delivers exactly one bootstrap commit and then dispatches on the tracked branch with the proposal head sha, while a Link App Work blocks withworkflowPendingand the pull request URL and dispatches nothing (APW05-G02); a verification Build's queued, running and terminal transitions each callAPP_PROVISION_EVENTS_PORT.buildUpdated, including a pre-dispatchblockedtransition (APW05-G11); the verification Build is created with triggerverificationand is never deployable (ACC-05-23). Extendpackages/plugins/github-actions-build/src/__tests__/run-observer.spec.ts— the artifact's job and smoke rows becomeverificationin the CONTRACTS §3 shape (ACC-05-23). Extendapp-build-watch.runner.spec.tsandapp-build-sweep.service.spec.ts— the per-run secret is deleted exactly once. Done when:pnpm --filter @ever-works/agent test -- app-builds.service app-build-watch.runner app-build-sweep.serviceandpnpm --filter @ever-works/github-actions-build-plugin test -- run-observerare green, and APW-04's verification loop fake receives{ jobs, componentsReady, smoke }. -
T44. Failure hand-off to agents in the user's words (FR-39, ACC-05-19). Create
packages/contracts/src/apps/build-failure-copy.ts—APP_BUILD_FAILURE_COPY_EN(14 classes,{ title, suggestion }templates with{param}placeholders equal to plan §8'sfailure.<class>leaves) and the typeAppBuildFailureHandoff { class; title; suggestion; excerpt: string[]; logsUrl: string | null; untrusted: true }(APW05-G12). Modifypackages/contracts/src/apps/index.ts(export) andpackages/agent/src/app-builds/app-build-failure-copy.ts—forAgent(build): AppBuildFailureHandoff→{ class, title, suggestion, excerpt, logsUrl, untrusted: true }, exported frompackages/agent/src/app-builds/index.tsfor APW-08, with the rule that no consumer re-fetches or re-redacts build logs (FR-38). Named consumer action (APW05-G12). T27's "APW-08's action when present" isrequestAppChangeAction({ workId, buildId, request? })→POST /api/works/:id/evolvewith the failed Build'sbuildIdpreset in APW-08'sRequestChangeDialog; hidden when APW-08 is absent or the viewer lacks edit access. Test:packages/agent/src/app-builds/__tests__/app-build-failure-copy.spec.ts— for a seededoutOfMemoryBuild with{ memory: '7Gi', max: '14 GiB' }the agent payload's title and suggestion equal the English UI copy of spec §6.3 with the same parameters, the excerpt equals the stored redacted excerpt,untrustedistrueand the returned object satisfiesAppBuildFailureHandoff(ACC-05-19).apps/web/src/lib/api/app-build-failure-copy.parity.unit.spec.ts— for every class, theen.jsonleavesdashboard.workDetail.builds.failure.<class>.title|suggestionequalAPP_BUILD_FAILURE_COPY_EN(ACC-05-19). Done when:pnpm --filter @ever-works/agent test -- app-build-failure-copyandpnpm --filter ever-works-web test -- app-build-failure-copy.parityare green. -
T45 (P1, lands with T4–T5; recheck with T34). Classify new tables for workspace backup (R-25). Modify
packages/agent/src/account-transfer/backup/collectors/domain-specs.ts— append to theworksdomain:{ file: 'builds.jsonl', entity: 'WorkBuild', scope: { by: 'parent', column: 'workId', from: 'workIds' } }and{ file: 'build-preparations.jsonl', entity: 'WorkBuildPreparation', scope: { by: 'parent', column: 'workId', from: 'workIds' } }(APW05-G03). Modifypackages/agent/src/account-transfer/backup/redaction.ts—BACKUP_BENIGN_COLUMNSgainsappSpecHash(a digest of an App spec, which holds no secret values),buildSecretNamesandverifySecretNames(secret names, never values),secretsSyncedAt(a timestamp) andsecretCheck(a verdict);ENTITY_DROPPED_COLUMNSgainsWorkBuild: ['buildInputsHash'](derived from the fingerprints of build values, meaningless outside this workspace) andWorkBuildPreparation: ['buildInputsHash'].WorkBuildPreparation.webhookId,runsEtagandrepositoryBlockcarry no secret value; nothing joinsBACKUP_DROPPED_ENTITIES; T34'sscanSummary,signatureStateandblockedEgressHostsneed no entry. Test: extendpackages/agent/src/account-transfer/backup/collectors/collectors.spec.ts—WorkBuildandWorkBuildPreparationare each referenced exactly once, inworks, scopedparentonworkIdfromworkIds, not dropped; anEntityBackupCollectorover theworksspec yields a fixtureWorkBuildrow withbuildSecretNames: ['EW_DATABASE_URL']intact and nobuildInputsHashkey, and a fixtureWorkBuildPreparationrow withwebhookId/runsEtagintact and nobuildInputsHashkey. Done when:pnpm --filter @ever-works/agent test -- collectors redactionis green — including the secret-shaped-column guard inredaction.spec.ts— anddata/works/builds.jsonlin a backup with one Build has nobuildInputsHashkey. -
T46 (P1, lands with T8 and T10). Restricted build values on pull requests and verifications (
XC-01, FR-71, FR-72). Modifypackages/plugins/github-actions-build/src/workflow/generator.ts— withallowBuildValuesOnPullRequestsfalse (the default) afromEnvbuild argument is emitted as<NAME>=${{ github.event_name == 'pull_request' && '<restricted literal>' || secrets.EW_<NAME> }}, one fixed marker per value name; the "Check build values" step'sEW_MISSINGcheck is emitted only for the non-pull-request path; with the setting true, the previous unrestricted form is emitted unchanged (plan §4.7b). Modifypackages/plugins/github-actions-build/src/settings.schema.ts— the two new Work-scope booleansallowBuildValuesOnPullRequests(defaultfalse) andverificationPromptedValuesRequireApproval(defaulttrue) (T7 creates the file; this task adds the keys and their copy). Modifypackages/agent/src/app-builds/app-builds.service.ts—startVerificationwritesEW_VERIFY__PROMPTEDonly when the verification-prompted-values gate of plan §4.7b passes (no build-affecting file in the base→head diff, or the owner approved it), and records why it withheld the values on the Build so the detail drawer can say "Owner approval is needed before prompted values are used for this verification." with Review the change. Theverifyjob references no storedEW_<NAME>at all — the value-free recipe is the only source. Modifyapps/web/src/components/works/detail/builds/BuildSettingsDialog.tsxand the newdashboard.workDetail.builds.settings.allowBuildValuesOnPullRequests,…allowBuildValuesOnPullRequestsWarningand…verificationPromptedValuesRequireApprovalleaves in all 21apps/web/messages/*.jsonfiles, plus a notice on the Build detail drawer. Test:packages/plugins/github-actions-build/src/__tests__/secret-mode.spec.ts— the pull-request path renders the restricted literal and contains nosecrets.EW_reference while the push path keeps${{ secrets.EW_<NAME> }}(ACC-05-31);EW_MISSINGis absent from the pull-request path; the goldenrestricted-valuesis byte-stable;allowBuildValuesOnPullRequests: truereproduces the unrestricted golden byte for byte. Extendpackages/agent/src/app-builds/__tests__/app-builds.service.spec.ts— a prompted name is withheld when the diff touches aDockerfile, and delivered when the owner approved the diff or the flag is false (ACC-05-32); a sentinel search proves the stored value is absent from the plan, the dispatch inputs and every log line. Done when:pnpm --filter @ever-works/github-actions-build-plugin test -- secret-mode generatorandpnpm --filter @ever-works/agent test -- app-builds.serviceare green, and ACC-05-31's honeypot leaves the canary sink empty on the injection fixture.
Definition of Done
- Every checkbox above is ticked for the phases being shipped.
pnpm format:check,pnpm lint,pnpm type-check,pnpm testandpnpm buildare green from the repo root.apps/api/src/ingest/github/github-check-intake.service.spec.ts,apps/api/src/plugins-capabilities/deploy/deploy.service.server-side.spec.tsand every existing deploy e2e spec pass unchanged — the additive-only guarantee.- Every acceptance box in spec §8 for the shipped phase has been walked against real GitHub-hosted runners, and each ACC-05 id appears in at least one Test line above.
- No file in the repository outside
packages/plugins/github-actions-build/andpackages/plugins/apps-builder/contains either plugin id as a string literal (grep in CI). - Every gate in plan §12 is confirmed, and its known gaps are still recorded there rather than silently closed.
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.
- T16 (2026-09-26,
3a956180e): the shipped facade (packages/agent/src/app-builds/build-facade.service.ts) now receives the plugin registry by token (@Inject(PluginRegistryService)): SWC, which builds what ships, emittedObjectfor thePluginRegistryService | undefinedparameter, so the running API constructed it with no registry and every Build plugin resolution failed, while every ts-jest spec saw one. It also loads its candidates (loadRegisteredPlugins) before readingbuildKind, which a cold lazy proxy answers with a forwarding function. Pinned by__tests__/build-facade.registry-token.spec.tsand__tests__/build-facade.cold-plugin.spec.ts, and guarded byapps/api/src/app-works-di-reachability.spec.ts. Still open (D19): the binding forwards noprepareRepositoryand no writer, so every production prepare still answerspluginUnavailable. - T17 (2026-09-26,
e23c2f844):AppBuildsModule(packages/agent) imports APW-07'sAppEnvModule, which no API orpackages/tasksmodule imported and which is not@Global(). Before,APP_ENV_RESOLVER_FINGERPRINTS(AppBuildsService),AppEnvResolver(the prepare runner) andAppEnvService(the watch runner) wereundefinedin the API, so every finalized Build's verdict wasstaleInputsand none was ever deployable, every secret-syncing prepare answeredbuildValuesUnavailable, and every watch without a plugin redactor was skippedredactorUnavailable.AppEnvResolver.read(workId, 'build')now resolves against the effective spec's ownbuild.services, so the verdict hashes build-service values the way the prepare does. Pinned byapps/api/src/app-builds/app-builds.module.spec.ts, which composes the API's ownAppBuildsModulein a real container (4 of 5 red before the fix, 5 of 5 green after).