Feature Specification: Plugins Capabilities (HTTP Surface)
Feature ID: plugins-capabilities
Branch: docs/spec-plugins-capabilities
Status: Implemented
Created: 2026-05-08
Last updated: 2026-05-08
Owner: Ever Works Team
1. Overview
The plugins-capabilities HTTP surface exposes the platform's six capability
families — deploy, search, screenshot, oauth, git-provider,
device-auth — to authenticated users through /api/<capability>/*
endpoints. Each sub-module is a thin HTTP shell that resolves the right
plugin via the matching agent-package facade, applies a four-level settings
cascade (work → user → admin → environment), validates the user's
permission against the work, and returns an envelope with status: 'success' | 'pending' | 'partial' | 'error'. The plugin system itself is documented
in plugin-system; this feature pins the user-facing
HTTP contract on top of it.
2. User Scenarios
2.1 Primary scenarios
- Given I have configured Tavily as my search provider, when I
POST /api/search/, then results from Tavily are returned with the provider name in the envelope. - Given I have a Vercel deploy token configured for my work,
when I
POST /api/deploy/works/:id, then the website deployment is dispatched, the verifier polls until it reaches a terminal state, and an activity-log entry pins the dispatch withactionType: DEPLOYMENT, action: 'work.deployed'. - Given I have OAuth-connected my GitHub account, when I
GET /api/git-providers/github/repositories?page=1&perPage=30, then the response is a paginated list of repositories with the user's permissions on each. - Given I have a screenshot plugin enabled (e.g. ScreenshotOne) and
configured for my work, when I
POST /api/screenshot/capture, then the response carries either acacheUrl(provider-hosted) or a base64-encodedimageBase64payload. - Given I want to connect a new OAuth provider, when I
GET /api/oauth/:providerId/connect/url, then the response carries a ready-to-redirect authorization URL with state. - Given a plugin requires device-auth (e.g. Claude Code OAuth),
when I
POST /api/device-auth/:pluginId/start, then the response carries the device-flow code and verification URL the user needs to paste into the provider's UI.
2.2 Edge cases & failures
- Given no search plugin has all required settings filled,
when I
GET /api/search/check-availability, then the response is{available: false, activeProvider: null, message: <reason>}— the message distinguishes "none enabled" from "enabled but unconfigured". - Given I am NOT the work creator AND the work owner has not
configured a deploy token, when I
POST /api/deploy/works/:id, then I get aBadRequestwith the message"The work owner has not configured <Provider> credentials."(i.e. the message points at the owner, not at me). - Given the deploy verifier hits its 13-minute hard cap with no
terminal state from the provider, when the timeout fires, then
the verifier emits
DeploymentFailedEventwithterminalState: 'TIMEOUT', persistsdeploymentState: 'TIMEOUT', and theActivityLogListenerrecords a failed entry. - Given I trigger a second deploy for the same work while a
verification poll is still in flight, when
startVerificationfires, then the prior verifier is cancelled (CANCELEDterminal event emitted exactly once via theterminatedidempotency guard) and a fresh poll loop replaces it in the in-memory queue. - Given the GitHub workflow dispatch fails on the first attempt,
when
dispatchWithRetryruns, then the service updates the website repository, writes a.deployment-triggerfile, commits + pushes, waits 3 seconds, and retries each candidate workflow filename; if both passes fail, the controller returnsBadRequestand NODeploymentDispatchedEventis emitted. - Given a screenshot capture returns
success: falsefrom the provider, when the controller runs, then it throwsBadRequestwith the provider'serrorfield (or"Failed to capture screenshot"fallback) — the response is NEVER{success: false}inside a 200. - Given my OAuth callback
codeis missing or empty, when IGET /api/oauth/:providerId/callback/plugins?state=…, then the controller returnsBadRequestwith{status: 'error', message: 'Authorization code is required'}BEFORE any service call. - Given I request
/api/git-providers/:providerId/userand my OAuth token has been revoked upstream, whengetUserthrows, then the controller returns{success: false, error: <message>}inside a 200 (NOT a 4xx) — the success/error envelope is the contract. - Given a batch deploy with 7 works, when
POST /api/deploy/batchruns, then the service runs them in two rolling batches of 5 with a 2-second sleep between batches; the controller returnsstatus: 'success' | 'partial' | 'error'based on the success/fail counts.
3. Functional Requirements
3.1 Cross-cutting
- FR-1 Every endpoint under
/api/{deploy,search,screenshot,oauth,git-providers,device-auth}/*MUST be wrapped by the globalAuthSessionGuardand resolve the user via the@CurrentUser()decorator. - FR-2 Every endpoint MUST return one of the four envelope shapes:
{status: 'success', …},{status: 'pending', …},{status: 'partial', …}, or{status: 'error', message: …}(the last one inside aBadRequestExceptionbody). - FR-3 Every controller MUST delegate provider resolution to its
matching agent-package facade (
DeployFacadeService,SearchFacadeService,ScreenshotFacadeService,OAuthFacadeService,GitFacadeService) and the per-userPluginRegistryService/PluginSettingsService— controllers MUST NOT reach into a specific plugin directly except through the registry. - FR-4 Every facade lookup MUST honour the four-level settings
cascade: work → user → admin → environment, with environment
variables resolved via the
x-envVarextension on the JSON Schema. Settings flaggedx-secret: trueMUST NOT be returned in any response or activity-log entry. - FR-5 Endpoints that touch a specific work MUST validate ownership
via
WorkOwnershipService.ensureCanEdit(mutating) or.ensureCanView(read-only) BEFORE the service call. If the user is NOT the creator, facade calls MUST be made withuserId = work.user.id(the owner's scope), not the caller'suserId.
3.2 Deploy (/api/deploy/*)
- FR-6
GET /api/deploy/providersMUST return{status, providers: getAvailableProvidersForUser(userId)}— per-user list (not the global registry). - FR-7
GET /api/deploy/providers/:providerId/configuredMUST return a four-flag envelope:{configured, available, enabled, message}distinguishing provider not registered, registered but disabled, and enabled but missing settings. - FR-8
POST /api/deploy/works/:idMUST run the chainensureCanEdit → isConfigured → validateToken → deploy → startVerification → activityLogService.log({action: 'work.deployed'}). Failure at any step short-circuits with aBadRequest. - FR-9
POST /api/deploy/works/:idMUST emit a fire-and-forget activity-log entry withactionType: DEPLOYMENT, action: 'work.deployed', status: COMPLETED, summary: 'Triggered deployment for <work.name> via <providerName>'ONLY whendeploymentInitiated === true. - FR-10 The deploy service MUST set the four required GitHub Actions
secrets on every dispatch (
TENANT_ID = work.id,DATA_REPOSITORY = work.getDataRepo(),<PROVIDER>_TOKEN = deployToken,DEPLOY_TOKEN = deployToken) and one repository variable (DEPLOY_PROVIDER = work.deployProvider || 'ever-works').DEPLOY_PROVIDERis set as a variable, not a secret, because GitHub Actions templates need it available inif:conditions. - FR-11 The deploy service MUST set two optional secrets when their
inputs are non-empty:
DEPLOY_TEAM_SCOPE = teamScopeandGH_TOKEN = gitToken. - FR-12 The deploy service MUST always generate and set a fresh
CRON_SECRET(32 bytes, hex-encoded) on every dispatch — the existing value is replaced, not preserved. - FR-13 The deploy service MUST call the optional plugin contract
plugin.getDeploymentSecrets(settings)if defined; the returnedRecord<string, string>MUST be pushed as additional GitHub Actions secrets. Failure insidegetDeploymentSecretsMUST be caught and logged; it MUST NOT abort the dispatch. - FR-14 The deploy service MUST resolve the workflow filenames to
dispatch via
plugin.getWorkflowFilenames()if defined, falling back to['deploy_prod.yaml']for legacy plugins. Each filename is tried in order; the first successful dispatch wins. - FR-15 When the first dispatch pass fails, the deploy service MUST
update the website repository via
WebsiteUpdateService.updateRepository, write.deployment-trigger(file content: a single ISO-timestamp line), commit + push it, wait 3 000 ms, then retry the dispatch loop exactly once. - FR-16 On successful dispatch, the deploy service MUST emit
DeploymentDispatchedEventwith payload{work, userId, providerId, providerName: plugin.providerName ?? plugin.name ?? plugin.id}. On failure, NO event is emitted. - FR-17
POST /api/deploy/batchMUST runensureCanEditfor each item BEFORE calling the service, run the underlying deploys in rolling batches ofMAX_CONCURRENT = 5with a2 000 mssleep between batches, then start verification for every result whosestatus === 'pending'. - FR-18
POST /api/deploy/batchMUST emit a single fire-and-forget activity-log entry withaction: 'deployment.batch_started', summary: 'Triggered batch deploy for <N> works', details: {workIds: [...]}regardless of per-work outcome. - FR-19
POST /api/deploy/batchMUST coerce the response status from the success/fail counts:failed === 0→'success';successfullyStarted > 0→'partial'; otherwise →'error'. - FR-20 The deployment verifier MUST poll
deployFacade.lookupExistingDeploymentevery 10 seconds, with two hard caps:FETCH_LIMIT = 18consecutivefound: falseresponses before declaringTIMEOUT, and a wall-clock cap of 13 minutes. - FR-21 The deployment verifier MUST persist intermediate
deploymentStatevalues (INITIALIZING,BUILDING,QUEUED, …) viaWorkRepository.updateand the resolvedwebsiteURL on every poll cycle that returns one. - FR-22 The deployment verifier MUST emit exactly one terminal
event per verification run:
DeploymentCompletedEventfor'READY'; otherwiseDeploymentFailedEventwithterminalState ∈ {'ERROR', 'TIMEOUT', 'CANCELED', 'UNKNOWN'}. Theterminatedboolean guard prevents duplicate emissions whencancel()and a poll resolution race. - FR-23 When
startVerificationis called for a work that already has an active poller inqueue, the active poller MUST be cancelled (emittingCANCELED) before the new one is registered. - FR-24 The deploy controller's domain endpoints (
GET/POST/DELETE/works/:id/domains[/:domain[/verify]]) MUST short-circuit with aBadRequestwhenwork.websiteis empty (i.e. before anydeployFacade.*Domaincall), with the message"No deployment exists for this work. Deploy first before managing domains.". - FR-25 The deploy controller's
addDomainbody MUST validate thedomainfield against the regex/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/and reject malformed input withBadRequestBEFORE controller code runs.
3.3 Search (/api/search/*)
- FR-26
GET /api/search/check-availabilityMUST resolve theactiveProviderby enumerating SEARCH-capability plugins viapluginRegistry.getEnabledPluginsScoped(SEARCH, undefined, userId), sortingdefaultForCapabilities-first, and returning the FIRST plugin whose required settings are all populated. - FR-27 Required-settings checks MUST skip fields flagged
x-envVarorx-adminOnlyand treatundefined/null/""as missing. - FR-28
GET /api/search/check-availabilityMUST distinguish two failure messages:"Search plugins are enabled but none have all required settings configured (e.g. API key)."(some enabled, none configured) and"No search provider is enabled. Enable a search plugin (e.g. Tavily, Linkup, Brave, Exa) in settings."(none enabled). - FR-29
POST /api/search/MUST runresolveConfiguredProviderBEFORE forwarding tosearchFacade.search, throwing aBadRequestwith"No search provider with all required settings configured is available."if no provider resolves. - FR-30
POST /api/search/MUST forward the resolved provider viaproviderOverrideso the facade short-circuits its own resolution step. - FR-31
searchFacade.searcherrors of typeNoProviderErrorMUST be remapped to"No search provider configured. Enable a search plugin in settings."with status'error'. All other Errors MUST surface theirmessage; non-Error throws MUST coerce to"Search failed".
3.4 Screenshot (/api/screenshot/*)
- FR-32
GET /api/screenshot/check-availability?workId=…MUST enumerate all enabled SCREENSHOT plugins (scoped byworkIdif provided), evaluate each one's configured-status against fully resolved settings, and return them asProviderOption[]withavailabletrue iff at least one is configured. - FR-33 Provider list ordering MUST be the three-tier sort:
default-first (active per
getDefaultForCapabilityScoped, elsedefaultForCapabilitiesflag, elsesystemPlugin), configured-second, alphabetical-by-name third. - FR-34
activeProviderMUST resolve as: matching the active plugin id → matching theisDefaultflag →null. - FR-35
POST /api/screenshot/captureMUST short-circuit withBadRequest"No screenshot provider configured"when no provider in the resolved list is configured. - FR-36
POST /api/screenshot/captureMUST forward the full DTO (url, viewport sizing,format,fullPage,delay, ad/tracker/cookie blocks) plus{userId, workId, providerOverride}context toscreenshotFacade.capture. - FR-37
POST /api/screenshot/captureMUST preferresult.cacheUrloverresult.imageUrlwhen constructing the responseimageUrl. Whenresult.imageBufferis present, returnimageBase64 = result.imageBuffer.toString('base64'); otherwiseimageBase64: null. - FR-38
POST /api/screenshot/captureMUST remapNoProviderErrorto"No screenshot provider configured or available"and rethrow every other error unchanged. - FR-39
POST /api/screenshot/get-urlMUST follow the same resolution + error-mapping contract ascapture, but return only{status, imageUrl}and rejectnull/emptyimageUrlwith aBadRequest"Failed to generate screenshot URL".
3.5 OAuth (/api/oauth/*)
- FR-40
GET /api/oauth/providersMUST return{configured: oauthFacade.isConfigured(), providers: getAvailableProviders()}so the UI can grey out disabled providers. - FR-41
GET /api/oauth/:providerId/connectionMUST resolve the user's connected provider account viaAuthAccountRepository.findConnectedProviderAccount(userId, providerId, {usePluginProviderId: true}), and validate the cachedaccessTokenby callingoauthFacade.getAuthenticatedUser(providerId, accessToken). Failure to resolve a user MUST coerce toconnected: false(NOT a 4xx). - FR-42
connectionSourceMUST be derived from the account'sproviderIdprefix:plugin:→'plugin', otherwise'social'. - FR-43
GET /api/oauth/:providerId/connect/urlMUST generate a 16-byte hexstatewhen none is supplied, resolve OAuth credentials from plugin settings (clientId, clientSecret, redirectUri, scopes), and reject withBadRequestwhenclientIdorclientSecretis absent. - FR-44
forceConsentMUST be parsed strict-'true': only the literal string'true'enables it;'false'/''/undefined/'TRUE'all map tofalse. - FR-45
GET /api/oauth/:providerId/callback/pluginsMUST short- circuit withBadRequest"Authorization code is required"BEFORE any service call whencodeis missing or empty. - FR-46
handleOAuthCallbackMUST upsert the resolvedAuthAccountrow withproviderId = buildPluginProviderId(providerId)(i.e. with theplugin:prefix),accountId = oauthUser.id, and populateaccessToken,refreshToken,tokenType,scope,accessTokenExpiresAt = now + expiresIn * 1000,email,username,metadata: {oauthUserId, name, avatarUrl}. - FR-47
DELETE /api/oauth/:providerIdMUST return 204-styleundefinedon success; the OAuth facade is expected to revoke the upstream token. - FR-48 Any service-layer error in
getOrganizations/getRepositories/getUser/disconnectProviderMUST be wrapped into{success: false, error: <message>}inside a 200 response — these endpoints NEVER throw 4xx.
3.6 Git Provider (/api/git-providers/*)
- FR-49
GET /api/git-providersMUST return{configured: gitFacade.isConfigured(), providers: getAvailableProviders()}. - FR-50
GET /api/git-providers/:providerId/connectionMUST resolve auth in two parallel branches: (a) OAuth viaAuthAccountRepository.findConnectedProviderAccount(usePluginProviderId: true); (b) PAT viagitFacade.hasValidCredentials({userId, providerId}). The user-resolution call differs by branch: OAuth-branch passes{providerId, token: accessToken}; PAT-branch passes{userId, providerId}. The response reportsauthMethodaccordingly:'oauth'when an OAuth token is present (even if user-resolution fails), else'personal-access-token'. - FR-51
GET /api/git-providers/:providerId/repositoriesMUST parsepageandperPagequery params withparseInt(_, 10)each independently undefined-able, and forward them as the second/third positional arguments togitFacade.listRepositories. - FR-52 Like the OAuth controller, every git-provider response
endpoint MUST wrap service errors in
{success: false, error: <message>}inside a 200 — NEVER 4xx.
3.7 Device Auth (/api/device-auth/*)
- FR-53
GET /api/device-auth/:pluginId/statusandPOST /api/device-auth/:pluginId/startMUST be thin two-line passthroughs topluginOperationsService.getPluginDeviceAuthStatus(pluginId, userId)/startPluginDeviceAuth(pluginId, userId)respectively. Errors MUST propagate unwrapped. - FR-54 Both endpoints MUST forward
(pluginId, userId)in positional order — NOT(userId, pluginId)— because the underlying service expectspluginIdfirst.
4. Non-Functional Requirements
- Performance: provider resolution and settings cascade are
in-memory after the first lookup per request via the
PluginRegistryServicecache. Search and screenshot capture latency is dominated by the upstream provider, not the resolver. - Reliability: the deploy verifier survives short upstream
outages: a single
lookupExistingDeploymentfailure short-circuits tocleanup('ERROR'), but transient provider blips during the 10-second poll window are absorbed by the next tick. The verification queue is in-memory only — process restarts cancel in-flight verifications (documented limitation; see Open Questions). - Security:
- All endpoints behind
AuthSessionGuard; ownership enforced viaWorkOwnershipServiceon every per-work mutation. - Plugin-supplied
getDeploymentSecretsoutput is pushed into GitHub Actions secrets viasetActionSecret, NEVER logged. Even the count is logged as a number, not the keys. CRON_SECRETis regenerated on every deploy — old workflows cannot reuse stale values.- OAuth tokens persist in
auth_accounts.accessToken/refreshToken; rotation is the OAuth provider's responsibility (we re-upsert on each successful exchange). - Domain regex on
addDomainrejects path/scheme/whitespace payloads upfront.
- All endpoints behind
- Observability: - Deploy:
DeploymentDispatchedEvent,DeploymentCompletedEvent,DeploymentFailedEventare emitted via NestJSEventEmitter2; theActivityLogListenertranslates them intoactionType: DEPLOYMENTrows. The single'work.deployed'activity-log entry is emitted directly from the controller (fire-and-forget). - Search/screenshot/OAuth/git-provider/device-auth: NO activity-log emission (per-call CRUD-style traffic is too high-volume to audit). - Errors are logged via the per-serviceLoggerwith the work id and provider id where applicable. - Compatibility: facade APIs are versioned via the
@ever-works/agentpackage. New plugin contract methods (getDeploymentSecrets,getWorkflowFilenames) are optional — older plugins continue to work via the fallback paths.
5. Key Entities & Domain Concepts
| Entity / concept | Description |
|---|---|
| Capability | One of: deploy, search, screenshot, oauth, git-provider, device-auth. Maps 1:1 to a sub-module under apps/api/src/plugins-capabilities/. |
| Capability facade | Agent-package service that resolves the right plugin and orchestrates the call (DeployFacadeService, SearchFacadeService, …). |
RepoContext | {owner, repo, token, publicKey} bundle used by DeployService to push secrets/variables to a single GitHub repo. |
RegisteredPlugin | Wrapper over a loaded plugin with manifest + state + plugin instance, returned by PluginRegistryService. |
ProviderOption | {id, name, description, configured, isDefault, icon} — the projected provider shape returned by /check-availability. |
DeploymentReadyState | 'BUILDING' | 'ERROR' | 'INITIALIZING' | 'QUEUED' | 'READY' | 'CANCELED' | 'TIMEOUT'. Authoritative for work.deploymentState. |
OAuthConnectionInfo | {connected, providerId, username?, email?, avatarUrl?, connectionSource: 'plugin' | 'social', authMethod?} envelope returned by /connection endpoints. |
GitProviderConnectionInfo | Same shape as OAuthConnectionInfo, plus authMethod ∈ {'oauth', 'personal-access-token'}. |
BatchDeployItemResultDto | {workId, slug, status: 'pending' | 'error', message, owner?, repository?} per-work record returned in the batch envelope. |
BatchDeployResponseDto | {status: 'success' | 'partial' | 'error', message, totalRequested, successfullyStarted, failed, results[]}. |
DeviceAuthStatus | {state, userCode?, verificationUri?, expiresAt?, …} envelope returned by the device-auth flow. |
6. Out of Scope
- The plugin system itself (registration, discovery, loading,
settings cascade) — see
plugin-system. - The plugin-management HTTP surface (
/api/plugins/*) for enable/disable/configure operations — see theplugins.controllerspec line inCOVERAGE-TRACKER.md. - The content-extractor capability — currently has no dedicated HTTP controller; surfaces via the work generation pipeline only.
- The AI-provider / pipeline / prompt-provider capabilities —
surfaced via
apps/api/src/works/*andapps/api/src/plugins/*, not underplugins-capabilities/. - Persistent verification queue across restarts (current queue is in-memory; see OQ-3 for the planned migration).
7. Acceptance Criteria
- All 6 capability sub-modules are mounted in
api.module.ts(DeployModule,SearchModule,ScreenshotModule,GitProviderModule,OAuthModule,DeviceAuthModule). - All endpoints are protected by
AuthSessionGuardand resolve the user via@CurrentUser(). -
WorkOwnershipService.ensureCanEdit/.ensureCanViewis the single source of truth for per-work permission gating. - Per-controller specs cover every endpoint:
deploy.service.spec(capability contract),search.controller.spec,screenshot.controller.spec,oauth.controller.spec/oauth.service.spec,git-provider.controller.spec/.service.spec,device-auth.controller.spec/.service.spec. - Plugin-driven workflow filenames + plugin-driven extra deploy
secrets ship behind the optional contract methods (
getWorkflowFilenames,getDeploymentSecrets); legacy plugins fall back to defaults (['deploy_prod.yaml'], no extras). -
DeploymentDispatchedEvent/DeploymentCompletedEvent/DeploymentFailedEventare emitted viaEventEmitter2; theActivityLogListeneris the only consumer the deploy services know about (no hard dependency onActivityLogServicefromDeployService/DeploymentVerifierService). - Per-controller spec exists for the deploy controller (status: MISSING — see follow-up T-DEPLOY-CTRL).
8. Open Questions
[NEEDS CLARIFICATION: should the deploy verifier persist its in-memory queue across API restarts via Trigger.dev so a redeploy doesn't silently abandon in-flight verifications?]— see OQ-3.[NEEDS CLARIFICATION: should the OAuth/git-provider/device-auth controllers ever return 4xx responses for downstream errors, or is the{success: false, error}-in-200 envelope the canonical contract?]— current code is asymmetric (deploy/search/screenshot throw 4xx; OAuth/git-provider/device-auth wrap into 200).[NEEDS CLARIFICATION: should the search endpoint exposeproviderOverridefrom the DTO instead of always falling through toresolveConfiguredProvider?]— search currently has no per-call provider override; screenshot does.[NEEDS CLARIFICATION: should the deploy controller emit a separate activity-log entry onvalidateTokenrejection so users can see failed-credentials attempts in the audit trail?]— currently no log entry untildeploymentInitiated === true.[NEEDS CLARIFICATION: should/api/deploy/teams(the workless variant) be removed since it always returns{teams: []}and redirects users to the work-specific endpoint?]— see OQ-1.
9. Constitution Gates
- I (Plugin-first): every capability is implemented via plugins
under
packages/plugins/. Controllers are thin shells over facades; no provider-specific logic leaks intoapps/api/. - II (Capability-driven resolution): every endpoint resolves
through a facade + the
PluginRegistryServicecapability lookup. - III (Source-of-truth repos): deploy writes via
WebsiteUpdateServiceto the website repo; OAuth/git-provider write toauth_accounts(not user repos). - IV (Long-running work via Trigger.dev): NOT applied here — verification is in-memory polling. Flagged in OQ-3 for future migration to a durable Trigger.dev task.
- V (Schema changes ship as forward-only migrations): no
schema changes —
auth_accountsandworksalready exist. - VI (Tests accompany the change): 5 controller specs + 4 service specs ship with this surface; deploy controller spec is flagged as a follow-up.
- VII (Secrets handled per
x-secretrules): plugin settings withx-secret: trueare read withincludeSecrets: trueonly on the server, never echoed in responses; deploy-time provider-supplied extras (getDeploymentSecrets) are pushed into GitHub Actions secrets via the GitHub plugin's libsodium-encryptedsetActionSecret, never logged. - VIII (Plugin counts touch the canonical doc only): not applicable — this spec discusses the HTTP surface, not the plugin catalog.
- IX (Behaviour-first): §§ 1–4 are observable behaviour;
implementation details (exact class names, file paths, package
mappings) live in
plan.md. - X (Backwards-compatible API/SDK/schema changes): optional
plugin contract methods (
getWorkflowFilenames,getDeploymentSecrets) preserve compatibility with older plugins.
10. References
- Implementation:
apps/api/src/plugins-capabilities/ - Related feature:
plugin-system(registry, discovery, settings cascade) - Related feature:
auth-jwt-oauth(AuthSessionGuard,AuthAccountRepository) - Related feature:
activity-log(ActivityLogListenerfor deploy events) - Related feature:
integrations-github-app(alternative GitHub auth path) - Constitution:
.specify/memory/constitution.md - Coverage tracker:
COVERAGE-TRACKER.md