Create initiative

Initiative Brief: Per-Initiative Repository Isolation and Applications

DRAFT
Intent— the specification (44,839 chars); the plan below decomposes it into work items. Click to expand.

# Initiative Brief: Per-Initiative Repository Isolation and Applications ## Desired outcome An initiative has a **durable, authoritative repository scope** stored in PostgreSQL. That set — not the brief, not `repositoryHints`, not the repositories its work items happen to reference — is the answer to "what may this initiative touch?" Every service that creates or mutates repository-targeted state on behalf of an initiative consults it and fails explicitly when a target falls outside it. Scope never widens as a side effect of planning, brief commits, hints, or history. **Applications** are first-class persisted Orchestrator records: a named logical group of repositories drawn from the trusted registry, with membership managed through the UI. A repository may belong to many Applications. An operator picks an Application when setting up an initiative and uses it to **populate** the initiative's scope; from that moment the initiative's scope is its own set, freely edited, and later changes to the Application never mutate it. The association is kept as context, not as a rule. `primaryRepository` becomes a real, nullable, persisted, operator-selectable property of the initiative — not a derivation input every caller omits. Three concepts, with **one** trust boundary and two independent memberships beneath it: 1. **Trusted repository registry** (code, ADR-0006) — what Orchestrator may operate on at all. The only trust boundary. 2. **Application** (persisted, UI-managed) — a logical grouping drawn from the registry. An input to scope, not a parent boundary over it. 3. **Initiative repository scope** (persisted, per initiative) — what *this* initiative may touch, drawn from the registry. Application membership and initiative scope are each independently constrained by the registry, and **not by each other**. An initiative populated from the "Outshine" Application may then add `Xyence/hub` because this particular piece of work needs it; the result is a scope that is not a subset of any Application, and that is a correct and expected outcome. Cross-application work is the case this design exists to support. Everything an operator needs to run this is in the UI, and served over `/orchestrator/*` so the Workspaces operator app uses the same surface rather than reaching into Orchestrator-only state. ## Motivation Issue #146 asked for repository-*independent* initiatives. #147 delivered a pure classifier (`deriveInitiativeRepositoryScope`) and a read-only panel, and stopped there. The classification has no storage, no operator control, and no consequence, so "scope" today is a description of what already happened rather than a boundary. The gap is not theoretical. Repository scope is recomputed on every render from the brief's `repositoryScope`, `context.repositoryHints` (jsonb, overwritten wholesale on brief commit), and the distinct repositories of existing work items. Nothing consults it. The planner receives the entire trusted registry on every pass, with hints reduced to a prompt line it is explicitly permitted to drop. Plan materialization checks registry membership, frozen state and the item's own contract — never the initiative. So an initiative whose brief declares one repository can silently acquire work in five others, and the only signal is the same read-only panel, after the fact, labelled "discovered". Two of #146's own requirements were never met at all: the operator-designated primary repository is an input no caller supplies, and "remove repositories that are no longer required" has no implementation anywhere. Worse, one canonical route accepts a repository with no validation of any kind. `fileWorkItem` (`packages/db/src/services/filedWorkItems.ts:43`) runs no registry check, no frozen check and no scope check; it persists the work item and sets `contract.allowedRepositories` to whatever string it was handed. `POST /orchestrator/initiatives/[id]/work-items/file` (#608, shipped 2026-08-24) exposes that to any authenticated operator or automation. Materialization later refuses with "untrusted repository" non-fatally, so the row persists in `PLANNED` and the wave — whose continuation means every item complete — cannot close. This is precisely why UI filtering cannot be the boundary. It is tracked independently as Xyence/orchestrator#610, because the registry, frozen and contract-consistency checks it is missing are owed whether or not scope enforcement is ever built. Separately, the system needs repository groups and has grown four incompatible partial answers — a Hub-manifest Application whose ingest has never worked, a per-repository `application` label that cannot express a group, the Application Platform's single-repository Applications with no table behind them, and release bundles, which work but are release-only. An operator scoping an initiative to "the platform" has no way to say so. ## Current state - What #146/#147 left standing - `packages/domain/src/initiativeScope.ts` — `deriveInitiativeRepositoryScope` and `expectedRepositoriesFromBriefScope`. Pure, well tested, recomputed per render. - Two consumers only: `apps/web/src/components/initiativeScopeView.ts`, and `packages/db/src/services/briefIntake.ts:300`, which seeds `context.repositoryHints` at import. - `apps/web/src/app/initiatives/[id]/page.tsx:431` calls the view with `briefSourceRepository`, `expectedRepositories` (brief `repositoryScope` + `context.repositoryHints`) and `activeRepositories` (distinct work-item repositories). It passes neither `primaryRepository` nor `discoveredRepositories`, so the primary is always the first expected repository and "discovered" means only "carries work but was not expected". - No durable scope object exists. `initiatives.context` (jsonb) holds `repositoryHints`, writable at creation, via `PATCH /api/initiatives/[id]`, and overwritten wholesale on brief commit (`apps/web/src/app/api/initiatives/[id]/brief/commit/route.ts:39`). - `packages/db/src/services/epics.ts:98` creates initiatives with `repositoryHints: [epic.repository]` — a second seeding path. Where repository targets are validated today, and against what — Every site below enforces the same two rules — trusted registry and not frozen — and none of them knows what initiative it is serving. | Seam | Location | Checks today | | --- | --- | --- | | Plan persistence | `packages/db/src/services/plans.ts:311-343` | contract `allowedRepositories`, `isKnownRepository`, `isFrozenRepository`, trusted validation keys | | Work-item amendment | `packages/db/src/services/workItems.ts:189-206` | same four | | Filed work item | `packages/db/src/services/filedWorkItems.ts:43` | **none** — tracked as #610 | | Issue import | `packages/db/src/services/importedIssues.ts:154-175` | registry, frozen, issue eligibility | | Epic link | `packages/db/src/services/epics.ts:58-63` | registry, frozen | | Related-epic declaration | `packages/db/src/services/relatedEpics.ts:65` | registry | | GitHub issue materialization | `packages/github/src/sync.ts:169` | registry (per item, non-fatal) | | PR creation | `packages/github/src/sync.ts:278` | repository read from the work item; run/lease ownership | | Worker claim | `packages/db/src/services/runs.ts:235-240` | worker credential capabilities only | The planner's world - `repositoryCatalog()` (`apps/web/src/server/planner.ts:74-86`) maps all of `REPOSITORIES`, unfiltered, and is used for first-wave (`:439`) and continuation (`:824`) planning alike. - The prompt says: *"Target ONLY repositories from the provided catalog. You may add or drop the operator's repository hints, but explain why in assumptions."* - `continuationRepositoryHints()` (`:805`) unions expected with already-modified repositories — widening the hint, constraining nothing. - `roadmapCandidatesFor()` (`:131`) selects roadmap candidates from `repositoryHints`. - Planner assumptions and normalizations already have a persistence path (`recordPlannerNormalizations`, `:455`). The UI - Every repository control on the standalone initiative page binds to `KNOWN_REPOSITORY_NAMES`: plan editor (`:673`), issue assembly (`:238`, `:701`), importer (`:641`, frozen filtered). - The Workspaces operator app (`Xyence/workspaces`, `apps/orchestrator`) has **no** repository surface at all — not on initiative detail, the list, or `/initiatives/new` — because `apps/web/src/server/canonicalInitiatives.ts` exposes no repository data. - `/api/repositories` lists the trusted registry but is gated behind an ENGINEERING RBAC middleware rule and the `REPOSITORIES_SURFACE_ENABLED` flag, which is off by default. It cannot serve a repository picker in Workspaces. The four meanings of "Application" - **`TopologyApplication`** (`packages/domain/src/topology.ts`) — id, name, owningTeam, components, `repositories[]`. Already a repository group, read from a Hub manifest at request time, persisted nowhere, consumed by `/applications` and `/applications/repositories` and nothing else. - **`PLATFORM_APPLICATION_MAPPING_SEED`** (`packages/policy/src/platformMapping.ts`) — a per-repository `application` label, strictly 1:1, seeded in code, overridable by `REPOSITORY_PLATFORM_MAPPING_JSON`. - **Application Platform Applications** (#501/#502/#508/#585) — one repository each. The Hub-manifest guide states Orchestrator owns Application identity and lifecycle, but there is no `applications` table in `packages/db/src/schema.ts`, and platform status resolves through a single hardcoded slug (`applicationStatus.ts`). - **`RELEASE_BUNDLES`** (`packages/policy/src/bundles.ts`) — the only functioning group: named, deploy-ordered, validated at startup in both directions. Release-only, a code constant, and it forbids a repository from belonging to two bundles because deploy order must be unambiguous. The Hub topology manifest ingest has never worked - `topologyManifest.ts:57` looks for `platform-topology.json`; Hub publishes `platform-topology/topology.yaml`. - `topologyManifest.ts:260` calls `JSON.parse`. The manifest is YAML; there is no YAML parser on that path. - The schema requires `version: number`, `owningTeam` per application and `repositories: ["org/name"]`. Hub writes `schema_version: "1.0"`, carries no per-application team, and uses `primary_repos: [outshine, hub]` — short keys. - Consequently `/applications` resolves to `unavailable` against the real manifest and has never displayed it. Overlapping membership is already the reality — Hub's manifest states the principle in its own header: *"A repository may contribute components to more than one application (e.g. hub → Platform and Orchestrator), which is exactly why applications — not repositories — are the top-level abstraction."* Its current data agrees: `outshine` is in `platform`, `content-management` and `console`; `hub` is in `platform` and `orchestrator`; `workspaces` is in `workspaces` and `orchestrator`; `stlouis-xyence-io` is in `content-management` and `st-louis-site`. Its Applications include cross-cutting capabilities such as `content-management` (`status: in-development`, spanning three repositories) — exactly the shape an initiative wants to scope to. ## Product principles - Scope is a decision, not a description. The operator states it; the system never infers it from what has already been touched. - An Application is a starting point, not a leash. It populates a scope and is then remembered as context; it never changes a scope on its own. - A refusal explains itself in operator language. "Xyence/console is not in this initiative's scope" with the current scope and the action that would change it — never a database constraint error. - Removing a repository is answered at the level of the work, not the row. If planned or active work targets it, the operator is shown that work and asked to resolve it. - One action for the common case: scoping to a known product is picking an Application. - Nothing in flight breaks. Every existing initiative is given a scope matching what it is already permitted to do, before anything is enforced. - Every operator capability here exists in the UI. A capability reachable only by API is not delivered. - The operator does this in the Workspaces app; standalone pages keep pace as break-glass. ## Architectural principles - Repository scope, Applications and Application membership are durable PostgreSQL state. Nothing about the boundary is reconstructed at read time. - There is exactly one trust boundary — the trusted registry — and two independent memberships beneath it: registry ⊇ Application membership, and, separately, registry ⊇ initiative scope. Applications populate scope by value; they never bound it afterward. No validation anywhere may require an initiative's scope to be a subset of its associated Application's membership. - Admission is checked at admission time, and admission is the only moment the registry gates. A repository that is frozen or leaves the registry after admission is retained, marked, and blocked — never silently deleted from an Application or an initiative. Boundaries move because an actor moved them. - The trusted registry stays code-owned under ADR-0006. Applications are operator data: every mutation carries a human actor, is validated against the registry, and is audited. "Trusted operator configuration, never model output" is satisfied by actor and validation, not by the file the data lives in — no planner, agent or model output may create or alter an Application. - Enforcement lives in the domain/service layer, not in routes and not in the UI. One shared guard is called by every seam that accepts an initiative identity and a repository target. - The planner is given a scoped world *and* its output is re-validated on persistence. Narrowing the catalog is an efficiency and honesty measure; the persistence check is the guarantee. - The existing pure derivation stays pure and becomes a **display classifier over persisted entries** — origin drives status, replacing set arithmetic over hints and work items. - Orchestrator owns the operational Application record. The Hub manifest is an import/export peer, reconciled explicitly on demand, never read on the request path for a runtime decision. - Scope mutations are recorded on the initiative's existing append-only timeline (`run_events`) with an actor; Application mutations are recorded in `audit_log`. - The canonical surface distinguishes an unset scope from an empty one, and additive fields only — no existing canonical field changes meaning. ## Domain concepts - Trusted repository registry: `REPOSITORIES` in `@orchestrator/policy`. The set Orchestrator may operate on at all. Code-owned, ADR-0006. Unchanged by this initiative. - Application: a persisted, named logical group of trusted repositories, managed through the UI. A repository may belong to zero, one, or many. An Application is a template an operator populates a scope from; it does not bound that scope afterward. - Application membership: the persisted set of repositories in an Application. Validated against the registry on every write and at startup. - Initiative repository scope: the persisted, authoritative set of repositories one initiative may operate on. Its own set, not a view of anything else. - Scope state: whether an initiative's scope has been established (`set`) or never was (`unset`). An unset scope is not an empty scope unset blocks planning with an explanation; empty is a deliberate operator statement. - Admission: the recorded act of adding a repository to an initiative's scope actor, time, origin, and the Application it came from when it came from one. - Origin: why a repository is in scope `brief`, `operator`, `application`, `planner_suggestion`, or `migration`. Origin is provenance for display and review; it grants nothing. - Blocked entry: a repository still in an Application or an initiative's scope that has since been frozen or removed from the trusted registry. It stays visible and marked, its historical work stays intact, no new repository-targeted operation may use it, and only an operator can remove it. - Primary repository: the operator-designated principal repository of an initiative. Nullable and never invented; when set, always a member of scope. - Associated Application: the Application an initiative's scope was populated from, retained as context. It has no ongoing effect on scope. - Scope suggestion: a trusted repository proposed for scope but not admitted named by the planner in its assumptions, carried in `context.repositoryHints`, or declared by a brief commit. Rendered with Add and Dismiss. Not a work item, not a question, and never an automatic admission. - Trusted repository registry entry: repository policy description, default branch, validation commands, protected paths, frozen flag. Supplies the metadata the planner and UI show for in-scope repositories. - Release bundle: the existing release-time group with deploy ordering. Unrelated to Applications and unchanged. ## Decisions - Applications are first-class persisted Orchestrator data — Membership is database state managed through the UI, not code or environment configuration. Seeding from trusted config or topology data is initialization only; the persisted record is the operational source of truth thereafter. - The trusted registry remains the only trust boundary — Application membership and initiative scope are each validated against it on admission, independently of one another. Membership can never launder an untrusted or frozen repository into an initiative. - An initiative's scope is not required to be a subset of its associated Application — An operator may add any trusted repository the work needs. Encoding the subset relation as a validation rule would forbid exactly the cross-application work this design exists to support, so no such rule may be written. - A repository frozen or removed from the registry after admission stays visible and blocked — It is not deleted from any Application or initiative scope. It is marked, its existing work stays intact and legible, every new repository-targeted operation against it is refused, and only an explicit operator action removes it once the affected work is resolved. Scope boundaries move because an actor moves them, never because upstream configuration changed. - A repository may belong to many Applications — Hub's manifest asserts this as the reason Applications are the top-level abstraction, and its current data already does it in four places. `RELEASE_BUNDLES`' single-membership rule exists because deploy order must be unambiguous; that reason does not transfer to scoping, so the constraint is not inherited. - Initiative repository scope is persisted on the initiative — A dedicated membership table, authoritative after migration. Derivation from hints, brief sections and work items is retired as a source of truth and retained only as a display classifier and as migration input. - Applications populate scope; they do not define it — Selecting an Application copies its repositories into the initiative's scope as ordinary entries. The association is stored as context. A later membership change never mutates an existing initiative's scope. - `primaryRepository` is a persisted, nullable initiative column — Operator-selectable, exposed canonically, required to be in scope when set, and never invented when the initiative does not need one. - Enforcement is a single shared domain guard called at every seam — Not a route check, not a UI filter, and not the planner prompt. Every service accepting an initiative identity and a repository target calls it. - The planner receives only in-scope repositories, and its output is re-validated on persistence — Prompt scoping alone is not enforcement; malformed or generated output naming an out-of-scope repository is rejected at write time. - Out-of-scope planner proposals become scope suggestions, not new objects — The planner already records assumptions and normalizations; a named-but-unavailable repository surfaces there and in the UI as a one-click addition. No new table, and no auto-routing through the question resolver, which would put a trust decision on a model-facing path. - Repository hints are retained as descriptive planner context and grant nothing — Their meaning narrows; they are not deprecated or removed. - Scope changes are recorded on the initiative timeline — A new `scope_changed` run-event kind, with the actor and the repositories added or removed. Application mutations go to `audit_log`. - Removing a repository with planned or active work is refused at the operator level — The UI names the blocking work items and requires the operator to resolve them. Historical or terminal references never block removal, and never re-add a removed repository. - Orchestrator's Application record is operational; Hub's manifest is a peer to import from and export to — Synchronization is explicit and on demand. No runtime decision reads the manifest. - One Application association per initiative for V1 — Stored as context, nullable. Widening to several later breaks nothing, because scope is populated by value and never bounded by an Application. Settled as a V1 constraint; it does not change what must be built now. - The persisted model and enforcement land before any UI filtering is treated as sufficient — Sequencing is part of the design, not a delivery preference. - The filed-work-item defect is referenced, not absorbed — Xyence/orchestrator#610 is a pre-existing invariant violation with its own acceptance criteria. This initiative requires it satisfied during the enforcement phase; it does not restate or own it. ## Rejected / deferred alternatives - Code- or environment-configured Application membership — Rejected by decision membership is operator data and belongs in the database with UI management. The registry, not the storage medium, is the trust boundary. - Live-linking Applications to initiative scope — Editing a group would silently change the boundary of every initiative that used it. Scope is populated by value; re-sync is an explicit, recorded action. - Inheriting the single-membership constraint from release bundles — Its justification is deploy-order determinism, which scoping does not have. Hub's data already overlaps. - Inventing a new grouping abstraction — One logical concept, reusing the topology Application's semantics. - Coupling initiative isolation to live Hub-manifest reads — Orchestrator needs its own durable representation for an operational decision on the hot path. - Planner prompt scoping as the enforcement mechanism — Prompt instructions are not a boundary; persistence checks are. - Enforcing scope only in the worker — The worker cannot be the boundary for work that should never have been planned. Its check is defence in depth. - A separate discovery-request object — Planner assumptions already persist; adding a table would duplicate a mechanism that exists. - Reusing the `questions` queue for out-of-scope proposals — Questions are routed to a clarification resolver, which would place a trust decision on a model-facing path. - Blocking repository discovery entirely — #146's central case is a repository that turns out to be needed. It becomes a suggestion the operator admits, not a refusal. - Tombstoning removed scope entries — Keeping removed rows reintroduces the inference problem. Removal is a delete; the history lives on the append-only timeline. - [deferred] Folding release bundles into Applications — Bundles carry deploy dependencies and a validated exclusivity rule. They will overlap Applications and are untouched here. - [deferred] Replacing the per-repository `application` label — A repository now has several Applications, so the singular label cannot be derived from membership. It stays as a display field on the repository surfaces and is reconciled separately. - [deferred] Making `/orchestrator/content/applications` a projection of the Application catalog — The content-Application discovery surface stays as it is; unifying it is a follow-on, not a blocker. - [deferred] Converging the Application Platform's created-Application lifecycle with this catalog — The coherent end state, and the reason this brief keeps one concept, but it touches the create-Application flow, Hub manifest generation and platform status. Later architectural work; it changes nothing about what is built here. - [deferred] Repairing the Hub manifest ingest as a prerequisite — It is broken four ways and is not on the critical path. Seeding transcribes the manifest's Applications once; repair and two-way sync are sequenced after enforcement lands. ## Constraints - Two repositories change: `Xyence/orchestrator` (schema, domain, policy, services, planner, canonical API, standalone pages) and `Xyence/workspaces` (`apps/orchestrator` — the operator UI and its API client). - Schema changes are made in `packages/db/src/schema.ts` followed by `pnpm db:generate`; files under `packages/db/drizzle/` are never hand-edited. This initiative adds tables, columns, and two enum values. - `@orchestrator/domain` stays SDK-free and pure; the scope classifier and the admission/removal rules are pure functions with unit tests. - All model/provider output crossing an adapter boundary stays zod-validated and fails closed. - No workflow state outside PostgreSQL; every mutation records an actor string (`human:*`, `worker:*`, `agent:*`, `system`). - Application and scope mutations may never be authored by a model. The services accept only human or system actors for these operations. - `.github/workflows/**` is a protected path in both repositories. - No new credentials, tokens or auth schemes — the Workspaces app reuses its existing scoped client and platform session. - Existing initiatives, plans and in-flight waves keep working through the migration; no currently valid operation becomes invalid at cutover. - Canonical API changes are additive; no existing field changes meaning or disappears. - The topology manifest's failure modes stay non-fatal: a missing or malformed manifest degrades to guidance, never a crash. Delivery sequencing - Schema, domain model and services first: `applications`, `application_repositories`, `initiative_repositories`, `initiatives.primary_repository`, `initiatives.application_id`, `initiatives.repository_scope_state`, the `scope_changed` event kind, and the shared `assertRepositoryInScope` guard. - Migration and backfill second, with a dry-run report an operator can read before it is applied. - Enforcement third, at every seam in the inventory, beginning with the filed-work-item path — #610 — because it validates nothing today. That issue may land independently first; if it has not, this phase closes it. - Planner scoping fourth: scoped catalog, scope-derived continuation and roadmap selection, contract subset validation, scope suggestions from assumptions. - Canonical API fifth. - UI last, in both apps. No UI filtering is treated as satisfying an acceptance criterion until the corresponding service-layer check exists. ## Invariants - Every repository admitted to an Application is in the trusted registry at the moment of admission. - Every repository admitted to an initiative's scope is in the trusted registry at the moment of admission. - A repository may belong to zero, one, or many Applications. - An initiative's scope is not required to be a subset of its associated Application's membership, and no validation may enforce that relation. - `primaryRepository`, when non-null, is a member of the initiative's scope. - The repository context given to the planner is a subset of the initiative's scope. - No persisted plan or work item may newly target a repository outside its initiative's scope, regardless of what the planner returned. - No initiative-scoped execution or GitHub action may target a repository outside the initiative's scope. - Application membership changes never mutate an existing initiative's scope. - Repository hints grant no repository access; they are descriptive context only. - Historical work-item references never re-add a repository an operator removed from scope. - An initiative with `repository_scope_state = 'unset'` cannot be planned, and the refusal says so. - A frozen repository can never be admitted to an Application or an initiative's scope. - A repository frozen or removed from the trusted registry after admission remains in every Application and every initiative scope that holds it, marked as blocked; it is never removed automatically, its existing work stays intact, and no new planning, filing, execution or GitHub action may target it. - A blocked repository is excluded from scope population when an Application is used to populate an initiative, and never appears in the planner's catalog. - Removing a repository is refused while any work item targeting it is in a non-terminal state. - Every scope change and every Application mutation has a recorded human or system actor; no model-authored actor may perform either. - The brief source repository is never admitted as an implementation target. ## Repository scope - `Xyence/orchestrator` — schema and migrations, domain scope and Application models, policy admissibility helpers, services and the shared enforcement guard, planner scoping, canonical `/orchestrator/*` routes, standalone `apps/web` pages. - `Xyence/workspaces` — `apps/orchestrator`: initiative scope panel, new-initiative scope step, Applications management surface, scope-bound repository pickers, API client. ## Expected behavior - Data model - `applications` — `id`, `key` (stable slug, unique), `name`, `description`, `owningTeam` (nullable), `status` (`active` | `archived`), `source` (`seed` | `operator` | `topology_import`), `topologyId` (nullable link to a Hub manifest application id), `createdByActor`, timestamps. - `application_repositories` — `id`, `applicationId` (cascade delete), `repository`, `addedByActor`, `addedAt`; unique on `(applicationId, repository)` and a plain index on `repository` so "which Applications contain this repository" is cheap and multi-membership is expressly permitted. - `initiative_repositories` — `id`, `initiativeId` (cascade delete), `repository`, `origin` (`brief` | `operator` | `application` | `planner_suggestion` | `migration`), `sourceApplicationId` (nullable, `set null` on Application delete), `admittedByActor`, `admittedAt`; unique on `(initiativeId, repository)`. - `initiatives` gains `primaryRepository` (text, nullable), `applicationId` (uuid, nullable, `set null`), and `repositoryScopeState` (`unset` | `set`, default `unset`). - `run_event_kind` gains `scope_changed`; `audit_log` carries Application mutations with `entityType = "application"`. Application management - List Applications with name, key, description, owning team, member count and status; archived Applications are listed separately, not hidden. - Open an Application to see its repositories, each with registry status, frozen state, and the other Applications that repository belongs to. - Create an Application: key, name, description, optional owning team, and an initial repository selection drawn from the trusted registry. - Edit name, description and owning team; add and remove repositories. - Adding a repository not in the registry, or a frozen one, is refused with the reason stated in the picker before submission and re-checked at the service. - Archive an Application. Archiving hides it from selection while leaving every initiative that used it unchanged. Delete is permitted only when no initiative references it; otherwise the UI offers archive and says why. - A member that has since been frozen or removed from the registry is shown, marked, and excluded from scope population; startup validation reports it rather than deleting it. - Every Application shows which initiatives currently reference it. Initiative repository scope - The initiative detail page shows scope state, the associated Application if any, the primary repository, and every repository in scope with its origin, who admitted it, and when. - An initiative with `unset` scope shows a prompt to establish scope, and its planning affordances are disabled with the reason given. - Populate from an Application: selecting one previews exactly which repositories will be added, including any skipped for being frozen or absent from the registry, with the reason, before confirming. - Add a repository from the trusted registry. Ineligible repositories are shown with their reason rather than omitted, so "why can't I add this?" is answerable in the UI. - Remove a repository. If work items in a non-terminal state target it, the removal is refused and the UI lists those items with links, offers to open them, and asks the operator to resolve them first. The message is written for an operator, never surfaced as a constraint violation. - Set, change, or clear the primary repository. Removing the current primary from scope requires choosing another primary or explicitly clearing it, in the same interaction. - Scope suggestions appear inline with their evidence and Add to scope / Dismiss: a repository the planner named in its assumptions, a repository carried in `context.repositoryHints`, or one introduced by a brief commit. Dismissals persist in `initiatives.context.dismissedScopeSuggestions` — jsonb, so no table and no migration — and the planner is told on the next pass. - A repository in scope that is frozen or no longer in the registry is shown with a Blocked marker and the reason. Its work items stay visible and linked. Nothing new may target it, and the entry can be removed once the affected work is resolved — the same removal flow, with the same blockers. - Every change appears on the initiative timeline. Planning and work - The planner's catalog contains only in-scope repositories, with the primary marked, and carries the registry metadata it needs to reason about them — description, default branch, trusted validation keys, frozen state. - Continuation planning reads persisted scope; `continuationRepositoryHints()` is retired. - Roadmap candidate selection keys on scope rather than `repositoryHints`. - A plan whose item targets an out-of-scope repository is refused at persistence with a message naming the repository and the initiative's current scope. - A work item's `contract.allowedRepositories` must be a subset of the initiative's scope. - The plan editor's repository picker offers only in-scope repositories. - Filing a work item validates registry, frozen state and scope — closing the hole where it validates nothing. - Importing an issue, linking an epic, and declaring a related epic all validate scope in addition to their existing checks. - Materialization, PR creation and worker claim re-check scope, so an item persisted before enforcement cannot execute against a repository that is no longer permitted. - An initiative whose purpose is to create a repository admits that repository to scope from the registry before creation, exactly as `Xyence/www-xyence-io` is pre-registered today (#571). Canonical API - `GET /orchestrator/initiatives/{id}/repository-scope` — scope state, entries with origin and provenance, primary repository, associated Application, and per-entry removability with blockers. - `POST /orchestrator/initiatives/{id}/repository-scope/repositories` and `DELETE .../repositories/{repository}`. - `PUT /orchestrator/initiatives/{id}/repository-scope/primary` — set or clear. - `POST /orchestrator/initiatives/{id}/repository-scope/populate-from-application`. - `GET|POST /orchestrator/applications`, `GET|PATCH|DELETE /orchestrator/applications/{key}`, `POST|DELETE /orchestrator/applications/{key}/repositories`. - `GET /orchestrator/repositories` — the trusted registry catalog with frozen state and Application membership, so the Workspaces picker has a source that is not behind the engineering RBAC rule and the `REPOSITORIES_SURFACE_ENABLED` flag. - `canonicalInitiativeSummary` gains a compact `repositoryScope` (primary, count, Application key); the detail read carries the full scope. Workspaces operator app - Initiative detail: the scope panel and every mutation above. - `/initiatives/new`: an Application selector and repository scope step, with the brief's declared `repositoryScope` pre-proposed and requiring confirmation. - An Applications section: list, detail, and management. - Every repository selector that has an initiative in context is bound to that initiative's scope. - Enforcement - Creating a plan version with an item targeting an out-of-scope repository is refused, and the error names the repository and the current scope. - Filing a work item against a repository outside the registry, frozen, or outside scope is refused at the service, and via both `POST /orchestrator/initiatives/{id}/work-items/file` and `POST /api/initiatives/{id}/work-items`. - Xyence/orchestrator#610 is satisfied — closed independently before this initiative, or by its enforcement phase. Its acceptance criteria are not restated here; they are a precondition of this one. - Amending a work item's repository to an out-of-scope target is refused. - Importing an issue, linking an epic, and declaring a related epic from an out-of-scope repository are each refused. - A work item whose repository left scope cannot be claimed for execution, cannot have an issue materialized, and cannot have a PR opened. - A work item whose `contract.allowedRepositories` is not a subset of scope is refused at persistence. - Every refusal above is proven by a test that calls the service directly, not the route, and not the UI. Planner scoping - The catalog handed to the planner for an initiative equals its persisted scope; a registry repository outside scope never appears. - A synthetic planner output naming an out-of-scope repository is rejected on persistence, with the rest of the wave unaffected where the plan format permits. - Continuation planning for an initiative whose work touched a repository since removed from scope does not reintroduce it. - `repositoryHints` containing an out-of-scope repository grants no access and produces no work item. Applications - An Application cannot be created or edited to contain a repository outside the trusted registry. - The same repository can be added to two Applications, and both list it. - An initiative populated from an Application, then given an additional trusted repository not in that Application, is valid: the addition succeeds, planning proceeds, and no validation refuses it. - A member frozen or removed from the registry after admission stays listed on the Application, is reported by startup validation, and is skipped when that Application populates a scope. - Startup validation reports any Application member missing from the registry, the way bundle validation already does. - Removing a repository from an Application leaves every initiative scope that was populated from it unchanged. - Archiving an Application removes it from selection and changes no initiative scope; deleting one that an initiative references is refused with archive offered instead. Initiative scope - `primaryRepository` cannot be set to a repository outside scope. - Removing the primary repository from scope requires selecting a new primary or clearing it explicitly. - An initiative created without scope has `repository_scope_state = 'unset'` and cannot be planned; the refusal explains what to do. - An initiative with a deliberately empty scope is distinguishable from an unset one in the API and the UI. - Removing a repository with a non-terminal work item is refused and the response identifies those items. - Removing a repository whose only references are terminal succeeds, and no later operation re-adds it. - Every scope change writes a `scope_changed` event with the actor and the repositories affected. - A repository frozen after admission remains in scope, is marked blocked, and refuses new planning, filing, execution and GitHub actions; its existing work items remain readable and linked. - A repository removed from the trusted registry after admission behaves identically, and no migration, startup validation, or background process deletes the entry. - A blocked repository is absent from the planner's catalog and from Add-to-scope pickers, while still visible in the scope panel. Migration - Every existing initiative has a persisted scope, or `unset` with a reason recorded. - No repository whose only evidence is `context.repositoryHints` is admitted by the migration to any initiative. - An initiative whose only evidence is hints is left `unset`, and its trusted hints appear as scope suggestions. - The migration produces a report, runnable as a dry run, listing each initiative's derived scope, each entry's evidence, every hint-derived suggestion it declined to admit, and every initiative left `unset`. - Migrated entries whose only evidence is a committed brief declaration — no work item — are marked in the UI as needing review. - No initiative in a terminal state is blocked by migration. - Seeded Applications match the transcribed Hub manifest, minus any member absent from the registry, and each omission is logged. UI - Every operator capability in this brief is exercisable in the Workspaces app without an API client, and each has a UI test. - Repository pickers with initiative context offer only in-scope repositories. - An ineligible repository is visible with its reason rather than absent. - A blocked removal renders an operator-level explanation naming the blocking work, never a raw constraint error. Migration and compatibility behavior - Scope is migrated only from evidence that already represented an operational commitment: the repositories of the initiative's work items in any state, and the repositories its committed brief declares in `repositoryScope`. Both are intersected with the trusted registry, with the brief source repository removed. Entries are written with origin `migration` and actor `system`, and are individually reviewable and removable. - `context.repositoryHints` are **not** admitted. Hints have never been an authorization boundary — the planner prompt explicitly permits dropping them — so promoting a historical hint into a persisted permission would manufacture exactly the accidental expansion this initiative exists to prevent. Trusted hints that are not already in scope are rendered as migration-time scope suggestions, labelled as coming from historical repository hints, with Add and Dismiss. - Where an initiative's only evidence is hints, scope is left `unset` and the operator establishes the boundary. Where there is no evidence at all, it is likewise `unset` rather than guessed. Non-terminal initiatives in that state block planning with an explanation; terminal initiatives are recorded and never blocked. - `primaryRepository` is backfilled only where unambiguous — a single candidate, or a repository the committed brief names as primary. Otherwise it stays null. - `context.repositoryHints` is retained with a narrowed meaning: descriptive planner context that grants nothing. The brief-commit overwrite at `apps/web/src/app/api/initiatives/[id]/brief/commit/route.ts:39` remains, and a commit naming a repository outside scope produces a scope suggestion rather than widening anything. - The brief's `sections.repositoryScope` is retained as declared intent. At intake it proposes scope entries; admission stays explicit, and intake reports declared repositories it could not admit — the same "report what was discarded" contract as #488. - `deriveInitiativeRepositoryScope` and `expectedRepositoriesFromBriefScope` are retained: the first as a display classifier over persisted entries, the second for intake proposals. Neither remains a source of truth. - `/applications` and `/applications/repositories` switch to the persisted catalog. `loadTopologyManifest` stays for the repository-relationship view and becomes the import source once repaired; no runtime scope decision reads it. - `RELEASE_BUNDLES` is untouched, and its single-membership validation is explicitly not applied to Applications. - Canonical serialization gains fields only. `workItemCount`, `brief`, `state` and every other existing field keep their meaning. - Two sources of truth do not persist: after migration, derivation-from-hints is removed from every decision path in the same change that introduces the persisted read. ## Open questions - None outstanding. Two questions raised during review were settled rather than left open: an initiative references at most one Application in V1, and convergence with the Application Platform's created-Application lifecycle is later architectural work. Neither changes what must be built now, and neither should hold up planning.

hints: `Xyence/orchestrator` — schema and migrations, domain scope and Application models, policy admissibility helpers, services and the shared enforcement guard, planner scoping, canonical `/orchestrator/*` routes, standalone `apps/web` pages., `Xyence/workspaces` — `apps/orchestrator`: initiative scope panel, new-initiative scope step, Applications management surface, scope-bound repository pickers, API client.created 8/25/2026, 3:19:02 PMby human:workspaces-orchestrator-app

Repository scope

targets the work spans — independent of where the brief was authored

Primary repository: orchestrator` — schema and migrations, domain scope and Application models, policy admissibility helpers, services and the shared enforcement guard, planner scoping, canonical `

  • orchestrator` — schema and migrations, domain scope and Application models, policy admissibility helpers, services and the shared enforcement guard, planner scoping, canonical `expected · primary
  • workspaces` — `appsexpected

Initiative Brief

Committed

The Initiative Brief is the reviewed contract Orchestrator plans and executes against. Source material (imported issues, direct intent) is provenance — nothing becomes binding until you commit, and a committed revision is never rewritten in place.

Committed brief — revision 1 (material)
Desired outcome
An initiative has a **durable, authoritative repository scope** stored in PostgreSQL. That set — not the brief, not `repositoryHints`, not the repositories its work items happen to reference — is the answer to "what may this initiative touch?" Every service that creates or mutates repository-targeted state on behalf of an initiative consults it and fails explicitly when a target falls outside it. Scope never widens as a side effect of planning, brief commits, hints, or history. **Applications** are first-class persisted Orchestrator records: a named logical group of repositories drawn from the trusted registry, with membership managed through the UI. A repository may belong to many Applications. An operator picks an Application when setting up an initiative and uses it to **populate** the initiative's scope; from that moment the initiative's scope is its own set, freely edited, and later changes to the Application never mutate it. The association is kept as context, not as a rule. `primaryRepository` becomes a real, nullable, persisted, operator-selectable property of the initiative — not a derivation input every caller omits. Three concepts, with **one** trust boundary and two independent memberships beneath it: 1. **Trusted repository registry** (code, ADR-0006) — what Orchestrator may operate on at all. The only trust boundary. 2. **Application** (persisted, UI-managed) — a logical grouping drawn from the registry. An input to scope, not a parent boundary over it. 3. **Initiative repository scope** (persisted, per initiative) — what *this* initiative may touch, drawn from the registry. Application membership and initiative scope are each independently constrained by the registry, and **not by each other**. An initiative populated from the "Outshine" Application may then add `Xyence/hub` because this particular piece of work needs it; the result is a scope that is not a subset of any Application, and that is a correct and expected outcome. Cross-application work is the case this design exists to support. Everything an operator needs to run this is in the UI, and served over `/orchestrator/*` so the Workspaces operator app uses the same surface rather than reaching into Orchestrator-only state.
Motivation
Issue #146 asked for repository-*independent* initiatives. #147 delivered a pure classifier (`deriveInitiativeRepositoryScope`) and a read-only panel, and stopped there. The classification has no storage, no operator control, and no consequence, so "scope" today is a description of what already happened rather than a boundary. The gap is not theoretical. Repository scope is recomputed on every render from the brief's `repositoryScope`, `context.repositoryHints` (jsonb, overwritten wholesale on brief commit), and the distinct repositories of existing work items. Nothing consults it. The planner receives the entire trusted registry on every pass, with hints reduced to a prompt line it is explicitly permitted to drop. Plan materialization checks registry membership, frozen state and the item's own contract — never the initiative. So an initiative whose brief declares one repository can silently acquire work in five others, and the only signal is the same read-only panel, after the fact, labelled "discovered". Two of #146's own requirements were never met at all: the operator-designated primary repository is an input no caller supplies, and "remove repositories that are no longer required" has no implementation anywhere. Worse, one canonical route accepts a repository with no validation of any kind. `fileWorkItem` (`packages/db/src/services/filedWorkItems.ts:43`) runs no registry check, no frozen check and no scope check; it persists the work item and sets `contract.allowedRepositories` to whatever string it was handed. `POST /orchestrator/initiatives/[id]/work-items/file` (#608, shipped 2026-08-24) exposes that to any authenticated operator or automation. Materialization later refuses with "untrusted repository" non-fatally, so the row persists in `PLANNED` and the wave — whose continuation means every item complete — cannot close. This is precisely why UI filtering cannot be the boundary. It is tracked independently as Xyence/orchestrator#610, because the registry, frozen and contract-consistency checks it is missing are owed whether or not scope enforcement is ever built. Separately, the system needs repository groups and has grown four incompatible partial answers — a Hub-manifest Application whose ingest has never worked, a per-repository `application` label that cannot express a group, the Application Platform's single-repository Applications with no table behind them, and release bundles, which work but are release-only. An operator scoping an initiative to "the platform" has no way to say so.
Current state
• What #146/#147 left standing • `packages/domain/src/initiativeScope.ts` — `deriveInitiativeRepositoryScope` and `expectedRepositoriesFromBriefScope`. Pure, well tested, recomputed per render. • Two consumers only: `apps/web/src/components/initiativeScopeView.ts`, and `packages/db/src/services/briefIntake.ts:300`, which seeds `context.repositoryHints` at import. • `apps/web/src/app/initiatives/[id]/page.tsx:431` calls the view with `briefSourceRepository`, `expectedRepositories` (brief `repositoryScope` + `context.repositoryHints`) and `activeRepositories` (distinct work-item repositories). It passes neither `primaryRepository` nor `discoveredRepositories`, so the primary is always the first expected repository and "discovered" means only "carries work but was not expected". • No durable scope object exists. `initiatives.context` (jsonb) holds `repositoryHints`, writable at creation, via `PATCH /api/initiatives/[id]`, and overwritten wholesale on brief commit (`apps/web/src/app/api/initiatives/[id]/brief/commit/route.ts:39`). • `packages/db/src/services/epics.ts:98` creates initiatives with `repositoryHints: [epic.repository]` — a second seeding path. Where repository targets are validated today, and against what — Every site below enforces the same two rules — trusted registry and not frozen — and none of them knows what initiative it is serving. | Seam | Location | Checks today | | --- | --- | --- | | Plan persistence | `packages/db/src/services/plans.ts:311-343` | contract `allowedRepositories`, `isKnownRepository`, `isFrozenRepository`, trusted validation keys | | Work-item amendment | `packages/db/src/services/workItems.ts:189-206` | same four | | Filed work item | `packages/db/src/services/filedWorkItems.ts:43` | **none** — tracked as #610 | | Issue import | `packages/db/src/services/importedIssues.ts:154-175` | registry, frozen, issue eligibility | | Epic link | `packages/db/src/services/epics.ts:58-63` | registry, frozen | | Related-epic declaration | `packages/db/src/services/relatedEpics.ts:65` | registry | | GitHub issue materialization | `packages/github/src/sync.ts:169` | registry (per item, non-fatal) | | PR creation | `packages/github/src/sync.ts:278` | repository read from the work item; run/lease ownership | | Worker claim | `packages/db/src/services/runs.ts:235-240` | worker credential capabilities only | The planner's world • `repositoryCatalog()` (`apps/web/src/server/planner.ts:74-86`) maps all of `REPOSITORIES`, unfiltered, and is used for first-wave (`:439`) and continuation (`:824`) planning alike. • The prompt says: *"Target ONLY repositories from the provided catalog. You may add or drop the operator's repository hints, but explain why in assumptions."* • `continuationRepositoryHints()` (`:805`) unions expected with already-modified repositories — widening the hint, constraining nothing. • `roadmapCandidatesFor()` (`:131`) selects roadmap candidates from `repositoryHints`. • Planner assumptions and normalizations already have a persistence path (`recordPlannerNormalizations`, `:455`). The UI • Every repository control on the standalone initiative page binds to `KNOWN_REPOSITORY_NAMES`: plan editor (`:673`), issue assembly (`:238`, `:701`), importer (`:641`, frozen filtered). • The Workspaces operator app (`Xyence/workspaces`, `apps/orchestrator`) has **no** repository surface at all — not on initiative detail, the list, or `/initiatives/new` — because `apps/web/src/server/canonicalInitiatives.ts` exposes no repository data. • `/api/repositories` lists the trusted registry but is gated behind an ENGINEERING RBAC middleware rule and the `REPOSITORIES_SURFACE_ENABLED` flag, which is off by default. It cannot serve a repository picker in Workspaces. The four meanings of "Application" • **`TopologyApplication`** (`packages/domain/src/topology.ts`) — id, name, owningTeam, components, `repositories[]`. Already a repository group, read from a Hub manifest at request time, persisted nowhere, consumed by `/applications` and `/applications/repositories` and nothing else. • **`PLATFORM_APPLICATION_MAPPING_SEED`** (`packages/policy/src/platformMapping.ts`) — a per-repository `application` label, strictly 1:1, seeded in code, overridable by `REPOSITORY_PLATFORM_MAPPING_JSON`. • **Application Platform Applications** (#501/#502/#508/#585) — one repository each. The Hub-manifest guide states Orchestrator owns Application identity and lifecycle, but there is no `applications` table in `packages/db/src/schema.ts`, and platform status resolves through a single hardcoded slug (`applicationStatus.ts`). • **`RELEASE_BUNDLES`** (`packages/policy/src/bundles.ts`) — the only functioning group: named, deploy-ordered, validated at startup in both directions. Release-only, a code constant, and it forbids a repository from belonging to two bundles because deploy order must be unambiguous. The Hub topology manifest ingest has never worked • `topologyManifest.ts:57` looks for `platform-topology.json`; Hub publishes `platform-topology/topology.yaml`. • `topologyManifest.ts:260` calls `JSON.parse`. The manifest is YAML; there is no YAML parser on that path. • The schema requires `version: number`, `owningTeam` per application and `repositories: ["org/name"]`. Hub writes `schema_version: "1.0"`, carries no per-application team, and uses `primary_repos: [outshine, hub]` — short keys. • Consequently `/applications` resolves to `unavailable` against the real manifest and has never displayed it. Overlapping membership is already the reality — Hub's manifest states the principle in its own header: *"A repository may contribute components to more than one application (e.g. hub → Platform and Orchestrator), which is exactly why applications — not repositories — are the top-level abstraction."* Its current data agrees: `outshine` is in `platform`, `content-management` and `console`; `hub` is in `platform` and `orchestrator`; `workspaces` is in `workspaces` and `orchestrator`; `stlouis-xyence-io` is in `content-management` and `st-louis-site`. Its Applications include cross-cutting capabilities such as `content-management` (`status: in-development`, spanning three repositories) — exactly the shape an initiative wants to scope to.
Product principles
• Scope is a decision, not a description. The operator states it; the system never infers it from what has already been touched. • An Application is a starting point, not a leash. It populates a scope and is then remembered as context; it never changes a scope on its own. • A refusal explains itself in operator language. "Xyence/console is not in this initiative's scope" with the current scope and the action that would change it — never a database constraint error. • Removing a repository is answered at the level of the work, not the row. If planned or active work targets it, the operator is shown that work and asked to resolve it. • One action for the common case: scoping to a known product is picking an Application. • Nothing in flight breaks. Every existing initiative is given a scope matching what it is already permitted to do, before anything is enforced. • Every operator capability here exists in the UI. A capability reachable only by API is not delivered. • The operator does this in the Workspaces app; standalone pages keep pace as break-glass.
Architectural principles
• Repository scope, Applications and Application membership are durable PostgreSQL state. Nothing about the boundary is reconstructed at read time. • There is exactly one trust boundary — the trusted registry — and two independent memberships beneath it: registry ⊇ Application membership, and, separately, registry ⊇ initiative scope. Applications populate scope by value; they never bound it afterward. No validation anywhere may require an initiative's scope to be a subset of its associated Application's membership. • Admission is checked at admission time, and admission is the only moment the registry gates. A repository that is frozen or leaves the registry after admission is retained, marked, and blocked — never silently deleted from an Application or an initiative. Boundaries move because an actor moved them. • The trusted registry stays code-owned under ADR-0006. Applications are operator data: every mutation carries a human actor, is validated against the registry, and is audited. "Trusted operator configuration, never model output" is satisfied by actor and validation, not by the file the data lives in — no planner, agent or model output may create or alter an Application. • Enforcement lives in the domain/service layer, not in routes and not in the UI. One shared guard is called by every seam that accepts an initiative identity and a repository target. • The planner is given a scoped world *and* its output is re-validated on persistence. Narrowing the catalog is an efficiency and honesty measure; the persistence check is the guarantee. • The existing pure derivation stays pure and becomes a **display classifier over persisted entries** — origin drives status, replacing set arithmetic over hints and work items. • Orchestrator owns the operational Application record. The Hub manifest is an import/export peer, reconciled explicitly on demand, never read on the request path for a runtime decision. • Scope mutations are recorded on the initiative's existing append-only timeline (`run_events`) with an actor; Application mutations are recorded in `audit_log`. • The canonical surface distinguishes an unset scope from an empty one, and additive fields only — no existing canonical field changes meaning.
Domain concepts
• Trusted repository registry: `REPOSITORIES` in `@orchestrator/policy`. The set Orchestrator may operate on at all. Code-owned, ADR-0006. Unchanged by this initiative. • Application: a persisted, named logical group of trusted repositories, managed through the UI. A repository may belong to zero, one, or many. An Application is a template an operator populates a scope from; it does not bound that scope afterward. • Application membership: the persisted set of repositories in an Application. Validated against the registry on every write and at startup. • Initiative repository scope: the persisted, authoritative set of repositories one initiative may operate on. Its own set, not a view of anything else. • Scope state: whether an initiative's scope has been established (`set`) or never was (`unset`). An unset scope is not an empty scope unset blocks planning with an explanation; empty is a deliberate operator statement. • Admission: the recorded act of adding a repository to an initiative's scope actor, time, origin, and the Application it came from when it came from one. • Origin: why a repository is in scope `brief`, `operator`, `application`, `planner_suggestion`, or `migration`. Origin is provenance for display and review; it grants nothing. • Blocked entry: a repository still in an Application or an initiative's scope that has since been frozen or removed from the trusted registry. It stays visible and marked, its historical work stays intact, no new repository-targeted operation may use it, and only an operator can remove it. • Primary repository: the operator-designated principal repository of an initiative. Nullable and never invented; when set, always a member of scope. • Associated Application: the Application an initiative's scope was populated from, retained as context. It has no ongoing effect on scope. • Scope suggestion: a trusted repository proposed for scope but not admitted named by the planner in its assumptions, carried in `context.repositoryHints`, or declared by a brief commit. Rendered with Add and Dismiss. Not a work item, not a question, and never an automatic admission. • Trusted repository registry entry: repository policy description, default branch, validation commands, protected paths, frozen flag. Supplies the metadata the planner and UI show for in-scope repositories. • Release bundle: the existing release-time group with deploy ordering. Unrelated to Applications and unchanged.
Decisions
• Applications are first-class persisted Orchestrator data — Membership is database state managed through the UI, not code or environment configuration. Seeding from trusted config or topology data is initialization only; the persisted record is the operational source of truth thereafter. • The trusted registry remains the only trust boundary — Application membership and initiative scope are each validated against it on admission, independently of one another. Membership can never launder an untrusted or frozen repository into an initiative. • An initiative's scope is not required to be a subset of its associated Application — An operator may add any trusted repository the work needs. Encoding the subset relation as a validation rule would forbid exactly the cross-application work this design exists to support, so no such rule may be written. • A repository frozen or removed from the registry after admission stays visible and blocked — It is not deleted from any Application or initiative scope. It is marked, its existing work stays intact and legible, every new repository-targeted operation against it is refused, and only an explicit operator action removes it once the affected work is resolved. Scope boundaries move because an actor moves them, never because upstream configuration changed. • A repository may belong to many Applications — Hub's manifest asserts this as the reason Applications are the top-level abstraction, and its current data already does it in four places. `RELEASE_BUNDLES`' single-membership rule exists because deploy order must be unambiguous; that reason does not transfer to scoping, so the constraint is not inherited. • Initiative repository scope is persisted on the initiative — A dedicated membership table, authoritative after migration. Derivation from hints, brief sections and work items is retired as a source of truth and retained only as a display classifier and as migration input. • Applications populate scope; they do not define it — Selecting an Application copies its repositories into the initiative's scope as ordinary entries. The association is stored as context. A later membership change never mutates an existing initiative's scope. • `primaryRepository` is a persisted, nullable initiative column — Operator-selectable, exposed canonically, required to be in scope when set, and never invented when the initiative does not need one. • Enforcement is a single shared domain guard called at every seam — Not a route check, not a UI filter, and not the planner prompt. Every service accepting an initiative identity and a repository target calls it. • The planner receives only in-scope repositories, and its output is re-validated on persistence — Prompt scoping alone is not enforcement; malformed or generated output naming an out-of-scope repository is rejected at write time. • Out-of-scope planner proposals become scope suggestions, not new objects — The planner already records assumptions and normalizations; a named-but-unavailable repository surfaces there and in the UI as a one-click addition. No new table, and no auto-routing through the question resolver, which would put a trust decision on a model-facing path. • Repository hints are retained as descriptive planner context and grant nothing — Their meaning narrows; they are not deprecated or removed. • Scope changes are recorded on the initiative timeline — A new `scope_changed` run-event kind, with the actor and the repositories added or removed. Application mutations go to `audit_log`. • Removing a repository with planned or active work is refused at the operator level — The UI names the blocking work items and requires the operator to resolve them. Historical or terminal references never block removal, and never re-add a removed repository. • Orchestrator's Application record is operational; Hub's manifest is a peer to import from and export to — Synchronization is explicit and on demand. No runtime decision reads the manifest. • One Application association per initiative for V1 — Stored as context, nullable. Widening to several later breaks nothing, because scope is populated by value and never bounded by an Application. Settled as a V1 constraint; it does not change what must be built now. • The persisted model and enforcement land before any UI filtering is treated as sufficient — Sequencing is part of the design, not a delivery preference. • The filed-work-item defect is referenced, not absorbed — Xyence/orchestrator#610 is a pre-existing invariant violation with its own acceptance criteria. This initiative requires it satisfied during the enforcement phase; it does not restate or own it.
Rejected / deferred alternatives
• Code- or environment-configured Application membership — Rejected by decision membership is operator data and belongs in the database with UI management. The registry, not the storage medium, is the trust boundary. • Live-linking Applications to initiative scope — Editing a group would silently change the boundary of every initiative that used it. Scope is populated by value; re-sync is an explicit, recorded action. • Inheriting the single-membership constraint from release bundles — Its justification is deploy-order determinism, which scoping does not have. Hub's data already overlaps. • Inventing a new grouping abstraction — One logical concept, reusing the topology Application's semantics. • Coupling initiative isolation to live Hub-manifest reads — Orchestrator needs its own durable representation for an operational decision on the hot path. • Planner prompt scoping as the enforcement mechanism — Prompt instructions are not a boundary; persistence checks are. • Enforcing scope only in the worker — The worker cannot be the boundary for work that should never have been planned. Its check is defence in depth. • A separate discovery-request object — Planner assumptions already persist; adding a table would duplicate a mechanism that exists. • Reusing the `questions` queue for out-of-scope proposals — Questions are routed to a clarification resolver, which would place a trust decision on a model-facing path. • Blocking repository discovery entirely — #146's central case is a repository that turns out to be needed. It becomes a suggestion the operator admits, not a refusal. • Tombstoning removed scope entries — Keeping removed rows reintroduces the inference problem. Removal is a delete; the history lives on the append-only timeline. • [deferred] Folding release bundles into Applications — Bundles carry deploy dependencies and a validated exclusivity rule. They will overlap Applications and are untouched here. • [deferred] Replacing the per-repository `application` label — A repository now has several Applications, so the singular label cannot be derived from membership. It stays as a display field on the repository surfaces and is reconciled separately. • [deferred] Making `/orchestrator/content/applications` a projection of the Application catalog — The content-Application discovery surface stays as it is; unifying it is a follow-on, not a blocker. • [deferred] Converging the Application Platform's created-Application lifecycle with this catalog — The coherent end state, and the reason this brief keeps one concept, but it touches the create-Application flow, Hub manifest generation and platform status. Later architectural work; it changes nothing about what is built here. • [deferred] Repairing the Hub manifest ingest as a prerequisite — It is broken four ways and is not on the critical path. Seeding transcribes the manifest's Applications once; repair and two-way sync are sequenced after enforcement lands.
Constraints
• Two repositories change: `Xyence/orchestrator` (schema, domain, policy, services, planner, canonical API, standalone pages) and `Xyence/workspaces` (`apps/orchestrator` — the operator UI and its API client). • Schema changes are made in `packages/db/src/schema.ts` followed by `pnpm db:generate`; files under `packages/db/drizzle/` are never hand-edited. This initiative adds tables, columns, and two enum values. • `@orchestrator/domain` stays SDK-free and pure; the scope classifier and the admission/removal rules are pure functions with unit tests. • All model/provider output crossing an adapter boundary stays zod-validated and fails closed. • No workflow state outside PostgreSQL; every mutation records an actor string (`human:*`, `worker:*`, `agent:*`, `system`). • Application and scope mutations may never be authored by a model. The services accept only human or system actors for these operations. • `.github/workflows/**` is a protected path in both repositories. • No new credentials, tokens or auth schemes — the Workspaces app reuses its existing scoped client and platform session. • Existing initiatives, plans and in-flight waves keep working through the migration; no currently valid operation becomes invalid at cutover. • Canonical API changes are additive; no existing field changes meaning or disappears. • The topology manifest's failure modes stay non-fatal: a missing or malformed manifest degrades to guidance, never a crash. Delivery sequencing • Schema, domain model and services first: `applications`, `application_repositories`, `initiative_repositories`, `initiatives.primary_repository`, `initiatives.application_id`, `initiatives.repository_scope_state`, the `scope_changed` event kind, and the shared `assertRepositoryInScope` guard. • Migration and backfill second, with a dry-run report an operator can read before it is applied. • Enforcement third, at every seam in the inventory, beginning with the filed-work-item path — #610 — because it validates nothing today. That issue may land independently first; if it has not, this phase closes it. • Planner scoping fourth: scoped catalog, scope-derived continuation and roadmap selection, contract subset validation, scope suggestions from assumptions. • Canonical API fifth. • UI last, in both apps. No UI filtering is treated as satisfying an acceptance criterion until the corresponding service-layer check exists.
Invariants
• Every repository admitted to an Application is in the trusted registry at the moment of admission. • Every repository admitted to an initiative's scope is in the trusted registry at the moment of admission. • A repository may belong to zero, one, or many Applications. • An initiative's scope is not required to be a subset of its associated Application's membership, and no validation may enforce that relation. • `primaryRepository`, when non-null, is a member of the initiative's scope. • The repository context given to the planner is a subset of the initiative's scope. • No persisted plan or work item may newly target a repository outside its initiative's scope, regardless of what the planner returned. • No initiative-scoped execution or GitHub action may target a repository outside the initiative's scope. • Application membership changes never mutate an existing initiative's scope. • Repository hints grant no repository access; they are descriptive context only. • Historical work-item references never re-add a repository an operator removed from scope. • An initiative with `repository_scope_state = 'unset'` cannot be planned, and the refusal says so. • A frozen repository can never be admitted to an Application or an initiative's scope. • A repository frozen or removed from the trusted registry after admission remains in every Application and every initiative scope that holds it, marked as blocked; it is never removed automatically, its existing work stays intact, and no new planning, filing, execution or GitHub action may target it. • A blocked repository is excluded from scope population when an Application is used to populate an initiative, and never appears in the planner's catalog. • Removing a repository is refused while any work item targeting it is in a non-terminal state. • Every scope change and every Application mutation has a recorded human or system actor; no model-authored actor may perform either. • The brief source repository is never admitted as an implementation target.
Repository scope
• `Xyence/orchestrator` — schema and migrations, domain scope and Application models, policy admissibility helpers, services and the shared enforcement guard, planner scoping, canonical `/orchestrator/*` routes, standalone `apps/web` pages. • `Xyence/workspaces` — `apps/orchestrator`: initiative scope panel, new-initiative scope step, Applications management surface, scope-bound repository pickers, API client.
Expected behavior
• Data model • `applications` — `id`, `key` (stable slug, unique), `name`, `description`, `owningTeam` (nullable), `status` (`active` | `archived`), `source` (`seed` | `operator` | `topology_import`), `topologyId` (nullable link to a Hub manifest application id), `createdByActor`, timestamps. • `application_repositories` — `id`, `applicationId` (cascade delete), `repository`, `addedByActor`, `addedAt`; unique on `(applicationId, repository)` and a plain index on `repository` so "which Applications contain this repository" is cheap and multi-membership is expressly permitted. • `initiative_repositories` — `id`, `initiativeId` (cascade delete), `repository`, `origin` (`brief` | `operator` | `application` | `planner_suggestion` | `migration`), `sourceApplicationId` (nullable, `set null` on Application delete), `admittedByActor`, `admittedAt`; unique on `(initiativeId, repository)`. • `initiatives` gains `primaryRepository` (text, nullable), `applicationId` (uuid, nullable, `set null`), and `repositoryScopeState` (`unset` | `set`, default `unset`). • `run_event_kind` gains `scope_changed`; `audit_log` carries Application mutations with `entityType = "application"`. Application management • List Applications with name, key, description, owning team, member count and status; archived Applications are listed separately, not hidden. • Open an Application to see its repositories, each with registry status, frozen state, and the other Applications that repository belongs to. • Create an Application: key, name, description, optional owning team, and an initial repository selection drawn from the trusted registry. • Edit name, description and owning team; add and remove repositories. • Adding a repository not in the registry, or a frozen one, is refused with the reason stated in the picker before submission and re-checked at the service. • Archive an Application. Archiving hides it from selection while leaving every initiative that used it unchanged. Delete is permitted only when no initiative references it; otherwise the UI offers archive and says why. • A member that has since been frozen or removed from the registry is shown, marked, and excluded from scope population; startup validation reports it rather than deleting it. • Every Application shows which initiatives currently reference it. Initiative repository scope • The initiative detail page shows scope state, the associated Application if any, the primary repository, and every repository in scope with its origin, who admitted it, and when. • An initiative with `unset` scope shows a prompt to establish scope, and its planning affordances are disabled with the reason given. • Populate from an Application: selecting one previews exactly which repositories will be added, including any skipped for being frozen or absent from the registry, with the reason, before confirming. • Add a repository from the trusted registry. Ineligible repositories are shown with their reason rather than omitted, so "why can't I add this?" is answerable in the UI. • Remove a repository. If work items in a non-terminal state target it, the removal is refused and the UI lists those items with links, offers to open them, and asks the operator to resolve them first. The message is written for an operator, never surfaced as a constraint violation. • Set, change, or clear the primary repository. Removing the current primary from scope requires choosing another primary or explicitly clearing it, in the same interaction. • Scope suggestions appear inline with their evidence and Add to scope / Dismiss: a repository the planner named in its assumptions, a repository carried in `context.repositoryHints`, or one introduced by a brief commit. Dismissals persist in `initiatives.context.dismissedScopeSuggestions` — jsonb, so no table and no migration — and the planner is told on the next pass. • A repository in scope that is frozen or no longer in the registry is shown with a Blocked marker and the reason. Its work items stay visible and linked. Nothing new may target it, and the entry can be removed once the affected work is resolved — the same removal flow, with the same blockers. • Every change appears on the initiative timeline. Planning and work • The planner's catalog contains only in-scope repositories, with the primary marked, and carries the registry metadata it needs to reason about them — description, default branch, trusted validation keys, frozen state. • Continuation planning reads persisted scope; `continuationRepositoryHints()` is retired. • Roadmap candidate selection keys on scope rather than `repositoryHints`. • A plan whose item targets an out-of-scope repository is refused at persistence with a message naming the repository and the initiative's current scope. • A work item's `contract.allowedRepositories` must be a subset of the initiative's scope. • The plan editor's repository picker offers only in-scope repositories. • Filing a work item validates registry, frozen state and scope — closing the hole where it validates nothing. • Importing an issue, linking an epic, and declaring a related epic all validate scope in addition to their existing checks. • Materialization, PR creation and worker claim re-check scope, so an item persisted before enforcement cannot execute against a repository that is no longer permitted. • An initiative whose purpose is to create a repository admits that repository to scope from the registry before creation, exactly as `Xyence/www-xyence-io` is pre-registered today (#571). Canonical API • `GET /orchestrator/initiatives/{id}/repository-scope` — scope state, entries with origin and provenance, primary repository, associated Application, and per-entry removability with blockers. • `POST /orchestrator/initiatives/{id}/repository-scope/repositories` and `DELETE .../repositories/{repository}`. • `PUT /orchestrator/initiatives/{id}/repository-scope/primary` — set or clear. • `POST /orchestrator/initiatives/{id}/repository-scope/populate-from-application`. • `GET|POST /orchestrator/applications`, `GET|PATCH|DELETE /orchestrator/applications/{key}`, `POST|DELETE /orchestrator/applications/{key}/repositories`. • `GET /orchestrator/repositories` — the trusted registry catalog with frozen state and Application membership, so the Workspaces picker has a source that is not behind the engineering RBAC rule and the `REPOSITORIES_SURFACE_ENABLED` flag. • `canonicalInitiativeSummary` gains a compact `repositoryScope` (primary, count, Application key); the detail read carries the full scope. Workspaces operator app • Initiative detail: the scope panel and every mutation above. • `/initiatives/new`: an Application selector and repository scope step, with the brief's declared `repositoryScope` pre-proposed and requiring confirmation. • An Applications section: list, detail, and management. • Every repository selector that has an initiative in context is bound to that initiative's scope. • Enforcement • Creating a plan version with an item targeting an out-of-scope repository is refused, and the error names the repository and the current scope. • Filing a work item against a repository outside the registry, frozen, or outside scope is refused at the service, and via both `POST /orchestrator/initiatives/{id}/work-items/file` and `POST /api/initiatives/{id}/work-items`. • Xyence/orchestrator#610 is satisfied — closed independently before this initiative, or by its enforcement phase. Its acceptance criteria are not restated here; they are a precondition of this one. • Amending a work item's repository to an out-of-scope target is refused. • Importing an issue, linking an epic, and declaring a related epic from an out-of-scope repository are each refused. • A work item whose repository left scope cannot be claimed for execution, cannot have an issue materialized, and cannot have a PR opened. • A work item whose `contract.allowedRepositories` is not a subset of scope is refused at persistence. • Every refusal above is proven by a test that calls the service directly, not the route, and not the UI. Planner scoping • The catalog handed to the planner for an initiative equals its persisted scope; a registry repository outside scope never appears. • A synthetic planner output naming an out-of-scope repository is rejected on persistence, with the rest of the wave unaffected where the plan format permits. • Continuation planning for an initiative whose work touched a repository since removed from scope does not reintroduce it. • `repositoryHints` containing an out-of-scope repository grants no access and produces no work item. Applications • An Application cannot be created or edited to contain a repository outside the trusted registry. • The same repository can be added to two Applications, and both list it. • An initiative populated from an Application, then given an additional trusted repository not in that Application, is valid: the addition succeeds, planning proceeds, and no validation refuses it. • A member frozen or removed from the registry after admission stays listed on the Application, is reported by startup validation, and is skipped when that Application populates a scope. • Startup validation reports any Application member missing from the registry, the way bundle validation already does. • Removing a repository from an Application leaves every initiative scope that was populated from it unchanged. • Archiving an Application removes it from selection and changes no initiative scope; deleting one that an initiative references is refused with archive offered instead. Initiative scope • `primaryRepository` cannot be set to a repository outside scope. • Removing the primary repository from scope requires selecting a new primary or clearing it explicitly. • An initiative created without scope has `repository_scope_state = 'unset'` and cannot be planned; the refusal explains what to do. • An initiative with a deliberately empty scope is distinguishable from an unset one in the API and the UI. • Removing a repository with a non-terminal work item is refused and the response identifies those items. • Removing a repository whose only references are terminal succeeds, and no later operation re-adds it. • Every scope change writes a `scope_changed` event with the actor and the repositories affected. • A repository frozen after admission remains in scope, is marked blocked, and refuses new planning, filing, execution and GitHub actions; its existing work items remain readable and linked. • A repository removed from the trusted registry after admission behaves identically, and no migration, startup validation, or background process deletes the entry. • A blocked repository is absent from the planner's catalog and from Add-to-scope pickers, while still visible in the scope panel. Migration • Every existing initiative has a persisted scope, or `unset` with a reason recorded. • No repository whose only evidence is `context.repositoryHints` is admitted by the migration to any initiative. • An initiative whose only evidence is hints is left `unset`, and its trusted hints appear as scope suggestions. • The migration produces a report, runnable as a dry run, listing each initiative's derived scope, each entry's evidence, every hint-derived suggestion it declined to admit, and every initiative left `unset`. • Migrated entries whose only evidence is a committed brief declaration — no work item — are marked in the UI as needing review. • No initiative in a terminal state is blocked by migration. • Seeded Applications match the transcribed Hub manifest, minus any member absent from the registry, and each omission is logged. UI • Every operator capability in this brief is exercisable in the Workspaces app without an API client, and each has a UI test. • Repository pickers with initiative context offer only in-scope repositories. • An ineligible repository is visible with its reason rather than absent. • A blocked removal renders an operator-level explanation naming the blocking work, never a raw constraint error. Migration and compatibility behavior • Scope is migrated only from evidence that already represented an operational commitment: the repositories of the initiative's work items in any state, and the repositories its committed brief declares in `repositoryScope`. Both are intersected with the trusted registry, with the brief source repository removed. Entries are written with origin `migration` and actor `system`, and are individually reviewable and removable. • `context.repositoryHints` are **not** admitted. Hints have never been an authorization boundary — the planner prompt explicitly permits dropping them — so promoting a historical hint into a persisted permission would manufacture exactly the accidental expansion this initiative exists to prevent. Trusted hints that are not already in scope are rendered as migration-time scope suggestions, labelled as coming from historical repository hints, with Add and Dismiss. • Where an initiative's only evidence is hints, scope is left `unset` and the operator establishes the boundary. Where there is no evidence at all, it is likewise `unset` rather than guessed. Non-terminal initiatives in that state block planning with an explanation; terminal initiatives are recorded and never blocked. • `primaryRepository` is backfilled only where unambiguous — a single candidate, or a repository the committed brief names as primary. Otherwise it stays null. • `context.repositoryHints` is retained with a narrowed meaning: descriptive planner context that grants nothing. The brief-commit overwrite at `apps/web/src/app/api/initiatives/[id]/brief/commit/route.ts:39` remains, and a commit naming a repository outside scope produces a scope suggestion rather than widening anything. • The brief's `sections.repositoryScope` is retained as declared intent. At intake it proposes scope entries; admission stays explicit, and intake reports declared repositories it could not admit — the same "report what was discarded" contract as #488. • `deriveInitiativeRepositoryScope` and `expectedRepositoriesFromBriefScope` are retained: the first as a display classifier over persisted entries, the second for intake proposals. Neither remains a source of truth. • `/applications` and `/applications/repositories` switch to the persisted catalog. `loadTopologyManifest` stays for the repository-relationship view and becomes the import source once repaired; no runtime scope decision reads it. • `RELEASE_BUNDLES` is untouched, and its single-membership validation is explicitly not applied to Applications. • Canonical serialization gains fields only. `workItemCount`, `brief`, `state` and every other existing field keep their meaning. • Two sources of truth do not persist: after migration, derivation-from-hints is removed from every decision path in the same change that introduces the persisted read.
Open questions
• None outstanding. Two questions raised during review were settled rather than left open: an initiative references at most one Application in V1, and convergence with the Application Platform's created-Application lifecycle is later architectural work. Neither changes what must be built now, and neither should hold up planning.
Need to change the committed intent? Begin a revision — the current brief stays in force until you commit the new one.
Revision history (1)
  • r1 · committed · material · Initiative Brief: Per-Initiative Repository Isolation and Applications — created from an Initiative Brief

Ideation

Describe the rough idea. The model can read the actual repositories while you refine it together; commit it (below) once it has taken shape.

turn 0/20

This initiative is a draft. Submit it to generate a proposed plan, or refine it further above.

Timeline

No events yet.

    ← All initiatives