Task Breakdown: Skills shelf
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: AW-08-skills-shelf
Spec: ./spec.md · Plan: ./plan.md
Status: Draft
Last updated: 2026-09-06
How to use
- Tasks are sequential by default.
(parallel)means it may run alongside its predecessor. - Every task names the exact files to create or modify. An implementer should never have to guess a path.
- "Done when" is stated explicitly for every task and is checkable without reading the diff.
- Add new tasks at the bottom rather than renumbering.
- Phase boundaries are ship boundaries:
developmust be green and deployable at the end of each phase. - Repo commands run from the monorepo root unless a task says otherwise. Migrations are
authored from
apps/api/.
Phase P1 — The shelf reads true
Delivers spec FR-1…FR-32 and FR-51…FR-62: the shelf, tags, the off switch, readiness badges, the requirements enumeration, the detail panels, and the hourly sweep. No repair, no capture.
P1.1 — Contracts
-
T1. Readiness and tag contracts. Create
packages/contracts/src/skills/readiness.tswithSKILL_READINESS_STATES,SkillReadinessState,SKILL_CARD_STATES,SkillCardState,SkillRequirementKind,SkillRequirementStatus,SkillRequirement,SkillReadinessDetail, and the constantsSKILL_TAG_MAX_LENGTH = 40,SKILL_TAGS_PER_SKILL_MAX = 12,SKILL_TAG_FACET_LIMIT = 200,SKILL_TAG_CHIPS_SHOWN = 12,SKILL_TAG_FILTER_MAX = 6,SKILL_READINESS_TTL_MS = 3_600_000,SKILL_READINESS_SWEEP_BATCH = 500,SKILL_READINESS_SWEEP_PER_USER = 200,SKILL_CAPTURE_BODY_MAX_CHARS = 16_000,SKILL_CAPTURE_BODY_MIN_CHARS = 200,SKILL_CAPTURE_BUDGET_MS = 90_000(exact shapes in plan §3.3). Modifypackages/contracts/src/skills/index.tsto addexport * from './readiness.js';. Test: extendpackages/contracts/src/skills/__tests__/with a spec pinning the two state unions (so a state cannot be added without a deliberate edit) and asserting every numeric constant. Done when:pnpm --filter @ever-works/contracts buildemits declarations andimport { SkillCardState } from '@ever-works/contracts'resolves fromapps/api. -
T2 (parallel with T1). Export the first-party provider id from its own plugin. Modify
packages/plugins/everworks-skills/src/index.tsto exportexport const EVERWORKS_SKILLS_PROVIDER_ID = 'everworks-skills';(reusing whatever literal the plugin already declares as its id — do not introduce a second copy). Test: extendpackages/plugins/everworks-skills/src/everworks-skills.plugin.spec.tswith an assertion that the exported constant equals the plugin's ownid. Done when: nothing outsidepackages/plugins/everworks-skills/contains the string literal'everworks-skills'after T13 lands (Constitution II).
P1.2 — Entity, table, migration
-
T3.
SkillTagentity. Createpackages/agent/src/entities/skill-tag.entity.tsexactly as specified in plan §3.2:id,skillId(@ManyToOne(() => Skill, { onDelete: 'CASCADE' })),userId(@ManyToOne(() => User, { onDelete: 'CASCADE' })),tag(varchar(40)),tenantId/organizationId(nullable uuid, no@ManyToOne— cycle avoidance, see the EW-654 note inpackages/agent/src/entities/user.entity.ts),createdAt. Indexes:uq_skill_tags_skill_tag(unique onskillId,tag),idx_skill_tags_user_tag,idx_skill_tags_skill. Modifypackages/agent/src/entities/index.ts— addexport * from './skill-tag.entity';next to the three existing skill exports. Modifypackages/agent/src/database/_entity-names.ts— add'SkillTag'toAGENT_ENTITY_NAMESin the Skills family block. Modifypackages/agent/src/database/_entities-inventory.ts— importSkillTagand add it toENTITIESbesideSkillFile. Test:packages/agent/src/entities/__tests__/skill-tag.entity.spec.ts— asserts the three index names, that both scope columns exist (soapps/api/src/scope/scope-stamping.subscriber.tswill stamp it), and thatcreatedAtuses the portable date column helper frompackages/agent/src/entities/_types.ts. Done when: the drift specs inpackages/agent/src/database/database.module.spec.tsanddatabase.config.spec.tspass without a magic-number edit. -
T4. Six additive columns on
Skill. Modifypackages/agent/src/entities/skill.entity.ts— appenddisabledAt(PortableDateColumn({ nullable: true })),readiness(varchar(24), default'unknown'),readinessDetail(simple-json, nullable),readinessCheckedAt(portable, nullable),reviewState(varchar(16), nullable),capturedFromRunId(uuid, nullable, no FK). Append the three new@Indexdeclarations from plan §3.1. Append only — never insert into the existing column order. Test:packages/agent/src/entities/__tests__/skill.entity.spec.ts(create if absent) — asserts the six columns, their defaults, and that no pre-existing column changed type. Done when:pnpm --filter @ever-works/agent buildis clean. -
T5. Migration + backfill. Create
apps/api/src/migrations/1791080000000-AddSkillShelfReadinessAndTags.ts. Generate the skeleton withcd apps/api && pnpm typeorm migration:generate -d typeorm.config.ts src/migrations/AddSkillShelfReadinessAndTags, then hand-write the tag backfill described in plan §3.4 — normalised in SQL, capped at 12 tags per skill,ON CONFLICT ("skillId", tag) DO NOTHING, branched onqueryRunner.connection.options.typeso SQLite dev/test databases skip the JSON expansion.readinesslands asNOT NULL DEFAULT 'unknown'— do not backfill it to'ready'.down()drops only whatup()created. Test:apps/api/src/migrations/__tests__/AddSkillShelfReadinessAndTags.spec.ts— the directory's naming convention is<MigrationName>.spec.ts. Asserts the migration is re-runnable (a secondup()inserts zero additional tag rows) and thatup()contains noDROP COLUMNor rename of a pre-existing column. Done when: a fresh database and a database with existing skills both migrate cleanly, and every pre-existing Skill readsreadiness = 'unknown'immediately after.
P1.3 — Repositories and the readiness service
-
T6.
SkillTagRepository. Createpackages/agent/src/database/repositories/skill-tag.repository.tswithreplaceForSkill(skillId, userId, tags, scope)(delete-then-insert inside the caller's transaction),findBySkillIds(skillIds, userId),facets(userId, scope, limit = 200)→Array<{ tag, count }>ordered count-desc then alphabetical, andskillIdsMatchingAll(userId, tags, scope)implementing AND semantics with aGROUP BY … HAVING count(DISTINCT tag) = :n. Createpackages/agent/src/database/repositories/skill-tag-normalize.ts— the pure normaliser (trim → lower-case → whitespace to single hyphen → strip anything outside[a-z0-9-]→ clamp to 40 → drop empties → dedupe → cap at 12, returning{ tags, dropped }). Modifypackages/agent/src/database/index.ts— addexport * from './repositories/skill-tag.repository';. Modifypackages/agent/src/skills/index.ts— re-exportSkillTagRepositoryalongside the other skill repositories. Test:packages/agent/src/skills/__tests__/skill-tags.spec.ts(normaliser table test: case, spaces, illegal characters, 40-char clamp, dedupe, 12-cap withdroppedreported) andpackages/agent/src/database/repositories/__tests__/skill-tag.repository.spec.ts(facet ordering, the 200 cap, AND semantics for 1/2/3 tags). Done when: both specs pass andpnpm --filter @ever-works/agent testis green. -
T7. Extend
SkillRepository.findByUserIdFiltered. Modifypackages/agent/src/database/repositories/skill.repository.ts— growListSkillsFilterwithtags?: string[],readiness?: SkillCardState | 'attention',provenance?,enabled?: boolean,sort?: 'updated' | 'name' | 'attention'. Add the tagINNER JOIN(only whentagsis present), the readiness predicate (derived: a request fordisabledmaps todisabledAt IS NOT NULL;needs_reviewtoreviewState = 'proposed';attentiontoNOT (readiness = 'ready' AND disabledAt IS NULL AND reviewState IS NULL)), the enabled predicate, the three sorts, and acountsByCardStatemethod returning the grouped counts for the summary line. Keep the existing escaped-LIKEsearch helper and extend theBracketsblock with a correlatedEXISTSoverskill_tagsso search also matches a tag (FR-3). Done when: today's call with no new filters produces the identical SQL shape it produces now, asserted by the golden-set test in T8. -
T8. Off switch in
resolveActive. Modifypackages/agent/src/database/repositories/skill-binding.repository.ts— add.andWhere('skill.disabledAt IS NULL')and.andWhere("(skill.reviewState IS NULL OR skill.reviewState <> 'proposed')"to theresolveActivequery builder. Nothing else in that method changes — not the OR-set, not the inject-flag predicates, not the ordering, not the dedupe. Test:packages/agent/src/database/repositories/__tests__/skill-binding.repository.disabled.spec.ts— a disabled Skill is excluded; aproposedSkill is excluded; a golden-set assertion that the full result for an untouched fixture is byte-identical to the pre-change result, so the new predicate provably narrows nothing else. Done when: the existingflow-skill-context-assembly.spec.tse2e still passes unchanged. -
T9.
SkillReadinessService. Createpackages/agent/src/skills/skill-readiness.service.ts. Constructor injects, all@Optional()so unit tests and runtimes without the policy module keep working:SkillBindingRepository,SkillRepository,AgentRepository,TOOL_GRANT_ENFORCER,CREDENTIAL_RESOLVER, the MCP connection repository, andPluginSettingsService.evaluate(skill, ctx): Promise<{ readiness, detail }>implements exactly the ladder in plan §2.1:- count bindings for the skill;
boundTargetCount === 0or every binding muted →needs_setup; - resolve the grant matrix for up to 10 agents in scope and call
filterSkillsByToolGrantsfrompackages/agent/src/policy/skill-activation.ts— import it, do not reimplement it; suppressed for every agent →blocked_by_access; - for each declared tool:
mcp__<server>__…→ look the connection up by name (missing/disabled); otherwiserequiredCredentialsForTool(name)frompackages/agent/src/policy/tool-credentials.ts→ askCredentialResolver.resolvefor the key set and diff the returned keys against the requested keys (never read a value); any plugin-backed capability → askSettingsSchemaValidatorServicewhich required keys are unset and report the key names; - any missing →
missing_requirements; elseready; - any thrown error in steps 2–3 → that requirement is
unknown; a total failure →unknownfor the whole skill. Neverreadyon an error. Truncatedetail.requirementsat 20 withtruncated: true. Modifypackages/agent/src/skills/skills.module.ts— provide and export it, and registerTypeOrmModule.forFeature([… SkillTag]). Modifypackages/agent/src/skills/index.ts— export the service. Test:packages/agent/src/skills/__tests__/skill-readiness.service.spec.tsandskill-readiness-precedence.spec.tscovering every branch listed in plan §10.1. Done when: no test in the suite can producereadyfrom a thrown dependency.
- count bindings for the skill;
-
T10. Wire readiness into the Skill write paths. Modify
packages/agent/src/skills/skills.service.ts:create,updateandinstallFromCatalogcallSkillTagRepository.replaceForSkill(...)inside the same transaction as the Skill write (FR-14), deriving tags fromfrontmatter.tagsthrough the T6 normaliser, and return thedroppedlist in the service result so the controller can report it (FR-10);- the same three methods, plus
createBindinganddeleteBinding, then callSkillReadinessService.evaluateand persist the three readiness columns; - add
enable(userId, id)/disable(userId, id)— set/cleardisabledAtviaSkillRepository.updateByIdAndUser(notupdateById— ownership must be in theWHERE), emit an activity row, and return the fresh card state. Both idempotent. Modifypackages/agent/src/entities/activity-log.types.ts— appendSKILL_ENABLED = 'skill_enabled'andSKILL_DISABLED = 'skill_disabled'toActivityActionType, next to the existingSKILL_*members. Appending only; the column is avarchar, so no migration is required for this. Test:packages/agent/src/skills/__tests__/skills.service.disable.spec.ts— idempotency; bindings unchanged in count, target, priority and both inject flags; the activity row carries actor + direction and contains no body text. Done when: creating a Skill with 15 tags stores 12 and reports 3 dropped.
-
T11. Reflect run-time suppression back onto the shelf. Modify
packages/agent/src/agents/agent-run.service.ts— inside the existingfor (const entry of suppressed)loop inresolveSkillsForRun(the loop that today only appends theWARNrun log), also fire a best-effort readiness write marking that Skillblocked_by_access. Use the samevoid … .catch(() => undefined)posture as the log append: a readiness write must never fail a run. Test:packages/agent/src/agents/__tests__/agent-run.skill-suppression.spec.ts— the write happens; a rejecting writer does not fail the run; theWARNlog is still appended. Done when: suppressing a Skill in a run changes its badge on the next shelf load.
P1.4 — API
-
T12. Extend the list DTO and response. Modify
apps/api/src/skills/dto/skill.dto.ts— growListSkillsQueryDtowithtags(comma-separated,@Transformtostring[],@ArrayMaxSize(6), each matching^[a-z0-9][a-z0-9-]{0,39}$),readiness,provenance,enabled,sort, each with@IsOptional()+@IsIn([...])from the T1 unions. Createapps/api/src/skills/dto/skill-shelf.dto.tswithSkillShelfRowDto(the extended row from plan §4.1) andSkillTagFacetDto. Done when: a request with 7 tags returns400with the copy from spec FR-12 and a request with none behaves exactly as today. -
T13. Provenance mapper. Create
apps/api/src/skills/skill-provenance.ts— a pure functionprovenanceOf(skill): 'firstParty' | 'plugin' | 'package' | 'authored'usingEVERWORKS_SKILLS_PROVIDER_IDimported from@ever-works/plugins-everworks-skills(T2) and thepkg:prefix for package sources. No string literal for a plugin id in this file. Test:apps/api/src/skills/skill-provenance.spec.ts— beside the file, matching this module's existing convention (skills.controller.spec.tssits beside its controller, not in a__tests__folder). Covers the four branches plus anull-source fallback. -
T14. Shelf endpoints on the skills controller. Modify
apps/api/src/skills/skills.controller.ts:- extend
GET /to pass the new filters through and to projectSkillShelfRowDto(tags viaSkillTagRepository.findBySkillIdsin one batched call,boundTargetCountvia one grouped count,cardStatederived fromreadiness+disabledAt+reviewState), and to returnmeta.counts; - add
GET /tags— declared before every:idroute, next to the existingGET invocablewhich carries the same comment; - add
POST :id/enable,POST :id/disable(@Throttle({ long: { limit: 60, ttl: 60_000 } })),GET :id/readiness,POST :id/readiness/refresh(@Throttle({ long: { limit: 30, ttl: 60_000 } })); @ApiOperationon every new method. Cross-workspace ids answer404on every verb, via the existingfindByIdAndUserpath. Modifyapps/api/src/skills/skills.module.ts— injectSkillReadinessServiceandSkillTagRepository. Test: extendapps/api/src/skills/skills.controller.spec.tsand createapps/api/src/skills/skills.controller.shelf.spec.tscovering everything in plan §10.2 for P1 — including the route-order case where a Skill's id is literallytags. Done when:cd apps/api && pnpm testis green and every new endpoint has a cross-workspace404assertion.
- extend
P1.5 — Web
-
T15. Typed client + page-data plumbing. Modify
apps/web/src/lib/api/skills.ts— extend theSkillmirror with the new fields, addtags,cardState,readiness,readinessDetail,provenance,boundTargetCount,openRepairTaskId; addskillsAPI.listTags(),skillsAPI.readiness(id). Modifyapps/web/src/lib/skills-page-data.ts— whitelisttags,readiness,provenance,enabled,sortinparseSkillsSearchParams(anything unknown is still dropped); fetch the tag facets inside the existingPromise.allinloadSkillsPageData; teachbuildSkillsHrefto serialise the five new params, still omitting defaults. Test: extendapps/web/src/lib/skills-page-data.unit.spec.ts— each new param parses, each malformed value falls back to the default, andbuildSkillsHrefround-trips. Done when:/agents?tags=billing&sort=attention#skillsreloads into the same view. -
T16.
SkillReadinessBadge. Createapps/web/src/components/skills/SkillReadinessBadge.tsx— pure presentational,SkillCardState→ icon + translated title + optional enumerated requirement list + optional action slot. Text label always rendered; colour is never the sole carrier (FR-55). Test:SkillReadinessBadge.unit.spec.tsx— all seven states render their title text; the enumerated list renders each requirement's kind, id and status; truncation rendersreadiness.truncated. -
T17.
SkillTagFilter. Createapps/web/src/components/skills/SkillTagFilter.tsx— 12 chips with counts, roving-tabindex (←/→move,Space/Entertoggle,Backspacedeselects),+{n} morepopover with its own search input, the 6-selection cap with the disabled seventh chip and its tooltip, and aClearcontrol that appears only when something is selected. Test:SkillTagFilter.unit.spec.tsx— selection AND-accumulates, the cap disables the seventh, the overflow popover filters, and the keyboard model works headlessly. -
T18.
SkillShelfCard. Createapps/web/src/components/skills/SkillShelfCard.tsx— title, description, tag pills, provenance chip, version, reach line, the optimistic on/off toggle (BFF call, revert on failure, idempotent), the badge, and the badge's primary action slot (a no-op placeholder in P1, filled by T26). Createapps/web/src/app/api/skills/[id]/enable/route.ts,apps/web/src/app/api/skills/[id]/disable/route.ts,apps/web/src/app/api/skills/[id]/readiness/route.ts— thin BFF proxies mirroring the existingapps/web/src/app/api/skills/[id]/files/route.ts. Test:SkillShelfCard.unit.spec.tsx— toggle optimism and revert; badge selection by card state; reach pluralisation;data-testidpresent for the e2e grid queries. -
T19.
SkillShelfand the summary line. Createapps/web/src/components/skills/SkillShelf.tsx— the grid, the sort control, the attention summary line (which is itself the attention filter), the three distinct empty states and the past-the-end state, and the URL sync using the samerouter.replace(basePath + params + hash)patternSkillsPageClientalready uses. Modifyapps/web/src/components/skills/SkillsPageClient.tsx—InstalledListbecomes a thin wrapper delegating toSkillShelf;updateUrllearns the five new params; the search input keeps its id and name and only its placeholder value changes. Theavailableandcustomsections are untouched. Modifyapps/web/src/components/skills/SkillsSection.tsx— pass the tag facets through. Done when: the shelf's badges are present in the server-rendered HTML (FR-8) — verify withcurlon the page, not just in the browser. -
T20. Detail-page panels. Create
apps/web/src/components/skills/SkillReadinessPanel.tsx— state, "Checked {ago}",Re-check(withuseTransition, keyboardRwhile focused), and the requirements table (kind · id · status), reusingSkillReadinessBadgeso the card and the panel can never disagree. Modifyapps/web/src/components/skills/SkillDetailClient.tsx— insert the readiness panel, the requirements table and a "Where it came from" block above the existing instructions section. Nothing below moves; the body editor, bindings, files and delete sections keep their current order, props and copy (FR-56). Modifyapps/web/src/app/[locale]/(dashboard)/skills/[id]/page.tsx— fetch the readiness alongside the existing parallel skill/bindings/files fetch. Done when: the existingskills.spec.tse2e still passes with no selector changes.
P1.6 — Background sweep
-
T21. Sweep dispatcher. Create
packages/agent/src/tasks/skill-readiness-sweep.types.ts(payload) andpackages/agent/src/tasks/skill-readiness-sweep-dispatcher.ts— interface +export const SKILL_READINESS_SWEEP_DISPATCHER = Symbol('SKILL_READINESS_SWEEP_DISPATCHER');followingpackages/agent/src/tasks/kb-reembed-work-dispatcher.tsexactly. Returnsstring | null(soft failure; the next tick recovers). Modifypackages/agent/src/tasks/index.ts— export both. Modifypackages/agent/src/tasks/_tasks-symbols.ts— add'SKILL_READINESS_SWEEP_DISPATCHER'toTASKS_BARREL_RUNTIME_SYMBOLS, alphabetically. Test:packages/agent/src/tasks/__tests__/skill-dispatchers.spec.ts— the token isSymbol(...)notSymbol.for(...), itsdescriptionmatches its name, and it is listed in the barrel inventory. Done when:packages/agent/src/tasks/tasks.spec.tspasses without a magic-number edit. -
T22. The scheduled sweep task. Create
packages/tasks/src/tasks/trigger/skill-readiness-sweep.task.ts— aschedules.taskon cron17 * * * *, modelled onpackages/tasks/src/tasks/trigger/kb-reconcile.task.ts. Select skills whosereadinessCheckedAtis null or older than 60 minutes, oldest first,LIMIT 500, with a per-userIdcap of 200 applied viaROW_NUMBER() OVER (PARTITION BY "userId"). Evaluate each and write the three readiness columns with an ownership-scopedUPDATE. Emitskill.readiness.sweep.completedwith{ scanned, changed, byState, durationMs }— counters only. Modifypackages/tasks/src/tasks/trigger/index.ts— export the task. Modify the API-side binding module that wires*_DISPATCHERsymbols onto the active job-runtime provider soSKILL_READINESS_SWEEP_DISPATCHERresolves throughbuildJobRuntimeProviderslike every other dispatcher (Constitution IV — no direct queue call, no vendor SDK import at a call site). Test: a unit spec beside the task asserting the cron string, both caps, and that a failure on one skill does not abort the tick. Done when: the cron does not collide withkb-reconcile(42 3) ormemory-consolidation-tick(37 8), and a local dispatch updates verdicts.
P1.7 — i18n, tests, docs
-
T23. i18n keys. Modify
apps/web/messages/en.json— add thedashboard.skillsPage.shelfanddashboard.skillsPage.readinesssub-trees from plan §8 verbatim. Then add the same keys to all 20 sibling locale files inapps/web/messages/. Every leaf key name is camelCase and contains no literal.— a dot in a leaf name is rejected at runtime and reds several e2e shards at once. Done when: a script grep for"[a-zA-Z]*\.[a-zA-Z]*":inside the new sub-trees returns nothing, andpnpm --filter web buildproduces no missing-message warnings. -
T24. P1 e2e. Create
apps/web/e2e/skills-shelf-badges.spec.ts,apps/web/e2e/skills-shelf-tags.spec.ts,apps/web/e2e/skills-shelf-toggle.spec.ts,apps/web/e2e/skills-shelf-empty-states.spec.ts,apps/web/e2e/skills-shelf-a11y.spec.ts— scenarios in plan §10.3. PrefergetByTestIdfor the card grid; reserve*ByRolefor the dialogs (role queries are the usual source of load-sensitive flakes in this workspace). Done when: the five new specs pass andskills.spec.ts,skills-list-filter.spec.ts,flow-skill-crud-scoping.spec.ts,flow-skill-bindings-deep.spec.ts,flow-skill-context-assembly.spec.tsandsec-pin-skills-scoping.spec.tsall still pass unchanged — that is the additive-only proof. -
T25. P1 ship gate. Run
pnpm format && pnpm lint && pnpm type-check && pnpm test && pnpm buildfrom the root. Update this file's status and tick the P1 boxes. Done when:developis green and the shelf is deployable with no repair and no capture.
Phase P2 — Repair
Delivers spec FR-33…FR-40: one-click repair, delegated repair as a Task, the single-open-repair guard and the permission-denied variant.
-
T26. Repair service. Create
packages/agent/src/skills/skill-repair.service.tswithrepair(userId, skillId, input, scope)implementing the five actions from plan §4.2:attach(create the binding through the existingSkillsService.createBinding, then re-evaluate readiness),unmute(flipinjectIntoAgentto true on the skill's bindings),enable,recheck, anddelegate.delegatecreates oneTask(missionId: null— the column is nullable), titleFix requirements for Skill: {title}, description enumerating every unmet requirement fromreadinessDetail,agentIdset to the chosen agent, then calls the same admission pathPOST /api/agents/:id/assign-taskuses so the run goes through the concurrency valve rather than around it. Single open repair Task is enforced by a scoped lookup for an open Task carryinglabelscontainingskill-repair:{skillId}— reusing the existingTask.labelscolumn rather than adding one.restart: truecancels the existing Task first. Modifypackages/agent/src/skills/skills.module.ts— provide and export it. Test:packages/agent/src/skills/__tests__/skill-repair.service.spec.ts— each action; the duplicate-binding conflict; the already-open guard;restartcancelling then reopening; a delegate whose run dispatch fails leaves the Task open. -
T27. Repair endpoint. Modify
apps/api/src/skills/dto/skill.dto.ts— addRepairSkillDtoexactly as in plan §4.2, with conditional validation (targetIdrequired unlesstargetType === 'tenant';agentIdrequired fordelegate). Modifyapps/api/src/skills/skills.controller.ts— addPOST :id/repairreturning202with{ kind, taskId?, runId?, bindingId?, readiness },@Throttle({ long: { limit: 10, ttl: 60_000 } }),@ApiOperation. Error codes exactly as tabulated in plan §4.2. Test: extendapps/api/src/skills/skills.controller.shelf.spec.ts— all five actions,409 repairInProgress,409 bindingExists, the permission-denied path, and a cross-workspace404. Done when: the endpoint returns in under 2 seconds without awaiting the run. -
T28. Repair and attach dialogs. Create
apps/web/src/components/skills/SkillRepairDialog.tsx— the enumerated missing items with a per-item deep link (fixTarget.surface→ route), the agent picker, theAsk {agentName} to fix thisbutton, the already-open variant with itsCancel that and start overaction, and the permission-denied variant that disables the action while keeping the item's name fully visible (FR-40). Createapps/web/src/components/skills/SkillAttachDialog.tsx— reusingloadBindingTargetOptionsActionfromapps/web/src/app/actions/skills.tsso the picker behaves identically to the one already on the Skill detail page. Modifyapps/web/src/app/actions/skills.ts— addrepairSkillActionwithrevalidatePathon the Agents route. Modifyapps/web/src/components/skills/SkillShelfCard.tsxandSkillReadinessPanel.tsx— fill the action slot left as a placeholder in T18/T20. Test:SkillRepairDialog.unit.spec.tsxandSkillAttachDialog.unit.spec.tsx— each variant renders;Esccloses and returns focus;Enterfires the primary action. -
T29. Repair i18n. Modify
apps/web/messages/en.json— add thedashboard.skillsPage.repairsub-tree from plan §8; mirror the keys into the 20 sibling locales. -
T30. P2 e2e. Create
apps/web/e2e/skills-shelf-repair.spec.ts— unbound Skill → Attach to… → badge clears in place; missing requirement → Ask an agent → Task created with the enumerated description → second attempt shows the already-open variant →restartreopens. Done when: the spec passes and no existing skill spec needed a selector change. -
T31. P2 ship gate. Root
format / lint / type-check / test / buildgreen; tick P2.
Phase P3 — Capture from a run
Delivers spec FR-41…FR-50: drafting a Skill from a completed run, the review state, and accept/discard with inline attach.
-
T32. Capture dispatcher. Create
packages/agent/src/tasks/skill-capture.types.tsandpackages/agent/src/tasks/skill-capture-dispatcher.ts—export const SKILL_CAPTURE_DISPATCHER = Symbol('SKILL_CAPTURE_DISPATCHER');, returningPromise<string>and propagating dispatch errors (a dropped capture strands aproposedplaceholder — the same reasoningKbReembedWorkDispatcherdocuments). Modifypackages/agent/src/tasks/index.tsandpackages/agent/src/tasks/_tasks-symbols.ts(alphabetical insertion). Test: extendpackages/agent/src/tasks/__tests__/skill-dispatchers.spec.ts. -
T33. Capture service. Create
packages/agent/src/skills/skill-capture.service.ts:start(userId, input, scope)— validates the run belongs to the caller and iscompleted, returns the existing draft id whencapturedFromRunIdalready matches (the partial unique index makes this a guarantee, not a race), otherwise creates the placeholder Skill row (reviewState: 'proposed', minimal body) and dispatches;applyDraft(skillId, draft)— renders the Markdown body in code from the structured draft (## When to use this/## Steps/## Edge cases) so the edge-cases section is guaranteed present, runsassertNoSecretsandassertNoInjectionTokensfrompackages/agent/src/utils/, enforces the 200-char floor and the 16,000-char ceiling, writes the body + tags (through the T6 normaliser, capped at 6 for a draft), then evaluates readiness;discardPlaceholder(skillId, reason)— deletes the row and appends oneINFOagent_run_logsline withstep: 'skill-capture'so the run page can render the outcome without a new table;accept(userId, skillId)— clearsreviewState, re-evaluates readiness,422when the Skill is notproposed. Test:packages/agent/src/skills/__tests__/skill-capture.spec.ts— body rendering, both size gates, secret and control-sequence rejection, the not-usable path creating no row, same-run idempotency, andaccepton a non-proposed Skill.
-
T34. The capture job. Create
packages/tasks/src/tasks/trigger/skill-capture-from-run.task.ts— reads theAgentRunplus up to 500AgentRunLogrows (the same capapps/api/src/agents/agents.controller.tsalready applies to run detail), fences the log text as untrusted input the waypackages/agent/src/services/memory-recall.tsfences recalled memory, callsAiFacadeService(never a provider SDK) for one structured completion{ title, whenToUse, steps[], edgeCases[], tags[] }, then callsSkillCaptureService.applyDraftordiscardPlaceholder.maxDuration90 s, 1 retry, emitsskill.capture.completed. Also handles the housekeeping sweep from plan §9.2: deleteproposedSkills with an empty body older than 24 hours (a stranded placeholder). Modifypackages/tasks/src/tasks/trigger/index.ts; wire the dispatcher throughbuildJobRuntimeProviderslike every other symbol. Test: a spec beside the task covering the fencing, the retry idempotency (a full replace keyed byskillId), and the deleted-mid-capture case (UPDATE … WHERE id AND userIdaffects 0 rows, job exits clean). -
T35. Capture and accept endpoints. Modify
apps/api/src/skills/dto/skill.dto.ts— addCaptureSkillFromRunDto. Modifyapps/api/src/skills/skills.controller.ts— addPOST /from-run(declared before every:idroute) returning202{ skillId, state: 'drafting' }with@Throttle({ long: { limit: 10, ttl: 3_600_000 } }), andPOST :id/acceptwith@Throttle({ long: { limit: 30, ttl: 60_000 } }). Test: extendapps/api/src/skills/skills.controller.shelf.spec.ts—422 runNotCompletedfor each non-completed status, same-run idempotency, cross-workspace404,422 notProposed. -
T36. Run-page action and capture dialog. Create
apps/web/src/components/skills/SkillCaptureDialog.tsx— scope picker (defaulting to the run's agent), optional title, optional emphasis, submit; then the inlineDrafting — this takes about a minute.state pollingGET /api/skills/:id/readinessevery 5 s for at most 120 s, reusing the poll cadenceSessionDetailClientalready runs rather than adding a second timer. Modifyapps/web/src/components/agents/SessionDetailClient.tsx— one header action, enabled only forrun.status === 'completed', with the disabled tooltip otherwise, and theView the Skill from this runvariant once a draft exists. Modifyapps/web/src/app/[locale]/(dashboard)/agents/sessions/[runId]/page.tsx— pass through whether a captured Skill already exists for this run. Modifyapps/web/src/app/actions/skills.ts— addcaptureSkillFromRunAction,acceptSkillAction,discardSkillDraftAction. Test:SkillCaptureDialog.unit.spec.tsx— enabled/disabled gating, the drafting state, the not-usable message, and poll teardown on unmount. -
T37. Review banner and accept flow. Create
apps/web/src/components/skills/SkillReviewBanner.tsx— theNeeds your reviewbanner withAccept/Discard, the accept dialog's inlineAttach to…variant when the Skill has no binding, and the discard confirmation. Modifyapps/web/src/components/skills/SkillDetailClient.tsx— render the banner above the readiness panel whenreviewState === 'proposed'; the body editor stays fully editable before accepting. Test:SkillReviewBanner.unit.spec.tsx— both accept paths and the discard confirmation. -
T38. Capture i18n. Modify
apps/web/messages/en.json— add thedashboard.skillsPage.capturesub-tree and the twodashboard.agentsPage.sessions.detail.saveAsSkill*keys from plan §8; mirror into the 20 sibling locales. -
T39. P3 e2e. Create
apps/web/e2e/skills-capture-from-run.spec.ts— completed run → save → drafting → draft badged Needs your review → the draft is absent from a new run's context → accept with inline attach → the Skill is live; plus the failed-run disabled variant and the not-usable message. Done when: it passes and no existing agent-session spec needed a selector change. -
T40. P3 ship gate. Root
format / lint / type-check / test / buildgreen; tick P3.
Cross-phase closing tasks
-
T41. Telemetry. Wire the ten events from plan §9.1 through the existing monitoring package, following the
this.posthog.capture({ distinctId, event, properties })pattern inpackages/agent/src/services/knowledge-base-reconcile.service.ts. Test: a spec asserting that no event payload contains a Skill body, a tag string, a credential key, a connection URL or a search query. -
T42. Docs. Create
docs/features/skills-shelf.md— the user-facing page: what each badge means, what each repair does, and how capture works. Modifyapps/docs/sidebarsPlatform.tsto list it (the sidebar is manual; unlisted files render only as orphan pages). Modifydocs/specs/features/agent-workspace/TRACKER.md— set this epic's spec status. Do not touchdocs/plugin-system/built-in-plugins.md; no plugin was added (Constitution VIII). -
T43. Update statuses. Set
spec.md,plan.mdand this file toImplemented/Done, and confirm every gate in plan §12 still holds against the merged code.
Definition of Done
- Every checkbox above is ticked.
pnpm format:check,pnpm lint,pnpm type-check,pnpm testandpnpm buildare green from the repo root.- The six pre-existing skill e2e specs named in T24 pass unchanged.
pnpm --filter ever-works-docs buildproduces no broken-link warnings.- Every acceptance-criteria box in spec §8 has been walked against a running build.
- Every gate in plan §12 is confirmed satisfied, and the three carried-forward gaps are still recorded there rather than silently closed.