diff --git a/.agents/skills/context-engineering/SKILL.md b/.agents/skills/context-engineering/SKILL.md index 9163b34170..59eb39b054 100644 --- a/.agents/skills/context-engineering/SKILL.md +++ b/.agents/skills/context-engineering/SKILL.md @@ -28,8 +28,10 @@ Before changing code: - Use `rg` to find existing patterns before inventing one. - Read the file you will edit and nearby tests. - For product features, read `docs/specs/README.md`, the owning system index, - and only the relevant requirement and system-design files. During migration, - use `docs/specs/INDEX.md` to find a legacy source. + adjacent indexes with similar capability names, and only the relevant + requirement and system-design files. Choose the owner from the durable + contract, not the affected code layer. During migration, use + `docs/specs/INDEX.md` to find a legacy source. - When implementing from a plan, read `plan.md` for orientation and only the current work order. Follow its `REQ-*`, `AC-*`, and system-design references. - For frontend/UI, include `/mobile-parity` and `/e2e` guidance when applicable. diff --git a/.agents/skills/fix/SKILL.md b/.agents/skills/fix/SKILL.md index 93663b3caf..3ac74a0683 100644 --- a/.agents/skills/fix/SKILL.md +++ b/.agents/skills/fix/SKILL.md @@ -39,6 +39,10 @@ Read `docs/specs/README.md`, the owning system index, and the relevant requirement and system-design documents. Use the legacy catalog only when the system has not migrated. +Search adjacent systems before you create or move an artifact. A UI symptom does +not make the repair UI-owned. Update the system that owns the failed contract, +and include observable UI recovery there when applicable. + Classify the bug: 1. **Implementation violates an active acceptance criterion.** Reference the @@ -53,6 +57,10 @@ Classify the bug: Do not create a standalone repair specification. The requirement is the durable behavioral source. The plan and work orders record this repair. +Do not create parallel feature and UI requirements for one repair. A separate UI +requirement is valid only when the repair changes an independent reusable UI +contract. + Use `/record` when the correction establishes a durable boundary, ownership rule, public contract, persistence rule, or security invariant with meaningful alternatives. diff --git a/.agents/skills/mobile-parity/SKILL.md b/.agents/skills/mobile-parity/SKILL.md index 259d8b1439..9cab3b6f5f 100644 --- a/.agents/skills/mobile-parity/SKILL.md +++ b/.agents/skills/mobile-parity/SKILL.md @@ -23,6 +23,16 @@ Apply when task changes user-facing UI: If task has no UI surface, say why this skill does not apply and continue. +## Specification ownership + +When the task uses durable specifications, put mobile outcomes in the system +that owns the feature contract. Do not create a second UI requirement or design +only because the feature needs a phone surface. Use the UI system only for an +independent reusable interaction contract. + +Keep the mobile composition in the owning feature design with its backend and +desktop boundaries. Link to an existing UI contract when the feature reuses one. + ## Mobile Design Contract When a task changes composition, navigation, overlays, touch behavior, scrolling, or breakpoint behavior, state these choices in the working plan or task notes. Keep them brief; do not create a separate document unless the task already uses a spec or committed plan. For copy, icon, color, or content-only styling inside an unchanged surface, identify the nearest mobile exemplar and the rendered mobile check instead of forcing the full contract. diff --git a/.agents/skills/record/SKILL.md b/.agents/skills/record/SKILL.md index 4fec25482e..ae8d63ed67 100644 --- a/.agents/skills/record/SKILL.md +++ b/.agents/skills/record/SKILL.md @@ -63,6 +63,8 @@ Read `docs/specs/README.md`, the owning system index, and the relevant files in Apply these rules: +- Confirm the owning system from the durable contract. Do not assign ownership + to UI only because users observe the decision there. - If the decision changes observable behavior, update the owning requirement and its `REQ-*` or `AC-*` criteria. - If the decision changes technical boundaries or contracts, update the owning diff --git a/.agents/skills/spec-driven-development/SKILL.md b/.agents/skills/spec-driven-development/SKILL.md index fcb2368d9a..192c20ec38 100644 --- a/.agents/skills/spec-driven-development/SKILL.md +++ b/.agents/skills/spec-driven-development/SKILL.md @@ -56,6 +56,12 @@ Keep a visible task list: Use `/interview-me` only when the request needs clarification. +Run the `/spec` ownership gate before you choose paths. Search adjacent systems +for the same capability. Choose the owner from the durable contract, not from +backend or frontend implementation layers. If a feature has a UI, include its +observable UI outcomes and frontend design in the feature owner's artifacts. +Create a separate UI artifact only for an independent reusable UI contract. + Create or update: - `docs/specs//requirements/.md` through `/spec`. @@ -70,6 +76,9 @@ Requirements define stable `REQ-*` and `AC-*` IDs. System designs map technical behavior to those IDs. Plans define delivery order. Work orders define one implementation result and its verification. +Keep one vertical requirement/design pair for one cohesive outcome. Work orders +can separate implementation boundaries after the specification has one owner. + Each work order must contain: - Frontmatter with `id`, `title`, `status`, `wave`, `depends_on`, `plan`, diff --git a/.agents/skills/spec/SKILL.md b/.agents/skills/spec/SKILL.md index 5b8c4fde9b..6fc5846866 100644 --- a/.agents/skills/spec/SKILL.md +++ b/.agents/skills/spec/SKILL.md @@ -43,8 +43,22 @@ Do not create a generic `spec.md` file. Read `docs/specs/README.md` and the likely system `README.md`. If the system has not migrated, use `docs/specs/INDEX.md` to locate the legacy source. -Choose the system that owns the concepts and contract. Other systems reference -that source. They do not copy it. +Search all system indexes, requirements, and designs for the capability name +and its main nouns. Update an existing capability when it owns the same actor, +lifecycle, and contract. + +Choose the system that owns the source of truth and durable contract. Do not +choose an owner from the code directories that change. Record one sentence in +the working notes that states why the selected system owns the capability. + +User visibility does not make a capability UI-owned. Keep provider state, task +state, permissions, persistence, and recovery with their owning systems. Put +desktop, mobile, accessibility, and visible failure outcomes in that owner's +requirement. Create a UI requirement only for an independent and reusable +presentation contract. + +The same system owns the requirement and its design. Other systems link to that +source. They do not copy it or claim its requirement IDs in design frontmatter. If no system owns the behavior, define the new system boundary before you write requirements. A new system needs a `README.md` based on the system template. @@ -74,6 +88,10 @@ Use user stories only when they clarify a natural actor and outcome. Do not put files, functions, database queries, or implementation sequences in a requirement. +Keep one cohesive vertical outcome together. Do not create separate backend and +UI requirements for the same feature. Split only when actors, lifecycles, or +contracts are independent. + ### 4. Write system design Create or update this file when the change needs a technical design: @@ -90,6 +108,11 @@ Describe stable components, models, contracts, flow, failure behavior, persistence, security, and observability when they apply. Link to global ADRs. Do not copy requirement or ADR text. +Cover all runtime boundaries that implement the owned outcome. A provider-owned +design can include backend services, storage, projections, frontend components, +responsive behavior, and tests. Do not create a parallel UI design for those +same requirements. + ### 5. Update the system index Add the new documents to the system `README.md`. State the system boundary and @@ -100,6 +123,18 @@ with a link or archive it. Do not leave two editable sources of truth. ### 6. Validate +Review the artifacts before you run the linter: + +- One system owns each requirement and its design. +- No adjacent system contains a copied requirement or UI-only duplicate. +- Requirements contain observable behavior, not storage, control flow, or file + details. +- Every acceptance criterion states a testable behavior. No criterion delegates + its meaning to migrated source detail. +- Designs map requirement IDs without copying requirement text. +- New files do not copy the legacy `Migrated source detail` wrapper. +- New artifacts appear in the owning system index. + Run: ```bash diff --git a/AGENTS.md b/AGENTS.md index 3e8b122035..4303e26bd3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -146,9 +146,11 @@ history and remains immutable. live under `docs/specs/**`. New work uses `docs/specs//requirements/` and `docs/specs//system-design/`. Read `docs/specs/README.md` and the - relevant file in `docs/specs/guide/`. Use `docs/specs/INDEX.md` only to find - unmigrated legacy specifications. Run `python3 scripts/lint-spec-files.py - --all` after specification changes. + relevant file in `docs/specs/guide/`. Choose the owner from the durable + contract, not the affected code layer. Keep one vertical requirement/design + pair and include its UI outcomes there. Use `docs/specs/INDEX.md` only to + find unmigrated legacy specifications. Run `python3 + scripts/lint-spec-files.py --all` after specification changes. - **Decisions:** Architecture decisions are recorded in `docs/decisions/`. Read `docs/decisions/INDEX.md` for an overview. When making significant architectural choices, create a new ADR via `/record decision`. - **Plans:** Implementation plans are generated from requirements and system designs through `/plan`. `docs/plans//plan.md` is a work-package diff --git a/docs/specs/agents/README.md b/docs/specs/agents/README.md index cf11249274..0ac3579bf0 100644 --- a/docs/specs/agents/README.md +++ b/docs/specs/agents/README.md @@ -2,7 +2,7 @@ status: draft system: agents specification_version: 1 -migration: complete +migration: in_progress owners: - kandev --- @@ -80,7 +80,8 @@ surface shared by task and Office consumers. ## Migration record -All legacy sources assigned to this system are now represented by the canonical requirement and system-design documents above. Source detail is retained in those documents or in their linked design parts. +Migration remains in progress while legacy source detail is extracted from the +canonical requirement and system-design documents above. ## Related systems diff --git a/docs/specs/auth/README.md b/docs/specs/auth/README.md index 886d592ef0..fafb4548a0 100644 --- a/docs/specs/auth/README.md +++ b/docs/specs/auth/README.md @@ -2,7 +2,7 @@ status: draft system: auth specification_version: 1 -migration: complete +migration: in_progress owners: - kandev --- @@ -50,7 +50,8 @@ for user and service requests. ## Migration record -All legacy sources assigned to this system are now represented by the canonical requirement and system-design documents above. Source detail is retained in those documents or in their linked design parts. +Migration remains in progress while legacy source detail is extracted from the +canonical requirement and system-design documents above. ## Related systems diff --git a/docs/specs/ci/README.md b/docs/specs/ci/README.md index 5d0fef0eb3..66bc900085 100644 --- a/docs/specs/ci/README.md +++ b/docs/specs/ci/README.md @@ -43,9 +43,8 @@ deployment, and pull request walkthrough generation. ## Migration -The new shared trust contract replaces the label portions of the legacy -[Claude fork review allowlist specification](../claude-fork-review-allowlist/spec.md) -and the legacy -[PR walkthrough specification](../pr-walkthrough/spec.md). Those documents -remain useful for provider and walkthrough detail until their content is -migrated fully. +The new shared trust contract replaces the label portions of the +[Claude fork review allowlist requirement](../integrations/requirements/claude-fork-review-allowlist.md) +and the +[PR walkthrough requirement](../ui/requirements/pr-walkthrough.md). Provider +and walkthrough behavior remains in those canonical owning systems. diff --git a/docs/specs/cli/README.md b/docs/specs/cli/README.md index 092ad44f09..867f52eb6b 100644 --- a/docs/specs/cli/README.md +++ b/docs/specs/cli/README.md @@ -2,7 +2,7 @@ status: draft system: cli specification_version: 1 -migration: complete +migration: in_progress owners: - kandev --- @@ -41,7 +41,8 @@ and CLI-specific compatibility behavior. ## Migration record -All legacy sources assigned to this system are now represented by the canonical requirement and system-design documents above. Source detail is retained in those documents or in their linked design parts. +Migration remains in progress while legacy source detail is extracted from the +canonical requirement and system-design documents above. ## Related systems diff --git a/docs/specs/costs/README.md b/docs/specs/costs/README.md index 87cc1ea815..274d48f6fb 100644 --- a/docs/specs/costs/README.md +++ b/docs/specs/costs/README.md @@ -2,7 +2,7 @@ status: draft system: costs specification_version: 1 -migration: complete +migration: in_progress owners: - kandev --- @@ -41,7 +41,8 @@ and cheap-model profiles used by task and Office execution. ## Migration record -All legacy sources assigned to this system are now represented by the canonical requirement and system-design documents above. Source detail is retained in those documents or in their linked design parts. +Migration remains in progress while legacy source detail is extracted from the +canonical requirement and system-design documents above. ## Related systems diff --git a/docs/specs/executors/README.md b/docs/specs/executors/README.md index a989c38f30..e720e50377 100644 --- a/docs/specs/executors/README.md +++ b/docs/specs/executors/README.md @@ -2,7 +2,7 @@ status: draft system: executors specification_version: 1 -migration: complete +migration: in_progress owners: - kandev --- @@ -50,7 +50,8 @@ failure and recovery contracts. ## Migration record -All legacy sources assigned to this system are now represented by the canonical requirement and system-design documents above. Source detail is retained in those documents or in their linked design parts. +Migration remains in progress while legacy source detail is extracted from the +canonical requirement and system-design documents above. ## Related systems diff --git a/docs/specs/guide/requirements.md b/docs/specs/guide/requirements.md index dcd9c347a9..e88b4ef6b6 100644 --- a/docs/specs/guide/requirements.md +++ b/docs/specs/guide/requirements.md @@ -88,3 +88,29 @@ user or system outcome. Use references instead of copied text when another system owns a requirement. +## Cross-surface requirements + +Keep one cohesive outcome in its owning system when delivery spans backend and +frontend code. Include observable desktop, mobile, accessibility, loading, +failure, and recovery behavior in that requirement when those details affect +the outcome. + +Do not create a UI requirement only because users activate or observe the +capability through the web application. Create a UI requirement when the UI +owns an independent presentation or interaction contract that several features +can use without copying feature state. + +Before you add a requirement, search all system indexes, requirement files, and +system-design files for the capability and its main nouns. Update the current +owner when the new behavior extends the same outcome. Create another +requirement only when the actor, lifecycle, or contract is independent. + +## Migration quality + +Do not use a generic acceptance criterion that points to behavior elsewhere in +the document. Extract each required behavior into a stable and testable `AC-*` +criterion. + +Do not keep copied `Why`, `What`, scenario, data-model, or API sections after +their facts move to the correct requirement and design sections. A migration +wrapper is temporary evidence, not a template for new specifications. diff --git a/docs/specs/guide/structure-and-ownership.md b/docs/specs/guide/structure-and-ownership.md index 05b4fcadd3..80da88c5bd 100644 --- a/docs/specs/guide/structure-and-ownership.md +++ b/docs/specs/guide/structure-and-ownership.md @@ -74,6 +74,28 @@ systems use the same capability. Create a first-class system when shared behavior has independent ownership and guarantees. Event delivery and persistence can become systems under this rule. +## Vertical feature ownership + +Choose the owner from the durable contract, not from the code directories that +the implementation changes. A capability can change backend, frontend, mobile, +and test code while one system owns its requirements and design. + +User visibility does not make a capability UI-owned. The UI system owns an +interaction contract only when that contract remains useful without the feature +or backend state that first uses it. Provider state, task state, permissions, +and persistence stay with their owning systems. Their requirements can include +desktop, mobile, accessibility, and failure outcomes. + +For example, a GitHub merge-queue capability belongs to the integration system. +Its integration requirement describes the visible queue controls and states. +Its integration design describes the GitHub client, persistence, API projection, +and React components. Do not create a second UI requirement or design for those +same controls. + +Create separate cross-system artifacts only when each system owns an independent +contract with a different lifecycle. Link the contracts in both system indexes. +Do not repeat acceptance criteria or technical sections. + ## File names Use a short kebab-case capability name. Use the same file name for the paired diff --git a/docs/specs/guide/system-design.md b/docs/specs/guide/system-design.md index 2d0e1dbcb8..61f416b933 100644 --- a/docs/specs/guide/system-design.md +++ b/docs/specs/guide/system-design.md @@ -42,6 +42,22 @@ short table is sufficient. Do not copy requirement text into the design. Reference the stable `REQ-*` and `AC-*` identifiers. +## Vertical designs + +Keep a design with the system that owns its requirements. Cover every stable +runtime boundary needed for the outcome. A single integration design can cover +a provider client, storage, HTTP and WebSocket projections, frontend state, +React components, responsive behavior, and tests. + +Do not create parallel backend and UI designs for one feature contract. Link to +another system design when that system owns an independent dependency. Do not +list another system's requirement in design frontmatter. + +Split a large design by capability, lifecycle, or contract boundary. If a +document needs numbered parts, keep every part under the same owning system and +map each part to specific design sections. Do not split only at the frontend and +backend boundary. + ## Decisions Use an ADR when a choice creates a durable boundary, contract, ownership rule, @@ -58,4 +74,3 @@ contract. Do not update it for a local refactor that preserves the design. When code and design disagree, stop and identify the intended source of truth. Do not silently rewrite the design to match accidental behavior. - diff --git a/docs/specs/guide/traceability-and-lifecycle.md b/docs/specs/guide/traceability-and-lifecycle.md index 1c57c55c8e..8de263461f 100644 --- a/docs/specs/guide/traceability-and-lifecycle.md +++ b/docs/specs/guide/traceability-and-lifecycle.md @@ -67,3 +67,7 @@ Migrate one system at a time. Use this sequence: 8. Set `migration: complete` and remove obsolete size exceptions. Do not keep two editable sources of truth during migration. + +Do not mark migration complete while requirement files contain generic criteria +that defer to copied legacy prose. Extract observable behavior into `AC-*` IDs. +Move technical facts into system designs, then remove the copied source detail. diff --git a/docs/specs/integrations/README.md b/docs/specs/integrations/README.md index 1935210e6d..a91a66be80 100644 --- a/docs/specs/integrations/README.md +++ b/docs/specs/integrations/README.md @@ -2,7 +2,7 @@ status: draft system: integrations specification_version: 1 -migration: complete +migration: in_progress owners: - kandev --- @@ -18,13 +18,16 @@ provider-specific contracts that synchronize or act on external work. This system owns provider credentials, provider identity, external issue and pull-request synchronization, integration settings, provider-aware review -automation, and external question or answer flows. +automation, external question or answer flows, and provider-specific UI +outcomes that expose those contracts. ## Exclusions - Generic authentication belongs to the [auth system](../auth/README.md). - Durable Kandev tasks belong to the [task system](../tasks/README.md). - Plugin-owned services belong to the [plugin system](../plugins/README.md). +- Reusable presentation contracts without provider state belong to the + [UI system](../ui/README.md). ## Specification map @@ -66,6 +69,7 @@ automation, and external question or answer flows. - [Workspace GitHub Authentication System Design Part 1](system-design/github-authentication-01.md) - [Workspace GitHub Authentication System Design Part 2](system-design/github-authentication-02.md) - [Workspace GitHub Authentication System Design Part 3](system-design/github-authentication-03.md) +- [GitHub PR Merge Queue](system-design/github-pr-merge-queue.md) - [GitLab Integration System Design Part 1](system-design/gitlab-integration-01.md) - [GitLab Integration System Design Part 2](system-design/gitlab-integration-02.md) - [GitLab MR Status Chip System Design Part 1](system-design/gitlab-mr-status-chip-01.md) @@ -79,7 +83,8 @@ automation, and external question or answer flows. ## Migration record -All legacy sources assigned to this system are now represented by the canonical requirement and system-design documents above. Source detail is retained in those documents or in their linked design parts. +Migration remains in progress while legacy source detail is extracted from the +canonical requirement and system-design documents above. ## Related systems diff --git a/docs/specs/integrations/requirements/github-pr-merge-queue.md b/docs/specs/integrations/requirements/github-pr-merge-queue.md index ae29a8152f..6b4c5f6e5f 100644 --- a/docs/specs/integrations/requirements/github-pr-merge-queue.md +++ b/docs/specs/integrations/requirements/github-pr-merge-queue.md @@ -5,231 +5,53 @@ created: 2026-08-17 owners: - Kandev --- + # GitHub PR Merge Queue Requirements ## Overview -Users can merge an eligible GitHub pull request from Kandev, but repositories that require GitHub's merge queue leave the same pull request looking blocked and force the user to leave Kandev. The merge action should respect the repository's GitHub rules and complete through the appropriate direct or queued path. +The integration system owns GitHub merge actions and merge-queue state. Users +must be able to merge an eligible pull request without leaving Kandev. Kandev +must show whether GitHub merged the pull request or added it to a merge queue. ## Requirements ### REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001: GitHub PR Merge Queue -**Intent:** Users can merge an eligible GitHub pull request from Kandev, but repositories that require GitHub's merge queue leave the same pull request looking blocked and force the user to leave Kandev. The merge action should respect the repository's GitHub rules and complete through the appropriate direct or queued path. +**Intent:** Users can complete an eligible GitHub pull request through the +repository's configured merge path. They can then observe its current queue +state from the existing pull-request surfaces. #### Acceptance criteria -- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.1:** An open, approved pull request with successful required checks exposes its merge action in the existing GitHub PR detail and compact status surfaces, including when GitHub reports it blocked only because the base branch uses a merge queue. -- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.2:** Activating the action asks GitHub to choose the appropriate merge behavior: merge immediately where permitted or add the pull request to the configured merge queue. -- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.3:** A direct merge reports that the pull request merged. An accepted queued merge reports that the pull request was added to the merge queue and prevents repeated submission while local PR state refreshes. -- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.4:** After GitHub reports an active merge queue entry, the linked pull request uses GitHub's merge-queue color, `#966600`, in task and compact PR indicators. This color is distinct from the yellow CI-in-progress, green passing, and emerald ready states. -- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.5:** The task PR hover summary, compact PR status popover or phone drawer, and PR detail panel identify the pull request as queued. They translate GitHub's queue state into user-facing copy instead of displaying the raw provider enum. -- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.6:** Queue surfaces show the pull request's current position in the merge queue. They also show GitHub's estimated time to merge when GitHub supplies one, formatted as a localized duration rather than raw seconds. Estimates below one minute use a localized sub-minute label; larger estimates round up to the next whole minute. -- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.7:** Queue membership takes precedence over other non-terminal icon colors while the pull request remains in the queue. Merged and closed states retain their existing terminal colors. -- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.8:** The action remains unavailable for drafts, conflicts, changes requested, failed or incomplete required checks, and unmet required reviews. +- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.1:** When GitHub requires a merge queue for an eligible pull request, Kandev shall expose the existing merge action. +- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.2:** When the user activates the merge action, Kandev shall let GitHub select a direct merge or the configured queue. +- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.3:** When GitHub accepts the request, Kandev shall report whether GitHub merged or queued the pull request. +- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.4:** After GitHub accepts the request, Kandev shall prevent repeated submission until the pull-request state refreshes. +- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.5:** When a pull request has an active queue entry, Kandev shall use GitHub's merge-queue color, `#966600`. +- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.6:** When states compete, Kandev shall prioritize terminal states, then queue state, then other non-terminal states. +- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.7:** When GitHub supplies queue metadata, Kandev shall show localized state, position, and available estimate data. +- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.8:** When an estimate is less than one minute, Kandev shall use a localized sub-minute label. Larger estimates shall round up. +- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.9:** When GitHub omits an estimate, Kandev shall show state and position without invented estimate data. +- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.10:** When GitHub supplies an unknown non-empty state, Kandev shall show generic localized copy instead of the raw value. +- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.11:** When a refresh cannot observe queue membership, Kandev shall preserve the last confirmed entry. An authoritative empty or terminal state shall clear it. +- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.12:** When a pull request is a draft, conflicted, failing checks, awaiting checks, missing reviews, or has requested changes, Kandev shall hide the merge action. +- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.13:** When GitHub rejects a merge request, Kandev shall show the error, preserve state, and leave the action available for retry. +- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.14:** When users view queue state on desktop or mobile, Kandev shall provide the same outcome without hover-dependent controls or horizontal page overflow. -## Migrated source detail +## Out of scope -## Why - -Users can merge an eligible GitHub pull request from Kandev, but repositories -that require GitHub's merge queue leave the same pull request looking blocked -and force the user to leave Kandev. The merge action should respect the -repository's GitHub rules and complete through the appropriate direct or queued -path. - -## What - -- An open, approved pull request with successful required checks exposes its - merge action in the existing GitHub PR detail and compact status surfaces, - including when GitHub reports it blocked only because the base branch uses a - merge queue. -- Activating the action asks GitHub to choose the appropriate merge behavior: - merge immediately where permitted or add the pull request to the configured - merge queue. -- A direct merge reports that the pull request merged. An accepted queued merge - reports that the pull request was added to the merge queue and prevents - repeated submission while local PR state refreshes. -- After GitHub reports an active merge queue entry, the linked pull request uses - GitHub's merge-queue color, `#966600`, in task and compact PR indicators. This - color is distinct from the yellow CI-in-progress, green passing, and emerald - ready states. -- The task PR hover summary, compact PR status popover or phone drawer, and PR - detail panel identify the pull request as queued. They translate GitHub's - queue state into user-facing copy instead of displaying the raw provider - enum. -- Queue surfaces show the pull request's current position in the merge queue. - They also show GitHub's estimated time to merge when GitHub supplies one, - formatted as a localized duration rather than raw seconds. Estimates below - one minute use a localized sub-minute label; larger estimates round up to the - next whole minute. -- Queue membership takes precedence over other non-terminal icon colors while - the pull request remains in the queue. Merged and closed states retain their - existing terminal colors. -- The action remains unavailable for drafts, conflicts, changes requested, - failed or incomplete required checks, and unmet required reviews. -- GitHub remains authoritative for final eligibility. A rejected request leaves - the action retryable and shows GitHub's error without claiming that the pull - request merged or entered the queue. -- Desktop and mobile use the existing PR detail behavior. On phones, the action - remains available through the full-height Review surface with a touch-sized - target and no horizontal document overflow. Queue status is also available - through that Review surface and the existing PR status drawer, so it never - depends on hover. - -## Data Model - -The linked pull request stores the last successfully observed merge queue -entry in `github_task_prs`: - -- `merge_queue_state TEXT NOT NULL DEFAULT ''` stores the normalized GitHub - `MergeQueueEntryState`. Supported values are `queued`, `awaiting_checks`, - `mergeable`, `unmergeable`, and `locked`. -- `merge_queue_position INTEGER NULL` stores GitHub's one-based queue position. -- `merge_queue_estimated_time_to_merge_seconds INTEGER NULL` stores GitHub's - current estimate as a non-negative duration in seconds. - -An empty state with null metadata means no active queue entry is stored. -Existing rows start in that state and converge on their next successful -GraphQL status sync. - -The queue fields are part of the existing `TaskPR` HTTP, boot, and WebSocket -payloads. The bounded task-status projection reduces a queued open pull request -to the aggregate state `queued` so task rows can retain the queue color before -the full task PR collection hydrates. - -## API Surface - -Kandev retains the existing endpoint and extends its success response: - -```http -PUT /api/v1/github/prs/:owner/:repo/:number/merge?workspace_id=:workspaceId -Content-Type: application/json - -{"merge_method":"squash"} -``` - -- `merge_method` remains optional and accepts `merge`, `squash`, or `rebase`. -- Kandev submits the request through GitHub's asynchronous merge API with - `merge_action=default`, allowing GitHub to select direct merge or merge queue. -- Success returns `200` with one of: - -```json -{"status":"merged"} -{"status":"queued"} -``` - -- A `pending` response includes a UUID. Kandev polls that request, including - the UUID returned by an existing-request `409`, until GitHub reports - `merged`, `enqueued`, or `failed`. -- Only `enqueued` maps to `queued`. An already-merged pull request maps to - `merged`, while `failed` remains an error that the user can retry. - -Queue status reuses the existing GitHub status synchronization contract. The -batched GraphQL pull-request selection reads -`mergeQueueEntry { state position estimatedTimeToMerge }`; it does not add an -endpoint or a poller. `position` is a one-based integer. -`estimatedTimeToMerge` is an optional duration in seconds, not a timestamp. A -status result records whether queue membership was actually observed so REST -and `gh pr view` reads, which do not include `mergeQueueEntry`, cannot clear a -valid stored queue entry. - -## State Machine - -- An open pull request with no `mergeQueueEntry` is not queued. -- A successful GraphQL observation of `mergeQueueEntry` atomically enters or - updates its state, position, and optional estimate, then publishes the normal - task-PR update. -- A later successful GraphQL observation with `mergeQueueEntry: null` leaves - the queue and clears the stored state, position, estimate, and queued UI. -- A merged or closed pull request clears queue state and renders its existing - terminal status even if a less capable read path did not observe the queue - entry disappear. -- A failed or queue-unaware status read preserves the complete last stored - queue entry. - -## Permissions - -The action uses the active workspace's personal-write GitHub routing and -requires the same GitHub content/pull-request permissions as the provider's -merge API. Kandev does not bypass repository rules or elevate the user. - -## Failure Modes - -- Missing GitHub credentials or required permissions return a non-success - response, surface a useful error, and leave the action retryable. -- GitHub validation, readiness, rate-limit, or transport failures do not change - local PR state and do not show a success notification. -- An unrecognized successful provider status fails closed rather than claiming - that the pull request merged or entered the queue. -- After an accepted request, Kandev invalidates cached PR feedback/status and - refreshes the linked pull request; the GitHub poller remains authoritative - for its eventual merged state. -- A failed GraphQL queue-status read preserves the previous queue state and - leaves the normal refresh affordance available. Kandev does not interpret a - missing field from a queue-unaware read as removal from the queue. -- An unknown future non-empty queue state still identifies the pull request as - queued with generic copy; Kandev does not expose the raw enum value. -- If GitHub omits the estimated time to merge, Kandev still shows queue state - and position without an empty or invented estimate. - -## Persistence Guarantees - -The last observed queue state, position, and estimate survive a Kandev restart -with the linked task PR. They can be temporarily stale until the next -successful GitHub status sync. Kandev labels the time as an estimate and does -not convert the duration to a durable predicted timestamp. - -## Scenarios - -- **GIVEN** an approved open PR with successful required checks on a branch - without a required merge queue, **WHEN** the user activates the merge action, - **THEN** GitHub merges it and Kandev reports that the PR merged. -- **GIVEN** an approved open PR with successful required checks on a branch - that requires a merge queue, **WHEN** the user activates the merge action, - **THEN** GitHub accepts it into the queue and Kandev reports that the PR was - added to the merge queue. -- **GIVEN** a PR already in the merge queue, **WHEN** the merge request is - repeated, **THEN** Kandev treats GitHub's idempotent response as queued and - does not report a failure. -- **GIVEN** a linked open PR with an active GitHub merge queue entry, **WHEN** - Kandev completes a status sync, **THEN** its task and compact PR indicators - use the dedicated queue color and its hover, drawer, popover, and detail - surfaces describe the queue state, position, and available merge estimate. -- **GIVEN** GitHub reports an active queue entry without an estimated time to - merge, **WHEN** a queue surface renders, **THEN** it shows queue state and - position and omits the estimate without placeholder or fallback data. -- **GIVEN** a linked PR with a stored queue state, **WHEN** a REST or `gh pr - view` feedback refresh completes without queue data, **THEN** the stored - queue state, position, estimate, and queued UI remain unchanged. -- **GIVEN** a linked queued PR is removed from the queue while remaining open, - **WHEN** a later GraphQL status sync observes no merge queue entry, **THEN** - Kandev clears the queued UI and resumes the normal review, CI, and merge - status color. -- **GIVEN** a linked queued PR reaches a merged or closed state, **WHEN** any - authoritative lifecycle sync completes, **THEN** the terminal state replaces - the queued presentation. -- **GIVEN** a draft, conflicted PR, failed checks, changes requested, or missing - required approvals, **WHEN** the PR surface renders, **THEN** no merge or - queue action is available. -- **GIVEN** GitHub rejects an otherwise eligible merge request, **WHEN** the - action completes, **THEN** Kandev shows the provider error and leaves the - action available for retry. -- **GIVEN** a phone-sized task view with an eligible queue-required PR, **WHEN** - the user opens Review and activates the action, **THEN** the queued outcome is - visible, the action is touch-usable, and the document has no horizontal - overflow. - -## Out of Scope - -- Displaying or navigating the full merge queue. +- Displaying or navigating the complete merge queue. - Removing a pull request from the merge queue. -- Selecting between direct merge and merge queue when GitHub policy permits - both; GitHub's `default` behavior remains authoritative. -- Changing Kandev's independent CI auto-merge automation setting. -- GitLab merge-request behavior. +- Selecting between direct merge and queue entry when GitHub permits both. +- Changing Kandev's independent CI auto-merge setting. +- Adding merge-queue behavior for GitLab merge requests. -## Implementation Plans +## System design + +- [GitHub PR Merge Queue System Design](../system-design/github-pr-merge-queue.md) + +## Implementation plans - [Queue-status visibility plan](../../../plans/github-pr-merge-queue-status/plan.md) - [Original queue-aware merge action plan](../../../plans/github-pr-merge-queue/plan.md) diff --git a/docs/specs/integrations/system-design/github-pr-merge-queue.md b/docs/specs/integrations/system-design/github-pr-merge-queue.md new file mode 100644 index 0000000000..393864ab0c --- /dev/null +++ b/docs/specs/integrations/system-design/github-pr-merge-queue.md @@ -0,0 +1,197 @@ +--- +status: current +system: integrations +requirements: + - REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001 +created: 2026-08-24 +owners: + - kandev +--- + +# GitHub PR Merge Queue System Design + +## Purpose and boundaries + +The integration system owns the GitHub merge operation and the normalized +merge-queue state. The task system consumes a bounded pull-request summary. The +UI renders the integration state but does not own a second queue contract. + +This design covers the complete vertical path. It includes the GitHub clients, +storage, task projection, HTTP response, frontend types, React surfaces, and +desktop and mobile behavior. + +## Requirement mapping + +| Requirement | Design sections | +| --- | --- | +| `REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001` | [Components](#components-and-responsibilities), [Data](#data-and-contracts), [Control flow](#control-flow), [Failure](#failure-and-recovery), [Presentation](#presentation-and-responsive-behavior) | + +## Components and responsibilities + +### GitHub provider boundary + +- `apps/backend/internal/github/client.go` defines the typed merge outcome. +- `gh_client.go` and `pat_client.go` call GitHub's asynchronous merge endpoint. +- `graphql.go` reads and normalizes `mergeQueueEntry` state and metadata. + +### Integration service and storage + +- `service_pr.go` routes the merge request through workspace-scoped write + credentials and invalidates stale pull-request data after success. +- `controller.go` exposes the existing merge endpoint and returns the typed + `merged` or `queued` result. +- `service_pr_watch.go` applies authoritative queue observations and publishes + the normal pull-request update. +- `models.go` owns the normalized `PRStatus` and persisted `TaskPR` fields. +- `store.go` owns the `github_task_prs` schema and all queue-field write paths. + +### Task projection + +- `apps/backend/internal/task/statussummary` reduces full pull-request state to + a bounded task status. +- `apps/backend/internal/backendapp/status_summary_adapter.go` supplies the + stored queue state during live updates and restart hydration. + +### Web application + +- `apps/web/lib/api/domains/github-pr-api.ts` types the merge result. +- `apps/web/lib/types/github.ts` defines the queue-state and `TaskPR` fields. +- `pr-merge-button.tsx` controls eligibility, submission, feedback, and refresh. +- `pr-merge-queue-status.tsx` normalizes queue presentation and duration copy. +- The task icon, status chip, summary, and detail components reuse that + presentation model. + +## Data and contracts + +`github_task_prs` stores the last authoritative queue observation: + +- `merge_queue_state TEXT NOT NULL DEFAULT ''` stores the normalized GitHub + state. Supported values are `queued`, `awaiting_checks`, `mergeable`, + `unmergeable`, and `locked`. +- `merge_queue_position INTEGER NULL` stores GitHub's one-based position. +- `merge_queue_estimated_time_to_merge_seconds INTEGER NULL` stores an optional + non-negative duration. + +An empty state with null metadata means that no active queue entry is stored. +The same three fields appear in the `TaskPR` HTTP, boot, and WebSocket payloads. +The bounded task projection reduces active membership to `queued`. + +The existing merge endpoint remains: + +```http +PUT /api/v1/github/prs/:owner/:repo/:number/merge?workspace_id=:workspaceId +Content-Type: application/json + +{"merge_method":"squash"} +``` + +`merge_method` is optional. It accepts `merge`, `squash`, or `rebase`. A +successful response is one of these objects: + +```json +{"status":"merged"} +{"status":"queued"} +``` + +The provider request uses `merge_action=default`. GitHub therefore selects the +allowed direct or queued path. + +## Control flow + +### Merge request + +1. The frontend derives eligibility from the current pull-request state. +2. The user activates the existing merge action. +3. The service selects workspace-scoped personal-write credentials. +4. The provider starts or resumes GitHub's asynchronous merge request. +5. The provider polls a pending request until GitHub returns a terminal result. +6. The controller returns `merged` or `queued` to the frontend. +7. The frontend shows outcome-specific feedback and requests a state refresh. + +Only GitHub's `enqueued` result maps to `queued`. An already merged pull request +maps to `merged`. A provider failure remains retryable. + +### Queue observation + +The batched GraphQL selection reads this field: + +```graphql +mergeQueueEntry { state position estimatedTimeToMerge } +``` + +`graphql.go` records whether the response observed queue membership. This guard +distinguishes an authoritative null from REST and `gh pr view` responses that +cannot read queue data. + +`SyncTaskPR` atomically replaces the queue state, position, and estimate after +an authoritative GraphQL observation. It preserves all three fields after a +queue-unaware read. It clears them after an authoritative null, merge, or close. +The update then publishes `github.task_pr.updated` through the existing path. + +## State transitions + +- An open pull request with no observed queue entry is not queued. +- A non-null authoritative entry creates or updates active queue membership. +- A later authoritative null removes active queue membership. +- A merged or closed state clears queue metadata and wins presentation priority. +- A queue-unaware or failed read preserves the last complete observation. +- An unknown future non-empty state remains active and uses generic UI copy. + +## Failure and recovery + +Missing credentials, permission errors, provider validation errors, rate limits, +and transport errors return a non-success response. These errors do not change +the stored pull-request state or show a successful outcome. + +An unknown successful provider result fails closed. Kandev does not claim that +the pull request merged or entered the queue. + +A failed GraphQL queue read preserves the last complete observation. The next +successful authoritative sync updates or clears it. This behavior also protects +queue state during restart recovery and mixed GraphQL, REST, and CLI refreshes. + +## Persistence + +The queue fields use additive SQLite columns in `github_task_prs`. Fresh schema, +migration, create, replace, restore, and update paths round-trip the complete +entry. Existing rows start with no queue entry and converge after a successful +GraphQL status sync. + +The estimate remains a duration. Kandev does not convert it to a durable target +timestamp because GitHub can change the estimate on each observation. + +## Security + +The merge action uses the active workspace's personal-write GitHub route. It +requires the same pull-request permissions as GitHub's merge API. Kandev does +not bypass repository rules or elevate the user. + +## Presentation and responsive behavior + +An active queue entry uses `#966600`. Terminal states take precedence. Queue +membership takes precedence over other non-terminal icon states. + +The task hover summary, desktop status popover, phone drawer, and pull-request +detail panel show localized state and position. Estimates below one minute use a +localized sub-minute label. Estimates of one minute or more round up to a +localized whole-minute duration. Missing estimates omit estimate text instead +of inventing a value. Unknown provider states use generic queue copy. + +Desktop and mobile share state derivation and actions. Mobile uses the existing +Review surface and status drawer. The action has a touch-sized target, does not +depend on hover, and does not create document-level horizontal overflow. + +## Verification evidence + +Focused backend coverage exists in the GitHub client, GraphQL conversion, +storage, synchronization, and task-status packages. Frontend unit coverage +exists for merge eligibility, merge outcomes, queue formatting, and status +priority. Playwright coverage exists in: + +- `apps/web/e2e/tests/pr/pr-merge-queue.spec.ts` +- `apps/web/e2e/tests/pr/mobile-pr-merge-queue.spec.ts` + +## Related delivery records + +- [Queue-status visibility plan](../../../plans/github-pr-merge-queue-status/plan.md) +- [Original queue-aware merge action plan](../../../plans/github-pr-merge-queue/plan.md) diff --git a/docs/specs/launcher/README.md b/docs/specs/launcher/README.md index 824b9f225d..8279f00632 100644 --- a/docs/specs/launcher/README.md +++ b/docs/specs/launcher/README.md @@ -43,12 +43,11 @@ handoffs. ## Migration status The startup-recovery documents are authoritative for bind-aware readiness and -launcher failure output. The legacy -[startup configuration parity](../platform/startup-configuration-parity.md) -specification remains authoritative for configuration discovery and -precedence. The legacy [Go development launcher](../go-dev-launcher/spec.md) -specification remains historical implementation context for the native -launcher migration. +launcher failure output. Configuration discovery and precedence are defined by +[startup configuration parity](../platform/requirements/startup-configuration-parity.md). +The native launcher migration also incorporates the +[Go development launcher](../platform/requirements/go-dev-launcher.md) +requirement. No removed legacy path remains authoritative. ## Related systems diff --git a/docs/specs/office/README.md b/docs/specs/office/README.md index 0e2bfaf4cd..86f0bc22b6 100644 --- a/docs/specs/office/README.md +++ b/docs/specs/office/README.md @@ -2,7 +2,7 @@ status: draft system: office specification_version: 1 -migration: complete +migration: in_progress owners: - kandev --- @@ -97,7 +97,8 @@ dashboard projections, and Office testing contracts. ## Migration record -All legacy sources assigned to this system are now represented by the canonical requirement and system-design documents above. Source detail is retained in those documents or in their linked design parts. +Migration remains in progress while legacy source detail is extracted from the +canonical requirement and system-design documents above. ## Related systems diff --git a/docs/specs/office/requirements/scheduler.md b/docs/specs/office/requirements/scheduler.md index f9ea2b9df3..3721d79595 100644 --- a/docs/specs/office/requirements/scheduler.md +++ b/docs/specs/office/requirements/scheduler.md @@ -11,7 +11,7 @@ owners: Kandev's base task scheduler is reactive: tasks enter the queue only when a user explicitly starts them or sends a prompt. Office adds autonomous agent operation, which requires the system to wake agents on its own when events happen (assignments, comments, blocker resolutions, approvals), on a schedule (routines), and on heartbeat ticks (periodic coordinator checks). Without an autonomous wakeup pipeline, every interaction needs a human to initiate it, and the cost / reliability story (idle skips, rate-limit retries, staleness, recovery) has nowhere to live. -Office supplies autonomous run producers and Office-specific maintenance. The persisted `runs` queue and its single backend-wide consumer are shared workflow infrastructure, not one scheduler per Office workspace. Shared ownership, scoping, and shutdown contract is defined by [run queue](../tasks/requirements/run-scheduling.md) and [ADR-2026-08-01-global-run-scheduler-ownership](../../decisions/2026-08-01-global-run-scheduler-ownership.md). +Office supplies autonomous run producers and Office-specific maintenance. The persisted `runs` queue and its single backend-wide consumer are shared workflow infrastructure, not one scheduler per Office workspace. Shared ownership, scoping, and shutdown contract is defined by [run queue](../../tasks/requirements/run-scheduling.md) and [ADR-2026-08-01-global-run-scheduler-ownership](../../../decisions/2026-08-01-global-run-scheduler-ownership.md). ## Requirements @@ -26,8 +26,8 @@ reliability story (idle skips, rate-limit retries, staleness, recovery) has nowh supplies autonomous run producers and Office-specific maintenance. The persisted `runs` queue and its single backend-wide consumer are shared workflow infrastructure, not one scheduler per Office workspace. Shared ownership, scoping, and shutdown contract is defined by [run -queue](../tasks/requirements/run-scheduling.md) and -[ADR-2026-08-01-global-run-scheduler-ownership](../../decisions/2026-08-01-global-run-scheduler-ownership.md). +queue](../../tasks/requirements/run-scheduling.md) and +[ADR-2026-08-01-global-run-scheduler-ownership](../../../decisions/2026-08-01-global-run-scheduler-ownership.md). #### Acceptance criteria diff --git a/docs/specs/platform/README.md b/docs/specs/platform/README.md index 97526aa748..0850feb8ba 100644 --- a/docs/specs/platform/README.md +++ b/docs/specs/platform/README.md @@ -2,7 +2,7 @@ status: draft system: platform specification_version: 1 -migration: complete +migration: in_progress owners: - kandev --- @@ -84,7 +84,8 @@ localization, feature toggles, health, and shared session recovery services. ## Migration record -All legacy sources assigned to this system are now represented by the canonical requirement and system-design documents above. Source detail is retained in those documents or in their linked design parts. +Migration remains in progress while legacy source detail is extracted from the +canonical requirement and system-design documents above. ## Related systems diff --git a/docs/specs/plugins/README.md b/docs/specs/plugins/README.md index da5e7f7025..f3577e9454 100644 --- a/docs/specs/plugins/README.md +++ b/docs/specs/plugins/README.md @@ -2,7 +2,7 @@ status: draft system: plugins specification_version: 1 -migration: complete +migration: in_progress owners: - kandev --- @@ -56,7 +56,8 @@ plugin security boundaries. ## Migration record -All legacy sources assigned to this system are now represented by the canonical requirement and system-design documents above. Source detail is retained in those documents or in their linked design parts. +Migration remains in progress while legacy source detail is extracted from the +canonical requirement and system-design documents above. ## Related systems diff --git a/docs/specs/plugins/requirements/marketplace.md b/docs/specs/plugins/requirements/marketplace.md index 2862a95151..9821fa9adf 100644 --- a/docs/specs/plugins/requirements/marketplace.md +++ b/docs/specs/plugins/requirements/marketplace.md @@ -10,7 +10,8 @@ owners: ## Overview Today a user can only install a plugin if they already know its release tarball URL -or have a `.tar.gz` to upload (see [plugins spec](spec.md) → "Install pipeline"). +or have a `.tar.gz` to upload (see the [plugin installation requirement](plugins.md) +for the existing install pipeline). There is no way to *discover* plugins from inside kandev, no curated list of what exists, and no signal for which plugins are worth trusting. Plugin authors have nowhere to publish, and teams have no sanctioned way to share an internal set of @@ -22,7 +23,8 @@ while keeping install-by-URL and sideloading as escape hatches. ### REQ-PLUGINS-MARKETPLACE-001: Plugin Marketplace **Intent:** Today a user can only install a plugin if they already know its release tarball URL or -have a `.tar.gz` to upload (see [plugins spec](spec.md) → "Install pipeline"). There is no way to +have a `.tar.gz` to upload (see the [plugin installation requirement](plugins.md) for the existing +install pipeline). There is no way to *discover* plugins from inside kandev, no curated list of what exists, and no signal for which plugins are worth trusting. Plugin authors have nowhere to publish, and teams have no sanctioned way to share an internal set of plugins. This feature adds a discoverable, curated catalog — kandev's diff --git a/docs/specs/release/README.md b/docs/specs/release/README.md index a7b8323c59..1b88a756a2 100644 --- a/docs/specs/release/README.md +++ b/docs/specs/release/README.md @@ -2,7 +2,7 @@ status: draft system: release specification_version: 1 -migration: complete +migration: in_progress owners: - kandev --- @@ -44,7 +44,8 @@ container, and desktop release publication contracts. ## Migration record -All legacy sources assigned to this system are now represented by the canonical requirement and system-design documents above. Source detail is retained in those documents or in their linked design parts. +Migration remains in progress while legacy source detail is extracted from the +canonical requirement and system-design documents above. ## Related systems diff --git a/docs/specs/system-page/README.md b/docs/specs/system-page/README.md index d3f6342d78..418587283d 100644 --- a/docs/specs/system-page/README.md +++ b/docs/specs/system-page/README.md @@ -2,7 +2,7 @@ status: draft system: system-page specification_version: 1 -migration: complete +migration: in_progress owners: - kandev --- @@ -47,7 +47,8 @@ feedback. ## Migration record -All legacy sources assigned to this system are now represented by the canonical requirement and system-design documents above. Source detail is retained in those documents or in their linked design parts. +Migration remains in progress while legacy source detail is extracted from the +canonical requirement and system-design documents above. ## Related systems diff --git a/docs/specs/tasks/README.md b/docs/specs/tasks/README.md index 234e817821..fc877af55f 100644 --- a/docs/specs/tasks/README.md +++ b/docs/specs/tasks/README.md @@ -2,7 +2,7 @@ status: active system: tasks specification_version: 1 -migration: complete +migration: in_progress owners: - kandev --- @@ -118,16 +118,22 @@ signals, and task-scoped scheduling contracts. - [External task ID idempotency](system-design/external-id-idempotency.md) - [Task model unification](system-design/model-unification.md) - [Remote Contribution Tasks](system-design/remote-contribution-tasks.md) +- [Task Archive Confirmation](system-design/archive-confirmation.md) - [Task Runtime Cleanup](system-design/runtime-cleanup.md) +- [Queued Run Scheduling](system-design/run-scheduling.md) - [Session Delete Preserves Task Workspaces](system-design/session-delete-resource-cleanup.md) - [Task Dependencies and Auto-Start Chains](system-design/task-dependencies.md) +- [Task Launch Failure Recovery](system-design/task-launch-failure-recovery.md) - [WIP Limits and Visible Overflow Queues](system-design/wip-limit-pull-system.md) - [Workflow quorum decision recording](system-design/workflow-quorum-decision-recording.md) - [Workflow task-step transition ledger](system-design/workflow-task-step-transition-ledger.md) ## Migration record -All legacy sources assigned to this system are now represented by the canonical requirement and system-design documents above. Source detail is retained in those documents or in their linked design parts. +Migration remains in progress. The three requirements above now have +authoritative, wrapper-free requirement/design pairs. Other migrated files still +need the same extraction before this system can return to a complete migration +state. ## Related systems diff --git a/docs/specs/tasks/requirements/archive-confirmation.md b/docs/specs/tasks/requirements/archive-confirmation.md index 03ce35c874..8104800109 100644 --- a/docs/specs/tasks/requirements/archive-confirmation.md +++ b/docs/specs/tasks/requirements/archive-confirmation.md @@ -2,92 +2,61 @@ status: active system: tasks created: 2026-07-15 -updated: 2026-08-12 +updated: 2026-08-24 owners: - kandev --- + # Task Archive Confirmation Requirements ## Overview -This document is the migrated task-system source for the capability. The source detail below remains authoritative while the system is migrated into separate requirement and design records. +Users can choose whether user-initiated task archiving requires confirmation. +The safer confirmed behavior remains the default, while experienced users can +archive quickly without changing deletion or programmatic archive behavior. ## Requirements ### REQ-TASKS-ARCHIVE-CONFIRMATION-001: Task Archive Confirmation -**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. +**Intent:** Let a user control archive confirmation without changing the +meaning of archive, deletion, or agent-driven operations. #### Acceptance criteria -- **AC-TASKS-ARCHIVE-CONFIRMATION-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. - -## Migrated source detail - -## Why - -Frequent task archiving makes a mandatory confirmation dialog costly for users who understand the cleanup consequences. Users need to choose whether archive actions require explicit confirmation while retaining the safer confirmed behavior by default. - -## What - -- A user-level setting named **Confirm before archiving tasks** controls whether user-initiated archive actions require the archive confirmation dialog. -- The setting is enabled by default for new users and for existing users whose saved settings predate the preference. -- When enabled, archive actions continue to show the existing cleanup summary and optional subtask cascade control before archiving. -- When disabled, archive actions from every UI surface archive immediately without rendering the confirmation dialog. -- Confirmation-free archive actions do not cascade to subtasks. Users who need to archive subtasks together can temporarily enable confirmation and use the existing cascade control. -- When an active task is archived, the rendered task and the task ID in the URL change together. -- If another live task remains, Kandev opens a task that is not part of the archive operation. -- If no task remains outside the archive operation, Kandev opens the task overview after the archive succeeds. -- A cascade archive never uses the parent or one of its descendants as the temporary or final navigation target. -- Desktop and mobile task switchers use the same post-archive navigation behavior. -- Delete confirmations and programmatic archive operations are unaffected. - -## Data model - -`users.settings` stores `confirm_task_archive` as a boolean in the existing per-user JSON settings blob. A missing field is interpreted as `true` for backward compatibility. - -## API surface - -The existing user settings endpoints carry the preference: - -- `GET /api/v1/user/settings`: `settings.confirm_task_archive: boolean` -- `PATCH /api/v1/user/settings`: optional `confirm_task_archive: boolean` -- `user.settings.updated` WebSocket payload: `confirm_task_archive: boolean` - -No archive endpoint contract changes. - -## Failure modes - -- If saving the preference fails, the settings control returns to its previous value and archive behavior remains unchanged. -- If user settings have not loaded or omit the field, the client requires confirmation. -- Archive API failures continue to use each archive surface's existing error handling. -- If an active task was temporarily replaced before an archive request fails, the client restores that task and its URL. -- If no safe replacement task exists, the client stays on the active task until the archive request succeeds. - -## Persistence guarantees - -The preference survives backend and client restarts as part of the existing user settings record. It applies across workspaces for the current user. - -## Scenarios - -- **GIVEN** a new or upgraded user has not changed the preference, **WHEN** they request an archive from any UI surface, **THEN** the archive confirmation dialog is shown. -- **GIVEN** confirmation is enabled, **WHEN** the user cancels the archive dialog, **THEN** the task remains active. -- **GIVEN** confirmation is disabled, **WHEN** the user requests an archive from the sidebar, task banner, task card, list, pipeline, mobile task switcher, or bulk action, **THEN** the archive starts immediately and no archive confirmation dialog appears. -- **GIVEN** confirmation is disabled and a task has active subtasks, **WHEN** the user archives the task, **THEN** the parent is archived with cascade disabled and the subtasks remain active. -- **GIVEN** an active parent has subtasks plus an unrelated live task, **WHEN** the user enables cascade archive, **THEN** Kandev opens the unrelated task, not a doomed descendant. -- **GIVEN** the active parent tree contains all live tasks, **WHEN** the user enables cascade archive, **THEN** Kandev waits for success and then opens the task overview. -- **GIVEN** an archive completes on desktop or mobile, **WHEN** the user opens another task, **THEN** the URL and rendered task update without a hard refresh. -- **GIVEN** an archive request fails after Kandev opened a replacement task, **WHEN** failure handling completes, **THEN** Kandev restores the original active task and URL. -- **GIVEN** saving the preference fails, **WHEN** the request completes, **THEN** the control and archive behavior revert to the previously persisted value. +- **AC-TASKS-ARCHIVE-CONFIRMATION-001.1:** When the preference is missing or + enabled, a user archive action shall show the existing confirmation summary + and optional subtask-cascade control. +- **AC-TASKS-ARCHIVE-CONFIRMATION-001.2:** When the preference is disabled, an + archive action from any desktop or mobile task surface shall archive only the + requested task without showing the confirmation dialog or cascading to + subtasks. +- **AC-TASKS-ARCHIVE-CONFIRMATION-001.3:** When a confirmed archive uses the + cascade option, the system shall exclude the archived tree from replacement + task selection. +- **AC-TASKS-ARCHIVE-CONFIRMATION-001.4:** When an active task is archived + successfully, the rendered task and URL shall change together to a surviving + task or the task overview. +- **AC-TASKS-ARCHIVE-CONFIRMATION-001.5:** When preference persistence fails, + the control and subsequent archive behavior shall remain at the previously + persisted value. +- **AC-TASKS-ARCHIVE-CONFIRMATION-001.6:** When archive execution fails after + temporary navigation, the system shall restore the original task and URL. +- **AC-TASKS-ARCHIVE-CONFIRMATION-001.7:** Delete confirmations and + programmatic, API, CLI, MCP, or agent-driven archive operations shall remain + unchanged. ## Out of scope -- Disabling confirmation for task deletion or other destructive actions. -- Adding a per-archive cascade default when confirmation is disabled. -- Changing API, CLI, MCP, automation, or agent-driven archive behavior. -- Changing the task switcher layout, archive dialog, or mobile composition. +- Disabling confirmation for deletion or other destructive actions. +- Adding a cascade default when confirmation is disabled. +- Changing the archive API or task-switcher composition. -## Implementation plan +## System design + +- [Task Archive Confirmation System Design](../system-design/archive-confirmation.md) + +## Implementation plans - [Archive Confirmation Preference](../../../plans/archive-confirmation-preference/plan.md) -- [Cascade Archive Navigation](../../../plans/cascade-archive-navigation/plan.md) \ No newline at end of file +- [Cascade Archive Navigation](../../../plans/cascade-archive-navigation/plan.md) diff --git a/docs/specs/tasks/requirements/run-scheduling.md b/docs/specs/tasks/requirements/run-scheduling.md index cc45a6f1fb..20d8ed5845 100644 --- a/docs/specs/tasks/requirements/run-scheduling.md +++ b/docs/specs/tasks/requirements/run-scheduling.md @@ -2,164 +2,63 @@ status: active system: tasks created: 2026-08-01 +updated: 2026-08-24 owners: - cfl --- -# Queued run scheduling Requirements + +# Queued Run Scheduling Requirements ## Overview -This document is the migrated task-system source for the capability. The source detail below remains authoritative while the system is migrated into separate requirement and design records. +Workflow actions and Office automation can enqueue work without a user +clicking Start. Kandev must dispatch that work across workspaces while keeping +ordinary Kanban assignment manual, Office maintenance scoped, and shutdown +safe. ## Requirements ### REQ-TASKS-RUN-SCHEDULING-001: Queued run scheduling -**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. +**Intent:** Provide one durable, restart-safe queue consumer for explicit and +autonomous launches without creating workspace-local schedulers. #### Acceptance criteria -- **AC-TASKS-RUN-SCHEDULING-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. - -## Migrated source detail - -## Why - -Workflow actions and Office automation can enqueue agent work without a user -clicking Start. Users need that work to be dispatched reliably across multiple -workspaces without making ordinary Kanban assignment autonomous, wasting -resources on Office-only scans when Office is unused, or producing database -errors during a normal shutdown. - -## What - -- Kandev has one backend-wide scheduler for the persisted `runs` queue. It - serves every workspace and never creates one scheduler goroutine per - workspace. -- Runs are claimed globally in request order while preserving the existing - per-agent serialization rule. A busy workspace does not create another - scheduler or bypass agent capacity checks. -- An explicit `queue_run` workflow action may enqueue work from any workflow - style. User-initiated Kanban launches continue to bypass `runs` and launch - through the interactive orchestrator path. -- Merely assigning a runner to an ordinary Kanban task does not make that task - autonomous. Office assignment subscribers and unstarted-task recovery act - only on authoritative Office tasks: tasks linked to an Office project or - tasks whose workflow is the workspace's canonical `office_workflow_id`. -- Office-only maintenance is separate from queue draining. Unstarted-task - recovery, Office routines, and Office budget work run only when their - corresponding Office configuration exists. A deployment with Office enabled - but no configured Office workflow performs no five-second Office task - recovery scan. -- The queue scheduler remains event-driven: an in-process queue signal wakes it - immediately, while a periodic safety pass recovers persisted work after a - missed signal or process restart. -- Every scheduler or cron loop owns the goroutine it starts. `Stop` is - idempotent, cancels future ticks, and waits for an in-progress tick and any - handler fan-out to return. -- Graceful shutdown stops external intake, quiesces and joins queue/cron - schedulers, stops the orchestrator and agent runtime, and only then closes - event-bus, repository, and database resources. -- A normal signal-driven shutdown emits no `sql: database is closed` scheduler - errors and does not log scheduler activity after `Graceful shutdown - complete`. - -Decision: [ADR-2026-08-01-global-run-scheduler-ownership](../../../decisions/2026-08-01-global-run-scheduler-ownership.md). - -Implementation plan: [run-scheduler-lifecycle](../../../plans/run-scheduler-lifecycle/plan.md). - -## Data model - -No schema change is required. - -- `runs` remains the durable, backend-wide queue and run record. -- `workspaces.office_workflow_id` plus a non-empty task `project_id` remain the - authoritative persisted Office-task identity used by `Task.IsFromOffice`. -- Office routines, budget policies, and related configuration remain - workspace-scoped in their existing tables. - -## State machine - -### Scheduler lifecycle - -```text -stopped -> running -> stopping -> stopped -``` - -- `Start` moves `stopped -> running` and owns exactly one loop goroutine. -- A duplicate `Start` while running is a no-op. -- `Stop` moves `running -> stopping`, cancels the loop, waits for the active - tick to drain, then moves to `stopped`. -- A duplicate `Stop` is a no-op. -- Parent-context cancellation follows the same drain path as `Stop`. - -### Persisted run lifecycle - -The existing `queued`, `claimed`, and terminal run states are unchanged. -Process shutdown does not create a new run state and does not mark queued work -failed. A claimed run interrupted by process exit is recovered through the -existing stale-claim policy after restart. - -## Failure modes - -| Condition | Required behavior | -|---|---| -| Queue signal is missed | The periodic safety pass eventually claims the persisted row. | -| No eligible run exists | The scheduler returns to waiting without an error. | -| Office is enabled but no Office workflow/configuration exists | Generic queue recovery remains available; Office-only recovery and cron work do not scan ordinary Kanban tasks. | -| A Kanban task has a runner but no explicit `queue_run` action | Office subscribers and recovery ignore it; the user or Kanban workflow controls launch timing. | -| Shutdown begins while the scheduler is idle | `Stop` cancels the wait and joins before the database closes. | -| Shutdown begins during a tick | Shutdown cancels future ticks and waits for the active tick and cron handler fan-out to return before repository cleanup. The backend does not impose an internal join deadline. | -| A handler does not return after cancellation | Graceful shutdown remains in progress and repository cleanup does not begin. The launcher or process supervisor may enforce its outer grace period and terminate the process, but the backend never closes SQLite underneath live scheduler work. | - -## Persistence guarantees - -- Queued and scheduled-retry runs survive restart. -- Shutdown never deletes or terminally fails a queued run merely because the - process is exiting. -- Scheduler ownership state, contexts, timers, and in-memory queue signals do - not survive restart; persisted rows are the recovery source of truth. -- Office activation state is derived from persisted workspace/workflow and - Office configuration rather than an in-memory workspace list. - -## Scenarios - -- **GIVEN** two workspaces have queued runs for different agents, **WHEN** the - global scheduler drains the queue, **THEN** it processes both workspaces in - request order while allowing at most one claimed run per agent. -- **GIVEN** Office mode is enabled and no workspace has an Office workflow, - Office project, active Office routine, or Office budget policy, **WHEN** the - backend remains idle, **THEN** no Office unstarted-task recovery query runs - on the five-second queue cadence and no task is launched. -- **GIVEN** an ordinary Kanban `TODO` task has a step runner, **WHEN** Office - assignment subscribers and recovery execute, **THEN** no `task_assigned` run - is created for that task. -- **GIVEN** an authoritative Office `TODO` task has a runner, was created inside - the recovery lookback window, and has no prior queued, claimed, or finished - run, **WHEN** Office recovery executes, **THEN** exactly one idempotent - `task_assigned` run is queued. -- **GIVEN** a custom non-Office workflow explicitly executes `queue_run`, - **WHEN** the row is persisted and signalled, **THEN** the global scheduler - processes it even though the task is not an Office task. -- **GIVEN** the backend receives SIGINT while all schedulers are idle, **WHEN** - graceful shutdown completes, **THEN** the database closes after every - scheduler has stopped and the log contains no `database is closed` error. -- **GIVEN** a scheduler tick or cron handler is blocked inside database-backed - work, **WHEN** graceful shutdown begins, **THEN** shutdown waits for that work - to return before closing the database and prints no scheduler-stopping log - after the completion message. -- **GIVEN** queued runs exist when the process exits, **WHEN** Kandev starts - again, **THEN** the periodic safety pass resumes them without requiring an - Office workspace to own a separate scheduler. +- **AC-TASKS-RUN-SCHEDULING-001.1:** When queued runs exist in multiple + workspaces, one backend-wide scheduler shall claim them in global request + order while preserving the existing per-agent serialization rule. +- **AC-TASKS-RUN-SCHEDULING-001.2:** When a workflow explicitly uses + `queue_run`, the system shall enqueue and process that work for any workflow + style; an interactive user launch shall retain its existing launch path. +- **AC-TASKS-RUN-SCHEDULING-001.3:** When a task is an ordinary Kanban task, + assigning a runner shall not make it autonomous; Office assignment and + recovery shall act only on authoritative Office tasks. +- **AC-TASKS-RUN-SCHEDULING-001.4:** When Office configuration is absent, the + system shall not run Office-only recovery or cron work on ordinary Kanban + tasks. +- **AC-TASKS-RUN-SCHEDULING-001.5:** When a queue signal is missed or the + process restarts, a periodic safety pass shall recover persisted queued work. +- **AC-TASKS-RUN-SCHEDULING-001.6:** When graceful shutdown begins, scheduler + and cron work shall stop and join before the repository or database closes. +- **AC-TASKS-RUN-SCHEDULING-001.7:** When queued or retryable runs exist during + shutdown, they shall remain recoverable after restart and shall not be + deleted or terminally failed only because the process exits. +- **AC-TASKS-RUN-SCHEDULING-001.8:** When shutdown completes normally, the + backend shall not emit scheduler database-closed errors or scheduler logs + after the graceful-shutdown completion message. ## Out of scope - Multiple active backend processes or distributed leader election. - Per-workspace scheduler goroutines. -- Changing global FIFO ordering or adding workspace fairness/quotas. -- Renaming the existing `office.run.*` WebSocket subjects. -- Changing retry counts, retry backoff, or agent cooldown policy. -- Adding a user-facing scheduler status page or new feature toggle. -- Replacing the five-second generic safety cadence with dynamic retry timers; - this change removes Office-only work from that cadence but preserves its - existing queue-recovery contract. \ No newline at end of file +- Changes to FIFO ordering, retry policy, or scheduler status UI. + +## System design + +- [Queued Run Scheduling System Design](../system-design/run-scheduling.md) + +## Related records + +- [Global run scheduler ownership](../../../decisions/2026-08-01-global-run-scheduler-ownership.md) +- [Run scheduler lifecycle](../../../plans/run-scheduler-lifecycle/plan.md) diff --git a/docs/specs/tasks/requirements/task-launch-failure-recovery.md b/docs/specs/tasks/requirements/task-launch-failure-recovery.md index d4019d3213..1e7da056de 100644 --- a/docs/specs/tasks/requirements/task-launch-failure-recovery.md +++ b/docs/specs/tasks/requirements/task-launch-failure-recovery.md @@ -2,291 +2,64 @@ status: draft system: tasks created: 2026-08-19 +updated: 2026-08-24 owners: - cfl12 --- + # Task Launch Failure Recovery Requirements ## Overview -This document is the migrated task-system source for the capability. The source detail below remains authoritative while the system is migrated into separate requirement and design records. +Task launch can fail because a pull request is already terminal, a base branch +is stale or missing, or a generic preparation error occurs. Users need a +durable, safe error summary and targeted recovery actions on desktop and +mobile. ## Requirements ### REQ-TASKS-TASK-LAUNCH-FAILURE-RECOVERY-001: Task Launch Failure Recovery -**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. +**Intent:** Make handled launch failures observable and recoverable without +launching work against the wrong pull request or repository branch. #### Acceptance criteria -- **AC-TASKS-TASK-LAUNCH-FAILURE-RECOVERY-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. - -## Migrated source detail - -## Why - -A task launch can fail after its base branch disappears or its stored default branch becomes stale. -Today, the user sees a transient raw Git error and no durable recovery path. - -PR-review automation can also launch work after the relevant PR is complete. -This wastes an agent run and can create a misleading failed task. - -## What - -- A workflow `on_enter` auto-start does not launch work when every relevant GitHub PR is terminal. -- The gate keeps the task in its current workflow step and records a durable informational reason. -- A manual launch always bypasses the PR gate. -- Every handled launch error has a typed category, a safe message, and an ordered set of recovery actions. -- The task surface shows the error after reload. The launch toast points the user to that surface. -- A stale repository default resolves from the live remote default before the launch stops. -- A successful fallback writes the resolved branch to the exact `task_repositories` row. -- Recovery actions target one task-repository row. Multi-repository and multi-branch tasks stay unambiguous. -- Desktop and mobile provide the same recovery outcomes. - -## Data model - -### Session-owned launch error - -A launch that created a session stores its error in `task_sessions.metadata["last_agent_error"]`. -The existing `LastAgentError` record gains two fields. - -```text -LastAgentError - message string safe user-readable message - occurred_at timestamp - agent_execution_id string optional - code string stable failure category - details string optional bounded detail - recovery_actions []string ordered known values, maximum 3 - task_repository_id string optional exact failed row - stamp string optional bounded identity for typed launch errors - dismissed_at timestamp optional -``` - -### Task-owned pre-session launch error - -An auto-start gate runs before a session exists. -It stores an informational error in `tasks.metadata["last_launch_error"]`. - -```text -TaskLaunchError - message string safe user-readable message - occurred_at timestamp - code string stable failure category - details string optional bounded safe diagnostic detail - recovery_actions []string ordered known values, maximum 3 - task_repository_id string optional exact affected row - stamp string stable identity for idempotent replay -``` - -The gate derives `stamp` from the sorted relevant PR identities and states. -The gate preserves the record and its timestamp when the same stamp already exists. - -The failure categories are: - -- `base_branch_missing` -- `pr_already_closed` -- `default_branch_unresolved` -- `generic_launch_failure` - -The recovery actions are: - -- `retry_default` -- `pick_base_branch` -- `mark_review_done` - -### Bounded task projection - -`TaskStatusSummary.active_error` projects the newest active task-owned or session-owned error. -It has this wire shape: - -```text -active_error - session_id string optional for a task-owned error - task_repository_id string optional exact affected row - stamp string - occurred_at timestamp - preview string maximum 512 UTF-8 bytes - category string maximum 64 UTF-8 bytes - recovery_actions []string known values only, deduplicated, maximum 3 -``` - -The projector ignores unknown recovery actions. -It preserves the declared action order after it removes duplicates. -Malformed metadata cannot make the full summary invalid. - -GitHub PR state remains in `github_task_prs`. -Task-repository PR identity remains in `task_repositories.metadata["pr_number"]`. -This feature adds no database column or table. - -## API surface - -### WebSocket action `task.launch.recover` - -The new action accepts this payload: - -```json -{ - "task_id": "task-id", - "session_id": "optional-failed-session-id", - "task_repository_id": "required-for-branch-actions", - "action": "retry_default | pick_base_branch | mark_review_done", - "base_branch": "required-only-for-pick_base_branch", - "error_stamp": "required-current-active-error-stamp" -} -``` - -The success response is: - -```json -{ - "ok": true, - "task_id": "task-id", - "session_id": "optional-new-or-reused-session-id" -} -``` - -The handler authorizes `task_id` first. -If `session_id` exists, the handler proves that the session belongs to the task. -If `task_repository_id` exists, the handler proves that the row belongs to the task. -The handler rejects a request when `error_stamp` does not match the current active error. - -`retry_default` and `pick_base_branch` require `task_repository_id`. -`mark_review_done` does not require a session or repository row. -The existing `session.recover` action and its action values do not change. - -### Status delivery - -Boot state, task reads, and `task.status_summary.updated` carry the extended `active_error`. -The contract remains a complete replacement value with a monotonic revision. - -### Default-branch resolution - -`gitref.DefaultBranchOrEmpty` returns empty when only the current local `HEAD` branch exists. -It stays a pure local-ref helper and does not run a network command. - -The worktree manager owns live remote-default refresh through -`Manager.ResolveRemoteDefaultBranch(ctx, repoPath)`. -It first reads `refs/remotes/origin/HEAD`. -If necessary, it runs one bounded, noninteractive `git remote set-head origin --auto` command. - -## State machine - -### Relevant PR selection - -The auto-start gate compares each `task_repositories` row with active PR links for the task. - -1. A positive metadata `pr_number` matches the exact `(repository_id, pr_number)` PR. -2. Without that identity, an exact normalized `(repository_id, checkout_branch, head_branch)` match applies. -3. A PR for a different repository or checkout branch is not relevant. - -Branch normalization trims whitespace and one leading Git ref prefix. -The accepted prefixes are `refs/heads/`, `refs/remotes/origin/`, and `origin/`. -The remaining branch comparison is case-sensitive. - -The gate skips launch only when at least one relevant PR exists and every relevant PR is `merged` or `closed`. -An `open`, empty, or unknown state keeps the normal launch path. -This rule makes an open relevant PR win over a terminal sibling PR. - -### Launch transitions - -| Trigger | Condition | Outcome | -| --- | --- | --- | -| Workflow `on_enter` | All relevant PRs are terminal | No session or worktree starts. The task keeps its step and stores `pr_already_closed`. | -| Workflow `on_enter` | A relevant PR is open or unknown | The launch proceeds. | -| Workflow `on_enter` | No relevant PR exists or lookup fails | The launch proceeds. | -| Manual launch | Any PR state | The launch proceeds. | -| Launch preparation | The selected base and all fallbacks are missing | The session and task enter their existing failed states. The session stores `base_branch_missing`. | -| Launch preparation | A valid fallback resolves | The launch continues. The exact task-repository base branch self-heals. | - -A successful later launch clears `tasks.metadata["last_launch_error"]`. -A failed manual launch replaces the projected error only when its occurrence time is newer. - -### Recovery transitions - -- `retry_default` resolves the live remote default for the targeted row. - It writes the repository default and the task-repository base, then relaunches. -- `pick_base_branch` validates the selected remote branch for the targeted row. - It writes the task-repository base, then relaunches. -- `mark_review_done` lists the workflow steps in position order. - It selects the final step only when `workflow/models.IsTerminalStep(final, nil)` returns true. - It also requires the relevant PR selection to contain only terminal states. - It moves the task through the normal task move service, including history, WIP, state, and events. - A recovery-only move option permits `FAILED` to become `COMPLETED` at that terminal step. - Normal manual moves continue to preserve failed and cancelled states. - -The UI offers `mark_review_done` only when the workflow and PR conditions are valid. -A forged request fails when either condition is invalid. -The action is idempotent when the task already occupies that terminal step. - -## Failure modes - -- If the PR lookup fails, the gate logs the error and launches normally. -- If no PR link matches a task-repository row, the gate launches normally. -- If relevant PR rows contain conflicting states, any open or unknown state launches normally. -- If live remote-default refresh times out, the error stays distinct from an unresolved default. -- If live remote-default refresh has an auth or network error, the typed cause remains available for logs. -- If no remote default resolves, the UI shows `default_branch_unresolved` and offers `pick_base_branch`. -- If branch existence cannot be determined, the error remains a verification error, not a missing-branch error. -- If task-repository self-heal fails, the launch continues and logs a warning. -- If repository identity is empty or ambiguous, the backend omits repository-scoped recovery actions. -- If the workflow has no valid terminal step, the backend omits `mark_review_done`. -- If a relevant PR is open or unknown, the backend omits `mark_review_done`. -- If a recovery request names a foreign session or repository row, the request fails without mutation. -- If a recovery request names a stale error stamp, the request fails without mutation. - -The typed task error replaces the old missing-branch warning message and its archive/delete actions. -The old `missing_pr_branch_recovery_claimed` metadata claim no longer applies to handled launch errors. -The handled path does not suppress the new pointer toast. - -## Persistence guarantees - -Session-owned errors survive restart in `task_sessions.metadata`. -Pre-session gate outcomes survive restart in `tasks.metadata`. -The status projection rebuild reads both sources and selects the newest active error. - -A successful recovery clears the source error after the required write and relaunch or move succeeds. -A failed recovery updates the same source with the new category and available actions. -No successful branch write is rolled back after a later launch error. - -The corrected repository default persists in `repositories.default_branch`. -The corrected task base persists in the exact `task_repositories.base_branch` row. - -## Desktop and mobile behavior - -The task Chat surface shows one persistent launch-error card before the empty-state message. -The card uses the same projection and action handler on all viewports. -If a matching failed session renders the card, the summary path does not render a duplicate. - -Desktop shows recovery buttons inline and opens the existing branch picker pattern. -Phone layouts keep the card inline and wrap actions without horizontal page overflow. -The branch choice uses `MobilePickerSheet`, with the task Chat surface as its entry point. - -Each new recovery control has a touch target of at least 44px. -The task Chat surface remains the only vertical scroll owner. -The picker owns its internal list scroll and clears the bottom safe area. - -## Scenarios - -- **GIVEN** a task row linked to one merged PR, **WHEN** `on_enter` auto-start runs, **THEN** no session starts and the task shows `pr_already_closed` after reload. -- **GIVEN** a task with an open PR and a terminal PR, **WHEN** auto-start runs, **THEN** the launch proceeds for the open PR. -- **GIVEN** one repository with two task branches, **WHEN** only the sibling branch PR is terminal, **THEN** the current branch launch proceeds. -- **GIVEN** a PR lookup error or no relevant PR, **WHEN** auto-start runs, **THEN** the launch proceeds. -- **GIVEN** absent selected and fallback bases, **WHEN** the live default resolves, **THEN** launch continues and the exact row self-heals. -- **GIVEN** no usable base or default, **WHEN** launch preparation stops, **THEN** the failed session shows `base_branch_missing` with unambiguous actions. -- **GIVEN** one multi-repository row with a missing base, **WHEN** the user selects another base, **THEN** only that row changes before relaunch. -- **GIVEN** a valid terminal step, **WHEN** the user selects Mark review done, **THEN** the move completes the task without launching. -- **GIVEN** a reopened relevant PR, **WHEN** the user sends a stale Mark review done action, **THEN** no task mutation occurs. -- **GIVEN** no valid terminal workflow step, **WHEN** the task error renders, **THEN** Mark review done is absent. -- **GIVEN** a local import with only a feature-branch `HEAD`, **WHEN** default detection runs, **THEN** the stored default stays empty. -- **GIVEN** a handled launch error, **WHEN** the browser receives it, **THEN** the toast points to task details without raw Git output. -- **GIVEN** recovery on a phone, **WHEN** the user chooses a base, **THEN** the task relaunches without horizontal page overflow. +- **AC-TASKS-TASK-LAUNCH-FAILURE-RECOVERY-001.1:** When workflow auto-start + finds relevant GitHub pull requests and every relevant pull request is + terminal, it shall leave the task + in its current step, avoid starting a session, and show a durable + `pr_already_closed` reason after reload; manual launch shall bypass this + gate. +- **AC-TASKS-TASK-LAUNCH-FAILURE-RECOVERY-001.2:** When a handled launch error + is projected, the task surface shall show a safe category, bounded detail, + and only the recovery actions valid for the affected task repository. +- **AC-TASKS-TASK-LAUNCH-FAILURE-RECOVERY-001.3:** When a live remote default + or user-selected branch resolves, the system shall persist it to the exact + task-repository row before relaunching. +- **AC-TASKS-TASK-LAUNCH-FAILURE-RECOVERY-001.4:** When a recovery request + names a foreign session or repository row, or an error stamp that is no + longer current, the system shall reject it without mutation. +- **AC-TASKS-TASK-LAUNCH-FAILURE-RECOVERY-001.5:** When a user selects + `mark_review_done`, the system shall offer and accept it only for a valid + terminal workflow step with all relevant pull requests in terminal states. +- **AC-TASKS-TASK-LAUNCH-FAILURE-RECOVERY-001.6:** When a launch lookup fails, + no relevant pull request exists, or a relevant pull request is open or + unknown, the system shall preserve the normal launch path. +- **AC-TASKS-TASK-LAUNCH-FAILURE-RECOVERY-001.7:** When recovery is used on + desktop or mobile, the same error projection, authorization, and outcome + shall apply without horizontal page overflow. +- **AC-TASKS-TASK-LAUNCH-FAILURE-RECOVERY-001.8:** When recovery fails, the + source error record shall remain visible and update with the new typed cause, + bounded details, and valid actions; a successful recovery shall clear it only + after the required relaunch or task move succeeds. ## Out of scope - A background poller for all repository defaults. -- A bulk repair of existing repository-default values. -- A PR gate for manual launches. -- PR gating for providers other than GitHub. -- Changes to ACP resume semantics. -- A different self-heal target for stacked PRs. \ No newline at end of file +- A PR gate for manual launches or providers other than GitHub. +- Changes to ACP resume semantics or bulk repository-default repair. + +## System design + +- [Task Launch Failure Recovery System Design](../system-design/task-launch-failure-recovery.md) diff --git a/docs/specs/tasks/system-design/archive-confirmation.md b/docs/specs/tasks/system-design/archive-confirmation.md new file mode 100644 index 0000000000..d58a0bcbda --- /dev/null +++ b/docs/specs/tasks/system-design/archive-confirmation.md @@ -0,0 +1,80 @@ +--- +status: current +system: tasks +requirements: + - REQ-TASKS-ARCHIVE-CONFIRMATION-001 +created: 2026-08-24 +owners: + - kandev +--- + +# Task Archive Confirmation System Design + +## Context and boundaries + +The task system owns the user-level archive-confirmation preference and the +post-archive task selection contract. Archive execution, deletion, and +programmatic archive callers remain separate contracts. + +## Requirement mapping + +| Requirement | Design sections | +| --- | --- | +| REQ-TASKS-ARCHIVE-CONFIRMATION-001 | Preference, archive flow, navigation, failure behavior | + +## Components and responsibilities + +- The user-settings service reads and writes the preference. +- Task archive actions ask the settings state whether confirmation is required. +- The archive dialog remains responsible for cleanup summary and optional + cascade selection. +- The task-navigation coordinator selects a surviving task or the task + overview after a successful archive. +- Desktop and mobile archive surfaces use the same preference and navigation + coordinator. + +## Data and API contracts + +The existing user settings JSON stores `confirm_task_archive`. A missing value +means true for backward compatibility. The existing user-settings GET, PATCH, +and `user.settings.updated` contracts carry the boolean. No archive endpoint +changes. + +## Control flow + +When confirmation is enabled, user archive actions open the existing dialog. +The dialog may request a cascade archive. When confirmation is disabled, the +action archives only the requested task immediately; it never silently adds +subtasks. + +After an active task is archived, navigation and the URL update as one +transaction from the user's perspective. A cascade archive excludes the +archived tree from replacement-task selection. If no safe task remains, the +overview is the destination. + +## Failure and recovery + +The settings client keeps the previous value when persistence fails. Until +settings load successfully, the client requires confirmation. If an archive +fails after temporary navigation, the coordinator restores the original task +and URL. Existing archive-surface error handling remains authoritative. + +## Persistence and compatibility + +The preference survives restarts in the existing per-user settings record and +applies across workspaces for that user. Delete confirmations, API/CLI/MCP +archive callers, and programmatic archive operations do not consult this +preference. + +## Verification + +- Unit-test missing, true, false, and failed settings mutations. +- Test archive navigation with unrelated tasks, cascade trees, and no safe + destination. +- Cover desktop and mobile archive entry points with the existing task-surface + integration tests. + +## Related records + +- [Archive Confirmation Preference](../../../plans/archive-confirmation-preference/plan.md) +- [Cascade Archive Navigation](../../../plans/cascade-archive-navigation/plan.md) diff --git a/docs/specs/tasks/system-design/run-scheduling.md b/docs/specs/tasks/system-design/run-scheduling.md new file mode 100644 index 0000000000..dd6f83688c --- /dev/null +++ b/docs/specs/tasks/system-design/run-scheduling.md @@ -0,0 +1,82 @@ +--- +status: current +system: tasks +requirements: + - REQ-TASKS-RUN-SCHEDULING-001 +created: 2026-08-24 +owners: + - cfl +--- + +# Queued Run Scheduling System Design + +## Context and boundaries + +The task system owns the persisted runs queue and its single backend-wide +consumer. Office supplies some run producers and Office-only maintenance, but +does not own a scheduler per workspace. + +## Requirement mapping + +| Requirement | Design sections | +| --- | --- | +| REQ-TASKS-RUN-SCHEDULING-001 | Queue ownership, classification, lifecycle, shutdown, recovery | + +## Queue ownership and classification + +The existing `runs` table is the durable queue. One scheduler claims work for all +workspaces in global request order while preserving the existing per-agent +serialization rule. Explicit `queue_run` workflow actions may enqueue from any +workflow style. +Interactive user launches stay on the interactive orchestrator path. + +Office assignment subscribers and unstarted-task recovery classify a task as +Office-owned only through the authoritative Office project/workflow identity. +An ordinary Kanban runner never becomes autonomous by assignment alone. +Office recovery and cron work are disabled when their corresponding Office +configuration is absent. + +## Scheduler lifecycle + +The scheduler and each cron loop own one goroutine and one cancellation context. +The lifecycle is stopped -> running -> stopping -> stopped. Duplicate Start and +Stop calls are idempotent. Stop cancels future ticks and waits for the active +tick and handler fan-out before returning. + +An in-process signal wakes the consumer immediately. A periodic safety pass +reclaims persisted work after a missed signal or restart. No new schema or run +state is introduced. + +## Shutdown and recovery + +Graceful shutdown stops external intake, joins queue and cron loops, stops the +orchestrator and agent runtime, then closes event-bus, repository, and database +resources. SQLite is never closed underneath live scheduler work. A blocked +handler keeps shutdown in progress; an outer supervisor may enforce its own +process grace period. + +Queued and retryable runs remain durable across restart. A claimed run follows +the existing stale-claim recovery policy. In-memory signals and scheduler +contexts are not persistence sources. + +## Failure and observability + +Missed signals are recovered by the safety pass. An empty queue is a normal +wait state. Office configuration without an Office workflow does not trigger +ordinary Kanban scans. A signal-driven shutdown must not emit database-closed +scheduler errors or scheduler logs after the graceful-shutdown completion +message. + +## Verification + +- Test one global consumer across multiple workspaces, global FIFO ordering, and + per-agent serialization. +- Test Office classification, explicit `queue_run`, missed-signal recovery, and + restart recovery. +- Test idempotent lifecycle calls and shutdown while idle or inside a handler. +- Test that queued rows remain available after process restart. + +## Related records + +- [Global run scheduler ownership](../../../decisions/2026-08-01-global-run-scheduler-ownership.md) +- [Run scheduler lifecycle](../../../plans/run-scheduler-lifecycle/plan.md) diff --git a/docs/specs/tasks/system-design/task-launch-failure-recovery.md b/docs/specs/tasks/system-design/task-launch-failure-recovery.md new file mode 100644 index 0000000000..565c626f0f --- /dev/null +++ b/docs/specs/tasks/system-design/task-launch-failure-recovery.md @@ -0,0 +1,103 @@ +--- +status: draft +system: tasks +requirements: + - REQ-TASKS-TASK-LAUNCH-FAILURE-RECOVERY-001 +created: 2026-08-24 +owners: + - cfl12 +--- + +# Task Launch Failure Recovery System Design + +## Context and boundaries + +The task system owns launch gating, typed launch-error projection, and recovery +actions for a task repository. GitHub remains the source of pull-request state; +the workspace system resolves repository branches; the UI renders the +task-owned projection. + +## Requirement mapping + +| Requirement | Design sections | +| --- | --- | +| REQ-TASKS-TASK-LAUNCH-FAILURE-RECOVERY-001 | PR gate, error projection, recovery actions, responsive surface | + +## PR gate and launch paths + +The auto-start gate selects relevant PRs by explicit repository and PR identity, +then exact repository/branch identity. It skips only when at least one relevant +PR exists and every one is merged or closed. An open, empty, unknown, failed +lookup, or absent PR leaves the normal auto-start path available. Manual launch +always bypasses this gate. + +The gate stores an informational task error when it suppresses an auto-start. +The error is stamped from the sorted relevant PR identities and states so the +same observation is idempotent. + +## Error model and projection + +Session-owned errors live in `task_sessions.metadata.last_agent_error`. +Pre-session gate errors live in `tasks.metadata.last_launch_error`. Both use a +safe message, timestamp, stable category, bounded details, ordered recovery +actions, exact task-repository identity when applicable, and an idempotency +stamp. + +The supported categories are `base_branch_missing`, `pr_already_closed`, +`default_branch_unresolved`, and `generic_launch_failure`. The supported +actions are `retry_default`, `pick_base_branch`, and `mark_review_done`. + +The `TaskStatusSummary.active_error` projection selects the newest active +record, limits strings and actions, removes duplicate actions without changing +their order, and ignores malformed optional metadata without invalidating the +full summary. Boot state, task reads, and `task.status_summary.updated` carry +the complete replacement projection. + +## Recovery action contract + +The `task.launch.recover` action authorizes the task first, then proves any +session and task-repository identities belong to that task. The request includes +the current error stamp; stale stamps fail without mutation. + +- `retry_default` resolves the live remote default for one repository row and + relaunches. +- `pick_base_branch` validates and persists one selected branch before + relaunch. +- `mark_review_done` is allowed only for a valid terminal workflow step and + when every relevant PR is terminal; it uses the normal task-move service. + +The existing `session.recover` action is unchanged. A failed recovery +preserves the source error record, keeps it visible, and updates its typed +category, bounded details, and valid actions. A successful recovery clears the +source error only after its write and relaunch or move succeed. + +## Branch resolution and persistence + +Local default detection remains a pure helper and returns empty when only a +local HEAD branch exists. The worktree manager owns bounded remote-default +refresh. A resolved default may be cached in `repositories.default_branch`, +but `retry_default` and `pick_base_branch` must write the resolved base to +the exact `task_repositories` row and that write must succeed before relaunch. +No new table is required. + +## Failure and security + +PR lookup failures launch normally. Remote-default timeout, authentication, +network, missing branch, and unresolved default remain distinct diagnostics. +Ambiguous repository identity omits repository-scoped actions. Foreign session +or repository IDs and stale stamps fail without mutation. + +## Responsive presentation + +The task Chat surface renders one persistent error card from the shared +projection. Desktop actions are inline; mobile actions wrap and branch +selection uses the existing mobile picker. Both surfaces use the same action +authorization and remain free of horizontal overflow. + +## Verification + +- Test relevant-PR selection, terminal/open precedence, lookup failures, and + manual bypass. +- Test error projection limits, stamps, persistence, and recovery authorization. +- Test branch self-healing and mark-review-done terminal-step checks. +- Cover desktop and mobile recovery actions with the existing task Chat tests. diff --git a/docs/specs/templates/requirement.md b/docs/specs/templates/requirement.md index 4186bf9755..9d084f0fd2 100644 --- a/docs/specs/templates/requirement.md +++ b/docs/specs/templates/requirement.md @@ -10,7 +10,8 @@ owners: ## Overview -Explain what the capability does and why an actor or consumer needs it. +Explain what the capability does and why an actor or consumer needs it. State +why this system owns the contract when another system exposes the behavior. ## Terminology @@ -33,5 +34,4 @@ Explain what the capability does and why an actor or consumer needs it. ## Out of scope -- List an explicit exclusion. - +- List an explicit exclusion, including adjacent system contracts. diff --git a/docs/specs/templates/system-design.md b/docs/specs/templates/system-design.md index 52b6578bd7..e0255cdc2d 100644 --- a/docs/specs/templates/system-design.md +++ b/docs/specs/templates/system-design.md @@ -9,7 +9,8 @@ requirements: ## Purpose and boundaries -Explain the technical responsibility and its boundaries. +Explain why this system owns the technical contract. Name adjacent contracts +that this design uses but does not own. ## Requirement mapping @@ -19,7 +20,8 @@ Explain the technical responsibility and its boundaries. ## Components and responsibilities -Name the runtime components and their responsibilities. +Name all stable runtime components and their responsibilities. Cover backend +and frontend components when both implement the same owned outcome. ## Data and contracts diff --git a/docs/specs/ui/README.md b/docs/specs/ui/README.md index d03b743190..58b598241c 100644 --- a/docs/specs/ui/README.md +++ b/docs/specs/ui/README.md @@ -2,7 +2,7 @@ status: draft system: ui specification_version: 1 -migration: complete +migration: in_progress owners: - kandev --- @@ -20,11 +20,18 @@ This system owns navigation, settings presentation, boards, task and review surfaces, walkthroughs, chat controls, visual feedback, and responsive interaction contracts that do not own backend state. +A control is not UI-owned only because it appears in the web application. A +provider or task system keeps ownership when the control configures or displays +that system's state. The UI system owns only the independent presentation +contract that other capabilities can reuse. + ## Exclusions - Durable task behavior belongs to the [task system](../tasks/README.md). - Agent profile behavior belongs to the [agent system](../agents/README.md). - Plugin contribution contracts belong to the [plugin system](../plugins/README.md). +- Provider-specific state and actions belong to the + [integration system](../integrations/README.md). ## Specification map @@ -148,7 +155,8 @@ interaction contracts that do not own backend state. ## Migration record -All legacy sources assigned to this system are now represented by the canonical requirement and system-design documents above. Source detail is retained in those documents or in their linked design parts. +Migration remains in progress while legacy source detail is extracted from the +canonical requirement and system-design documents above. ## Related systems diff --git a/docs/specs/ui/requirements/ci-pr-automation.md b/docs/specs/ui/requirements/ci-pr-automation.md index f9a9115040..4a47512f9e 100644 --- a/docs/specs/ui/requirements/ci-pr-automation.md +++ b/docs/specs/ui/requirements/ci-pr-automation.md @@ -21,7 +21,7 @@ Task-to-PR associations survive restarts and archive/unarchive. Hard deletion removes task-owned associations and refresh watches; it is not contribution history. Decision: ADR-2026-08-13-hard-delete-task-contribution-links. -Decision: [ADR-0051](../../decisions/0051-pr-agent-notifications-extend-task-pr-automation.md) +Decision: [ADR-0051](../../../decisions/0051-pr-agent-notifications-extend-task-pr-automation.md) (the task-level control plane for the five switches was superseded by per-PR scoping; see that ADR's Consequences section). @@ -37,7 +37,7 @@ linked PR moving throughout its lifecycle, configured independently per linked P several open PRs does not force the same setting onto all of them. Task-to-PR associations survive restarts and archive/unarchive. Hard deletion removes task-owned associations and refresh watches; it is not contribution history. Decision: ADR-2026-08-13-hard-delete-task-contribution-links. -Decision: [ADR-0051](../../decisions/0051-pr-agent-notifications-extend-task-pr-automation.md) (the +Decision: [ADR-0051](../../../decisions/0051-pr-agent-notifications-extend-task-pr-automation.md) (the task-level control plane for the five switches was superseded by per-PR scoping; see that ADR's Consequences section). diff --git a/docs/specs/workspaces/README.md b/docs/specs/workspaces/README.md index 275ee0823e..8842d001ca 100644 --- a/docs/specs/workspaces/README.md +++ b/docs/specs/workspaces/README.md @@ -2,7 +2,7 @@ status: draft system: workspaces specification_version: 1 -migration: complete +migration: in_progress owners: - kandev --- @@ -52,7 +52,8 @@ Git state. ## Migration record -All legacy sources assigned to this system are now represented by the canonical requirement and system-design documents above. Source detail is retained in those documents or in their linked design parts. +Migration remains in progress while legacy source detail is extracted from the +canonical requirement and system-design documents above. ## Related systems diff --git a/scripts/lint-spec-files.py b/scripts/lint-spec-files.py index 8c7a2561f1..7c65800306 100644 --- a/scripts/lint-spec-files.py +++ b/scripts/lint-spec-files.py @@ -62,7 +62,7 @@ def lint_specs(root: Path, config: dict | None = None) -> list[Violation]: violations: list[Violation] = [] requirement_definitions: dict[str, list[tuple[Path, int]]] = {} acceptance_definitions: dict[str, list[tuple[Path, int]]] = {} - design_references: list[tuple[str, Path, int]] = [] + design_references: list[tuple[str, Path, int, str | None]] = [] systems_with_new_artifacts: set[str] = set() system_indexes: set[str] = set() @@ -157,7 +157,7 @@ def lint_specs(root: Path, config: dict | None = None) -> list[Violation]: references = metadata.get("requirements", []) if isinstance(references, list): for reference in references: - design_references.append((str(reference), path, 1)) + design_references.append((str(reference), path, 1, system)) for system in sorted(systems_with_new_artifacts - system_indexes): violations.append( @@ -172,7 +172,7 @@ def lint_specs(root: Path, config: dict | None = None) -> list[Violation]: violations.extend(check_duplicate_ids("requirement", requirement_definitions)) violations.extend(check_duplicate_ids("acceptance", acceptance_definitions)) known_requirements = set(requirement_definitions) - for requirement, path, line in design_references: + for requirement, path, line, design_system in design_references: if requirement not in known_requirements: violations.append( Violation( @@ -182,6 +182,19 @@ def lint_specs(root: Path, config: dict | None = None) -> list[Violation]: f"system design references unknown requirement {requirement}", ) ) + continue + definition_path, _ = requirement_definitions[requirement][0] + _, requirement_system = classify_path(definition_path.relative_to(root)) + if requirement_system != design_system: + violations.append( + Violation( + "cross-system-requirement-reference", + path, + line, + f"system design in `{design_system}` cannot claim requirement {requirement} " + f"owned by `{requirement_system}`; link to the owning system instead", + ) + ) violations.extend(check_size_exception_catalog(root, config)) violations.extend(check_legacy_size_ratchet(root, config, previous_config)) diff --git a/scripts/lint-spec-files.test.py b/scripts/lint-spec-files.test.py index 1c3fe34083..ebfb77569c 100644 --- a/scripts/lint-spec-files.test.py +++ b/scripts/lint-spec-files.test.py @@ -227,6 +227,22 @@ migration: in_progress self.assertIn("unknown-requirement-reference", self.rules(violations)) + def test_rejects_a_system_design_reference_owned_by_another_system(self) -> None: + self.write("docs/specs/task-system/README.md", self.valid_system_index()) + self.write("docs/specs/task-system/requirements/dependencies.md", self.valid_requirement()) + self.write( + "docs/specs/ui/README.md", + self.valid_system_index().replace("task-system", "ui"), + ) + self.write( + "docs/specs/ui/system-design/dependencies.md", + self.valid_design().replace("system: task-system", "system: ui"), + ) + + violations = load_linter().lint_specs(self.root, self.config) + + self.assertIn("cross-system-requirement-reference", self.rules(violations)) + def test_rejects_a_requirement_without_acceptance_criteria(self) -> None: content = self.valid_requirement().split("#### Acceptance criteria", 1)[0] self.write("docs/specs/task-system/requirements/dependencies.md", content)