Task Breakdown: App Work kind & create from any repository URL
Ordered tasks derived from
plan.md. Each is small enough to land in one PR and ships with tests per Constitution VI. This epic adds no migration (plan §3.1); slot1792010000000stays reserved.
Epic ID: APW-01-app-work-kind
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.
- Every task carries Test (the spec file and what it asserts, or the command that runs it) and Done when (an observable, checkable condition).
- "Done when" is stated explicitly for every task and is checkable without reading the diff.
- Add new tasks at the bottom of their phase rather than renumbering (T39–T40 were added by the program audit; T41–T43 by the 2026-09-17 ordering pass).
- Phase boundaries are ship boundaries:
developmust be green and deployable at the end of each phase. - A task marked (own PR) merges by itself, ahead of its phase, because another epic binds or compiles against
what it creates; the PR description names the ordering. The program merge order lives in
TRACKER.md. - Prerequisites (exact tasks — every one of them is merged before this epic's P1; the program merge order in
TRACKER.mdplaces them there). The earlier line named only "APW-03 P1", but the symbols this epic compiles against are not all in APW-03 P1:- APW-02 P0 (its T1–T8) and P1 (its T9–T34, T43, T44) — checkout keys and
waitForReady(T2–T5),findExistingFork(T17), thegetRepositoryextensions (T16),WorkUpstreamStateand its repository (T12–T14), the readiness job and theAPP_FORK_READY_HANDLERtoken (T23–T24), the dispatcher symbols and runtime bindings (T31), the setup-pull-request follow-through (T43),createBranchFromSha(T18) and the non-production readiness deadline (T44). - APW-03 P1 — T1 (the contracts barrel) and T12 (
AppSpecService). - APW-03 P2 — the "Wave 1 catalog seam", which lands before this epic's P1: T22 (
commitFiles?and the facade wrapper), T24 (AppsCatalogService), T26 (the Blueprint resolver andAppSourceCatalogAdapter, which binds this epic's T11 port), T28 with T53 (the apply job and theapp.blueprint.matchedrecord) and T32 (AppsCatalogBrowser). - APW-06 T1–T3 only —
packages/contracts/src/apps/app-runtime.ts,supportsApps/isAppDeploymentPluginonIDeploymentPlugin, andpackages/agent/src/app-runtime/ports.tswithAPPS_TIER_POLICYandDisabledAppsTierPolicy. Types, symbols and closed defaults only; no runtime behaviour, so they carry no dependency on APW-01. - APW-13 P0 — its T1–T5: the fake GitHub, its contract test, the harness runner and the
EVER_WORKS_E2E_FAKESswitch. T40 and the T30 ship gate need them. T11 lands first, on its own, immediately after APW-02 T15, because APW-03 T26 binds the port T11 creates. APW-03 P3'sAppLicenseService(its T42) and APW-04'sAppProvisioningServicemerge after this epic: both are injected@Optional(), and until they land the calls are skipped and their tests use a typed fake. Only those two bindings are optional — the symbols exist before this epic merges. Tasks below name the APW-02/03/04/06/10/13 symbols they consume; they never re-declare them.
- APW-02 P0 (its T1–T8) and P1 (its T9–T34, T43, T44) — checkout keys and
- Program audit resolutions (CONTRACTS §0)
applied here: R-1 (T2), R-2 (T6), R-3 (T12, T22), R-4 (T13, T15, T24), R-5 (T12, T13, T14), R-6 (T12, T13, T17),
R-7 (T3), R-12 (T2, T22, T28), R-15 (T39), R-22 (no suite under
apps/api/test/). - No task in this file performs a real GitHub write. Live-provider scenarios run in APW-13's suite against a throwaway repository in a test organization.
- Test commands run from the monorepo root unless a task says otherwise: contracts
cd packages/contracts && npx vitest run <path>; agentcd packages/agent && npx jest <path>; APIcd apps/api && npx jest <path>; web unitcd apps/web && npx vitest run <path>; web e2ecd apps/web && npx playwright test <path>; MCPcd apps/mcp && npx vitest run <path>; taskscd packages/tasks && npx vitest run <path>.
Phase P1 — Create an App Work from any GitHub URL
Delivers spec FR-1…FR-53 (incl. FR-29a, FR-40a, FR-46a) and ACC-01-01…ACC-01-20.
P1.1 — Contracts
-
T1. The
appkind. Modifypackages/contracts/src/domain/work-kind.ts— append'app'toUSER_SELECTABLE_WORK_KINDS(after'repo'), document it in the JSDoc besiderepo, and addisAppWorkKind(value)(kind test, loose input, never throws). Modifypackages/contracts/src/domain/index.ts— exportisAppWorkKindif the barrel re-exports by name. Test: extendpackages/contracts/src/domain/__tests__/domain.spec.ts—normalizeWorkKind('APP ')isapp;isAppWorkKindfalse forrepo,awesome-repo,application,null. Done when: T1 and T3 land in one PR andpnpm --filter @ever-works/contracts testis green with both. T1 cannot be met on its own: appending'app'toUSER_SELECTABLE_WORK_KINDSwhileWORK_KIND_CAPABILITIES(Record<WorkKind, …>) has noappentry turnswork-capabilities.spec.ts:105-127red with aTypeError, the type-check red, andwork-kind-docs-parity.spec.tsred untildocs/features/work-kinds.mdgains its App row — and both of those only arrive in T3. T2 and the remaining P1 tasks still follow T3. (No task is renumbered and nothing is removed: T1 keeps its id, its content and its own test; only its completion condition now names the PR it must ship in.) -
T2 (parallel with T1). App source contracts in the shared App Works folder (Resolution R-1). Create
packages/contracts/src/apps/app-source.tswithAPP_SOURCE_REPOSITORY_TYPES,AppSourceRepositoryType,AppUpstreamRef,APP_DEPLOY_TARGET_CHOICES(none,your-cluster,ever-works-apps— R-12),AppDeployTargetChoice,APP_REPOSITORY_MODES,AppRepositoryMode,APP_SOURCE_REASON_CODES(24 codes),AppSourceReasonCode,AppModeAvailability,AppTargetOwner(incl.existingForkChecked: boolean),AppDeployTargetAvailability(extends AppModeAvailabilitywithproviderId?: string, set only when the target is available and is notnone— FR-33/FR-34),AppSourceInspectRequest,AppSourceInspectResponse(incl.deployTargetsand the P1scanIncomplete: boolean) and the Blueprint prompt shape ({ name, description?, required }, never a value — FR-55), plus the nine numeric constants exactly as in plan §3.2. Modifypackages/contracts/src/apps/index.ts—export * from './app-source.js';(Create it, and addexport * from './apps/index.js';topackages/contracts/src/index.ts, if APW-03 T1 has not landed). Modifypackages/contracts/src/api/work/import-source.dto.ts—import type { AppSourceRepositoryType, AppUpstreamRef } from '../../apps/app-source.js'; widenSourceRepository.typetoImportSourceType | AppSourceRepositoryType; addupstream?: AppUpstreamRef,blueprintId?: stringandcreatedByThisWork?: boolean. Do not touchIMPORT_SOURCE_TYPES. Test: createpackages/contracts/src/apps/__tests__/app-source.spec.ts— pins the mode list, the 24 reason codes, the three deploy target choices, every constant (15,60_000,8_000,30,512_000,5,120_000,600_000,10_000), thatscanIncompleteis a required boolean, thatexistingForkCheckedis a required boolean and thatAppDeployTargetAvailability.providerIdis optional, and thatIMPORT_SOURCE_TYPESstill has exactly four members. Done when:import { AppSourceInspectResponse } from '@ever-works/contracts'type-checks inapps/apiandapps/web(pnpm --filter ever-works-api type-check,pnpm --filter ever-works-web type-check), andgit grep -n "app-source.dto" packages/contractsreturns nothing. Note:APP_REPOSITORY_MODES,AppDeployTargetChoiceand the reason codes keep their existing members — this task adds fields and types, it renames and removes nothing. -
T3. Capabilities for
app, the two new flags, and the replaced invariants (Resolution R-7). Modifypackages/contracts/src/domain/work-capabilities.ts— addreadonly builds: booleanandreadonly appEnvironment: booleantoWorkCapabilities, set bothfalseinDIRECTORY_CAPABILITIESand in every existing kind entry, and append theappentry from plan §3.2 (bothtrue) with a comment citing README D1 and R-7. Modifypackages/contracts/src/domain/__tests__/work-capabilities.spec.ts— add anappdescribe block (deploy/kb/builds/appEnvironment on; items, taxonomy, comparisons, communityPr, importExport, sourceValidation off; repos data only; metricsagents,open-tasks,deploy-status,days-active). Replace the pin "keepsrepothe only user-selectable kind without a website repository" with "onlyrepoandapplack a website repository", and "never deploys a kind that has no website repository" with "a kind without a website repository deploys only when it isapp". Leave everyrepoassertion as is. Modifydocs/features/work-kinds.md— add the App row sowork-kind-docs-parity.spec.tspasses. Test:packages/contracts/src/domain/__tests__/work-capabilities.spec.ts— theappblock above, and for everyWorkKindother thanappbothbuildsandappEnvironmentarefalse;packages/contracts/src/domain/__tests__/work-kind-docs-parity.spec.tspasses with the new row. Done when:pnpm --filter @ever-works/contracts testis green, exactly one kind hasbuilds === true, and the diff of the spec file removes norepoassertion. -
T4 (parallel with T3). Source-sync signature. Modify
packages/agent/src/import/source-sync-support.ts— widensupportsWorkSourceSync's parameter tostring | null | undefined; the whitelist is unchanged. Test: extendpackages/agent/src/import/__tests__/source-sync-support.spec.ts—app_link,app_fork,app_private_copyanswerfalse. Done when:cd packages/agent && npx tsc --noEmit -p tsconfig.jsonis clean.
P1.2 — Entity, DTO, settings (no migration)
-
T5.
CreateWorkDtofields. Modifypackages/agent/src/dto/create-work.dto.ts— addrepositoryMode?(@IsOptional,@IsIn(APP_REPOSITORY_MODES)) andtargetOwner?(@IsOptional,@IsString,@MaxLength(100),@Matches(/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/), trimmed) andblueprintId?(@MaxLength(100),@Matches(/^[a-z0-9][a-z0-9-]{0,99}$/)) andautoProvision?(@IsOptional,@IsBoolean— kindapponly, defaulttrue,falseis the member's decline of FR-29a's automatic start); extend thekindandrepositoryUrl@ApiPropertyOptionaldescriptions to coverapp. The two required fields are required on the DTO, not only in the service (FR-12, T18's assertion):repositoryModegains@ValidateIf((o) => normalizeCreateWorkKind(o.kind) === 'app')+@IsDefined(), andtargetOwnergains the same@ValidateIf((o) => normalizeCreateWorkKind(o.kind) === 'app')predicate widened with&& (o.repositoryMode === 'fork' || o.repositoryMode === 'private-copy'), plus@IsDefined(), so the pipe answers400 repositoryMode must be definedbefore the controller runs.normalizeCreateWorkKindis the existing loose-kind normalizer; every other kind and every existing field keeps its current validation exactly. Test: createpackages/agent/src/dto/create-work.dto.app.spec.ts— class-validator accepts{ kind: 'app', repositoryMode: 'fork', targetOwner: 'my-org' }, accepts{ kind: 'app', repositoryMode: 'link' }without an owner, and acceptsautoProvision: false; rejectsrepositoryMode: 'mirror',targetOwner: '../x', a missingrepositoryModeforkind: 'app'(repositoryMode must be defined), a missingtargetOwnerforforkand forprivate-copy, and a non-booleanautoProvision; every non-appkind still validates without either field (ACC-01-03); runcd packages/agent && npx jest src/dto/create-work.dto.app.spec.ts. Done when: the OpenAPI document generated byapps/apilists the four fields onCreateWorkDto. -
T6 (parallel with T5). Activity types (Resolution R-2). Modify
packages/agent/src/entities/activity-log.types.ts— appendAPP_SOURCE = 'app_source'in an "App Works (APW-01)" block; rows store the dotted event (app.source.linked·forked·copied·failed) inaction, per plan §3.3. Append only; no migration. Test: extendpackages/agent/src/entities/__tests__/activity-log.types.spec.ts— pins['APP_SOURCE', 'app_source']; every pre-existing pair unchanged. Done when: the spec is green and no existing enum member moved. -
T7 (parallel with T5). Instance setting. Modify
packages/agent/src/config/index.ts— addeverWorks.apps.worksEnabled()readingEVER_WORKS_APP_WORKS_ENABLED === 'true'(defaultfalse), beside the other*_ENABLEDgetters. Modifyapps/api/.env.example— document the variable withfalse. Test: extendpackages/agent/src/config/config.spec.ts— unset ⇒false,'true'⇒true,'1'⇒false. Done when: the spec is green andapps/api/.env.examplecontainsEVER_WORKS_APP_WORKS_ENABLED=false. -
T7b. The web chip must fail CLOSED for
app(R-6 says so; the shipped helper does the opposite). Modifyapps/web/src/lib/feature-flags/work-kinds.ts— today it is documented as fail-open ("DEFAULT IS ENABLED … a missing flag, or anundefinedvalue → the chip is ENABLED"; only an explicitfalsedisables it,:13-20,75-76), which would leave the App chip visible whenever the PostHog flag is missing — exactly the case R-6 must prevent. Addappto a fail-CLOSED set for this one kind: the chip renders only when the flag resolves strictlytrueand the API-side gate agrees. Test: extend the existing work-kinds spec — missing flag ⇒ App chip absent;false⇒ absent;true⇒ present; every otherworks-<kind>flag keeps its fail-open behaviour (pin that too, so the change is provably scoped toapp). Done when: the spec is green and no other kind's flag semantics changed. (Why this is a task in APW-01: CONTRACTS §7/R-6 require the fail-closed chip, but no epic owned the file that implements it.) -
T8. Repository lookups. Modify
packages/agent/src/database/repositories/work.repository.ts— addfindAppWorksByDataRepository(userId, owner, repo)(kindapp, indexedowner,datarole compared case-insensitively in memory, the same portable split asfindRepositoryWorksWrapping) andfindWorksUsingRepository(owner, repo, { kinds: ['repo', 'app'] })returning{ id, userId, kind, relation }for the conflict checks. Do not changefindRepositoryWorksWrapping. Test: createpackages/agent/src/database/repositories/__tests__/work.repository.app-lookups.spec.ts— case-insensitive match, other kinds ignored, a row with a missing data owner never matches (ACC-01-09). Done when: the new spec and the existingfindRepositoryWorksWrappingspecs pass unchanged.
P1.3 — Services
-
T9. App Work guard. Create
packages/agent/src/works/app-work-guard.ts—APP_WORK_REFUSAL = 'is an App Work',isAppWork(subject),assertNotAppWork(subject, action)throwingBadRequestExceptionin the exact message shape ofassertNotRepositoryWork. Test: createpackages/agent/src/works/__tests__/app-work-guard.spec.ts— refusesapp, passesrepo,default,directory, unknown kinds (ACC-01-11). Done when: the spec is green andpackages/agent/src/works/repository-work-guard.tshas no diff. -
T10. Writer refusals. Modify, adding one
assertNotAppWork(orisAppWorkskip) line beside each existing repo check, per plan §4.4:packages/agent/src/services/work-generation.service.ts(ensureNotRepositoryWork),packages/agent/src/services/work-schedule.service.ts(updateSchedule),packages/agent/src/comparison-generator/comparison-generation.service.ts(both entry points),packages/agent/src/services/item-health.service.ts(checkItem),packages/agent/src/community-pr/community-pr-processor.service.ts(schedule skip +processWork),packages/agent/src/services/repository-management.service.ts(updateRepositoryVisibility),packages/agent/src/services/work-query.service.ts(workItems⇒[]),packages/agent/src/services/work-lifecycle.service.ts(syncFromDataRepository, post-creategetItemsskip),packages/agent/src/works-config/services/works-config-repository-sync.service.ts(syncWorkno-op),packages/agent/src/services/knowledge-base-git-mirror.service.ts(skip commit/push forapp). Test: add oneappcase beside therepocase inpackages/agent/src/services/__tests__/work-generation.service.spec.ts,packages/agent/src/services/__tests__/work-schedule.service.spec.ts,packages/agent/src/services/__tests__/item-health.service.spec.ts,packages/agent/src/community-pr/community-pr-processor.service.spec.ts,packages/agent/src/services/__tests__/repository-management.service.spec.ts,packages/agent/src/services/__tests__/work-query.service.spec.ts(item listing[]with no clone call) andpackages/agent/src/services/__tests__/work-lifecycle.service.spec.ts(syncFromDataRepositoryrefused); createpackages/agent/src/comparison-generator/comparison-generation.service.app-work.spec.ts(both entry points refuse),packages/agent/src/works-config/__tests__/works-config-repository-sync.app.spec.ts(no write, no push) andpackages/agent/src/services/__tests__/knowledge-base-git-mirror.app.spec.ts(no commit, no push) — together ACC-01-11. Done when: every row of plan §4.4 has a greenappassertion and everyrepoassertion is unchanged. -
T11. Catalog port and module wiring. (Own PR, before APW-03 P2 — TRACKER merge order.) Create
packages/agent/src/app-works/app-source-catalog.port.ts—AppSourceCatalogPort(matchBlueprint({ owner, repo, blueprintId? }),classifyLicense(spdx), plus the optionalmatchSource,promptsanddisplayNamefields of plan §7) andAPP_SOURCE_CATALOG_PORT = Symbol('APP_SOURCE_CATALOG_PORT')exactly as plan §7. Createpackages/agent/src/app-works/app-prompted-values.port.ts—AppPromptedValuesPort(storePrompted(workId, values)),APP_PROMPTED_VALUES_PORT = Symbol('APP_PROMPTED_VALUES_PORT')and its no-op default (plan §7). Modifypackages/agent/src/app-works/index.ts(created by APW-02 T15) — export both ports. Modifypackages/agent/src/app-works/app-works.module.ts(created by APW-02 T15) — import the modules exporting APW-03'sAppSpecService,AppBlueprintApplyService,AppLicenseServiceand APW-04'sAppProvisioningServicewhen present (each consumer injects them@Optional()), and provide + export this epic's services as T12–T15 add them. Modifypackages/agent/src/services/work.module.ts— importAppWorksModulesoWorkLifecycleServicecan receiveAppWorkCreateService;AppWorkCreateServicemust not inject any provider ofWorkModule(slug checks go throughWorkRepository), so no module cycle is introduced. Test: extendpackages/agent/src/services/work.module.spec.ts— the module graph compiles with and without APW-03/04 providers registered. Done when:@ever-works/agent/app-worksresolves fromapps/apiandpackages/tasks, and the port file matches the shape APW-03 T26 binds without any change to this task's PR. -
T12.
AppSourceInspectorService. Createpackages/agent/src/app-works/app-source-inspector.service.ts—inspect(url, user, opts)implementing plan §2.2:config.everWorks.apps.worksEnabled()first (refuses every client alike, R-6); parser;GitFacadeService.getRepository(APW-02 fields); 403 classification from APW-02's typed provider errors; owners viagetUser+getOrganizations(first 30, A–Z) with theforkTemplateForUsercase-insensitive match;findExistingForkper owner;.gitattributesfilter=lfsprobe throughgetFileContent; conflict lookups (T8) that expose another account's usage as a boolean only;APP_SOURCE_CATALOG_PORT.matchBlueprintwith the optionalblueprintIdandclassifyLicense(@Optional(), try/catch ⇒unavailable/unknown) per the license preview rule of plan §7, returning the match'smatchSource,displayNameandprompts(FR-55, FR-56);deployTargets(nonealways available;your-clusterwhen an installed deploy plugin reportssupportsApps, elsecluster_target_unavailable;ever-works-appsonly whenAppsTierPolicy.isOpen()— injected@Optional(), absent ⇒managed_hosting_unavailable; R-5, R-12), each available non-nonetarget carrying theproviderIdthe create request will persist; default-mode rules FR-17/FR-18; a provider-call counter charged per actual call — the fixed checks first (repository read 1, default-branch read ≤ 1, caller read 1, organizations read 1,.gitattributesread 1 = ≤ 5), thenfindExistingForkin owner order (the caller first, then organizations A–Z, ≤ 30 in P1), starting a new owner only while at least 3 calls remain of the 15-call budget. Owners the budget did not reach keep their computedavailable, getexistingForkChecked: falseand no reason code, and the response setsscanIncomplete: true— neverrate_limitedorunavailable(FR-7, FR-9); a 60 s in-memory cache keyed byuserId + lower(owner/repo)(bypassed whenopts.fresh). Modifypackages/agent/src/app-works/index.ts— export it. Test: createpackages/agent/src/app-works/__tests__/app-source-inspector.service.spec.ts— setting off ⇒app_works_disabledbefore any provider call; every reason code; the default-mode matrix; the call cap; cache hit/miss/fresh; own-fork paste (parent becomes upstream); existing renamed fork found per owner (ACC-01-04); organization owners listed from the member's connection only (ACC-01-03); private-copy availability (ACC-01-05); renamed repository (movedFrom); empty repository; archived; 512 000 KB boundary (512 000 allowed, 512 001 refused); another account's link ⇒ Link unavailable, Fork available (ACC-01-09); facade spies record zero write calls (ACC-01-06);deployTargetswith the policy unbound, closed and open, and neveravailable: truewithout aproviderIdfor a non-nonetarget; the Blueprint match carrying itsmatchSource,displayNameand prompt descriptors, and a source-only preview carrying none (ACC-01-21, ACC-01-22); three scan cases — a 30-organization fixture with no forks gives ≤ 15 recorded provider calls, the caller checked, every organization stillavailable: true, the organizations the scan did not reach flaggedexistingForkChecked: falseand the responsescanIncomplete: true; a 2-organization fixture givesscanIncomplete: false; and no unreached owner carries a reason code (ACC-01-26). Done when: no test path can produce a mode or deploy targetavailable: truealongside a reason code, and no unscanned owner is reported as having no fork. -
T13.
AppWorkCreateService. Createpackages/agent/src/app-works/app-work-create.service.ts—create(dto, user)implementing the steps of plan §4.2 (including step 6a), injectingAppSourceInspectorService,DistributedTaskLockService,GitFacadeService,DeployFacadeService,WorkRepository,WorkUpstreamStateRepository(APW-02),APP_FORK_READINESS_DISPATCHER(APW-02),EventEmitter2,APP_SOURCE_CATALOG_PORT(for the Blueprint resolution of step 6a and theblueprint_mismatchrefusal),APPS_TIER_POLICY— the token imported frompackages/agent/src/app-runtime/ports.ts(APW-06 T3, merged ahead of this epic) and injected@Optional(), an unbound token meaning closed (R-5) — andAPP_PROMPTED_VALUES_PORT(@Optional(); unbound ⇒ the values are logged as dropped, never a refusal). Error bodies{ status: 'error', code, message, details? }. PersistsourceRepository.blueprintIdandsourceRepository.blueprintMatchSourcefrom step 6a (never from the request alone),sourceRepository.createdByThisWork=trueonly when this request issued the fork request or created the private-copy repository (R-4), andsourceRepository.autoProvision = falseonly when the request carriedautoProvision: false(plan §4.2 step 10; absent means on, so no migration and no existing caller moves). The deploy target of step 4 is persisted as the resolved plugin id — the managed choice as theapps-tierplugin's id, never the literal'ever-works'— and itsproviderIdis echoed inappSource.deployTarget. Modifypackages/agent/src/services/work-lifecycle.service.ts— increateWork, branch toAppWorkCreateService.createwhenisAppWorkKind(normalizedKind); append the service to the constructor last and@Optional()(the positional-spec arity rule the class documents); skipresolveProviderDefaultsfor the kind. Modifypackages/agent/src/app-works/app-works.module.ts— provide + exportAppWorkCreateService,AppSourceInspectorService. Test: createpackages/agent/src/app-works/__tests__/app-work-create.service.spec.ts— a facade spy proves zero provider writes and zero repository writes before step 9 for every refusal, including each of the eight ACC-01-07 codes; setting off ⇒400 app_works_disabled(R-6); lock not acquired ⇒409 create_in_progress; same slug within 600 000 ms ⇒alreadyExisted(ACC-01-08); after ⇒409 app_work_exists; adopted fork issues no fork call and persistscreatedByThisWork: false(ACC-01-04); a fork request persistscreatedByThisWork: true(ACC-01-02); organization target uses the member's connection (ACC-01-03); copy name ladder stops at-copy-5and the shell is private (ACC-01-05); transaction failure after the fork leaves no row and a retry adopts the fork; the Work row and itsWorkUpstreamStaterow are written through onewithTransactionmanager (both creates receive it; a throw rolls both back and leaves the fork); the state row'sdataOwner/dataRepo/dataDefaultBranch/upstreamStatus/actionsState/nextSyncAtmatch plan §4.2 step 10's per-relation table — in particularactionsState: 'not_applicable'forlinkand a private copy whosedataDefaultBranchis the upstream's;nulldispatch recordsdispatch_unavailable;deployProvider: 'ever-works'refused whileAppsTierPolicyis unbound or closed (ACC-01-12); the managed choice persisted as theapps-tierplugin id and never as the literal'ever-works'; onboarding deploy default never applied; the Blueprint step-6a matrix — no id sent plus a catalog match ⇒ that id andmatchSource: 'manifest'persisted, no id sent plusnone,unavailableor an unbound port ⇒ noblueprintIdand create still succeeds, id sent and equal to the match ⇒ the match's own source kept, id sent for an unlisted repository ⇒explicit, unknown id ⇒400 blueprint_mismatchwith zero provider and zero repository writes (ACC-01-21); a renamed fork in an organization the scan did not reach is still adopted at create with no fork request (ACC-01-26); a request carryingappEnvsucceeds with the port unbound and callsstorePromptedonce per name when it is bound, while no read response contains a value (ACC-01-22);autoProvision: falsepersistssourceRepository.autoProvision === falsewhile an omitted ortruefield leaves the property absent, andlinkwith no owner still succeeds (ACC-01-28); the readiness dispatch is called once with{ workId, attempt: 1, reason: 'initial', providerId, credentialVersion }— norelationfield, per APW-02's dispatcher payload — andproviderId/credentialVersionare the values captured before the transaction, not re-read after it. Done when:createWorkfor every non-appkind passes its existing specs unchanged. -
T14. Update and delete semantics. Modify
packages/agent/src/services/work-lifecycle.service.ts—updateWork: freezeownerforapp(same shape asrepo), restrictdeployProvidertonullor an apps-capable plugin, map it exactly as T13's step 4 ('ever-works'accepted only as an input alias for the managed target and rewritten to the apps-tier plugin id; the literal is never persisted), refuse the managed target unlessAppsTierPolicy.isOpen()(R-5);deleteWork: the App branch of plan §4.3 —=== trueonly, link refused, upstream full-name guard, admin check throughgetRepository(...).permissions.admin, partial-failure message, per-Work checkout removal (checkoutKey: 'work:<id>:data', APW-02). (The workload removal of R-15 is T39.) Test: createpackages/agent/src/services/__tests__/work-lifecycle.app-kind.spec.ts— the full delete matrix (link + flag ⇒ 400; fork + omitted ⇒ kept; fork +true+ admin ⇒ deleted; fork +truewithout admin ⇒ kept with message; Work Repository equal to upstream ⇒ never deleted; provider failure ⇒ row deleted, message names the repository) and the update rules (ACC-01-10). Done when: the new spec is green andpackages/agent/src/services/__tests__/work-lifecycle.delete.spec.tspasses unchanged. -
T15.
AppSourceInitializerService(the ready handler, Resolution R-4). Createpackages/agent/src/app-works/app-source-initializer.service.tsimplementing APW-02'sAppForkReadyHandler.onDataRepositoryReady({ workId })per plan §6:AppSpecService.initialize; Blueprint path (sourceRepository.blueprintIdand the default-branch file does not already record that same Blueprint withspec.source) ⇒AppBlueprintApplyService.request(workId, blueprintId, { userId, matchSource: sourceRepository.blueprintMatchSource ?? 'explicit', confirmForkMatch: false }), no file write here, and outcomeblueprint_requested— which means readiness stayspreparingwithreadinessReason: 'blueprint_applying'(noreadyAt, noapp.fork.ready, no APW-04forkReady, nonextSyncAt), APW-03's apply job reporting back through APW-02'sAPP_SOURCE_APPLY_REPORTER; when the file already records the Blueprint, fall through to the minimal path so thesetup_mergedre-invocation returnsunchangedand runs its follow-ups exactly once. APW-03'sapplyInProgressrefusal also returnsblueprint_requested; any other apply refusal isfailed/blueprint_<code>. Minimal path ⇒ never clones:getLatestCommit+getFileContent('.works/works.yml'), parse with the works-config loader, prototype-pollution strip, setversion/kind/spec.source, content compare; whencreatedByThisWork(fork or private copy) ⇒ oneGitFacadeService.commitFileson the default branch (nonFastForward⇒ re-read head, ≤ 3 retries); otherwise (link, pasted or adopted fork) ⇒ setup pull request fromever-works/app-setup, created withGitFacadeService.createBranchFromSha(owner, repo, 'ever-works/app-setup', head.sha)— nevercreateBranchwith a sha: the existingcreateBranchresolvesheads/<fromRef>and so takes a branch name only, and a sha passed to it 404s (git.facade.ts:692-697,github-api.service.ts:576-580) — reusing an open pull request when there is one, reusing an existing branch with the branch's own head asbaseShawhen there is not, and returningwaiting_for_setup_prwith URL and number;GitOperationNotSupportedErrorfrom either capability ⇒failed/provider_unsupported; refusal on unparseable or other-kind files; follow-ups only when the source is on the default branch (initializedorunchanged):AppLicenseService.requestand — whenAppSpecService.hasValidAppSpec(workId, sha)is false, which asource-only file always is —AppProvisioningService.start({ workId, trigger: 'auto-create' }); one Activity row per outcome viaActivityLogService.logwith{ blueprint: boolean, setupPullRequest: boolean }. Modifypackages/agent/src/app-works/app-works.module.ts— provide and exportAppSourceInitializerService. Modifyapps/api/src/app-works/app-works.module.ts(created by APW-02 T27) — bindAPP_FORK_READY_HANDLERwithuseExisting: AppSourceInitializerService. The handler runs in the API process: every dependency it needs (the database,ActivityLogService, APW-03's and APW-04's services, the dispatchers) lives in API-side injection, and it clones nothing. Modifyapps/api/src/trigger/trigger-internal.controller.ts— add@Optional() private readonly appSourceInitializerService?: AppSourceInitializerServiceappended last (the controller's arity rule) andAppSourceInitializerService: this.appSourceInitializerServicetoremoteMap;onDataRepositoryReadyis not added toRETRY_SAFE_REMOTE_METHODS. Modifypackages/tasks/src/trigger/worker/modules/trigger-worker.module.ts— bindAPP_FORK_READY_HANDLERtocreateRemoteProxy(apiClient, 'AppSourceInitializerService'). Do not provide the class or importAppWorksModulein the worker: it has no database module and noActivityLogService. Test: createpackages/agent/src/app-works/__tests__/app-source-initializer.service.spec.ts— Blueprint path requests apply and writes nothing, returnsblueprint_requestedand emits no readiness (ACC-01-19); a re-invocation on a file that already records the Blueprint returnsunchangedwith zerorequestcalls and its follow-ups once, and anapplyInProgressrefusal still returnsblueprint_requested; created fork ⇒ exactly onecommitFileson the default branch and zerocloneOrPullcalls (ACC-01-02); link ⇒ zero default-branch writes, onecreateBranchFromShacall whose 4th argument equals the mockedgetLatestCommitsha followed bycommitFileswith that samebaseSha, zerocreateBranchcalls, one setup pull request, source relationlink, Activityapp.source.linked(ACC-01-01); missingcreateBranchFromSha⇒failed/provider_unsupportedand no pull request; an existing branch is reused with the branch's own head asbaseSha; adopted fork ⇒ setup pull request, nevercommitFileson the default branch (ACC-01-04); retry reuses the open pull request (ACC-01-17); keys preserved; unchanged ⇒ no commit; unparseable ⇒ untouched + failed; other kind ⇒ untouched + failed; provisioning: created fork on the minimal path ⇒startexactly once whilevalidationStatusis stillmissing, post-merge re-invocation on a source-only file ⇒startonce,unchangedon a file that already holds a valid full spec ⇒startnot called, a full spec with errors ⇒startcalled (ACC-01-19);sourceRepository.autoProvision === false⇒startnot called on the created-fork path or on the post-merge re-invocation, while the license request still runs and the Activity row is the sameapp.source.*row (ACC-01-28); Activity payloads contain no body text. Done when: running the handler twice on the same Work produces exactly one commit (created fork) or exactly one open setup pull request (link), the spec's facade spy records zerocloneOrPullcalls, andapps/api/src/trigger/trigger-internal.controller.spec.tslistsAppSourceInitializerServiceinremoteMapwithonDataRepositoryReadyin its method allow-list. -
T16. Deploy refusal until the App runtime. Create
packages/agent/src/app-works/app-work-deploy-route.port.ts—AppDeployRoute(request(input: AppDeployRequest): Promise<AppDeployRequestOutcome>), the two plain-JSON shapes, andAPP_WORK_DEPLOY_ROUTE = Symbol('APP_WORK_DEPLOY_ROUTE')exactly as plan §7. This epic declares the token — likeAPP_WORK_DELETION_PORT(T39) andAPP_PROMPTED_VALUES_PORT(T11) — so this task names no symbol another epic owns. Modifypackages/agent/src/app-works/index.ts— export the port. Modifyapps/api/src/plugins-capabilities/deploy/deploy.service.ts— beside thereporefusal (~198), refuseisAppWorkKindwith409 { code: 'app_runtime_unavailable', message: 'Deploying App Works arrives with the App runtime.' }beforegetPluginAndTokenAndSettingswhenever the token is unbound; injectAPP_WORK_DEPLOY_ROUTE@Optional()and appended last (the class's arity rule). When the token is bound the refusal is replaced by a delegation to it, never by the website workflow dispatch. APW-06 T34 binds the token withuseExisting: AppDeployRequestServicein the same file and takes the App path over. Test: extendapps/api/src/plugins-capabilities/deploy/deploy.service.spec.ts—apprefused beforegetPluginAndTokenAndSettingsis called, with the facade mocked to throw as production does (ACC-01-12), and the same spec re-run with the token bound ⇒ the refusal is replaced by the App path and the route receives the request exactly once. Done when: noappWork can reach the website workflow dispatch (the spec's dispatch spy records zero calls) and the port's shapes match what APW-06 T34 consumes without a change to this task's PR.
P1.4 — API
-
T17. Inspect endpoint. Create
apps/api/src/works/dto/app-source-inspect.dto.ts—AppSourceInspectRequestDto(repositoryUrl≤ 400 trimmed,gitProvider?,blueprintId?) andAppSourceInspectResponseDto(Swagger mirror of the contract, incl.deployTargets). Createapps/api/src/works/app-source.controller.ts—@Post('works/app-source/inspect'),@HttpCode(200),@Throttle({ long: { limit: 30, ttl: 60_000 } }),@ApiOperation; delegates toAppSourceInspectorService(which owns the setting check, R-6). Modifyapps/api/src/works/works.module.ts— register the controller. Test: createapps/api/src/works/app-source.controller.spec.ts— throttle metadata; setting off ⇒ 400app_works_disabledwhateverUser-Agentor client header the request carries (ACC-01-13); invalid URL ⇒ 400; provider refusals ⇒ 200 with reasons; aPOST works/app-source/inspectis not routed to anyworks/:idhandler. Done when:cd apps/api && pnpm testis green. -
T18. Create endpoint coverage. Modify
apps/api/src/works/works.controller.ts—@ApiResponseentries for409and503onPOST works(no behaviour change; the service branch is T13). Test: extendapps/api/src/works/works.controller.crud.spec.ts—kind: 'app'with mode/owner reaches the service; missingrepositoryMode⇒ 400 naming the field; quick-create withkind: 'app'⇒ 400 before any row (quick-create carries no mode). Done when: the OpenAPI JSON shows the new response codes. -
T19 (parallel with T18). MCP parity. Modify
apps/mcp/src/openapi-tools/whitelist.ts— addinspect_app_sourcewithannotations: { readOnlyHint: true }in the Works block; update the block's count comment. Test: createapps/mcp/test/whitelist-app-works.spec.ts(sibling ofapps/mcp/test/whitelist-missions-ideas.spec.ts) and extendapps/mcp/test/tool-registration.spec.ts— the tool registers and is read-only. The field assertion must not stop at that suite's own fixture: it builds hand-writtensampleOperationsand never loads the API's OpenAPI, so add the real check inapps/api/src/works/works.controller.crud.spec.ts(or a second MCP suite that reads the committedapps/apiOpenAPI snapshot), asserting that the generatedcreate_workinput schema listsrepositoryMode,targetOwner,blueprintIdandautoProvision— withappEnvpresent and marked write-only — because that generated schema is the only thing the MCP tool actually exposes (ACC-01-14). Done when:pnpm --filter ever-works-mcp testis green and the four create fields are asserted against the generated OpenAPI document, not against a hand-written fixture.
P1.5 — Web
-
T20. Flag semantics for
app. Createapps/web/src/lib/work-kinds/flag-gated-kinds.ts—HIDDEN_WHEN_DISABLED_WORK_KINDS = ['app'] as constandisHiddenWhenDisabled(value), importable from client components (noserver-only). It is the single list behind both the fail-closed flag set and the chip removal of T23. Modifyapps/web/src/lib/feature-flags/work-kinds.ts—FAIL_CLOSED_WORK_KINDS= that list; for those values, missing key, missing flag,undefined, error and timeout all add the value to the disabled set, and pre-seed them so the partial set a timeout returns cannot re-enable the kind. Modify the same file (and the server-side caller that reads it) — when no PostHog client is configured at all (noPOSTHOG_API_KEY), the fail-closed set is decided by the runtime instance setting read server-side at request time (EVER_WORKS_APP_WORKS_ENABLED, the API twin — never a build-timeNEXT_PUBLIC_*variable), so chip and API always agree. This is what makes the chip visible in the PR e2e lane, in local development and on a self-hosted install without PostHog, and every otherworks-<kind>flag keeps its fail-open behaviour untouched (FR-47). Test: createapps/web/src/lib/feature-flags/work-kinds.unit.spec.ts—appdisabled without PostHog and the runtime setting unset; enabled without PostHog and the runtime settingtrue; disabled with a PostHog client and a missing flag, anundefinedvalue, an error or a timeout that returns a partial set; enabled only on an explicittruewith a client;blogstill fail-open in all of those cases (ACC-01-13, ACC-01-25); createapps/web/src/lib/work-kinds/flag-gated-kinds.unit.spec.ts— the list is exactly['app']andisHiddenWhenDisabledis false for every other kind. Done when: an instance with no PostHog key and the setting off shows no App chip, an instance with no PostHog key and the setting on shows it, and no other kind's flag semantics changed. -
T21. Client plumbing. Modify
apps/web/src/lib/api/work.ts—CreateWorkDto+repositoryMode?,targetOwner?,blueprintId?;workAPI.inspectAppSource(body). Modifyapps/web/src/app/actions/dashboard/works.ts—workKindSchema+'app';aiWorkKindSchemaexcludes['repo', 'app']; schema fields +superRefine; personal-connection gate forapp;inspectAppSourceAction. Createapps/web/src/lib/work-kinds/app-source-reasons.ts— reason code ⇒ i18n key map. Test: extendapps/web/src/app/actions/dashboard/works.unit.spec.ts—apprequires mode and owner; managed storage does not bypass the connection gate; AI path rejectsapp. Createapps/web/src/lib/work-kinds/app-source-reasons.unit.spec.ts— every one of the 24 codes maps to a key present inen.json. Done when:pnpm --filter ever-works-web type-checkis clean. -
T22. The create form. Create
apps/web/src/components/works/app/AppWorkForm.tsx,apps/web/src/components/works/app/AppSourcePreviewCard.tsx,apps/web/src/components/works/app/AppModeCards.tsx,apps/web/src/components/works/app/AppTargetOwnerPicker.tsxandapps/web/src/components/works/app/AppDeployTargetPicker.tsxandapps/web/src/components/works/app/AppBlueprintPrompts.tsxper plan §5.2–5.3 and spec §6.1–6.3, including the Paste a URL / Browse the Apps catalog tabs that mount APW-03'sAppsCatalogBrowser(a pick fills the URL, carriesblueprintIdand runs inspect once), the R-3 license chip copy, the deploy target picker defaulting to None — don't deploy yet (R-12) with availability from inspect'sdeployTargets, the Let an agent work out how to run it checkbox (spec §6.2 — rendered only when the preview carries no matched Blueprint and noblueprintIdwas picked, ticked by default, with its spend-help line, submitted asautoProvisionand sent only when unticked), anddata-testidsapp-work-url,app-work-check,app-work-mode-<mode>,app-work-owner,app-work-deploy-none,app-work-deploy-your-cluster,app-work-deploy-ever-works-apps,app-work-auto-provision,app-work-submit. Submit carries the Blueprint and the target, never a guess. On the Paste a URL tab, when the current preview hasblueprint.status === 'matched'the submit payload sendsblueprintId = preview.blueprint.id; clearing the URL or the preview clears it. For a non-nonedeploy target the payload sends the target'sproviderIdfrompreview.deployTargets[choice]— the picker never invents an id (FR-34).AppBlueprintPromptsrenders the preview's prompt descriptors (name, description,Required) for a matched Blueprint, one input per prompt, never pre-filled from a stored value; its answers are collected into the write-onlyappEnvfield of the submit payload, and while aRequiredprompt is empty the submit button is disabled withFill in the values the app needs first.(FR-55). Test: createapps/web/src/components/works/app/AppWorkForm.unit.spec.tsx,apps/web/src/components/works/app/AppModeCards.unit.spec.tsx,apps/web/src/components/works/app/AppBlueprintPrompts.unit.spec.tsxandapps/web/src/components/works/app/AppTargetOwnerPicker.unit.spec.tsx— inspect only on click/Enter; URL change clears the preview; disabled cards announce reasons (every ACC-01-07 code);↑/↓skip disabled; owner switch re-derives without a request; private-copy trade-off copy shown before submit (ACC-01-05); submit label per mode; single-flight submit (ACC-01-08); None selected by default and Ever Works Apps disabled with its reason (ACC-01-12); a catalog pick fills the URL, keepsblueprintIdand inspects exactly once (ACC-01-19); run a provider check proving that an owner the preview did not check (existingForkChecked: false) is still selectable and shows no existing-fork line (ACC-01-26); paste with a matched preview ⇒ the submit payload carries thatblueprintId, no match ⇒ it omits it, URL change ⇒ it is cleared (ACC-01-21); prompts render with their required markers, aRequiredprompt left empty disables submit with its copy, and the submittedappEnvholds the typed values (ACC-01-22); a non-nonetarget submits itsproviderId(ACC-01-27); the provisioning checkbox is absent for a matched Blueprint and present and ticked without one, unticking it submitsautoProvision: false, ticking it back omits the field, and changing the mode, the owner or the deploy target never resets it (ACC-01-28). Done when: the form renders every spec §6.3 reason string from a fixture. -
T23. Chips and routing. Modify
apps/web/src/components/new/NewPageClient.tsx,apps/web/src/app/[locale]/(dashboard)/new/page.tsx,apps/web/src/app/[locale]/(dashboard)/works/new/page.tsx,apps/web/src/app/[locale]/(dashboard)/works/new/new-work-client.tsx,apps/web/src/components/works/WorksCreateComposer.tsx,apps/web/src/components/works/WorkAICreator.tsx(type only),apps/web/src/lib/work-kinds/catalog.ts— theappchip, icon, placeholders, intent, canonical-URL routing to/works/new?mode=manual&kind=app&prompt=…, andAppWorkFormrendering, per plan §5.2. A kind thatisHiddenWhenDisabledmatches anddisabledSetcontains (T20) is left out ofallChips/workKindChips/ the composer's chip list instead of being markedcomingSoon, so a disabled App chip is absent as FR-47/S24/ACC-01-13 require; every other disabled kind still renders its inert SOON chip,ALL_NEW_CHIP_VALUES/ALL_WORK_KIND_CHIP_VALUESkeepapp(they feed flag evaluation) and the existingeffectiveChip/effectiveKind/initialTypefallbacks stay, so?type=appand?kind=appdegrade exactly as they do today. Test: extendapps/web/src/components/new/NewPageClient.unit.spec.tsx,apps/web/src/app/[locale]/(dashboard)/works/new/new-work-client.unit.spec.tsxandapps/web/src/components/works/WorksCreateComposer.unit.spec.tsx— one routing case each; a pasted URL with credentials routes with noprompt; withappdisabled the App chip is absent from every one of those surfaces (no SOON chip, no rendered chip whose label isApp) whileblogandstorestill render theirs as SOON unchanged (ACC-01-13). Done when: the existingreporouting tests pass unchanged. -
T24. App Work page. Create
apps/web/src/components/works/app/AppSourceRelation.tsxandapps/web/src/components/works/app/AppSourceStatusCard.tsx(consumes APW-02'sworkAPI.getUpstreamandretryUpstreamReadinessAction; states preparing, timed out, failed, waiting for the setup pull request, setup pull request closed). Modifyapps/web/src/components/works/detail/WorkHeader.tsx,apps/web/src/app/[locale]/(dashboard)/works/[id]/page.tsx,apps/web/src/components/works/detail/WorkTabs.tsx,apps/web/src/app/[locale]/(dashboard)/works/[id]/generator/page.tsx,apps/web/src/app/[locale]/(dashboard)/works/[id]/generator/schedule/page.tsx,apps/web/src/components/works/detail/settings/SettingsForm.tsx,apps/web/src/components/works/detail/settings/SourceSettings.tsx,apps/web/src/components/works/detail/settings/DeleteComponent.tsx,apps/web/src/components/activity-log/ActivityTypeBadge.tsx. Hiding the Generator tab is not enough on its own: both generator routes guard withisRepositoryWorkKind(work.kind)today (generator/page.tsx:44,generator/schedule/page.tsx:92), so an App Work opening/works/:id/generatordirectly would still render the generator surface (spec S10). Each page gainsisAppWorkKind(work.kind)beside the existingrepoguard — the same refusal, the same redirect/not-found shape therepobranch already uses, and the existingrepoguard untouched. Test: createapps/web/src/components/works/app/AppSourceStatusCard.unit.spec.tsx(5 s poll while preparing, stops at 240 polls; 30 s poll while waiting for the setup pull request, stops at 120 polls; teardown on unmount and on terminal states; relation header and ready transition — ACC-01-02; waiting copy with Open pull request and the closed-without-merge copy with Try again — ACC-01-17); extendapps/web/src/components/works/detail/WorkTabs.unit.spec.tsx(generator hidden forapp) andapps/web/src/components/works/detail/settings/DeleteComponent.unit.spec.tsx(link note; fork checkbox needs typed name; payload flag only then — ACC-01-10); createapps/web/src/app/[locale]/(dashboard)/works/[id]/generator/page.unit.spec.tsxandapps/web/src/app/[locale]/(dashboard)/works/[id]/generator/schedule/page.unit.spec.tsx— an App Work renders neither generator page (theappcase) while arepoWork still renders both exactly as today (the unchanged case, ACC-01-11); createapps/web/src/components/activity-log/ActivityTypeBadge.unit.spec.tsx(theapp_sourcelabel). Done when: an App Work page shows the relation line and no Items/Generator/Comparisons surface, and both generator routes refuse an App Work even when opened by URL. -
T25. Chat tool. Modify
apps/web/src/lib/ai/tools/work.tools.ts—workKindEnum+'app'with its description;createWorkManualgainsrepositoryMode,targetOwner,confirmed; returnsConfirmationRequiredforkind === 'app'unlessconfirmed === true, naming the repository to be created or the repository a setup pull request will be opened on. Test: extendapps/web/src/lib/ai/tools/tool-selection.unit.spec.tsand createapps/web/src/lib/ai/tools/work.tools.app.unit.spec.ts— unconfirmed ⇒ nocreateWorkcall; confirmed ⇒ one call with mode/owner; the AI creator schema rejectsapp(ACC-01-14). Done when:apps/web/src/lib/ai/tools/generated/registry-parity.unit.spec.tspasses.
P1.6 — Background
- T26. Wire dispatch and handler.
Modify
packages/agent/src/app-works/app-work-create.service.ts— injectAPP_FORK_READINESS_DISPATCHER(APW-02) and call it after commit with APW-02's payload:{ workId, attempt: 1, reason: 'initial', providerId, credentialVersion }.relationis never sent — APW-02 reads it from theWorkUpstreamStaterow — andproviderId/credentialVersionare captured at the enqueue site (thepackages/agent/src/tasks/credential-version.service.tspattern) so APW-02 T32's "drained credentials skip" can drop a run whose connection rotated or was disconnected in between. Modifypackages/tasks/src/trigger/worker/modules/trigger-worker.module.tsonly if T15's binding is missing. Test: createpackages/tasks/src/trigger/worker/modules/__tests__/trigger-worker.app-works.spec.ts(sibling oftrigger-workflow-run.module.spec.ts) —APP_FORK_READY_HANDLERresolves toAppSourceInitializerService; extendpackages/agent/src/app-works/__tests__/app-work-create.service.spec.ts— dispatch is called once, after the transaction commits, with the four payload fields and norelation, and a provider whose credential version moved between capture and dispatch is still dispatched with the captured value; runcd packages/tasks && npx vitest run src/trigger/worker/modules/__tests__/trigger-worker.app-works.spec.ts. Done when: both specs are green and a local dispatch against a fixture Work runs the handler once.
P1.7 — i18n, tests, docs
-
T27. i18n keys. Modify
apps/web/messages/en.json— every key in plan §8 with spec §6 copy verbatim, including the keys added by the program audit:dashboard.workCreation.app.promptsTitle,promptsHint,promptRequired,promptsIncomplete, the Blueprint setup-note variants (linkSetupNoteBlueprint,existingForkSetupNoteBlueprint), the Blueprint-delete notedeleteAppStoredDataTypeToConfirm, the skipped-import notedashboard.settings.import.appSkipped, and thepreparingBlueprintTitle/preparingBlueprintBodypair forblueprint_applying. Modify the 20 sibling locale files inapps/web/messages/(localised where a translator is available, English otherwise;apps/web/scripts/sync-locale-parity.mjsseeds missing leaves). No leaf key contains.. Test: createapps/web/src/components/works/app/app-works-messages.unit.spec.ts(pattern ofapps/web/src/components/tasks/tasks-kanban-messages.unit.spec.ts) — loads all 21 files inapps/web/messages/, asserts every leaf underdashboard.workCreation.app,dashboard.workDetail.appSource, thedashboard.workDetail.settings.deleteApp*leaves, the four chip/kindappkeys,dashboard.settings.import.appSkippedanddashboard.activity.filters.types.appSourceexists in every file, and that no leaf key under those trees contains.(ACC-01-18). It also asserts the failure copy of spec §6.4: every leaf indashboard.workDetail.appSource.failedReasonexists, theautoProvisionlabel and its help line exist, the Name/Slug/Description/slug-help/invalid-slug/Cancel/create-failed leaves exist, and rendering each of the readiness reason codes (works_yml_unparseable,works_yml_other_kind,push_rejected,provider_unsupported,too_large_for_private_copy,uses_lfs,handler_failed:<code>, an unknown code,retry_limit_reached) yields English copy and never the raw code; runcd apps/web && npx vitest run src/components/works/app/app-works-messages.unit.spec.ts. Done when: the spec is green over the 21 files. -
T28. e2e. Create
apps/web/e2e/flow-app-work-create-refusals.spec.ts—invalid_url(400 names the field),provider_not_connected, quick-create refusesapp, the client half of the instance-setting refusal (the App chip is absent and the create form is unreachable), and — when the lane runs withEVER_WORKS_APP_WORKS_ENABLED=false— the fullapp_works_disabledrefusal for both inspect and create (ACC-01-06, ACC-01-07, ACC-01-13). The PR lane runs that setting on (ACCEPTANCE §0.2) and one API process serves the whole shard, so no Playwright spec can flip it per test; the server half of ACC-01-13 is therefore proven where it is observable — the API Jest specs of T12, T13, T17 and T18 (apps/api/src/works/works.controller.crud.spec.tswith the setting off ⇒400 app_works_disabledwhatever client header, andapp-source.controller.spec.tsthe same for inspect) and the unit specs of T20 — and the e2e case above runs in the setting-off shard named in ACCEPTANCE §0.1. Nothing is dropped and no spec toggles the setting. Createapps/web/e2e/flow-app-work-create-form.spec.ts— chip → form → mocked inspect route → preview copy, disabled Link with reason, deploy picker defaults to None — don't deploy yet, Ever Works Apps disabled (ACC-01-12). PrefergetByTestId. Test:cd apps/web && npx playwright test e2e/flow-app-work-create-refusals.spec.ts e2e/flow-app-work-create-form.spec.ts— ACC-01-06, ACC-01-07, ACC-01-12 in the PR lane, plus ACC-01-13's client half there and its server half in the setting-off shard and in the API Jest specs of T18. Done when: both pass andapps/web/e2e/flow-work-kind-template-activation-deep.spec.ts,apps/web/e2e/flow-work-kind-variants.spec.tspass unchanged. -
T29. Docs. Create
docs/features/app-works.md— what an App Work is, the three modes and their trade-offs, what gets written to your repository (one commit on a fork or copy Ever Works created, a setup pull request otherwise), deploy targets (None, Your cluster, Ever Works Apps), deleting safely (workloads removed, stored data kept unless chosen, fork kept unless chosen). Modifydocs/features/creating-a-work.md— App row in the creation-methods table. Modifyapps/docs/sidebarsPlatform.ts— list the new page. Test:pnpm --filter ever-works-docs build. Done when: the docs build has no broken-link warnings.
P1.8 — Program audit additions (2026-09-17)
-
T39. Deleting an App Work removes its workloads and keeps stored data by default (Resolution R-15). Create
packages/agent/src/app-works/app-work-deletion.port.ts—AppWorkDeletionRequest,AppWorkDeletionOutcome(status: 'pending' | 'done',target,reason?),AppWorkDeletionPort.requestDeletion({ workId, userId, deleteStoredData })andAPP_WORK_DELETION_PORT = Symbol('APP_WORK_DELETION_PORT')exactly as plan §7. Modifypackages/agent/src/app-works/index.ts— export the port. Modifypackages/agent/src/items-generator/dto/delete-items-generator.dto.ts—DeleteWorkDtogainsdelete_stored_data?: boolean = false(@IsOptional,@IsBoolean,@ApiPropertyOptionalstating it applies to kindapponly) andconfirm_slug?: string(@IsOptional,@IsString,@MaxLength(200)), the server-side half of the typed confirmation (FR-40b). Modifypackages/agent/src/services/work-lifecycle.service.ts— injectAPP_WORK_DELETION_PORT@Optional()(appended last); indeleteWorkfor kindapp, refuse the linked-repository request first, then — whendelete_stored_data === true— requireconfirm_slug === work.slug, else422 { code: 'confirmation_mismatch' }before the port is called or any repository step runs (FR-40b); then callrequestDeletionbefore any repository step withdeleteStoredData: delete_stored_data === true; unbound or a throw ⇒ taken asdone(a throw appends the target and reason code tomessage);done⇒ the row is deleted in the request;pending⇒200 { deleting: true, message }and the row stays. AddcompleteAppWorkDeletion(workId)(public, called by APW-06'sAppRuntimeDeletionService) — deletes the row and the local checkout, idempotent, never touches a repository. Modifypackages/agent/src/items-generator/dto/delete-items-generator.dto.tsresponse —DeleteWorkResponseDtogainsdeleting?: boolean. Modifyapps/mcp/src/openapi-tools/whitelist.ts— thedelete_workentry gainsomitArgs: ['delete_stored_data', 'confirm_slug'], so neither the stored-data flag nor its confirmation is advertised as a tool argument and no agent can destroy stored data through MCP (FR-40b). Modifyapps/web/src/lib/api/work.ts—DeleteWorkDtogainsdelete_stored_data?: booleanandconfirm_slug?: string. Modifyapps/web/src/components/works/detail/settings/DeleteComponent.tsx— forapp: the workloads note; Also delete stored data checkbox + typed App Work slug, shown when the deploy target is notnone, sendingdelete_stored_data: trueandconfirm_slug: <typed slug>only when both are satisfied; kept independent of the fork checkbox. The target it reads is APW-06'sGET /api/works/:id/app-target, and a404(APW-06 not merged, or no runtime row yet) is treated asnone, which hides the checkbox (FR-34). Test: extendpackages/agent/src/services/__tests__/work-lifecycle.app-kind.spec.ts— a linked-repository delete request is refused before the port is called;delete_stored_data: truewithoutconfirm_slug, and with a non-matching one, is refused with422 confirmation_mismatchand the port is never called; the matching pair proceeds; port called once, before any repository deletion call, withdeleteStoredData: falsewhen the flag is omitted andtrueonly when it istrue; the fork decision is identical with either value;done⇒ row deleted;pending⇒ row kept, responsedeleting: true, and a latercompleteAppWorkDeletion(workId)deletes it (a second call is a no-op); port unbound ⇒ deletion proceeds; port throws ⇒ row deleted and the message names the target; non-appkinds never call the port. Extendapps/mcp/test/whitelist-app-works.spec.ts—delete_worklists both names inomitArgsand the generated tool schema carries neither. Extendapps/web/src/components/works/detail/settings/DeleteComponent.unit.spec.tsx— stored-data checkbox hidden for targetnoneand whenGET app-targetanswers 404; payload carriesdelete_stored_data: trueand the typedconfirm_slugonly after the typed App Work slug matches; ticking it never setsdelete_data_repository(ACC-01-20, ACC-01-24). Done when: both specs are green andpackages/agent/src/services/__tests__/work-lifecycle.delete.spec.tspasses unchanged. -
T40. Preparing-card acceptance: timeout and lost access (ACC-01-15, ACC-01-16). Modify
apps/web/src/components/works/app/AppSourceStatusCard.unit.spec.tsx(created by T24) —timed_outrenders "Your fork is taking longer than 15 minutes." with Try again and Open on GitHub;failedwithaccess_revokedrenders the lost-access copy with Reconnect GitHub and Try again; Try again callsretryUpstreamReadinessActionexactly once and nevercreateWork;retry_limit_reachedrenders its copy. Createapps/web/e2e/flow-app-work-preparing-card.spec.ts— in the fake-GitHub shard (APW-13'sEVER_WORKS_E2E_FAKES=1harness, with APW-02 T44's shortened non-production readiness deadline): a fork that never finishes shows the timed-out card; Try again sends onePOST /api/works/:id/upstream/readiness/retryand the fake records exactly one fork request in total; the fake answering 401 for the member's token while preparing shows the lost-access card; after the token is restored, Try again reaches ready with still one fork request. Test:cd apps/web && npx vitest run src/components/works/app/AppSourceStatusCard.unit.spec.tsxandcd apps/web && EVER_WORKS_E2E_FAKES=1 npx playwright test e2e/flow-app-work-preparing-card.spec.ts— together they assert ACC-01-15 (lost access ⇒ Failed with Reconnect; Try again resumes on the existing fork with no second fork request) and ACC-01-16 (the 15-minute copy; Try again never requests a second fork). Done when: both pass in the fake-GitHub e2e shard; a run withEVER_WORKS_E2E_FAKESunset (where the spec self-skips, as everyflow-app-work-*spec does) is not accepted as evidence. -
T41. Account import and restore never create or convert an App Work (FR-54). Modify
packages/agent/src/account-transfer/account-import.service.ts—normalizeImportedWorkKind(lines 62-69) must returnundefinedfor'app'instead of accepting it, and the overwrite branch (updateData.kind = importedKind, lines 732-738) must skip the assignment when either the imported kind or the existing Work's kind isapp, so an import can neither create an App Work nor turn a Repository Work into one (nor an App Work back). The entry is reported as skipped with the kind it kept; no error is raised and no other entry's import changes. Modifypackages/agent/src/account-transfer/account-import.service.ts— the import report gains the skip reason codeapp_kind_not_importablebeside the existing per-entry outcomes. Test: extendpackages/agent/src/account-transfer/account-import.service.spec.ts— an export whose entry claims kindappcreates nothing and is listed as skipped; an overwrite whose exported kind isappleaves the existing Work's kind untouched; an overwrite of an existing App Work by arepoentry leaves itapp; every other kind imports exactly as before (ACC-01-23). Done when: the spec is green,WORK_KINDSis unchanged, and a grep over the import path shows'app'is never assigned tokind. -
T42. Blueprint parity across every client (FR-56). Modify
apps/web/src/lib/ai/tools/work.tools.ts— forkind === 'app'the confirmation calls the read-onlyinspectAppSourcefirst and appends "with the {name} App Blueprint" when a Blueprint matched, so a Blueprint is never applied unseen; the tool passes neither a guessed nor a staleblueprintId. Modifyapps/mcp/src/openapi-tools/whitelist.ts— the count comment beside the Works block, so the new read-only inspect tool and theappEnvfield are accounted for. Modifydocs/features/app-works.md(created by T29) — one paragraph: the Blueprint is chosen by the server from the resolution order, whichever client creates the App Work, and a client that names a different one is refused. Test: extendapps/web/src/lib/ai/tools/work.tools.app.unit.spec.ts— a matched Blueprint is named in the confirmation; the created call carries noblueprintIdof its own; extendapps/mcp/test/whitelist-app-works.spec.ts—create_workexposesappEnvand no blueprint override; run the APW-01 T13 create spec against a fake catalog to assert the same Blueprint id is persisted for a web payload and a chat payload with no id (ACC-01-21). Done when: the three specs are green and the same resolution order is asserted for the web, chat and MCP paths. -
T43. Prompted values, end to end (FR-55). Modify
packages/agent/src/dto/create-work.dto.ts—appEnv?: Record<string, string>(@IsOptional,@IsObject, each value@IsString,@MaxLength(8192),@ApiPropertyOptionalstating write-only), and mark it so the generated OpenAPI never lists it as a response property. Modifypackages/agent/src/app-works/app-work-create.service.ts— after the transaction of T13 step 10 commits, hand the values toAPP_PROMPTED_VALUES_PORT.storePrompted(workId, values)@Optional(), once, and never log them; unbound ⇒ log the count only and continue. Modifyapps/api/src/works/dto/create-work.dto.ts(or the Swagger decorators on the agent DTO) — no response DTO ever echoesappEnv. Test: extendpackages/agent/src/app-works/__tests__/app-work-create.service.spec.ts— the port receives exactly the typed names once per create; unbound ⇒ the create still succeeds; a value is never written to the Activity row, the telemetry payload or the response; the schema rejects a non-string value and a value over 8 192 characters (ACC-01-22). Done when: the spec is green andgit grep -n "appEnv" apps/api/srcshows no read path.
P1.9 — Ship gate
- T30. P1 ship gate.
Modify
docs/specs/features/app-works/APW-01-app-work-kind/tasks.md— tick every P1 task; Modifydocs/specs/features/app-works/ACCEPTANCE.md— record the ACC-01 ids covered by automated tests and those deferred to APW-13's live suite. Test:pnpm format && pnpm lint && pnpm type-check && pnpm test && pnpm buildfrom the root, once withEVER_WORKS_APP_WORKS_ENABLED=falseand once withtrue. Done when:developis green withEVER_WORKS_APP_WORKS_ENABLED=false(production default) and withtrue(staging), and every ACC-01-01…ACC-01-20 id names a green test.
Phase P2 — Polish
Refines spec FR-9 (all organizations) and adds the P2 items in plan §11.
-
T31. Existing-fork scan across all organizations. Modify
packages/agent/src/app-works/app-source-inspector.service.ts— paginate owners up to 200, stop at the 15-call cap and mark unscanned ownersexistingFork: undefinedwith ascanIncomplete: trueflag on the response. Modifypackages/contracts/src/apps/app-source.ts— add optionalscanIncomplete?: boolean. Test: extendpackages/agent/src/app-works/__tests__/app-source-inspector.service.spec.ts— 150 organizations, cap honoured, flag set; extendpackages/contracts/src/apps/__tests__/app-source.spec.ts— the field is optional. Done when: an inspect over a 150-organization fixture makes at most 15 provider calls and returnsscanIncomplete: true. -
T32 (parallel with T31).
inspectAppSourcechat tool. Modifyapps/web/src/lib/ai/tools/work.tools.ts— read-only tool returning modes, reasons, default mode and owners (no URL echo beyondfullName). Test: extendapps/web/src/lib/ai/tools/tool-selection.unit.spec.ts(picks it for "can I fork github.com/…?") andapps/web/src/lib/ai/tools/work.tools.app.unit.spec.ts(no confirmation, no write call). Done when: both specs are green andapps/web/src/lib/ai/tools/generated/registry-parity.unit.spec.tspasses. -
T33. Composer suggestion. Modify
apps/web/src/components/new/NewPageClient.tsxandapps/web/src/components/works/WorksCreateComposer.tsx— when the prompt reduces to a canonical GitHub URL and the App chip is enabled, show a one-line suggestion "Looks like a repository. Create an App Work from it?" with Use App. Modifyapps/web/messages/en.jsonand the 20 sibling locale files —dashboard.newPage.appSuggestion,dashboard.newPage.appSuggestionAction. Test: extendapps/web/src/components/new/NewPageClient.unit.spec.tsxandapps/web/src/components/works/WorksCreateComposer.unit.spec.tsx— suggestion only for canonical URLs; never for URLs with credentials; extendapps/web/src/components/works/app/app-works-messages.unit.spec.tswith the two keys. Done when: the three specs are green. -
T34. Remix deep link and Blueprint display name. Modify
apps/web/src/components/works/app/AppWorkForm.tsx— acceptpromptas the URL seed and auto-run inspect once; Modifyapps/web/src/components/works/app/AppSourcePreviewCard.tsx— show the Blueprint display name and notice from APW-03 when present. Test: extendapps/web/src/components/works/app/AppWorkForm.unit.spec.tsx— seeded URL inspects exactly once; createapps/web/src/components/works/app/AppSourcePreviewCard.unit.spec.tsx— display name and notice rendered when present, absent otherwise. Done when: both specs are green. -
T35. P2 ship gate. Modify
docs/specs/features/app-works/APW-01-app-work-kind/tasks.md— tick P2. Test:pnpm format && pnpm lint && pnpm type-check && pnpm test && pnpm buildfrom the root. Done when:developis green with T31–T34 merged.
Cross-phase closing tasks
-
T36. Telemetry. Status (2026-09-25): done (
781f9a2e5). Landed:packages/agent/src/app-works/app-works-telemetry.service.ts(APP_WORKS_TELEMETRY_SINK,APP_WORKS_TELEMETRY_EVENTS,AppWorksTelemetryService,appWorkCreateOutcomeOf), provided and exported byAppWorksModule; emitters in the inspector, the create service, the ready handler andWorkLifecycleService.deleteWork; API bindingAppWorksTelemetryBindingModule(@Global,useExistingAnalyticsService) imported byApiModule. Specs:__tests__/app-works.telemetry.spec.ts(30 cases) andapps/api/src/telemetry/app-works-telemetry-binding.module.spec.ts. The Done-when grep is empty. The outcome union gainedfailed(plan §9.1). Createpackages/agent/src/app-works/app-works-telemetry.service.ts—AppWorksTelemetryServicewithtrack(event, props)over an injected,@Optional(), env-configured sink (APP_WORKS_TELEMETRY_SINK, bound by the API to the existing PostHog client exactly asZeroFrictionFunnelServicebindsFunnelAnalyticsSink—packages/agent/src/services/zero-friction-funnel.service.ts:56); unbound ⇒ the events are counted and dropped, never a refusal or a throw. No epic emits through a package dependency:packages/agent,packages/pluginandpackages/tasksdo not depend on a monitoring package, so the sink is a token this epic declares, not an import. Modifypackages/agent/src/app-works/app-source-inspector.service.ts,packages/agent/src/app-works/app-work-create.service.ts,packages/agent/src/app-works/app-source-initializer.service.tsandpackages/agent/src/services/work-lifecycle.service.ts(delete path) — emit the five events of plan §9.1 through that service. FR-53 is delivered by P1 (tasks.md:64), so this task lands with P1.7's T27 i18n pass, ahead of the P2 tasks, even though its number sits in this closing block. Test: createpackages/agent/src/app-works/__tests__/app-works.telemetry.spec.ts— each event emitted once per outcome with a bound sink; nothing thrown and nothing logged but a count with the sink unbound; no payload contains a repository name, URL, owner, token or file content (FR-53). Done when: the spec is green andgit grep -n "@ever-works/monitoring" packages/agent/src/app-worksreturns nothing. -
T37. Tracker and contracts. Modify
docs/specs/features/app-works/TRACKER.md— APW-01 status and PR links. Modifydocs/specs/features/app-works/CONTRACTS.mdonly if a name this epic created is missing — every name in the plan header must appear with APW-01 as owner. Test:git grep -n "APW-01" docs/specs/features/app-works/CONTRACTS.mdlistscreatedByThisWork,APP_WORK_DELETION_PORT,delete_stored_data,builds/appEnvironmentand every other name in the plan header. Done when: that listing is complete and TRACKER.md links every merged APW-01 PR. -
T38. Update statuses. Modify
docs/specs/features/app-works/APW-01-app-work-kind/spec.md,docs/specs/features/app-works/APW-01-app-work-kind/plan.mdand this file —Implemented/Done. Test:npx prettier --check docs/specs/features/app-works/APW-01-app-work-kind/*.md, and each gate in plan §12 re-checked against the merged code. Done when: the three files carry the new status and prettier reports no changes needed.
Definition of Done
- Every checkbox above is ticked.
pnpm format:check,pnpm lint,pnpm type-check,pnpm testandpnpm buildare green from the root.packages/agent/src/works/repository-work-guard.tsand therepobranch ofWorkLifecycleService.createWorkhave no diff; every pre-existingrepotest passes unchanged.- With
EVER_WORKS_APP_WORKS_ENABLED=false, no chip, chat call, MCP call or command-line call can inspect or create an App Work. - Inspect produces no GitHub write in any test (asserted by facade spies); no code path in this epic clones a data repository to record its source, or pushes to a default branch it did not create.
- ACC-01-01 … ACC-01-27 are each covered by an automated test here or listed as a live scenario in ACCEPTANCE.md for APW-13.
- Every task named in the Prerequisites is merged before this epic's P1, in the program merge order; no task here adds, renames or removes a symbol another epic owns.
- Every gate in plan §12 is confirmed, and its carried-forward gaps are still recorded there.