Task Breakdown: App Provisioner
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-04-app-provisioner
Spec: ./spec.md · Plan: ./plan.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; (new) marks a file this epic creates), 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 (T46+ were added by the program audit; T48+ by the APW-04 gap audit — each names its phase and the tasks it lands with).
- Phase boundaries are ship boundaries:
developmust be green and deployable at the end of each phase. - Cross-epic preconditions are listed per phase; do not implement another epic's owned names here (CONTRACTS.md). The
verification hooks — APW-05
startBuild({ verification }), APW-06AppRenderInput.purpose: 'verification', APW-07's ephemeralAppRuntimeEnvSourcemode — are accepted (Resolution R-10) and built by their owners; this epic consumes them. - Catalog tasks (T30–T32) land in
ever-works/agentsandever-works/skills, not in this monorepo. - Test commands: agent package
pnpm --filter @ever-works/agent test -- <pattern>(Jest); APIpnpm --filter ever-works-api test -- <pattern>(Jest); workerpnpm --filter @ever-works/trigger-tasks test -- <pattern>(Vitest); 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/(R-22). - Binding program resolutions (CONTRACTS.md §0):
R-1 contracts in
packages/contracts/src/apps/; R-2actionType: 'app_provision'; R-10 hooks accepted; R-13auto; R-17 safety rails; R-22 test locations.
Phase P0 — Prerequisites (inert until P1)
No user-visible change. Each task is independently shippable.
-
T1. Pipeline networking flag and sandbox session runner contract. Modify
packages/plugin/src/contracts/capabilities/pipeline-plugin.interface.ts— add optionalreadonly enforcesRuntimeNetworking?: booleanto the pipeline plugin contract with a doc comment ("true only when the plugin turnsruntimeEnvironment.networkingMode = 'limited'into an enforced sandbox policy"), and the optionalrunSandboxSession?(input, signal)method with theSandboxSessionInput/SandboxSessionResultshapes of plan §2.6 (additive: every existing pipeline is unaffected). Modifypackages/plugins/claude-managed-agent/src/claude-managed-agent.plugin.ts— declaretrueand implementrunSandboxSessionon the primitives the plugin already has (resolveManagedAgentSettings,ensureControlPlanewith the pre-resolved environment,buildSessionResources,createSessionwithbudgetUsdand an ephemeral agent + environment,sendUserMessage/waitForSessionIdle,extractAgentTranscript,toManagedSessionTokenUsage); arequires_actionidle event returnsfailedwithrequiresActioninstead of throwing, and the last assistant message comes back asfinalText. Test:packages/plugins/claude-managed-agent/src/claude-managed-agent.plugin.runtime-environment.spec.ts— assert the flag and that alimitedenvironment produces alimitedpolicy withallow_mcp_servers: false(ACC-04-05, unit half); a newclaude-managed-agent.plugin.run-sandbox-session.spec.ts(new) with a client double — the system prompt, the one tokenless repository mount and the budget are sent,requires_actionreturnsfailed/requiresAction, and usage maps to{ inputTokens, outputTokens, costUsd }. Done when: no other pipeline plugin declares the flag or implements the method (grep), and the plugin package builds. -
T2 (parallel). Repo-backed agent template instantiation. Modify
packages/agent/src/agents/agent-templates.service.ts— addcreateFromRepoTemplate(userId, slug, input, ownershipScope?)readingtemplates/<slug>/.works/agent.yml,SOUL.mdandskills.ymlatEVER_WORKS_AGENTS_REFwith the same tokenless-raw / App-installation fallback asapps/api/src/agents/agent-template-catalog.service.ts; validate the manifest's required keys; strip HTML; cap lengths; writeSOUL.mdviaAgentFileService.write; permissions all false; guardrailsrequire_approval. Only slugs inREPO_TEMPLATE_INSTANTIABLE_SLUGS = ['app-provisioner']are accepted (404 otherwise). Createpackages/agent/src/agents/repo-agent-template.reader.ts(new) — the fetch + parse helper (pure parse functions exported for tests). Test:packages/agent/src/agents/__tests__/agent-templates.repo-template.spec.ts(new) — allow-list, missing required key → refused, HTML stripped, SOUL written, existingcreateFromTemplateuntouched. Done when:packages/agent/src/agents/__tests__/agent-templates.service.spec.tspasses unchanged. -
T3 (parallel). Worker branch for provisioning Tasks (inert). Modify
packages/tasks/src/tasks/trigger/agent-task-execute.task.ts— beforeprovisionForRun, look up a provisioning row bytaskIdthrough a port (APP_PROVISIONING_LOOKUP_PORT, resolved@Optional()); when found, skip workspace provisioning, the L0 pre-check and the gate loop, and do not run the default post-runfinalizeRun— finalize is deferred to the job, which pushes and opens the pull request through the Task finalize variants of T16 (R-17); callAppProvisionSessionRunner.run(runId)(T48) instead ofrunner.execute(...)— the pre-resolvedruntimeEnvironmentand the row'sattachedReposreach the sandbox there and nowhere else — and on completion callnotify({ event: 'run-finished' }). There is no fallback torunner.executefor a provisioning Task. Createpackages/agent/src/app-provisioning/app-provisioning-lookup.port.ts(new) — interface + symbol. Test:packages/tasks/src/__tests__/agent-task-execute.provisioning-branch.spec.ts(new) — a Task with a row takes the branch and never calls the agentcommitToRepo/openPullRequesttools, and never callsAgentRunService.execute; a Task without a row runs the existing path byte-for-byte (golden assertions on the calls). Done when:packages/tasks/src/__tests__/agent-task-execute.task.spec.tspasses unchanged. -
T4. Sandbox isolation live spec (runnable root — R-22). Create
packages/plugins/claude-managed-agent/src/provision-sandbox-isolation.live.spec.ts(new) —describe.runIf(process.env.APW_E2E_LIVE === '1')(skipped in the PR lane); opens a real managed session through T1'srunSandboxSessionwith the plan §7.3 environment and one fixed model turn whose deterministic tool-use instruction probes the network (the managed-agents client exposes onlysendUserMessageandwaitForSessionIdle, both model turns, and no command-execution API): the platform API origin taken fromAPW_E2E_ALLOWED_BASE_URLS, an RFC1918 address,169.254.169.254and an unlisted public host must fail;github.comandregistry.npmjs.orgmust succeed; environment names andgit config --listare passed throughscanForSecretswith zero matches; a control case withnetworkingMode: 'unrestricted'must see the unlisted host succeed. Settings come from the plugin-config seam (resolveManagedAgentSettings), falling back toAPW_E2E_MANAGED_AGENT_API_KEY(ACCEPTANCE.md:120) when no plugin setting is present; with neither, the spec reports skipped, never failed. Test:APW_E2E_LIVE=1 pnpm --filter @ever-works/claude-managed-agent-plugin test -- provision-sandbox-isolation.liveon the nightly lane (ACC-04-05, ACC-04-06); withoutAPW_E2E_LIVEthe file reports skipped, never failed. Done when: the nightly lane runs it green and the recorded control run shows the unrestricted case reaching the unlisted host; no file for this check exists underapps/api/test/. The lane step itself is requested from APW-13 (its job list isinterlocks → fixture → umami → safety → cleanup → evidence) — until that step exists on the lane, the live half is recorded as not yet observed, never assumed. -
T5. P0 ship gate. Modify
docs/specs/features/app-works/APW-04-app-provisioner/tasks.md(tick T1–T4) and the APW-04 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 P0 merge commit.
Phase P1 — Provision, verify in the build runner, ask (Wave 1)
Delivers spec FR-1…FR-50, FR-56…FR-62 with the runner verification target; ACC-04-01…20, 22…29, 32…38.
Preconditions: APW-01 P1 (kind app, creation hook), APW-03 P1 (validateAppSpecDocument, validate endpoint,
packages/contracts/src/apps/index.ts), APW-05 P1 (IBuildPlugin with the verification option and
supportedStrategies), APW-07 P1 (AppRuntimeEnvSource ephemeral mode, ephemeral dependency containers for the
runner) — the R-10 hooks.
P1.1 — Contracts, entity, migration
-
T6. Contracts. Create
packages/contracts/src/apps/app-provisioning.ts(new) with the unions (incl.APP_PROVISIONING_PARK_REASONS,APP_PROVISIONING_DETECTION_SOURCESwithauto, question reasonsafety-rail), the singular type aliases,AppProvisioningAttempt,AppProvisioningStepView,AppProvisioningAttemptView,AppProvisioningQuestionView,AppProvisioningView,AppProvisioningSummaryView,AppProvisioningReadiness,AppProvisioningResponse,APP_PROVISION_LIMITS(incl.startDedupeMs: 10_000),APP_PROVISION_WRITABLE_PATHS,APP_PROVISION_PRESERVED_SPEC_FIELDS, and the four*_I18N_KEYmaps — exactly as plan §3.2. Createpackages/contracts/src/apps/app-provisioning-copy.ts(new) per T49. Modifypackages/contracts/src/apps/index.ts(created by APW-03 T1) —export * from './app-provisioning.js';andexport * from './app-provisioning-copy.js';(R-1: nosrc/app-works/folder). Test:packages/contracts/src/apps/__tests__/app-provisioning.spec.ts(new) — pins every union, the singular aliases, every key of every view interface (so T23 can assert key-set equality against the wire),publicRepositoryin the readiness keys,private-repositoryin the failure reasons, and every numeric limit includingstartDedupeMs(a limit cannot change without a deliberate edit). Done when:import { APP_PROVISION_LIMITS, AppProvisioningResponse } from '@ever-works/contracts'resolves inapps/apiandapps/web. -
T7.
WorkAppProvisioningentity. Createpackages/agent/src/entities/work-app-provisioning.entity.ts(new) per plan §3.1 (incl.parkedReason,parkedAt,questionReason,questionParams,lastRunOutput), dates viaPortableDateColumnfrompackages/agent/src/entities/_types.ts, scope columns without relations. Modifypackages/agent/src/entities/index.ts,packages/agent/src/database/_entity-names.ts,packages/agent/src/database/_entities-inventory.ts. Createpackages/agent/src/database/repositories/work-app-provisioning.repository.ts(new) —findActiveByWork,findByTaskId,claimLease,releaseLease,casAttemptsUsed(id, expected),countActiveForUser,countActiveForOrg,listExpiredTargets(limit),listStaleQuestions(limit),listQueued(limit),findRecentAgentId(userId, organizationId). Test:packages/agent/src/entities/__tests__/work-app-provisioning.entity.spec.ts(new) (index names, scope columns) andpackages/agent/src/database/repositories/__tests__/work-app-provisioning.repository.spec.ts(new) (lease CAS, attempts CAS, active partial-unique behaviour on SQLite so two concurrent inserts yield one row — ACC-04-03; a user's 4th active row is counted for queueing — ACC-04-25). Done when: the database drift specs pass without a magic-number edit. -
T8. Migration. Create
apps/api/src/migrations/1792040000000-CreateWorkAppProvisionings.ts(new) — table + six indexes, including thequestionReason/questionParams/lastRunOutputcolumns of plan §3.1, partial indexes on both Postgres and SQLite,down()dropping only whatup()created. Test:apps/api/src/migrations/__tests__/CreateWorkAppProvisionings.spec.ts(new) — no statement touches a pre-existing table;up()thendown()thenup()succeeds. Done when: a fresh database migrates; re-stamped above the newest migration ondevelopat merge time (1791240000000-AddSafetyRailsCore.tsatee45946e5).
P1.2 — Guard, schema, prompt, evidence (pure)
-
T9. Output schema. Create
packages/agent/src/app-provisioning/provision-output.schema.ts(new) — strict Zod schema of plan §3.3 (detection source fromAPP_PROVISIONING_DETECTION_SOURCES) andextractProvisionOutput(finalMessage)(last fencedprovision-outputblock; ≤ 512 KB). Test:packages/agent/src/app-provisioning/__tests__/provision-output.schema.spec.ts(new) — last-block selection, size cap, strict unknown keys, detection sourceautoaccepted and any value outsideAPP_PROVISIONING_DETECTION_SOURCESrefused. Done when: the module has no I/O andpnpm --filter @ever-works/agent test -- provision-output.schemapasses. -
T10. Output guard. Create
packages/agent/src/app-provisioning/provision-output.guard.ts(new) — every rule in plan §7.6 with stable violation codes (pathNotWritable,tooManyFiles,tooManyLines,fileTooLarge,secretLiteral,exampleValueReused,preservedFieldChanged,unpinnedBaseImage,missingSmoke,missingProbes,cronWithoutAuth,tooManyChecks,invalidSpec,yamlParse), usingscanForSecretsfrompackages/agent/src/utils/secret-scan.tsand APW-03'svalidateAppSpecDocument. ThebaseSpecYamlargument is the last valid applied spec merged with APW-01'ssourceRepository(plan §3.2) — never the raw head file: when the head is absent or invalid the base is{ source }plus schema-safe defaults, so an injected pre-seededupstreamPullRequests.requireApproval: falseis not inherited (ACC-NEG-05) while a genuine narrowing of a platform-owned field still fails the guard. Test:packages/agent/src/app-provisioning/__tests__/provision-output.guard.spec.ts(new) — one case per code; an edit outside.works/works.yml/.works/overlay/**is rejected naming the path (ACC-04-07); a literal secret or example-file value rejected naming the variable only (ACC-04-08); a changed source, Blueprint, license or upstream field rejected (ACC-04-09); a cron route without auth rejected (ACC-04-14, guard half); an injectedAGENTS.mdoutput that writes outside.works/rejected (ACC-04-34, guard half); a hostile pre-seeded head spec withupstreamPullRequests.requireApproval: falseis refused by the guard when the proposal keeps it and passes when the proposal drops it; a property check that no violation object contains any file content substring longer than 8 characters. Done when: the guard is pure (no DI, no I/O) and 100% of codes are covered. -
T11 (parallel). Prompt builder. Create
packages/agent/src/app-provisioning/app-provision-prompt.builder.ts(new) — plan §7.5 order; the Task brief carriesbuildStrategiesfrom the App Work's build plugin (R-13) and never a builder name; fences reuse the delimiter + neutralisation approach inpackages/agent/src/services/memory-recall.ts; instruction files truncated at 32 KB each, ≤ 8 files; evidence truncated at 200 lines / 16 KB afterredactSecrets. Test:packages/agent/src/app-provisioning/__tests__/app-provision-prompt.builder.spec.ts(new) — a fixtureAGENTS.mdcontaining a closing-fence sequence, "ignore previous instructions", "print the environment" and an external URL stays inside itsUNTRUSTED PROJECT INSTRUCTIONSfence (ACC-04-34); the brief listsautoonly when the build plugin double supports it and contains no builder name (ACC-04-38). Done when: the builder is pure andpnpm --filter @ever-works/agent test -- app-provision-prompt.builderpasses. -
T12 (parallel). Evidence renderer. Create
packages/agent/src/app-provisioning/app-provision-evidence.renderer.ts(new) — PR comment Markdown from anAppProvisioningAttempt+ spend +appProvisionCopy.evidence.*(T49, English);redactSecrets; 60,000-char cap. Test:packages/agent/src/app-provisioning/__tests__/app-provision-evidence.renderer.spec.ts(new) — a green attempt renders build log link, image digest, target kind, smoke table and spend with no env value (ACC-04-16); a runner-target attempt says "Verified in the build runner" (ACC-04-22); truncation at 60,000. Done when: the renderer is pure and its output passesscanForSecretsfor every fixture.
P1.3 — Agent, Skill, sandbox, tools
-
T13. Agent resolver. Create
packages/agent/src/app-provisioning/app-provisioner-agent.resolver.ts(new) — reusefindRecentAgentId(userId, scope)orcreateFromRepoTemplate('app-provisioner')(T2); installprovision-appwithSkillsService.installFromCatalogand bind it (injectIntoAgent: true, priority 10); write the allow list of plan §7.4 (ask_human,appProvisionReport,appSpecValidateDraft) and the deny list (incl.commitToRepo,openPullRequest— R-17 — plustransitionTask,createTask,commentOnTask,resolve_escalation) throughpackages/agent/src/policy/tool-grant.service.ts; setcanCallExternalTools: false;assertReady(agentId)re-checks skill binding and grants before every dispatch and re-installs the Skill once. Test:packages/agent/src/app-provisioning/__tests__/app-provisioner-agent.resolver.spec.ts(new) — create once, reuse; the deny list includes both git tools (ACC-04-37, grant half); a resolved tool set that contains anything outside the allow list refuses dispatch (ACC-04-43);transitionTaskis refused by grant and withheld by the run context (T55); widened grants refuse dispatch; skill re-install once. Done when:pnpm --filter @ever-works/agent test -- app-provisioner-agent.resolverpasses. -
T14. Sandbox environment + pipeline selection. Create
packages/agent/src/app-provisioning/app-provision-sandbox.ts(new) — the pre-resolvedruntimeEnvironment(plan §7.3 host list as a frozen constant), theattachedReposbuilder (one entry,mountDir: 'repo', tokenless, public repositories only: a private copy or a linkedprivate/internalrepository yields no mount and readinesspublicRepository: false, plan §2.2 / FR-63), andresolveIsolatedPipeline()(noagentId— no Agent carries a pipeline binding) requiring the flag andrunSandboxSessionand resolvable plugin settings for the user and Work. Test:packages/agent/src/app-provisioning/__tests__/app-provision-sandbox.spec.ts(new) — host list has no IP literal and no internal suffix and excludes the platform origin (ACC-04-05, unit half); the environment carries no env files, no credential and one tokenless repository mount (ACC-04-06, unit half); readiness false when no pipeline has the flag or has the flag without the runner method, so no Run, Task or pull request is created (ACC-04-04); a private copy and a private link each give readinesspublicRepository: false, build no mount, and record zero calls toGitFacadeService.getInstallationTokenForOwnerandgetAccessToken(ACC-04-40). Done when: the host list is a frozen constant asserted by snapshot. -
T15. Provisioning tools. Create
packages/agent/src/app-provisioning/app-provision-tools.ts(new) —appProvisionReport(≤ 280 chars, 1 / minute, writes the analysis step note) andappSpecValidateDraft(≤ 128 KB, APW-03 validator, returns messages only). Modifypackages/agent/src/agents/agent-tool.service.ts— register both only when the run's Task has a provisioning row (lookup port from T3). Test:packages/agent/src/app-provisioning/__tests__/app-provision-tools.spec.ts(new) — absent for other Tasks; rate limit; size cap;packages/agent/src/agents/__tests__/agent-tool.service.spec.tspasses unchanged. Done when: the two tools appear only in provisioning runs' tool lists. Also registered, unchanged, for the sandbox path: the restricted session of plan §2.6 exposes no platform tool at all, so questions reach the job through thequestionoutcome of the output contract and validation stays in the job (guard + validate step). These two tools stay registered exactly as above — the in-host paths keep using them, and nothing is removed from them.
P1.4 — Writer, pull request, service, job
-
T16. Writer and PR variants (Task finalize — R-17). Modify
packages/agent/src/tasks-domain/task-workspace.service.ts— addpushProvisioningChangesandopenProvisioningPullRequest(plan §7.6) on the Task finalize path (WorkspaceFacadeService.finalize,openPullRequestForBranch); add optionaltransitionToReview/attemptAgentMergeargs (defaulttrue) to the privateopenPullRequestForBranch. Test:packages/agent/src/tasks-domain/__tests__/task-workspace.provisioning.spec.ts(new) — push-only; PR without transition or merge attempt; conflict → one rebase; a rejected write pushes nothing (ACC-04-07, writer half); existingfinalizeRungolden calls unchanged. Done when: existing task-workspace specs underpackages/agent/src/tasks-domain/__tests__/pass unchanged. -
T17. Dispatcher. Create
packages/agent/src/tasks/app-provision-dispatcher.ts(new) (plan §6.1). Modifypackages/agent/src/tasks/_tasks-symbols.ts(add toTASKS_BARREL_RUNTIME_SYMBOLS, alphabetical) andpackages/agent/src/tasks/index.ts. Test: extendpackages/agent/src/tasks/tasks.spec.ts— the symbol isSymbol(...)and listed. Done when:pnpm --filter @ever-works/agent test -- tasks.specpasses. -
T18. Step runner. Create
packages/agent/src/app-provisioning/app-provisioning-step-runner.ts(new) —advance(row, event) → { patch, effects }over plan §2.5 — including its outcome table (no-change→succeededwith no pull request and a verify-mode build of the base head;not-runnable→failed/not-runnable;question→needs_inputwith a reason;blocked→needs_input/missing-required-value; parked → a wait) and its reason → options → row-patch table — with attempt CAS, fingerprinting (normalisation frompackages/agent/src/agents/loop-detector.ts), infra retries (3 in 30 min), caps (§6.4, incl. the build reservation and itscancelBuildatstartedAt + reservation), question reasons, ceilings (3 questions, 9 attempts), deadline (8 h active), target selection (runner only in P1), the §4 start-semantics outcome for a non-restart start, the TaskprStateprecedence of plan §6.2, and the R-17 outcome map of plan §6.2 (waits vsneeds_input). Test:packages/agent/src/app-provisioning/__tests__/app-provisioning-step-runner.spec.ts(new) — table test covering every step × event and every outcome of the §2.5 outcome table with its status, step and effect, plus one case per reason → options row (option count ≤ 4,verify-on-clusterabsent in P1) and one per option → row patch; plus named cases: a red attempt resumes the Agent with counters that agree (ACC-04-17); a repeated fingerprint asks without spending an attempt (ACC-04-18); 3 reds → one question with ≤ 4 options, an answer grants 2, no 4th question, no 10th attempt (ACC-04-19); infra failures leave the counter and fail after 3 retries in 30 min (ACC-04-20); target None boots and smokes in the runner (ACC-04-22); token and runner-minute caps refuse dispatch and ask with receipts — used 151 with a reservation of 90 is refused, used 150 is allowed (ACC-04-24); closing the PR cancels and merging mid-attempt ends merged-unverified (ACC-04-29); a job woken by any event while the Task'sprStateisclosedendscancelled; a hostile pre-seeded head spec (invalid,upstreamPullRequests.requireApproval: false) yields a proposal that does not inherit it (ACC-NEG-05); parkedkill-switch/agent-paused/workspace-paused/scope-pausedruns leaveattemptsUsed, infra retries andactiveMsunchanged and resume on lift (ACC-04-35); every other safetyreasonCodeends inneeds_inputwith reasonsafety-rail, never red (ACC-04-36). Done when: the runner has no I/O and every effect carries an idempotency key. -
T19. Service. Create
packages/agent/src/app-provisioning/app-provisioning.service.ts(new) —start,cancel,notify,readiness,get(active + 10 recent), effect executors (Task viapackages/agent/src/tasks-domain/tasks.service.ts, run dispatch through the existing assign-task path, build via the APW-05 facade, comments viaGitFacadeService.createPullRequestComment, Activity viapackages/agent/src/activity-log/activity-log.service.ts, questions per plan §7.7 viapackages/agent/src/inbox/inbox.service.ts, resume viapackages/agent/src/agents/run-steering.service.ts, receipts viapackages/agent/src/agents/run-receipt.service.ts, push + PR via T16's Task finalize variants); per-user 3 / per-org 10 queueing; stop-flag check viapackages/agent/src/agents/run-kill-switch.ts. Coordinate (safety rails, R-17): the assign-task dispatch already passespackages/agent/src/agents/run-admission-chain.ts(stop flag → AW-23 Agent brake → concurrency → credits) and tool calls passSAFETY_GATE(packages/agent/src/safety/safety-gate.port.ts); read the parkedqueuedReason(QUEUED_REASON_KILL_SWITCH,QUEUED_REASON_AGENT_PAUSED) and the gate'sreasonCodeintoparkedReason/parkedAtor asafety-railquestion per plan §6.2. Do not re-implement the brake. Modifypackages/agent/src/entities/activity-log.types.ts— appendAPP_PROVISION = 'app_provision'toActivityActionType; every Activity row uses it asactionTypewith the dottedapp.provision.*name asaction(R-2). Createpackages/agent/src/app-provisioning/app-provisioning.module.tsandpackages/agent/src/app-provisioning/index.ts(new); implement the lookup port (T3) andAPP_PROVISION_EVENTS_PORT(plan §6.5) — all five methods, includingpullRequestStateChanged(taskId, prState)(T52's producer calls it); clearlastRunOutputonce the guard has read it (plan §2.6). Modifypackages/agent/src/entities/activity-log.types.ts— appendAPP_PROVISION = 'app_provision'toActivityActionType; every Activity row uses it asactionTypewith the dottedapp.provision.*name asaction(R-2).app.provision.failedis logged withActivityStatus.FAILEDand every otherapp.provision.*row withActivityStatus.COMPLETED;packages/agent/src/activity-log/feed-kind.tsgains[ActivityActionType.APP_PROVISION]: 'work'in a commented "App Works (APW-04)" block (T56). Test:packages/agent/src/app-provisioning/__tests__/app-provisioning.service.spec.ts(new) — start dedupe, restart cancels; the whole §4 start-semantics order (plan §4): dedupe insidestartDedupeMsreturns the existing row withdeduplicated: truewhateverrestartsaid and cancels nothing, a 409 only outside the window, an automatic trigger on an active row is a no-op, an insert that losesuq_work_app_provisionings_activereturns the winner, readiness false withrestart: truecancels nothing and writes nothing, and a queued promotion whoseisolatedRuntimeis false fails withno-isolated-runtimeand dispatches no Run (ACC-04-41); a user's 4th provisioning is Queued and never more than one active Run or Build (ACC-04-25);pullRequestStateChangeddispatchespr-stateexactly once for aneeds_inputorsucceededrow and is a no-op for a terminal row (ACC-04-29); Activity holdsstarted,proposed,attempted,needs_input,succeeded,failedwithactionType 'app_provision'and no body text or values (ACC-04-27, P1 types); a stop-flag or pause park is a wait (ACC-04-35) and a gate refusal a question (ACC-04-36).packages/agent/src/app-provisioning/__tests__/app-provisioning.finalize-path.spec.ts(new) — with the Agent'spublishrung set toask, the proposal is pushed and its PR opened throughTaskWorkspaceServicewith no held action created, andcommitToRepo/openPullRequestare refused for the run (ACC-04-37). Done when:pnpm --filter @ever-works/agent test -- app-provisioning.service app-provisioning.finalize-pathpasses. -
T20. Inbox answer hook. Modify
packages/agent/src/inbox/inbox.service.ts— when a replied item id equals a row'sopenInboxItemId, callnotify({ event: 'answered', refId: optionId })in addition to the existing resume routing (the step runner decides whether to resume, cancel, raise a cap or switch target). Modifypackages/agent/src/app-provisioning/app-provisioning.service.ts— a provisioning question is filed with noagentRunId(plan §7.7), sorouteQuestionReplyneither steers nor resumes the run; theanswerednotify is the only route from the Inbox into the provisioning, which is what keeps the caps and the attempt ceilings authoritative. Test:packages/agent/src/inbox/__tests__/inbox.service.provisioning-answer.spec.ts(new) — a matching reply notifies once, dispatches zerosteerand zeroresumecalls, and the existing reply behaviour is unchanged for items without a row (ACC-04-19, answer half);packages/agent/src/inbox/__tests__/inbox.service.spec.tspasses unchanged. Done when: both specs pass. -
T21. Jobs. Create
packages/tasks/src/tasks/trigger/app-provision.task.ts(new) (lease,advance, effects, delayed re-dispatch, 10-minmaxDuration, 2 retries, 5-minute re-tick while parked) andpackages/tasks/src/tasks/trigger/app-provision-sweep.task.ts(new) (cron7,22,37,52 * * * *, items 2–6 of plan §6.3; item 1 arrives in P2). Modifypackages/tasks/src/tasks/trigger/index.ts; wire the dispatcher throughpackages/agent/src/tasks/job-runtime.providers.ts. Test:packages/tasks/src/__tests__/app-provision.task.spec.ts(new) — lost lease exits quietly; effect replay idempotent; a parked row re-ticks without advancing (ACC-04-35); a job woken by abuild-updatedtick for a row whose TaskprStateisclosedcancels instead of building.packages/tasks/src/__tests__/app-provision-sweep.task.spec.ts(new) — sweeper caps at 200 rows; the fallback path emitspr-statefor a closed-PR row the port missed (ACC-04-29); a queued promotion whose readiness is false fails withno-isolated-runtimeand dispatches nothing; stale questions remind at 72 h and fail at 14 days. Done when:pnpm --filter @ever-works/trigger-tasks test -- app-provisionpasses andpackages/agent/src/tasks/__tests__/job-runtime.providers.spec.tspasses after recounting. -
T22. Budget-override approval path. Modify
packages/agent/src/app-provisioning/app-provisioning.service.ts— cap questions offerraise-cap; selecting it creates an approval proposal of action typebudget_overridethrough the existing proposal producer (AgentApprovalsService.createProposal) carrying the discriminator payload{ kind: 'app-provision-cap', provisioningId, capKind: 'token' | 'runner-minute', delta }(delta=1_000_000tokens or60minutes); approval patches the cap and dispatchesanswered. Createpackages/agent/src/app-provisioning/app-provision-cap-approval.listener.ts(new) — an@OnEventlistener onAgentActionProposalDecidedEvent(the pattern ofpackages/agent/src/email/email-draft-approval.listener.ts) that accepts onlydecidedVia === 'user'and onlyapproved, re-reads the row, appliestokenCap = min(tokenCapMax, tokenCap + 1_000_000)/runnerMinuteCap = min(runnerMinuteCapMax, max(runnerMinuteCap + 60, runnerMinutesUsed + reservation)), and dispatchesanswered; any other decision leaves the rowneeds_inputand records the outcome on the item. Test:packages/agent/src/app-provisioning/__tests__/app-provisioning.budget-override.spec.ts(new) — guardrails always queue it (risk flagbudget_override); the payload round-trips and a proposal for another action type is ignored; a rejected, expired or system-decided proposal leaves the rowneeds_input; the raise always fits one more build reservation and never exceedsrunnerMinuteCapMax/tokenCapMax; the cap question carries receipts (ACC-04-24). Done when: the spec passes and no approval can raise a cap above its maximum.
P1.5 — API
-
T23. Controller and DTOs. Create
apps/api/src/works/app-provisioning.controller.tsandapps/api/src/works/dto/app-provisioning.dto.ts(new) —POST provision,GET provisioning,POST provision/cancel(plan §4), throttles, error contract,canEdit. The GET response isAppProvisioningResponse(plan §3.2) — includinglatest,readiness.publicRepositoryandquestion.inboxItemIdfor the recipient only — and the never-returned key list of plan §3.2 is enforced by the DTO, not by convention. Modifyapps/api/src/works/works.module.ts— register the new controller. Test:apps/api/src/works/app-provisioning.controller.spec.ts(new) — 202 in under 2 s without awaiting the job and two concurrent starts yield one id (ACC-04-03); a double restart inside the 10 s window yields one id withdeduplicated: trueand 409 only outside it; 409/422 codes; cross-scope 404 on every route; a viewer gets 200 withcanEdit: falseand a caller without read permission gets 404 (ACC-04-32); the serialised GET body's key set equals the view interfaces' key set exactly, with none of the never-returned keys present (plan §3.2). Done when: the three routes are in the OpenAPI document with their error codes. -
T24. Creation hook. Modify
packages/agent/src/app-provisioning/app-provisioning.service.ts—start({ workId, trigger: 'auto-create' })is idempotent per App Work, returns{ started: false, missing }without throwing (and writes nothing) when any readiness flag is false, and refuses when APW-03 reports a resolved Blueprint, a valid App spec, or the creator declined (sourceRepository.autoProvision === false— manual, chat and endpoint starts ignore that flag). Coordinate with APW-01: itsAppSourceInitializerServiceminimal path calls it when no Blueprint and no valid App spec exist, unless the creator declined. Test:packages/agent/src/app-provisioning/__tests__/app-provisioning.auto-start.spec.ts(new) — with APW-01's creation-service test double: no Blueprint and no App spec → a provisioning within 60 s of readiness with the Task, the row andapp.provision.started(ACC-04-01); a matched Blueprint, a valid App spec or a declined creator → none (ACC-04-02); readiness false → no row, no Task, no Run and no throw, and a later manualPOST /provisionreturns 202 (ACC-04-41). Done when:pnpm --filter @ever-works/agent test -- app-provisioning.auto-startpasses.
P1.6 — Web, chat, i18n, telemetry
-
T25. Card, steps, dialogs. Create
apps/web/src/components/works/detail/overview/AppProvisioningCard.tsx,apps/web/src/components/works/detail/overview/AppProvisioningSteps.tsx,apps/web/src/components/works/detail/overview/AppReprovisionDialog.tsx(new) andapps/web/src/app/api/works/[id]/provisioning/route.ts(new). Modifyapps/web/src/app/[locale]/(dashboard)/works/[id]/page.tsx(render forkind === 'app'),apps/web/src/lib/api/work.ts,apps/web/src/app/actions/dashboard/works.ts. Test:apps/web/src/components/works/detail/overview/AppProvisioningCard.unit.spec.tsx,apps/web/src/components/works/detail/overview/AppReprovisionDialog.unit.spec.tsx(new) — every headline state (incl. the private-repository state) and 8 step rows, poll every 5 s only while active and no request once terminal (ACC-04-26), the waiting note (ACC-04-35), viewer hides actions (ACC-04-32), every field rendered comes fromAppProvisioningResponsewith no extra fetch, the Needs-input headline rendersquestion.subject.<reason>fromquestion.reason+question.paramswithout an Inbox fetch, View report links to the lastrunIdsentry (orpullRequest.urlwhen one exists), and the dialog sendsrestart: trueonly in the running-provisioning variant (and reopens that variant on a 409). Done when:pnpm --filter ever-works-web test -- AppProvisioningCard AppReprovisionDialogpasses. -
T26. Chat milestones and chat action. Create
packages/agent/src/app-provisioning/app-provision-chat.notifier.ts(new) — plan §7.8 viapackages/agent/src/conversations/conversation-message.service.ts, body copy fromappProvisionCopy.chat.*(T49, stored English), ≤ 12 messages. Modifyapps/web/src/lib/ai/tools/work.tools.ts—provisionAppWorkwith confirmation; it never sendsrestart(plan §4). Test:packages/agent/src/app-provisioning/__tests__/app-provision-chat.notifier.spec.ts(new) — milestones land in the Work thread, the thread is created when none exists, and a provisioning that would post a 13th message posts nothing more (ACC-04-28);apps/web/src/lib/ai/tools/work.tools.unit.spec.ts(new) — the chat tool requires confirmation and its request body carries norestartfield. Done when: both specs pass andchatMessagesPostednever exceeds 12 in any fixture. -
T27. i18n. Modify
apps/web/messages/en.json— thedashboard.workDetail.appProvisioningsub-tree of plan §9 (incl. the waiting notes, thesafety-railquestion, the twoqueuedReasonkeys,headline.privateRepository/reason.privateRepository, thestepNote.note*keys and the reminder title/body); mirror into 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/overview/app-provisioning-messages.unit.spec.ts(new) — modelled onapps/web/src/components/works/meetings/meetings-messages.unit.spec.ts: ≥ 21 locales discovered; every key the card, steps, dialogs, questions, chat milestones and evidence read exists in every locale; camelCase leaves with no.; every message survives acreateTranslatorround trip; and everyquestion.*,chat.*,evidence.*,reason.*andqueuedReason.*leaf equals the matching template in@ever-works/contractsapp-provisioning-copy.ts, while every value of the four*_I18N_KEYmaps exists as a leaf in all 21 locales (ACC-04-44). Test:pnpm --filter ever-works-web test -- app-provisioning-messages(ACC-04-33, ACC-04-44). Done when: the spec passes and the message-key lint reports nothing for the sub-tree. -
T28. Telemetry. Create
packages/monitoring/src/posthog/app-provision-events.ts(new) — the six events of plan §10.1 with a forbidden-property list, followingpackages/monitoring/src/posthog/kb-events.ts. Modifypackages/monitoring/src/posthog/index.ts(export) andpackages/agent/src/app-provisioning/app-provisioning.service.ts(emit). Test:packages/monitoring/src/posthog/__tests__/app-provision-events.spec.ts(new) — no payload contains a repository name, path, env name, question or report text; forbidden keys throw. Done when:pnpm --filter @ever-works/monitoring test -- app-provision-eventspasses. -
T29. P1 e2e. Create
apps/web/e2e/app-provisioning-card.spec.ts,apps/web/e2e/app-provisioning-reprovision.spec.ts,apps/web/e2e/app-provisioning-needs-input.spec.ts,apps/web/e2e/app-provisioning-a11y.spec.ts(new). Modifydocs/specs/features/app-works/ACCEPTANCE.md— wire ACC-04-01…04, 16…20, 22…29, 32…38 under APW-04 against the APW-13 fixture app. Test:pnpm --filter ever-works-web test:e2e -- app-provisioning-card app-provisioning-reprovision app-provisioning-needs-input app-provisioning-a11y— setup state with no isolated sandbox (ACC-04-04); a private-repository App Work shows the private state with no actions (ACC-04-40); a red attempt shows matching counters (ACC-04-17); amber → My Decisions → answer → running (ACC-04-19); every headline, 8 rows, polling stops when terminal and axe clean (ACC-04-26); closed PR → cancelled card (ACC-04-29); viewer sees no actions (ACC-04-32). Done when: all pass; no existing Work Overview or Inbox spec needed a selector change.
P1.7 — Catalog repositories
-
T30. Agent template (
ever-works/agents). Create inever-works/agents:templates/app-provisioner/.works/agent.yml,templates/app-provisioner/SOUL.md,templates/app-provisioner/skills.ymlfromagent-template-draft/, plus the companion files its header comment lists (templates/app-provisioner/prompts/system.md,templates/app-provisioner/prompts/tasks/provision-repository.md,templates/app-provisioner/prompts/tasks/fix-verification.md,templates/app-provisioner/kb/playbooks/{detection-order,env-classification,bootstrap-risks}.md,templates/app-provisioner/README.md,templates/app-provisioner/icon.svg) andeval/app-provisioner.yml(the catalogue entry of T32). All of those companions are drafted here underagent-template-draft/—prompts/,kb/playbooks/,README.md,icon.svgandmanifest-row.json— because that repository'sscripts/validate.jsfails when any path a manifest points at is missing, andSOUL.mdplus the kb seed path must exist too. Modifymanifest.jsoninever-works/agents— atemplates[]row, copied from the draftedagent-template-draft/manifest-row.json(slug,path,name,title,summary,scope,avatarIcon,tags,manifestPath,soulPath,readmePath,systemPromptPath,skillsPath,kbPath,iconPath,evalPath). Branch: the catalog pull request targets that repository's default branch (main); if its contributing guide names a different integration branch at merge time, use that branch and record it in the task's PR description — never push directly tomain. ⚠️ Read this before writing the row: the manifest is what the platform reads, and the files are new. Verified in source: the catalog service fetchesmanifest.jsononly (apps/api/src/agents/agent-template-catalog.service.ts:50-51,191-212) and maps a row of exactly{ slug, name, title, summary, scope, avatarIcon, tags }(:8-16), while the shipped templates live in code (packages/agent/src/agents/agent-templates.ts:62, read byagent-templates.service.ts, written out asSOUL.md). So the row is what makesGET /api/agent-templateslistapp-provisioner, and theSOUL.md/agent.yml/skills.ymlfiles are additive catalog content that no reader consumes yet — loading them is its own work (EXISTING-SUBSTRATE.mdsays so, and APW-04 plan:607 calls it "(new, additive)"). This task must not assume the in-code catalog is replaced: the two coexist, and the existing templates keep shipping from code exactly as they do today. Test: that repository's schema CI on the pull request (validatesagent.ymlandskills.yml), thenGET /api/agent-templateson dev. Done when: the catalog's schema CI is green andGET /api/agent-templateslistsapp-provisionerwhile the pre-existing templates still list unchanged. -
T31 (parallel). Skill (
ever-works/skills). Create inever-works/skills:skills/provision-app/SKILL.mdfromskill-draft/SKILL.md. Modifymanifest.jsoninever-works/skills— a row copied from the draftedskill-draft/manifest-row.json:slug,path,name,summary,skillPath,tags,version: 0.1.0,license: MITandsourceUrl(the provenance field that repository'sschema/skill-manifest.schema.jsonrequires —https://github.com/ever-works/skills/tree/main/skills/provision-app). Provenance: theever-works/skillsREADME seeds every skill from an upstream permissive repository, so a first-party skill needs its attribution row there too — draft it beside the manifest row (skill-draft/README-attribution.md) and add it in the same pull request. Test: extendpackages/plugins/everworks-skills/src/everworks-skills.plugin.spec.tswith a fixture copy of the Skill's frontmatter assertingallowedToolsparses as the array['ask_human', 'appProvisionReport', 'appSpecValidateDraft']. Done when: the first-party skills provider lists it and its parsed frontmatter carriesallowedTools. -
T32. Skill evals. Create
eval/app-provisioner.ymlinever-works/agentsfrom the draftedeval-draft/app-provisioner.yml— the eight fixture cases of plan §11.4, each referencing its fixture repository and the structured expectation drafted ineval-draft/expectations.json, including the zero-config case run twice: withautoin the brief (expect detection sourceauto, no overlay Dockerfile) and without it (expect an overlay Dockerfile). Createpackages/agent/src/app-provisioning/__tests__/app-provisioner.eval.spec.ts(new) — the runnable half: reads the eight drafted fixture trees undereval-draft/fixtures/and asserts every expectation ofexpectations.json(detection source, dependency set with the citing file, env classifications, jobs, cron, probes, overlay yes/no) as a table test, so an eval regression is red in this repository rather than only in a catalog run. Test:pnpm --filter @ever-works/agent test -- app-provisioner.evalplus that repository's schema CI on the catalog pull request — detection order across six fixtures (ACC-04-10), public-prefix build-time and fixed-length key generator + validation (ACC-04-11), dependency inference citing files (ACC-04-12), first-deploy job + negative smoke and pre-deploy migration (ACC-04-13), cron entry with auth and none for a descriptor-only route (ACC-04-14), liveness never on a database-touching endpoint (ACC-04-15),autovs overlay and no builder named (ACC-04-38). Done when: every case passes in the monorepo spec and the catalog file validates in the pull request. -
T33. P1 ship gate. Modify
docs/specs/features/app-works/APW-04-app-provisioner/tasks.md(tick T6–T32, T46) and the APW-04 row ofdocs/specs/features/app-works/TRACKER.md; pinEVER_WORKS_AGENTS_REF/ the skills catalog ref for the environment. Test:pnpm format:check && pnpm lint && pnpm type-check && pnpm test && pnpm build; T4's live spec green on the nightly lane. Done when: all commands are green on the P1 merge commit and the nightly isolation run is green.
Phase P2 — Verify on your cluster; upstream breakage (Wave 1)
Delivers FR-32 cluster branch, FR-51; ACC-04-21, 30 (manual half).
Preconditions: APW-06 P1 (AppRenderInput.purpose: 'verification', deployApp, getAppStatus, destroyApp,
checkAppCluster, app-cluster-op on the isolated worker, smoke runner — R-10), APW-02 P1 (app.upstream.synced
range), APW-07 P1 (in-namespace ephemeral dependencies without PVCs).
-
T34. Cluster verification target. Create
packages/agent/src/app-provisioning/app-provision-verification-target.service.ts(new) — the APW-04-side selector and TTL bookkeeper:checkAppClusterprobe throughapp-cluster-op(10 s), target choice per plan §2.4,deployApp({ purpose: 'verification', ttlMinutes: 90 })with APW-07's ephemeral env mode, pending-on-capacity > 10 min → infra verdict + runner fallback once, TTL tracking anddestroyon attempt end. It delegates the namespace lifecycle to APW-06 T60'sAppVerificationTargetService(packages/agent/src/app-runtime/app-verification-target.service.ts), which owns the<ns>-v<attempt>scheme; APW-04 keeps no namespace implementation of its own, so there is exactly one verification-target service and one naming rule on the branch. The verification namespace name comes back fromverification-deploy, never from here — APW-06 §9.2 derives<ns>-v<attempt>from the live namespace and owns it; this service keeps the returned handle and passes it toverification-destroy. Do not compute, prefix or hard-code a name (ewv-…was an earlier draft: it would have destroyed a namespace APW-06 never created, leaking one per attempt). Modifypackages/agent/src/app-provisioning/app-provisioning-step-runner.ts— target selection of plan §2.4. Test:packages/agent/src/app-provisioning/__tests__/app-provisioning.verification-target.service.spec.ts(new) with an APW-06 plugin double: no Ingress and no PVC in the render input, destroy called on green, red, cancel and TTL (ACC-04-21); the namespace value the double returns is the only one ever passed toverification-destroy, and no name is computed locally; extendpackages/agent/src/app-provisioning/__tests__/app-provisioning.verification-env.spec.ts(T46) with the cluster target (ACC-04-23). Done when:pnpm --filter @ever-works/agent test -- app-provision-verification-target app-provisioning.verification-envpasses. -
T35. Sweeper item 1. Modify
packages/tasks/src/tasks/trigger/app-provision-sweep.task.ts— expired targets destroyed; three failed destroys add the card note. Test: extendpackages/tasks/src/__tests__/app-provision-sweep.task.spec.ts— a target past its 90-minute expiry is destroyed within one tick and never older than 90 minutes (ACC-04-21); retry cadence every 15 minutes. Done when:pnpm --filter @ever-works/trigger-tasks test -- app-provision-sweeppasses. -
T36. Upstream breakage banner and range. Modify
packages/agent/src/app-provisioning/app-provisioning.service.ts— implementsmokeFailedAfterUpstreamSyncon the events port;GET provisioningreturnsupstreamSmokeBroken; a re-provision started from the banner carriesupstreamFromSha/upstreamToShainto the Task brief. Modifyapps/web/src/components/works/detail/overview/AppProvisioningCard.tsxfor the banner. Test: extendpackages/agent/src/app-provisioning/__tests__/app-provisioning.service.spec.ts— the banner state after a failing first post-sync Deployment and no automatic start without the opt-in (ACC-04-30, manual half); extendapps/web/e2e/app-provisioning-card.spec.tswith the banner case. Done when: both specs pass. -
T37. P2 e2e and ship gate. Modify
docs/specs/features/app-works/ACCEPTANCE.md— extend the APW-04 journey with a cluster-target attempt (ACC-04-21);docs/specs/features/app-works/APW-04-app-provisioner/tasks.md(tick T34–T36) and the APW-04 row ofdocs/specs/features/app-works/TRACKER.md. Test:pnpm format:check && pnpm lint && pnpm type-check && pnpm test && pnpm build; the nightlyapps/web/e2e/flow-app-works-live-provisioner-path.spec.tsrun with targetcluster(ACC-04-21). Done when: all commands are green and the nightly evidence names the APW-06-derived<ns>-v<attempt>verification namespace (the valueverification-deployreturned — never a locally computed name) gone within 5 minutes.
Phase P3 — Blueprint suggestions; opt-in automatic re-provision (Wave 2)
Delivers FR-52…FR-55; ACC-04-30 (automatic half), 31.
Preconditions: APW-03 license class on WorkAppSpecState; spec.provisioning.autoReprovision in the APW-03 schema.
-
T38. Suggestion endpoint and bundle. Modify
apps/api/src/works/app-provisioning.controller.ts—POST …/blueprint-suggestion(consent, eligibility, 5 / 30 days, one open per upstream). Createpackages/agent/src/app-provisioning/app-blueprint-suggestion.builder.ts(new) — stripssource,domainshosts, generated and prompted values; keeps overlay files, smoke tests, upstream repo + commit, license, evidence links; ≤ 256 KB. Test:packages/agent/src/app-provisioning/__tests__/app-blueprint-suggestion.builder.spec.ts(new) — no domain, generated or prompted value survives (ACC-04-31); extendapps/api/src/works/app-provisioning.controller.spec.ts— 422 when FR-53 fails and 409 for a second suggestion for the same upstream (ACC-04-31); extendpackages/agent/src/app-provisioning/__tests__/app-provisioning.service.spec.ts—app.provision.blueprint_suggestedcompletes the seven event types (ACC-04-27). Done when: the three specs pass. -
T39. Admin review list. Create
apps/api/src/works/admin-app-blueprint-suggestions.controller.ts(new) —@Controller('api/admin/app-blueprint-suggestions'), platform-admin guard in the style ofapps/api/src/budgets/admin-usage.controller.ts; list ≤ 50 per page; bundle download. Createapps/web/src/app/[locale]/(dashboard)/admin/app-blueprint-suggestions/page.tsx(new, read-only) andapps/web/src/components/works/detail/overview/AppBlueprintSuggestDialog.tsx(new). Modifyapps/api/src/works/works.module.ts— register the admin controller. Test:apps/api/src/works/admin-app-blueprint-suggestions.controller.spec.ts(new) — non-admin 404;apps/web/e2e/app-provisioning-suggest-blueprint.spec.ts(new) — hidden unless eligible, consent gates submit, suggested state (ACC-04-31). Done when: both specs pass. -
T40. Automatic re-provision (opt-in). Modify
packages/agent/src/app-provisioning/app-provisioning.service.ts— readspec.provisioning.autoReprovisionthroughAppSpecService.getEffectiveSpec; onsmokeFailedAfterUpstreamSyncstart with triggerauto-upstream-smokeat most 1 per sync commit and 2 per 7 days. Test: extendpackages/agent/src/app-provisioning/__tests__/app-provisioning.service.spec.ts— both limits; never starts when the flag is absent or false (ACC-04-30, automatic half). Done when: the spec passes. -
T41. Per-Organization cap override. Modify
packages/agent/src/entities/organization.entity.ts— nullable simple-jsonappProvisionCaps({ tokenCap?, runnerMinuteCap? });apps/api/src/organizations/dto/update-organization.dto.ts— accept it within plan §3.2 bounds;packages/agent/src/app-provisioning/app-provisioning.service.ts— resolve Organization → instance env → default. Createapps/api/src/migrations/1792040100000-AddOrganizationAppProvisionCaps.ts(new) — adds the one nullable column. Test:packages/agent/src/app-provisioning/__tests__/app-provisioning.caps.spec.ts(new) — clamping and the resolution order; Activity records field names only;apps/api/src/migrations/__tests__/AddOrganizationAppProvisionCaps.spec.ts(new) — only the new column is added and dropped. Done when: both specs pass andpackages/agent/src/entities/__tests__/organization.entity.spec.tspasses. -
T42. P3 ship gate. Modify
docs/specs/features/app-works/APW-04-app-provisioner/tasks.md(tick T38–T41) and the APW-04 row ofdocs/specs/features/app-works/TRACKER.md. Test:pnpm format:check && pnpm lint && pnpm type-check && pnpm test && pnpm build; walk ACC-04-30 and ACC-04-31 on stage. Done when: all commands are green and both criteria are ticked in spec §8.
Cross-phase closing tasks
-
T43. Docs. Create
docs/features/app-provisioner.md(new) — what the card states mean (incl. waiting on a stop or pause), what the sandbox can and cannot reach, what the evidence proves, caps and how to raise them. Modifyapps/docs/sidebarsPlatform.tsto list it. Do not touchdocs/plugin-system/built-in-plugins.md(no plugin added). Test:pnpm --filter ever-works-docs build— no broken-link warning. Done when: the page is reachable from the platform docs sidebar. -
T44. Vocabulary. Modify
docs/specs/features/app-works/README.md— add App provisioning to §1 of the program (spec §5.2) in the same PR as T7 (already present at authoring — confirm the row still matches the shipped entity name). Test:rg -n "App provisioning" docs/specs/features/app-works/README.mdshows the §1 row namingWorkAppProvisioning. Done when: the README row and the entity name match. -
T45. Statuses. Modify
docs/specs/features/app-works/APW-04-app-provisioner/spec.md,…/plan.mdand this file — setImplemented/Done. Test: walk every gate in the plan §13 constitution compliance checklist and the "Known gaps" list against the merged code. Done when: statuses readImplemented/Doneand every gate still holds. -
T46 (P1, lands with T18–T19). Verification never stores env values. Modify
packages/agent/src/app-provisioning/app-provisioning.service.ts— every verification request passes APW-07's ephemeralAppRuntimeEnvSourcemode (runner:startBuild({ mode: 'verify', verification })with in-memory generated and derived values; cluster, from P2: the verification render input) and never calls aWorkAppEnvValuewrite path. Test:packages/agent/src/app-provisioning/__tests__/app-provisioning.verification-env.spec.ts(new) — a red → green runner journey with aWorkAppEnvValuerepository spy records zerosave/insert/updatecalls, every verification request carries the ephemeral mode, and the stored env of the App Work is byte-identical before and after (ACC-04-23). Done when:pnpm --filter @ever-works/agent test -- app-provisioning.verification-envpasses. -
T47 (P1, lands with T7–T8). Classify new tables for workspace backup (R-25). Modify
packages/agent/src/account-transfer/backup/collectors/domain-specs.ts— append to theworksdomain:{ file: 'app-provisionings.jsonl', entity: 'WorkAppProvisioning', scope: { by: 'parent', column: 'workId', from: 'workIds' } }(through the Work, neverby: 'user', which would also export runs the person started in other workspaces). Modifypackages/agent/src/account-transfer/backup/redaction.ts— addtokenCaptoBACKUP_BENIGN_COLUMNS("a model-token ceiling, not a credential");tokensUsedis already exempt. Nothing joinsBACKUP_DROPPED_ENTITIES. T41'sOrganization.appProvisionCapsneeds no entry:Organizationalready exports asdata/organizations/organization.jsonland the column name is not secret-shaped. Test: extendpackages/agent/src/account-transfer/backup/collectors/collectors.spec.ts—WorkAppProvisioningis referenced exactly once, inworks, scopedparentonworkIdfromworkIds, not dropped;Organizationis still referenced byorganizations/organization.jsonl. 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 with one provisioning run listsdata/works/app-provisionings.jsonl.
Added by the APW-04 gap audit (2026-09-17)
Each task names its phase and the tasks it lands with. Nothing above is renumbered or replaced: these are additions.
-
T48 (P1, lands before T19). Provisioning session runner. Create
packages/agent/src/app-provisioning/app-provision-session.runner.ts(new) per plan §2.6 — resolves the first enabled pipeline plugin that hasenforcesRuntimeNetworkingandrunSandboxSession(T1) plus resolvable settings, opens one restricted session with the Skill body assystem, the plan §7.5 brief asprompt, the plan §7.3 environment, one tokenlessattachedReposentry,budgetUsdfrom plan §6.4 andtimeoutMsfrom FR-14; maps the result (completed/failed/cancelled/timeout/budget-exhausted,failureCode,finalText,usage); writes the run's terminal status, tokens and cost, a redacted ≤ 2,000-char summary, andlastRunOutput(≤ 512 KB, cleared after the guard reads it); notifiesrun-finished. Modifypackages/tasks/src/tasks/trigger/agent-task-execute.task.ts— the T3 branch calls it and never falls back toAgentRunService.execute; a provisioning Task with no capable plugin is refused before any spend. Test:packages/agent/src/app-provisioning/__tests__/app-provision-session.runner.spec.ts(new) — the cases listed in plan §11.1, including "no capable plugin → refused with zero run spend" and "no fallback toexecute" (ACC-04-42, ACC-04-04). Done when: no provisioning run reaches the in-process tool loop (asserted by a spy onAgentRunService.execute) and the live isolation spec (T4) opens its session through this runner. -
T49 (P1, lands with T6–T12). Provisioning copy module and id→key maps. Create
packages/contracts/src/apps/app-provisioning-copy.ts(new) —appProvisionCopy.questionSubject,.questionOptions,.reasonText,.queuedReason,.chat.*,.evidence.*and.reminder, one exported constant per string, values character-for-character those of plan §9'sen.jsonleaves (spec §6 is the copy of record). Modifypackages/contracts/src/apps/app-provisioning.ts— the four*_I18N_KEYmaps of plan §3.2. Test:packages/contracts/src/apps/__tests__/app-provisioning-copy.spec.ts(new) — every reason, option, failure and queued id has a map entry matching/^[a-z][a-zA-Z0-9]*$/; every template is non-empty; no template contains a placeholder that is not a declared param;choose:<n>resolves through its prefix. Done when: the module has no I/O and T27's equality test passes against it. -
T50 (P1, lands with T19). Start semantics: dedupe, readiness, restart. Modify
packages/agent/src/app-provisioning/app-provisioning.service.ts— the seven ordered steps of plan §4 "Start semantics", in one transaction: dedupe insidestartDedupeMs, 409 only outside it for manual/chat, no-op for automatic triggers on an active row, readiness refusal with nothing written and nothing cancelled, restart compare-and-set, queueing reasons, and the lost-unique-insert race returning the winner. Test: extendpackages/agent/src/app-provisioning/__tests__/app-provisioning.service.spec.ts— the cases named in T19 (ACC-04-41, ACC-04-03, ACC-04-25). Done when: the service spec covers all seven steps and no path can create two active rows for one App Work. -
T51 (P1, lands with T16–T19). Provisioning Tasks are excluded from CI auto-resume and the merge gate. Modify
packages/agent/src/tasks-domain/task-pr-status.service.ts— beforeofferRedGateToFixLoop(:319-358), look the Task up through the provisioning lookup port (T3) and skip the fix-loop offer for a provisioning Task; skipTaskMergeGateService's merge attempt and approval raise for the same Tasks (task-merge-gate.service.ts, called from the poll at:515) so FR-9 holds whatever the merge policy says and no uncapped resume is started outside the step runner. Test:packages/agent/src/tasks-domain/__tests__/task-pr-status.provisioning.spec.ts(new) — a red check on a provisioning PR creates no auto-resume attempt and no Inbox notice; green CI on a provisioning PR creates no agent merge and raises no approval;task-pr-status.service.spec.tsand the merge-gate spec pass unchanged (ACC-04-37, FR-9). Done when: the two specs pass and a grep shows no other caller merging a provisioning Task's pull request. -
T52 (P1, lands with T16/T21). PR-state producer hook (S17 within 5 minutes). Modify
packages/agent/src/tasks-domain/task-pr-status.service.ts— capturetask.prStatebeforeupdatePrStatusCache; when the new state isclosedormergedand differs, callAPP_PROVISION_EVENTS_PORT.pullRequestStateChanged(task.id, state)fire-and-forget (catch, warn, never fail the refresh); inject the port@Optional() @Inject(APP_PROVISION_EVENTS_PORT)last in the constructor, and coordinate with APW-08 T22 (whichever PR lands second appends after the other). Test:packages/agent/src/tasks-domain/__tests__/task-pr-status.app-provision.spec.ts(new) — open→closed and open→merged each call the port once, on both the sync and the on-demand refresh; no call when the state is unchanged or the port is absent; a throwing port does not fail the refresh;task-pr-status.service.spec.tspasses unchanged (ACC-04-29). Done when: the spec passes and S17's 5-minute bound is met by the producer plus the §6.3 item 6 fallback. -
T53 (P1, lands with T7/T13). Per-person Agent ownership. Modify
packages/agent/src/app-provisioning/app-provisioner-agent.resolver.ts—resolve({ userId, scope })withscope = ownershipScopeOf(work); never reuse another Organization member's Agent. Modifypackages/agent/src/database/repositories/work-app-provisioning.repository.ts—findRecentAgentId(userId, scope)matchesuserIdand the exact tenant/organization ids. Test: extendpackages/agent/src/app-provisioning/__tests__/app-provisioner-agent.resolver.spec.ts— two editors of one Organization each get their own Agent and neither run endsagent-not-found; the same person in a personal space and in an Organization gets two Agents with no name conflict (ACC-04-39). Done when: the resolver spec passes and the repository spec asserts the scope predicate. -
T54 (P1, lands with T19). Events-port producers and their contract tests. Modify
packages/agent/src/app-provisioning/app-provisioning.service.ts— implement all five port methods, includingbuildUpdated(buildId)andtargetUpdated(workId, namespace)(both already in the CONTRACTS §3 row) andsmokeFailedAfterUpstreamSync(workId, fromSha, toSha)(T36). Recognize "the first Deployment built from an upstream sync commit" fromWorkBuild's recorded upstream-sync origin (APW-05/APW-02 field; the APW-05 and APW-06 task rows that call these producers are reported for their owners, not written here). Test: extendpackages/agent/src/app-provisioning/__tests__/app-provisioning.service.spec.ts— each producer moves the row and dispatches the matching event exactly once, an unknown build or namespace is a no-op, and the banner state is reachable through the port rather than by calling the service directly (ACC-04-30). Done when: no port method is reachable only from a test double. -
T55 (P1, lands with T13). Tool allow list and the withheld virtual transition. Modify
packages/agent/src/app-provisioning/app-provisioner-agent.resolver.ts— write the Agent-scopeallow: ['ask_human', 'appProvisionReport', 'appSpecValidateDraft']row and the extended deny list (transitionTask,createTask,commentOnTask,resolve_escalationadded) throughToolGrantService.upsert. Modifypackages/agent/src/agents/agent-run.service.ts— add the optionalAgentRunContext.withholdVirtualTransition?: booleanand skip the virtualtransitionTaskdescriptor (:852-870, descriptor at:1365) when it is set; the T3 branch dispatches with it. Every other run is unchanged. Test: extendpackages/agent/src/app-provisioning/__tests__/app-provisioner-agent.resolver.spec.ts— the grant row's allow list is exactly those three names;transitionTask,createTask,commentOnTask,resolve_escalationand a sample tool outside the list are all refused; with the flag unset the descriptor is still offered to every other run (ACC-04-43, ACC-04-37). Done when: both specs pass andagent-run.service.spec.tspasses unchanged. -
T56 (P1, lands with T19). Activity feed-kind and the pinned enum count. Modify
packages/agent/src/activity-log/feed-kind.ts— add[ActivityActionType.APP_PROVISION]: 'work'in a commented "App Works (APW-04)" block. Modifypackages/agent/src/entities/__tests__/activity-log.types.spec.ts— add the['APP_PROVISION', 'app_provision']pair tocasesand recount the pinnedtoHaveLength(...)(:410) to the merged enum (200 ondevelop@873274c9f, plus every member APW-01/03/05/07 add before this lands), with the recount recorded in the comment. Test:pnpm --filter @ever-works/agent test -- feed-kind activity-log.types— everyActivityActionTypemember resolves to a feed kind,app_provisioniswork, and afailedrow is aproblem(FEED_PROBLEM_STATUSES,feed-kind.ts:35-38) (ACC-04-27). Done when: both specs pass without a magic-number edit elsewhere; no change is needed inpackages/agent/src/shared-views/publishable-activity.ts(NEVER_PUBLISH_ACTIVITY_ACTIONSis derived,:28-32). -
T57 (P1, lands with T10/T18). Guard base spec for an invalid pre-seeded spec. Modify
packages/agent/src/app-provisioning/provision-output.guard.tsandpackages/agent/src/app-provisioning/app-provisioning-step-runner.ts— the base forpreservedFieldChangedis the last valid applied spec merged with APW-01'ssourceRepository(plan §3.2); with an absent or invalid head the base is{ source }plus schema-safe defaults (upstreamPullRequests.requireApproval: true,display.protectedPathsfrom the Blueprint or empty,provisioning.autoReprovision: false). Test: extend the T10 guard spec and the T18 step-runner spec — the injection fixture's pre-seededupstreamPullRequests.requireApproval: falseis not inherited (ACC-NEG-05), the proposal that drops it passes, and a genuine narrowing of a platform-owned field still failspreservedFieldChanged(ACC-04-09). Done when: both specs pass and ACC-NEG-05's e2e lane stays green. -
T58 (P1, lands with T24). Declined automatic start. Modify
packages/agent/src/app-provisioning/app-provisioning.service.ts—start({ trigger: 'auto-create' })is a no-op (no row, no Task, noapp.provision.started) whensourceRepository.autoProvision === false; manual, chat and endpoint starts ignore the flag. Test: extendpackages/agent/src/app-provisioning/__tests__/app-provisioning.auto-start.spec.ts— a declined creation provisions nothing, the card is Not started, and a later manualPOST /provisionreturns 202 (ACC-04-02, ACC-04-01). Done when: the spec passes and APW-01'ssourceRepository.autoProvisionfield is read (never re-derived). -
T59 (P1, lands with T14). Private data repository refusal. Modify
packages/agent/src/app-provisioning/app-provision-sandbox.tsandpackages/agent/src/app-provisioning/app-provisioning.service.ts— readiness gainspublicRepository(read from APW-02'sIGitProviderPlugin.getRepositoryvisibility); a private copy or aprivate/internallink builds no mount, mints no token, writes no row, and shows the private-repository state (spec §6). Test: extendpackages/agent/src/app-provisioning/__tests__/app-provision-sandbox.spec.tsand the controller spec — a private copy and a private link each give readinesspublicRepository: false,422 provisioningUnavailablewithmissing: ['publicRepository'], zero Run/Task/PR and zero mint calls (ACC-04-40). Done when: both specs pass and no code path callsGitFacadeService.getInstallationTokenForOwnerfor a provisioning mount. -
T60 (P1, lands with T30–T32). Catalog drafts: agent template companions, manifest rows and the eval package. Create under
agent-template-draft/:prompts/system.md,prompts/tasks/provision-repository.md,prompts/tasks/fix-verification.md,kb/playbooks/detection-order.md,kb/playbooks/env-classification.md,kb/playbooks/bootstrap-risks.md,README.md,icon.svgandmanifest-row.json— every path the drafted.works/agent.ymlpoints at, soever-works/agents'scripts/validate.jscan pass on the pull request. Createskill-draft/manifest-row.json(incl.sourceUrl) andskill-draft/README-attribution.md. Create undereval-draft/:app-provisioner.yml,expectations.jsonandfixtures/<case>/tree.mdfor the eight cases of plan §11.4. Test: the drafts are consumed by T30, T31 and T32 — this task's own check is that every path referenced byagent-template-draft/.works/agent.yml, bymanifest-row.jsonand byeval-draft/app-provisioner.ymlexists in this folder (rgover the drafts, asserted by the T30/T32 review checklist since the catalog repository is outside this monorepo). Done when: T30, T31 and T32 can be executed from these drafts without inventing a file.
Definition of Done
- Every checkbox above is ticked for the phases being shipped.
pnpm format:check,pnpm lint,pnpm type-check,pnpm testandpnpm buildare green from the repo root.- The sandbox isolation live spec (T4) is green on the nightly lane and its unrestricted control was shown to reach the unlisted host.
packages/tasks/src/__tests__/agent-task-execute.task.spec.ts,packages/agent/src/agents/__tests__/agent-templates.service.spec.tsand the task-workspace specs pass unchanged.- Every ACC-04 criterion in spec §8 (ACC-04-01…ACC-04-44) for the shipped phases has a named test above or a walked acceptance step.
- No secret value appears in any fixture, snapshot, log assertion, Activity row, telemetry payload or PR comment test.
- No test file lives under
apps/api/test/; every shared contract lives inpackages/contracts/src/apps/(R-1); no proposal or brief names a builder forauto.