Task Breakdown: App spec, Apps catalog and license gate
Ordered tasks derived from
plan.md. Each is small enough to land in one PR and ships with tests per Constitution VI. The schema task ships its migration in the same PR per Constitution V.
Epic ID: APW-03-app-spec-and-catalog
Spec: ./spec.md · Plan: ./plan.md · Reference: ./schema.md, ./catalog.md
Status: Draft
Last updated: 2026-09-17 (program audit fix pass — CONTRACTS.md §0 R-1…R-24 applied)
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 (Create / Modify), the test that proves it (Test — for a test-only task, the command that runs it) and an observable Done when.
- Add new tasks at the bottom rather than renumbering (T53+ were added by the program audit; each names the phase it ships in). T55–T58 were added by the 2026-09-17 ordering and wiring pass.
- Phase boundaries are ship boundaries:
developis green and deployable at the end of each phase. - Commands run from the monorepo root; migrations are authored from
apps/api/. - Prerequisites (exact tasks). P1: none — T1–T21, T54 depend on no other App Works epic. T11 (the Wave 1
catalog seam) lands first, on its own, immediately after APW-02 T15, because T26 below binds
APP_SOURCE_CATALOG_PORTinpackages/agent/src/app-works/— the folder APW-02 T15 creates. P2: APW-01 T11 (the port file and its barrel export) and APW-06 T1–T3 (packages/contracts/src/apps/app-runtime.ts, theIDeploymentPluginApp additions andpackages/agent/src/app-runtime/ports.tswithAPPS_TIER_POLICYandDisabledAppsTierPolicy) are merged. Two pieces of P2 are interface-first and merge ahead of APW-01 P1 — T22'scommitFiles?, and T26'sAppSourceCatalogAdapter— and the whole of P2 merges ahead of APW-01 P1, so no phase-level loop exists (T55 below states the ordering). P3: P1 and P2 (plan §11 snapshot exception). Ifpackages/agent/src/app-runtime/ports.tsdoes not exist when T24 or T26 lands (APW-06 T3 not yet merged), that task creates it from CONTRACTS §3 verbatim — theAppsTierPolicyinterface, theAPPS_TIER_POLICYsymbol andDisabledAppsTierPolicy— and APW-06 T3 then modifies it instead of creating it; nothing here ever removes or renames what APW-06 declared. - Own-PR seam (no phase-level loop). The program merge order runs APW-01 T11 → the APW-03 P2 seam (T22, T24, T26, T28 + T53, T32) → APW-01 P1 (its remaining tasks) → APW-05 P1 → APW-06 P1 (T4 onward). T55 restates that ordering in one place; no task in this file waits on APW-01 P1.
- Test commands: agent package
pnpm --filter @ever-works/agent test -- <pattern>(Jest); APIpnpm --filter ever-works-api test -- <pattern>(Jest); web unitpnpm --filter ever-works-web test -- <pattern>(Vitest); web e2epnpm --filter ever-works-web test:e2e -- <file>(Playwright,apps/web/e2e/). Nothing is placed underapps/api/test/(Resolution R-22). - Binding program resolutions (CONTRACTS.md §0):
R-1 shared types in
packages/contracts/src/apps/; R-2 Activityaction= dotted name,actionType= family; R-3 red/amber hosting; R-4 first write; R-5AppsTierPolicy; R-11 key pair formats; R-13auto; R-22 test locations.
Phase P1 — The App spec reads true
Delivers spec FR-1…FR-26 and the App spec tab (FR-66…FR-70, FR-74…FR-80 for it). No catalog, no license gate.
P1.1 — Contracts
-
T1. Shared App Works types and limits. Create in
packages/contracts/src/apps/(R-1):app-spec.types.ts,app-spec-issues.ts,app-license.types.ts,apps-catalog.types.ts,work-app-spec.dto.ts,apps-limits.ts,index.tsexactly as plan §3.2 — every constant;APP_SPEC_ISSUE_CODESwith every code of schema.md §22–§23 includingkeypair_format_unsupportedandkeypair_password_invalid;ManagedHostingReasonwithupstreamAgreementMissing;BlueprintMatchSourcewithexplicit;AppSpecBuildStrategy = 'dockerfile' | 'image' | 'auto' | 'none'(R-13);AppSpecKeypairFormatas'pem' | 'base64url-raw' | 'pkcs12'(R-11);AppSpecValidationModeas'draft' | 'data-repository' | 'blueprint'(schema.md §3); the pureisSourceOnlyAppSpec(spec)helper that T12 and APW-01 consume; andspec.agents.requireHumanMergePaths(astring[]of globs, ≤ 50, default[]) on the App spec types (CONTRACTS §1 "Additions (APW-08)"; T3 owns the schema and schema.md §18 defines it). Modifypackages/contracts/src/index.ts—export * from './apps/index.js';. Test:packages/contracts/src/apps/__tests__/apps-contracts.spec.ts— pins the issue-code tuple (append-only snapshot), theManagedHostingReasonunion in evaluation order,BlueprintMatchSource, both new string unions, theAppSpecValidationModetuple and every numeric constant. Done when:pnpm --filter @ever-works/contracts buildemits the declarations andapps/apiimportsAppSpecIssuefrom@ever-works/contracts; no file is created under asrc/app-works/folder. -
T2 (parallel). Activity action types and domain events. Modify
packages/agent/src/entities/activity-log.types.ts— appendAPP_SPEC = 'app_spec',APP_BLUEPRINT = 'app_blueprint',APP_LICENSE = 'app_license'toActivityActionType(append only; varchar column, no migration). Rows written by this epic use these asactionTypeand the dotted CONTRACTS §6 name asaction(R-2). Three fields every row of these three families must carry (CreateActivityLogDtorequires them —packages/agent/src/entities/activity-log.types.ts:427-433— while the plan defines only the event name and itsdetails, §6.2, §6.3 and §2.5; Resolution R-34 is the binding rule this task implements): -status—ActivityStatus.COMPLETEDfor a classification, an apply, an attestation, an upgrade notice or a catalog refresh;FAILEDforapp.spec.invalid, an apply failure, a license refusal, a catalog read that failed, and anyapp.license.*row that records a mismatch;CANCELLEDonly when a member cancels. -summary— one English template per event, plus its i18n key, because the web renders the summary rather than deriving one:app.spec.validated→ "App spec checked — {warningCount} warnings";app.spec.invalid→ "App spec has {errorCount} problems";app.spec.applied→ "App spec applied";app.blueprint.matched→ "Blueprint {name} matched";app.blueprint.applied→ "Blueprint {name} applied";app.blueprint.apply_failed→ "Blueprint {name} could not be applied";app.license.classified→ "License classified: {class}";app.license.changed→ "License changed from {fromSpdx} to {toSpdx}";app.license.attestation_required→ "License attestation needed";app.license.declared_mismatch→ "Declared license does not match the registry";apps.catalog.refreshed→ "Apps catalog refreshed". (The final copy and its keys land with T18; this task declares the fields and the per-event templates so no row is ever written with an emptysummary.) -userId— the App Work's owner, always, including for webhook-, cron- and apply-job-triggered rows: there is no system actor in the model, so a background row is attributed to the member whose Work it is (the same rule T53's conflict comment uses). A row whose Work has no resolvable owner is not written; it is counted and logged instead. Modifyapps/web/src/components/activity-log/ActivityTypeBadge.tsx— one colour and oneTYPE_TO_I18Nentry per family (app_spec,app_blueprint,app_license), as APW-02 T30 does for its three and APW-08 for its own; without this the three families render as raw enum text (ActivityTypeBadge.tsx:29-49). Modifyapps/web/src/components/activity-log/ActivityFilters.tsxandapps/web/messages/en.json(+ the 20 siblings) — the matching filter labels and badge colours, which T18's message spec asserts. Createpackages/agent/src/events/app-spec-applied.event.ts(EVENT_NAME = 'app.spec.applied', payload{ workId, commitSha, previousCommitSha, specHash, addedDependencies, changedEnvNames, changedBlocks }) andpackages/agent/src/events/app-license-changed.event.ts('app.license.changed'). Modifypackages/agent/src/events/index.ts— export both. Test: extendpackages/agent/src/events/events.spec.ts— names are unique and dotted; extendpackages/agent/src/entities/__tests__/activity-log.types.spec.ts— the three values exist and existing values are unchanged; extendpackages/agent/src/activity-log/feed-kind.spec.ts(via theFEED_KIND_RULESentry R-34 requires) and the publishable-activity spec so three new members are classified; createpackages/agent/src/activity-log/__tests__/app-works-activity-rows.spec.ts— every row written by this epic carries a non-emptysummary, astatusand the Work owner'suserId, and a row with no resolvable owner is dropped rather than written blank (R-34). Done when:pnpm --filter @ever-works/agent test -- events.spec activity-log.types app-works-activity-rowspasses, no existing enum value changed, andActivityTypeBadgerenders all three families with a colour and a label instead of raw text.
P1.2 — Schema and validator
-
T3. Structural App spec schema. Create
packages/agent/src/works-config/schema/app-spec.schema.ts—z.strictObjectfor every block of schema.md §5–§20 with bounds, enums and.describe()defaults (build.strategyenumdockerfile | image | auto | none— no builder name, R-13;generate.keypair{ type, format, passwordEnv }, R-11;agents.requireHumanMergePaths: an array of glob strings, ≤ 50, default[]— schema.md §18, CONTRACTS §1 "Additions (APW-08)", R-11-style additive key);appSpecSchema;stripExtensionKeys(value)returning a copy withoutx-*keys at any depth; and the validation modes of schema.md §3 exactly as that table now reads —blueprintmode allows and expectsspec.sourceandspec.blueprint(the 2026-09-17 correction to §3's row;blueprint_mode_forbidden_keystays in §23's list for the structural cases §2 describes but is no longer emitted for those two keys), so a Blueprint repository's own file is validatable with the same rules the platform runs. Modifypackages/agent/src/works-config/schema/works-config.schema.ts— addapp: appSpecSchematoKIND_SPEC_SCHEMAS. Test:packages/agent/src/works-config/schema/__tests__/app-spec.schema.spec.ts— the three schema.md §24.1–§24.3 examples parse (ACC-03-01); one failing case per bound and enum;x-keys accepted and a newerappSpecVersiondowngradesunknown_field(ACC-03-02);autoaccepted, any other strategy string refused, and each §12 key pair example —pem,base64url-raw,pkcs12— parses (ACC-03-49); a type-level assertion thatz.input<typeof appSpecSchema>andAppSpec(T1) are mutually assignable. Done when: the spec passes and no other kind's fixture changes. -
T4. Reference grammar and resolution. Create
packages/agent/src/works-config/schema/app-spec.refs.ts— tokenizer for schema.md §21,resolveReference(ref, spec), output tables per dependency, secrecy and phase propagation, Tarjan cycle detection overtemplateedges, depth limit 10;build.commitSharesolves fordockerfileandauto. Test:packages/agent/src/works-config/schema/__tests__/app-spec.refs.spec.ts— every row of schema.md §21's table (incl.build.commitShaunderautoand its refusal underimage),bucket.<name>, a three-entry cycle naming all three, depth 11 refused. Done when:pnpm --filter @ever-works/agent test -- app-spec.refspasses with one case per §21 row. -
T5. Cross-field rules R1–R26. Create
packages/agent/src/works-config/schema/app-spec.rules.ts— one exported pure function per rule; R4 counts each keypair entry's implicit<NAME>_PUBLIC; R9 generates 3 samples from a fixed seed (abase64url-rawkey pair has generated length 43); R10 usesscanForSecrets(re-exported bypackages/agent/src/utils/secret-scan.ts) plus the name list; R21 usescomputeNextCronFirefrompackages/agent/src/schedules/cadence.ts; R25keypair_format_unsupported; R26keypair_password_invalid. Test:packages/agent/src/works-config/schema/__tests__/app-spec.rules.spec.ts— a failing and a passing fixture per rule R1–R26, asserting code, severity, line and column (ACC-03-03); R19 is an error inblueprintmode for a verified entry context; the §12 invalid examples report exactly R25 and R26 (ACC-03-49). Done when: every rule id in schema.md §22 has a named failing and passing case. -
T6. Positioned validation and issue builder. Create
packages/agent/src/works-config/schema/app-spec.issues.ts—describeIssue(code, params)whose params type admits names only; Damerau–Levenshtein suggestion ≤ 2. Createpackages/agent/src/works-config/schema/app-spec.validate.ts—validateAppSpecDocument(text, { mode, context? })andvalidateAppSpecObject(obj, options)per plan §2.2: 256 KiB,uniqueKeys,maxAliasCount: 100, depth 12,LineCounterpointer map, nearest-ancestor positions,displayPathby names, sort, 200 cap, newerappSpecVersiondowngrade, never throws; server-onlybuild_strategy_unavailablewhencontext.buildStrategieslacks the declared strategy and that strategy isdockerfileorauto(imageandnoneneed no builder, so they never warn).RuleContextis defined here and is the only place the server-only rules read from:recordedRelation(link·fork·private-copy),catalogIdsorcatalogUnavailable,buildStrategiesornull,dependencyProviderskeyed by deploy target ornull,trackedBranchExists. Anull(unknown) field skips its rule, so before APW-05 and APW-07 exist nothing is reported for a strategy or a dependency nobody could have resolved;AppSpecService.evaluatefills each field fromWork.sourceRepository,AppsCatalogService's registry,IBuildPlugin.supportedStrategiesover the enabled build plugins andIAppDependencyProvider.supportsover the configured providers for the Work's target — an unavailable source isnull, never an empty list. The rule set runs whenever the document parses (plan §2.2, ACC-03-01/S5): structural errors that stop parsing areyaml_syntax,file_too_largeandyaml_alias_limit/depth failures; every other structural error is reported and R1–R26 run on a best-effort copy with the offending keys and invalid leaves removed, so §24.4 yields its six codes together and a single invalid leaf never produces a duplicate report. A subtree that cannot be copied is named in the issue'sdetailsand suppresses only the rules that read it. Test:packages/agent/src/works-config/schema/__tests__/app-spec.validate.spec.ts— schema.md §24.4 yields exactly its six codes and display paths with the rules having run despite the structuralunknown_field(ACC-03-01); S5's pair (unknown_field+web_component_needs_port) together;yaml_syntax,file_too_largeand the alias/depth failures are the only inputs that suppress the rule set, each asserted; a case where one invalid leaf suppresses exactly the rules that read it and no others; unknown key suggestion,x-silence, newer version warning (ACC-03-02); line/column for present and absent keys; a property test inserting random secret-shaped strings intobuild.args, secretvalues andprompt.exampleand asserting no issue string contains them (ACC-03-04); 300 KiB, 101-alias and 13-level files each report one error (ACC-03-05); a 256 KiB draft validates under 2 s (ACC-03-08);autowith an emptybuildStrategiescontext reportsbuild_strategy_unavailable(ACC-03-49);autowith an emptybuildStrategiescontext reportsbuild_strategy_unavailable, whileimageandnonenever do (ACC-03-49); everyRuleContextfield leftnullskips its rule, so a default context produces no server-only issue at all (ACC-03-58); theblueprint-draftmode accepts a draft carryingsourceandblueprintthat the strictblueprintmode reports asblueprint_mode_forbidden_key(ACC-03-52). Done when: a 256 KiB fixture validates in under 2 seconds on CI. -
T7. Route
kind: appthrough the new validator. Modifypackages/agent/src/works-config/schema/works-config.schema.ts— invalidateWorksConfig, when the resolved kind isapp, callvalidateAppSpecObjectand map issues through the existingformatIssuesstring shape (errors →errors, warnings →warnings). Modifypackages/agent/src/works-config/index.ts— export the validator, schema and refs modules. Test: extendpackages/agent/src/works-config/schema/__tests__/works-config.schema.spec.tswithkind: appcases; every existing case passes unmodified (ACC-03-06). Done when: the diff ofworks-config.schema.spec.tscontains only added cases. -
T8. JSON Schemas and the public route. Modify
packages/agent/src/works-config/schema/emit-json-schema.ts— rootallOfif/then forkind: app; post-processpatternProperties: { "^x-": {} }on strict objects. Createpackages/agent/src/works-config/schema/emit-app-spec-json-schema.tsand the committedpackages/agent/src/works-config/schema/app-spec.v1.schema.json. Modifypackages/agent/src/works-config/schema/works.v2.schema.json(regenerated). Modifyapps/api/src/onboarding/works-schema.controller.ts—@Public()GET api/schema/app-spec.schema.json,Cache-Control: public, max-age=300. Test: extendpackages/agent/src/works-config/schema/__tests__/emit-json-schema.spec.ts(an in-test JSON Schema validator rejectsreplicaunder an app component; the escape branch still accepts an unknown kind); createpackages/agent/src/works-config/schema/__tests__/emit-app-spec-json-schema.spec.ts(drift guard) andapps/api/src/onboarding/works-schema.controller.spec.ts(both public routes, 5-minute cache header) — ACC-03-07. Done when: both committed JSON files equal their generators' output andGET /api/schema/app-spec.schema.jsonanswers 200 without authentication.
P1.3 — Entity, table, migration
-
T9.
WorkAppSpecStateentity. Createpackages/agent/src/entities/work-app-spec-state.entity.ts— every column and index of plan §3.1 (incl.blueprintMatchedAt), modelled onpackages/agent/src/entities/skill-tag.entity.ts. Modifypackages/agent/src/entities/index.ts,packages/agent/src/database/_entity-names.ts,packages/agent/src/database/_entities-inventory.ts. Test:packages/agent/src/entities/__tests__/work-app-spec-state.entity.spec.ts— index names, scope columns, portable dates. Done when: the database drift specs pass without a magic-number edit. -
T10. Migration. Create
apps/api/src/migrations/1792030000000-CreateWorkAppSpecStates.ts— generate withcd apps/api && pnpm typeorm migration:generate -d typeorm.config.ts src/migrations/CreateWorkAppSpecStates, then re-stamp to1792030000000(re-stamp again ifdevelophas a newer migration than1791240000000-AddSafetyRailsCore.ts). Test:apps/api/src/migrations/__tests__/CreateWorkAppSpecStates.spec.ts— creates only the new table and three indexes;down()drops only them; noALTERof an existing table. Done when: a fresh database migrates up, down and up again. -
T11. State repository. Create
packages/agent/src/database/repositories/work-app-spec-state.repository.ts—findByWorkId,initialize(workId, branch, scope),requestEvaluation(workId)(oneUPDATE … RETURNINGimplementing the coalescing rule),markStarted(workId),writeEvaluation(workId, seq, result)guarded byevaluatedSeq < :seq,writeLicense,markBlueprintMatched(workId, blueprintId, version, matchSource)(the once-only guard of plan §2.5 step 0),findUpgradeCandidates,findStaleRegistry. Modifypackages/agent/src/database/repositories/work.repository.ts— addfindAppWorksByDataRepoFullName(fullName, { userId?, organizationId? }): kindapponly, matching the Work Repository (websiterole) case-insensitively, with nogithubAppInstalledfilter and scoped to the binding's user or organization. This is the lookup the push consumer of T14 uses —findByDataRepoFullNameselects only Works with the platform GitHub App installed (work.repository.ts:263-277), so an App Work on a member's own fork (the normal Wave 1 case, whose webhook APW-02 created) would never be found, and it ignores the delivery's owner binding, so one tenant's delivery would request evaluations for every tenant's Works on that repository.findByDataRepoFullNameitself is unchanged. Modifypackages/agent/src/database/index.ts— export it. Test:packages/agent/src/database/repositories/__tests__/work-app-spec-state.repository.spec.ts— coalescing arithmetic (dispatch / skip), an older seq writes nothing so the newer result stays (ACC-03-12), a deleted Work affects 0 rows,markBlueprintMatchedreturnstrueonce per id + version. Done when: the spec passes on SQLite and the out-of-order case is a named test.
P1.4 — Evaluation service and jobs
-
T12.
AppSpecService. Createpackages/agent/src/app-spec/app-spec.service.ts,packages/agent/src/app-spec/app-spec-hash.ts,packages/agent/src/app-spec/app-spec-guarded-blocks.ts(diffGuardedSpecBlocksoversource,blueprint,license,display.protectedPaths,upstreamPullRequests,provisioning;isProtectedPath),packages/agent/src/app-spec/app-spec.module.ts,packages/agent/src/app-spec/index.ts. Methods:initialize,requestEvaluation(workId, trigger),evaluate(workId)(lockapp-spec-evaluate:<workId>viaDistributedTaskLockService.runExclusive, read head withGitFacadeService.getLatestCommit+getFileContent, validate, write, emit Activity throughActivityLogService.logwithactionType: APP_SPECand the dottedactiononly on head-hash change, emitAppSpecAppliedEventon effective-hash change, tracked-branch move per plan §2.3),getEffectiveSpec(workId, commitSha?),validateDraft(workId, text),getState(workId)with the 60-second lazy head check, andhasValidAppSpec(workId, commitSha): Promise<boolean>— true only when.works/works.ymlatcommitShaparses, selects kindapp, validates with zero errors (warnings allowed) and itsspecholds at least one key outside{ kind, appSpecVersion, source };x-*keys are ignored, and a file holding onlysourceis therefore false. It reads that commit synchronously throughGitFacadeService, neverWorkAppSpecState.validationStatusor the effective-spec cache (both are asynchronous — the row APW-01 just initialized has not been evaluated yet), and it never writes state. The key set comes from one pure helper,isSourceOnlyAppSpec(spec)inpackages/contracts/src/apps/, which APW-03's apply-job fresh rule and APW-01 use too. Without this predicate the minimal path's{version, kind, spec.source}file would read as valid (R2 requiresbuild/componentsonly when one of them is present), so the App Provisioner would never start and APW-01 FR-29a could not hold.diffGuardedSpecBlocksreports removals fromdisplay.protectedPathsandagents.requireHumanMergePaths(additions stay allowed) oversource,blueprint,license, both path lists,upstreamPullRequestsandprovisioning;isProtectedPathusesminimatchwith{ dot: true }— the matcher APW-08 plan §4 uses — added topackages/agent/package.jsonat the version the lockfile resolves, so both epics match globs identically. Modifypackages/agent/package.json— add the./app-specexport. Test:packages/agent/src/app-spec/__tests__/app-spec.service.spec.ts— every branch in plan §10.1 for this file: a push that changes the spec updates the state and one on another branch changes nothing (ACC-03-09); an invalid head keeps the effective commit andgetEffectiveSpec(workId, badSha)returnsinvalid(ACC-03-10); no Activity on an identical re-evaluation (ACC-03-11); the lazy check schedules at most once a minute (ACC-03-14);unreadablekeeps the effective spec.packages/agent/src/app-spec/__tests__/app-spec-hash.spec.ts— canonical hash stability;packages/agent/src/app-spec/__tests__/app-spec-guarded-blocks.spec.ts— alicense.classchange and a removed protected path are reported (ACC-03-15), and so is a removal fromagents.requireHumanMergePathswhile an addition to either list is not;hasValidAppSpeccases in the same spec: file absent ⇒ false;sourceonly ⇒ false;sourceplus anx-key ⇒ false;source+ a validbuild+components⇒ true; the same with an R1 error ⇒ false; and it returns the right answer whilevalidationStatusis stillmissing(nothing evaluated yet) (ACC-03-57). Done when:pnpm --filter @ever-works/agent test -- app-specpasses andgetEffectiveSpecis exported from@ever-works/agent/app-spec. -
T13.
app-spec-evaluatedispatcher and job. Createpackages/agent/src/tasks/app-spec-evaluate.types.ts,packages/agent/src/tasks/app-spec-evaluate-dispatcher.ts(APP_SPEC_EVALUATE_DISPATCHER, modelled onpackages/agent/src/tasks/work-import-dispatcher.ts),packages/agent/src/tasks/app-works-jobs.ts(job ids + handlers, likepackages/agent/src/tasks/memory-fact-jobs.ts),packages/tasks/src/tasks/trigger/app-spec-evaluate.task.ts. Modifypackages/agent/src/tasks/index.ts,packages/agent/src/tasks/_tasks-symbols.ts,packages/agent/src/tasks/job-runtime.providers.ts(DISPATCHER_SYMBOLS+ recounted provider arity),packages/tasks/src/tasks/trigger/index.ts. Test:packages/agent/src/tasks/__tests__/app-works-dispatchers.spec.ts— the symbol isSymbol(...), listed in the barrel inventory and inDISPATCHER_SYMBOLS;packages/agent/src/tasks/tasks.spec.tsandpackages/agent/src/tasks/__tests__/job-runtime.providers.spec.tspass after recounting. Done when: anulldispatch runs the handler in-process for this job only. -
T14. GitHub push and merge intake. Create
apps/api/src/ingest/github/app-spec-github-intake.service.tsimplementingGitHubWebhookConsumer(events: ['push', 'pull_request']), registered inonModuleInitlikeapps/api/src/ingest/github/github-check-intake.service.ts; Works found withWorkRepository.findByDataRepoFullName; per-Work cap 30 dispatches per minute. Modifyapps/api/src/ingest/ingest.module.ts— provide the new consumer next toGitHubCheckIntakeService. Test:apps/api/src/ingest/github/app-spec-github-intake.service.spec.ts— a push to the tracked branch requests evaluation and a push to another branch does not (ACC-03-09); merged vs closed-unmerged; non-app Works ignored; the cap; an App Work whose repository has no platform GitHub App installed is still found; a delivery bound to another user or organization matches nothing (ACC-03-56); extendpackages/agent/src/database/repositories/__tests__/work.repository.spec.ts— the new lookup ignoresgithubAppInstalled, matches case-insensitively and honours the owner binding. Done when:pnpm --filter ever-works-api test -- app-spec-github-intakepasses and existinggithub-check-intakespecs pass unchanged.
P1.5 — API
- T15.
GET app-specandPOST app-spec/validate. Createapps/api/src/works/work-app-spec.controller.tsandapps/api/src/works/dto/app-spec.dto.ts— the two routes of plan §4.1 withWorkOwnershipService.ensureCanView/ensureCanEdit,422 notAnAppWork,413 file_too_large, throttles (6/min per Work forbranch, 30/min per member forcontent),@ApiOperationon both. Modifyapps/api/src/works/works.module.ts— register the controller and importAppSpecModule. Test:apps/api/src/works/work-app-spec.controller.spec.ts— every P1 row of plan §4.1–§4.2; acontentvalidation stores nothing and the 31st request in a minute is refused (ACC-03-08); three Re-check presses in 5 s run one evaluation and the 7th in a minute is refused (ACC-03-13);GETwith a stale head schedules at most one evaluation per minute (ACC-03-14); viewer vs editor and cross-account 404 (ACC-03-41);202never awaits the job. Done when: both routes appear in the generated OpenAPI document with their error codes.
P1.6 — Web
-
T16. Route, tab and clients. Modify
apps/web/src/lib/constants.ts—ROUTES.DASHBOARD_WORK_SETTINGS_APP_SPEC. Modifyapps/web/src/components/works/detail/settings/SettingsSubTabs.tsx— fourth tab visible whenuseWorkDetail().work.kind === 'app'; General'sisActiveexcludes/settings/app-spec. Createapps/web/src/lib/api/work-app-spec.ts(server-only) andapps/web/src/app/actions/dashboard/app-spec.ts(recheckAppSpecAction). Test:apps/web/src/components/works/detail/settings/SettingsSubTabs.unit.spec.tsx(new) — tab present forapp, absent forwebsite(ACC-03-39). Done when:pnpm --filter ever-works-web test -- SettingsSubTabspasses and the three existing tabs render unchanged for every other kind. -
T17. App spec page (P1 surfaces). Create
apps/web/src/app/[locale]/(dashboard)/works/[id]/settings/app-spec/page.tsxand, underapps/web/src/components/works/detail/settings/app-spec/,AppSpecPageClient.tsx,AppSpecStatusBanner.tsx,AppSpecProblemsList.tsx,AppSpecSections.tsxper plan §5; slots left for the Blueprint and License cards (filled in T31, T45). Test:apps/web/src/components/works/detail/settings/app-spec/AppSpecStatusBanner.unit.spec.tsx,apps/web/src/components/works/detail/settings/app-spec/AppSpecProblemsList.unit.spec.tsx— every §6.2 banner state, sorting, filtering, link built fromlinks.file+lineAnchor(ACC-03-40), polling stops after 24 polls. Done when: the banner state is present in server-rendered HTML.
P1.7 — i18n, tests, docs
-
T18. P1 i18n. Modify
apps/web/messages/en.json—dashboard.workDetail.settings.tabs.appSpecand thedashboard.workDetail.settings.appSpecstatus, problems, env andissues.<camelCode>keys of plan §8 (incl.issues.keypairFormatUnsupported,issues.keypairPasswordInvalid); the same keys in the 20 sibling locale filesapps/web/messages/{ar,bg,de,es,fr,he,hi,id,it,ja,ko,nl,pl,pt,ru,th,tr,uk,vi,zh}.json. Createapps/web/src/components/works/detail/settings/app-spec/app-spec-messages.unit.spec.ts— modelled onapps/web/src/components/meetings/meetings-messages.unit.spec.ts: discovers ≥ 21 locales; every key the P1 components read exists in every locale; every leaf is camelCase with no.; every message survives acreateTranslatorround trip; oneissues.<camelCode>leaf perAPP_SPEC_ISSUE_CODESentry. Test:pnpm --filter ever-works-web test -- app-spec-messages(ACC-03-43, P1 surfaces). Done when: the spec passes and the web build reports no missing messages. -
T19. P1 e2e. Create
apps/web/e2e/flow-app-spec-settings.spec.tsandapps/web/e2e/flow-app-spec-recheck.spec.ts(plan §10.3); prefergetByTestIdfor problem rows. Test:pnpm --filter ever-works-web test:e2e -- flow-app-spec-settings flow-app-spec-recheck— tab absent on awebsiteWork and present on an App Work (ACC-03-39); every seeded banner state and problem link (ACC-03-40); Re-check → Checking… with one evaluation (ACC-03-13); a viewer sees no Re-check (ACC-03-41). Done when: both pass andapps/web/e2e/flow-work-kind-template-activation-deep.spec.tspasses unchanged. -
T20 (parallel). P1 docs. Modify
docs/agent-services/works-yml-schema.md— addappto the kind table, a### appsection with the schema.md §24.1 example, a link to the published reference, and a "Validation behaviour" note thatappspecs are strict in reporting and still preserved on write; documentbuild.strategy: autowithout naming a builder and the key pairformatvalues. Modifydocs/api/works.md— documentGET /api/works/:id/app-specandPOST /api/works/:id/app-spec/validatewith request, response and error tables. Test:pnpm --filter ever-works-docs build— no broken-link warning for the two pages. Done when: the docs build is green and both routes appear indocs/api/works.md. -
T21. P1 ship gate. Modify
docs/specs/features/app-works/APW-03-app-spec-and-catalog/tasks.md(tick T1–T20) and the APW-03 row ofdocs/specs/features/app-works/TRACKER.md. Test:pnpm format:check && pnpm lint && pnpm type-check && pnpm test && pnpm build. Done when: all five commands are green on the P1 merge commit and TRACKER shows APW-03 P1 merged.
Phase P2 — Apps catalog and Blueprints
Delivers spec FR-27…FR-52, FR-71…FR-73, FR-81 and FR-82.
-
T22. Git provider additions for catalogs. Status (2026-09-25): partially done.
GitRepository.topics?is in the contract (e74f6e045) and the GitHub plugin maps it (an absent key means "not reported",[]means "no topics", non-string entries are dropped);commitFiles?andgetFileWebUrl?are not declared yet. Shipped beside it: the pure licence classifier (classifyLicenseExpressioninpackages/agent/src/app-license/license-classify.ts, withNOASSERTIONfixed red —catalog.md§4) andAppSourceCatalogAdapter, bound toAPP_SOURCE_CATALOG_PORTin the packages/agentAppWorksModulerather than anapps-catalog.module.ts. The adapter classifies on the bundled seed snapshot — a typed.tsconstant (license-registry.snapshot.ts, a verbatim copy ofever-works/templates@46d12bb'slicenses.yml, not yet legal-reviewed) instead of plan §2.6'slicense-registry.snapshot.yml, becausenest build -b swccopies no non-TS assets. Open: the live / last-good registry read (T24/T38), obligations output, the forcedmanagedHosting: falsefor a snapshot classification (FR-38), and the Blueprint match, held behind the apply gate until T28 bindsAPP_BLUEPRINT_APPLY_SERVICE. Modifypackages/plugin/src/contracts/capabilities/git-provider.interface.ts—GitRepository.topics?,commitFiles?,getFileWebUrl?exactly as plan §7. Modifypackages/plugins/github/src/github-api.service.ts(maptopics; Git Data API commit with non-forceupdateRefand anonFastForwarderror code; file URL +#L{line}anchor),packages/plugins/github/src/github.plugin.ts(delegations),packages/agent/src/facades/git.facade.ts(wrappers that returnnull/ throwunsupportedwhen the plugin lacks a method). Test:packages/plugins/github/src/__tests__/github-api.app-works.spec.ts(Vitest, mocked Octokit) — topics mapping, multi-file commit with non-fast-forward code, file URL anchor; andpackages/agent/src/facades/__tests__/git.facade.app-works.spec.ts— the unsupported paths. Done when:pnpm --filter @ever-works/github-plugin testandpnpm --filter @ever-works/agent test -- git.facade.app-workspass. -
T23 (parallel). Catalog sanitizer and managed availability. Create
packages/agent/src/apps-catalog/apps-catalog.mapper.tsandpackages/agent/src/apps-catalog/apps-catalog.types.ts— every rule of catalog.md §3,isVerified, and the puremanagedHostingAvailability(entry, registry, tier, match?)of plan §2.4: drops a row whose license classifiesred(R-3); stripsmanagedHosting.upstreamAgreementfrom non-amber rows; reasons in orderlicenseNotGreen,upstreamAgreementMissing,entryDisallows,blueprintNotVerified,managedTierDisabled, with the tier state passed in — never read fromEVER_WORKS_APPS_MANAGED_ENABLED(R-5). Test:packages/agent/src/apps-catalog/__tests__/apps-catalog.mapper.spec.ts— one fixture per rule, a bad row dropped while the rest survive (ACC-03-17); expired evidence unverified (ACC-03-20); each availability reason from its fixture (ACC-03-21); aredrow dropped (ACC-03-46); amber with and without an agreement, red and unknown (ACC-03-47);{ open: false }⇒managedTierDisabledand{ open: true, scope: 'verified-blueprints' }+ unverified ⇒blueprintNotVerified(ACC-03-48); an explicit match for an unlisted repository neveravailable(ACC-03-44); the purity guard — the same inputs give the same verdict withEVER_WORKS_APPS_MANAGED_ENABLEDset to both values in turn and with a throw-on-touch spy standing in forAPPS_TIER_POLICY, provingmanagedHostingAvailabilityand the mapper never reach for a policy or an environment of their own (plan §2.4). Done when: the mapper file imports nothing fromprocess.env, neithermanagedHostingAvailabilitynor the mapper callsAppsTierPolicy(the tier argument is the only way tier state enters), and every reason has a named case. -
T24.
AppsCatalogService. Createpackages/agent/src/apps-catalog/apps-catalog.service.ts,packages/agent/src/apps-catalog/apps-catalog.module.ts,packages/agent/src/apps-catalog/index.tsper plan §2.4 (tokenless → authenticated, byte-counted bodies, 1 h / 30 s cache, last-good registry for 7 days, mutable-ref warning,getDetailREADME ≤ 64 KiB and spec summary at the pinned sha,refresh()); tier state read fromAPPS_TIER_POLICY— imported frompackages/agent/src/app-runtime/ports.ts(APW-06 T3, merged before this phase); if that file does not exist yet, create it from CONTRACTS §3 verbatim and let APW-06 T3 modify it — (isOpen(),managedScope(),@Optional(), unbound ⇒ closed — R-5). Modifypackages/agent/package.json—./apps-catalogexport. Test:packages/agent/src/apps-catalog/__tests__/apps-catalog.service.spec.ts— both read paths, TTLs, size guards; an unreachable catalog yields an emptyavailable: falseresult and no refetch within 30 s (ACC-03-16); a fetch/facade spy failing the test on any request whose repository is not the catalog repository or anever-works/*Blueprint repository (ACC-03-18); an unbound port ⇒managedTierDisabled, a bound open port withEVER_WORKS_APPS_MANAGED_ENABLEDunset still reports tier-driven availability (ACC-03-48). Done when:pnpm --filter @ever-works/agent test -- apps-catalog.servicepasses. -
T25. Public catalog API and web client. Create
apps/api/src/apps-catalog/apps-catalog.controller.tsandapps/api/src/apps-catalog/apps-catalog.module.ts— the three public routes of plan §4.1 (licensesdeclared before:id),@Throttle120/min,Cache-Control: public, max-age=300. Modifyapps/api/src/api.module.ts— importAppsCatalogModule. Createapps/web/src/lib/api/apps-catalog.ts,apps/web/src/lib/api/apps-catalog.server.ts,apps/web/src/app/api/apps-catalog/route.ts(mirrorsapps/web/src/app/api/work-templates/route.ts). Test:apps/api/src/apps-catalog/apps-catalog.controller.spec.ts— filters each narrow and searchcafinds Cal.diy whilecis ignored (ACC-03-19),limitcapped at 100, route order, unavailable ⇒ 200available: false(ACC-03-16), a red fixture row absent from list and detail (ACC-03-46). Done when:GET /api/apps-cataloganswers without authentication with the cache header. -
T26. Blueprint resolver. Status (2026-09-25): explicit (FR-81) and probe (FR-43) paths done in
AppBlueprintResolverService(781f9a2e5): ≤ 3 reads, a 1 h / 10 min in-process cache (500 entries, oldest evicted; it replaces plan §2.5'sCACHE_MANAGERkey becauseAppWorksModulehas no cache module), SSRF-contained toever-works/<[a-z0-9-]>and.works/works.yml(APP_BLUEPRINT_SPEC_PATH— the only file the resolver reads). Acceptance also requires a public repository, anever-works/fullNameafter redirects, the topicever-works-app-blueprint, rootkind: appandspec.blueprint.repoequal to the repository. Credential: the GitHub App installation onever-works, elseEVER_WORKS_APPS_CATALOG_TOKEN, elseGITHUB_TOKEN. A match is held behind the apply gate until T28 bindsAPP_BLUEPRINT_APPLY_SERVICEin the packages/agentAppWorksModulegraph. Open: manifest lookup, aliases and rename re-lookup, fork network, ref constraints. Createpackages/agent/src/apps-catalog/app-blueprint-resolver.service.tsper plan §2.5 (index of repos and aliases,getRepositoryrename; fork networks matched by the rootsourcerepository and thenparent; explicitblueprintIdpath —source: explicit, no ref check for an unlisted repository, never verified; probe with ≤ 3 reads andblueprint-mode validation; ref constraints withsemver; 1 h / 10 min caches), reusing the parser inpackages/agent/src/works/repository-work-source.ts. Createpackages/agent/src/apps-catalog/app-source-catalog.adapter.ts— binds APW-01'sAPP_SOURCE_CATALOG_PORT(matchBlueprint({ owner, repo, blueprintId? })→ resolver,nullonnone,refMismatchor an unconfirmed fork match;classifyLicense→ the registry,unknownuntil P3 lands), and maps the resolver's match source onto the port'smatchSource, carriesdisplayName(FR-63's default Work name, APW-01 T13 consumes it) and the entry'spromptsdescriptors (names and descriptions only, never a value — APW-01 FR-55), inpackages/agent/src/apps-catalog/apps-catalog.module.ts. The port is APW-01 T11's file (packages/agent/src/app-works/app-source-catalog.port.ts, an own PR that merges first); this task binds it and declares no port of its own. Ifpackages/agent/src/app-runtime/ports.tsis absent, create it from CONTRACTS §3 verbatim (see "How to use"). Test:packages/agent/src/apps-catalog/__tests__/app-blueprint-resolver.spec.ts—calcom/cal.comandCALCOM/CAL.DIYresolve tocal(ACC-03-23); a fork of a listed upstream needs confirmation and an excluded tag gives the ref reason (ACC-03-24); the probe finds a topic-carrying template as Unlisted with ≤ 3 reads (ACC-03-25); an explicit id for a per-run generated repository resolves withsource: explicitand an unknown id givesblueprintNotFound(ACC-03-44); a fork of a fork resolves through the rootsource(ACC-03-45).packages/agent/src/apps-catalog/__tests__/app-source-catalog.adapter.spec.ts— eachnullcase,blueprintIdpassed through, a throwing resolver mapped tonull(ACC-03-24). Done when: APW-01's inspect endpoint returns a match through the port with no APW-01 code change, the adapter spec satisfies APW-01'sAppSourceCatalogPortinterface for everynullcase and a match, and resolving writes no Activity. -
T27 (parallel). Three-way spec merge. Create
packages/agent/src/apps-catalog/app-spec-merge.ts— key-wise merge, named-array merge byname, conflict list,sourcefrom ours. Test:packages/agent/src/apps-catalog/__tests__/app-spec-merge.spec.ts— user change kept, both sides changed ⇒ conflict, removed-by-Blueprint env entry kept when user edited it (ACC-03-29). Done when: the merge is pure (no I/O) andsourcenever comes fromtheirs. -
T28. Apply and upgrade job. Create
packages/agent/src/apps-catalog/app-blueprint-apply.service.ts,packages/agent/src/tasks/app-blueprint-apply.types.ts,packages/agent/src/tasks/app-blueprint-apply-dispatcher.ts(APP_BLUEPRINT_APPLY_DISPATCHER),packages/tasks/src/tasks/trigger/app-blueprint-apply.task.ts. Modifypackages/agent/src/tasks/app-works-jobs.ts(handler),packages/agent/src/tasks/index.ts,packages/agent/src/tasks/_tasks-symbols.ts,packages/agent/src/tasks/job-runtime.providers.ts,packages/tasks/src/tasks/trigger/index.ts. Behaviour: plan §2.5 steps 1–6 and the upgrade branch — R-4: a fresh fork or private copy (file absent orsource-only) getssourcefromWork.sourceRepository+ the Blueprint spec in onecommitFilescommit (no clone); Link and everything else get a pull request and never a push; after apply, request spec and license evaluation. Activityapp.blueprint.applied/app.blueprint.apply_failedwithactionType: APP_BLUEPRINT(R-2). Theapp.blueprint.matchedrecord is T53. Three bindings this task must honour: - Resolve against the right repository (G05).requestresolves the Blueprint againstsourceRepository.upstreamfor relationsforkandprivate-copy, and against the Work Repository forlink— never against the fork itself.userIdis optional with a documented fallback: the Work owner for a system-triggered apply. The match source and any fork confirmation APW-01 persisted (sourceRepository.blueprintMatchSource) are reused, so a ready-handler apply never re-asks for a confirmation the member already gave. - Compose a spec that validates for every relation (GAP-02). Before validation, adapt relation-dependent blocks: when the relation islink, dropupstreamSyncfrom the Blueprint's spec and forceupstreamPullRequests.enabled: false, because schema.md R13 makessource.relation: linkwithupstreamSyncan error (upstream_sync_requires_upstream). The dropped keys are listed in the pull request body and in the Activity details — nothing is silently discarded, and the Blueprint repository's own file is never edited. - Report the outcome (APW-01 FR-29b/FR-29a). After persistingblueprintApplyRef, call APW-02'sAPP_SOURCE_APPLY_REPORTER.recordSourceApplied(workId, result)@Optional()with{kind:'commit', sha},{kind:'pull_request', number, url}or{kind:'failed', reason}; a missing port or a throw is logged by code and never fatal. The Blueprint's trademark display name (FR-63) is applied here:WorkAppSpecState.displayNamefrom the entry'sdisplayName, andworks.nameis set to it only while the Work still carries the creation default (APW-01 T13's name), so a member who renamed the Work is never overridden. Upgrade mode makes no reporter call and no rename. Test:packages/agent/src/apps-catalog/__tests__/app-blueprint-apply.spec.ts— a fresh Fork App Work gets exactly one commit holding spec + add-only overlays and a Link App Work gets a PR with no push (ACC-03-26); a.github/workflows/x.ymloverlay refused and an existing file never overwritten (ACC-03-27); a missing pinned commit refuses and writes nothing (ACC-03-28); upgrade PR keeps a user field, lists a conflict and is updated by a newer version (ACC-03-29); an explicit Blueprint applied to a generated repository (ACC-03-44); Link plus a Blueprint whose spec carriesupstreamSynccomposes a valid spec, drops that key and forcesupstreamPullRequests.enabled: false, and lists what it dropped (ACC-03-53); a fork App Work resolves against its upstream and not against the fork, reusing the persisted match source, anduserIdomitted falls back to the Work owner (ACC-03-54); the reporter receivescommit,pull_requestandfailedin their three cases and the upgrade mode makes no reporter call (ACC-03-55); the trademark display name lands onWorkAppSpecState.displayNameand onworks.namewhile the name is still the creation default, and a renamed Work keeps its name (ACC-03-37). Done when:pnpm --filter @ever-works/agent test -- app-blueprint-applypasses and the dispatcher symbol is inDISPATCHER_SYMBOLS. -
T29. Apply, upgrade and dismiss endpoints. Modify
apps/api/src/works/work-app-spec.controller.tsandapps/api/src/works/dto/app-spec.dto.ts—POST …/app-spec/blueprint,…/blueprint/upgrade,…/blueprint/dismisswith the plan §4.2 codes and 5/hour throttles. Test: extendapps/api/src/works/work-app-spec.controller.spec.ts—blueprintNotFound,forkMatchNeedsConfirmation,applyInProgress,noUpgrade; a viewer gets no upgrade action and another account's Work is 404 (ACC-03-41). Done when: the three routes are in the OpenAPI document and return202/200without awaiting a job. -
T30. Hourly catalog refresh. Create
packages/tasks/src/tasks/trigger/apps-catalog-refresh.task.ts(cron23 * * * *, pinned equal toAPPS_CATALOG_REFRESH_CRON) andapps/api/src/apps-catalog/apps-catalog-refresh-cron.service.ts(fallback when Trigger.dev is not the runtime, locked likeapps/api/src/skills/skill-readiness-sweep-cron.service.ts); upgrade fan-out ≤ 500 per run emittingapp.blueprint.upgrade_availableonce per version. Modifypackages/tasks/src/tasks/trigger/index.ts— register the scheduled task. Test:packages/tasks/src/__tests__/apps-catalog-refresh.task.spec.tsandapps/api/src/apps-catalog/apps-catalog-refresh-cron.service.spec.ts— cron string, the 500 cap per run (ACC-03-22, upgrade half), once-per-version, one failing Work does not abort the run. Done when: both specs pass and the cron constant is asserted equal in both files. -
T31. Blueprint card and upgrade dialog. Create
apps/web/src/components/works/detail/settings/app-spec/AppBlueprintCard.tsxandapps/web/src/components/works/detail/settings/app-spec/AppBlueprintUpgradeDialog.tsx(apply and upgrade variants). Modifyapps/web/src/components/works/detail/settings/app-spec/AppSpecPageClient.tsx(fill the slot) andapps/web/src/app/actions/dashboard/app-spec.ts—applyBlueprintAction,upgradeBlueprintAction,dismissBlueprintUpgradeAction. Test:apps/web/src/components/works/detail/settings/app-spec/AppBlueprintCard.unit.spec.tsx— chips (incl.Chosen for this repositoryfor an explicit match), breaking notice, pending PR state, dismiss hides only that version (ACC-03-29). Done when:pnpm --filter ever-works-web test -- AppBlueprintCardpasses. -
T32 (parallel). Apps catalog browser. Create
apps/web/src/components/apps-catalog/AppsCatalogBrowser.tsx,apps/web/src/components/apps-catalog/AppsCatalogCard.tsx,apps/web/src/components/apps-catalog/AppsCatalogDetailsDrawer.tsxwith the{ onSelect, initialQuery? }contract of plan §5.1. Test:apps/web/src/components/apps-catalog/AppsCatalogBrowser.unit.spec.tsx,apps/web/src/components/apps-catalog/AppsCatalogCard.unit.spec.tsx— 300 ms debounce, 2-character minimum (ACC-03-19), category chips, placeholder not selectable, three distinct empty states,/focus, arrow-key roving focus, icon rendered with<img>only, theupstreamAgreementMissingtooltip copy. Done when: APW-01 can mount it with no change to the component. -
T33. The
ever-works/templatescatalog repository (outside this monorepo). Status (2026-09-26): the repository is a pure listing (its PRs #1542a96cand #28437f29, 2026-09-25):manifest.json(one row per-templaterepository, website and app alike — the app rowscal→ever-works/cal-template(metadata-only) andumami→ever-works/umami-template, bothplaceholder),schema/templates-manifest.schema.json,schema/app-spec.schema.json,licenses.yml, and a validator (tools/validate-specs.mjs, run by itsvalidate.ymlworkflow on push tomain, on pull requests and weekly) that fetches each app row's own.works/works.ymlfrom its template repository and validates it. It holds no per-template folders and no copy of any App spec; the spec lives only in the template repository's.works/works.yml, which is the only file the platform's resolver reads (APP_BLUEPRINT_SPEC_PATH). The fixture Blueprintever-works/app-fixture-hello-templatestays private and is not listed.CONTRIBUTING.mdandREADME.mdexist; the rest of this task (checks C1–C12 in itsvalidate.mjsscript,release.yml,verify-expiry.yml,CODEOWNERS, as below) is still the target. Create inever-works/templates— the repository that exists (2026-09-17); the earlier drafts called itever-works/apps, andEVER_WORKS_APPS_CATALOG_REPOaccepts either value, so the older name keeps working and nothing is renamed away —manifest.json(schemaVersion: 1, emptyapps),licenses.yml(legal-review draft per catalog.md §4, classes fixed per R-3),schema/manifest.schema.json(incl.managedHosting.upstreamAgreement),schema/licenses.schema.json,schema/app-spec.schema.json(copy of T8's output),scripts/validate.mjs(checks C1–C12, with C5 failingredand C6 the upstream-agreement rule; its C4 leg runs T57's published validator package —@ever-works/contractsat the version pinned in this repository'spackage.json— and its C12 leg compares the vendored schema against that same package's committed schema, so nothing here re-implements the platform's rules),.github/workflows/validate.yml(blueprint-draft mode for.works/works.yml, data-repository mode with a stubsourceforprofiles/*.works.yml),.github/workflows/release.yml(tagvX.Y.Zand print the commit sha the catalog pull request pins intosha:— never stampblueprint:into the file: blueprint mode forbids it),.github/workflows/verify-expiry.yml,.github/CODEOWNERS(legal reviewers ownlicenses.ymlandmanagedHosting.upstreamAgreementchanges),CONTRIBUTING.md,README.md. Test:scripts/__tests__/validate.test.mjsin that repository (node --test scripts/__tests__) — one failing fixture per check C1–C12, including aredentry (C5) and an amberallowed: trueentry without an agreement (C6), and the listed Blueprint repositories' own.works/works.ymlpassing the C4 leg (the APW-13 drafts were the source until the live repositories replaced them, 2026-09-25). Done when: the repository is public,validate.ymlis required onmain, and the platform reads an empty catalog asavailable: truewith zero entries. -
T34. P2 i18n. Modify
apps/web/messages/en.json—dashboard.workCreation.appsCatalog(incl.managedReason.upstreamAgreementMissing) anddashboard.workDetail.settings.appSpec.blueprint(incl.chipChosen) keys of plan §8, plus the 20 sibling locale files underapps/web/messages/. Modifyapps/web/src/components/works/detail/settings/app-spec/app-spec-messages.unit.spec.ts— add the two namespaces and the keys the T31/T32 components read. Test:pnpm --filter ever-works-web test -- app-spec-messages(ACC-03-43, P2 surfaces). Done when: the spec passes with the P2 namespaces listed. -
T35. P2 e2e. Create
apps/web/e2e/flow-apps-catalog-browse.spec.ts(stubbed catalog, mounted in a test harness page until APW-01's form exists) andapps/web/e2e/flow-app-blueprint-upgrade.spec.ts. Test:pnpm --filter ever-works-web test:e2e -- flow-apps-catalog-browse flow-app-blueprint-upgrade— search, category, badges, details drawer, placeholder not selectable (ACC-03-19); unavailable state (ACC-03-16); notice → dialog → pending PR, dismiss hides only that version (ACC-03-29). Done when: both pass andapps/web/e2e/flow-templates-catalog-pagination.spec.tsandapps/web/e2e/flow-website-template-catalog.spec.tspass unchanged. -
T36 (parallel). P2 docs. Create
docs/api/apps-catalog.md— the three public routes, query parameters, response shape, caching, availability reasons (incl.upstreamAgreementMissing), examples. Modifydocs/api/works.md— apply, upgrade, dismiss; the explicitblueprintIdon create. Modifydocs/environment-variables.md—EVER_WORKS_APPS_CATALOG_REPO,EVER_WORKS_APPS_CATALOG_REF(pin a sha or tag in production),EVER_WORKS_APPS_CATALOG_TOKEN. Test:pnpm --filter ever-works-docs build— no broken-link warning. Done when: the docs build is green and the three variables are listed. -
T37. P2 ship gate. Modify
docs/specs/features/app-works/APW-03-app-spec-and-catalog/tasks.md(tick T22–T36, T53) and the APW-03 row ofdocs/specs/features/app-works/TRACKER.md. Test:pnpm format:check && pnpm lint && pnpm type-check && pnpm test && pnpm build. Done when: all five commands are green on the P2 merge commit.
Phase P3 — License gate
Delivers spec FR-53…FR-65.
-
T38. Recursive repository listing. Modify
packages/plugin/src/contracts/capabilities/git-provider.interface.ts—getRepositoryTree?per plan §7;packages/plugins/github/src/github-api.service.ts(git.getTreerecursive=1,truncated,maxEntries),packages/plugins/github/src/github.plugin.ts,packages/agent/src/facades/git.facade.ts. Test: extendpackages/plugins/github/src/__tests__/github-api.app-works.spec.ts— truncation flag, entry cap. Done when:pnpm --filter @ever-works/github-plugin testpasses and a provider without the method makes the facade returnnull. -
T39 (parallel). SPDX expressions and classification. Create
packages/agent/src/app-license/spdx-expression.ts(OR, AND, WITH, parentheses,LicenseRef-*; ≤ 200 chars) andpackages/agent/src/app-license/license-classify.ts(best/worst/exception rules, obligations union, mixed and unknown handling). Test:packages/agent/src/app-license/__tests__/spdx-expression.spec.ts,packages/agent/src/app-license/__tests__/license-classify.spec.ts— MIT, Apache-2.0, AGPL green, BUSL-1.1 amber, no license unknown (ACC-03-30);MIT OR BUSL-1.1green andMIT AND BUSL-1.1amber (ACC-03-31). Done when: both modules are pure andpnpm --filter @ever-works/agent test -- spdx-expression license-classifypasses. -
T40 (parallel). Registry, snapshot and reference texts. Create
packages/agent/src/app-license/license-registry.ts(validation per catalog.md §4 incl. the fixed R-3classes, source preference live → last good → snapshot),packages/agent/src/app-license/license-registry.snapshot.yml, andpackages/agent/src/app-license/texts/<spdx>.txtfor every identifier in the snapshot. Test:packages/agent/src/app-license/__tests__/license-registry.spec.ts— every catalog.md §4 rule; a snapshot-sourced verdict carriesmanagedHosting: falseafter 8 days without a live read (ACC-03-38); a changedtextunder an unchangedtextIdis rejected; a registry that setsred.catalog: trueor loosens a class is rejected for the last good copy (ACC-03-46). Done when: every identifier in the snapshot has a text file (asserted by the spec). -
T41. Detection and header scanning. Create
packages/agent/src/app-license/license-detect.ts(root files, four package manifests, normalised word-bigram Dice ≥ 0.90, aliases, mixed directories at depth ≤ 4, nested license files, evidence ≤ 20) andpackages/agent/src/app-license/license-headers.ts(scanLicenseHeaders, first 2 KiB per file). Test:packages/agent/src/app-license/__tests__/license-detect.spec.ts— similarity 0.89 vs 0.90, depth 4 vs 5, anee/directory makes the repository mixed and at least amber with the path in evidence (ACC-03-32), ≤ 12 reads;packages/agent/src/app-license/__tests__/license-headers.spec.ts. Done when: both specs pass and detection performs no write. -
T42.
AppLicenseService, dispatcher and job. Createpackages/agent/src/app-license/app-license.service.ts(request,evaluate,getHostingEligibility,attest,previewUpstream,recordEvidence),packages/agent/src/app-license/app-license.module.ts,packages/agent/src/app-license/index.ts;packages/agent/src/tasks/app-license-evaluate.types.ts,packages/agent/src/tasks/app-license-evaluate-dispatcher.ts(APP_LICENSE_EVALUATE_DISPATCHER),packages/tasks/src/tasks/trigger/app-license-evaluate.task.ts. Modifypackages/agent/package.json(./app-license),packages/agent/src/tasks/app-works-jobs.ts(handler),packages/agent/src/tasks/index.ts,packages/agent/src/tasks/_tasks-symbols.ts,packages/agent/src/tasks/job-runtime.providers.ts,packages/tasks/src/tasks/trigger/index.ts. Eligibility per R-3 (plan §2.6): Your cluster after the owner's attestation for amber, red and unknown; Ever Works Apps for green, and for amber only with a recorded upstream agreement; red and unknown never. Source offer required only when the obligation applies and the relation islinkor the Work Repository is ahead of upstream (plan §2.6, same condition as APW-06 FR-44). The four settled points of plan §2.6 are written into the code:getHostingEligibility(workId, opts?: { commitSha?: string }), a nullaheadBycounts as required, the public check isGitRepository.visibility === 'public'(falling back toisPrivate === falsefromgetRepository, withinternalor a failed lookup counting as not public), andsourceOffer.urlis built withgetFileWebUrl(dataOwner, dataRepo, commitSha, '')— an empty path returns the tree URL at that commit (https://github.com/<o>/<r>/tree/<sha>, nolineAnchor) and isnullwhen the call carried nocommitSha. Activityapp.license.classified/changed/attestation_required/attestedwithactionType: APP_LICENSE(R-2);AppLicenseChangedEventon change. Test:packages/agent/src/app-license/__tests__/app-license.service.spec.ts— an amber Work refused for Ever Works Apps bygetHostingEligibility(ACC-03-33); manager refused, owner recorded with text hash and commit, a new text id clears it (ACC-03-34); MIT → BUSL-1.1 notifies, keeps running Deployments and requires attestation next (ACC-03-35); private AGPL withoutsourceOfferUrl⇒sourceOfferMissing(ACC-03-36); 8-day registry outage never managed-eligible (ACC-03-38); amber with an agreement eligible, red on Your cluster only after attestation and never managed (ACC-03-47); the eligibility line for a Work whose Blueprint carries a trademark notice names the notice the Blueprint set (WorkAppSpecState.displayName, written by T28 — the license service never creates or renames a Work, so ACC-03-37's Work-name assertion lives inpackages/agent/src/apps-catalog/__tests__/app-blueprint-apply.spec.tsand in APW-01's create spec). Done when:pnpm --filter @ever-works/agent test -- app-license.servicepasses and the dispatcher symbol is inDISPATCHER_SYMBOLS. -
T43. Wire the triggers. Modify
packages/agent/src/app-spec/app-spec.service.ts— after an effective-commit change, request license evaluation whengetCompareDiff(licenseCommitSha, head)touches a license-relevant path (orlicenseCommitShais null). Modifyapps/api/src/apps-catalog/apps-catalog-refresh-cron.service.tsandpackages/tasks/src/tasks/trigger/apps-catalog-refresh.task.ts— registry-change fan-out ≤ 500 per run. Test: extendpackages/agent/src/app-spec/__tests__/app-spec.service.spec.ts(license request on a relevant diff only),packages/tasks/src/__tests__/apps-catalog-refresh.task.spec.tsandapps/api/src/apps-catalog/apps-catalog-refresh-cron.service.spec.ts— a registry change re-classifies at most 500 Works per run and continues next run (ACC-03-22). Done when: the three specs pass. -
T44. Attestation endpoint. Modify
apps/api/src/works/work-app-spec.controller.tsandapps/api/src/works/dto/app-spec.dto.ts—POST /api/works/:id/app-license/attestwithWorkOwnershipService.ensureIsOwner,403 ownerOnly,422 attestationNotRequired,409 licenseChanged, 10/min throttle; include the license part in theGET app-specDTO. Test: extendapps/api/src/works/work-app-spec.controller.spec.ts— manager403 ownerOnly, owner200(ACC-03-34); viewer and cross-account Work (ACC-03-41). Done when: the route is in the OpenAPI document with its four error codes. -
T45. License card and attestation dialog. Create
apps/web/src/components/works/detail/settings/app-spec/AppLicenseCard.tsxandapps/web/src/components/works/detail/settings/app-spec/AppLicenseAttestDialog.tsx. Modifyapps/web/src/components/works/detail/settings/app-spec/AppSpecPageClient.tsx(fill the slot) andapps/web/src/app/actions/dashboard/app-spec.ts—attestLicenseAction. Test:apps/web/src/components/works/detail/settings/app-spec/AppLicenseCard.unit.spec.tsx,apps/web/src/components/works/detail/settings/app-spec/AppLicenseAttestDialog.unit.spec.tsx— every eligibility line (incl.allowed under an agreement with the app's authors), mixed and incomplete evidence, source-offer variants, checkbox gating, owner-only copy, disclaimer present (ACC-03-34). Done when:pnpm --filter ever-works-web test -- AppLicenseCard AppLicenseAttestDialogpasses. -
T46. P3 i18n. Modify
apps/web/messages/en.json—dashboard.workDetail.settings.appSpec.licensekeys of plan §8 (incl.managedAllowedAgreement); the 20 sibling locale files underapps/web/messages/. Modifyapps/web/src/components/works/detail/settings/app-spec/app-spec-messages.unit.spec.ts— add the license namespace and the keys T45 reads. Test:pnpm --filter ever-works-web test -- app-spec-messages(ACC-03-43, P3 surfaces — the full criterion). Done when: the spec lists every namespace of the App spec tab, catalog browser, Blueprint card, License card and attestation dialog and passes in all 21 locales. -
T47. P3 e2e. Create
apps/web/e2e/flow-app-license-attest.spec.tsandapps/web/e2e/flow-app-works-a11y.spec.ts. Test:pnpm --filter ever-works-web test:e2e -- flow-app-license-attest flow-app-works-a11y— amber Work: owner confirms, manager sees owner-only copy, eligibility lines update (ACC-03-34, ACC-03-41); axe over the catalog browser, problems list and attestation dialog plus the spec §6.4 keyboard paths (ACC-03-42). Done when: both pass with no new axe violations against the recorded baseline. -
T48. P3 ship gate. Modify
docs/specs/features/app-works/APW-03-app-spec-and-catalog/tasks.md(tick T38–T47) and the APW-03 row ofdocs/specs/features/app-works/TRACKER.md. Test:pnpm format:check && pnpm lint && pnpm type-check && pnpm test && pnpm build. Done when: all five commands are green on the P3 merge commit.
Cross-phase closing tasks
-
T49. Telemetry. Create
packages/agent/src/app-spec/app-spec-telemetry.port.ts—AppSpecTelemetryEvent, the per-event property allow-list andAPP_SPEC_TELEMETRY_SINKper plan §9.1:track(event, props, distinctId), fire-and-forget, never throwing; a property outside the allow-list is dropped at the port boundary; unbound ⇒ counted and dropped.packages/agentgains no dependency: it does not depend on@ever-works/monitoringtoday (onlyapps/apidoes), and the agent-package precedent is a narrow injected client port (packages/agent/src/services/knowledge-base-reconcile.service.ts:55-67). Createpackages/monitoring/src/posthog/app-spec-events.ts—emitAppSpecEvent(client, distinctId, event)for the eight events of plan §9.1 with a forbidden-property list (file content, env values, search text, user-typed repository names), modelled onpackages/monitoring/src/posthog/kb-events.ts. Modifypackages/monitoring/src/posthog/index.ts— export it. Modifyapps/api/src/app-works/app-works.module.ts(created by APW-02 T27) — bindAPP_SPEC_TELEMETRY_SINKto a sink that callsemitAppSpecEventwith the API's PostHog client, exactly asFunnelAnalyticsSinkis bound forzero-friction-funnel.service.ts. Modifypackages/agent/src/app-spec/app-spec.service.ts,packages/agent/src/apps-catalog/apps-catalog.service.ts,packages/agent/src/apps-catalog/app-blueprint-apply.service.ts,packages/agent/src/app-license/app-license.service.ts— inject the port@Optional()and emit through it; no service imports the monitoring package. Test:packages/monitoring/src/posthog/__tests__/app-spec-events.spec.ts— no payload contains file content, an env value, a search query or an upstream repository name typed by a user; forbidden keys throw; andpackages/agent/src/app-spec/__tests__/app-spec-telemetry.spec.ts— the four services emit through the bound fake, nothing is thrown with the port unbound, and a property outside the allow-list never reaches the sink. Done when:pnpm --filter @ever-works/monitoring test -- app-spec-eventspasses,git grep -n "monitoring" packages/agent/package.jsonstill matches nothing, and the four services' specs are green with the sink unbound. -
T50. User documentation. Create
docs/features/app-blueprints.mdfromuser-doc-draft.md, updated to what shipped. Modifyapps/docs/sidebarsPlatform.ts— list it next tofeatures/work-blueprints. Modifydocs/features/work-blueprints.md— add a row for the Apps catalog to "Three catalogs, one picker" and a link. Test:pnpm --filter ever-works-docs build— no broken-link warning. Done when: the page is reachable from the platform docs sidebar. -
T51. Acceptance wiring. Modify
docs/specs/features/app-works/ACCEPTANCE.md— map ACC-03-01…ACC-03-49 to the test files that prove them. Modifydocs/specs/features/app-works/TRACKER.md— APW-03 spec and implementation status. Test:rg -c "ACC-03-(0[1-9]|[1-4][0-9])" docs/specs/features/app-works/ACCEPTANCE.mdcounts every id with a named file and noTBDcell remains for APW-03. Done when: ACCEPTANCE §3 APW-03 has 49 rows, each with a test file. -
T52. Close out. Modify
docs/specs/features/app-works/APW-03-app-spec-and-catalog/spec.md,…/plan.mdand this file — setImplemented/Done;docs/specs/features/app-works/CONTRACTS.mdonly if a shipped name differs. Test: walk every gate in plan §12 against the merged code and diff CONTRACTS §1–§6 APW-03 names againstpackages/contracts/src/apps/. Done when: statuses readImplemented/Doneand CONTRACTS.md matches every shipped name. -
T53 (P2, lands with T28). Record
app.blueprint.matched. Modifypackages/agent/src/apps-catalog/app-blueprint-apply.service.ts—request(workId, blueprintId, { userId, matchSource, confirmForkMatch })per plan §2.5 step 0: resolve (explicit id honoured for any repository, spec FR-81), persist the match, thenWorkAppSpecStateRepository.markBlueprintMatchedand, only when it returnstrue,ActivityLogService.log({ action: 'app.blueprint.matched', actionType: APP_BLUEPRINT, details: { blueprintId, version, matchSource } })before dispatchingAPP_BLUEPRINT_APPLY_DISPATCHER. Modifypackages/agent/src/database/repositories/work-app-spec-state.repository.ts—markBlueprintMatched(if not already added in T11). Test:packages/agent/src/apps-catalog/__tests__/app-blueprint-matched.spec.ts— exactly oneapp.blueprint.matchedper Work + version for a manifest match, a catalog pick and an explicit id on a per-run generated repository; it precedesapp.blueprint.appliedandapp.blueprint.apply_failed; a retried request, a job retry and a re-dispatch record nothing new; details hold only id, version and match source; inspect (AppSourceCatalogAdapter.matchBlueprint) records nothing (ACC-03-44). Done when:pnpm --filter @ever-works/agent test -- app-blueprint-matchedpasses and the ACC-E2E-05 Activity order (app.blueprint.matched→app.blueprint.applied→app.spec.applied) holds in the service-level integration case. -
T54 (P1, lands with T9–T10). Classify new tables for workspace backup (R-25). Modify
packages/agent/src/account-transfer/backup/collectors/domain-specs.ts— append to theworksdomain:{ file: 'app-spec-states.jsonl', entity: 'WorkAppSpecState', scope: { by: 'parent', column: 'workId', from: 'workIds' } }. Modifypackages/agent/src/account-transfer/backup/redaction.ts— addheadSpecHash,effectiveSpecHashandlicenseRegistryHashtoBACKUP_BENIGN_COLUMNS, each with its reason (a sha256 of the canonical App spec, which holds no secret values, or of the public license registry). Nothing joinsBACKUP_DROPPED_ENTITIES;effectiveSpecandattestationexport as stored. Test: extendpackages/agent/src/account-transfer/backup/collectors/collectors.spec.ts—WorkAppSpecStateis referenced exactly once, inworks, scopedparentonworkIdfromworkIds, not dropped; anEntityBackupCollectorover theworksspec yields a fixture row with its three*Hashcolumns unchanged. Done when:pnpm --filter @ever-works/agent test -- collectors redactionis green — including the secret-shaped-column guard inredaction.spec.ts— and a backup of a workspace holding one App Work listsdata/works/app-spec-states.jsonl. -
T55 (P2 seam, own PRs — the interface-first split; no whole-phase loop). Three small PRs, in this order, each complete on its own and each merged before APW-01 P1: 1.
commitFiles?— the git-capability slice of T22:GitRepository.commitFiles?inpackages/plugin/src/contracts/capabilities/git-provider.interface.ts, its GitHub implementation inpackages/plugins/github/src/github-api.service.ts, the delegation inpackages/plugins/github/src/github.plugin.tsand thenull/unsupportedwrapper inpackages/agent/src/facades/git.facade.ts. T22 then adds onlytopics?andgetFileWebUrl?. 2. The catalog seam — T24's read paths and T26's adapter, sopackages/agent/src/app-works/gains its binding:AppSourceCatalogAdapterimplements APW-01 T11'sAppSourceCatalogPort(includingmatchSource,displayNameandprompts) inpackages/agent/src/apps-catalog/apps-catalog.module.ts. 3. The apply seam — T28 with T53 (the apply job and theapp.blueprint.matchedrecord), whichAppSourceInitializerService(APW-01 T15) calls. Create nothing beyond those three PRs' own files; the point of this task is the ordering, recorded once. Test: each PR runs its own task's spec (T22's, T24/T26's, T28/T53's) andpnpm --filter @ever-works/agent test -- app-specstays green. Done when: APW-01 P1 can be built against all three without any other APW-03 task having merged, andgit grep -n "APW-01 P1" docs/specs/features/app-works/APW-03-app-spec-and-catalog/tasks.mdfinds only this sentence. -
T56 (P1 and P2, lands with T13, T28 and T42). Job wiring: Trigger.dev bindings, worker RPC and where each event is emitted. The three jobs this epic dispatches follow the existing
MEMORY_FACT_EMBED_DISPATCHERpattern end to end; symbol files and a trigger task alone are not enough. Create (P1, with T13)packages/tasks/src/dispatchers/app-spec-evaluate.dispatcher.ts; Create (P2, with T28)packages/tasks/src/dispatchers/app-blueprint-apply.dispatcher.ts; Create (P3, with T42)packages/tasks/src/dispatchers/app-license-evaluate.dispatcher.ts. Modifypackages/tasks/src/trigger/trigger.service.ts(dispatchAppSpecEvaluate,dispatchAppBlueprintApply,dispatchAppLicenseEvaluate, mirroringdispatchMemoryFactEmbed, with the idempotency keys APW-03's payloads carry),packages/tasks/src/trigger/trigger-tenant-client.factory.ts(the three ids),packages/tasks/src/trigger/trigger.module.ts(provider + export, beside the existing dispatcher at:164),packages/tasks/src/index.ts(exports) andpackages/tasks/src/tasks/trigger/index.ts. Modifyapps/api/src/trigger/trigger-internal.controller.ts— add@Optional()AppSpecService,AppBlueprintApplyServiceandAppLicenseServiceparameters appended last, and theirremoteMapentries; Modifyapps/api/src/trigger/trigger-internal.module.ts— import the agentAppSpecModule,AppsCatalogModuleandAppLicenseModule; Modifypackages/tasks/src/trigger/worker/modules/trigger-internal.module.ts—APP_SPEC_SERVICE,APP_BLUEPRINT_APPLY_SERVICEandAPP_LICENSE_SERVICEthroughcreateRemoteProxy, exported, followingMemoryFactEmbedService(:431-433). The worker never evaluates. Every Trigger task calls its service through the RPC proxy, so the evaluation, the database writes and the in-process events all run in the API process — which is what makesAppSpecAppliedEventandAppLicenseChangedEventreach APW-05, APW-07 and APW-08, whose@OnEventlisteners live in API-side agent modules. The worker has its ownEventEmitterModule(trigger-plugins.module.ts:41), so an event emitted inside the worker would reach nobody; plan §6.1 states this and names the emitting process. Test: createpackages/tasks/src/dispatchers/__tests__/app-spec-evaluate.dispatcher.spec.ts(and the sibling specs for the other two) — the id is forwarded, anulldispatch runs the handler in-process; extendpackages/tasks/src/__tests__/trigger.service.spec.ts— three dispatchers, unconfigured ⇒null, idempotency-key shape; extendapps/api/src/trigger/trigger-internal.controller.spec.ts— the three names inremoteMapwith their method allow-lists; extendpackages/tasks/src/trigger/worker/modules/trigger-internal.module.spec.ts— the worker compiles with no database module and each of the three resolves to a remote proxy, and an event emitted by a service call reaches an API-side@OnEventlistener in the integration case (ACC-03-50). Done when: all four specs are green andgit grep -n "AppSpecAppliedEvent" packages/tasks/srcfinds no emit site. -
T57 (P2, lands with T33 and T8). The published validator artifact C4 and C12 run. Create
packages/contracts/src/apps/validator/— a publishable entry point re-exporting the pure validator (validateAppSpecDocument,validateAppSpecObject,APP_SPEC_ISSUE_CODES) frompackages/agent/src/works-config/schema/'s pure modules, plus the committed JSON Schema of T8. It lives inpackages/contractsbecause that package is not private, so Blueprint CI can install it; nothing is moved out ofpackages/agent— the agent package re-exports the same modules it does today. Modifypackages/contracts/package.json— a./apps/validatorsubpath export and thefilesentry; Create.github/workflows/publish-app-spec-validator.yml— publishes@ever-works/contractson a tag, withEVER_WORKS_APP_SPEC_VALIDATOR_VERSIONrecorded in the catalog repository'spackage.jsonpin. Test:packages/contracts/src/apps/__tests__/validator-entry.spec.ts— the entry validates schema.md §24.1–§24.4 and reports the same codes the agent package reports; a drift guard asserts the committed JSON Schema equals T8's generator output (ACC-03-51). Done when: a scratch install of the packed tarball validates a Blueprint draft and the catalog repository'spackage.jsonnames its exact version. -
T58 (P2, lands with T33). Validate every Blueprint draft in
blueprintmode. Note (2026-09-26): thecal-diy/andumami/drafts underAPW-13-golden-paths/blueprints/are being retired: the live repositoriesever-works/cal-template(formerlycal-diy-template) andever-works/umami-templateare the source, andever-works/templates' CI validates their own.works/works.yml(T33 status). Once they are gone this walk covers the drafts still present (the privateapp-fixture-hellofixture) and "all three drafts" below reads "every draft present". Createpackages/agent/src/works-config/schema/__tests__/blueprint-drafts.spec.ts— walksdocs/specs/features/app-works/APW-13-golden-paths/blueprints/*/.works/works.ymland validates each inblueprintmode, which schema.md §3's corrected row defines as allowing and expectingsourceandblueprint(the 2026-09-17 correction — the drafts declare both, and the apply job composes the realsourceitself), asserting zero errors; warnings are reported but not fatal (ACC-03-52). This is the same mode and the same rules catalog CI check C4 runs through T57's published artifact, so a draft that passes here cannot fail there. Modifydocs/specs/features/app-works/APW-03-app-spec-and-catalog/catalog.md§5 — reference this spec as the mechanism behind catalog check C4's blueprint-mode leg. Test:pnpm --filter @ever-works/agent test -- blueprint-drafts— every draft directory present underblueprints/is validated, and the spec fails loudly if the folder holds a draft it did not read. Done when: the spec is green for all three drafts, and the same three files still fail for the reasons that are real defects (a reservedEVER_WORKS_*env name, a look-aroundvalidate.pattern, a below-minimum memory request) whenever those are reintroduced — a contrast case keeps that honest. -
T59 (P2, lands with T8 and T33). A machine-checkable schema and a fixture corpus beside the prose. This file is normative prose; until T8's generator exists there is nothing a Blueprint draft, a profile draft or a CI job can be checked against, and drift has already happened once (a small validator found two invalid Blueprint drafts). The corpus below is additive documentation, committed with the spec, not a second source of truth: schema.md stays the reference and T8's generated schema stays the artifact the platform ships. Create
docs/specs/features/app-works/contracts/app-spec.schema.json— JSON Schema 2020-12 mirroring schema.md §1–§22: types, enums, bounds (minLength/maxLength/maximum/minimum),required,additionalProperties: falsewithpatternProperties'^x-'for extension keys. Structural only — the cross-field rules (schema.md §22) stay in code, and the file's header comment says so. Createdocs/specs/features/app-works/contracts/fixtures/app-spec/valid/*.yml— the three Blueprint drafts underAPW-13-golden-paths/blueprints/plus schema.md §24.1, §24.2 and §24.3 (see T58's note: the live repositories'.works/works.ymlreplace the retired Cal and Umami drafts). Createdocs/specs/features/app-works/contracts/fixtures/app-spec/invalid/<code>.yml— one file per structural issue code and one per rule in schema.md §22, each with a# expect: <code>header, so a failure names the file that must report it. Modifydocs/specs/features/app-works/APW-03-app-spec-and-catalog/schema.md§0 — state that the machine-checkable mirror of §1–§22 iscontracts/app-spec.schema.jsonwith its corpus, that it is checked in CI, and that where the two ever disagree the prose wins and the JSON Schema is corrected. Test: createpackages/agent/src/works-config/schema/__tests__/app-spec.corpus.spec.ts— T8's generated schema andvalidateAppSpecDocument/validateAppSpecObjectgive identical verdicts over every file in the corpus (eachvalid/*.yml⇒ no errors, eachinvalid/<code>.yml⇒ exactly the code in its header); the committedcontracts/app-spec.schema.jsonaccepts everyvalid/*.ymlunderajv2020-12 (Ajv2020from the rootnode_modules); and a drift guard fails when avalid/*.ymlis edited without the verdict being re-checked, so the corpus cannot rot silently. Done when: the corpus spec is green,npx ajv-style validation of the corpus throughcontracts/app-spec.schema.jsonagrees with the code validator, and the three Blueprint drafts are in the corpus asvalid/files (T58 keeps validating them where they live).
Definition of Done
- Every checkbox above is ticked.
pnpm format:check,pnpm lint,pnpm type-check,pnpm testandpnpm buildare green from the root.- Every pre-existing
works-configschema test and the three catalog e2e specs named in T19 and T35 pass unchanged — the additive-only proof. works.v2.schema.jsonandapp-spec.v1.schema.jsonmatch their generators; the copy inever-works/appsmatchesapp-spec.v1.schema.json.pnpm --filter ever-works-docs buildproduces no broken-link warnings.- Every ACC-03 box in spec §8 (ACC-03-01…ACC-03-49) has a named test above and has been walked against a running build.
- No catalog-service test observed a request to an upstream repository or a manifest link.
- No code path reads
EVER_WORKS_APPS_MANAGED_ENABLEDin this epic's modules, no App spec names a builder forauto, and no test file lives underapps/api/test/.