AW-18 — Task Breakdown: Shared read-only views & channel guests
Ordered, executable tasks derived from
plan.md. Each task is small enough to land in one PR and ships with tests (Constitution VI).
Feature ID: aw-18-shared-dashboards
Spec: ./spec.md · Plan: ./plan.md
Status: Draft
Last updated: 2026-09-06
Blocking dependency: AW-02 Task board must have
landed its Focus-layout column projection before T9.
How to use
- Tasks are sequential unless marked
(parallel). - Every task names the exact files to create or modify and what "done" means.
- Add new tasks at the bottom rather than renumbering.
- Standing rules for every task in this list:
- Run
pnpm lint && pnpm type-checkbefore opening the PR. - A TypeORM entity change must carry its migration in the same PR (Constitution V).
- Every user-visible string is an i18n key with a camelCase leaf name containing no literal dot — next-intl rejects dotted leaf names at runtime.
- New endpoints answer
404, never403, for an unentitled caller.
- Run
Phase 1 — Publish (P1)
Ships FR-1…FR-24 and FR-34…FR-49. The Knowledge toggle renders disabled until P3.
Data model & contracts
-
T1. Add the
SharedViewentity.- Create
packages/agent/src/entities/shared-view.entity.tswith the columns inplan.md§3.1:organizationId(unique),tenantId,ownerUserId,tokenHash(unique),tokenEncrypted(EncryptedJsonColumn()frompackages/agent/src/entities/_secret-json-column.ts),status,sections,knowledgeClasses,searchIndexable,viewCount,lastViewedAt,firstViewNotifiedAt,tokenRotatedAt,rotationCount,createdById, timestamps. - Export from
packages/agent/src/entities/index.ts. - Register in
packages/agent/src/database/_entities-inventory.tsandpackages/agent/src/database/_entity-names.ts(alphabetical insertion) — this repo has noautoLoadEntities, and a drift spec fails CI if either is missed. - Test:
packages/agent/src/entities/__tests__/shared-view.entity.spec.ts— asserts the defaultsectionsshape,knowledgeClassesdefaults to[],searchIndexabledefaults tofalse,statusdefaults to'active'. - Done when:
cd packages/agent && pnpm testpasses and the entity-name drift spec inpackages/agent/src/database/__tests__/is green.
- Create
-
T2. Write the migration for
shared_views.- Create
apps/api/src/migrations/1791180000000-CreateSharedViews.ts(generate withpnpm typeorm migration:generate -d typeorm.config.ts src/migrations/CreateSharedViewsfromapps/api/, then hand-check it). - Must create the table plus
UNIQUE(organizationId),UNIQUE(tokenHash)andINDEX(tenantId), withON DELETE CASCADEon the organization FK. - Done when: the SQL is purely additive (no
DROP, noALTER … TYPE),down()drops only the table and its indexes, and a fresh boot withRUN_MIGRATIONS=trueapplies it cleanly.
- Create
-
T3 (parallel with T2). Add the published contracts.
- Create
packages/contracts/src/api/shared-view/withshared-view.dto.ts,published-board.dto.ts,published-document.dto.ts,publishable-activity.tsandindex.ts. - Declare
SharedViewSettingsDto,SharedViewSectionsDto,SharedViewIndexingMode,PublishedBoardDto,PublishedColumnDto,PublishedTaskCardDto,PublishedAgentDto,PublishedActivityLineDto,PublishedDocumentSummaryDto,PublishedDocumentDto, and the frozenPUBLISHABLE_ACTIVITY_ACTIONSconstant. - Every published DTO is closed: no index signature, no
Record<string, unknown>. - Re-export from
packages/contracts/src/api/index.ts. - Done when:
turbo build --filter=@ever-works/contractssucceeds and the declaration output contains no[key: string]on anyPublished*type.
- Create
Token, projection and publish filters
-
T4. Add the share-token service.
- Create
packages/agent/src/shared-views/shared-view-token.service.ts: 256-bit generation vianode:crypto, URL-safe encoding,sha256hash, and the encrypt/decrypt round trip through the entity column. - Test:
packages/agent/src/shared-views/__tests__/shared-view-token.spec.ts— 43-character output, hash stability, round-trip equality, and an assertion that a serialised entity never contains the raw token. - Done when: the spec passes and
grepfinds no path that logs the token.
- Create
-
T5. Add the publish filters (the security boundary).
- Create
packages/agent/src/shared-views/publish-filter.tsexporting pure functionspublishTaskCard,publishAgent,publishActivityLine,publishDocumentSummary,publishDocument. - Test:
packages/agent/src/shared-views/__tests__/publish-filter.spec.ts— asserts the exact key set of each output object, and feeds each filter an input carryingcostUsd,budget,tokenCount,model,instructions,comments,repoUrl,emailand the Task's owner columns (missionId,workId,ideaId,teamId,agentId,goalId) and asserts none survives — the shared view publishes no provenance chip (FR-21). - Done when: adding a field to
Taskand re-running the spec fails until the field is deliberately classified.
- Create
-
T6. Classify every activity action for publication.
- Create
packages/agent/src/shared-views/publishable-activity.tswithPUBLISHABLE_ACTIVITY_ACTIONSandNEVER_PUBLISH_ACTIVITY_ACTIONS. - Test:
packages/agent/src/shared-views/__tests__/publishable-activity.spec.ts— iterates every member ofActivityActionType(packages/agent/src/entities/activity-log.types.ts) and fails if any member is on neither list. - Done when: the spec is green, and adding a new action type without classifying it turns CI red.
- Create
-
T7. Add the
SharedViewrepository.- Create
packages/agent/src/database/repositories/shared-view.repository.tswithfindByOrganization,findByTokenHash(single indexed read),createForOrganization,rotateToken(atomicUPDATE … WHERE rotationCount = :seen),updateSettings,applyViewDelta. - Register in
packages/agent/src/database/_repository-inventory.tsifDatabaseModuleowns it, otherwise export it from the feature module and frompackages/agent/src/database/index.ts. - Test:
packages/agent/src/database/repositories/__tests__/shared-view.repository.spec.ts— covers the optimistic-concurrency loser path. - Done when: the DatabaseModule drift spec in
packages/agent/src/database/database.module.spec.tsstays green.
- Create
-
T8. Add the
SharedViewdomain service.- Create
packages/agent/src/shared-views/shared-view.service.tsandpackages/agent/src/shared-views/shared-views.module.ts. - Methods:
getForOrganization,enable,disable,regenerate(resetsfirstViewNotifiedAttoNULL, incrementsrotationCount),updateSettings,deleteForOrganization,resolveByToken. - Test:
packages/agent/src/shared-views/__tests__/shared-view.service.spec.ts— enable is idempotent, pause keeps the token, regenerate replaces it, a stalerotationCountraises the conflict.
- Create
-
T9. Add the board projection service. Requires AW-02's Focus-layout column projection.
- Create
packages/agent/src/shared-views/shared-view-projection.service.ts:projectBoard(organizationId)reads the same column query the private Task board uses for its Focus layout (Backlog,In flight,Needs you,Donegrouping the sevenTaskStatusvalues), caps each column at 50 rows, projects Agents and the last 20 publishable activity rows, and passes everything through T5's filters. - Test:
packages/agent/src/shared-views/__tests__/shared-view-projection.service.spec.ts— column order and membership match the private Focus-layout fixture; cancelled Tasks, recurring templates and board-hidden Tasks are absent and are not counted; the+N moreoverflow number is correct.
- Create
API — owner side
-
T10. Add the owner guard.
- Create
apps/api/src/shared-views/shared-view-owner.guard.ts: resolvesorganizationId → tenantId → tenant.ownerUserIdand throwsNotFoundException(neverForbiddenException). - Test:
apps/api/src/shared-views/shared-view-owner.guard.spec.ts.
- Create
-
T11. Add the owner controller.
- Create
apps/api/src/shared-views/shared-views.controller.tsandapps/api/src/shared-views/shared-views.module.ts; register the module inapps/api/src/api.module.ts. - Routes per
plan.md§4.1:GET /,POST /,POST /regenerate,PATCH /,DELETE /,GET /preview,GET /knowledge-classes(returns zeroes until P3). - Class-level guards:
AuthSessionGuard,OrganizationOwnershipGuard; addSharedViewOwnerGuardon every write and on the token-bearing read. @Throttle10/min onPOST /andPOST /regenerate, 30/min onPATCH /.- Swagger decorators on every route.
- Test:
apps/api/src/shared-views/shared-views.controller.spec.ts— owner-only writes, non-owner member gets settings withoutlink, non-member404, throttle metadata present.
- Create
-
T12. Emit activity rows for every owner-side change.
- Append
SHARED_VIEW_ENABLED,SHARED_VIEW_DISABLED,SHARED_VIEW_REGENERATED,SHARED_VIEW_SECTIONS_CHANGED,SHARED_VIEW_INDEXING_CHANGEDtopackages/agent/src/entities/activity-log.types.ts(append only — no reordering; no migration needed, the column is a freevarchar(50)). - Write one row per changed facet from
SharedViewServiceviapackages/agent/src/activity-log/activity-log.service.ts. - Test: extend
packages/agent/src/shared-views/__tests__/shared-view.service.spec.ts— asserts one row per facet and that no row's payload contains the token.
- Append
API — public side
-
T13. Add the public-response header interceptor.
- Create
apps/api/src/shared-views/shared-view-headers.interceptor.tssettingCache-Control: no-store,Referrer-Policy: no-referrer,X-Content-Type-Options: nosniff, andX-Robots-Tag: noindex, nofollow, noarchive, nosnippetunless the resolved Shared view hassearchIndexable === true. - Test: covered by T14's controller spec.
- Create
-
T14. Add the public controller.
- Create
apps/api/src/shared-views/shared-view-public.controller.ts,@Controller('api/public/shared-view'),@Public()(decorator fromapps/api/src/auth/decorators/public.decorator.ts). POST /sessionswith body{ token }(DTO field namedtoken, soSentryInterceptor.SENSITIVE_BODY_KEYSalready drops it) →{ viewSession, expiresAt }, andGET /boardauthorised byAuthorization: Bearer <viewSession>through aSharedViewSessionGuard(T14a). No route takes the token in its path or query string (spec FR-7a; plan §4.2). Knowledge routes are added in P3 and return404until then.- Two throttle buckets: 60/min keyed on the token hash (the Shared view id for reads),
600/hour keyed on the client; both evaluated before the projection query.
429carriesRetry-After: 60. - Unknown / rotated / paused tokens, and a view session whose link was regenerated or paused, must produce a byte-identical response.
- Drive the view counter and first-view notification from
POST /sessions, not from each read. - Test:
apps/api/src/shared-views/shared-view-public.controller.spec.ts— identical bodies for every failure cause; every header present;X-Robots-Tagomitted only when indexable; the429shape; an assertion that no read route accepts a write verb; and a reflective assertion over the controller's route metadata that no path or query parameter carries the token.
- Create
-
T14a. Add the view-session service and guard.
- Create
apps/api/src/shared-views/shared-view-session.service.tsandshared-view-session.guard.tsper plan §4.2: HMAC-SHA256 compact token with claims{ v, sid, rot, exp }, 15-minute TTL, secretSHARED_VIEW_SESSION_SECRETfalling back toBETTER_AUTH_SECRET/AUTH_SECRET(theTerminalAttachServiceposture), fail closed with no secret. The guard verifies MAC and expiry withtimingSafeEqual, then requiresstatus = 'active'androtationCount = roton the row. - Test:
apps/api/src/shared-views/shared-view-session.service.spec.ts— round trip; tampered MAC, expired, stalerotand paused view all refused identically; no secret refuses everything; the claims contain no token and no token hash. - Done when: a view session minted before
POST /regenerateis refused on its next request.
- Create
-
T14b. Redact share tokens in every request recorder.
- Create
packages/monitoring/src/redaction/secret-url.tsexportingredactSecretUrlandredactSecretValue(share token after a/share/segment, with or without a locale prefix;Bearerview sessions; bodytokenvalues →[redacted]), exported from the package index. - Modify
apps/api/src/logging.interceptor.tsto logredactSecretUrl(originalUrl)on the request, response and error lines. - Modify
packages/monitoring/src/interceptors/sentry.interceptor.ts(contexturl,transactionandendpointtags),packages/monitoring/src/sentry/sentry.config.ts(beforeSend/beforeSendTransaction:request.url, transaction name, breadcrumbdata.url) andpackages/monitoring/src/interceptors/posthog.interceptor.ts(endpoint). - Route the public controller's thrown errors, and any message or URL logged by an
APP_FILTERfilter underapps/api/src/common/filters/, throughredactSecretValue. - Modify
apps/web/src/components/posthog/PostHogProvider.tsx: noposthog.initand no page-view capture on a share route (spec FR-40), plus asanitize_propertieshook redacting$current_url,$pathnameand$referrer. - Test:
packages/monitoring/src/redaction/__tests__/secret-url.spec.ts(new); extendapps/api/src/logging.interceptor.spec.ts,packages/monitoring/src/interceptors/__tests__/sentry.interceptor.spec.ts,posthog.interceptor.spec.tsandpackages/monitoring/src/sentry/__tests__/sentry.config.spec.ts; a web unit spec that the provider does not initialise on/share/<token>or/<locale>/share/<token>. - Done when: the specs pass, and the edge access-log format for the web host has been
confirmed to drop or redact
/share/paths (recorded in the PR description).
- Create
-
T14c. Prove the token never reaches a log line.
- Create
apps/api/test/shared-view-log-hygiene.e2e-spec.ts: boot the API with the realLoggingInterceptor,SentryInterceptorandPostHogInterceptor, a capturing NestLogger, and spies onSentry.captureException/setContext/setTag/addBreadcrumband PostHogtrackEvent. Run an exchange, a board read, an unknown-token exchange, a read after regenerate, a throttled read and a forced500withconfig.debug()on. - Assert that neither the raw token nor the view session occurs as a substring in any captured log line, Sentry payload or PostHog property.
- Create
apps/web/e2e/shared-view-token-transport.spec.ts: record every request the published page issues over three poll cycles in a fresh context; assert no request URL contains the token, no analytics request is issued, and the token appears only inPOST /sessionsbodies. - Done when: both specs are green and part of the P1 gate.
- Create
Background work
-
T15. Add the view-counter buffer and flush task.
- Add
SHARED_VIEW_COUNTER_FLUSH_DISPATCHERtopackages/agent/src/tasks/_tasks-symbols.ts(alphabetical) and createpackages/agent/src/tasks/shared-view-counter-flush-dispatcher.tsand its siblingshared-view-counter-flush.types.ts(the token + payload convention every other dispatcher in that folder follows). - Create
packages/tasks/src/tasks/trigger/shared-view-counter-flush.task.ts(cron every 5 minutes) and export it frompackages/tasks/src/tasks/trigger/index.ts. - Buffer key: salted, truncated client hash + token hash, 10-minute dedupe window, 24 h TTL, never persisted and never containing a raw IP.
- Bind through
buildJobRuntimeProviders()inpackages/tasks/src/trigger/trigger.module.ts— the call site depends on the symbol only (Constitution IV). - Test:
packages/tasks/src/__tests__/shared-view-counter-flush.task.spec.ts— dedupe window, idempotent flush, no raw IP anywhere in the buffer payload. - Done when:
packages/agent/src/tasks/tasks.spec.ts(which pins the exported runtime-symbol set) is green.
- Add
-
T16. Add the first-view notification.
- Add
notifySharedViewFirstView()topackages/agent/src/notifications/notification.service.tswith event keyshared_view_first_view. - Register the key in
CORE_EVENTSinapps/api/src/notifications/notification-event-type-bootstrap.service.tswithdefaultChannels: ['in-app']— an unregistered key can never fan out to a channel and never appears in the preference matrix. - Fire from the flush task when
firstViewNotifiedAt IS NULL, then set it. - Test:
packages/agent/src/notifications/notification.service.spec.ts(extend) — fires once, never twice; and a bootstrap spec asserting the key is registered.
- Add
Web
-
T17. Add the routes and make the share route public.
- In
apps/web/src/lib/constants.ts: addDASHBOARD_SETTINGS_SHARING: '/settings/sharing',SHARE_VIEW: '/share/:token'and ashareView(token)href helper; addROUTES.SHARE_VIEWtoPUBLIC_ROUTESin the same commit. - Test: extend
apps/web/src/lib/__tests__/public-routes.unit.spec.tsto pin/share/<token>as public. - Done when: the spec is green. Omitting the
PUBLIC_ROUTESentry makesapps/web/src/proxy.tsbounce the visitor and clear the session cookie — this task exists to stop that shipping.
- In
-
T18. Build the published page.
- Create
apps/web/src/app/[locale]/share/[token]/page.tsx(server component; first paint requires no client JS; exchanges the token viaPOST /sessionsin a request body and passes only{ viewSession, expiresAt }to client components — plan §4.4),apps/web/src/app/[locale]/share/[token]/not-active.tsx,apps/web/src/components/share/PublishedShell.tsx,apps/web/src/components/share/PublishedBoard.tsx. - Implement all states from
spec.md§6.7: loading skeleton, empty board, not active, throttled, poll-paused, preview banner, and the ≥360 px single-column layout with sticky column headers. - Poll every 20 s with
Authorization: Bearer <viewSession>; never build an API URL from the token. Re-exchange (token in a body) whenexpiresAtis under 60 s away or a read returns the not-active response; a failed re-exchange renders not-active. Pause ondocument.hidden; stop after 30 min idle with a Resume control; back off to 60 s on429; keep the last good render on a network error. - Keyboard map and polite live region per
spec.md§6.12. - Test:
apps/web/src/components/share/__tests__/PublishedBoard.unit.spec.tsxfor the poll state machine.
- Create
-
T19. Apply the per-view crawler directive where it can actually vary.
/robots.txtis a single site-wide file with no knowledge of which/share/[token]a crawler will fetch, so it cannot express a per-view choice. Do not make it depend onsearchIndexable.- The per-view directive is carried by the resolved response itself: the page and the public
API send
X-Robots-Tag: noindex, nofollow, noarchive, nosnippetand render<meta name="robots" content="noindex, nofollow">unless the resolved view hassearchIndexable === true(T14/T18 already emit the header; this task owns the page meta and the tests). - If a site-wide
robots.tsis added for other reasons, it stays independent of Shared views and must notDisallow: /share/, because a disallowed URL is never fetched and so itsnoindexheader is never seen. - Done when: a blocked view returns the header and meta; an indexable view returns
neither; and
/robots.txtis byte-identical whatever any Shared view's setting is.
-
T20. Build Settings → Sharing.
- Create
apps/web/src/app/[locale]/(dashboard)/settings/sharing/page.tsx,apps/web/src/components/settings/SharingSettings.tsx, andapps/web/src/app/actions/shared-view.ts(getSharedView,createSharedView,regenerateSharedViewLink,updateSharedView,deleteSharedView,getKnowledgeClassCounts). - Add the
Sharingentry afterOrganizationinapps/web/src/app/[locale]/(dashboard)/settings/settings-layout-client.tsx. - Render all states from
spec.md§6.2–§6.4, including the non-owner read-only state and all three confirmation dialogs with their exact copy. - The Knowledge toggle renders disabled with the existing "coming soon" treatment until P3.
- Test:
apps/web/src/components/settings/SharingSettings.unit.spec.tsx— non-owner never receives the link in props; every destructive action is behind a confirmation.
- Create
-
T21. Add the P1 i18n keys.
- Add the
dashboard.sharingand top-levelsharenamespaces toapps/web/messages/en.jsonexactly as listed inplan.md§8.1 and §8.3, plusnotifications-v2.sharedViewFirstViewTitle/…Body. - Mirror the same key set into all 20 sibling locale files in
apps/web/messages/. - Done when: no leaf key contains a literal
., every leaf is camelCase, the key sets match across all 21 files, and the hydration e2e shard is green.
- Add the
-
T22. P1 end-to-end coverage.
- Create
apps/api/test/shared-view.e2e-spec.ts— publish → read → regenerate → old-token-dead against a real HTTP stack. - Create
apps/web/e2e/shared-view-publish.spec.ts,apps/web/e2e/shared-view-public-page.spec.ts,apps/web/e2e/shared-view-revoke.spec.ts,apps/web/e2e/shared-view-noindex.spec.ts,apps/web/e2e/shared-view-a11y.spec.ts. - Every spec that opens
/share/:tokenmust use a browser context with no storage state, or it proves nothing about anonymous access. - Done when: all five pass locally and in CI, and the revoke spec observes the "no longer active" swap within 20 s in a second context.
- Create
Phase 2 — Participate (P2)
Ships FR-50…FR-79.
Data model
-
T23. Add the
ChannelGuestentity.- Create
packages/agent/src/entities/channel-guest.entity.tswith the columns inplan.md§3.2, keyed onbindingId(FK →ingest_install_bindings,ON DELETE CASCADE). - Export from
packages/agent/src/entities/index.ts; register inpackages/agent/src/database/_entities-inventory.tsand_entity-names.ts. - Indexes:
UNIQUE(bindingId, externalUserId),INDEX(ownerUserId, status),INDEX(bindingId, externalUserId, status). - Test:
packages/agent/src/entities/__tests__/channel-guest.entity.spec.ts.
- Create
-
T24. Write the
channel_guestsmigration.- Create
apps/api/src/migrations/1791180100000-CreateChannelGuests.ts. - Done when: additive only;
down()drops just the table and its indexes.
- Create
-
T25. Write the attribution-columns migration. - Create
apps/api/src/migrations/1791180200000-AddRequesterAttribution.tsaddingrequestedByGuestId(FK →channel_guests,ON DELETE SET NULL) andrequestedByLabel varchar(160)totasks,missions,agent_action_proposals,agent_escalations, plusoriginConversationRef varchar(256)toagent_action_proposalsandagent_escalations. -tasksis the primary case (a guest's request produces Tasks);missionscarries the same pair only for the case where the Run sets up a standing initiative at the guest's request (FR-68). - Add the matching nullable columns topackages/agent/src/entities/task.entity.ts,packages/agent/src/entities/mission.entity.ts,packages/agent/src/entities/agent-action-proposal.entity.ts,packages/agent/src/entities/agent-escalation.entity.tsin the same PR. - Done when: every column is nullable, no existing column is touched, and the existing specs for all four entities still pass.
The gate
-
T26. Add the
ChannelGuestrepository.- Create
packages/agent/src/database/repositories/channel-guest.repository.ts:findActiveForBindingAndExternalUser(the gate's single indexed read),listForBinding,countForBinding,countForOwner,create,update,recordRequest(bumpslastSeenAt+requestCount). - Test:
packages/agent/src/database/repositories/__tests__/channel-guest.repository.spec.ts.
- Create
-
T27. Add the guest text fence.
- Create
packages/agent/src/channel-guests/guest-text-fence.ts, reusing the fencing posture already established inpackages/agent/src/services/memory-recall.ts(fence wrapper, forged-marker break, control-marker strip) rather than inventing a third mechanism. - Truncate at 4,000 characters with a visible marker (FR-64).
- Test:
packages/agent/src/channel-guests/__tests__/guest-text-fence.spec.ts— forged boundary markers are neutralised; control markers are stripped; truncation is marked.
- Create
-
T28. Add the admission gate.
- Create
packages/agent/src/channel-guests/channel-guest-admission.service.tsimplementing theConnectorPairingAuthorizersignature and returningConnectorAuthorizationDecisionfrompackages/plugin/src/contracts/capabilities/connector.interface.ts. - Order: signature verified → binding resolves the owner → sender is the owner
(admit) → sender is an
activeguest of that binding (admit) → deny. - Rate buckets: 30 messages/hour per guest, 300/day per Workspace, one refusal per unknown sender per 24 h, one throttle notice per guest per hour.
- Append
CHANNEL_GUEST_ADMITTED,CHANNEL_GUEST_DENIED,CHANNEL_GUEST_THROTTLEDtopackages/agent/src/entities/activity-log.types.tsand write one row per outcome — never the message body. - Test:
packages/agent/src/channel-guests/__tests__/channel-guest-admission.service.spec.ts— gate order; owner always admitted and never counted against caps; revoked denied; the 24 h single-refusal ceiling; and an assertion that zero facade calls occur on a denial.
- Create
-
T29. Insert the gate into the inbound path.
- Modify
apps/api/src/ingest/slack/slack-chat-bridge.service.tsto callChannelGuestAdmissionService.authorize()after signature verification and binding resolution and before any call intoapps/api/src/ai-conversation/openai-compat.service.ts. - On a denial, post the refusal (or nothing, inside the 24 h window) through the existing reply path and return without dispatching.
- Test: extend
apps/api/src/ingest/slack/slack-chat-bridge.service.spec.ts— denial short-circuits before the completion service; admission passes the fenced attribution preamble through.
- Modify
Attribution and decision routing
-
T30. Add the requester-attribution service.
- Create
packages/agent/src/channel-guests/requester-attribution.service.ts: builds"{displayName} · {channelName}", stampsrequestedByGuestId+requestedByLabelon the Tasks, Approvals and Escalations a guest-originated Run creates — and on a Mission only when that Run sets up a standing initiative at the guest's request — and appends(revoked)when rendering a revoked guest's historical label. - Test:
packages/agent/src/channel-guests/__tests__/requester-attribution.service.spec.ts— label format; stamping on all four record kinds; owner-originated work carries no label; a revoked guest's historical label is unchanged in the database.
- Create
-
T31. Route guest-originated decisions to the owner only.
- Modify the Approval and Escalation creation paths in
packages/agent/src/agent-approvals/and the escalation service underpackages/agent/src/so that a guest-originated Run's decision is always addressed to the Tenant owner and carriesoriginConversationRef. - Emit the fixed decision-pending reply into the originating conversation exactly once.
- Test: extend the approvals and escalations service specs — a guest-originated decision is never addressed to the guest; a guest message can never satisfy or dismiss a pending decision.
- Modify the Approval and Escalation creation paths in
-
T32. Add the decision-outcome post-back task.
- Add
DECISION_OUTCOME_POSTBACK_DISPATCHERtopackages/agent/src/tasks/_tasks-symbols.ts(alphabetical) and createpackages/agent/src/tasks/decision-outcome-postback-dispatcher.tsplus its siblingpackages/agent/src/tasks/decision-outcome-postback.types.ts— the token + payload convention every other dispatcher in that folder follows. - Create
packages/tasks/src/tasks/trigger/decision-outcome-postback.task.ts, exported frompackages/tasks/src/tasks/trigger/index.ts; retries 30 s → 2 m → 8 m, max 4 attempts. - Enqueue on approve/reject/resolve when
originConversationRefis present; suppress the post when the guest has been revoked; on final failure, record the failure so the settled item shows "Couldn't reply in {channel}". - Test:
packages/tasks/src/__tests__/decision-outcome-postback.task.spec.ts- the runtime-symbol pin in
packages/agent/src/tasks/tasks.spec.ts.
- the runtime-symbol pin in
- Add
API and web
-
T33. Add the guests controller.
- Create
apps/api/src/channel-guests/channel-guests.controller.ts,channel-guests.module.tsandconnection-owner.guard.ts; register the module inapps/api/src/api.module.ts. - Routes per
plan.md§4.3:GET /,POST /,PATCH /:guestId,DELETE /:guestId, throttled 20/min and 30/min respectively. GET /returns{ bindingReady: false }when no signature-verified binding exists yet — an owner-typed external workspace id is never accepted as proof of ownership.- Append
CHANNEL_GUEST_ADDED,CHANNEL_GUEST_RENAMED,CHANNEL_GUEST_REVOKEDtoactivity-log.types.tsand write one row per change. - Test:
apps/api/src/channel-guests/channel-guests.controller.spec.ts— CRUD, the not-ready shape,409duplicate,422over the 25/100 caps, non-owner404.
- Create
-
T34. Build the allowlist panel.
- Create
apps/web/src/components/settings/ChannelGuestsPanel.tsxandapps/web/src/app/actions/channel-guests.ts. - Mount the panel per channel inside
apps/web/src/components/settings/NotificationChannelsSettings.tsx. - Render all states from
spec.md§6.8: populated list, not-ready, full, and the revoke confirmation. The service name in the helper copy comes from the plugin manifest via the registry — no switch over plugin ids inapps/web. - Test:
apps/web/src/components/settings/ChannelGuestsPanel.unit.spec.tsx.
- Create
-
T35. Surface the requester label.
- Add the optional label line to the Task card and Task detail header (AW-02 components), to the Mission detail header, and to the My Decisions row (AW-03 component).
- Render nothing when the label is absent — the absence of a label means "the owner asked", and the UI must not invent one.
- Confirm the label is not present in any
Published*DTO (T5's key-set spec already enforces this). - Test: extend the relevant component unit specs.
-
T36. Add the P2 i18n keys.
- Add
dashboard.channelGueststoapps/web/messages/en.jsonperplan.md§8.2, plusnotifications-v2.channelGuestDeniedTitle/…Body. - Add the six agent-facing channel replies from
spec.md§6.10 underapi.channelGuestin the API's message catalogue, resolved in the owner's locale with{ownerName},{note}and{channel}interpolations. - Register
channel_guest_deniedinCORE_EVENTSinapps/api/src/notifications/notification-event-type-bootstrap.service.ts. - Mirror the key set into all 20 sibling locale files.
- Done when: no leaf key contains a literal
., key sets match across all 21 files, and the hydration shard is green.
- Add
-
T37. P2 end-to-end coverage.
- Create
apps/web/e2e/channel-guests.spec.ts— add, rename, revoke, caps, the not-ready state, and a non-owner seeing nothing. - Extend
apps/api/test/shared-view.e2e-spec.ts(or addapps/api/test/channel-guests.e2e-spec.ts) with the gate paths: denied sender starts no run; the 24 h refusal ceiling; the hourly cap. - Done when: both pass in CI.
- Create
-
T38. Update the program vocabulary table.
- Add Shared view and Channel guest to the vocabulary table in
docs/specs/features/agent-workspace/README.md§1, and mark AW-18 indocs/specs/features/agent-workspace/TRACKER.md. - Done when: both nouns appear with their "do not introduce" synonym bans, as program rule #2 requires.
- Add Shared view and Channel guest to the vocabulary table in
Phase 3 — Refine (P3)
Ships FR-25…FR-33 and the deferred half of FR-28.
-
T39. Add the knowledge publish predicate.
- Create
packages/agent/src/shared-views/knowledge-publish-predicate.ts: a document is published only if itskbDocumentClassis inSharedView.knowledgeClasses, itsstatus === 'active', itsreviewStateis not'proposed', and it is not excluded. - Test:
packages/agent/src/shared-views/__tests__/knowledge-publish-predicate.spec.ts— an empty class list yields zero documents (fails closed, never "everything").
- Create
-
T40. Add the per-document exclusion flag.
- Add
sharedViewExcluded: boolean(defaultfalse) topackages/agent/src/entities/work-knowledge-document.entity.tsand shipapps/api/src/migrations/1791180300000-AddKbSharedViewExcluded.tsin the same PR. - Done when: the column is
NOT NULL DEFAULT false,down()drops only it, and existing KB specs still pass.
- Add
-
T41. Extend the projection and public API for knowledge.
- Add
projectKnowledgeListandprojectKnowledgeDocumenttopackages/agent/src/shared-views/shared-view-projection.service.ts. - Add
GET /knowledgeandGET /knowledge/:docIdtoapps/api/src/shared-views/shared-view-public.controller.ts, both behindSharedViewSessionGuard(T14a) — the token is never in the path —qmin 2 characters, 50 per page, 200 total, 200 distinct documents per hour per Shared view. Extendapps/api/test/shared-view-log-hygiene.e2e-spec.ts(T14c) with a knowledge list, a search and a document read. - Test: extend
apps/api/src/shared-views/shared-view-public.controller.spec.ts— a deselected class 404s on the next request; git history, citations, retrieval trail, uploads and originals are never in the response.
- Add
-
T42. Build the knowledge reader.
- Create
apps/web/src/components/share/PublishedKnowledge.tsx(two-pane list + reader, 300 ms debounced search) and wire the tab inapps/web/src/components/share/PublishedShell.tsx. - Implement the empty state and the "no longer published" state from
spec.md§6.7.
- Create
-
T43. Enable the Knowledge section in Settings → Sharing.
- Un-disable the toggle in
apps/web/src/components/settings/SharingSettings.tsx; add the class picker with live per-class counts fromGET /knowledge-classes; implement the confirm dialog that states the exact number of documents that will become public. - Add the per-document "Exclude from shared view" control to the KB document metadata panel.
- Test: extend
SharingSettings.unit.spec.tsx— enabling with no classes selected is blocked at the UI and serves nothing at the API.
- Un-disable the toggle in
-
T44. Add the guest activity report.
- Add a "Requests from guests, last 30 days" block to
apps/web/src/components/settings/SharingSettings.tsx, sourced from thechannel_guest_*activity rows. - Test: unit spec on the aggregation.
- Add a "Requests from guests, last 30 days" block to
-
T45. Add the P3 i18n keys and e2e.
- Extend
dashboard.sharingandsharewith the class-picker, reader and exclusion-control strings; mirror into the 20 sibling locale files. - Create
apps/web/e2e/shared-view-knowledge.spec.ts— publish one class, read a document anonymously, deselect the class, confirm the document 404s.
- Extend
-
T46. Close the open questions.
- Take
spec.md§9 to review; land decisions for link expiry, link scope, guest-identifier ergonomics (pairing codes), co-owned workspaces, the activity strip's titles, and per-document exclusion. - Done when: every
[NEEDS CLARIFICATION: …]marker inspec.mdis either resolved in place or converted into a follow-up epic reference.
- Take