From 8620537f0c952db8603777618088a2b63a8a727b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Sun, 23 Aug 2026 17:34:52 +0000 Subject: [PATCH] docs: migrate task and workflow specs (#2957) --- .../pr-walkthrough-workflow-contract_test.py | 6 +- .github/workflows/pr-walkthrough.yml | 3 +- .../backend/internal/mcp/handlers/handlers.go | 2 +- .../mcp/handlers/message_target_session.go | 2 +- .../handlers/message_target_session_test.go | 2 +- .../mcp/server/agent_decision_tool.go | 2 +- .../repository/sqlite/base_migrations.go | 4 +- apps/backend/internal/task/dto/dto.go | 2 +- .../internal/task/handlers/task_handlers.go | 2 +- .../task/handlers/task_http_handlers.go | 10 +- .../task_http_handlers_external_id_test.go | 4 +- apps/backend/internal/task/models/models.go | 2 +- .../task/repository/repoerrors/errors.go | 2 +- .../task/repository/sqlite/base_migrations.go | 2 +- .../repository/sqlite/task_external_id.go | 2 +- .../sqlite/task_external_id_postgres_test.go | 2 +- .../internal/task/service/external_id.go | 6 +- .../internal/task/service/service_events.go | 2 +- .../internal/task/service/service_requests.go | 2 +- .../internal/task/service/service_tasks.go | 4 +- .../workflow-quorum-transitions.spec.ts | 2 +- docs/decisions/0013-multi-branch-tasks.md | 2 +- ...icit-completion-signal-for-auto-advance.md | 2 +- ...-08-autopilot-mcp-question-capabilities.md | 2 +- .../decisions/2026-08-08-mcp-tool-profiles.md | 2 +- .../2026-08-08-task-autopilot-contract.md | 2 +- .../plans/agent-generated-task-titles/plan.md | 2 +- .../task-01-backend-title-lifecycle.md | 2 +- .../task-02-mcp-title-tool.md | 2 +- .../task-03-frontend-title-flow.md | 2 +- .../task-04-e2e-and-docs.md | 2 +- .../task-05-single-owner-title-handoff.md | 2 +- .../task-06-default-on-title-preference.md | 2 +- .../task-07-regression-and-docs.md | 2 +- .../plans/agent-title-branch-renaming/plan.md | 2 +- .../task-01-branch-rename-runtime.md | 2 +- .../task-02-title-tool-integration.md | 2 +- .../task-03-regression-documentation.md | 2 +- .../archive-confirmation-preference/plan.md | 2 +- .../task-01-backend-preference.md | 2 +- .../task-02-frontend-behavior.md | 2 +- .../task-03-e2e-and-verification.md | 4 +- docs/plans/attach-workspace-sources/plan.md | 2 +- .../task-01-durable-source-contracts.md | 2 +- .../task-02-attachment-service.md | 2 +- .../task-03-protocol-surfaces.md | 2 +- .../task-04-host-materialization.md | 2 +- .../task-05-remote-materialization.md | 2 +- .../task-06-shared-source-picker.md | 2 +- .../task-07-files-panel-surface.md | 2 +- .../task-08-end-to-end-coverage.md | 2 +- .../task-09-public-documentation.md | 2 +- .../task-10-final-verification.md | 2 +- .../task-11-shared-local-remote-selector.md | 2 +- ...task-12-unified-files-workspace-actions.md | 2 +- .../task-13-ux-delta-e2e.md | 2 +- .../task-14-ux-terminology-documentation.md | 2 +- .../plans/backend-failure-containment/plan.md | 2 +- ...ask-06-classify-startup-runtime-cleanup.md | 2 +- .../plan.md | 4 +- .../task-02-truncate-generated-titles.md | 4 +- .../task-03-confirmed-dead-runtime-cleanup.md | 4 +- docs/plans/cancelled-turn-completion/plan.md | 2 +- .../task-01-step-contract-and-template.md | 2 +- .../task-02-configuration-surfaces.md | 2 +- .../task-03-cancellation-routing.md | 2 +- .../task-04-settings-ui.md | 2 +- .../task-05-public-docs.md | 2 +- .../task-06-e2e-coverage.md | 2 +- docs/plans/cascade-archive-navigation/plan.md | 2 +- .../task-01-cascade-aware-switch.md | 2 +- .../task-02-archive-navigation-e2e.md | 2 +- .../clarification-active-lifecycle/plan.md | 2 +- .../task-01-clarification-regression-red.md | 2 +- .../task-02-current-turn-backend-authority.md | 2 +- .../task-03-summary-pending-convergence.md | 2 +- .../task-04-pending-owner-navigation.md | 2 +- .../task-05-clarification-regression-green.md | 2 +- .../plan.md | 2 +- ...ask-01-publish-clarification-task-state.md | 2 +- .../task-02-prove-live-sidebar-regrouping.md | 2 +- .../task-03-address-publication-review.md | 4 +- docs/plans/creation-flow-reliability/plan.md | 2 +- .../task-01-repository-tooltip.md | 4 +- .../creator-session-task-inheritance/plan.md | 2 +- .../task-01-initial-runtime-seed.md | 2 +- .../task-02-creator-session-resolution.md | 2 +- .../task-03-explain-session-default.md | 2 +- .../task-04-end-to-end-inheritance.md | 2 +- docs/plans/fork-pr-comparison-targets/plan.md | 2 +- ...task-03-pr-reconciliation-and-lifecycle.md | 2 +- docs/plans/github-issue-task-linking/plan.md | 2 +- .../task-01-backend-reverse-lookup.md | 2 +- .../task-02-frontend-link-and-indicator.md | 2 +- .../task-03-e2e-and-verification.md | 2 +- docs/plans/interrupted-task-indicator/plan.md | 2 +- .../task-01-backend-startup-marker.md | 2 +- .../task-02-backend-dto-and-clearing.md | 2 +- .../task-03-frontend-data-plumbing.md | 2 +- .../task-04-frontend-icon-rendering.md | 2 +- .../task-05-e2e-verification.md | 2 +- .../plan.md | 2 +- .../task-01-task-only-eager-tool.md | 2 +- .../task-02-canonical-tool-naming.md | 2 +- .../task-03-pin-and-reconnect-claude-acp.md | 2 +- ...task-04-kanban-reliability-verification.md | 2 +- .../mark-selected-task-repositories/plan.md | 2 +- .../task-01-workspace-repository-marker.md | 4 +- .../task-02-remote-repository-marker.md | 4 +- .../task-03-remote-identity-remediation.md | 2 +- .../task-04-workspace-coverage-remediation.md | 2 +- .../task-05-remote-identity-review-fix.md | 2 +- .../task-06-github-case-normalization.md | 2 +- .../task-07-lint-cleanup.md | 2 +- .../task-08-remote-popover-lint.md | 2 +- .../task-09-compact-selected-marker.md | 2 +- .../mcp-task-agent-profile-default/plan.md | 2 +- .../task-01-backend-preference-contract.md | 2 +- .../task-02-mcp-profile-resolution.md | 2 +- ...task-03-responsive-task-actions-setting.md | 2 +- .../task-04-desktop-mobile-e2e.md | 2 +- .../task-05-public-documentation.md | 2 +- docs/plans/multi-pr-review-selector/plan.md | 2 +- docs/plans/multi-repo-chat-file-links/plan.md | 2 +- ...-01-expose-authoritative-workspace-root.md | 2 +- ...-resolve-chat-links-from-workspace-root.md | 2 +- .../owned-link-target-mismatch-repair/plan.md | 4 +- .../task-01-task-unique-root-name.md | 2 +- .../task-02-owned-link-self-heal.md | 2 +- .../task-03-ownership-aware-repoint.md | 2 +- .../task-04-rollback-faithful-repoint.md | 4 +- .../task-05-safe-atomic-link-replacement.md | 2 +- docs/plans/parent-child-task-stop/plan.md | 2 +- .../task-01-execution-stop-semantics.md | 2 +- .../task-02-coordinator-stop-operation.md | 2 +- .../task-03-mcp-stop-handler.md | 2 +- .../task-04-task-mcp-tool.md | 2 +- .../task-05-integration-regression.md | 2 +- .../task-06-public-docs.md | 2 +- .../prevent-agent-autostart-on-open/plan.md | 2 +- .../task-01-backend-settings-field.md | 2 +- .../task-02-backend-ensure-override.md | 2 +- .../task-03-frontend-settings-plumbing.md | 2 +- .../task-04-settings-ui-card.md | 2 +- .../task-05-open-time-gates.md | 2 +- .../task-06-e2e.md | 2 +- docs/plans/prompt-attachments/plan.md | 2 +- .../task-01-attachment-storage-api.md | 2 +- .../task-02-agent-delivery-and-queue.md | 2 +- .../task-03-frontend-staged-uploads.md | 2 +- .../task-04-attachment-e2e.md | 2 +- .../prompt-attachments/task-05-public-docs.md | 6 +- .../queued-message-attachment-preview/plan.md | 2 +- .../task-01-queue-attachment-rendering.md | 4 +- docs/plans/queued-task-moves/plan.md | 2 +- .../task-01-atomic-move-admission.md | 2 +- .../task-02-deferred-destination-lifecycle.md | 2 +- .../task-03-kanban-queue-sections.md | 2 +- .../task-04-sidebar-queue-position.md | 2 +- .../task-05-e2e-and-documentation.md | 2 +- docs/plans/quick-chat-expiration/plan.md | 2 +- .../task-01-backend-candidate-query.md | 2 +- .../task-02-backend-expiration-service.md | 2 +- .../task-03-boot-hydration.md | 2 +- .../task-04-e2e-and-verification.md | 2 +- .../quick-chat-repository-context/plan.md | 2 +- .../task-01-backend-contract.md | 2 +- .../task-02-shared-repository-chips.md | 2 +- .../task-03-quick-chat-setup.md | 2 +- .../task-04-e2e-verification.md | 2 +- .../remote-contribution-head-drift/plan.md | 4 +- .../task-01-upstream-status-contract.md | 2 +- ...-checkout-relation-and-action-semantics.md | 2 +- ...3-diverged-changes-ui-and-mobile-parity.md | 2 +- ...04-responsive-force-push-regression-e2e.md | 2 +- .../plan.md | 2 +- ...ask-01-recover-provider-commit-evidence.md | 2 +- ...2-reconcile-and-style-commit-provenance.md | 2 +- ...03-verify-desktop-mobile-reconciliation.md | 2 +- .../remote-contribution-local-first/plan.md | 2 +- ...-01-exact-lease-contribution-operations.md | 2 +- ...local-first-relation-and-shared-actions.md | 2 +- ...-03-desktop-local-first-contribution-ui.md | 2 +- ...-04-mobile-contribution-version-choices.md | 2 +- .../task-05-desktop-mobile-drift-e2e.md | 2 +- ...sk-06-branch-scoped-drift-ux-correction.md | 2 +- docs/plans/remote-contribution-tasks/plan.md | 2 +- ...ask-01-provider-contribution-resolution.md | 2 +- .../task-02-mcp-creation-and-persistence.md | 2 +- ...03-runtime-contribution-materialization.md | 2 +- .../task-04-credential-and-push-routing.md | 2 +- .../task-05-integration-coverage-and-docs.md | 2 +- .../plan.md | 2 +- .../task-01-public-provider-resolution.md | 4 +- .../task-02-explicit-url-entry.md | 4 +- .../task-03-resolution-errors-and-retry.md | 4 +- .../task-04-mobile-e2e.md | 2 +- docs/plans/restore-live-add-branch/plan.md | 2 +- .../task-01-restore-live-materialization.md | 2 +- .../task-02-return-materialized-paths.md | 2 +- .../task-03-update-public-docs.md | 2 +- .../review-watcher-double-autostart/plan.md | 2 +- .../task-01-shared-autostart-claim.md | 2 +- docs/plans/rich-task-title-previews/plan.md | 2 +- .../task-01-preview-and-hierarchy.md | 2 +- .../task-02-contribution-status-and-state.md | 2 +- .../task-03-end-to-end-verification.md | 2 +- docs/plans/run-scheduler-lifecycle/plan.md | 2 +- .../task-01-owned-loop-lifecycles.md | 2 +- .../task-02-office-task-scoping.md | 2 +- .../task-03-office-maintenance-separation.md | 2 +- .../task-04-shutdown-composition.md | 2 +- .../plan.md | 2 +- ...task-01-order-runtime-state-publication.md | 2 +- docs/plans/sidebar-task-edit/plan.md | 2 +- .../task-01-shared-sidebar-edit-flow.md | 2 +- .../task-02-sidebar-edit-e2e.md | 4 +- docs/plans/stable-pr-changes-refresh/plan.md | 2 +- .../task-01-stabilize-pr-file-refresh.md | 2 +- .../stable-review-dialog-refresh/plan.md | 2 +- .../task-01-preserve-review-diff-refresh.md | 4 +- docs/plans/start-agent-composer-hint/plan.md | 2 +- .../task-01-shared-predicate-and-removal.md | 2 +- .../task-02-composer-hint-ui.md | 2 +- .../start-agent-composer-hint/task-03-e2e.md | 2 +- ...04-peer-message-resumes-stopped-session.md | 2 +- .../task-01-durable-task-cleanup.md | 2 +- docs/plans/subtask-completion-trigger/plan.md | 2 +- ...-01-backend-child-completion-repository.md | 4 +- .../task-02-workflow-event-contracts.md | 4 +- .../task-03-orchestrator-trigger-dispatch.md | 4 +- .../task-04-e2e-verification.md | 4 +- docs/plans/subtask-detachment/plan.md | 2 +- .../task-01-backend-detachment.md | 2 +- .../task-02-frontend-actions.md | 2 +- .../task-03-e2e-and-verification.md | 2 +- .../subtask-reparenting-drag-drop/plan.md | 2 +- .../task-01-backend-composite-semantics.md | 2 +- ...sk-02-frontend-nest-drop-infrastructure.md | 2 +- ...task-03-frontend-wiring-and-affordances.md | 2 +- .../task-04-e2e-and-verification.md | 2 +- docs/plans/task-autopilot/plan.md | 2 +- .../task-01-persist-task-contract.md | 2 +- .../task-02-derive-runtime-contract.md | 2 +- ...task-03-build-parent-question-lifecycle.md | 2 +- .../task-04-build-autopilot-ui.md | 2 +- .../task-05-prove-autopilot-e2e.md | 2 +- .../task-06-document-autopilot.md | 2 +- docs/plans/task-cleanup-retry-bounds/plan.md | 2 +- .../task-01-idempotent-missing-resources.md | 2 +- .../task-02-bounded-cleanup-retries.md | 2 +- .../task-context-menu-drag-guard/plan.md | 2 +- ...sk-01-guard-context-menu-pointer-events.md | 4 +- .../task-02-cancel-touch-drag-on-menu-open.md | 6 +- .../task-create-advanced-settings/plan.md | 4 +- .../task-01-advanced-settings-ui.md | 4 +- .../task-02-advanced-settings-e2e.md | 4 +- .../task-create-dependency-selector/plan.md | 2 +- .../task-01-selector-ui.md | 4 +- .../task-02-selector-e2e.md | 4 +- .../task-create-escape-dismissal/plan.md | 2 +- .../task-01-guard-dialog-escape.md | 4 +- .../task-02-capture-autocomplete-escape.md | 2 +- .../task-create-executor-default/plan.md | 2 +- .../task-01-executor-first-selection.md | 2 +- .../task-02-create-task-e2e.md | 2 +- .../task-create-source-executor-reset/plan.md | 2 +- .../task-01-reset-source-executor.md | 2 +- .../task-02-dialog-transition-e2e.md | 2 +- .../plans/task-create-workflow-memory/plan.md | 2 +- .../task-01-backend-workflow-memory.md | 2 +- .../task-02-frontend-workflow-resolution.md | 2 +- .../task-03-create-task-e2e.md | 2 +- docs/plans/task-dependencies/plan.md | 2 +- .../task-01-core-dependency-relationship.md | 2 +- .../task-02-gate-automated-launch.md | 2 +- .../task-03-resolve-and-auto-start.md | 2 +- .../task-04-failed-predecessor-halt.md | 2 +- .../task-05-mcp-dependency-tools.md | 2 +- .../task-06-kanban-dependency-ui.md | 2 +- .../task-07-dependencies-board-view.md | 2 +- .../task-08-verify-flows-and-docs.md | 4 +- .../task-launch-failure-recovery/plan.md | 2 +- .../task-01-failure-taxonomy-contracts.md | 2 +- .../task-02-gitref-default-hardening.md | 2 +- .../task-03-worktree-live-default-fallback.md | 2 +- .../task-04-launch-failure-classification.md | 2 +- .../task-05-pr-review-autostart-gating.md | 2 +- .../task-06-recovery-actions-ws.md | 2 +- .../task-07-status-summary-projection.md | 2 +- ...8-frontend-failure-surface-and-recovery.md | 2 +- .../task-09-e2e-and-i18n.md | 2 +- .../task-10-task-base-self-heal.md | 2 +- ...task-11-responsive-launch-error-surface.md | 2 +- .../task-12-remove-legacy-launch-guidance.md | 2 +- .../task-01-task-worktree-persistence.md | 2 +- docs/plans/task-runtime-cleanup/plan.md | 2 +- .../task-01-runtime-inventory.md | 4 +- .../task-02-task-cleanup-ordering.md | 2 +- ...task-03-agentctl-process-group-shutdown.md | 2 +- .../task-04-startup-reconciliation.md | 2 +- .../task-05-verification-and-doc-sync.md | 4 +- .../plans/task-step-transition-ledger/plan.md | 4 +- .../task-01-telemetry-activation-registry.md | 2 +- .../task-02-turn-start-step-stamp.md | 2 +- .../task-03-ledger-schema-migration.md | 2 +- .../task-04-ledger-writer-chokepoints.md | 2 +- .../task-05-caller-attribution-wiring.md | 2 +- .../task-06-writer-health-controls.md | 2 +- docs/plans/task-title-caret/plan.md | 4 +- .../task-01-regression-e2e.md | 2 +- .../task-02-caret-preserving-clamp.md | 2 +- .../task-03-rename-edit-dialogs.md | 2 +- .../task-04-sibling-title-inputs.md | 6 +- docs/plans/task-title-length-limit/plan.md | 2 +- .../task-01-backend-mcp-contract.md | 2 +- .../task-02-frontend-title-limit.md | 2 +- .../task-03-e2e-docs.md | 2 +- .../turn-cancellation-responsiveness/plan.md | 2 +- ...task-01-break-cancellation-stream-cycle.md | 2 +- ...sk-02-prove-responsive-cancellation-e2e.md | 2 +- docs/plans/typed-quick-chat-sessions/plan.md | 2 +- .../task-01-backend-boot-contract.md | 2 +- .../task-02-unified-frontend-sessions.md | 2 +- .../task-03-clarification-responsive-ux.md | 2 +- .../task-04-e2e-and-verification.md | 2 +- docs/plans/unlink-task-github-pr/plan.md | 2 +- .../task-01-backend-unlink-contract.md | 2 +- .../task-02-frontend-multi-pr-unlink.md | 2 +- .../task-03-pr-unlink-e2e.md | 2 +- .../task-04-public-documentation.md | 2 +- .../plans/user-question-turn-boundary/plan.md | 2 +- ...sk-01-strengthen-question-wait-guidance.md | 6 +- docs/plans/wip-limited-task-creation/plan.md | 2 +- .../task-01-atomic-repository-admission.md | 2 +- .../task-02-task-service-enforcement.md | 2 +- ...-conflict-adapters-and-watcher-deferral.md | 2 +- .../task-04-public-documentation.md | 2 +- docs/plans/wip-overflow-queues/plan.md | 2 +- .../task-01-persist-wip-admission-state.md | 2 +- ...-02-implement-atomic-overflow-placement.md | 2 +- .../task-03-reconcile-queued-promotion.md | 2 +- .../task-04-defer-queued-task-launch.md | 2 +- .../task-05-adapt-integration-watchers.md | 2 +- .../task-06-expose-visible-queue-ux.md | 2 +- ...-verify-browser-flows-and-documentation.md | 2 +- docs/plans/workflow-cycle-guardrails/plan.md | 2 +- .../task-01-cycle-analyzer.md | 2 +- .../task-02-mutation-guard.md | 2 +- .../task-03-diagnostic-ui.md | 2 +- .../task-04-e2e-verification.md | 2 +- docs/plans/workflow-duplication/plan.md | 2 +- .../task-01-build-duplication-drafts.md | 2 +- .../task-02-expose-duplicate-action.md | 2 +- .../task-03-prove-duplication-flows.md | 2 +- docs/plans/workflow-session-settings/plan.md | 2 +- ...ask-01-workflow-contract-and-validation.md | 2 +- ...task-02-original-session-initialization.md | 2 +- .../task-03-runtime-rule-application.md | 2 +- .../task-04-editor-and-carry-analysis.md | 2 +- .../task-05-e2e-docs-and-verification.md | 2 +- ...task-06-workflow-session-options-toggle.md | 2 +- docs/plans/workflow-settings-autosave/plan.md | 2 +- .../task-01-autosave-state.md | 2 +- .../task-02-responsive-layout.md | 2 +- .../task-03-e2e-coverage.md | 2 +- .../plan.md | 2 +- .../task-01-reset-skips-created.md | 2 +- .../task-02-queue-prompt-on-failed-launch.md | 2 +- .../workflow-sync-workspace-authz/plan.md | 2 +- .../task-01-service-authorization.md | 2 +- .../task-02-handler-error-mapping.md | 2 +- docs/specs/INDEX.md | 62 +- .../clarification-active-lifecycle/spec.md | 462 ------- .../specs/external-question-answering/spec.md | 4 +- docs/specs/gitlab-mr-task-list-badges/spec.md | 2 +- docs/specs/office/scheduler.md | 2 +- .../session-delete-resource-cleanup/spec.md | 2 +- docs/specs/spec-lint.json | 10 +- docs/specs/tasks/README.md | 131 ++ .../tasks/external-id-idempotency/spec.md | 1103 --------------- .../agent-generated-titles.md | 31 +- .../archive-confirmation.md | 27 +- .../requirements/attach-workspace-sources.md | 30 + .../{ => requirements}/autopilot-mode.md | 29 +- .../blocked-task-escalation.md | 23 +- ...larification-active-lifecycle-scenarios.md | 183 +++ .../clarification-active-lifecycle.md | 171 +++ .../tasks/{ => requirements}/documents.md | 25 +- .../{ => requirements}/execution-stages.md | 25 +- .../external-id-idempotency-boundaries.md | 82 ++ .../external-id-idempotency-scenarios.md | 310 +++++ .../requirements/external-id-idempotency.md | 183 +++ .../interrupted-task-indicator.md | 33 +- docs/specs/tasks/{ => requirements}/labels.md | 25 +- .../link-existing-task-github-issue.md | 25 +- .../mcp-task-agent-profile-default.md} | 25 +- .../tasks/requirements/model-unification.md | 29 + .../spec.md => requirements/multi-branch.md} | 26 +- .../parent-child-message-interrupt.md | 27 +- .../parent-child-task-stop.md | 27 +- .../prevent-agent-autostart-on-open.md} | 25 +- .../{ => requirements}/prompt-attachments.md | 25 +- .../quick-chat-expiration.md | 29 +- .../quick-chat-repository-context.md | 27 +- .../requirements/remote-contribution-tasks.md | 30 + .../rich-task-title-previews.md | 25 +- .../{ => requirements}/run-scheduling.md | 29 +- .../tasks/requirements/runtime-cleanup.md | 30 + .../runtime-state-publication-order.md | 28 +- .../{ => requirements}/sidebar-task-edit.md | 25 +- .../{ => requirements}/subtask-checklist.md | 25 +- .../subtask-completion-trigger.md | 23 +- .../{ => requirements}/subtask-detachment.md | 25 +- .../subtask-reparenting-drag-drop.md | 25 +- .../{ => requirements}/subtree-controls.md | 27 +- .../task-create-escape-dismissal.md | 25 +- .../task-create-executor-default.md | 30 +- .../task-create-workflow-memory.md | 32 +- ...encies-create-dialog-advanced-settings.md} | 25 +- ...cies-create-dialog-dependency-selector.md} | 25 +- .../tasks/requirements/task-dependencies.md | 22 + .../task-launch-failure-recovery.md} | 23 +- .../{ => requirements}/title-length-limit.md | 25 +- .../user-question-turn-boundary.md | 25 +- .../requirements/wip-limit-pull-system.md | 30 + .../without-repositories.md | 23 +- .../workflow-cancelled-turn-completion.md} | 25 +- .../workflow-cycle-guardrails.md} | 25 +- .../requirements/workflow-duplication.md} | 25 +- .../workflow-explicit-completion-signal.md} | 27 +- ...quorum-decision-recording-agent-surface.md | 104 ++ ...w-quorum-decision-recording-concurrency.md | 131 ++ ...w-quorum-decision-recording-diagnostics.md | 195 +++ ...um-decision-recording-participant-slate.md | 137 ++ ...-quorum-decision-recording-reevaluation.md | 238 ++++ ...ow-quorum-decision-recording-regression.md | 69 + ...-quorum-decision-recording-step-binding.md | 71 + ...m-decision-recording-verdict-vocabulary.md | 103 ++ .../workflow-quorum-decision-recording.md | 95 ++ .../workflow-session-settings.md} | 29 +- .../workflow-settings-autosave.md} | 28 +- .../workflow-step-agent-start-ownership.md} | 23 +- .../workflow-sync-workspace-authz.md} | 25 +- ...w-task-step-transition-ledger-scenarios.md | 209 +++ .../workflow-task-step-transition-ledger.md | 131 ++ .../attach-workspace-sources.md | 37 +- .../clarification-active-lifecycle.md | 217 +++ .../external-id-idempotency-operations.md | 170 +++ .../system-design/external-id-idempotency.md | 540 ++++++++ .../{ => system-design}/model-unification.md | 25 +- .../remote-contribution-tasks.md | 33 +- .../{ => system-design}/runtime-cleanup.md | 23 +- .../system-design/task-dependencies.md} | 29 +- .../wip-limit-pull-system.md | 31 +- .../workflow-quorum-decision-recording.md | 398 ++++++ .../workflow-task-step-transition-ledger.md} | 256 +--- .../ui/acp-model-configuration-summary.md | 2 +- docs/specs/ui/settings-manual-save.md | 2 +- .../spec.md | 1179 ----------------- 460 files changed, 5554 insertions(+), 3724 deletions(-) delete mode 100644 docs/specs/clarification-active-lifecycle/spec.md create mode 100644 docs/specs/tasks/README.md delete mode 100644 docs/specs/tasks/external-id-idempotency/spec.md rename docs/specs/tasks/{ => requirements}/agent-generated-titles.md (94%) rename docs/specs/tasks/{ => requirements}/archive-confirmation.md (84%) create mode 100644 docs/specs/tasks/requirements/attach-workspace-sources.md rename docs/specs/tasks/{ => requirements}/autopilot-mode.md (93%) rename docs/specs/tasks/{ => requirements}/blocked-task-escalation.md (85%) create mode 100644 docs/specs/tasks/requirements/clarification-active-lifecycle-scenarios.md create mode 100644 docs/specs/tasks/requirements/clarification-active-lifecycle.md rename docs/specs/tasks/{ => requirements}/documents.md (90%) rename docs/specs/tasks/{ => requirements}/execution-stages.md (91%) create mode 100644 docs/specs/tasks/requirements/external-id-idempotency-boundaries.md create mode 100644 docs/specs/tasks/requirements/external-id-idempotency-scenarios.md create mode 100644 docs/specs/tasks/requirements/external-id-idempotency.md rename docs/specs/tasks/{ => requirements}/interrupted-task-indicator.md (88%) rename docs/specs/tasks/{ => requirements}/labels.md (81%) rename docs/specs/tasks/{ => requirements}/link-existing-task-github-issue.md (93%) rename docs/specs/tasks/{mcp-task-agent-profile-default/spec.md => requirements/mcp-task-agent-profile-default.md} (92%) create mode 100644 docs/specs/tasks/requirements/model-unification.md rename docs/specs/tasks/{multi-branch/spec.md => requirements/multi-branch.md} (95%) rename docs/specs/tasks/{ => requirements}/parent-child-message-interrupt.md (94%) rename docs/specs/tasks/{ => requirements}/parent-child-task-stop.md (93%) rename docs/specs/{prevent-agent-autostart-on-open/spec.md => tasks/requirements/prevent-agent-autostart-on-open.md} (93%) rename docs/specs/tasks/{ => requirements}/prompt-attachments.md (93%) rename docs/specs/tasks/{ => requirements}/quick-chat-expiration.md (94%) rename docs/specs/tasks/{ => requirements}/quick-chat-repository-context.md (87%) create mode 100644 docs/specs/tasks/requirements/remote-contribution-tasks.md rename docs/specs/tasks/{ => requirements}/rich-task-title-previews.md (79%) rename docs/specs/tasks/{ => requirements}/run-scheduling.md (89%) create mode 100644 docs/specs/tasks/requirements/runtime-cleanup.md rename docs/specs/tasks/{ => requirements}/runtime-state-publication-order.md (86%) rename docs/specs/tasks/{ => requirements}/sidebar-task-edit.md (86%) rename docs/specs/tasks/{ => requirements}/subtask-checklist.md (83%) rename docs/specs/tasks/{ => requirements}/subtask-completion-trigger.md (93%) rename docs/specs/tasks/{ => requirements}/subtask-detachment.md (88%) rename docs/specs/tasks/{ => requirements}/subtask-reparenting-drag-drop.md (91%) rename docs/specs/tasks/{ => requirements}/subtree-controls.md (93%) rename docs/specs/tasks/{ => requirements}/task-create-escape-dismissal.md (72%) rename docs/specs/tasks/{ => requirements}/task-create-executor-default.md (83%) rename docs/specs/tasks/{ => requirements}/task-create-workflow-memory.md (85%) rename docs/specs/{task-dependencies/create-dialog-advanced-settings.md => tasks/requirements/task-dependencies-create-dialog-advanced-settings.md} (90%) rename docs/specs/{task-dependencies/create-dialog-dependency-selector.md => tasks/requirements/task-dependencies-create-dialog-dependency-selector.md} (87%) create mode 100644 docs/specs/tasks/requirements/task-dependencies.md rename docs/specs/{task-launch-failure-recovery/spec.md => tasks/requirements/task-launch-failure-recovery.md} (94%) rename docs/specs/tasks/{ => requirements}/title-length-limit.md (89%) rename docs/specs/tasks/{ => requirements}/user-question-turn-boundary.md (81%) create mode 100644 docs/specs/tasks/requirements/wip-limit-pull-system.md rename docs/specs/tasks/{ => requirements}/without-repositories.md (86%) rename docs/specs/{workflow/cancelled-turn-completion/spec.md => tasks/requirements/workflow-cancelled-turn-completion.md} (95%) rename docs/specs/{workflow-cycle-guardrails/spec.md => tasks/requirements/workflow-cycle-guardrails.md} (93%) rename docs/specs/{workflow-duplication/spec.md => tasks/requirements/workflow-duplication.md} (86%) rename docs/specs/{workflow/explicit-completion-signal/spec.md => tasks/requirements/workflow-explicit-completion-signal.md} (84%) create mode 100644 docs/specs/tasks/requirements/workflow-quorum-decision-recording-agent-surface.md create mode 100644 docs/specs/tasks/requirements/workflow-quorum-decision-recording-concurrency.md create mode 100644 docs/specs/tasks/requirements/workflow-quorum-decision-recording-diagnostics.md create mode 100644 docs/specs/tasks/requirements/workflow-quorum-decision-recording-participant-slate.md create mode 100644 docs/specs/tasks/requirements/workflow-quorum-decision-recording-reevaluation.md create mode 100644 docs/specs/tasks/requirements/workflow-quorum-decision-recording-regression.md create mode 100644 docs/specs/tasks/requirements/workflow-quorum-decision-recording-step-binding.md create mode 100644 docs/specs/tasks/requirements/workflow-quorum-decision-recording-verdict-vocabulary.md create mode 100644 docs/specs/tasks/requirements/workflow-quorum-decision-recording.md rename docs/specs/{workflow-session-settings/spec.md => tasks/requirements/workflow-session-settings.md} (94%) rename docs/specs/{workflow-settings-autosave/spec.md => tasks/requirements/workflow-settings-autosave.md} (76%) rename docs/specs/{workflow-step-agent-start-ownership/spec.md => tasks/requirements/workflow-step-agent-start-ownership.md} (89%) rename docs/specs/{workflow-sync-workspace-authz/spec.md => tasks/requirements/workflow-sync-workspace-authz.md} (89%) create mode 100644 docs/specs/tasks/requirements/workflow-task-step-transition-ledger-scenarios.md create mode 100644 docs/specs/tasks/requirements/workflow-task-step-transition-ledger.md rename docs/specs/tasks/{ => system-design}/attach-workspace-sources.md (94%) create mode 100644 docs/specs/tasks/system-design/clarification-active-lifecycle.md create mode 100644 docs/specs/tasks/system-design/external-id-idempotency-operations.md create mode 100644 docs/specs/tasks/system-design/external-id-idempotency.md rename docs/specs/tasks/{ => system-design}/model-unification.md (96%) rename docs/specs/tasks/{ => system-design}/remote-contribution-tasks.md (95%) rename docs/specs/tasks/{ => system-design}/runtime-cleanup.md (97%) rename docs/specs/{task-dependencies/spec.md => tasks/system-design/task-dependencies.md} (97%) rename docs/specs/tasks/{ => system-design}/wip-limit-pull-system.md (95%) create mode 100644 docs/specs/tasks/system-design/workflow-quorum-decision-recording.md rename docs/specs/{workflow/task-step-transition-ledger/spec.md => tasks/system-design/workflow-task-step-transition-ledger.md} (61%) delete mode 100644 docs/specs/workflow-quorum-decision-recording/spec.md diff --git a/.github/scripts/pr-walkthrough-workflow-contract_test.py b/.github/scripts/pr-walkthrough-workflow-contract_test.py index 21e198af36..758c869f9f 100644 --- a/.github/scripts/pr-walkthrough-workflow-contract_test.py +++ b/.github/scripts/pr-walkthrough-workflow-contract_test.py @@ -280,7 +280,11 @@ class PRWalkthroughWorkflowContractTest(unittest.TestCase): self.assertIn("scripts/pr-walkthrough-pr-body", self.link) self.assertIn("--github-response", self.link) self.assertIn("--input", self.link) - self.assertIn("needs.pr-walkthrough-publish.outputs.url", self.link) + self.assertIn( + 'PUBLIC_URL="https://walkthrough.kandev.ai/pr/${PR_NUMBER}/${HEAD_SHA}.html"', + self.link, + ) + self.assertNotIn("needs.pr-walkthrough-publish.outputs.url", self.link) self.assertNotIn("CLOUDFLARE_R2", self.link) self.assertNotIn("OPENCODE_API_KEY", self.link) self.assertTrue(PR_BODY_HELPER.is_file()) diff --git a/.github/workflows/pr-walkthrough.yml b/.github/workflows/pr-walkthrough.yml index 76490d2ed6..525dc12285 100644 --- a/.github/workflows/pr-walkthrough.yml +++ b/.github/workflows/pr-walkthrough.yml @@ -337,11 +337,10 @@ jobs: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} PR_NUMBER: ${{ github.event.pull_request.number }} - PUBLIC_URL: ${{ needs.pr-walkthrough-publish.outputs.url }} run: | set -euo pipefail test "$(git rev-parse HEAD)" = "$BASE_SHA" - test -n "$PUBLIC_URL" + PUBLIC_URL="https://walkthrough.kandev.ai/pr/${PR_NUMBER}/${HEAD_SHA}.html" gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" > pr-response.json diff --git a/apps/backend/internal/mcp/handlers/handlers.go b/apps/backend/internal/mcp/handlers/handlers.go index 2b94be7aac..6c797df0e0 100644 --- a/apps/backend/internal/mcp/handlers/handlers.go +++ b/apps/backend/internal/mcp/handlers/handlers.go @@ -913,7 +913,7 @@ func (h *Handlers) handleCreateTask(ctx context.Context, msg *ws.Message) (*ws.M // mcpCreateTaskResult is create_task_kandev's tool-result shape: the task // DTO plus deduplicated/creation_complete, per -// docs/specs/tasks/external-id-idempotency/spec.md, "MCP" — required +// docs/specs/tasks/system-design/external-id-idempotency.md, "MCP" — required // booleans, not presence-only markers, mirroring the REST create response. type mcpCreateTaskResult struct { dto.TaskDTO diff --git a/apps/backend/internal/mcp/handlers/message_target_session.go b/apps/backend/internal/mcp/handlers/message_target_session.go index 8b8af2e7ae..5aeedcc3a9 100644 --- a/apps/backend/internal/mcp/handlers/message_target_session.go +++ b/apps/backend/internal/mcp/handlers/message_target_session.go @@ -93,7 +93,7 @@ func (h *Handlers) resolveMessageTargetSession( // A terminal primary with nothing live is handed to dispatch rather than // rejected here, so the caller still gets the documented // INTERNAL_ERROR for "target session FAILED/CANCELLED" - // (docs/specs/tasks/parent-child-message-interrupt.md, "Errors"). + // (docs/specs/tasks/requirements/parent-child-message-interrupt.md, "Errors"). // Only the message improves; the code is a contract this change has no // business rewriting as a side effect. return primary, true, nil diff --git a/apps/backend/internal/mcp/handlers/message_target_session_test.go b/apps/backend/internal/mcp/handlers/message_target_session_test.go index fa6a89f8ef..b704ef37b3 100644 --- a/apps/backend/internal/mcp/handlers/message_target_session_test.go +++ b/apps/backend/internal/mcp/handlers/message_target_session_test.go @@ -117,7 +117,7 @@ func TestHandleMessageTask_LivePrimaryKeepsPriorityOverNewerSessions(t *testing. // // The CODE stays INTERNAL_ERROR. "Target session FAILED/CANCELLED -> // INTERNAL_ERROR" is written down in -// docs/specs/tasks/parent-child-message-interrupt.md, so improving the message +// docs/specs/tasks/requirements/parent-child-message-interrupt.md, so improving the message // must not quietly reclassify it: a terminal primary with nothing live is // handed to dispatch exactly as before. func TestHandleMessageTask_AllSessionsTerminalNamesTheRecoveryTool(t *testing.T) { diff --git a/apps/backend/internal/mcp/server/agent_decision_tool.go b/apps/backend/internal/mcp/server/agent_decision_tool.go index 9b4398b1db..c225b29725 100644 --- a/apps/backend/internal/mcp/server/agent_decision_tool.go +++ b/apps/backend/internal/mcp/server/agent_decision_tool.go @@ -13,7 +13,7 @@ import ( // registerRecordStepDecisionTool registers record_step_decision_kandev, the // Office quorum decision-recording tool. Per the tool contract in -// docs/specs/workflow-quorum-decision-recording/spec.md, it takes no +// docs/specs/tasks/requirements/workflow-quorum-decision-recording-agent-surface.md, it takes no // task_id — task/step/role are resolved server-side from the calling // session, matching every other Office tool. func (s *Server) registerRecordStepDecisionTool() { diff --git a/apps/backend/internal/office/repository/sqlite/base_migrations.go b/apps/backend/internal/office/repository/sqlite/base_migrations.go index 1c9d2932c0..512c200e4b 100644 --- a/apps/backend/internal/office/repository/sqlite/base_migrations.go +++ b/apps/backend/internal/office/repository/sqlite/base_migrations.go @@ -368,7 +368,7 @@ func (r *Repository) runTaskPriorityRecreate() error { _, _ = conn.ExecContext(ctx, `ALTER TABLE tasks ADD COLUMN queued_at TIMESTAMP`) _, _ = conn.ExecContext(ctx, `ALTER TABLE tasks ADD COLUMN autopilot_enabled INTEGER NOT NULL DEFAULT 0`) // Same defensive add for external_id/external_id_settled_at - // (docs/specs/tasks/external-id-idempotency): real installs already have + // (docs/specs/tasks/system-design/external-id-idempotency.md): real installs already have // these from task/repository/sqlite/base.go runMigrations(), but older // priority-migration fixtures predate them too. _, _ = conn.ExecContext(ctx, `ALTER TABLE tasks ADD COLUMN external_id TEXT COLLATE BINARY`) @@ -476,7 +476,7 @@ func taskPriorityMigrationStatements() []string { // per-task assignee moved to workflow_step_participants. // // uniq_tasks_external_id enforces task create-idempotency - // (docs/specs/tasks/external-id-idempotency). DROP TABLE tasks above + // (docs/specs/tasks/system-design/external-id-idempotency.md). DROP TABLE tasks above // silently drops every index on the old table, this one included — // unlike a plain ALTER TABLE ADD COLUMN, recreating the table means // every index must be explicitly relisted here or it is gone from diff --git a/apps/backend/internal/task/dto/dto.go b/apps/backend/internal/task/dto/dto.go index 1ae7c02b6f..9603badf9e 100644 --- a/apps/backend/internal/task/dto/dto.go +++ b/apps/backend/internal/task/dto/dto.go @@ -236,7 +236,7 @@ type TaskDTO struct { Labels string `json:"labels,omitempty"` Identifier string `json:"identifier,omitempty"` // ExternalID is a caller-supplied identity used for task create- - // idempotency (docs/specs/tasks/external-id-idempotency/spec.md). Omitted + // idempotency (docs/specs/tasks/requirements/external-id-idempotency.md). Omitted // when the task holds none. ExternalID string `json:"external_id,omitempty"` // IsFromOffice is the authoritative "this task is owned by office" diff --git a/apps/backend/internal/task/handlers/task_handlers.go b/apps/backend/internal/task/handlers/task_handlers.go index e96378a707..6d6545d733 100644 --- a/apps/backend/internal/task/handlers/task_handlers.go +++ b/apps/backend/internal/task/handlers/task_handlers.go @@ -138,7 +138,7 @@ func (h *TaskHandlers) registerHTTP(router *gin.Engine) { api := router.Group("/api/v1") api.GET("/workflows/:id/tasks", h.httpListTasks) api.GET("/workspaces/:id/tasks", h.httpListTasksByWorkspace) - // Task create-idempotency (docs/specs/tasks/external-id-idempotency): + // Task create-idempotency (docs/specs/tasks/requirements/external-id-idempotency.md): // side-effect-free lookup, and an operator-only release. Both take // external_id as a query parameter. api.GET("/workspaces/:id/tasks/by-external-id", h.httpGetTaskByExternalID) diff --git a/apps/backend/internal/task/handlers/task_http_handlers.go b/apps/backend/internal/task/handlers/task_http_handlers.go index 41710c40b5..5d7f951ccd 100644 --- a/apps/backend/internal/task/handlers/task_http_handlers.go +++ b/apps/backend/internal/task/handlers/task_http_handlers.go @@ -747,7 +747,7 @@ type httpCreateTaskRequest struct { StartWhenUnblocked *bool `json:"start_when_unblocked,omitempty"` ProjectID string `json:"project_id,omitempty"` // ExternalID is a caller-supplied identity used for create-idempotency - // (docs/specs/tasks/external-id-idempotency/spec.md). + // (docs/specs/tasks/requirements/external-id-idempotency.md). ExternalID string `json:"external_id,omitempty"` Labels []string `json:"labels,omitempty"` // Office task-handoffs phase 5 — workspace policy. Optional; same @@ -764,7 +764,7 @@ type createTaskResponse struct { AgentExecutionID string `json:"agent_execution_id,omitempty"` // Deduplicated and CreationComplete are required booleans (not // presence-only markers) on every create-idempotency outcome, per - // docs/specs/tasks/external-id-idempotency/spec.md. Deduplicated is true + // docs/specs/tasks/requirements/external-id-idempotency.md. Deduplicated is true // for both Found outcomes. CreationComplete is false only for // Found-unsettled — every other outcome (including CreatedIdentityLost) // carries true, because the field means only "this task's required @@ -1082,7 +1082,7 @@ type lookupTaskResponse struct { } // httpGetTaskByExternalID is the REST lookup route -// (docs/specs/tasks/external-id-idempotency/spec.md, "REST — lookup"): a +// (docs/specs/tasks/system-design/external-id-idempotency.md, "REST — lookup"): a // side-effect-free way to ask what holds an identity without risking a // create. Returns the task whether settled or not, including archived tasks. func (h *TaskHandlers) httpGetTaskByExternalID(c *gin.Context) { @@ -1098,7 +1098,7 @@ func (h *TaskHandlers) httpGetTaskByExternalID(c *gin.Context) { } // httpReleaseTaskExternalID is the REST release route -// (docs/specs/tasks/external-id-idempotency/spec.md, "REST — release"): an +// (docs/specs/tasks/system-design/external-id-idempotency.md, "REST — release"): an // operator action for an identity a human has determined is abandoned. Frees // the identity without deleting or otherwise modifying the task. MUST NOT be // called automatically in response to creation_complete:false — see "The one @@ -1375,7 +1375,7 @@ type startAgentDispatch struct { // step (part of step 6, "required synchronous post-create work") — it MUST // run, and be allowed to fail, before settlement (step 7). Only the // resulting agent launch is asynchronous dispatch (step 8), which must run -// after settlement (docs/specs/tasks/external-id-idempotency/spec.md, +// after settlement (docs/specs/tasks/system-design/external-id-idempotency.md, // "Settlement call site (normative, per surface)": "the helper must expose // preparation and dispatch separately so settlement can sit between them"). // A prior shape settled before calling this at all, which satisfied "no diff --git a/apps/backend/internal/task/handlers/task_http_handlers_external_id_test.go b/apps/backend/internal/task/handlers/task_http_handlers_external_id_test.go index f7802f59eb..86483ec2b4 100644 --- a/apps/backend/internal/task/handlers/task_http_handlers_external_id_test.go +++ b/apps/backend/internal/task/handlers/task_http_handlers_external_id_test.go @@ -382,7 +382,7 @@ func TestHTTPCreateTask_InvalidExternalIDReturns400(t *testing.T) { // // Synchronous session prepare (create-sequence step 6) DOES run here, and // must: it happens before settlement (step 7) discovers the CreatedIdentityLost -// outcome, per the settlement-ordering fix (docs/specs/tasks/external-id-idempotency/spec.md, +// outcome, per the settlement-ordering fix (docs/specs/tasks/system-design/external-id-idempotency.md, // "Settlement call site (normative, per surface)"). Only the asynchronous // start dispatch (step 8) is guaranteed to never fire — that is the actual // "no side effects" guarantee the spec makes for this outcome ("no @@ -522,7 +522,7 @@ func TestHTTPReleaseTaskExternalID_NotFound(t *testing.T) { // orderRecordingOrchestrator records whether the task was already settled // (external_id_settled_at set) at the moment synchronous session prepare // (orchestrator.IntentPrepare) was invoked — a direct, observable proof of -// create-sequence ordering (docs/specs/tasks/external-id-idempotency/spec.md, +// create-sequence ordering (docs/specs/tasks/system-design/external-id-idempotency.md, // "Settlement call site (normative, per surface)"): settlement (step 7) must // never precede synchronous session prepare (part of step 6). Only the // asynchronous dispatch (step 8, the actual agent-start goroutine) may run diff --git a/apps/backend/internal/task/models/models.go b/apps/backend/internal/task/models/models.go index 25be5506f3..0f140f7bfa 100644 --- a/apps/backend/internal/task/models/models.go +++ b/apps/backend/internal/task/models/models.go @@ -834,7 +834,7 @@ type Task struct { Identifier string `json:"identifier,omitempty"` // e.g. "KAN-42" // ExternalID is a caller-supplied identity used for create-idempotency - // (docs/specs/tasks/external-id-idempotency/spec.md). Empty when the task + // (docs/specs/tasks/requirements/external-id-idempotency.md). Empty when the task // holds none. Unique per (workspace_id, external_id) when non-empty. ExternalID string `json:"external_id,omitempty"` // ExternalIDSettledAt is non-nil once the create that claimed ExternalID diff --git a/apps/backend/internal/task/repository/repoerrors/errors.go b/apps/backend/internal/task/repository/repoerrors/errors.go index 10a927e01a..5951f38dd7 100644 --- a/apps/backend/internal/task/repository/repoerrors/errors.go +++ b/apps/backend/internal/task/repository/repoerrors/errors.go @@ -30,7 +30,7 @@ var ErrTaskEnvironmentNotFound = errors.New("task environment not found") // ErrExternalIDConflict reports that a task insert lost the uniqueness race // on uniq_tasks_external_id — the TOCTOU backstop for the create sequence's -// step-3 lookup (docs/specs/tasks/external-id-idempotency/spec.md). Callers +// step-3 lookup (docs/specs/tasks/system-design/external-id-idempotency.md). Callers // must re-read by (workspace_id, external_id) and return the winner as a // Found outcome rather than surfacing this error. var ErrExternalIDConflict = errors.New("external_id already claimed by another task") diff --git a/apps/backend/internal/task/repository/sqlite/base_migrations.go b/apps/backend/internal/task/repository/sqlite/base_migrations.go index cfae284bc3..ec40dd97f1 100644 --- a/apps/backend/internal/task/repository/sqlite/base_migrations.go +++ b/apps/backend/internal/task/repository/sqlite/base_migrations.go @@ -276,7 +276,7 @@ func (r *Repository) runMigrations() error { // unarchive can restore exactly the descendants that cascade archived. r.migrate.Apply("tasks.archived_by_cascade_id", `ALTER TABLE tasks ADD COLUMN archived_by_cascade_id TEXT DEFAULT ''`) - // Task create-idempotency (docs/specs/tasks/external-id-idempotency). + // Task create-idempotency (docs/specs/tasks/system-design/external-id-idempotency.md). // external_id needs an explicit deterministic collation: SQLite TEXT // columns already compare BINARY by default, but an unqualified Postgres // column silently inherits the database's default collation, which may be diff --git a/apps/backend/internal/task/repository/sqlite/task_external_id.go b/apps/backend/internal/task/repository/sqlite/task_external_id.go index dbc6582407..9e715589b6 100644 --- a/apps/backend/internal/task/repository/sqlite/task_external_id.go +++ b/apps/backend/internal/task/repository/sqlite/task_external_id.go @@ -16,7 +16,7 @@ import ( // externalIDIndexName is the partial unique index enforcing at most one task // per (workspace_id, external_id). Naming it explicitly here keeps // isExternalIDUniqueViolation attributable to this constraint specifically — -// see docs/specs/tasks/external-id-idempotency/spec.md, "Unique-violation +// see docs/specs/tasks/system-design/external-id-idempotency.md, "Unique-violation // classification across dialects". const externalIDIndexName = "uniq_tasks_external_id" diff --git a/apps/backend/internal/task/repository/sqlite/task_external_id_postgres_test.go b/apps/backend/internal/task/repository/sqlite/task_external_id_postgres_test.go index 8b3e21405d..65c48e2394 100644 --- a/apps/backend/internal/task/repository/sqlite/task_external_id_postgres_test.go +++ b/apps/backend/internal/task/repository/sqlite/task_external_id_postgres_test.go @@ -46,7 +46,7 @@ func TestPostgresTaskPriorityMigrationConvertsLegacyInteger(t *testing.T) { // TestPostgresExternalIDConcurrentInsertYieldsExactlyOneWinner is the // PostgreSQL twin of the SQLite concurrency coverage -// (docs/specs/tasks/external-id-idempotency/spec.md, "Uniqueness and +// (docs/specs/tasks/system-design/external-id-idempotency.md, "Uniqueness and // concurrency"): the SQLite path passing is not evidence for Postgres, since // the two drivers classify unique-violation errors completely differently // (typed pgconn.PgError vs. SQLite driver string-matching). Skips unless diff --git a/apps/backend/internal/task/service/external_id.go b/apps/backend/internal/task/service/external_id.go index d024d036b4..e33b7e8245 100644 --- a/apps/backend/internal/task/service/external_id.go +++ b/apps/backend/internal/task/service/external_id.go @@ -12,7 +12,7 @@ import ( ) // ExternalIDMaxBytes is the maximum UTF-8 byte length of a normalized -// external_id (docs/specs/tasks/external-id-idempotency/spec.md, +// external_id (docs/specs/tasks/system-design/external-id-idempotency.md, // "Validation and normalization"). const ExternalIDMaxBytes = 255 @@ -96,7 +96,7 @@ func (s *Service) GetTaskByExternalID(ctx context.Context, workspaceID, rawExter // ReleaseTaskExternalID is the REST release route's service-level // implementation: an operator action that frees an identity without -// deleting the task holding it (docs/specs/tasks/external-id-idempotency/spec.md, +// deleting the task holding it (docs/specs/tasks/requirements/external-id-idempotency.md, // "The one unsafe thing a caller can do" — this is for a human who has // determined the task is abandoned, not an automated retry loop). Returns // whether a task held the identity. Publishes task.updated on success, like @@ -123,7 +123,7 @@ func (s *Service) ReleaseTaskExternalID(ctx context.Context, workspaceID, rawExt } // SettleExternalID performs the create sequence's step 7 (settlement) for a -// task the caller just created (docs/specs/tasks/external-id-idempotency/spec.md, +// task the caller just created (docs/specs/tasks/system-design/external-id-idempotency.md, // "Settlement"). Handlers call this after their surface's required // synchronous work finishes and before any asynchronous dispatch. Safe to // call unconditionally: a request with no external_id (externalID == "") diff --git a/apps/backend/internal/task/service/service_events.go b/apps/backend/internal/task/service/service_events.go index 88ff15c454..b983692daa 100644 --- a/apps/backend/internal/task/service/service_events.go +++ b/apps/backend/internal/task/service/service_events.go @@ -404,7 +404,7 @@ func (s *Service) publishTaskEventNow(ctx context.Context, eventType string, tas if task.ParentID != "" { data["parent_id"] = task.ParentID } - // external_id (docs/specs/tasks/external-id-idempotency): omitted rather + // external_id (docs/specs/tasks/requirements/external-id-idempotency.md): omitted rather // than sent as null/"" when the task holds none, matching the REST DTO's // omitempty and parent_id's convention above. This map is hand-built, not // derived from TaskDTO, so it needs its own explicit field. diff --git a/apps/backend/internal/task/service/service_requests.go b/apps/backend/internal/task/service/service_requests.go index 8caad13c91..561af0822e 100644 --- a/apps/backend/internal/task/service/service_requests.go +++ b/apps/backend/internal/task/service/service_requests.go @@ -73,7 +73,7 @@ type CreateTaskRequest struct { WorkspacePath string `json:"workspace_path,omitempty"` // Optional host folder for repo-less tasks // ExternalID is a caller-supplied identity used for create-idempotency - // (docs/specs/tasks/external-id-idempotency/spec.md). Accepted on REST + // (docs/specs/tasks/requirements/external-id-idempotency.md). Accepted on REST // and MCP; empty means no idempotency key. ExternalID string `json:"external_id,omitempty"` diff --git a/apps/backend/internal/task/service/service_tasks.go b/apps/backend/internal/task/service/service_tasks.go index 9c21d68251..afce8a85b3 100644 --- a/apps/backend/internal/task/service/service_tasks.go +++ b/apps/backend/internal/task/service/service_tasks.go @@ -120,7 +120,7 @@ func isOfficeRequest(req *CreateTaskRequest) bool { // CreateTaskOutcome distinguishes why Service.CreateTask returned the task it // did, per the create-idempotency contract -// (docs/specs/tasks/external-id-idempotency/spec.md). Only meaningful when +// (docs/specs/tasks/requirements/external-id-idempotency.md). Only meaningful when // the request carried an external_id; a request without one always reports // CreateTaskOutcomeCreated. The fourth contract outcome, CreatedIdentityLost, // is not produced here — it is decided by the handler during settlement, @@ -162,7 +162,7 @@ func foundOutcomeFor(task *models.Task) CreateTaskOutcome { // CreateTask creates a new task and publishes a task.created event, or — // when the request carries an external_id already held by a task — returns // that task instead, per the create-idempotency contract -// (docs/specs/tasks/external-id-idempotency/spec.md). WorkflowID is required +// (docs/specs/tasks/requirements/external-id-idempotency.md). WorkflowID is required // for non-ephemeral kanban tasks. Office tasks (project_id set, or origin is // agent_created/routine) auto-resolve to the workspace's office workflow. // Ephemeral tasks (quick chat, config chat) must NOT have a workflow. diff --git a/apps/web/e2e/tests/office/workflow-quorum-transitions.spec.ts b/apps/web/e2e/tests/office/workflow-quorum-transitions.spec.ts index c0adf4c9f4..6b28965571 100644 --- a/apps/web/e2e/tests/office/workflow-quorum-transitions.spec.ts +++ b/apps/web/e2e/tests/office/workflow-quorum-transitions.spec.ts @@ -6,7 +6,7 @@ import type { ApiClient } from "../../helpers/api-client"; * E2E coverage for the workflow engine's guarded `move_to_step` transitions * (the office-default template's Review/Approval steps, each gated by a * `wait_for_quorum` action). Spec: - * docs/specs/workflow-quorum-decision-recording/spec.md + * docs/specs/tasks/requirements/workflow-quorum-decision-recording.md * * Drives Work -> Review -> Approval with a reviewer and an approver * attached, asserting advance-on-approve, return-on-reject, and the AC-25 diff --git a/docs/decisions/0013-multi-branch-tasks.md b/docs/decisions/0013-multi-branch-tasks.md index 125e57d906..cd6e0c00bd 100644 --- a/docs/decisions/0013-multi-branch-tasks.md +++ b/docs/decisions/0013-multi-branch-tasks.md @@ -63,4 +63,4 @@ Backend: Frontend: - `apps/web/components/task/chat/messages/kandev/task-renderers.tsx` -Spec: `docs/specs/tasks/multi-branch/spec.md` +Spec: `docs/specs/tasks/requirements/multi-branch.md` diff --git a/docs/decisions/0015-explicit-completion-signal-for-auto-advance.md b/docs/decisions/0015-explicit-completion-signal-for-auto-advance.md index c6d92d210a..dd3780ba06 100644 --- a/docs/decisions/0015-explicit-completion-signal-for-auto-advance.md +++ b/docs/decisions/0015-explicit-completion-signal-for-auto-advance.md @@ -139,4 +139,4 @@ Frontend: - `apps/web/lib/types/http.ts` *(extend `WorkflowStep`)* - `apps/web/lib/state/slices/` *(transitional state for "completion pending")* -Spec: `docs/specs/workflow/explicit-completion-signal/spec.md` *(to be written via `/spec` once this ADR is accepted)* +Spec: `docs/specs/tasks/requirements/workflow-explicit-completion-signal.md` diff --git a/docs/decisions/2026-08-08-autopilot-mcp-question-capabilities.md b/docs/decisions/2026-08-08-autopilot-mcp-question-capabilities.md index a3202bdf34..23334cd4f8 100644 --- a/docs/decisions/2026-08-08-autopilot-mcp-question-capabilities.md +++ b/docs/decisions/2026-08-08-autopilot-mcp-question-capabilities.md @@ -4,7 +4,7 @@ **Date:** 2026-08-08 **Area:** backend, agentctl, protocol **Related ADR:** [ADR-2026-08-08-task-autopilot-contract](2026-08-08-task-autopilot-contract.md) -**Related spec:** [Task Autopilot Mode](../specs/tasks/autopilot-mode.md) +**Related spec:** [Task Autopilot Mode](../specs/tasks/requirements/autopilot-mode.md) ## Context diff --git a/docs/decisions/2026-08-08-mcp-tool-profiles.md b/docs/decisions/2026-08-08-mcp-tool-profiles.md index d069fe155c..074524b981 100644 --- a/docs/decisions/2026-08-08-mcp-tool-profiles.md +++ b/docs/decisions/2026-08-08-mcp-tool-profiles.md @@ -4,7 +4,7 @@ **Date:** 2026-08-08 **Area:** backend, agentctl, protocol, workflow **Related ADR:** [ADR-2026-08-08-autopilot-mcp-question-capabilities](2026-08-08-autopilot-mcp-question-capabilities.md) -**Related spec:** [Task Autopilot Mode](../specs/tasks/autopilot-mode.md) +**Related spec:** [Task Autopilot Mode](../specs/tasks/requirements/autopilot-mode.md) ## Context diff --git a/docs/decisions/2026-08-08-task-autopilot-contract.md b/docs/decisions/2026-08-08-task-autopilot-contract.md index e487783256..f2dd2793b5 100644 --- a/docs/decisions/2026-08-08-task-autopilot-contract.md +++ b/docs/decisions/2026-08-08-task-autopilot-contract.md @@ -4,7 +4,7 @@ **Date:** 2026-08-08 **Area:** backend, agentctl, frontend, protocol, workflow **Related issue:** [#2425](https://github.com/kdlbs/kandev/issues/2425) -**Related spec:** [Task Autopilot Mode](../specs/tasks/autopilot-mode.md) +**Related spec:** [Task Autopilot Mode](../specs/tasks/requirements/autopilot-mode.md) ## Context diff --git a/docs/plans/agent-generated-task-titles/plan.md b/docs/plans/agent-generated-task-titles/plan.md index 7e3c2bfa3a..f81088a6f9 100644 --- a/docs/plans/agent-generated-task-titles/plan.md +++ b/docs/plans/agent-generated-task-titles/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/agent-generated-titles.md +spec: docs/specs/tasks/requirements/agent-generated-titles.md created: 2026-07-31 updated: 2026-08-02 status: done diff --git a/docs/plans/agent-generated-task-titles/task-01-backend-title-lifecycle.md b/docs/plans/agent-generated-task-titles/task-01-backend-title-lifecycle.md index e87859baf2..015a24244b 100644 --- a/docs/plans/agent-generated-task-titles/task-01-backend-title-lifecycle.md +++ b/docs/plans/agent-generated-task-titles/task-01-backend-title-lifecycle.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/agent-generated-titles.md" +spec: "../../specs/tasks/requirements/agent-generated-titles.md" --- # Task 01: Backend preference and provisional-title lifecycle diff --git a/docs/plans/agent-generated-task-titles/task-02-mcp-title-tool.md b/docs/plans/agent-generated-task-titles/task-02-mcp-title-tool.md index 2f26f4be44..c89378ca83 100644 --- a/docs/plans/agent-generated-task-titles/task-02-mcp-title-tool.md +++ b/docs/plans/agent-generated-task-titles/task-02-mcp-title-tool.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-backend-title-lifecycle"] plan: "plan.md" -spec: "../../specs/tasks/agent-generated-titles.md" +spec: "../../specs/tasks/requirements/agent-generated-titles.md" --- # Task 02: Task MCP tool and first-turn instruction diff --git a/docs/plans/agent-generated-task-titles/task-03-frontend-title-flow.md b/docs/plans/agent-generated-task-titles/task-03-frontend-title-flow.md index 72c48cb95e..892b9c2261 100644 --- a/docs/plans/agent-generated-task-titles/task-03-frontend-title-flow.md +++ b/docs/plans/agent-generated-task-titles/task-03-frontend-title-flow.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["01-backend-title-lifecycle", "02-mcp-title-tool"] plan: "plan.md" -spec: "../../specs/tasks/agent-generated-titles.md" +spec: "../../specs/tasks/requirements/agent-generated-titles.md" --- # Task 03: Settings and task/subtask dialogs diff --git a/docs/plans/agent-generated-task-titles/task-04-e2e-and-docs.md b/docs/plans/agent-generated-task-titles/task-04-e2e-and-docs.md index 7e75e5c399..7f00fa3703 100644 --- a/docs/plans/agent-generated-task-titles/task-04-e2e-and-docs.md +++ b/docs/plans/agent-generated-task-titles/task-04-e2e-and-docs.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["01-backend-title-lifecycle", "02-mcp-title-tool", "03-frontend-title-flow"] plan: "plan.md" -spec: "../../specs/tasks/agent-generated-titles.md" +spec: "../../specs/tasks/requirements/agent-generated-titles.md" --- # Task 04: End-to-end coverage and public docs diff --git a/docs/plans/agent-generated-task-titles/task-05-single-owner-title-handoff.md b/docs/plans/agent-generated-task-titles/task-05-single-owner-title-handoff.md index 06abd93427..ebb543ddac 100644 --- a/docs/plans/agent-generated-task-titles/task-05-single-owner-title-handoff.md +++ b/docs/plans/agent-generated-task-titles/task-05-single-owner-title-handoff.md @@ -5,7 +5,7 @@ status: done wave: 5 depends_on: ["02-mcp-title-tool"] plan: "plan.md" -spec: "../../specs/tasks/agent-generated-titles.md" +spec: "../../specs/tasks/requirements/agent-generated-titles.md" --- # Task 05: Single-owner title handoff diff --git a/docs/plans/agent-generated-task-titles/task-06-default-on-title-preference.md b/docs/plans/agent-generated-task-titles/task-06-default-on-title-preference.md index 0bb4682685..ea7d98ca71 100644 --- a/docs/plans/agent-generated-task-titles/task-06-default-on-title-preference.md +++ b/docs/plans/agent-generated-task-titles/task-06-default-on-title-preference.md @@ -5,7 +5,7 @@ status: done wave: 6 depends_on: ["05-single-owner-title-handoff"] plan: "plan.md" -spec: "../../specs/tasks/agent-generated-titles.md" +spec: "../../specs/tasks/requirements/agent-generated-titles.md" --- # Task 06: Default-on title preference diff --git a/docs/plans/agent-generated-task-titles/task-07-regression-and-docs.md b/docs/plans/agent-generated-task-titles/task-07-regression-and-docs.md index e2ffc73a2e..3a15f24bc8 100644 --- a/docs/plans/agent-generated-task-titles/task-07-regression-and-docs.md +++ b/docs/plans/agent-generated-task-titles/task-07-regression-and-docs.md @@ -5,7 +5,7 @@ status: done wave: 7 depends_on: ["05-single-owner-title-handoff", "06-default-on-title-preference"] plan: "plan.md" -spec: "../../specs/tasks/agent-generated-titles.md" +spec: "../../specs/tasks/requirements/agent-generated-titles.md" --- # Task 07: Regression coverage and public docs diff --git a/docs/plans/agent-title-branch-renaming/plan.md b/docs/plans/agent-title-branch-renaming/plan.md index 5ab3f6a62b..c3e1a5732d 100644 --- a/docs/plans/agent-title-branch-renaming/plan.md +++ b/docs/plans/agent-title-branch-renaming/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/agent-generated-titles.md +spec: docs/specs/tasks/requirements/agent-generated-titles.md created: 2026-08-04 status: complete --- diff --git a/docs/plans/agent-title-branch-renaming/task-01-branch-rename-runtime.md b/docs/plans/agent-title-branch-renaming/task-01-branch-rename-runtime.md index b2189b16f8..a3693b23dc 100644 --- a/docs/plans/agent-title-branch-renaming/task-01-branch-rename-runtime.md +++ b/docs/plans/agent-title-branch-renaming/task-01-branch-rename-runtime.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/agent-generated-titles.md" +spec: "../../specs/tasks/requirements/agent-generated-titles.md" --- # Task 01: Branch rename runtime diff --git a/docs/plans/agent-title-branch-renaming/task-02-title-tool-integration.md b/docs/plans/agent-title-branch-renaming/task-02-title-tool-integration.md index 954ea96bf2..980f6753c6 100644 --- a/docs/plans/agent-title-branch-renaming/task-02-title-tool-integration.md +++ b/docs/plans/agent-title-branch-renaming/task-02-title-tool-integration.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-branch-rename-runtime"] plan: "plan.md" -spec: "../../specs/tasks/agent-generated-titles.md" +spec: "../../specs/tasks/requirements/agent-generated-titles.md" --- # Task 02: Title tool integration diff --git a/docs/plans/agent-title-branch-renaming/task-03-regression-documentation.md b/docs/plans/agent-title-branch-renaming/task-03-regression-documentation.md index c2579a25de..303ff2e083 100644 --- a/docs/plans/agent-title-branch-renaming/task-03-regression-documentation.md +++ b/docs/plans/agent-title-branch-renaming/task-03-regression-documentation.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-title-tool-integration"] plan: "plan.md" -spec: "../../specs/tasks/agent-generated-titles.md" +spec: "../../specs/tasks/requirements/agent-generated-titles.md" --- # Task 03: Regression documentation diff --git a/docs/plans/archive-confirmation-preference/plan.md b/docs/plans/archive-confirmation-preference/plan.md index cd4f073a9d..3f8a340529 100644 --- a/docs/plans/archive-confirmation-preference/plan.md +++ b/docs/plans/archive-confirmation-preference/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/archive-confirmation.md +spec: docs/specs/tasks/requirements/archive-confirmation.md created: 2026-07-15 status: completed --- diff --git a/docs/plans/archive-confirmation-preference/task-01-backend-preference.md b/docs/plans/archive-confirmation-preference/task-01-backend-preference.md index 8986b6ffce..eb94e1c7de 100644 --- a/docs/plans/archive-confirmation-preference/task-01-backend-preference.md +++ b/docs/plans/archive-confirmation-preference/task-01-backend-preference.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/archive-confirmation.md" +spec: "../../specs/tasks/requirements/archive-confirmation.md" --- # Task 01: Backend Archive Confirmation Preference diff --git a/docs/plans/archive-confirmation-preference/task-02-frontend-behavior.md b/docs/plans/archive-confirmation-preference/task-02-frontend-behavior.md index ea18bbb066..5b67f12031 100644 --- a/docs/plans/archive-confirmation-preference/task-02-frontend-behavior.md +++ b/docs/plans/archive-confirmation-preference/task-02-frontend-behavior.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-backend-preference"] plan: "plan.md" -spec: "../../specs/tasks/archive-confirmation.md" +spec: "../../specs/tasks/requirements/archive-confirmation.md" --- # Task 02: Frontend Archive Confirmation Behavior diff --git a/docs/plans/archive-confirmation-preference/task-03-e2e-and-verification.md b/docs/plans/archive-confirmation-preference/task-03-e2e-and-verification.md index f5e1546da9..8d225084a8 100644 --- a/docs/plans/archive-confirmation-preference/task-03-e2e-and-verification.md +++ b/docs/plans/archive-confirmation-preference/task-03-e2e-and-verification.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-frontend-behavior"] plan: "plan.md" -spec: "../../specs/tasks/archive-confirmation.md" +spec: "../../specs/tasks/requirements/archive-confirmation.md" --- # Task 03: Archive Preference E2E and Verification @@ -30,7 +30,7 @@ cd apps/web && pnpm run typecheck && pnpm run lint - `apps/web/e2e/tests/task/archive-confirmation-preference.spec.ts` - `apps/web/e2e/tests/task/mobile-archive-confirmation-preference.spec.ts` -- `docs/specs/tasks/archive-confirmation.md` +- `docs/specs/tasks/requirements/archive-confirmation.md` - `docs/plans/archive-confirmation-preference/*.md` ## Inputs diff --git a/docs/plans/attach-workspace-sources/plan.md b/docs/plans/attach-workspace-sources/plan.md index 1944c83297..19b96c71e9 100644 --- a/docs/plans/attach-workspace-sources/plan.md +++ b/docs/plans/attach-workspace-sources/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/attach-workspace-sources.md +spec: docs/specs/tasks/system-design/attach-workspace-sources.md created: 2026-07-22 status: completed --- diff --git a/docs/plans/attach-workspace-sources/task-01-durable-source-contracts.md b/docs/plans/attach-workspace-sources/task-01-durable-source-contracts.md index 45741cb304..1cf08761d9 100644 --- a/docs/plans/attach-workspace-sources/task-01-durable-source-contracts.md +++ b/docs/plans/attach-workspace-sources/task-01-durable-source-contracts.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 01: Durable Source Contracts diff --git a/docs/plans/attach-workspace-sources/task-02-attachment-service.md b/docs/plans/attach-workspace-sources/task-02-attachment-service.md index ffef2e8307..783c4d6d62 100644 --- a/docs/plans/attach-workspace-sources/task-02-attachment-service.md +++ b/docs/plans/attach-workspace-sources/task-02-attachment-service.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-durable-source-contracts"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 02: Attachment Service diff --git a/docs/plans/attach-workspace-sources/task-03-protocol-surfaces.md b/docs/plans/attach-workspace-sources/task-03-protocol-surfaces.md index 2d3da971b6..6ad0a0c912 100644 --- a/docs/plans/attach-workspace-sources/task-03-protocol-surfaces.md +++ b/docs/plans/attach-workspace-sources/task-03-protocol-surfaces.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-attachment-service"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 03: Protocol Surfaces diff --git a/docs/plans/attach-workspace-sources/task-04-host-materialization.md b/docs/plans/attach-workspace-sources/task-04-host-materialization.md index bd718e2612..3041e38de3 100644 --- a/docs/plans/attach-workspace-sources/task-04-host-materialization.md +++ b/docs/plans/attach-workspace-sources/task-04-host-materialization.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-attachment-service"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 04: Host Materialization diff --git a/docs/plans/attach-workspace-sources/task-05-remote-materialization.md b/docs/plans/attach-workspace-sources/task-05-remote-materialization.md index a7ecedd969..5e34fe9aad 100644 --- a/docs/plans/attach-workspace-sources/task-05-remote-materialization.md +++ b/docs/plans/attach-workspace-sources/task-05-remote-materialization.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["03-protocol-surfaces", "04-host-materialization"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 05: Remote Materialization diff --git a/docs/plans/attach-workspace-sources/task-06-shared-source-picker.md b/docs/plans/attach-workspace-sources/task-06-shared-source-picker.md index 85e4a0fd69..a630d7eb63 100644 --- a/docs/plans/attach-workspace-sources/task-06-shared-source-picker.md +++ b/docs/plans/attach-workspace-sources/task-06-shared-source-picker.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-attachment-service"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 06: Shared Source Picker diff --git a/docs/plans/attach-workspace-sources/task-07-files-panel-surface.md b/docs/plans/attach-workspace-sources/task-07-files-panel-surface.md index cf27013849..b6b8d32ad2 100644 --- a/docs/plans/attach-workspace-sources/task-07-files-panel-surface.md +++ b/docs/plans/attach-workspace-sources/task-07-files-panel-surface.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["03-protocol-surfaces", "06-shared-source-picker"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 07: Files Panel Surface diff --git a/docs/plans/attach-workspace-sources/task-08-end-to-end-coverage.md b/docs/plans/attach-workspace-sources/task-08-end-to-end-coverage.md index e3ab2caebf..0011119fd4 100644 --- a/docs/plans/attach-workspace-sources/task-08-end-to-end-coverage.md +++ b/docs/plans/attach-workspace-sources/task-08-end-to-end-coverage.md @@ -5,7 +5,7 @@ status: completed wave: 5 depends_on: ["05-remote-materialization", "07-files-panel-surface"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 08: End-to-End Coverage diff --git a/docs/plans/attach-workspace-sources/task-09-public-documentation.md b/docs/plans/attach-workspace-sources/task-09-public-documentation.md index 9778a1107f..6454c59632 100644 --- a/docs/plans/attach-workspace-sources/task-09-public-documentation.md +++ b/docs/plans/attach-workspace-sources/task-09-public-documentation.md @@ -5,7 +5,7 @@ status: done wave: 5 depends_on: ["05-remote-materialization", "07-files-panel-surface"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 09: Public Documentation diff --git a/docs/plans/attach-workspace-sources/task-10-final-verification.md b/docs/plans/attach-workspace-sources/task-10-final-verification.md index bef9e1237a..44a6a0c02e 100644 --- a/docs/plans/attach-workspace-sources/task-10-final-verification.md +++ b/docs/plans/attach-workspace-sources/task-10-final-verification.md @@ -5,7 +5,7 @@ status: completed wave: 6 depends_on: ["08-end-to-end-coverage", "09-public-documentation"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 10: Final Verification diff --git a/docs/plans/attach-workspace-sources/task-11-shared-local-remote-selector.md b/docs/plans/attach-workspace-sources/task-11-shared-local-remote-selector.md index 434bd8a51e..bd0ff23dbc 100644 --- a/docs/plans/attach-workspace-sources/task-11-shared-local-remote-selector.md +++ b/docs/plans/attach-workspace-sources/task-11-shared-local-remote-selector.md @@ -6,7 +6,7 @@ wave: 7 depends_on: - "06-shared-source-picker" plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 11: Tab-free repository source chooser diff --git a/docs/plans/attach-workspace-sources/task-12-unified-files-workspace-actions.md b/docs/plans/attach-workspace-sources/task-12-unified-files-workspace-actions.md index cfbdfbbf4f..ae5189198b 100644 --- a/docs/plans/attach-workspace-sources/task-12-unified-files-workspace-actions.md +++ b/docs/plans/attach-workspace-sources/task-12-unified-files-workspace-actions.md @@ -6,7 +6,7 @@ wave: 7 depends_on: - "07-files-panel-surface" plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 12: Unified Files workspace actions diff --git a/docs/plans/attach-workspace-sources/task-13-ux-delta-e2e.md b/docs/plans/attach-workspace-sources/task-13-ux-delta-e2e.md index 318582d471..a1c72d4406 100644 --- a/docs/plans/attach-workspace-sources/task-13-ux-delta-e2e.md +++ b/docs/plans/attach-workspace-sources/task-13-ux-delta-e2e.md @@ -7,7 +7,7 @@ depends_on: - "11-shared-local-remote-selector" - "12-unified-files-workspace-actions" plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 13: UX delta end-to-end coverage diff --git a/docs/plans/attach-workspace-sources/task-14-ux-terminology-documentation.md b/docs/plans/attach-workspace-sources/task-14-ux-terminology-documentation.md index a6a5cfdcc3..41f691cb7e 100644 --- a/docs/plans/attach-workspace-sources/task-14-ux-terminology-documentation.md +++ b/docs/plans/attach-workspace-sources/task-14-ux-terminology-documentation.md @@ -7,7 +7,7 @@ depends_on: - "11-shared-local-remote-selector" - "12-unified-files-workspace-actions" plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 14: UX terminology documentation diff --git a/docs/plans/backend-failure-containment/plan.md b/docs/plans/backend-failure-containment/plan.md index 635e01d30b..5dd1549a31 100644 --- a/docs/plans/backend-failure-containment/plan.md +++ b/docs/plans/backend-failure-containment/plan.md @@ -2,7 +2,7 @@ spec: docs/specs/platform/agent-runtime-availability.md related_specs: - docs/specs/office/costs.md - - docs/specs/tasks/runtime-cleanup.md + - docs/specs/tasks/system-design/runtime-cleanup.md - docs/specs/ui/entity-reference-composer.md created: 2026-08-08 status: completed diff --git a/docs/plans/backend-failure-containment/task-06-classify-startup-runtime-cleanup.md b/docs/plans/backend-failure-containment/task-06-classify-startup-runtime-cleanup.md index a7ca1a3cf5..1915025fe4 100644 --- a/docs/plans/backend-failure-containment/task-06-classify-startup-runtime-cleanup.md +++ b/docs/plans/backend-failure-containment/task-06-classify-startup-runtime-cleanup.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/runtime-cleanup.md" +spec: "../../specs/tasks/system-design/runtime-cleanup.md" --- # Task 06: Classify startup runtime cleanup diff --git a/docs/plans/backend-regressions-office-disabled-cleanup/plan.md b/docs/plans/backend-regressions-office-disabled-cleanup/plan.md index c2ee5283df..e25a446662 100644 --- a/docs/plans/backend-regressions-office-disabled-cleanup/plan.md +++ b/docs/plans/backend-regressions-office-disabled-cleanup/plan.md @@ -13,8 +13,8 @@ package and is separately reviewable. ## Related Specs - `docs/specs/platform/cron-office-disabled-safety.md` (Task 01, new repair spec) -- `docs/specs/tasks/title-length-limit.md` (Task 02, amended) -- `docs/specs/tasks/runtime-cleanup.md` (Task 03, amended) +- `docs/specs/tasks/requirements/title-length-limit.md` (Task 02, amended) +- `docs/specs/tasks/system-design/runtime-cleanup.md` (Task 03, amended) ## Root Causes diff --git a/docs/plans/backend-regressions-office-disabled-cleanup/task-02-truncate-generated-titles.md b/docs/plans/backend-regressions-office-disabled-cleanup/task-02-truncate-generated-titles.md index 363ccc1624..95a75ea6ff 100644 --- a/docs/plans/backend-regressions-office-disabled-cleanup/task-02-truncate-generated-titles.md +++ b/docs/plans/backend-regressions-office-disabled-cleanup/task-02-truncate-generated-titles.md @@ -5,7 +5,7 @@ status: complete wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/title-length-limit.md" +spec: "../../specs/tasks/requirements/title-length-limit.md" parallel-safe: true --- @@ -71,7 +71,7 @@ None. ## Inputs -- Amended spec: `docs/specs/tasks/title-length-limit.md` (watcher-title bullet +- Amended spec: `docs/specs/tasks/requirements/title-length-limit.md` (watcher-title bullet and scenarios). - Confirmed 5 "task title is too long" review-task failures in the logs (PRs #12096, #12057, #12054). diff --git a/docs/plans/backend-regressions-office-disabled-cleanup/task-03-confirmed-dead-runtime-cleanup.md b/docs/plans/backend-regressions-office-disabled-cleanup/task-03-confirmed-dead-runtime-cleanup.md index 3a5508f4eb..a6e6bf03fc 100644 --- a/docs/plans/backend-regressions-office-disabled-cleanup/task-03-confirmed-dead-runtime-cleanup.md +++ b/docs/plans/backend-regressions-office-disabled-cleanup/task-03-confirmed-dead-runtime-cleanup.md @@ -5,7 +5,7 @@ status: complete wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/runtime-cleanup.md" +spec: "../../specs/tasks/system-design/runtime-cleanup.md" parallel-safe: false --- @@ -82,7 +82,7 @@ consistent. Not parallel-safe. ## Inputs -- Amended spec: `docs/specs/tasks/runtime-cleanup.md` (confirmed-dead +- Amended spec: `docs/specs/tasks/system-design/runtime-cleanup.md` (confirmed-dead not-found-as-stopped bullets, failure modes, and scenarios). - Existing runtime-aware liveness: `lifecycle.RowProcessLiveness` / `models.ProcessLiveness` and `RowMustBePreserved` diff --git a/docs/plans/cancelled-turn-completion/plan.md b/docs/plans/cancelled-turn-completion/plan.md index a88ba02611..fc10b55dd0 100644 --- a/docs/plans/cancelled-turn-completion/plan.md +++ b/docs/plans/cancelled-turn-completion/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/workflow/cancelled-turn-completion/spec.md +spec: docs/specs/tasks/requirements/workflow-cancelled-turn-completion.md created: 2026-08-02 status: implemented --- diff --git a/docs/plans/cancelled-turn-completion/task-01-step-contract-and-template.md b/docs/plans/cancelled-turn-completion/task-01-step-contract-and-template.md index 961eb7f09b..5ce1792ade 100644 --- a/docs/plans/cancelled-turn-completion/task-01-step-contract-and-template.md +++ b/docs/plans/cancelled-turn-completion/task-01-step-contract-and-template.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/workflow/cancelled-turn-completion/spec.md" +spec: "../../specs/tasks/requirements/workflow-cancelled-turn-completion.md" --- # Task 01: Workflow step persistence contract diff --git a/docs/plans/cancelled-turn-completion/task-02-configuration-surfaces.md b/docs/plans/cancelled-turn-completion/task-02-configuration-surfaces.md index 8af3deae40..1219e9bd8b 100644 --- a/docs/plans/cancelled-turn-completion/task-02-configuration-surfaces.md +++ b/docs/plans/cancelled-turn-completion/task-02-configuration-surfaces.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-step-contract-and-template"] plan: "plan.md" -spec: "../../specs/workflow/cancelled-turn-completion/spec.md" +spec: "../../specs/tasks/requirements/workflow-cancelled-turn-completion.md" --- # Task 02: Workflow configuration surfaces diff --git a/docs/plans/cancelled-turn-completion/task-03-cancellation-routing.md b/docs/plans/cancelled-turn-completion/task-03-cancellation-routing.md index 841ae94d95..85543f25bf 100644 --- a/docs/plans/cancelled-turn-completion/task-03-cancellation-routing.md +++ b/docs/plans/cancelled-turn-completion/task-03-cancellation-routing.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-step-contract-and-template"] plan: "plan.md" -spec: "../../specs/workflow/cancelled-turn-completion/spec.md" +spec: "../../specs/tasks/requirements/workflow-cancelled-turn-completion.md" --- # Task 03: Explicit cancellation workflow routing diff --git a/docs/plans/cancelled-turn-completion/task-04-settings-ui.md b/docs/plans/cancelled-turn-completion/task-04-settings-ui.md index a963ec997b..c635703eec 100644 --- a/docs/plans/cancelled-turn-completion/task-04-settings-ui.md +++ b/docs/plans/cancelled-turn-completion/task-04-settings-ui.md @@ -5,7 +5,7 @@ status: completed wave: 3 depends_on: ["02-configuration-surfaces"] plan: "plan.md" -spec: "../../specs/workflow/cancelled-turn-completion/spec.md" +spec: "../../specs/tasks/requirements/workflow-cancelled-turn-completion.md" --- # Task 04: Workflow cancellation setting UI diff --git a/docs/plans/cancelled-turn-completion/task-05-public-docs.md b/docs/plans/cancelled-turn-completion/task-05-public-docs.md index 58d5732ee0..8a43892784 100644 --- a/docs/plans/cancelled-turn-completion/task-05-public-docs.md +++ b/docs/plans/cancelled-turn-completion/task-05-public-docs.md @@ -5,7 +5,7 @@ status: completed wave: 3 depends_on: ["01-step-contract-and-template", "02-configuration-surfaces"] plan: "plan.md" -spec: "../../specs/workflow/cancelled-turn-completion/spec.md" +spec: "../../specs/tasks/requirements/workflow-cancelled-turn-completion.md" --- # Task 05: Cancelled turn public documentation diff --git a/docs/plans/cancelled-turn-completion/task-06-e2e-coverage.md b/docs/plans/cancelled-turn-completion/task-06-e2e-coverage.md index 0505370dde..15c7d7340e 100644 --- a/docs/plans/cancelled-turn-completion/task-06-e2e-coverage.md +++ b/docs/plans/cancelled-turn-completion/task-06-e2e-coverage.md @@ -5,7 +5,7 @@ status: completed wave: 4 depends_on: ["03-cancellation-routing", "04-settings-ui"] plan: "plan.md" -spec: "../../specs/workflow/cancelled-turn-completion/spec.md" +spec: "../../specs/tasks/requirements/workflow-cancelled-turn-completion.md" --- # Task 06: Cancelled turn E2E coverage diff --git a/docs/plans/cascade-archive-navigation/plan.md b/docs/plans/cascade-archive-navigation/plan.md index 99e57fc793..5ebc59cded 100644 --- a/docs/plans/cascade-archive-navigation/plan.md +++ b/docs/plans/cascade-archive-navigation/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/archive-confirmation.md +spec: docs/specs/tasks/requirements/archive-confirmation.md created: 2026-08-12 status: complete --- diff --git a/docs/plans/cascade-archive-navigation/task-01-cascade-aware-switch.md b/docs/plans/cascade-archive-navigation/task-01-cascade-aware-switch.md index 0f387115c8..f51ca97117 100644 --- a/docs/plans/cascade-archive-navigation/task-01-cascade-aware-switch.md +++ b/docs/plans/cascade-archive-navigation/task-01-cascade-aware-switch.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/archive-confirmation.md" +spec: "../../specs/tasks/requirements/archive-confirmation.md" --- # Task 01: Make Archive Switching Cascade-Aware diff --git a/docs/plans/cascade-archive-navigation/task-02-archive-navigation-e2e.md b/docs/plans/cascade-archive-navigation/task-02-archive-navigation-e2e.md index 821ea0454d..2a1522ce62 100644 --- a/docs/plans/cascade-archive-navigation/task-02-archive-navigation-e2e.md +++ b/docs/plans/cascade-archive-navigation/task-02-archive-navigation-e2e.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-cascade-aware-switch"] plan: "plan.md" -spec: "../../specs/tasks/archive-confirmation.md" +spec: "../../specs/tasks/requirements/archive-confirmation.md" --- # Task 02: Add Archive Navigation E2E Coverage diff --git a/docs/plans/clarification-active-lifecycle/plan.md b/docs/plans/clarification-active-lifecycle/plan.md index f6e516198c..984d1c3075 100644 --- a/docs/plans/clarification-active-lifecycle/plan.md +++ b/docs/plans/clarification-active-lifecycle/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/clarification-active-lifecycle/spec.md +spec: docs/specs/tasks/requirements/clarification-active-lifecycle.md created: 2026-08-14 status: completed --- diff --git a/docs/plans/clarification-active-lifecycle/task-01-clarification-regression-red.md b/docs/plans/clarification-active-lifecycle/task-01-clarification-regression-red.md index 9ae447e7de..9d786efcff 100644 --- a/docs/plans/clarification-active-lifecycle/task-01-clarification-regression-red.md +++ b/docs/plans/clarification-active-lifecycle/task-01-clarification-regression-red.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/clarification-active-lifecycle/spec.md" +spec: "../../specs/tasks/requirements/clarification-active-lifecycle.md" --- # Task 01: Clarification regression red diff --git a/docs/plans/clarification-active-lifecycle/task-02-current-turn-backend-authority.md b/docs/plans/clarification-active-lifecycle/task-02-current-turn-backend-authority.md index 004317b7b3..97f661d07f 100644 --- a/docs/plans/clarification-active-lifecycle/task-02-current-turn-backend-authority.md +++ b/docs/plans/clarification-active-lifecycle/task-02-current-turn-backend-authority.md @@ -5,7 +5,7 @@ status: completed wave: 2 depends_on: ["01-clarification-regression-red"] plan: "plan.md" -spec: "../../specs/clarification-active-lifecycle/spec.md" +spec: "../../specs/tasks/requirements/clarification-active-lifecycle.md" --- # Task 02: Current-turn backend authority diff --git a/docs/plans/clarification-active-lifecycle/task-03-summary-pending-convergence.md b/docs/plans/clarification-active-lifecycle/task-03-summary-pending-convergence.md index 446df5b9a9..d964249b1e 100644 --- a/docs/plans/clarification-active-lifecycle/task-03-summary-pending-convergence.md +++ b/docs/plans/clarification-active-lifecycle/task-03-summary-pending-convergence.md @@ -5,7 +5,7 @@ status: completed wave: 3 depends_on: ["02-current-turn-backend-authority"] plan: "plan.md" -spec: "../../specs/clarification-active-lifecycle/spec.md" +spec: "../../specs/tasks/requirements/clarification-active-lifecycle.md" --- # Task 03: Summary pending convergence diff --git a/docs/plans/clarification-active-lifecycle/task-04-pending-owner-navigation.md b/docs/plans/clarification-active-lifecycle/task-04-pending-owner-navigation.md index 649b94bac4..41cc47f15c 100644 --- a/docs/plans/clarification-active-lifecycle/task-04-pending-owner-navigation.md +++ b/docs/plans/clarification-active-lifecycle/task-04-pending-owner-navigation.md @@ -5,7 +5,7 @@ status: completed wave: 4 depends_on: ["03-summary-pending-convergence"] plan: "plan.md" -spec: "../../specs/clarification-active-lifecycle/spec.md" +spec: "../../specs/tasks/requirements/clarification-active-lifecycle.md" --- # Task 04: Pending-owner navigation diff --git a/docs/plans/clarification-active-lifecycle/task-05-clarification-regression-green.md b/docs/plans/clarification-active-lifecycle/task-05-clarification-regression-green.md index 6b0999926e..4a3ac6c9dd 100644 --- a/docs/plans/clarification-active-lifecycle/task-05-clarification-regression-green.md +++ b/docs/plans/clarification-active-lifecycle/task-05-clarification-regression-green.md @@ -11,7 +11,7 @@ depends_on: "04-pending-owner-navigation", ] plan: "plan.md" -spec: "../../specs/clarification-active-lifecycle/spec.md" +spec: "../../specs/tasks/requirements/clarification-active-lifecycle.md" --- # Task 05: Clarification regression green diff --git a/docs/plans/clarification-task-state-publication/plan.md b/docs/plans/clarification-task-state-publication/plan.md index 28ff30a46b..76e6f5616f 100644 --- a/docs/plans/clarification-task-state-publication/plan.md +++ b/docs/plans/clarification-task-state-publication/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/runtime-state-publication-order.md +spec: docs/specs/tasks/requirements/runtime-state-publication-order.md created: 2026-08-02 status: complete --- diff --git a/docs/plans/clarification-task-state-publication/task-01-publish-clarification-task-state.md b/docs/plans/clarification-task-state-publication/task-01-publish-clarification-task-state.md index 0e6d6f843d..244c9aa729 100644 --- a/docs/plans/clarification-task-state-publication/task-01-publish-clarification-task-state.md +++ b/docs/plans/clarification-task-state-publication/task-01-publish-clarification-task-state.md @@ -5,7 +5,7 @@ status: completed wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/runtime-state-publication-order.md" +spec: "../../specs/tasks/requirements/runtime-state-publication-order.md" --- # Task 01: Publish clarification task state diff --git a/docs/plans/clarification-task-state-publication/task-02-prove-live-sidebar-regrouping.md b/docs/plans/clarification-task-state-publication/task-02-prove-live-sidebar-regrouping.md index df906e5244..d23ee0886e 100644 --- a/docs/plans/clarification-task-state-publication/task-02-prove-live-sidebar-regrouping.md +++ b/docs/plans/clarification-task-state-publication/task-02-prove-live-sidebar-regrouping.md @@ -5,7 +5,7 @@ status: completed wave: 2 depends_on: ["01-publish-clarification-task-state"] plan: "plan.md" -spec: "../../specs/tasks/runtime-state-publication-order.md" +spec: "../../specs/tasks/requirements/runtime-state-publication-order.md" --- # Task 02: Prove live sidebar regrouping diff --git a/docs/plans/clarification-task-state-publication/task-03-address-publication-review.md b/docs/plans/clarification-task-state-publication/task-03-address-publication-review.md index 8e1424475d..31a23b6e22 100644 --- a/docs/plans/clarification-task-state-publication/task-03-address-publication-review.md +++ b/docs/plans/clarification-task-state-publication/task-03-address-publication-review.md @@ -5,7 +5,7 @@ status: completed wave: 3 depends_on: ["01-publish-clarification-task-state"] plan: "plan.md" -spec: "../../specs/tasks/runtime-state-publication-order.md" +spec: "../../specs/tasks/requirements/runtime-state-publication-order.md" --- # Task 03: Address ordered publication review findings @@ -25,7 +25,7 @@ spec: "../../specs/tasks/runtime-state-publication-order.md" - `apps/backend/internal/mcp/handlers/handlers.go` - `apps/backend/internal/mcp/handlers/handlers_test.go` - `apps/backend/internal/task/service/service_events.go` -- `docs/specs/tasks/runtime-state-publication-order.md` +- `docs/specs/tasks/requirements/runtime-state-publication-order.md` - `docs/decisions/2026-07-30-runtime-task-state-before-running-event.md` - `docs/plans/clarification-task-state-publication/plan.md` - `docs/plans/clarification-task-state-publication/task-01-publish-clarification-task-state.md` diff --git a/docs/plans/creation-flow-reliability/plan.md b/docs/plans/creation-flow-reliability/plan.md index e6a72e2b66..2def1f51b2 100644 --- a/docs/plans/creation-flow-reliability/plan.md +++ b/docs/plans/creation-flow-reliability/plan.md @@ -13,7 +13,7 @@ behavior, then verify both through their existing user-facing creation flows. Th keeps Office onboarding separate while the frontend change preserves the current desktop and mobile dialog composition. -Related requirement: `docs/specs/tasks/multi-branch/spec.md` task-creation scenarios. +Related requirement: `docs/specs/tasks/requirements/multi-branch.md` task-creation scenarios. ## Backend diff --git a/docs/plans/creation-flow-reliability/task-01-repository-tooltip.md b/docs/plans/creation-flow-reliability/task-01-repository-tooltip.md index a0cdd793e2..6a2bd9c190 100644 --- a/docs/plans/creation-flow-reliability/task-01-repository-tooltip.md +++ b/docs/plans/creation-flow-reliability/task-01-repository-tooltip.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/multi-branch/spec.md" +spec: "../../specs/tasks/requirements/multi-branch.md" --- # Task 01: Repository tooltip containment and disclosure @@ -38,7 +38,7 @@ None. ## Inputs -- `docs/specs/tasks/multi-branch/spec.md` task-creation scenarios. +- `docs/specs/tasks/requirements/multi-branch.md` task-creation scenarios. - `docs/plans/creation-flow-reliability/plan.md` frontend and mobile design sections. - Existing `useTooltipMountGate` behavior and Radix tooltip guidance in `apps/web/AGENTS.md`. diff --git a/docs/plans/creator-session-task-inheritance/plan.md b/docs/plans/creator-session-task-inheritance/plan.md index 403e260419..c3a030c1ea 100644 --- a/docs/plans/creator-session-task-inheritance/plan.md +++ b/docs/plans/creator-session-task-inheritance/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/mcp-task-agent-profile-default/spec.md +spec: docs/specs/tasks/requirements/mcp-task-agent-profile-default.md created: 2026-08-12 status: complete --- diff --git a/docs/plans/creator-session-task-inheritance/task-01-initial-runtime-seed.md b/docs/plans/creator-session-task-inheritance/task-01-initial-runtime-seed.md index 2645006b63..54e2269e4b 100644 --- a/docs/plans/creator-session-task-inheritance/task-01-initial-runtime-seed.md +++ b/docs/plans/creator-session-task-inheritance/task-01-initial-runtime-seed.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/mcp-task-agent-profile-default/spec.md" +spec: "../../specs/tasks/requirements/mcp-task-agent-profile-default.md" --- # Task 01: Initial runtime seed diff --git a/docs/plans/creator-session-task-inheritance/task-02-creator-session-resolution.md b/docs/plans/creator-session-task-inheritance/task-02-creator-session-resolution.md index eae980dc47..bc0abb9b79 100644 --- a/docs/plans/creator-session-task-inheritance/task-02-creator-session-resolution.md +++ b/docs/plans/creator-session-task-inheritance/task-02-creator-session-resolution.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-initial-runtime-seed"] plan: "plan.md" -spec: "../../specs/tasks/mcp-task-agent-profile-default/spec.md" +spec: "../../specs/tasks/requirements/mcp-task-agent-profile-default.md" --- # Task 02: Creator-session resolution diff --git a/docs/plans/creator-session-task-inheritance/task-03-explain-session-default.md b/docs/plans/creator-session-task-inheritance/task-03-explain-session-default.md index d41375d1cb..2e15ca6420 100644 --- a/docs/plans/creator-session-task-inheritance/task-03-explain-session-default.md +++ b/docs/plans/creator-session-task-inheritance/task-03-explain-session-default.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-creator-session-resolution"] plan: "plan.md" -spec: "../../specs/tasks/mcp-task-agent-profile-default/spec.md" +spec: "../../specs/tasks/requirements/mcp-task-agent-profile-default.md" --- # Task 03: Explain creator-session default diff --git a/docs/plans/creator-session-task-inheritance/task-04-end-to-end-inheritance.md b/docs/plans/creator-session-task-inheritance/task-04-end-to-end-inheritance.md index 922746d4b8..3353144713 100644 --- a/docs/plans/creator-session-task-inheritance/task-04-end-to-end-inheritance.md +++ b/docs/plans/creator-session-task-inheritance/task-04-end-to-end-inheritance.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["02-creator-session-resolution", "03-explain-session-default"] plan: "plan.md" -spec: "../../specs/tasks/mcp-task-agent-profile-default/spec.md" +spec: "../../specs/tasks/requirements/mcp-task-agent-profile-default.md" --- # Task 04: End-to-end inheritance diff --git a/docs/plans/fork-pr-comparison-targets/plan.md b/docs/plans/fork-pr-comparison-targets/plan.md index 388ff17e37..14cc151922 100644 --- a/docs/plans/fork-pr-comparison-targets/plan.md +++ b/docs/plans/fork-pr-comparison-targets/plan.md @@ -21,7 +21,7 @@ This repairs the reported case where the fork's stale `origin/main` made Kandev Related contracts: - [Workspace Git Status](../../specs/platform/workspace-git-status.md) -- [Remote Contribution Tasks](../../specs/tasks/remote-contribution-tasks.md) +- [Remote Contribution Tasks](../../specs/tasks/system-design/remote-contribution-tasks.md) - [ADR: Qualify Git Comparison Targets by Repository](../../decisions/2026-08-19-repository-qualified-comparison-targets.md) - [ADR: Multi-branch task support](../../decisions/0013-multi-branch-tasks.md) - [ADR: Bind Fork Push Destinations to Tasks](../../decisions/2026-08-12-task-bound-fork-destinations.md) diff --git a/docs/plans/fork-pr-comparison-targets/task-03-pr-reconciliation-and-lifecycle.md b/docs/plans/fork-pr-comparison-targets/task-03-pr-reconciliation-and-lifecycle.md index 3e928df2fa..b47df380d6 100644 --- a/docs/plans/fork-pr-comparison-targets/task-03-pr-reconciliation-and-lifecycle.md +++ b/docs/plans/fork-pr-comparison-targets/task-03-pr-reconciliation-and-lifecycle.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-agentctl-comparison-target"] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 03: PR reconciliation, lifecycle refresh, and summaries diff --git a/docs/plans/github-issue-task-linking/plan.md b/docs/plans/github-issue-task-linking/plan.md index 910ebe1c14..e4772e9ee0 100644 --- a/docs/plans/github-issue-task-linking/plan.md +++ b/docs/plans/github-issue-task-linking/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/link-existing-task-github-issue.md +spec: docs/specs/tasks/requirements/link-existing-task-github-issue.md created: 2026-07-13 status: completed --- diff --git a/docs/plans/github-issue-task-linking/task-01-backend-reverse-lookup.md b/docs/plans/github-issue-task-linking/task-01-backend-reverse-lookup.md index 7b9400360d..cc06ba9711 100644 --- a/docs/plans/github-issue-task-linking/task-01-backend-reverse-lookup.md +++ b/docs/plans/github-issue-task-linking/task-01-backend-reverse-lookup.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/link-existing-task-github-issue.md" +spec: "../../specs/tasks/requirements/link-existing-task-github-issue.md" --- # Task 01: Backend Issue-Link Reverse Lookup diff --git a/docs/plans/github-issue-task-linking/task-02-frontend-link-and-indicator.md b/docs/plans/github-issue-task-linking/task-02-frontend-link-and-indicator.md index fb7dc7652a..a22456840c 100644 --- a/docs/plans/github-issue-task-linking/task-02-frontend-link-and-indicator.md +++ b/docs/plans/github-issue-task-linking/task-02-frontend-link-and-indicator.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-backend-reverse-lookup"] plan: "plan.md" -spec: "../../specs/tasks/link-existing-task-github-issue.md" +spec: "../../specs/tasks/requirements/link-existing-task-github-issue.md" --- # Task 02: Frontend Automatic Link And Issue Indicator diff --git a/docs/plans/github-issue-task-linking/task-03-e2e-and-verification.md b/docs/plans/github-issue-task-linking/task-03-e2e-and-verification.md index 2cdbd7c9a8..058e2bcacf 100644 --- a/docs/plans/github-issue-task-linking/task-03-e2e-and-verification.md +++ b/docs/plans/github-issue-task-linking/task-03-e2e-and-verification.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-frontend-link-and-indicator"] plan: "plan.md" -spec: "../../specs/tasks/link-existing-task-github-issue.md" +spec: "../../specs/tasks/requirements/link-existing-task-github-issue.md" --- # Task 03: Issue Indicator E2E And Verification diff --git a/docs/plans/interrupted-task-indicator/plan.md b/docs/plans/interrupted-task-indicator/plan.md index c8b5084e56..9a05aa99c1 100644 --- a/docs/plans/interrupted-task-indicator/plan.md +++ b/docs/plans/interrupted-task-indicator/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/interrupted-task-indicator.md +spec: docs/specs/tasks/requirements/interrupted-task-indicator.md created: 2026-08-02 status: complete --- diff --git a/docs/plans/interrupted-task-indicator/task-01-backend-startup-marker.md b/docs/plans/interrupted-task-indicator/task-01-backend-startup-marker.md index ba594829d9..a49e1d243b 100644 --- a/docs/plans/interrupted-task-indicator/task-01-backend-startup-marker.md +++ b/docs/plans/interrupted-task-indicator/task-01-backend-startup-marker.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/interrupted-task-indicator.md" +spec: "../../specs/tasks/requirements/interrupted-task-indicator.md" --- # Task 01: Backend startup interruption marker diff --git a/docs/plans/interrupted-task-indicator/task-02-backend-dto-and-clearing.md b/docs/plans/interrupted-task-indicator/task-02-backend-dto-and-clearing.md index bcce6447a0..7fc101e1cb 100644 --- a/docs/plans/interrupted-task-indicator/task-02-backend-dto-and-clearing.md +++ b/docs/plans/interrupted-task-indicator/task-02-backend-dto-and-clearing.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-backend-startup-marker"] plan: "plan.md" -spec: "../../specs/tasks/interrupted-task-indicator.md" +spec: "../../specs/tasks/requirements/interrupted-task-indicator.md" --- # Task 02: Backend DTO exposure and marker clearing diff --git a/docs/plans/interrupted-task-indicator/task-03-frontend-data-plumbing.md b/docs/plans/interrupted-task-indicator/task-03-frontend-data-plumbing.md index 2551e01db7..3812f4cddc 100644 --- a/docs/plans/interrupted-task-indicator/task-03-frontend-data-plumbing.md +++ b/docs/plans/interrupted-task-indicator/task-03-frontend-data-plumbing.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-backend-dto-and-clearing"] plan: "plan.md" -spec: "../../specs/tasks/interrupted-task-indicator.md" +spec: "../../specs/tasks/requirements/interrupted-task-indicator.md" --- # Task 03: Frontend interrupted-field plumbing diff --git a/docs/plans/interrupted-task-indicator/task-04-frontend-icon-rendering.md b/docs/plans/interrupted-task-indicator/task-04-frontend-icon-rendering.md index 76b431ecb9..59a1ba28ef 100644 --- a/docs/plans/interrupted-task-indicator/task-04-frontend-icon-rendering.md +++ b/docs/plans/interrupted-task-indicator/task-04-frontend-icon-rendering.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["03-frontend-data-plumbing"] plan: "plan.md" -spec: "../../specs/tasks/interrupted-task-indicator.md" +spec: "../../specs/tasks/requirements/interrupted-task-indicator.md" --- # Task 04: Frontend interrupted icon rendering diff --git a/docs/plans/interrupted-task-indicator/task-05-e2e-verification.md b/docs/plans/interrupted-task-indicator/task-05-e2e-verification.md index 20aad357bc..6ea198a1c5 100644 --- a/docs/plans/interrupted-task-indicator/task-05-e2e-verification.md +++ b/docs/plans/interrupted-task-indicator/task-05-e2e-verification.md @@ -5,7 +5,7 @@ status: done wave: 5 depends_on: ["04-frontend-icon-rendering"] plan: "plan.md" -spec: "../../specs/tasks/interrupted-task-indicator.md" +spec: "../../specs/tasks/requirements/interrupted-task-indicator.md" --- # Task 05: E2E verification and final checks diff --git a/docs/plans/kanban-completion-tool-reliability/plan.md b/docs/plans/kanban-completion-tool-reliability/plan.md index a369281ada..8704e3cd00 100644 --- a/docs/plans/kanban-completion-tool-reliability/plan.md +++ b/docs/plans/kanban-completion-tool-reliability/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/workflow/explicit-completion-signal/spec.md +spec: docs/specs/tasks/requirements/workflow-explicit-completion-signal.md created: 2026-07-22 status: implemented --- diff --git a/docs/plans/kanban-completion-tool-reliability/task-01-task-only-eager-tool.md b/docs/plans/kanban-completion-tool-reliability/task-01-task-only-eager-tool.md index dc66ffd37d..044e412005 100644 --- a/docs/plans/kanban-completion-tool-reliability/task-01-task-only-eager-tool.md +++ b/docs/plans/kanban-completion-tool-reliability/task-01-task-only-eager-tool.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/workflow/explicit-completion-signal/spec.md" +spec: "../../specs/tasks/requirements/workflow-explicit-completion-signal.md" --- # Task 01: Keep the task completion tool discoverable diff --git a/docs/plans/kanban-completion-tool-reliability/task-02-canonical-tool-naming.md b/docs/plans/kanban-completion-tool-reliability/task-02-canonical-tool-naming.md index 4ae9d74626..2f2bea120a 100644 --- a/docs/plans/kanban-completion-tool-reliability/task-02-canonical-tool-naming.md +++ b/docs/plans/kanban-completion-tool-reliability/task-02-canonical-tool-naming.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-task-only-eager-tool"] plan: "plan.md" -spec: "../../specs/workflow/explicit-completion-signal/spec.md" +spec: "../../specs/tasks/requirements/workflow-explicit-completion-signal.md" --- # Task 02: Clarify canonical MCP tool names diff --git a/docs/plans/kanban-completion-tool-reliability/task-03-pin-and-reconnect-claude-acp.md b/docs/plans/kanban-completion-tool-reliability/task-03-pin-and-reconnect-claude-acp.md index b503db8910..4756c51f5b 100644 --- a/docs/plans/kanban-completion-tool-reliability/task-03-pin-and-reconnect-claude-acp.md +++ b/docs/plans/kanban-completion-tool-reliability/task-03-pin-and-reconnect-claude-acp.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-task-only-eager-tool"] plan: "plan.md" -spec: "../../specs/workflow/explicit-completion-signal/spec.md" +spec: "../../specs/tasks/requirements/workflow-explicit-completion-signal.md" --- # Task 03: Verify the Claude ACP bridge and reconnect path diff --git a/docs/plans/kanban-completion-tool-reliability/task-04-kanban-reliability-verification.md b/docs/plans/kanban-completion-tool-reliability/task-04-kanban-reliability-verification.md index 059705ce2f..7d1d1967db 100644 --- a/docs/plans/kanban-completion-tool-reliability/task-04-kanban-reliability-verification.md +++ b/docs/plans/kanban-completion-tool-reliability/task-04-kanban-reliability-verification.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["01-task-only-eager-tool", "02-canonical-tool-naming", "03-pin-and-reconnect-claude-acp"] plan: "plan.md" -spec: "../../specs/workflow/explicit-completion-signal/spec.md" +spec: "../../specs/tasks/requirements/workflow-explicit-completion-signal.md" --- # Task 04: Verify Kanban completion reliability diff --git a/docs/plans/mark-selected-task-repositories/plan.md b/docs/plans/mark-selected-task-repositories/plan.md index fa351816c2..f2f0079ac5 100644 --- a/docs/plans/mark-selected-task-repositories/plan.md +++ b/docs/plans/mark-selected-task-repositories/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/multi-branch/spec.md +spec: docs/specs/tasks/requirements/multi-branch.md created: 2026-07-22 status: completed --- diff --git a/docs/plans/mark-selected-task-repositories/task-01-workspace-repository-marker.md b/docs/plans/mark-selected-task-repositories/task-01-workspace-repository-marker.md index 709ba2b969..9c78aad8c9 100644 --- a/docs/plans/mark-selected-task-repositories/task-01-workspace-repository-marker.md +++ b/docs/plans/mark-selected-task-repositories/task-01-workspace-repository-marker.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/multi-branch/spec.md" +spec: "../../specs/tasks/requirements/multi-branch.md" --- # Task 01: Mark selected workspace repositories @@ -31,7 +31,7 @@ spec: "../../specs/tasks/multi-branch/spec.md" ## Inputs -- `docs/specs/tasks/multi-branch/spec.md`, Frontend and Task-creation scenarios. +- `docs/specs/tasks/requirements/multi-branch.md`, Frontend and Task-creation scenarios. - `docs/plans/mark-selected-task-repositories/plan.md`, Workspace and discovered repositories plus Mobile design contract. - Existing `WorkspaceRepoChips` duplicate policy used by quick chat. diff --git a/docs/plans/mark-selected-task-repositories/task-02-remote-repository-marker.md b/docs/plans/mark-selected-task-repositories/task-02-remote-repository-marker.md index ad13d39024..2881a1bb60 100644 --- a/docs/plans/mark-selected-task-repositories/task-02-remote-repository-marker.md +++ b/docs/plans/mark-selected-task-repositories/task-02-remote-repository-marker.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/multi-branch/spec.md" +spec: "../../specs/tasks/requirements/multi-branch.md" --- # Task 02: Mark selected Remote repositories @@ -32,7 +32,7 @@ spec: "../../specs/tasks/multi-branch/spec.md" ## Inputs -- `docs/specs/tasks/multi-branch/spec.md`, Frontend and Task-creation scenarios. +- `docs/specs/tasks/requirements/multi-branch.md`, Frontend and Task-creation scenarios. - `docs/plans/mark-selected-task-repositories/plan.md`, Remote provider repositories plus Mobile design contract. - Existing `TaskRemoteRepoRow` picker metadata and `RemoteRepository` identity fields. diff --git a/docs/plans/mark-selected-task-repositories/task-03-remote-identity-remediation.md b/docs/plans/mark-selected-task-repositories/task-03-remote-identity-remediation.md index 9b96b3346a..8ff5342610 100644 --- a/docs/plans/mark-selected-task-repositories/task-03-remote-identity-remediation.md +++ b/docs/plans/mark-selected-task-repositories/task-03-remote-identity-remediation.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["02-remote-repository-marker"] plan: "plan.md" -spec: "../../specs/tasks/multi-branch/spec.md" +spec: "../../specs/tasks/requirements/multi-branch.md" --- # Task 03: Canonicalize Remote repository identity diff --git a/docs/plans/mark-selected-task-repositories/task-04-workspace-coverage-remediation.md b/docs/plans/mark-selected-task-repositories/task-04-workspace-coverage-remediation.md index d45cb82779..ac98788ea3 100644 --- a/docs/plans/mark-selected-task-repositories/task-04-workspace-coverage-remediation.md +++ b/docs/plans/mark-selected-task-repositories/task-04-workspace-coverage-remediation.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-workspace-repository-marker"] plan: "plan.md" -spec: "../../specs/tasks/multi-branch/spec.md" +spec: "../../specs/tasks/requirements/multi-branch.md" --- # Task 04: Prove workspace marker exclusion diff --git a/docs/plans/mark-selected-task-repositories/task-05-remote-identity-review-fix.md b/docs/plans/mark-selected-task-repositories/task-05-remote-identity-review-fix.md index a5899626d0..26dbab209e 100644 --- a/docs/plans/mark-selected-task-repositories/task-05-remote-identity-review-fix.md +++ b/docs/plans/mark-selected-task-repositories/task-05-remote-identity-review-fix.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["03-remote-identity-remediation"] plan: "plan.md" -spec: "../../specs/tasks/multi-branch/spec.md" +spec: "../../specs/tasks/requirements/multi-branch.md" --- # Task 05: Close Remote identity review gaps diff --git a/docs/plans/mark-selected-task-repositories/task-06-github-case-normalization.md b/docs/plans/mark-selected-task-repositories/task-06-github-case-normalization.md index db3758448a..09e41dd97d 100644 --- a/docs/plans/mark-selected-task-repositories/task-06-github-case-normalization.md +++ b/docs/plans/mark-selected-task-repositories/task-06-github-case-normalization.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["05-remote-identity-review-fix"] plan: "plan.md" -spec: "../../specs/tasks/multi-branch/spec.md" +spec: "../../specs/tasks/requirements/multi-branch.md" --- # Task 06: Normalize GitHub repository case diff --git a/docs/plans/mark-selected-task-repositories/task-07-lint-cleanup.md b/docs/plans/mark-selected-task-repositories/task-07-lint-cleanup.md index 8fa560b7b9..cf1217ba79 100644 --- a/docs/plans/mark-selected-task-repositories/task-07-lint-cleanup.md +++ b/docs/plans/mark-selected-task-repositories/task-07-lint-cleanup.md @@ -5,7 +5,7 @@ status: done wave: 5 depends_on: ["04-workspace-coverage-remediation", "06-github-case-normalization"] plan: "plan.md" -spec: "../../specs/tasks/multi-branch/spec.md" +spec: "../../specs/tasks/requirements/multi-branch.md" --- # Task 07: Clear repository marker lint warnings diff --git a/docs/plans/mark-selected-task-repositories/task-08-remote-popover-lint.md b/docs/plans/mark-selected-task-repositories/task-08-remote-popover-lint.md index 61a7c5d6e5..cbcd313156 100644 --- a/docs/plans/mark-selected-task-repositories/task-08-remote-popover-lint.md +++ b/docs/plans/mark-selected-task-repositories/task-08-remote-popover-lint.md @@ -5,7 +5,7 @@ status: done wave: 6 depends_on: ["07-lint-cleanup"] plan: "plan.md" -spec: "../../specs/tasks/multi-branch/spec.md" +spec: "../../specs/tasks/requirements/multi-branch.md" --- # Task 08: Reduce Remote popover function size diff --git a/docs/plans/mark-selected-task-repositories/task-09-compact-selected-marker.md b/docs/plans/mark-selected-task-repositories/task-09-compact-selected-marker.md index 0c548af6b9..32e50107cd 100644 --- a/docs/plans/mark-selected-task-repositories/task-09-compact-selected-marker.md +++ b/docs/plans/mark-selected-task-repositories/task-09-compact-selected-marker.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/multi-branch/spec.md +spec: docs/specs/tasks/requirements/multi-branch.md created: 2026-07-23 status: completed --- diff --git a/docs/plans/mcp-task-agent-profile-default/plan.md b/docs/plans/mcp-task-agent-profile-default/plan.md index 0620b86692..853deb88c5 100644 --- a/docs/plans/mcp-task-agent-profile-default/plan.md +++ b/docs/plans/mcp-task-agent-profile-default/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/mcp-task-agent-profile-default/spec.md +spec: docs/specs/tasks/requirements/mcp-task-agent-profile-default.md created: 2026-07-19 status: complete --- diff --git a/docs/plans/mcp-task-agent-profile-default/task-01-backend-preference-contract.md b/docs/plans/mcp-task-agent-profile-default/task-01-backend-preference-contract.md index eeb2373985..127dd86c76 100644 --- a/docs/plans/mcp-task-agent-profile-default/task-01-backend-preference-contract.md +++ b/docs/plans/mcp-task-agent-profile-default/task-01-backend-preference-contract.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/mcp-task-agent-profile-default/spec.md" +spec: "../../specs/tasks/requirements/mcp-task-agent-profile-default.md" --- # Task 01: Backend Preference Contract diff --git a/docs/plans/mcp-task-agent-profile-default/task-02-mcp-profile-resolution.md b/docs/plans/mcp-task-agent-profile-default/task-02-mcp-profile-resolution.md index 9f7204548a..50d85feb31 100644 --- a/docs/plans/mcp-task-agent-profile-default/task-02-mcp-profile-resolution.md +++ b/docs/plans/mcp-task-agent-profile-default/task-02-mcp-profile-resolution.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-backend-preference-contract"] plan: "plan.md" -spec: "../../specs/tasks/mcp-task-agent-profile-default/spec.md" +spec: "../../specs/tasks/requirements/mcp-task-agent-profile-default.md" --- # Task 02: MCP Profile Resolution diff --git a/docs/plans/mcp-task-agent-profile-default/task-03-responsive-task-actions-setting.md b/docs/plans/mcp-task-agent-profile-default/task-03-responsive-task-actions-setting.md index 95497ef0a6..9854f3c947 100644 --- a/docs/plans/mcp-task-agent-profile-default/task-03-responsive-task-actions-setting.md +++ b/docs/plans/mcp-task-agent-profile-default/task-03-responsive-task-actions-setting.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-backend-preference-contract"] plan: "plan.md" -spec: "../../specs/tasks/mcp-task-agent-profile-default/spec.md" +spec: "../../specs/tasks/requirements/mcp-task-agent-profile-default.md" --- # Task 03: Responsive Task Actions Setting diff --git a/docs/plans/mcp-task-agent-profile-default/task-04-desktop-mobile-e2e.md b/docs/plans/mcp-task-agent-profile-default/task-04-desktop-mobile-e2e.md index 9035e59946..dd8df0650d 100644 --- a/docs/plans/mcp-task-agent-profile-default/task-04-desktop-mobile-e2e.md +++ b/docs/plans/mcp-task-agent-profile-default/task-04-desktop-mobile-e2e.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-mcp-profile-resolution", "03-responsive-task-actions-setting"] plan: "plan.md" -spec: "../../specs/tasks/mcp-task-agent-profile-default/spec.md" +spec: "../../specs/tasks/requirements/mcp-task-agent-profile-default.md" --- # Task 04: Desktop and Mobile E2E diff --git a/docs/plans/mcp-task-agent-profile-default/task-05-public-documentation.md b/docs/plans/mcp-task-agent-profile-default/task-05-public-documentation.md index 695a02e30a..5ae266cec8 100644 --- a/docs/plans/mcp-task-agent-profile-default/task-05-public-documentation.md +++ b/docs/plans/mcp-task-agent-profile-default/task-05-public-documentation.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-mcp-profile-resolution", "03-responsive-task-actions-setting"] plan: "plan.md" -spec: "../../specs/tasks/mcp-task-agent-profile-default/spec.md" +spec: "../../specs/tasks/requirements/mcp-task-agent-profile-default.md" --- # Task 05: Public Documentation diff --git a/docs/plans/multi-pr-review-selector/plan.md b/docs/plans/multi-pr-review-selector/plan.md index b65f8c953f..c9a1b108c6 100644 --- a/docs/plans/multi-pr-review-selector/plan.md +++ b/docs/plans/multi-pr-review-selector/plan.md @@ -1,6 +1,6 @@ # Multi-PR Review selector -**Spec:** [Multi-branch tasks](../../specs/tasks/multi-branch/spec.md) +**Spec:** [Multi-branch tasks](../../specs/tasks/requirements/multi-branch.md) ## Outcome diff --git a/docs/plans/multi-repo-chat-file-links/plan.md b/docs/plans/multi-repo-chat-file-links/plan.md index 17044098aa..66d608c54e 100644 --- a/docs/plans/multi-repo-chat-file-links/plan.md +++ b/docs/plans/multi-repo-chat-file-links/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/attach-workspace-sources.md +spec: docs/specs/tasks/system-design/attach-workspace-sources.md decision: docs/decisions/2026-07-22-runtime-mutable-task-workspace-sources.md created: 2026-08-01 status: completed diff --git a/docs/plans/multi-repo-chat-file-links/task-01-expose-authoritative-workspace-root.md b/docs/plans/multi-repo-chat-file-links/task-01-expose-authoritative-workspace-root.md index 4dea42b079..2a3e11facc 100644 --- a/docs/plans/multi-repo-chat-file-links/task-01-expose-authoritative-workspace-root.md +++ b/docs/plans/multi-repo-chat-file-links/task-01-expose-authoritative-workspace-root.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 01: Expose the authoritative workspace root diff --git a/docs/plans/multi-repo-chat-file-links/task-02-resolve-chat-links-from-workspace-root.md b/docs/plans/multi-repo-chat-file-links/task-02-resolve-chat-links-from-workspace-root.md index ce91891aa3..74d1dd8513 100644 --- a/docs/plans/multi-repo-chat-file-links/task-02-resolve-chat-links-from-workspace-root.md +++ b/docs/plans/multi-repo-chat-file-links/task-02-resolve-chat-links-from-workspace-root.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-expose-authoritative-workspace-root"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 02: Resolve chat links from the workspace root diff --git a/docs/plans/owned-link-target-mismatch-repair/plan.md b/docs/plans/owned-link-target-mismatch-repair/plan.md index 6c00c44945..854ca8f093 100644 --- a/docs/plans/owned-link-target-mismatch-repair/plan.md +++ b/docs/plans/owned-link-target-mismatch-repair/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/attach-workspace-sources.md +spec: docs/specs/tasks/system-design/attach-workspace-sources.md created: 2026-08-04 status: completed --- @@ -136,7 +136,7 @@ and `materializeWorktreeSources` (`:569-574`) append an entry to the `created` s `rollbackHostWorkspaceMaterialization` (`:229-244`) `os.Remove`s every `created` entry (`:231-233`) on a later failure (rescan/persist/adopt at `:134,:139,:145`). So a failed submission **deletes** a link that existed before the attachment instead of restoring its prior target — violating the spec's -atomicity guarantee (`docs/specs/tasks/attach-workspace-sources.md:40-43`,`:51`,`:62`). The two +atomicity guarantee (`docs/specs/tasks/system-design/attach-workspace-sources.md:40-43`,`:51`,`:62`). The two reconcile call sites ignore the return values, so the rollback bug is confined to the two `materialize*` sites. diff --git a/docs/plans/owned-link-target-mismatch-repair/task-01-task-unique-root-name.md b/docs/plans/owned-link-target-mismatch-repair/task-01-task-unique-root-name.md index fa4959f0b5..b308859af9 100644 --- a/docs/plans/owned-link-target-mismatch-repair/task-01-task-unique-root-name.md +++ b/docs/plans/owned-link-target-mismatch-repair/task-01-task-unique-root-name.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" parallelism: sequential --- diff --git a/docs/plans/owned-link-target-mismatch-repair/task-02-owned-link-self-heal.md b/docs/plans/owned-link-target-mismatch-repair/task-02-owned-link-self-heal.md index 688d05cda7..02b54c7f90 100644 --- a/docs/plans/owned-link-target-mismatch-repair/task-02-owned-link-self-heal.md +++ b/docs/plans/owned-link-target-mismatch-repair/task-02-owned-link-self-heal.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-task-unique-root-name"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" parallelism: sequential --- diff --git a/docs/plans/owned-link-target-mismatch-repair/task-03-ownership-aware-repoint.md b/docs/plans/owned-link-target-mismatch-repair/task-03-ownership-aware-repoint.md index c0395005a8..9c9d9b6d0f 100644 --- a/docs/plans/owned-link-target-mismatch-repair/task-03-ownership-aware-repoint.md +++ b/docs/plans/owned-link-target-mismatch-repair/task-03-ownership-aware-repoint.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-owned-link-self-heal"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" parallelism: sequential --- diff --git a/docs/plans/owned-link-target-mismatch-repair/task-04-rollback-faithful-repoint.md b/docs/plans/owned-link-target-mismatch-repair/task-04-rollback-faithful-repoint.md index fce4b29702..a68cc20929 100644 --- a/docs/plans/owned-link-target-mismatch-repair/task-04-rollback-faithful-repoint.md +++ b/docs/plans/owned-link-target-mismatch-repair/task-04-rollback-faithful-repoint.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["03-ownership-aware-repoint"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" parallelism: sequential --- @@ -64,7 +64,7 @@ Task 03 (defines the `EnsureOwnedDirectoryLink` result struct with `PriorTarget` ## Inputs -- Spec: strengthened atomicity behavior (`docs/specs/tasks/attach-workspace-sources.md:40-43`) and the +- Spec: strengthened atomicity behavior (`docs/specs/tasks/system-design/attach-workspace-sources.md:40-43`) and the failure-mode row for restoring a repointed pre-existing link on a failed submission. Cancel/intact guarantees at `:51` and `:62`. - Plan: PR #2253 review remediation — Finding 2 (confirmed, most serious). diff --git a/docs/plans/owned-link-target-mismatch-repair/task-05-safe-atomic-link-replacement.md b/docs/plans/owned-link-target-mismatch-repair/task-05-safe-atomic-link-replacement.md index fb0bf05761..6e491e4cb9 100644 --- a/docs/plans/owned-link-target-mismatch-repair/task-05-safe-atomic-link-replacement.md +++ b/docs/plans/owned-link-target-mismatch-repair/task-05-safe-atomic-link-replacement.md @@ -5,7 +5,7 @@ status: done wave: 5 depends_on: ["04-rollback-faithful-repoint"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" parallelism: sequential --- diff --git a/docs/plans/parent-child-task-stop/plan.md b/docs/plans/parent-child-task-stop/plan.md index 4ecc41b7e7..76378f02c6 100644 --- a/docs/plans/parent-child-task-stop/plan.md +++ b/docs/plans/parent-child-task-stop/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/parent-child-task-stop.md +spec: docs/specs/tasks/requirements/parent-child-task-stop.md created: 2026-07-19 status: complete --- diff --git a/docs/plans/parent-child-task-stop/task-01-execution-stop-semantics.md b/docs/plans/parent-child-task-stop/task-01-execution-stop-semantics.md index 41ba1a1a37..caf2df070e 100644 --- a/docs/plans/parent-child-task-stop/task-01-execution-stop-semantics.md +++ b/docs/plans/parent-child-task-stop/task-01-execution-stop-semantics.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/parent-child-task-stop.md" +spec: "../../specs/tasks/requirements/parent-child-task-stop.md" --- # Task 01: Execution stop outcomes diff --git a/docs/plans/parent-child-task-stop/task-02-coordinator-stop-operation.md b/docs/plans/parent-child-task-stop/task-02-coordinator-stop-operation.md index 374a1add9b..c32a5e8e42 100644 --- a/docs/plans/parent-child-task-stop/task-02-coordinator-stop-operation.md +++ b/docs/plans/parent-child-task-stop/task-02-coordinator-stop-operation.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-execution-stop-semantics"] plan: "plan.md" -spec: "../../specs/tasks/parent-child-task-stop.md" +spec: "../../specs/tasks/requirements/parent-child-task-stop.md" --- # Task 02: Coordinator stop operation diff --git a/docs/plans/parent-child-task-stop/task-03-mcp-stop-handler.md b/docs/plans/parent-child-task-stop/task-03-mcp-stop-handler.md index 00aa3a8157..842daf67aa 100644 --- a/docs/plans/parent-child-task-stop/task-03-mcp-stop-handler.md +++ b/docs/plans/parent-child-task-stop/task-03-mcp-stop-handler.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-coordinator-stop-operation"] plan: "plan.md" -spec: "../../specs/tasks/parent-child-task-stop.md" +spec: "../../specs/tasks/requirements/parent-child-task-stop.md" --- # Task 03: MCP stop handler diff --git a/docs/plans/parent-child-task-stop/task-04-task-mcp-tool.md b/docs/plans/parent-child-task-stop/task-04-task-mcp-tool.md index 45ef413883..2f2b1156ea 100644 --- a/docs/plans/parent-child-task-stop/task-04-task-mcp-tool.md +++ b/docs/plans/parent-child-task-stop/task-04-task-mcp-tool.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["03-mcp-stop-handler"] plan: "plan.md" -spec: "../../specs/tasks/parent-child-task-stop.md" +spec: "../../specs/tasks/requirements/parent-child-task-stop.md" --- # Task 04: Task MCP stop tool diff --git a/docs/plans/parent-child-task-stop/task-05-integration-regression.md b/docs/plans/parent-child-task-stop/task-05-integration-regression.md index 6c44e7a7f1..1b353797b0 100644 --- a/docs/plans/parent-child-task-stop/task-05-integration-regression.md +++ b/docs/plans/parent-child-task-stop/task-05-integration-regression.md @@ -11,7 +11,7 @@ depends_on: "04-task-mcp-tool", ] plan: "plan.md" -spec: "../../specs/tasks/parent-child-task-stop.md" +spec: "../../specs/tasks/requirements/parent-child-task-stop.md" --- # Task 05: Integrated stop regression diff --git a/docs/plans/parent-child-task-stop/task-06-public-docs.md b/docs/plans/parent-child-task-stop/task-06-public-docs.md index 518863a32f..848f28f488 100644 --- a/docs/plans/parent-child-task-stop/task-06-public-docs.md +++ b/docs/plans/parent-child-task-stop/task-06-public-docs.md @@ -5,7 +5,7 @@ status: done wave: 5 depends_on: ["03-mcp-stop-handler", "04-task-mcp-tool"] plan: "plan.md" -spec: "../../specs/tasks/parent-child-task-stop.md" +spec: "../../specs/tasks/requirements/parent-child-task-stop.md" --- # Task 06: Public stop documentation diff --git a/docs/plans/prevent-agent-autostart-on-open/plan.md b/docs/plans/prevent-agent-autostart-on-open/plan.md index 9471d461b1..ed228d0fb3 100644 --- a/docs/plans/prevent-agent-autostart-on-open/plan.md +++ b/docs/plans/prevent-agent-autostart-on-open/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/prevent-agent-autostart-on-open/spec.md +spec: docs/specs/tasks/requirements/prevent-agent-autostart-on-open.md created: 2026-08-11 status: complete --- diff --git a/docs/plans/prevent-agent-autostart-on-open/task-01-backend-settings-field.md b/docs/plans/prevent-agent-autostart-on-open/task-01-backend-settings-field.md index 488a01d336..013fef34be 100644 --- a/docs/plans/prevent-agent-autostart-on-open/task-01-backend-settings-field.md +++ b/docs/plans/prevent-agent-autostart-on-open/task-01-backend-settings-field.md @@ -6,7 +6,7 @@ wave: 1 parallelism: sequential depends_on: [] plan: "plan.md" -spec: "../../specs/prevent-agent-autostart-on-open/spec.md" +spec: "../../specs/tasks/requirements/prevent-agent-autostart-on-open.md" --- # Task 01: Backend user-settings field diff --git a/docs/plans/prevent-agent-autostart-on-open/task-02-backend-ensure-override.md b/docs/plans/prevent-agent-autostart-on-open/task-02-backend-ensure-override.md index b9212074d7..19ed743ee1 100644 --- a/docs/plans/prevent-agent-autostart-on-open/task-02-backend-ensure-override.md +++ b/docs/plans/prevent-agent-autostart-on-open/task-02-backend-ensure-override.md @@ -6,7 +6,7 @@ wave: 1 parallelism: sequential depends_on: [] plan: "plan.md" -spec: "../../specs/prevent-agent-autostart-on-open/spec.md" +spec: "../../specs/tasks/requirements/prevent-agent-autostart-on-open.md" --- # Task 02: Backend `session.ensure` `auto_start` override diff --git a/docs/plans/prevent-agent-autostart-on-open/task-03-frontend-settings-plumbing.md b/docs/plans/prevent-agent-autostart-on-open/task-03-frontend-settings-plumbing.md index 95f204e221..7266c537c0 100644 --- a/docs/plans/prevent-agent-autostart-on-open/task-03-frontend-settings-plumbing.md +++ b/docs/plans/prevent-agent-autostart-on-open/task-03-frontend-settings-plumbing.md @@ -6,7 +6,7 @@ wave: 1 parallelism: sequential depends_on: ["01-backend-settings-field"] plan: "plan.md" -spec: "../../specs/prevent-agent-autostart-on-open/spec.md" +spec: "../../specs/tasks/requirements/prevent-agent-autostart-on-open.md" --- # Task 03: Frontend settings plumbing diff --git a/docs/plans/prevent-agent-autostart-on-open/task-04-settings-ui-card.md b/docs/plans/prevent-agent-autostart-on-open/task-04-settings-ui-card.md index 612f7e2bcc..ac1d7e8f6f 100644 --- a/docs/plans/prevent-agent-autostart-on-open/task-04-settings-ui-card.md +++ b/docs/plans/prevent-agent-autostart-on-open/task-04-settings-ui-card.md @@ -6,7 +6,7 @@ wave: 1 parallelism: sequential depends_on: ["03-frontend-settings-plumbing"] plan: "plan.md" -spec: "../../specs/prevent-agent-autostart-on-open/spec.md" +spec: "../../specs/tasks/requirements/prevent-agent-autostart-on-open.md" --- # Task 04: Settings UI card diff --git a/docs/plans/prevent-agent-autostart-on-open/task-05-open-time-gates.md b/docs/plans/prevent-agent-autostart-on-open/task-05-open-time-gates.md index cf9b7aa51b..18cfb9c889 100644 --- a/docs/plans/prevent-agent-autostart-on-open/task-05-open-time-gates.md +++ b/docs/plans/prevent-agent-autostart-on-open/task-05-open-time-gates.md @@ -6,7 +6,7 @@ wave: 1 parallelism: sequential depends_on: ["03-frontend-settings-plumbing"] plan: "plan.md" -spec: "../../specs/prevent-agent-autostart-on-open/spec.md" +spec: "../../specs/tasks/requirements/prevent-agent-autostart-on-open.md" --- # Task 05: Open-time gates in ensure and resume hooks diff --git a/docs/plans/prevent-agent-autostart-on-open/task-06-e2e.md b/docs/plans/prevent-agent-autostart-on-open/task-06-e2e.md index 2184755ccf..793878d795 100644 --- a/docs/plans/prevent-agent-autostart-on-open/task-06-e2e.md +++ b/docs/plans/prevent-agent-autostart-on-open/task-06-e2e.md @@ -6,7 +6,7 @@ wave: 2 parallelism: sequential depends_on: ["05-open-time-gates"] plan: "plan.md" -spec: "../../specs/prevent-agent-autostart-on-open/spec.md" +spec: "../../specs/tasks/requirements/prevent-agent-autostart-on-open.md" --- # Task 06: E2E — prevent auto-start on open diff --git a/docs/plans/prompt-attachments/plan.md b/docs/plans/prompt-attachments/plan.md index 4be24aea1f..17860cd477 100644 --- a/docs/plans/prompt-attachments/plan.md +++ b/docs/plans/prompt-attachments/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/prompt-attachments.md +spec: docs/specs/tasks/requirements/prompt-attachments.md created: 2026-08-04 status: in_progress --- diff --git a/docs/plans/prompt-attachments/task-01-attachment-storage-api.md b/docs/plans/prompt-attachments/task-01-attachment-storage-api.md index f2c8018ed0..4850f0fb3f 100644 --- a/docs/plans/prompt-attachments/task-01-attachment-storage-api.md +++ b/docs/plans/prompt-attachments/task-01-attachment-storage-api.md @@ -5,7 +5,7 @@ status: completed wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/prompt-attachments.md" +spec: "../../specs/tasks/requirements/prompt-attachments.md" --- # Task 01: Attachment storage API diff --git a/docs/plans/prompt-attachments/task-02-agent-delivery-and-queue.md b/docs/plans/prompt-attachments/task-02-agent-delivery-and-queue.md index c368da5ad6..e623b69527 100644 --- a/docs/plans/prompt-attachments/task-02-agent-delivery-and-queue.md +++ b/docs/plans/prompt-attachments/task-02-agent-delivery-and-queue.md @@ -5,7 +5,7 @@ status: completed wave: 2 depends_on: ["01-attachment-storage-api"] plan: "plan.md" -spec: "../../specs/tasks/prompt-attachments.md" +spec: "../../specs/tasks/requirements/prompt-attachments.md" --- # Task 02: Agent attachment delivery diff --git a/docs/plans/prompt-attachments/task-03-frontend-staged-uploads.md b/docs/plans/prompt-attachments/task-03-frontend-staged-uploads.md index b0a3bea523..c3e687bf0c 100644 --- a/docs/plans/prompt-attachments/task-03-frontend-staged-uploads.md +++ b/docs/plans/prompt-attachments/task-03-frontend-staged-uploads.md @@ -5,7 +5,7 @@ status: completed wave: 3 depends_on: ["01-attachment-storage-api", "02-agent-delivery-and-queue"] plan: "plan.md" -spec: "../../specs/tasks/prompt-attachments.md" +spec: "../../specs/tasks/requirements/prompt-attachments.md" --- # Task 03: Frontend staged uploads diff --git a/docs/plans/prompt-attachments/task-04-attachment-e2e.md b/docs/plans/prompt-attachments/task-04-attachment-e2e.md index ac004bff80..0cbd62c20b 100644 --- a/docs/plans/prompt-attachments/task-04-attachment-e2e.md +++ b/docs/plans/prompt-attachments/task-04-attachment-e2e.md @@ -5,7 +5,7 @@ status: pending wave: 4 depends_on: ["03-frontend-staged-uploads"] plan: "plan.md" -spec: "../../specs/tasks/prompt-attachments.md" +spec: "../../specs/tasks/requirements/prompt-attachments.md" --- # Task 04: Attachment E2E coverage diff --git a/docs/plans/prompt-attachments/task-05-public-docs.md b/docs/plans/prompt-attachments/task-05-public-docs.md index deb111c371..48386dbf61 100644 --- a/docs/plans/prompt-attachments/task-05-public-docs.md +++ b/docs/plans/prompt-attachments/task-05-public-docs.md @@ -5,7 +5,7 @@ status: completed wave: 4 depends_on: ["03-frontend-staged-uploads"] plan: "plan.md" -spec: "../../specs/tasks/prompt-attachments.md" +spec: "../../specs/tasks/requirements/prompt-attachments.md" --- # Task 05: Prompt attachment documentation @@ -25,7 +25,7 @@ spec: "../../specs/tasks/prompt-attachments.md" ## Verification ```bash -rg -n "10 MB|10 MiB|20 MB|20 MiB|32 MiB|100 MiB|attachment" docs/public/tasks-and-workflows.md docs/public/websocket-api.md docs/specs/office/live-updates.md docs/specs/tasks/prompt-attachments.md docs/decisions/2026-08-04-file-backed-prompt-attachments.md +rg -n "10 MB|10 MiB|20 MB|20 MiB|32 MiB|100 MiB|attachment" docs/public/tasks-and-workflows.md docs/public/websocket-api.md docs/specs/office/live-updates.md docs/specs/tasks/requirements/prompt-attachments.md docs/decisions/2026-08-04-file-backed-prompt-attachments.md node --test scripts/validate-public-docs.test.mjs node scripts/validate-public-docs.mjs ``` @@ -35,7 +35,7 @@ node scripts/validate-public-docs.mjs - `docs/public/tasks-and-workflows.md` - `docs/public/websocket-api.md` - `docs/specs/office/live-updates.md` -- `docs/specs/tasks/prompt-attachments.md` only if implementation reveals a +- `docs/specs/tasks/requirements/prompt-attachments.md` only if implementation reveals a confirmed contract correction ## Dependencies diff --git a/docs/plans/queued-message-attachment-preview/plan.md b/docs/plans/queued-message-attachment-preview/plan.md index 4f31162bf2..dbfdc1e2d4 100644 --- a/docs/plans/queued-message-attachment-preview/plan.md +++ b/docs/plans/queued-message-attachment-preview/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/prompt-attachments.md +spec: docs/specs/tasks/requirements/prompt-attachments.md created: 2026-08-13 status: completed --- diff --git a/docs/plans/queued-message-attachment-preview/task-01-queue-attachment-rendering.md b/docs/plans/queued-message-attachment-preview/task-01-queue-attachment-rendering.md index d1950ad96e..8e72639b8c 100644 --- a/docs/plans/queued-message-attachment-preview/task-01-queue-attachment-rendering.md +++ b/docs/plans/queued-message-attachment-preview/task-01-queue-attachment-rendering.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/prompt-attachments.md" +spec: "../../specs/tasks/requirements/prompt-attachments.md" --- # Task 01: Render queued attachment IDs @@ -48,7 +48,7 @@ contract. ## Inputs -- `docs/specs/tasks/prompt-attachments.md`, queued preview scenario +- `docs/specs/tasks/requirements/prompt-attachments.md`, queued preview scenario - `docs/plans/queued-message-attachment-preview/plan.md` - Existing source resolution in `apps/web/components/task/chat/messages/chat-message.tsx` diff --git a/docs/plans/queued-task-moves/plan.md b/docs/plans/queued-task-moves/plan.md index 937fabf68d..828b6226ea 100644 --- a/docs/plans/queued-task-moves/plan.md +++ b/docs/plans/queued-task-moves/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/wip-limit-pull-system.md +spec: docs/specs/tasks/system-design/wip-limit-pull-system.md created: 2026-08-12 status: completed --- diff --git a/docs/plans/queued-task-moves/task-01-atomic-move-admission.md b/docs/plans/queued-task-moves/task-01-atomic-move-admission.md index f6d2767491..c23e161a2d 100644 --- a/docs/plans/queued-task-moves/task-01-atomic-move-admission.md +++ b/docs/plans/queued-task-moves/task-01-atomic-move-admission.md @@ -5,7 +5,7 @@ status: completed wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 01: Add Atomic Move Admission diff --git a/docs/plans/queued-task-moves/task-02-deferred-destination-lifecycle.md b/docs/plans/queued-task-moves/task-02-deferred-destination-lifecycle.md index 2d0dbb6f77..aafb52bb28 100644 --- a/docs/plans/queued-task-moves/task-02-deferred-destination-lifecycle.md +++ b/docs/plans/queued-task-moves/task-02-deferred-destination-lifecycle.md @@ -5,7 +5,7 @@ status: completed wave: 2 depends_on: ["01-atomic-move-admission"] plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 02: Defer Destination Lifecycle Until Promotion diff --git a/docs/plans/queued-task-moves/task-03-kanban-queue-sections.md b/docs/plans/queued-task-moves/task-03-kanban-queue-sections.md index 62147263b7..eac82b56c3 100644 --- a/docs/plans/queued-task-moves/task-03-kanban-queue-sections.md +++ b/docs/plans/queued-task-moves/task-03-kanban-queue-sections.md @@ -5,7 +5,7 @@ status: completed wave: 3 depends_on: ["02-deferred-destination-lifecycle"] plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 03: Add Frontend Queue Guidance diff --git a/docs/plans/queued-task-moves/task-04-sidebar-queue-position.md b/docs/plans/queued-task-moves/task-04-sidebar-queue-position.md index 3acd371f08..ec2c3fbb9f 100644 --- a/docs/plans/queued-task-moves/task-04-sidebar-queue-position.md +++ b/docs/plans/queued-task-moves/task-04-sidebar-queue-position.md @@ -5,7 +5,7 @@ status: completed wave: 4 depends_on: ["03-kanban-queue-sections"] plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 04: Show Sidebar Queue Position diff --git a/docs/plans/queued-task-moves/task-05-e2e-and-documentation.md b/docs/plans/queued-task-moves/task-05-e2e-and-documentation.md index 65e6b4e1e8..fc7f7fbe75 100644 --- a/docs/plans/queued-task-moves/task-05-e2e-and-documentation.md +++ b/docs/plans/queued-task-moves/task-05-e2e-and-documentation.md @@ -9,7 +9,7 @@ depends_on: - "03-kanban-queue-sections" - "04-sidebar-queue-position" plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 05: Complete Feature Verification diff --git a/docs/plans/quick-chat-expiration/plan.md b/docs/plans/quick-chat-expiration/plan.md index da6a6dc709..e97fc4954b 100644 --- a/docs/plans/quick-chat-expiration/plan.md +++ b/docs/plans/quick-chat-expiration/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/quick-chat-expiration.md +spec: docs/specs/tasks/requirements/quick-chat-expiration.md created: 2026-07-03 status: implemented --- diff --git a/docs/plans/quick-chat-expiration/task-01-backend-candidate-query.md b/docs/plans/quick-chat-expiration/task-01-backend-candidate-query.md index c4311dd206..cccd4cd28e 100644 --- a/docs/plans/quick-chat-expiration/task-01-backend-candidate-query.md +++ b/docs/plans/quick-chat-expiration/task-01-backend-candidate-query.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/quick-chat-expiration.md" +spec: "../../specs/tasks/requirements/quick-chat-expiration.md" --- # Task 01: Backend Candidate Query diff --git a/docs/plans/quick-chat-expiration/task-02-backend-expiration-service.md b/docs/plans/quick-chat-expiration/task-02-backend-expiration-service.md index 066f549954..b4f670b412 100644 --- a/docs/plans/quick-chat-expiration/task-02-backend-expiration-service.md +++ b/docs/plans/quick-chat-expiration/task-02-backend-expiration-service.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-backend-candidate-query"] plan: "plan.md" -spec: "../../specs/tasks/quick-chat-expiration.md" +spec: "../../specs/tasks/requirements/quick-chat-expiration.md" --- # Task 02: Backend Expiration Service diff --git a/docs/plans/quick-chat-expiration/task-03-boot-hydration.md b/docs/plans/quick-chat-expiration/task-03-boot-hydration.md index c3c9b79265..00be7f9f3e 100644 --- a/docs/plans/quick-chat-expiration/task-03-boot-hydration.md +++ b/docs/plans/quick-chat-expiration/task-03-boot-hydration.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/quick-chat-expiration.md" +spec: "../../specs/tasks/requirements/quick-chat-expiration.md" --- # Task 03: Boot Hydration diff --git a/docs/plans/quick-chat-expiration/task-04-e2e-and-verification.md b/docs/plans/quick-chat-expiration/task-04-e2e-and-verification.md index 4c83af4879..3df7251b9f 100644 --- a/docs/plans/quick-chat-expiration/task-04-e2e-and-verification.md +++ b/docs/plans/quick-chat-expiration/task-04-e2e-and-verification.md @@ -5,7 +5,7 @@ status: pending wave: 3 depends_on: ["02-backend-expiration-service", "03-boot-hydration"] plan: "plan.md" -spec: "../../specs/tasks/quick-chat-expiration.md" +spec: "../../specs/tasks/requirements/quick-chat-expiration.md" --- # Task 04: E2E and Verification diff --git a/docs/plans/quick-chat-repository-context/plan.md b/docs/plans/quick-chat-repository-context/plan.md index 233bd169c5..962d9560bb 100644 --- a/docs/plans/quick-chat-repository-context/plan.md +++ b/docs/plans/quick-chat-repository-context/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/quick-chat-repository-context.md +spec: docs/specs/tasks/requirements/quick-chat-repository-context.md created: 2026-07-14 status: complete --- diff --git a/docs/plans/quick-chat-repository-context/task-01-backend-contract.md b/docs/plans/quick-chat-repository-context/task-01-backend-contract.md index d7af371d16..3951677505 100644 --- a/docs/plans/quick-chat-repository-context/task-01-backend-contract.md +++ b/docs/plans/quick-chat-repository-context/task-01-backend-contract.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/quick-chat-repository-context.md" +spec: "../../specs/tasks/requirements/quick-chat-repository-context.md" --- # Task 01: Backend quick-chat repository contract diff --git a/docs/plans/quick-chat-repository-context/task-02-shared-repository-chips.md b/docs/plans/quick-chat-repository-context/task-02-shared-repository-chips.md index bd3db1c9cb..cd71c8ac0b 100644 --- a/docs/plans/quick-chat-repository-context/task-02-shared-repository-chips.md +++ b/docs/plans/quick-chat-repository-context/task-02-shared-repository-chips.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/quick-chat-repository-context.md" +spec: "../../specs/tasks/requirements/quick-chat-repository-context.md" --- # Task 02: Shared controlled repository chips diff --git a/docs/plans/quick-chat-repository-context/task-03-quick-chat-setup.md b/docs/plans/quick-chat-repository-context/task-03-quick-chat-setup.md index ff9e8eb6e3..23a6006e66 100644 --- a/docs/plans/quick-chat-repository-context/task-03-quick-chat-setup.md +++ b/docs/plans/quick-chat-repository-context/task-03-quick-chat-setup.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-backend-contract", "02-shared-repository-chips"] plan: "plan.md" -spec: "../../specs/tasks/quick-chat-repository-context.md" +spec: "../../specs/tasks/requirements/quick-chat-repository-context.md" --- # Task 03: Responsive quick-chat setup diff --git a/docs/plans/quick-chat-repository-context/task-04-e2e-verification.md b/docs/plans/quick-chat-repository-context/task-04-e2e-verification.md index 56e6beb0dc..0c60024efd 100644 --- a/docs/plans/quick-chat-repository-context/task-04-e2e-verification.md +++ b/docs/plans/quick-chat-repository-context/task-04-e2e-verification.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["03-quick-chat-setup"] plan: "plan.md" -spec: "../../specs/tasks/quick-chat-repository-context.md" +spec: "../../specs/tasks/requirements/quick-chat-repository-context.md" --- # Task 04: Quick-chat repository E2E verification diff --git a/docs/plans/remote-contribution-head-drift/plan.md b/docs/plans/remote-contribution-head-drift/plan.md index fb67ebff1c..2807d0ca95 100644 --- a/docs/plans/remote-contribution-head-drift/plan.md +++ b/docs/plans/remote-contribution-head-drift/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/remote-contribution-tasks.md +spec: docs/specs/tasks/system-design/remote-contribution-tasks.md created: 2026-08-10 status: approved --- @@ -21,7 +21,7 @@ show an inline warning and two separately titled commit lists. No observation au rebases, merges, or deletes local work. This plan implements the amended -[Remote Contribution Tasks spec](../../specs/tasks/remote-contribution-tasks.md) and +[Remote Contribution Tasks spec](../../specs/tasks/system-design/remote-contribution-tasks.md) and [ADR-2026-08-10](../../decisions/2026-08-10-remote-contribution-head-drift.md). ## Implementation status diff --git a/docs/plans/remote-contribution-head-drift/task-01-upstream-status-contract.md b/docs/plans/remote-contribution-head-drift/task-01-upstream-status-contract.md index 07331d6509..43d30b4f0b 100644 --- a/docs/plans/remote-contribution-head-drift/task-01-upstream-status-contract.md +++ b/docs/plans/remote-contribution-head-drift/task-01-upstream-status-contract.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 01: Publish Complete Upstream Status Evidence diff --git a/docs/plans/remote-contribution-head-drift/task-02-checkout-relation-and-action-semantics.md b/docs/plans/remote-contribution-head-drift/task-02-checkout-relation-and-action-semantics.md index 1a46d7e836..0a5dd49319 100644 --- a/docs/plans/remote-contribution-head-drift/task-02-checkout-relation-and-action-semantics.md +++ b/docs/plans/remote-contribution-head-drift/task-02-checkout-relation-and-action-semantics.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-upstream-status-contract"] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 02: Classify Checkout Drift and Git Actions diff --git a/docs/plans/remote-contribution-head-drift/task-03-diverged-changes-ui-and-mobile-parity.md b/docs/plans/remote-contribution-head-drift/task-03-diverged-changes-ui-and-mobile-parity.md index 77ad36c4ed..0de62c4c65 100644 --- a/docs/plans/remote-contribution-head-drift/task-03-diverged-changes-ui-and-mobile-parity.md +++ b/docs/plans/remote-contribution-head-drift/task-03-diverged-changes-ui-and-mobile-parity.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-checkout-relation-and-action-semantics"] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 03: Separate Diverged Histories Across Responsive UI diff --git a/docs/plans/remote-contribution-head-drift/task-04-responsive-force-push-regression-e2e.md b/docs/plans/remote-contribution-head-drift/task-04-responsive-force-push-regression-e2e.md index 6ca8901ff4..020ea2fb8b 100644 --- a/docs/plans/remote-contribution-head-drift/task-04-responsive-force-push-regression-e2e.md +++ b/docs/plans/remote-contribution-head-drift/task-04-responsive-force-push-regression-e2e.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["03-diverged-changes-ui-and-mobile-parity"] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 04: Prove Rewritten PR Behavior End to End diff --git a/docs/plans/remote-contribution-history-reconciliation/plan.md b/docs/plans/remote-contribution-history-reconciliation/plan.md index 3ebe64cb6b..67cbd7a708 100644 --- a/docs/plans/remote-contribution-history-reconciliation/plan.md +++ b/docs/plans/remote-contribution-history-reconciliation/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/remote-contribution-tasks.md +spec: docs/specs/tasks/system-design/remote-contribution-tasks.md created: 2026-08-13 status: complete --- diff --git a/docs/plans/remote-contribution-history-reconciliation/task-01-recover-provider-commit-evidence.md b/docs/plans/remote-contribution-history-reconciliation/task-01-recover-provider-commit-evidence.md index 1f01addab6..d4d0d01b61 100644 --- a/docs/plans/remote-contribution-history-reconciliation/task-01-recover-provider-commit-evidence.md +++ b/docs/plans/remote-contribution-history-reconciliation/task-01-recover-provider-commit-evidence.md @@ -5,7 +5,7 @@ status: complete wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 01: Recover Provider Commit Evidence diff --git a/docs/plans/remote-contribution-history-reconciliation/task-02-reconcile-and-style-commit-provenance.md b/docs/plans/remote-contribution-history-reconciliation/task-02-reconcile-and-style-commit-provenance.md index bcbb2f1e33..7f425da8a6 100644 --- a/docs/plans/remote-contribution-history-reconciliation/task-02-reconcile-and-style-commit-provenance.md +++ b/docs/plans/remote-contribution-history-reconciliation/task-02-reconcile-and-style-commit-provenance.md @@ -5,7 +5,7 @@ status: complete wave: 2 depends_on: ["01-recover-provider-commit-evidence"] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 02: Reconcile and Style Commit Provenance diff --git a/docs/plans/remote-contribution-history-reconciliation/task-03-verify-desktop-mobile-reconciliation.md b/docs/plans/remote-contribution-history-reconciliation/task-03-verify-desktop-mobile-reconciliation.md index ce8a6578bc..520e8d4914 100644 --- a/docs/plans/remote-contribution-history-reconciliation/task-03-verify-desktop-mobile-reconciliation.md +++ b/docs/plans/remote-contribution-history-reconciliation/task-03-verify-desktop-mobile-reconciliation.md @@ -5,7 +5,7 @@ status: complete wave: 3 depends_on: ["01-recover-provider-commit-evidence", "02-reconcile-and-style-commit-provenance"] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 03: Verify Desktop and Mobile Reconciliation diff --git a/docs/plans/remote-contribution-local-first/plan.md b/docs/plans/remote-contribution-local-first/plan.md index f2027e023b..fc64334784 100644 --- a/docs/plans/remote-contribution-local-first/plan.md +++ b/docs/plans/remote-contribution-local-first/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/remote-contribution-tasks.md +spec: docs/specs/tasks/system-design/remote-contribution-tasks.md created: 2026-08-12 status: completed --- diff --git a/docs/plans/remote-contribution-local-first/task-01-exact-lease-contribution-operations.md b/docs/plans/remote-contribution-local-first/task-01-exact-lease-contribution-operations.md index ffaaa3f07f..80ab58be42 100644 --- a/docs/plans/remote-contribution-local-first/task-01-exact-lease-contribution-operations.md +++ b/docs/plans/remote-contribution-local-first/task-01-exact-lease-contribution-operations.md @@ -5,7 +5,7 @@ status: completed wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 01: Exact-Lease Contribution Operations diff --git a/docs/plans/remote-contribution-local-first/task-02-local-first-relation-and-shared-actions.md b/docs/plans/remote-contribution-local-first/task-02-local-first-relation-and-shared-actions.md index 62817aa920..2bd4c13682 100644 --- a/docs/plans/remote-contribution-local-first/task-02-local-first-relation-and-shared-actions.md +++ b/docs/plans/remote-contribution-local-first/task-02-local-first-relation-and-shared-actions.md @@ -5,7 +5,7 @@ status: completed wave: 2 depends_on: ["01-exact-lease-contribution-operations"] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 02: Local-First Relation and Shared Actions diff --git a/docs/plans/remote-contribution-local-first/task-03-desktop-local-first-contribution-ui.md b/docs/plans/remote-contribution-local-first/task-03-desktop-local-first-contribution-ui.md index f6576c7f3a..0a611a225b 100644 --- a/docs/plans/remote-contribution-local-first/task-03-desktop-local-first-contribution-ui.md +++ b/docs/plans/remote-contribution-local-first/task-03-desktop-local-first-contribution-ui.md @@ -5,7 +5,7 @@ status: completed wave: 3 depends_on: ["02-local-first-relation-and-shared-actions"] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 03: Desktop Local-First Contribution UI diff --git a/docs/plans/remote-contribution-local-first/task-04-mobile-contribution-version-choices.md b/docs/plans/remote-contribution-local-first/task-04-mobile-contribution-version-choices.md index df77b5a2cd..29a95ebcdd 100644 --- a/docs/plans/remote-contribution-local-first/task-04-mobile-contribution-version-choices.md +++ b/docs/plans/remote-contribution-local-first/task-04-mobile-contribution-version-choices.md @@ -5,7 +5,7 @@ status: completed wave: 3 depends_on: ["02-local-first-relation-and-shared-actions"] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 04: Mobile Contribution Version Choices diff --git a/docs/plans/remote-contribution-local-first/task-05-desktop-mobile-drift-e2e.md b/docs/plans/remote-contribution-local-first/task-05-desktop-mobile-drift-e2e.md index b71d08f5e3..4af3af5046 100644 --- a/docs/plans/remote-contribution-local-first/task-05-desktop-mobile-drift-e2e.md +++ b/docs/plans/remote-contribution-local-first/task-05-desktop-mobile-drift-e2e.md @@ -8,7 +8,7 @@ depends_on: - "03-desktop-local-first-contribution-ui" - "04-mobile-contribution-version-choices" plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 05: Desktop and Mobile Drift E2E diff --git a/docs/plans/remote-contribution-local-first/task-06-branch-scoped-drift-ux-correction.md b/docs/plans/remote-contribution-local-first/task-06-branch-scoped-drift-ux-correction.md index 4ee6976d84..4262714041 100644 --- a/docs/plans/remote-contribution-local-first/task-06-branch-scoped-drift-ux-correction.md +++ b/docs/plans/remote-contribution-local-first/task-06-branch-scoped-drift-ux-correction.md @@ -5,7 +5,7 @@ status: completed wave: 5 depends_on: ["05-desktop-mobile-drift-e2e"] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 06: Branch-Scoped Drift UX Correction diff --git a/docs/plans/remote-contribution-tasks/plan.md b/docs/plans/remote-contribution-tasks/plan.md index 8f1d958c63..f952a3827e 100644 --- a/docs/plans/remote-contribution-tasks/plan.md +++ b/docs/plans/remote-contribution-tasks/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/remote-contribution-tasks.md +spec: docs/specs/tasks/system-design/remote-contribution-tasks.md created: 2026-08-04 status: completed --- diff --git a/docs/plans/remote-contribution-tasks/task-01-provider-contribution-resolution.md b/docs/plans/remote-contribution-tasks/task-01-provider-contribution-resolution.md index 0b76084445..10c4dbae4b 100644 --- a/docs/plans/remote-contribution-tasks/task-01-provider-contribution-resolution.md +++ b/docs/plans/remote-contribution-tasks/task-01-provider-contribution-resolution.md @@ -5,7 +5,7 @@ status: completed wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 01: Provider Contribution Resolution diff --git a/docs/plans/remote-contribution-tasks/task-02-mcp-creation-and-persistence.md b/docs/plans/remote-contribution-tasks/task-02-mcp-creation-and-persistence.md index afd3a6773f..f7441eb873 100644 --- a/docs/plans/remote-contribution-tasks/task-02-mcp-creation-and-persistence.md +++ b/docs/plans/remote-contribution-tasks/task-02-mcp-creation-and-persistence.md @@ -5,7 +5,7 @@ status: completed wave: 2 depends_on: ["01-provider-contribution-resolution"] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 02: MCP Creation and Persistence diff --git a/docs/plans/remote-contribution-tasks/task-03-runtime-contribution-materialization.md b/docs/plans/remote-contribution-tasks/task-03-runtime-contribution-materialization.md index 01be839b09..dde576f975 100644 --- a/docs/plans/remote-contribution-tasks/task-03-runtime-contribution-materialization.md +++ b/docs/plans/remote-contribution-tasks/task-03-runtime-contribution-materialization.md @@ -5,7 +5,7 @@ status: completed wave: 3 depends_on: ["02-mcp-creation-and-persistence"] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 03: Runtime Contribution Materialization diff --git a/docs/plans/remote-contribution-tasks/task-04-credential-and-push-routing.md b/docs/plans/remote-contribution-tasks/task-04-credential-and-push-routing.md index d0a9bf28c4..ec654df01c 100644 --- a/docs/plans/remote-contribution-tasks/task-04-credential-and-push-routing.md +++ b/docs/plans/remote-contribution-tasks/task-04-credential-and-push-routing.md @@ -5,7 +5,7 @@ status: completed wave: 4 depends_on: ["03-runtime-contribution-materialization"] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 04: Credential and Push Routing diff --git a/docs/plans/remote-contribution-tasks/task-05-integration-coverage-and-docs.md b/docs/plans/remote-contribution-tasks/task-05-integration-coverage-and-docs.md index 3d820088d5..9162b524d2 100644 --- a/docs/plans/remote-contribution-tasks/task-05-integration-coverage-and-docs.md +++ b/docs/plans/remote-contribution-tasks/task-05-integration-coverage-and-docs.md @@ -5,7 +5,7 @@ status: completed wave: 5 depends_on: ["04-credential-and-push-routing"] plan: "plan.md" -spec: "../../specs/tasks/remote-contribution-tasks.md" +spec: "../../specs/tasks/system-design/remote-contribution-tasks.md" --- # Task 05: Integration Coverage and Public Docs diff --git a/docs/plans/remote-repository-entry-reliability/plan.md b/docs/plans/remote-repository-entry-reliability/plan.md index f7cec31edc..bf28831fb4 100644 --- a/docs/plans/remote-repository-entry-reliability/plan.md +++ b/docs/plans/remote-repository-entry-reliability/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/multi-branch/spec.md +spec: docs/specs/tasks/requirements/multi-branch.md related_specs: - docs/specs/gitlab-integration/spec.md created: 2026-07-24 diff --git a/docs/plans/remote-repository-entry-reliability/task-01-public-provider-resolution.md b/docs/plans/remote-repository-entry-reliability/task-01-public-provider-resolution.md index 2c406cb035..f855369b92 100644 --- a/docs/plans/remote-repository-entry-reliability/task-01-public-provider-resolution.md +++ b/docs/plans/remote-repository-entry-reliability/task-01-public-provider-resolution.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/multi-branch/spec.md" +spec: "../../specs/tasks/requirements/multi-branch.md" --- # Task 01: Public provider resolution @@ -36,7 +36,7 @@ None. ## Inputs -- `docs/specs/tasks/multi-branch/spec.md` — Remote task-creation scenarios. +- `docs/specs/tasks/requirements/multi-branch.md` — Remote task-creation scenarios. - `docs/specs/gitlab-integration/spec.md` — task-creation anonymous exception. - Existing `listRepoBranchesAnonymous` GitHub implementation and GitLab workspace-client resolution. diff --git a/docs/plans/remote-repository-entry-reliability/task-02-explicit-url-entry.md b/docs/plans/remote-repository-entry-reliability/task-02-explicit-url-entry.md index e2f94e3acf..f277a5c1dd 100644 --- a/docs/plans/remote-repository-entry-reliability/task-02-explicit-url-entry.md +++ b/docs/plans/remote-repository-entry-reliability/task-02-explicit-url-entry.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-public-provider-resolution"] plan: "plan.md" -spec: "../../specs/tasks/multi-branch/spec.md" +spec: "../../specs/tasks/requirements/multi-branch.md" --- # Task 02: Explicit remote URL entry @@ -31,7 +31,7 @@ spec: "../../specs/tasks/multi-branch/spec.md" ## Inputs -- `docs/specs/tasks/multi-branch/spec.md` — explicit-submit scenarios. +- `docs/specs/tasks/requirements/multi-branch.md` — explicit-submit scenarios. - Existing Remote popover input and mobile geometry. ## Output contract diff --git a/docs/plans/remote-repository-entry-reliability/task-03-resolution-errors-and-retry.md b/docs/plans/remote-repository-entry-reliability/task-03-resolution-errors-and-retry.md index ae06e0524b..5b21bd300e 100644 --- a/docs/plans/remote-repository-entry-reliability/task-03-resolution-errors-and-retry.md +++ b/docs/plans/remote-repository-entry-reliability/task-03-resolution-errors-and-retry.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-public-provider-resolution"] plan: "plan.md" -spec: "../../specs/tasks/multi-branch/spec.md" +spec: "../../specs/tasks/requirements/multi-branch.md" --- # Task 03: Remote resolution errors and retry @@ -36,7 +36,7 @@ spec: "../../specs/tasks/multi-branch/spec.md" ## Inputs -- `docs/specs/tasks/multi-branch/spec.md` — failure and retry scenarios. +- `docs/specs/tasks/requirements/multi-branch.md` — failure and retry scenarios. - Existing per-URL abort and sequence guards in both hooks. ## Output contract diff --git a/docs/plans/remote-repository-entry-reliability/task-04-mobile-e2e.md b/docs/plans/remote-repository-entry-reliability/task-04-mobile-e2e.md index 3045349fab..5da78892f4 100644 --- a/docs/plans/remote-repository-entry-reliability/task-04-mobile-e2e.md +++ b/docs/plans/remote-repository-entry-reliability/task-04-mobile-e2e.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-explicit-url-entry", "03-resolution-errors-and-retry"] plan: "plan.md" -spec: "../../specs/tasks/multi-branch/spec.md" +spec: "../../specs/tasks/requirements/multi-branch.md" --- # Task 04: Mobile remote entry coverage diff --git a/docs/plans/restore-live-add-branch/plan.md b/docs/plans/restore-live-add-branch/plan.md index 52884424c6..4d129a9632 100644 --- a/docs/plans/restore-live-add-branch/plan.md +++ b/docs/plans/restore-live-add-branch/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/attach-workspace-sources.md +spec: docs/specs/tasks/system-design/attach-workspace-sources.md decision: docs/decisions/2026-07-27-legacy-add-branch-live-rescan.md created: 2026-07-27 status: completed diff --git a/docs/plans/restore-live-add-branch/task-01-restore-live-materialization.md b/docs/plans/restore-live-add-branch/task-01-restore-live-materialization.md index 2d186efcac..cddbe1592b 100644 --- a/docs/plans/restore-live-add-branch/task-01-restore-live-materialization.md +++ b/docs/plans/restore-live-add-branch/task-01-restore-live-materialization.md @@ -5,7 +5,7 @@ status: completed wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 01: Restore live add-branch materialization diff --git a/docs/plans/restore-live-add-branch/task-02-return-materialized-paths.md b/docs/plans/restore-live-add-branch/task-02-return-materialized-paths.md index 79fef09909..5d2241bf66 100644 --- a/docs/plans/restore-live-add-branch/task-02-return-materialized-paths.md +++ b/docs/plans/restore-live-add-branch/task-02-return-materialized-paths.md @@ -5,7 +5,7 @@ status: completed wave: 2 depends_on: ["01-restore-live-materialization"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 02: Return materialized paths from the MCP tool diff --git a/docs/plans/restore-live-add-branch/task-03-update-public-docs.md b/docs/plans/restore-live-add-branch/task-03-update-public-docs.md index d349937093..d4a59fc793 100644 --- a/docs/plans/restore-live-add-branch/task-03-update-public-docs.md +++ b/docs/plans/restore-live-add-branch/task-03-update-public-docs.md @@ -5,7 +5,7 @@ status: completed wave: 3 depends_on: ["02-return-materialized-paths"] plan: "plan.md" -spec: "../../specs/tasks/attach-workspace-sources.md" +spec: "../../specs/tasks/system-design/attach-workspace-sources.md" --- # Task 03: Document live add-branch compatibility diff --git a/docs/plans/review-watcher-double-autostart/plan.md b/docs/plans/review-watcher-double-autostart/plan.md index 2be635c428..412b4f7373 100644 --- a/docs/plans/review-watcher-double-autostart/plan.md +++ b/docs/plans/review-watcher-double-autostart/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/wip-limit-pull-system.md +spec: docs/specs/tasks/system-design/wip-limit-pull-system.md created: 2026-07-29 status: complete --- diff --git a/docs/plans/review-watcher-double-autostart/task-01-shared-autostart-claim.md b/docs/plans/review-watcher-double-autostart/task-01-shared-autostart-claim.md index 493f0a7692..42be4af8ce 100644 --- a/docs/plans/review-watcher-double-autostart/task-01-shared-autostart-claim.md +++ b/docs/plans/review-watcher-double-autostart/task-01-shared-autostart-claim.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 01: Shared race-safe auto-start claim diff --git a/docs/plans/rich-task-title-previews/plan.md b/docs/plans/rich-task-title-previews/plan.md index f01ad79996..11c21b021b 100644 --- a/docs/plans/rich-task-title-previews/plan.md +++ b/docs/plans/rich-task-title-previews/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/rich-task-title-previews.md +spec: docs/specs/tasks/requirements/rich-task-title-previews.md created: 2026-08-18 status: complete --- diff --git a/docs/plans/rich-task-title-previews/task-01-preview-and-hierarchy.md b/docs/plans/rich-task-title-previews/task-01-preview-and-hierarchy.md index 5d94f0b520..c37d783952 100644 --- a/docs/plans/rich-task-title-previews/task-01-preview-and-hierarchy.md +++ b/docs/plans/rich-task-title-previews/task-01-preview-and-hierarchy.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/rich-task-title-previews.md" +spec: "../../specs/tasks/requirements/rich-task-title-previews.md" --- # Task 01: Preview and hierarchy diff --git a/docs/plans/rich-task-title-previews/task-02-contribution-status-and-state.md b/docs/plans/rich-task-title-previews/task-02-contribution-status-and-state.md index 232db396f4..0a035dd92e 100644 --- a/docs/plans/rich-task-title-previews/task-02-contribution-status-and-state.md +++ b/docs/plans/rich-task-title-previews/task-02-contribution-status-and-state.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/rich-task-title-previews.md" +spec: "../../specs/tasks/requirements/rich-task-title-previews.md" --- # Task 02: Contribution status and state diff --git a/docs/plans/rich-task-title-previews/task-03-end-to-end-verification.md b/docs/plans/rich-task-title-previews/task-03-end-to-end-verification.md index 1429c07657..14a5ce7303 100644 --- a/docs/plans/rich-task-title-previews/task-03-end-to-end-verification.md +++ b/docs/plans/rich-task-title-previews/task-03-end-to-end-verification.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-preview-and-hierarchy", "02-contribution-status-and-state"] plan: "plan.md" -spec: "../../specs/tasks/rich-task-title-previews.md" +spec: "../../specs/tasks/requirements/rich-task-title-previews.md" --- # Task 03: End-to-end verification diff --git a/docs/plans/run-scheduler-lifecycle/plan.md b/docs/plans/run-scheduler-lifecycle/plan.md index a40944eb4b..b84fe28092 100644 --- a/docs/plans/run-scheduler-lifecycle/plan.md +++ b/docs/plans/run-scheduler-lifecycle/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/run-scheduling.md +spec: docs/specs/tasks/requirements/run-scheduling.md created: 2026-08-01 status: done --- diff --git a/docs/plans/run-scheduler-lifecycle/task-01-owned-loop-lifecycles.md b/docs/plans/run-scheduler-lifecycle/task-01-owned-loop-lifecycles.md index 58ebfd5010..7f28a10dff 100644 --- a/docs/plans/run-scheduler-lifecycle/task-01-owned-loop-lifecycles.md +++ b/docs/plans/run-scheduler-lifecycle/task-01-owned-loop-lifecycles.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/run-scheduling.md" +spec: "../../specs/tasks/requirements/run-scheduling.md" --- # Task 01: Own scheduler loop lifecycles diff --git a/docs/plans/run-scheduler-lifecycle/task-02-office-task-scoping.md b/docs/plans/run-scheduler-lifecycle/task-02-office-task-scoping.md index 5a2ff88855..c02d67092c 100644 --- a/docs/plans/run-scheduler-lifecycle/task-02-office-task-scoping.md +++ b/docs/plans/run-scheduler-lifecycle/task-02-office-task-scoping.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/run-scheduling.md" +spec: "../../specs/tasks/requirements/run-scheduling.md" --- # Task 02: Scope autonomous assignment to Office tasks diff --git a/docs/plans/run-scheduler-lifecycle/task-03-office-maintenance-separation.md b/docs/plans/run-scheduler-lifecycle/task-03-office-maintenance-separation.md index fb43b2ac2a..17f791519e 100644 --- a/docs/plans/run-scheduler-lifecycle/task-03-office-maintenance-separation.md +++ b/docs/plans/run-scheduler-lifecycle/task-03-office-maintenance-separation.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-owned-loop-lifecycles", "02-office-task-scoping"] plan: "plan.md" -spec: "../../specs/tasks/run-scheduling.md" +spec: "../../specs/tasks/requirements/run-scheduling.md" --- # Task 03: Separate Office recovery maintenance diff --git a/docs/plans/run-scheduler-lifecycle/task-04-shutdown-composition.md b/docs/plans/run-scheduler-lifecycle/task-04-shutdown-composition.md index b2be36db0d..a8b20f884b 100644 --- a/docs/plans/run-scheduler-lifecycle/task-04-shutdown-composition.md +++ b/docs/plans/run-scheduler-lifecycle/task-04-shutdown-composition.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["01-owned-loop-lifecycles", "03-office-maintenance-separation"] plan: "plan.md" -spec: "../../specs/tasks/run-scheduling.md" +spec: "../../specs/tasks/requirements/run-scheduling.md" --- # Task 04: Stop schedulers before database cleanup diff --git a/docs/plans/runtime-task-state-publication-order/plan.md b/docs/plans/runtime-task-state-publication-order/plan.md index 7c064fd347..ea3bf3a17e 100644 --- a/docs/plans/runtime-task-state-publication-order/plan.md +++ b/docs/plans/runtime-task-state-publication-order/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/runtime-state-publication-order.md +spec: docs/specs/tasks/requirements/runtime-state-publication-order.md created: 2026-07-30 status: complete --- diff --git a/docs/plans/runtime-task-state-publication-order/task-01-order-runtime-state-publication.md b/docs/plans/runtime-task-state-publication-order/task-01-order-runtime-state-publication.md index cd1b9a86ff..e58256fd7d 100644 --- a/docs/plans/runtime-task-state-publication-order/task-01-order-runtime-state-publication.md +++ b/docs/plans/runtime-task-state-publication-order/task-01-order-runtime-state-publication.md @@ -5,7 +5,7 @@ status: completed wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/runtime-state-publication-order.md" +spec: "../../specs/tasks/requirements/runtime-state-publication-order.md" --- # Task 01: Order runtime state publication diff --git a/docs/plans/sidebar-task-edit/plan.md b/docs/plans/sidebar-task-edit/plan.md index 6e5336e749..f1b91a5772 100644 --- a/docs/plans/sidebar-task-edit/plan.md +++ b/docs/plans/sidebar-task-edit/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/sidebar-task-edit.md +spec: docs/specs/tasks/requirements/sidebar-task-edit.md created: 2026-08-03 status: complete --- diff --git a/docs/plans/sidebar-task-edit/task-01-shared-sidebar-edit-flow.md b/docs/plans/sidebar-task-edit/task-01-shared-sidebar-edit-flow.md index ff12d88cd2..c278908554 100644 --- a/docs/plans/sidebar-task-edit/task-01-shared-sidebar-edit-flow.md +++ b/docs/plans/sidebar-task-edit/task-01-shared-sidebar-edit-flow.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/sidebar-task-edit.md" +spec: "../../specs/tasks/requirements/sidebar-task-edit.md" --- # Task 01: Add shared sidebar edit flow diff --git a/docs/plans/sidebar-task-edit/task-02-sidebar-edit-e2e.md b/docs/plans/sidebar-task-edit/task-02-sidebar-edit-e2e.md index 75c896d37a..d5748dfbfb 100644 --- a/docs/plans/sidebar-task-edit/task-02-sidebar-edit-e2e.md +++ b/docs/plans/sidebar-task-edit/task-02-sidebar-edit-e2e.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-shared-sidebar-edit-flow"] plan: "plan.md" -spec: "../../specs/tasks/sidebar-task-edit.md" +spec: "../../specs/tasks/requirements/sidebar-task-edit.md" --- # Task 02: Prove sidebar edit parity @@ -44,7 +44,7 @@ Sequential after Task 01. These tests consume Task 01's menu item, dialog wiring ## Inputs -- Every scenario in `docs/specs/tasks/sidebar-task-edit.md`. +- Every scenario in `docs/specs/tasks/requirements/sidebar-task-edit.md`. - Plan `E2E Tests` and `Mobile design contract` sections. - Existing patterns: `SessionPage.sidebar`, the visible **Task actions** button, managed `prCapture` evidence, API seeding/polling, and the current mobile menu geometry assertions. diff --git a/docs/plans/stable-pr-changes-refresh/plan.md b/docs/plans/stable-pr-changes-refresh/plan.md index 1d96402c36..1a02e3b17e 100644 --- a/docs/plans/stable-pr-changes-refresh/plan.md +++ b/docs/plans/stable-pr-changes-refresh/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/multi-branch/spec.md +spec: docs/specs/tasks/requirements/multi-branch.md created: 2026-08-12 status: complete --- diff --git a/docs/plans/stable-pr-changes-refresh/task-01-stabilize-pr-file-refresh.md b/docs/plans/stable-pr-changes-refresh/task-01-stabilize-pr-file-refresh.md index faf6a27071..afb59a7767 100644 --- a/docs/plans/stable-pr-changes-refresh/task-01-stabilize-pr-file-refresh.md +++ b/docs/plans/stable-pr-changes-refresh/task-01-stabilize-pr-file-refresh.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/multi-branch/spec.md" +spec: "../../specs/tasks/requirements/multi-branch.md" --- # Task 01: Stabilize PR File Refresh diff --git a/docs/plans/stable-review-dialog-refresh/plan.md b/docs/plans/stable-review-dialog-refresh/plan.md index 523a93a093..703165b58c 100644 --- a/docs/plans/stable-review-dialog-refresh/plan.md +++ b/docs/plans/stable-review-dialog-refresh/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/multi-branch/spec.md +spec: docs/specs/tasks/requirements/multi-branch.md created: 2026-08-21 status: complete --- diff --git a/docs/plans/stable-review-dialog-refresh/task-01-preserve-review-diff-refresh.md b/docs/plans/stable-review-dialog-refresh/task-01-preserve-review-diff-refresh.md index 61e536717d..30752fa701 100644 --- a/docs/plans/stable-review-dialog-refresh/task-01-preserve-review-diff-refresh.md +++ b/docs/plans/stable-review-dialog-refresh/task-01-preserve-review-diff-refresh.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/multi-branch/spec.md" +spec: "../../specs/tasks/requirements/multi-branch.md" --- # Task 01: Preserve Review diff during refresh @@ -34,7 +34,7 @@ cd apps/web && pnpm e2e:run --no-build --project mobile-chrome tests/review/mobi - `apps/web/hooks/domains/github/use-pr-diff.ts` - `apps/web/hooks/domains/github/use-pr-diff.test.ts` -- `docs/specs/tasks/multi-branch/spec.md` +- `docs/specs/tasks/requirements/multi-branch.md` - `docs/plans/stable-review-dialog-refresh/plan.md` - this task file diff --git a/docs/plans/start-agent-composer-hint/plan.md b/docs/plans/start-agent-composer-hint/plan.md index 29994b984c..089934d2f7 100644 --- a/docs/plans/start-agent-composer-hint/plan.md +++ b/docs/plans/start-agent-composer-hint/plan.md @@ -1,5 +1,5 @@ --- -spec: ../../specs/prevent-agent-autostart-on-open/spec.md +spec: ../../specs/tasks/requirements/prevent-agent-autostart-on-open.md created: 2026-08-13 status: done --- diff --git a/docs/plans/start-agent-composer-hint/task-01-shared-predicate-and-removal.md b/docs/plans/start-agent-composer-hint/task-01-shared-predicate-and-removal.md index 0a74b0c682..592008997e 100644 --- a/docs/plans/start-agent-composer-hint/task-01-shared-predicate-and-removal.md +++ b/docs/plans/start-agent-composer-hint/task-01-shared-predicate-and-removal.md @@ -6,7 +6,7 @@ wave: 1 parallelism: sequential depends_on: [] plan: "plan.md" -spec: "../../specs/prevent-agent-autostart-on-open/spec.md" +spec: "../../specs/tasks/requirements/prevent-agent-autostart-on-open.md" --- # Task 01: Shared hint predicate and footer resume-button removal diff --git a/docs/plans/start-agent-composer-hint/task-02-composer-hint-ui.md b/docs/plans/start-agent-composer-hint/task-02-composer-hint-ui.md index 87f688212e..37cf7d8a69 100644 --- a/docs/plans/start-agent-composer-hint/task-02-composer-hint-ui.md +++ b/docs/plans/start-agent-composer-hint/task-02-composer-hint-ui.md @@ -6,7 +6,7 @@ wave: 1 parallelism: sequential depends_on: ["01-shared-predicate-and-removal"] plan: "plan.md" -spec: "../../specs/prevent-agent-autostart-on-open/spec.md" +spec: "../../specs/tasks/requirements/prevent-agent-autostart-on-open.md" --- # Task 02: Composer hint UI and i18n diff --git a/docs/plans/start-agent-composer-hint/task-03-e2e.md b/docs/plans/start-agent-composer-hint/task-03-e2e.md index 9cc152e74b..aeb622daa7 100644 --- a/docs/plans/start-agent-composer-hint/task-03-e2e.md +++ b/docs/plans/start-agent-composer-hint/task-03-e2e.md @@ -6,7 +6,7 @@ wave: 1 parallelism: sequential depends_on: ["02-composer-hint-ui"] plan: "plan.md" -spec: "../../specs/prevent-agent-autostart-on-open/spec.md" +spec: "../../specs/tasks/requirements/prevent-agent-autostart-on-open.md" --- # Task 03: E2E — hint replaces resume button, desktop and mobile diff --git a/docs/plans/start-agent-composer-hint/task-04-peer-message-resumes-stopped-session.md b/docs/plans/start-agent-composer-hint/task-04-peer-message-resumes-stopped-session.md index cf39c19dc6..23d1b90601 100644 --- a/docs/plans/start-agent-composer-hint/task-04-peer-message-resumes-stopped-session.md +++ b/docs/plans/start-agent-composer-hint/task-04-peer-message-resumes-stopped-session.md @@ -6,7 +6,7 @@ wave: 1 parallelism: sequential depends_on: [] plan: "plan.md" -spec: "../../specs/prevent-agent-autostart-on-open/spec.md" +spec: "../../specs/tasks/requirements/prevent-agent-autostart-on-open.md" --- # Task 04: Backend pin test — peer message resumes a stopped session diff --git a/docs/plans/storage-maintenance/task-01-durable-task-cleanup.md b/docs/plans/storage-maintenance/task-01-durable-task-cleanup.md index 4c1ed6e01a..ed41dd0121 100644 --- a/docs/plans/storage-maintenance/task-01-durable-task-cleanup.md +++ b/docs/plans/storage-maintenance/task-01-durable-task-cleanup.md @@ -46,7 +46,7 @@ None. ## Inputs - Spec sections: Task cleanup and orphan workspaces; Unarchive compatibility; Persistence guarantees -- `docs/specs/tasks/runtime-cleanup.md` +- `docs/specs/tasks/system-design/runtime-cleanup.md` - ADR 0025 runtime inventory rules - PR #1687 unarchive CAS and branch recovery behavior diff --git a/docs/plans/subtask-completion-trigger/plan.md b/docs/plans/subtask-completion-trigger/plan.md index 660edbba8e..5d89b3e652 100644 --- a/docs/plans/subtask-completion-trigger/plan.md +++ b/docs/plans/subtask-completion-trigger/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/subtask-completion-trigger.md +spec: docs/specs/tasks/requirements/subtask-completion-trigger.md created: 2026-06-23 status: implemented --- diff --git a/docs/plans/subtask-completion-trigger/task-01-backend-child-completion-repository.md b/docs/plans/subtask-completion-trigger/task-01-backend-child-completion-repository.md index 20f26332e9..7470a492f8 100644 --- a/docs/plans/subtask-completion-trigger/task-01-backend-child-completion-repository.md +++ b/docs/plans/subtask-completion-trigger/task-01-backend-child-completion-repository.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/subtask-completion-trigger.md" +spec: "../../specs/tasks/requirements/subtask-completion-trigger.md" --- # Task 01: Backend child completion repository @@ -36,7 +36,7 @@ None. ## Inputs -- Spec: `docs/specs/tasks/subtask-completion-trigger.md`, sections `What`, +- Spec: `docs/specs/tasks/requirements/subtask-completion-trigger.md`, sections `What`, `Data model`, `Scenarios`. - Existing patterns: `TaskRepository.ListChildren`, `office/repository/sqlite.AreAllChildrenTerminal`, and diff --git a/docs/plans/subtask-completion-trigger/task-02-workflow-event-contracts.md b/docs/plans/subtask-completion-trigger/task-02-workflow-event-contracts.md index 8bc39606df..b30fc36a4d 100644 --- a/docs/plans/subtask-completion-trigger/task-02-workflow-event-contracts.md +++ b/docs/plans/subtask-completion-trigger/task-02-workflow-event-contracts.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/subtask-completion-trigger.md" +spec: "../../specs/tasks/requirements/subtask-completion-trigger.md" --- # Task 02: Workflow event contracts @@ -39,7 +39,7 @@ None. ## Inputs -- Spec: `docs/specs/tasks/subtask-completion-trigger.md`, section +- Spec: `docs/specs/tasks/requirements/subtask-completion-trigger.md`, section `API surface`. - Existing backend model: `apps/backend/internal/workflow/models/models.go`. - Existing frontend type file: `apps/web/lib/types/workflow-actions.ts`. diff --git a/docs/plans/subtask-completion-trigger/task-03-orchestrator-trigger-dispatch.md b/docs/plans/subtask-completion-trigger/task-03-orchestrator-trigger-dispatch.md index e2c08bdf6d..122fec0529 100644 --- a/docs/plans/subtask-completion-trigger/task-03-orchestrator-trigger-dispatch.md +++ b/docs/plans/subtask-completion-trigger/task-03-orchestrator-trigger-dispatch.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-backend-child-completion-repository", "02-workflow-event-contracts"] plan: "plan.md" -spec: "../../specs/tasks/subtask-completion-trigger.md" +spec: "../../specs/tasks/requirements/subtask-completion-trigger.md" --- # Task 03: Orchestrator trigger dispatch @@ -41,7 +41,7 @@ go test ./internal/orchestrator/... ## Inputs -- Spec: `docs/specs/tasks/subtask-completion-trigger.md`, sections `State +- Spec: `docs/specs/tasks/requirements/subtask-completion-trigger.md`, sections `State machine`, `Failure modes`, `Persistence guarantees`, `Scenarios`. - Plan: Backend `Workflow Trigger Dispatch`. - Existing patterns: `processOnTurnCompleteViaEngine`, diff --git a/docs/plans/subtask-completion-trigger/task-04-e2e-verification.md b/docs/plans/subtask-completion-trigger/task-04-e2e-verification.md index f972a3b29f..ef2132f89c 100644 --- a/docs/plans/subtask-completion-trigger/task-04-e2e-verification.md +++ b/docs/plans/subtask-completion-trigger/task-04-e2e-verification.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["03-orchestrator-trigger-dispatch"] plan: "plan.md" -spec: "../../specs/tasks/subtask-completion-trigger.md" +spec: "../../specs/tasks/requirements/subtask-completion-trigger.md" --- # Task 04: E2E verification @@ -38,7 +38,7 @@ make typecheck test lint ## Inputs -- Spec: `docs/specs/tasks/subtask-completion-trigger.md`, `Scenarios`. +- Spec: `docs/specs/tasks/requirements/subtask-completion-trigger.md`, `Scenarios`. - Existing E2E patterns: `apps/web/e2e/tests/task/subtask.spec.ts`, `apps/web/e2e/helpers/api-client.ts`, and mock-agent MCP script examples. diff --git a/docs/plans/subtask-detachment/plan.md b/docs/plans/subtask-detachment/plan.md index 441506d3ad..c9813dfff0 100644 --- a/docs/plans/subtask-detachment/plan.md +++ b/docs/plans/subtask-detachment/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/subtask-detachment.md +spec: docs/specs/tasks/requirements/subtask-detachment.md created: 2026-07-18 status: complete --- diff --git a/docs/plans/subtask-detachment/task-01-backend-detachment.md b/docs/plans/subtask-detachment/task-01-backend-detachment.md index 7faba553d9..0c7e54aede 100644 --- a/docs/plans/subtask-detachment/task-01-backend-detachment.md +++ b/docs/plans/subtask-detachment/task-01-backend-detachment.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/subtask-detachment.md" +spec: "../../specs/tasks/requirements/subtask-detachment.md" --- # Task 01: Backend detach contract diff --git a/docs/plans/subtask-detachment/task-02-frontend-actions.md b/docs/plans/subtask-detachment/task-02-frontend-actions.md index c699fb2195..ff6d2aa737 100644 --- a/docs/plans/subtask-detachment/task-02-frontend-actions.md +++ b/docs/plans/subtask-detachment/task-02-frontend-actions.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/subtask-detachment.md" +spec: "../../specs/tasks/requirements/subtask-detachment.md" --- # Task 02: Frontend detach actions diff --git a/docs/plans/subtask-detachment/task-03-e2e-and-verification.md b/docs/plans/subtask-detachment/task-03-e2e-and-verification.md index 84718cf29a..f9ba367e3a 100644 --- a/docs/plans/subtask-detachment/task-03-e2e-and-verification.md +++ b/docs/plans/subtask-detachment/task-03-e2e-and-verification.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-backend-detachment", "02-frontend-actions"] plan: "plan.md" -spec: "../../specs/tasks/subtask-detachment.md" +spec: "../../specs/tasks/requirements/subtask-detachment.md" --- # Task 03: E2E and verification diff --git a/docs/plans/subtask-reparenting-drag-drop/plan.md b/docs/plans/subtask-reparenting-drag-drop/plan.md index 207997ae09..962dac63a6 100644 --- a/docs/plans/subtask-reparenting-drag-drop/plan.md +++ b/docs/plans/subtask-reparenting-drag-drop/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/subtask-reparenting-drag-drop.md +spec: docs/specs/tasks/requirements/subtask-reparenting-drag-drop.md created: 2026-08-04 status: complete --- diff --git a/docs/plans/subtask-reparenting-drag-drop/task-01-backend-composite-semantics.md b/docs/plans/subtask-reparenting-drag-drop/task-01-backend-composite-semantics.md index f822bb8c32..8386d35bcd 100644 --- a/docs/plans/subtask-reparenting-drag-drop/task-01-backend-composite-semantics.md +++ b/docs/plans/subtask-reparenting-drag-drop/task-01-backend-composite-semantics.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/subtask-reparenting-drag-drop.md" +spec: "../../specs/tasks/requirements/subtask-reparenting-drag-drop.md" --- # Task 01: Backend composite re-parent semantics diff --git a/docs/plans/subtask-reparenting-drag-drop/task-02-frontend-nest-drop-infrastructure.md b/docs/plans/subtask-reparenting-drag-drop/task-02-frontend-nest-drop-infrastructure.md index 1ab3f59d1e..714e09f406 100644 --- a/docs/plans/subtask-reparenting-drag-drop/task-02-frontend-nest-drop-infrastructure.md +++ b/docs/plans/subtask-reparenting-drag-drop/task-02-frontend-nest-drop-infrastructure.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-backend-composite-semantics"] plan: "plan.md" -spec: "../../specs/tasks/subtask-reparenting-drag-drop.md" +spec: "../../specs/tasks/requirements/subtask-reparenting-drag-drop.md" --- # Task 02: Frontend nest-drop infrastructure diff --git a/docs/plans/subtask-reparenting-drag-drop/task-03-frontend-wiring-and-affordances.md b/docs/plans/subtask-reparenting-drag-drop/task-03-frontend-wiring-and-affordances.md index 2e69010538..9bbee17f4e 100644 --- a/docs/plans/subtask-reparenting-drag-drop/task-03-frontend-wiring-and-affordances.md +++ b/docs/plans/subtask-reparenting-drag-drop/task-03-frontend-wiring-and-affordances.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-frontend-nest-drop-infrastructure"] plan: "plan.md" -spec: "../../specs/tasks/subtask-reparenting-drag-drop.md" +spec: "../../specs/tasks/requirements/subtask-reparenting-drag-drop.md" --- # Task 03: Frontend wiring and affordances diff --git a/docs/plans/subtask-reparenting-drag-drop/task-04-e2e-and-verification.md b/docs/plans/subtask-reparenting-drag-drop/task-04-e2e-and-verification.md index 10dcb293c4..a35e263988 100644 --- a/docs/plans/subtask-reparenting-drag-drop/task-04-e2e-and-verification.md +++ b/docs/plans/subtask-reparenting-drag-drop/task-04-e2e-and-verification.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["03-frontend-wiring-and-affordances"] plan: "plan.md" -spec: "../../specs/tasks/subtask-reparenting-drag-drop.md" +spec: "../../specs/tasks/requirements/subtask-reparenting-drag-drop.md" --- # Task 04: E2E and full verification diff --git a/docs/plans/task-autopilot/plan.md b/docs/plans/task-autopilot/plan.md index 2fbe915d6f..a65c3cb239 100644 --- a/docs/plans/task-autopilot/plan.md +++ b/docs/plans/task-autopilot/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/autopilot-mode.md +spec: docs/specs/tasks/requirements/autopilot-mode.md created: 2026-08-08 status: implemented --- diff --git a/docs/plans/task-autopilot/task-01-persist-task-contract.md b/docs/plans/task-autopilot/task-01-persist-task-contract.md index eeb93eb747..32f4e4d2ab 100644 --- a/docs/plans/task-autopilot/task-01-persist-task-contract.md +++ b/docs/plans/task-autopilot/task-01-persist-task-contract.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/autopilot-mode.md" +spec: "../../specs/tasks/requirements/autopilot-mode.md" --- # Task 01: Persist the Autopilot Task Contract diff --git a/docs/plans/task-autopilot/task-02-derive-runtime-contract.md b/docs/plans/task-autopilot/task-02-derive-runtime-contract.md index d9cfc59548..ece9fb56d4 100644 --- a/docs/plans/task-autopilot/task-02-derive-runtime-contract.md +++ b/docs/plans/task-autopilot/task-02-derive-runtime-contract.md @@ -6,7 +6,7 @@ wave: 2 depends_on: - "01-persist-task-contract" plan: "plan.md" -spec: "../../specs/tasks/autopilot-mode.md" +spec: "../../specs/tasks/requirements/autopilot-mode.md" --- # Task 02: Derive the Autopilot Runtime Contract diff --git a/docs/plans/task-autopilot/task-03-build-parent-question-lifecycle.md b/docs/plans/task-autopilot/task-03-build-parent-question-lifecycle.md index 301f9bed19..8eeceb1af9 100644 --- a/docs/plans/task-autopilot/task-03-build-parent-question-lifecycle.md +++ b/docs/plans/task-autopilot/task-03-build-parent-question-lifecycle.md @@ -7,7 +7,7 @@ depends_on: - "01-persist-task-contract" - "02-derive-runtime-contract" plan: "plan.md" -spec: "../../specs/tasks/autopilot-mode.md" +spec: "../../specs/tasks/requirements/autopilot-mode.md" --- # Task 03: Build the Parent Question Lifecycle diff --git a/docs/plans/task-autopilot/task-04-build-autopilot-ui.md b/docs/plans/task-autopilot/task-04-build-autopilot-ui.md index 2ba93e7243..c7cf7c4b71 100644 --- a/docs/plans/task-autopilot/task-04-build-autopilot-ui.md +++ b/docs/plans/task-autopilot/task-04-build-autopilot-ui.md @@ -6,7 +6,7 @@ wave: 2 depends_on: - "01-persist-task-contract" plan: "plan.md" -spec: "../../specs/tasks/autopilot-mode.md" +spec: "../../specs/tasks/requirements/autopilot-mode.md" --- # Task 04: Build the Autopilot Task UI diff --git a/docs/plans/task-autopilot/task-05-prove-autopilot-e2e.md b/docs/plans/task-autopilot/task-05-prove-autopilot-e2e.md index 9c0c384309..b9f75be6a0 100644 --- a/docs/plans/task-autopilot/task-05-prove-autopilot-e2e.md +++ b/docs/plans/task-autopilot/task-05-prove-autopilot-e2e.md @@ -7,7 +7,7 @@ depends_on: - "03-build-parent-question-lifecycle" - "04-build-autopilot-ui" plan: "plan.md" -spec: "../../specs/tasks/autopilot-mode.md" +spec: "../../specs/tasks/requirements/autopilot-mode.md" --- # Task 05: Prove the Autopilot Lifecycle End to End diff --git a/docs/plans/task-autopilot/task-06-document-autopilot.md b/docs/plans/task-autopilot/task-06-document-autopilot.md index 46016cc05e..1999982ef9 100644 --- a/docs/plans/task-autopilot/task-06-document-autopilot.md +++ b/docs/plans/task-autopilot/task-06-document-autopilot.md @@ -7,7 +7,7 @@ depends_on: - "01-persist-task-contract" - "02-derive-runtime-contract" plan: "plan.md" -spec: "../../specs/tasks/autopilot-mode.md" +spec: "../../specs/tasks/requirements/autopilot-mode.md" --- # Task 06: Document the Autopilot Public Contract diff --git a/docs/plans/task-cleanup-retry-bounds/plan.md b/docs/plans/task-cleanup-retry-bounds/plan.md index d450a6b238..fa622eb6ce 100644 --- a/docs/plans/task-cleanup-retry-bounds/plan.md +++ b/docs/plans/task-cleanup-retry-bounds/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/runtime-cleanup.md +spec: docs/specs/tasks/system-design/runtime-cleanup.md created: 2026-07-29 status: implemented --- diff --git a/docs/plans/task-cleanup-retry-bounds/task-01-idempotent-missing-resources.md b/docs/plans/task-cleanup-retry-bounds/task-01-idempotent-missing-resources.md index 7dcd66d9c5..f93707206a 100644 --- a/docs/plans/task-cleanup-retry-bounds/task-01-idempotent-missing-resources.md +++ b/docs/plans/task-cleanup-retry-bounds/task-01-idempotent-missing-resources.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/runtime-cleanup.md" +spec: "../../specs/tasks/system-design/runtime-cleanup.md" --- # Task 01: Make Missing Cleanup Resources Idempotent diff --git a/docs/plans/task-cleanup-retry-bounds/task-02-bounded-cleanup-retries.md b/docs/plans/task-cleanup-retry-bounds/task-02-bounded-cleanup-retries.md index 73bfb32001..f0007ba846 100644 --- a/docs/plans/task-cleanup-retry-bounds/task-02-bounded-cleanup-retries.md +++ b/docs/plans/task-cleanup-retry-bounds/task-02-bounded-cleanup-retries.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-idempotent-missing-resources"] plan: "plan.md" -spec: "../../specs/tasks/runtime-cleanup.md" +spec: "../../specs/tasks/system-design/runtime-cleanup.md" --- # Task 02: Bound Cleanup Retries diff --git a/docs/plans/task-context-menu-drag-guard/plan.md b/docs/plans/task-context-menu-drag-guard/plan.md index e1e30da2d3..d13022718d 100644 --- a/docs/plans/task-context-menu-drag-guard/plan.md +++ b/docs/plans/task-context-menu-drag-guard/plan.md @@ -1,5 +1,5 @@ --- -spec: ../../specs/tasks/subtask-reparenting-drag-drop.md +spec: ../../specs/tasks/requirements/subtask-reparenting-drag-drop.md created: 2026-08-13 status: done --- diff --git a/docs/plans/task-context-menu-drag-guard/task-01-guard-context-menu-pointer-events.md b/docs/plans/task-context-menu-drag-guard/task-01-guard-context-menu-pointer-events.md index e806f9936b..de83b5329a 100644 --- a/docs/plans/task-context-menu-drag-guard/task-01-guard-context-menu-pointer-events.md +++ b/docs/plans/task-context-menu-drag-guard/task-01-guard-context-menu-pointer-events.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/subtask-reparenting-drag-drop.md" +spec: "../../specs/tasks/requirements/subtask-reparenting-drag-drop.md" --- # Task 01: Guard context-menu pointer events @@ -75,7 +75,7 @@ Sequential; the guard and its regression tests change together. ## Inputs -- [Subtask re-parenting by drag and drop spec](../../specs/tasks/subtask-reparenting-drag-drop.md) (amended with the menu-containment scenarios) +- [Subtask re-parenting by drag and drop spec](../../specs/tasks/requirements/subtask-reparenting-drag-drop.md) (amended with the menu-containment scenarios) - [Fix plan](plan.md) - dnd-kit sensor behavior (`MouseSensor.activators` ignores only right-click; `PointerSensor` requires primary button) from `@dnd-kit/core` source. diff --git a/docs/plans/task-context-menu-drag-guard/task-02-cancel-touch-drag-on-menu-open.md b/docs/plans/task-context-menu-drag-guard/task-02-cancel-touch-drag-on-menu-open.md index 9ed3ec230b..930337dca3 100644 --- a/docs/plans/task-context-menu-drag-guard/task-02-cancel-touch-drag-on-menu-open.md +++ b/docs/plans/task-context-menu-drag-guard/task-02-cancel-touch-drag-on-menu-open.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: ["task-context-menu-drag-guard-01"] plan: "plan.md" -spec: "../../specs/tasks/subtask-reparenting-drag-drop.md" +spec: "../../specs/tasks/requirements/subtask-reparenting-drag-drop.md" --- # Task 02: Cancel touch drag when the context menu opens @@ -56,7 +56,7 @@ make fmt && make typecheck && make test && make lint (stub `TouchEvent` for happy-dom). - `apps/web/e2e/tests/task/mobile-subtask-reparent-drag-drop.spec.ts` — new "long-press opens the context menu without dragging the row" test. -- `docs/specs/tasks/subtask-reparenting-drag-drop.md` — touch long-press +- `docs/specs/tasks/requirements/subtask-reparenting-drag-drop.md` — touch long-press cancellation contract (What bullet + scenario). ## Dependencies @@ -70,7 +70,7 @@ Sequential; task 02 depends on task 01 and changes the same component. ## Inputs -- [Subtask re-parenting by drag and drop spec](../../specs/tasks/subtask-reparenting-drag-drop.md) (amended) +- [Subtask re-parenting by drag and drop spec](../../specs/tasks/requirements/subtask-reparenting-drag-drop.md) (amended) - [Fix plan](plan.md) - dnd-kit `TouchSensor` source: activation after the 250ms delay; `touchcancel` listener attached to the touchstart target element while a drag is active; diff --git a/docs/plans/task-create-advanced-settings/plan.md b/docs/plans/task-create-advanced-settings/plan.md index 5c24f4747e..b8673a306d 100644 --- a/docs/plans/task-create-advanced-settings/plan.md +++ b/docs/plans/task-create-advanced-settings/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/task-dependencies/create-dialog-advanced-settings.md +spec: docs/specs/tasks/requirements/task-dependencies-create-dialog-advanced-settings.md created: 2026-08-13 status: implemented --- @@ -15,7 +15,7 @@ leaving a contained region for future uncommon create options. This is a frontend-only presentation change. It builds on the implemented dependency selector refinement in -`docs/specs/task-dependencies/create-dialog-dependency-selector.md` and does +`docs/specs/tasks/requirements/task-dependencies-create-dialog-dependency-selector.md` and does not change the backend, API client, persistence, or WebSocket contracts. ## Frontend composition diff --git a/docs/plans/task-create-advanced-settings/task-01-advanced-settings-ui.md b/docs/plans/task-create-advanced-settings/task-01-advanced-settings-ui.md index 73c2b90517..1dc2b593cd 100644 --- a/docs/plans/task-create-advanced-settings/task-01-advanced-settings-ui.md +++ b/docs/plans/task-create-advanced-settings/task-01-advanced-settings-ui.md @@ -5,7 +5,7 @@ status: completed wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/task-dependencies/create-dialog-advanced-settings.md" +spec: "../../specs/tasks/requirements/task-dependencies-create-dialog-advanced-settings.md" --- # Task 01: Add the advanced settings disclosure @@ -82,7 +82,7 @@ selectors used by Task 02. ## Inputs -- Spec: `docs/specs/task-dependencies/create-dialog-advanced-settings.md` +- Spec: `docs/specs/tasks/requirements/task-dependencies-create-dialog-advanced-settings.md` - Plan: `docs/plans/task-create-advanced-settings/plan.md` - Existing dependency behavior in `apps/web/components/task-create-dialog-dependencies.tsx` diff --git a/docs/plans/task-create-advanced-settings/task-02-advanced-settings-e2e.md b/docs/plans/task-create-advanced-settings/task-02-advanced-settings-e2e.md index 67a435cba8..04e2fc4039 100644 --- a/docs/plans/task-create-advanced-settings/task-02-advanced-settings-e2e.md +++ b/docs/plans/task-create-advanced-settings/task-02-advanced-settings-e2e.md @@ -5,7 +5,7 @@ status: completed wave: 2 depends_on: ["01-advanced-settings-ui"] plan: "plan.md" -spec: "../../specs/task-dependencies/create-dialog-advanced-settings.md" +spec: "../../specs/tasks/requirements/task-dependencies-create-dialog-advanced-settings.md" --- # Task 02: Verify disclosure behavior on desktop and mobile @@ -61,7 +61,7 @@ but they depend on the final UI contract. ## Inputs - Scenarios and mobile contract in - `docs/specs/task-dependencies/create-dialog-advanced-settings.md` + `docs/specs/tasks/requirements/task-dependencies-create-dialog-advanced-settings.md` - Existing desktop coverage in `apps/web/e2e/tests/task/create-task-dependency-selector.spec.ts` - Existing mobile coverage in diff --git a/docs/plans/task-create-dependency-selector/plan.md b/docs/plans/task-create-dependency-selector/plan.md index 5b5f6783c6..25cff300f7 100644 --- a/docs/plans/task-create-dependency-selector/plan.md +++ b/docs/plans/task-create-dependency-selector/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/task-dependencies/create-dialog-dependency-selector.md +spec: docs/specs/tasks/requirements/task-dependencies-create-dialog-dependency-selector.md created: 2026-08-13 status: implemented --- diff --git a/docs/plans/task-create-dependency-selector/task-01-selector-ui.md b/docs/plans/task-create-dependency-selector/task-01-selector-ui.md index 6e443272d4..53a76c79b3 100644 --- a/docs/plans/task-create-dependency-selector/task-01-selector-ui.md +++ b/docs/plans/task-create-dependency-selector/task-01-selector-ui.md @@ -5,7 +5,7 @@ status: completed wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/task-dependencies/create-dialog-dependency-selector.md" +spec: "../../specs/tasks/requirements/task-dependencies-create-dialog-dependency-selector.md" --- # Task 01: Implement the selector and layout @@ -59,7 +59,7 @@ must be verified together. ## Inputs -- Spec: `docs/specs/task-dependencies/create-dialog-dependency-selector.md` +- Spec: `docs/specs/tasks/requirements/task-dependencies-create-dialog-dependency-selector.md` - Plan: `docs/plans/task-create-dependency-selector/plan.md`, Frontend and Mobile parity sections - Existing candidate derivation and payload behavior in diff --git a/docs/plans/task-create-dependency-selector/task-02-selector-e2e.md b/docs/plans/task-create-dependency-selector/task-02-selector-e2e.md index f86e35751c..b83d8db919 100644 --- a/docs/plans/task-create-dependency-selector/task-02-selector-e2e.md +++ b/docs/plans/task-create-dependency-selector/task-02-selector-e2e.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-selector-ui"] plan: "plan.md" -spec: "../../specs/task-dependencies/create-dialog-dependency-selector.md" +spec: "../../specs/tasks/requirements/task-dependencies-create-dialog-dependency-selector.md" --- # Task 02: Verify desktop and mobile flows @@ -52,7 +52,7 @@ implementation. ## Inputs - Spec scenarios and Mobile design contract in - `docs/specs/task-dependencies/create-dialog-dependency-selector.md` + `docs/specs/tasks/requirements/task-dependencies-create-dialog-dependency-selector.md` - Plan E2E Tests and Mobile parity contract in `plan.md` - `apps/web/e2e/fixtures/test-base.ts`, `pages/kanban-page.ts`, and `pages/mobile-kanban-page.ts` diff --git a/docs/plans/task-create-escape-dismissal/plan.md b/docs/plans/task-create-escape-dismissal/plan.md index 88cc880460..f2629cc9cb 100644 --- a/docs/plans/task-create-escape-dismissal/plan.md +++ b/docs/plans/task-create-escape-dismissal/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/task-create-escape-dismissal.md +spec: docs/specs/tasks/requirements/task-create-escape-dismissal.md created: 2026-08-19 status: complete --- diff --git a/docs/plans/task-create-escape-dismissal/task-01-guard-dialog-escape.md b/docs/plans/task-create-escape-dismissal/task-01-guard-dialog-escape.md index d42161d38c..6ae67f7f3e 100644 --- a/docs/plans/task-create-escape-dismissal/task-01-guard-dialog-escape.md +++ b/docs/plans/task-create-escape-dismissal/task-01-guard-dialog-escape.md @@ -5,7 +5,7 @@ status: complete wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/task-create-escape-dismissal.md" +spec: "../../specs/tasks/requirements/task-create-escape-dismissal.md" --- # Task 01: Guard Create Task Escape Dismissal @@ -17,7 +17,7 @@ Keep nested autocomplete dismissal and non-create dialog behavior unchanged. ## Inputs -- `docs/specs/tasks/task-create-escape-dismissal.md` +- `docs/specs/tasks/requirements/task-create-escape-dismissal.md` - `docs/plans/task-create-escape-dismissal/plan.md` - `apps/web/components/task-create-dialog.tsx` - `apps/web/components/task-create-dialog.test.tsx` diff --git a/docs/plans/task-create-escape-dismissal/task-02-capture-autocomplete-escape.md b/docs/plans/task-create-escape-dismissal/task-02-capture-autocomplete-escape.md index 9284bf08ee..a991966318 100644 --- a/docs/plans/task-create-escape-dismissal/task-02-capture-autocomplete-escape.md +++ b/docs/plans/task-create-escape-dismissal/task-02-capture-autocomplete-escape.md @@ -5,7 +5,7 @@ status: complete wave: 2 depends_on: ["01-guard-dialog-escape"] plan: "plan.md" -spec: "../../specs/tasks/task-create-escape-dismissal.md" +spec: "../../specs/tasks/requirements/task-create-escape-dismissal.md" --- # Task 02: Capture Autocomplete Escape And Retain Focus diff --git a/docs/plans/task-create-executor-default/plan.md b/docs/plans/task-create-executor-default/plan.md index 3a4c744ee4..8f27f3235d 100644 --- a/docs/plans/task-create-executor-default/plan.md +++ b/docs/plans/task-create-executor-default/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/task-create-executor-default.md +spec: docs/specs/tasks/requirements/task-create-executor-default.md created: 2026-08-01 status: complete --- diff --git a/docs/plans/task-create-executor-default/task-01-executor-first-selection.md b/docs/plans/task-create-executor-default/task-01-executor-first-selection.md index f41934951f..38092eba29 100644 --- a/docs/plans/task-create-executor-default/task-01-executor-first-selection.md +++ b/docs/plans/task-create-executor-default/task-01-executor-first-selection.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/task-create-executor-default.md" +spec: "../../specs/tasks/requirements/task-create-executor-default.md" --- # Task 01: Enforce executor-first profile selection diff --git a/docs/plans/task-create-executor-default/task-02-create-task-e2e.md b/docs/plans/task-create-executor-default/task-02-create-task-e2e.md index b0b2720b3a..28dde38652 100644 --- a/docs/plans/task-create-executor-default/task-02-create-task-e2e.md +++ b/docs/plans/task-create-executor-default/task-02-create-task-e2e.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-executor-first-selection"] plan: "plan.md" -spec: "../../specs/tasks/task-create-executor-default.md" +spec: "../../specs/tasks/requirements/task-create-executor-default.md" --- # Task 02: Prove portable settings behavior in Create Task diff --git a/docs/plans/task-create-source-executor-reset/plan.md b/docs/plans/task-create-source-executor-reset/plan.md index d3a50a3829..3ba8fc1db7 100644 --- a/docs/plans/task-create-source-executor-reset/plan.md +++ b/docs/plans/task-create-source-executor-reset/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/task-create-executor-default.md +spec: docs/specs/tasks/requirements/task-create-executor-default.md created: 2026-08-18 status: complete --- diff --git a/docs/plans/task-create-source-executor-reset/task-01-reset-source-executor.md b/docs/plans/task-create-source-executor-reset/task-01-reset-source-executor.md index 8f98456b54..89c3c8e50b 100644 --- a/docs/plans/task-create-source-executor-reset/task-01-reset-source-executor.md +++ b/docs/plans/task-create-source-executor-reset/task-01-reset-source-executor.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/task-create-executor-default.md" +spec: "../../specs/tasks/requirements/task-create-executor-default.md" --- # Task 01: Reset Executor on Source Transition diff --git a/docs/plans/task-create-source-executor-reset/task-02-dialog-transition-e2e.md b/docs/plans/task-create-source-executor-reset/task-02-dialog-transition-e2e.md index 89fedaadb6..7204ec97c7 100644 --- a/docs/plans/task-create-source-executor-reset/task-02-dialog-transition-e2e.md +++ b/docs/plans/task-create-source-executor-reset/task-02-dialog-transition-e2e.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-reset-source-executor"] plan: "plan.md" -spec: "../../specs/tasks/task-create-executor-default.md" +spec: "../../specs/tasks/requirements/task-create-executor-default.md" --- # Task 02: Prove the Real Dialog Transition diff --git a/docs/plans/task-create-workflow-memory/plan.md b/docs/plans/task-create-workflow-memory/plan.md index c55d80a4e9..744c4672fc 100644 --- a/docs/plans/task-create-workflow-memory/plan.md +++ b/docs/plans/task-create-workflow-memory/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/task-create-workflow-memory.md +spec: docs/specs/tasks/requirements/task-create-workflow-memory.md created: 2026-08-08 status: done --- diff --git a/docs/plans/task-create-workflow-memory/task-01-backend-workflow-memory.md b/docs/plans/task-create-workflow-memory/task-01-backend-workflow-memory.md index 07c01a3fba..b8b81cb994 100644 --- a/docs/plans/task-create-workflow-memory/task-01-backend-workflow-memory.md +++ b/docs/plans/task-create-workflow-memory/task-01-backend-workflow-memory.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/task-create-workflow-memory.md" +spec: "../../specs/tasks/requirements/task-create-workflow-memory.md" --- # Task 01: Persist Per-Workspace Workflow Memory diff --git a/docs/plans/task-create-workflow-memory/task-02-frontend-workflow-resolution.md b/docs/plans/task-create-workflow-memory/task-02-frontend-workflow-resolution.md index 9132f8948f..b0bc34916a 100644 --- a/docs/plans/task-create-workflow-memory/task-02-frontend-workflow-resolution.md +++ b/docs/plans/task-create-workflow-memory/task-02-frontend-workflow-resolution.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-backend-workflow-memory"] plan: "plan.md" -spec: "../../specs/tasks/task-create-workflow-memory.md" +spec: "../../specs/tasks/requirements/task-create-workflow-memory.md" --- # Task 02: Restore the Remembered Workflow diff --git a/docs/plans/task-create-workflow-memory/task-03-create-task-e2e.md b/docs/plans/task-create-workflow-memory/task-03-create-task-e2e.md index 5764649b03..bb24e0b729 100644 --- a/docs/plans/task-create-workflow-memory/task-03-create-task-e2e.md +++ b/docs/plans/task-create-workflow-memory/task-03-create-task-e2e.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["01-backend-workflow-memory", "02-frontend-workflow-resolution"] plan: "plan.md" -spec: "../../specs/tasks/task-create-workflow-memory.md" +spec: "../../specs/tasks/requirements/task-create-workflow-memory.md" --- # Task 03: Prove Workflow Memory Through Create Task diff --git a/docs/plans/task-dependencies/plan.md b/docs/plans/task-dependencies/plan.md index 71ff3c0f3c..f895222a1c 100644 --- a/docs/plans/task-dependencies/plan.md +++ b/docs/plans/task-dependencies/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/task-dependencies/spec.md +spec: docs/specs/tasks/system-design/task-dependencies.md created: 2026-08-09 status: not_started --- diff --git a/docs/plans/task-dependencies/task-01-core-dependency-relationship.md b/docs/plans/task-dependencies/task-01-core-dependency-relationship.md index b5598ef785..b83b616fcc 100644 --- a/docs/plans/task-dependencies/task-01-core-dependency-relationship.md +++ b/docs/plans/task-dependencies/task-01-core-dependency-relationship.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/task-dependencies/spec.md" +spec: "../../specs/tasks/system-design/task-dependencies.md" --- # Task 01: Promote Dependency Edges to a Core Relationship diff --git a/docs/plans/task-dependencies/task-02-gate-automated-launch.md b/docs/plans/task-dependencies/task-02-gate-automated-launch.md index 0d30f674f2..87947e23a5 100644 --- a/docs/plans/task-dependencies/task-02-gate-automated-launch.md +++ b/docs/plans/task-dependencies/task-02-gate-automated-launch.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-core-dependency-relationship"] plan: "plan.md" -spec: "../../specs/task-dependencies/spec.md" +spec: "../../specs/tasks/system-design/task-dependencies.md" --- # Task 02: Gate Automated Launches on Unresolved Dependencies diff --git a/docs/plans/task-dependencies/task-03-resolve-and-auto-start.md b/docs/plans/task-dependencies/task-03-resolve-and-auto-start.md index 7accd43dce..febd08dd61 100644 --- a/docs/plans/task-dependencies/task-03-resolve-and-auto-start.md +++ b/docs/plans/task-dependencies/task-03-resolve-and-auto-start.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-gate-automated-launch"] plan: "plan.md" -spec: "../../specs/task-dependencies/spec.md" +spec: "../../specs/tasks/system-design/task-dependencies.md" --- # Task 03: Resolve Dependencies and Auto-Start on Unblock diff --git a/docs/plans/task-dependencies/task-04-failed-predecessor-halt.md b/docs/plans/task-dependencies/task-04-failed-predecessor-halt.md index 04be5d4b65..16d5fdf925 100644 --- a/docs/plans/task-dependencies/task-04-failed-predecessor-halt.md +++ b/docs/plans/task-dependencies/task-04-failed-predecessor-halt.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["03-resolve-and-auto-start"] plan: "plan.md" -spec: "../../specs/task-dependencies/spec.md" +spec: "../../specs/tasks/system-design/task-dependencies.md" --- # Task 04: Halt Chains on a Failed Predecessor diff --git a/docs/plans/task-dependencies/task-05-mcp-dependency-tools.md b/docs/plans/task-dependencies/task-05-mcp-dependency-tools.md index 621f10479f..abb38b6fbb 100644 --- a/docs/plans/task-dependencies/task-05-mcp-dependency-tools.md +++ b/docs/plans/task-dependencies/task-05-mcp-dependency-tools.md @@ -5,7 +5,7 @@ status: done wave: 5 depends_on: ["01-core-dependency-relationship"] plan: "plan.md" -spec: "../../specs/task-dependencies/spec.md" +spec: "../../specs/tasks/system-design/task-dependencies.md" --- # Task 05: MCP Dependency Tools diff --git a/docs/plans/task-dependencies/task-06-kanban-dependency-ui.md b/docs/plans/task-dependencies/task-06-kanban-dependency-ui.md index 76bd04ab84..7ef9a926e5 100644 --- a/docs/plans/task-dependencies/task-06-kanban-dependency-ui.md +++ b/docs/plans/task-dependencies/task-06-kanban-dependency-ui.md @@ -5,7 +5,7 @@ status: done wave: 6 depends_on: ["01-core-dependency-relationship"] plan: "plan.md" -spec: "../../specs/task-dependencies/spec.md" +spec: "../../specs/tasks/system-design/task-dependencies.md" --- # Task 06: Kanban Dependency UI diff --git a/docs/plans/task-dependencies/task-07-dependencies-board-view.md b/docs/plans/task-dependencies/task-07-dependencies-board-view.md index 5d1b7457a6..9e6aa94d8d 100644 --- a/docs/plans/task-dependencies/task-07-dependencies-board-view.md +++ b/docs/plans/task-dependencies/task-07-dependencies-board-view.md @@ -5,7 +5,7 @@ status: dropped wave: 7 depends_on: ["06-kanban-dependency-ui"] plan: "plan.md" -spec: "../../specs/task-dependencies/spec.md" +spec: "../../specs/tasks/system-design/task-dependencies.md" --- # Task 07: Dependencies Board View diff --git a/docs/plans/task-dependencies/task-08-verify-flows-and-docs.md b/docs/plans/task-dependencies/task-08-verify-flows-and-docs.md index 71be48581d..989a86b0c5 100644 --- a/docs/plans/task-dependencies/task-08-verify-flows-and-docs.md +++ b/docs/plans/task-dependencies/task-08-verify-flows-and-docs.md @@ -5,7 +5,7 @@ status: done wave: 8 depends_on: ["04-failed-predecessor-halt", "06-kanban-dependency-ui"] plan: "plan.md" -spec: "../../specs/task-dependencies/spec.md" +spec: "../../specs/tasks/system-design/task-dependencies.md" --- # Task 08: Verify Browser Flows and Documentation @@ -91,7 +91,7 @@ of `main`. - `docs/public/tasks-and-workflows.md` - `docs/public/workflow-tips.md` - `docs/features.md` -- `docs/specs/task-dependencies/spec.md` +- `docs/specs/tasks/system-design/task-dependencies.md` - `docs/specs/INDEX.md` - `docs/plans/task-dependencies/plan.md` diff --git a/docs/plans/task-launch-failure-recovery/plan.md b/docs/plans/task-launch-failure-recovery/plan.md index bcd1e49777..159290185c 100644 --- a/docs/plans/task-launch-failure-recovery/plan.md +++ b/docs/plans/task-launch-failure-recovery/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/task-launch-failure-recovery/spec.md +spec: docs/specs/tasks/requirements/task-launch-failure-recovery.md created: 2026-08-19 status: done --- diff --git a/docs/plans/task-launch-failure-recovery/task-01-failure-taxonomy-contracts.md b/docs/plans/task-launch-failure-recovery/task-01-failure-taxonomy-contracts.md index d974b63eb8..dff2f0228f 100644 --- a/docs/plans/task-launch-failure-recovery/task-01-failure-taxonomy-contracts.md +++ b/docs/plans/task-launch-failure-recovery/task-01-failure-taxonomy-contracts.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/task-launch-failure-recovery/spec.md" +spec: "../../specs/tasks/requirements/task-launch-failure-recovery.md" --- # Task 01: Launch-error persistence contracts diff --git a/docs/plans/task-launch-failure-recovery/task-02-gitref-default-hardening.md b/docs/plans/task-launch-failure-recovery/task-02-gitref-default-hardening.md index 5271545dbc..914f8ffb0d 100644 --- a/docs/plans/task-launch-failure-recovery/task-02-gitref-default-hardening.md +++ b/docs/plans/task-launch-failure-recovery/task-02-gitref-default-hardening.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/task-launch-failure-recovery/spec.md" +spec: "../../specs/tasks/requirements/task-launch-failure-recovery.md" --- # Task 02: Local default-branch detection diff --git a/docs/plans/task-launch-failure-recovery/task-03-worktree-live-default-fallback.md b/docs/plans/task-launch-failure-recovery/task-03-worktree-live-default-fallback.md index b6ec34d192..92ada2bdb2 100644 --- a/docs/plans/task-launch-failure-recovery/task-03-worktree-live-default-fallback.md +++ b/docs/plans/task-launch-failure-recovery/task-03-worktree-live-default-fallback.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["02-gitref-default-hardening"] plan: "plan.md" -spec: "../../specs/task-launch-failure-recovery/spec.md" +spec: "../../specs/tasks/requirements/task-launch-failure-recovery.md" --- # Task 03: Worktree fallback resolves live remote default diff --git a/docs/plans/task-launch-failure-recovery/task-04-launch-failure-classification.md b/docs/plans/task-launch-failure-recovery/task-04-launch-failure-classification.md index 0d2bd92909..6493f07f5d 100644 --- a/docs/plans/task-launch-failure-recovery/task-04-launch-failure-classification.md +++ b/docs/plans/task-launch-failure-recovery/task-04-launch-failure-classification.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["01-failure-taxonomy-contracts", "10-task-base-self-heal"] plan: "plan.md" -spec: "../../specs/task-launch-failure-recovery/spec.md" +spec: "../../specs/tasks/requirements/task-launch-failure-recovery.md" --- # Task 04: Persist typed launch-failure reason diff --git a/docs/plans/task-launch-failure-recovery/task-05-pr-review-autostart-gating.md b/docs/plans/task-launch-failure-recovery/task-05-pr-review-autostart-gating.md index 2f687000cb..5c7258ba4c 100644 --- a/docs/plans/task-launch-failure-recovery/task-05-pr-review-autostart-gating.md +++ b/docs/plans/task-launch-failure-recovery/task-05-pr-review-autostart-gating.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-failure-taxonomy-contracts"] plan: "plan.md" -spec: "../../specs/task-launch-failure-recovery/spec.md" +spec: "../../specs/tasks/requirements/task-launch-failure-recovery.md" --- # Task 05: Gate terminal PR auto-start diff --git a/docs/plans/task-launch-failure-recovery/task-06-recovery-actions-ws.md b/docs/plans/task-launch-failure-recovery/task-06-recovery-actions-ws.md index 4aae2726c7..ea5544c6f2 100644 --- a/docs/plans/task-launch-failure-recovery/task-06-recovery-actions-ws.md +++ b/docs/plans/task-launch-failure-recovery/task-06-recovery-actions-ws.md @@ -5,7 +5,7 @@ status: done wave: 5 depends_on: ["01-failure-taxonomy-contracts", "02-gitref-default-hardening", "03-worktree-live-default-fallback", "04-launch-failure-classification", "05-pr-review-autostart-gating", "10-task-base-self-heal", "12-remove-legacy-launch-guidance"] plan: "plan.md" -spec: "../../specs/task-launch-failure-recovery/spec.md" +spec: "../../specs/tasks/requirements/task-launch-failure-recovery.md" --- # Task 06: Task launch recovery action diff --git a/docs/plans/task-launch-failure-recovery/task-07-status-summary-projection.md b/docs/plans/task-launch-failure-recovery/task-07-status-summary-projection.md index 9c1177128f..f49a75387a 100644 --- a/docs/plans/task-launch-failure-recovery/task-07-status-summary-projection.md +++ b/docs/plans/task-launch-failure-recovery/task-07-status-summary-projection.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["01-failure-taxonomy-contracts", "04-launch-failure-classification", "05-pr-review-autostart-gating"] plan: "plan.md" -spec: "../../specs/task-launch-failure-recovery/spec.md" +spec: "../../specs/tasks/requirements/task-launch-failure-recovery.md" --- # Task 07: Project bounded launch errors diff --git a/docs/plans/task-launch-failure-recovery/task-08-frontend-failure-surface-and-recovery.md b/docs/plans/task-launch-failure-recovery/task-08-frontend-failure-surface-and-recovery.md index 4dd94d1c74..a29e10072d 100644 --- a/docs/plans/task-launch-failure-recovery/task-08-frontend-failure-surface-and-recovery.md +++ b/docs/plans/task-launch-failure-recovery/task-08-frontend-failure-surface-and-recovery.md @@ -5,7 +5,7 @@ status: done wave: 5 depends_on: ["07-status-summary-projection"] plan: "plan.md" -spec: "../../specs/task-launch-failure-recovery/spec.md" +spec: "../../specs/tasks/requirements/task-launch-failure-recovery.md" --- # Task 08: Frontend launch-error data diff --git a/docs/plans/task-launch-failure-recovery/task-09-e2e-and-i18n.md b/docs/plans/task-launch-failure-recovery/task-09-e2e-and-i18n.md index 0fedb51818..6bfdf3d6bb 100644 --- a/docs/plans/task-launch-failure-recovery/task-09-e2e-and-i18n.md +++ b/docs/plans/task-launch-failure-recovery/task-09-e2e-and-i18n.md @@ -5,7 +5,7 @@ status: done wave: 7 depends_on: ["11-responsive-launch-error-surface"] plan: "plan.md" -spec: "../../specs/task-launch-failure-recovery/spec.md" +spec: "../../specs/tasks/requirements/task-launch-failure-recovery.md" --- # Task 09: Localized recovery E2E diff --git a/docs/plans/task-launch-failure-recovery/task-10-task-base-self-heal.md b/docs/plans/task-launch-failure-recovery/task-10-task-base-self-heal.md index 94db24f4cd..b435cc5262 100644 --- a/docs/plans/task-launch-failure-recovery/task-10-task-base-self-heal.md +++ b/docs/plans/task-launch-failure-recovery/task-10-task-base-self-heal.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-failure-taxonomy-contracts"] plan: "plan.md" -spec: "../../specs/task-launch-failure-recovery/spec.md" +spec: "../../specs/tasks/requirements/task-launch-failure-recovery.md" --- # Task 10: Carry task-repository launch identity diff --git a/docs/plans/task-launch-failure-recovery/task-11-responsive-launch-error-surface.md b/docs/plans/task-launch-failure-recovery/task-11-responsive-launch-error-surface.md index aac1608217..c52122b1f9 100644 --- a/docs/plans/task-launch-failure-recovery/task-11-responsive-launch-error-surface.md +++ b/docs/plans/task-launch-failure-recovery/task-11-responsive-launch-error-surface.md @@ -5,7 +5,7 @@ status: done wave: 6 depends_on: ["06-recovery-actions-ws", "08-frontend-failure-surface-and-recovery"] plan: "plan.md" -spec: "../../specs/task-launch-failure-recovery/spec.md" +spec: "../../specs/tasks/requirements/task-launch-failure-recovery.md" --- # Task 11: Responsive launch-error surface diff --git a/docs/plans/task-launch-failure-recovery/task-12-remove-legacy-launch-guidance.md b/docs/plans/task-launch-failure-recovery/task-12-remove-legacy-launch-guidance.md index b5e57e129b..3c8cb1d234 100644 --- a/docs/plans/task-launch-failure-recovery/task-12-remove-legacy-launch-guidance.md +++ b/docs/plans/task-launch-failure-recovery/task-12-remove-legacy-launch-guidance.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["04-launch-failure-classification"] plan: "plan.md" -spec: "../../specs/task-launch-failure-recovery/spec.md" +spec: "../../specs/tasks/requirements/task-launch-failure-recovery.md" --- # Task 12: Remove legacy launch guidance diff --git a/docs/plans/task-owned-worktree-lifecycle/task-01-task-worktree-persistence.md b/docs/plans/task-owned-worktree-lifecycle/task-01-task-worktree-persistence.md index 64f60000f3..8a839fd1e6 100644 --- a/docs/plans/task-owned-worktree-lifecycle/task-01-task-worktree-persistence.md +++ b/docs/plans/task-owned-worktree-lifecycle/task-01-task-worktree-persistence.md @@ -116,7 +116,7 @@ None. ## Inputs -- `docs/specs/tasks/runtime-cleanup.md`, especially the normalized task +- `docs/specs/tasks/system-design/runtime-cleanup.md`, especially the normalized task environment data model. - `docs/decisions/2026-08-08-task-owned-worktree-lifetime.md`. - Existing `TaskEnvironment` ownership-transfer behavior and worktree manager diff --git a/docs/plans/task-runtime-cleanup/plan.md b/docs/plans/task-runtime-cleanup/plan.md index bc28232f52..33d7fe6f4b 100644 --- a/docs/plans/task-runtime-cleanup/plan.md +++ b/docs/plans/task-runtime-cleanup/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/runtime-cleanup.md +spec: docs/specs/tasks/system-design/runtime-cleanup.md created: 2026-06-22 status: implemented --- diff --git a/docs/plans/task-runtime-cleanup/task-01-runtime-inventory.md b/docs/plans/task-runtime-cleanup/task-01-runtime-inventory.md index bfbc4a6819..6bdc01159e 100644 --- a/docs/plans/task-runtime-cleanup/task-01-runtime-inventory.md +++ b/docs/plans/task-runtime-cleanup/task-01-runtime-inventory.md @@ -5,7 +5,7 @@ status: completed wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/runtime-cleanup.md" +spec: "../../specs/tasks/system-design/runtime-cleanup.md" --- # Task 01: Runtime Inventory @@ -36,7 +36,7 @@ None. ## Inputs -- Spec: `docs/specs/tasks/runtime-cleanup.md` +- Spec: `docs/specs/tasks/system-design/runtime-cleanup.md` - ADR: `docs/decisions/0025-runtime-cleanup-uses-executors-running.md` - Existing repository patterns in `apps/backend/internal/task/repository/sqlite/executor.go` diff --git a/docs/plans/task-runtime-cleanup/task-02-task-cleanup-ordering.md b/docs/plans/task-runtime-cleanup/task-02-task-cleanup-ordering.md index 6f6e252028..358d9d445d 100644 --- a/docs/plans/task-runtime-cleanup/task-02-task-cleanup-ordering.md +++ b/docs/plans/task-runtime-cleanup/task-02-task-cleanup-ordering.md @@ -5,7 +5,7 @@ status: completed wave: 1 depends_on: ["01-runtime-inventory"] plan: "plan.md" -spec: "../../specs/tasks/runtime-cleanup.md" +spec: "../../specs/tasks/system-design/runtime-cleanup.md" --- # Task 02: Task Cleanup Ordering diff --git a/docs/plans/task-runtime-cleanup/task-03-agentctl-process-group-shutdown.md b/docs/plans/task-runtime-cleanup/task-03-agentctl-process-group-shutdown.md index b981932d10..6600007f68 100644 --- a/docs/plans/task-runtime-cleanup/task-03-agentctl-process-group-shutdown.md +++ b/docs/plans/task-runtime-cleanup/task-03-agentctl-process-group-shutdown.md @@ -5,7 +5,7 @@ status: completed wave: 2 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/runtime-cleanup.md" +spec: "../../specs/tasks/system-design/runtime-cleanup.md" --- # Task 03: Agentctl Process Group Shutdown diff --git a/docs/plans/task-runtime-cleanup/task-04-startup-reconciliation.md b/docs/plans/task-runtime-cleanup/task-04-startup-reconciliation.md index 1e605ad0a2..b9d1f037dc 100644 --- a/docs/plans/task-runtime-cleanup/task-04-startup-reconciliation.md +++ b/docs/plans/task-runtime-cleanup/task-04-startup-reconciliation.md @@ -5,7 +5,7 @@ status: completed wave: 2 depends_on: ["01-runtime-inventory", "02-task-cleanup-ordering"] plan: "plan.md" -spec: "../../specs/tasks/runtime-cleanup.md" +spec: "../../specs/tasks/system-design/runtime-cleanup.md" --- # Task 04: Startup Reconciliation diff --git a/docs/plans/task-runtime-cleanup/task-05-verification-and-doc-sync.md b/docs/plans/task-runtime-cleanup/task-05-verification-and-doc-sync.md index 8fafa2f877..f6353b9914 100644 --- a/docs/plans/task-runtime-cleanup/task-05-verification-and-doc-sync.md +++ b/docs/plans/task-runtime-cleanup/task-05-verification-and-doc-sync.md @@ -5,7 +5,7 @@ status: completed wave: 3 depends_on: ["01-runtime-inventory", "02-task-cleanup-ordering", "03-agentctl-process-group-shutdown", "04-startup-reconciliation"] plan: "plan.md" -spec: "../../specs/tasks/runtime-cleanup.md" +spec: "../../specs/tasks/system-design/runtime-cleanup.md" --- # Task 05: Verification and Doc Sync @@ -27,7 +27,7 @@ make -C apps/backend build ## Files likely touched -- `docs/specs/tasks/runtime-cleanup.md` +- `docs/specs/tasks/system-design/runtime-cleanup.md` - `docs/decisions/0025-runtime-cleanup-uses-executors-running.md` - `docs/plans/task-runtime-cleanup/plan.md` - `apps/backend/AGENTS.md` diff --git a/docs/plans/task-step-transition-ledger/plan.md b/docs/plans/task-step-transition-ledger/plan.md index cb3c8823cf..09ae019d82 100644 --- a/docs/plans/task-step-transition-ledger/plan.md +++ b/docs/plans/task-step-transition-ledger/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/workflow/task-step-transition-ledger/spec.md +spec: docs/specs/tasks/requirements/workflow-task-step-transition-ledger.md created: 2026-08-13 status: draft --- @@ -558,7 +558,7 @@ is subsumed by the equal-values check above it) — harmless, left as-is rather than churning a working file for a no-op cleanup. **Test-honesty audit** against all ~47 GIVEN/WHEN/THEN scenarios in -`docs/specs/workflow/task-step-transition-ledger/spec.md` § Scenarios, by +`docs/specs/tasks/requirements/workflow-task-step-transition-ledger.md` § Scenarios, by category: | Category | Scenarios | Result | diff --git a/docs/plans/task-step-transition-ledger/task-01-telemetry-activation-registry.md b/docs/plans/task-step-transition-ledger/task-01-telemetry-activation-registry.md index 50f9278836..4ce78baca6 100644 --- a/docs/plans/task-step-transition-ledger/task-01-telemetry-activation-registry.md +++ b/docs/plans/task-step-transition-ledger/task-01-telemetry-activation-registry.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/workflow/task-step-transition-ledger/spec.md" +spec: "../../specs/tasks/requirements/workflow-task-step-transition-ledger.md" --- # Task 01: Telemetry contract activation registry diff --git a/docs/plans/task-step-transition-ledger/task-02-turn-start-step-stamp.md b/docs/plans/task-step-transition-ledger/task-02-turn-start-step-stamp.md index ae138a848f..04da3288c9 100644 --- a/docs/plans/task-step-transition-ledger/task-02-turn-start-step-stamp.md +++ b/docs/plans/task-step-transition-ledger/task-02-turn-start-step-stamp.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-telemetry-activation-registry"] plan: "plan.md" -spec: "../../specs/workflow/task-step-transition-ledger/spec.md" +spec: "../../specs/tasks/requirements/workflow-task-step-transition-ledger.md" --- # Task 02: Turn-start step stamp (Slice 1) diff --git a/docs/plans/task-step-transition-ledger/task-03-ledger-schema-migration.md b/docs/plans/task-step-transition-ledger/task-03-ledger-schema-migration.md index 3d761ecfa1..29316b4f51 100644 --- a/docs/plans/task-step-transition-ledger/task-03-ledger-schema-migration.md +++ b/docs/plans/task-step-transition-ledger/task-03-ledger-schema-migration.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["01-telemetry-activation-registry", "02-turn-start-step-stamp"] plan: "plan.md" -spec: "../../specs/workflow/task-step-transition-ledger/spec.md" +spec: "../../specs/tasks/requirements/workflow-task-step-transition-ledger.md" --- # Task 03: Ledger schema and migration diff --git a/docs/plans/task-step-transition-ledger/task-04-ledger-writer-chokepoints.md b/docs/plans/task-step-transition-ledger/task-04-ledger-writer-chokepoints.md index 18a1232084..7e05a23c04 100644 --- a/docs/plans/task-step-transition-ledger/task-04-ledger-writer-chokepoints.md +++ b/docs/plans/task-step-transition-ledger/task-04-ledger-writer-chokepoints.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["03-ledger-schema-migration"] plan: "plan.md" -spec: "../../specs/workflow/task-step-transition-ledger/spec.md" +spec: "../../specs/tasks/requirements/workflow-task-step-transition-ledger.md" --- # Task 04: Ledger writer in the mutating transactions diff --git a/docs/plans/task-step-transition-ledger/task-05-caller-attribution-wiring.md b/docs/plans/task-step-transition-ledger/task-05-caller-attribution-wiring.md index dca3fe9262..b6f088e213 100644 --- a/docs/plans/task-step-transition-ledger/task-05-caller-attribution-wiring.md +++ b/docs/plans/task-step-transition-ledger/task-05-caller-attribution-wiring.md @@ -5,7 +5,7 @@ status: done wave: 5 depends_on: ["04-ledger-writer-chokepoints"] plan: "plan.md" -spec: "../../specs/workflow/task-step-transition-ledger/spec.md" +spec: "../../specs/tasks/requirements/workflow-task-step-transition-ledger.md" --- # Task 05: Caller attribution wiring diff --git a/docs/plans/task-step-transition-ledger/task-06-writer-health-controls.md b/docs/plans/task-step-transition-ledger/task-06-writer-health-controls.md index d9af7b2729..0f9c6e013e 100644 --- a/docs/plans/task-step-transition-ledger/task-06-writer-health-controls.md +++ b/docs/plans/task-step-transition-ledger/task-06-writer-health-controls.md @@ -5,7 +5,7 @@ status: done wave: 6 depends_on: ["04-ledger-writer-chokepoints", "05-caller-attribution-wiring"] plan: "plan.md" -spec: "../../specs/workflow/task-step-transition-ledger/spec.md" +spec: "../../specs/tasks/requirements/workflow-task-step-transition-ledger.md" --- # Task 06: Writer health controls diff --git a/docs/plans/task-title-caret/plan.md b/docs/plans/task-title-caret/plan.md index 061401e10b..cd8a60551b 100644 --- a/docs/plans/task-title-caret/plan.md +++ b/docs/plans/task-title-caret/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/title-length-limit.md +spec: docs/specs/tasks/requirements/title-length-limit.md created: 2026-08-11 status: complete --- @@ -159,7 +159,7 @@ spec proves the same user value on the phone viewport. - `apps/web/components/automations/automation-editor-sections.tsx` - `apps/web/e2e/tests/task/task-title-caret.spec.ts` (new) - `apps/web/e2e/tests/task/mobile-task-title-caret.spec.ts` (new) -- `docs/specs/tasks/title-length-limit.md` (amended: caret scenario) +- `docs/specs/tasks/requirements/title-length-limit.md` (amended: caret scenario) ## Verification diff --git a/docs/plans/task-title-caret/task-01-regression-e2e.md b/docs/plans/task-title-caret/task-01-regression-e2e.md index 797e4fa8a2..db94e115ba 100644 --- a/docs/plans/task-title-caret/task-01-regression-e2e.md +++ b/docs/plans/task-title-caret/task-01-regression-e2e.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/title-length-limit.md" +spec: "../../specs/tasks/requirements/title-length-limit.md" --- # Task 01: Write failing regression tests for the caret jump diff --git a/docs/plans/task-title-caret/task-02-caret-preserving-clamp.md b/docs/plans/task-title-caret/task-02-caret-preserving-clamp.md index 4e93619357..c14d935122 100644 --- a/docs/plans/task-title-caret/task-02-caret-preserving-clamp.md +++ b/docs/plans/task-title-caret/task-02-caret-preserving-clamp.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-regression-e2e"] plan: "plan.md" -spec: "../../specs/tasks/title-length-limit.md" +spec: "../../specs/tasks/requirements/title-length-limit.md" --- # Task 02: Implement the caret-preserving clamp hook diff --git a/docs/plans/task-title-caret/task-03-rename-edit-dialogs.md b/docs/plans/task-title-caret/task-03-rename-edit-dialogs.md index 1b4c4c4d58..0d6a00fa6a 100644 --- a/docs/plans/task-title-caret/task-03-rename-edit-dialogs.md +++ b/docs/plans/task-title-caret/task-03-rename-edit-dialogs.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-caret-preserving-clamp"] plan: "plan.md" -spec: "../../specs/tasks/title-length-limit.md" +spec: "../../specs/tasks/requirements/title-length-limit.md" --- # Task 03: Apply the hook to the rename and edit dialogs diff --git a/docs/plans/task-title-caret/task-04-sibling-title-inputs.md b/docs/plans/task-title-caret/task-04-sibling-title-inputs.md index 925aa16ce6..16115e8fb0 100644 --- a/docs/plans/task-title-caret/task-04-sibling-title-inputs.md +++ b/docs/plans/task-title-caret/task-04-sibling-title-inputs.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["03-rename-edit-dialogs"] plan: "plan.md" -spec: "../../specs/tasks/title-length-limit.md" +spec: "../../specs/tasks/requirements/title-length-limit.md" --- # Task 04: Extend the hook to sibling task-title inputs @@ -27,7 +27,7 @@ spec: "../../specs/tasks/title-length-limit.md" user-facing copy, clamp semantics unchanged. - The full verification gate passes: `make fmt`, then `make typecheck test lint`. -- The amended spec `docs/specs/tasks/title-length-limit.md` status flips back +- The amended spec `docs/specs/tasks/requirements/title-length-limit.md` status flips back to `complete` and the plan/task statuses are updated. ## Verification @@ -49,7 +49,7 @@ spec: "../../specs/tasks/title-length-limit.md" - `apps/web/app/office/components/new-task-dialog.tsx` - `apps/web/app/office/setup/step-task.tsx` - `apps/web/components/automations/automation-editor-sections.tsx` -- `docs/specs/tasks/title-length-limit.md` (status back to `complete`) +- `docs/specs/tasks/requirements/title-length-limit.md` (status back to `complete`) ## Dependencies diff --git a/docs/plans/task-title-length-limit/plan.md b/docs/plans/task-title-length-limit/plan.md index 715ca50dcf..a16a2c917c 100644 --- a/docs/plans/task-title-length-limit/plan.md +++ b/docs/plans/task-title-length-limit/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/title-length-limit.md +spec: docs/specs/tasks/requirements/title-length-limit.md created: 2026-08-01 status: implemented --- diff --git a/docs/plans/task-title-length-limit/task-01-backend-mcp-contract.md b/docs/plans/task-title-length-limit/task-01-backend-mcp-contract.md index b476fd9a6c..993eb4c89b 100644 --- a/docs/plans/task-title-length-limit/task-01-backend-mcp-contract.md +++ b/docs/plans/task-title-length-limit/task-01-backend-mcp-contract.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/title-length-limit.md" +spec: "../../specs/tasks/requirements/title-length-limit.md" --- # Task 01: Backend and MCP title contract diff --git a/docs/plans/task-title-length-limit/task-02-frontend-title-limit.md b/docs/plans/task-title-length-limit/task-02-frontend-title-limit.md index 38e5dee6db..e1e0c58450 100644 --- a/docs/plans/task-title-length-limit/task-02-frontend-title-limit.md +++ b/docs/plans/task-title-length-limit/task-02-frontend-title-limit.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/title-length-limit.md" +spec: "../../specs/tasks/requirements/title-length-limit.md" --- # Task 02: Frontend title inputs and remote prefill diff --git a/docs/plans/task-title-length-limit/task-03-e2e-docs.md b/docs/plans/task-title-length-limit/task-03-e2e-docs.md index 03c22735fd..9197ff7731 100644 --- a/docs/plans/task-title-length-limit/task-03-e2e-docs.md +++ b/docs/plans/task-title-length-limit/task-03-e2e-docs.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-backend-mcp-contract", "02-frontend-title-limit"] plan: "plan.md" -spec: "../../specs/tasks/title-length-limit.md" +spec: "../../specs/tasks/requirements/title-length-limit.md" --- # Task 03: E2E coverage and public documentation diff --git a/docs/plans/turn-cancellation-responsiveness/plan.md b/docs/plans/turn-cancellation-responsiveness/plan.md index 17d9cbd827..7ed2d5af9a 100644 --- a/docs/plans/turn-cancellation-responsiveness/plan.md +++ b/docs/plans/turn-cancellation-responsiveness/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/workflow/cancelled-turn-completion/spec.md +spec: docs/specs/tasks/requirements/workflow-cancelled-turn-completion.md created: 2026-08-03 status: complete --- diff --git a/docs/plans/turn-cancellation-responsiveness/task-01-break-cancellation-stream-cycle.md b/docs/plans/turn-cancellation-responsiveness/task-01-break-cancellation-stream-cycle.md index e259d48ed6..daa11dbb4a 100644 --- a/docs/plans/turn-cancellation-responsiveness/task-01-break-cancellation-stream-cycle.md +++ b/docs/plans/turn-cancellation-responsiveness/task-01-break-cancellation-stream-cycle.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/workflow/cancelled-turn-completion/spec.md" +spec: "../../specs/tasks/requirements/workflow-cancelled-turn-completion.md" --- # Task 01: Break the cancellation stream cycle diff --git a/docs/plans/turn-cancellation-responsiveness/task-02-prove-responsive-cancellation-e2e.md b/docs/plans/turn-cancellation-responsiveness/task-02-prove-responsive-cancellation-e2e.md index 137399e56a..59b8c1d59a 100644 --- a/docs/plans/turn-cancellation-responsiveness/task-02-prove-responsive-cancellation-e2e.md +++ b/docs/plans/turn-cancellation-responsiveness/task-02-prove-responsive-cancellation-e2e.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-break-cancellation-stream-cycle"] plan: "plan.md" -spec: "../../specs/workflow/cancelled-turn-completion/spec.md" +spec: "../../specs/tasks/requirements/workflow-cancelled-turn-completion.md" --- # Task 02: Prove responsive cancellation end to end diff --git a/docs/plans/typed-quick-chat-sessions/plan.md b/docs/plans/typed-quick-chat-sessions/plan.md index ee3969434a..3b5fa28a99 100644 --- a/docs/plans/typed-quick-chat-sessions/plan.md +++ b/docs/plans/typed-quick-chat-sessions/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/quick-chat-expiration.md +spec: docs/specs/tasks/requirements/quick-chat-expiration.md created: 2026-07-14 status: implemented --- diff --git a/docs/plans/typed-quick-chat-sessions/task-01-backend-boot-contract.md b/docs/plans/typed-quick-chat-sessions/task-01-backend-boot-contract.md index 94d51cce29..03c315740a 100644 --- a/docs/plans/typed-quick-chat-sessions/task-01-backend-boot-contract.md +++ b/docs/plans/typed-quick-chat-sessions/task-01-backend-boot-contract.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/quick-chat-expiration.md" +spec: "../../specs/tasks/requirements/quick-chat-expiration.md" --- # Task 01: Backend Boot Contract diff --git a/docs/plans/typed-quick-chat-sessions/task-02-unified-frontend-sessions.md b/docs/plans/typed-quick-chat-sessions/task-02-unified-frontend-sessions.md index 277e2f97d8..6347492e5a 100644 --- a/docs/plans/typed-quick-chat-sessions/task-02-unified-frontend-sessions.md +++ b/docs/plans/typed-quick-chat-sessions/task-02-unified-frontend-sessions.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-backend-boot-contract"] plan: "plan.md" -spec: "../../specs/tasks/quick-chat-expiration.md" +spec: "../../specs/tasks/requirements/quick-chat-expiration.md" --- # Task 02: Unified Frontend Sessions diff --git a/docs/plans/typed-quick-chat-sessions/task-03-clarification-responsive-ux.md b/docs/plans/typed-quick-chat-sessions/task-03-clarification-responsive-ux.md index 5c48c2df3c..63206e1d69 100644 --- a/docs/plans/typed-quick-chat-sessions/task-03-clarification-responsive-ux.md +++ b/docs/plans/typed-quick-chat-sessions/task-03-clarification-responsive-ux.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/quick-chat-expiration.md" +spec: "../../specs/tasks/requirements/quick-chat-expiration.md" --- # Task 03: Clarification Responsive UX diff --git a/docs/plans/typed-quick-chat-sessions/task-04-e2e-and-verification.md b/docs/plans/typed-quick-chat-sessions/task-04-e2e-and-verification.md index fad8105d6d..39ec9c2866 100644 --- a/docs/plans/typed-quick-chat-sessions/task-04-e2e-and-verification.md +++ b/docs/plans/typed-quick-chat-sessions/task-04-e2e-and-verification.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-unified-frontend-sessions", "03-clarification-responsive-ux"] plan: "plan.md" -spec: "../../specs/tasks/quick-chat-expiration.md" +spec: "../../specs/tasks/requirements/quick-chat-expiration.md" --- # Task 04: E2E and Verification diff --git a/docs/plans/unlink-task-github-pr/plan.md b/docs/plans/unlink-task-github-pr/plan.md index d059f75540..b08bfbfbea 100644 --- a/docs/plans/unlink-task-github-pr/plan.md +++ b/docs/plans/unlink-task-github-pr/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/link-existing-task-github-issue.md +spec: docs/specs/tasks/requirements/link-existing-task-github-issue.md created: 2026-07-31 status: implemented --- diff --git a/docs/plans/unlink-task-github-pr/task-01-backend-unlink-contract.md b/docs/plans/unlink-task-github-pr/task-01-backend-unlink-contract.md index bf38fcce16..42ed91365c 100644 --- a/docs/plans/unlink-task-github-pr/task-01-backend-unlink-contract.md +++ b/docs/plans/unlink-task-github-pr/task-01-backend-unlink-contract.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/link-existing-task-github-issue.md" +spec: "../../specs/tasks/requirements/link-existing-task-github-issue.md" --- # Task 01: Backend Unlink Contract diff --git a/docs/plans/unlink-task-github-pr/task-02-frontend-multi-pr-unlink.md b/docs/plans/unlink-task-github-pr/task-02-frontend-multi-pr-unlink.md index 37cc3ab970..a34eaf9ff0 100644 --- a/docs/plans/unlink-task-github-pr/task-02-frontend-multi-pr-unlink.md +++ b/docs/plans/unlink-task-github-pr/task-02-frontend-multi-pr-unlink.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-backend-unlink-contract"] plan: "plan.md" -spec: "../../specs/tasks/link-existing-task-github-issue.md" +spec: "../../specs/tasks/requirements/link-existing-task-github-issue.md" --- # Task 02: Frontend Multi-PR Unlink UI diff --git a/docs/plans/unlink-task-github-pr/task-03-pr-unlink-e2e.md b/docs/plans/unlink-task-github-pr/task-03-pr-unlink-e2e.md index 2ec60cb55f..691f50810e 100644 --- a/docs/plans/unlink-task-github-pr/task-03-pr-unlink-e2e.md +++ b/docs/plans/unlink-task-github-pr/task-03-pr-unlink-e2e.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["01-backend-unlink-contract", "02-frontend-multi-pr-unlink"] plan: "plan.md" -spec: "../../specs/tasks/link-existing-task-github-issue.md" +spec: "../../specs/tasks/requirements/link-existing-task-github-issue.md" --- # Task 03: PR Unlink E2E Coverage diff --git a/docs/plans/unlink-task-github-pr/task-04-public-documentation.md b/docs/plans/unlink-task-github-pr/task-04-public-documentation.md index 01fd1930d7..afcba85189 100644 --- a/docs/plans/unlink-task-github-pr/task-04-public-documentation.md +++ b/docs/plans/unlink-task-github-pr/task-04-public-documentation.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/link-existing-task-github-issue.md" +spec: "../../specs/tasks/requirements/link-existing-task-github-issue.md" --- # Task 04: Public Documentation diff --git a/docs/plans/user-question-turn-boundary/plan.md b/docs/plans/user-question-turn-boundary/plan.md index d823a54260..6e7e32bee9 100644 --- a/docs/plans/user-question-turn-boundary/plan.md +++ b/docs/plans/user-question-turn-boundary/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/user-question-turn-boundary.md +spec: docs/specs/tasks/requirements/user-question-turn-boundary.md created: 2026-08-22 status: shipped --- diff --git a/docs/plans/user-question-turn-boundary/task-01-strengthen-question-wait-guidance.md b/docs/plans/user-question-turn-boundary/task-01-strengthen-question-wait-guidance.md index 36d2fa0358..9f216a91d5 100644 --- a/docs/plans/user-question-turn-boundary/task-01-strengthen-question-wait-guidance.md +++ b/docs/plans/user-question-turn-boundary/task-01-strengthen-question-wait-guidance.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/user-question-turn-boundary.md" +spec: "../../specs/tasks/requirements/user-question-turn-boundary.md" --- # Task 01: Strengthen Question Wait Guidance @@ -41,7 +41,7 @@ git diff --check - `apps/backend/internal/sysprompt/sysprompt.go` - `apps/backend/internal/sysprompt/sysprompt_test.go` -- `docs/specs/tasks/user-question-turn-boundary.md` +- `docs/specs/tasks/requirements/user-question-turn-boundary.md` - `docs/specs/INDEX.md` - `docs/plans/user-question-turn-boundary/plan.md` - `docs/plans/user-question-turn-boundary/task-01-strengthen-question-wait-guidance.md` @@ -57,7 +57,7 @@ behavioral repair. ## Inputs -- [User Question Turn Boundary spec](../../specs/tasks/user-question-turn-boundary.md) +- [User Question Turn Boundary spec](../../specs/tasks/requirements/user-question-turn-boundary.md) - [Fix plan](plan.md) - `userQuestionSection`, `parentQuestionSection`, and `autopilotSection` in `apps/backend/internal/sysprompt/sysprompt.go` diff --git a/docs/plans/wip-limited-task-creation/plan.md b/docs/plans/wip-limited-task-creation/plan.md index 047d40e3b6..94c939ebe2 100644 --- a/docs/plans/wip-limited-task-creation/plan.md +++ b/docs/plans/wip-limited-task-creation/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/wip-limit-pull-system.md +spec: docs/specs/tasks/system-design/wip-limit-pull-system.md created: 2026-07-27 status: complete --- diff --git a/docs/plans/wip-limited-task-creation/task-01-atomic-repository-admission.md b/docs/plans/wip-limited-task-creation/task-01-atomic-repository-admission.md index 3693412fe0..d1893d37dd 100644 --- a/docs/plans/wip-limited-task-creation/task-01-atomic-repository-admission.md +++ b/docs/plans/wip-limited-task-creation/task-01-atomic-repository-admission.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 01: Atomic Repository Admission diff --git a/docs/plans/wip-limited-task-creation/task-02-task-service-enforcement.md b/docs/plans/wip-limited-task-creation/task-02-task-service-enforcement.md index 8652830393..0eb406e7cc 100644 --- a/docs/plans/wip-limited-task-creation/task-02-task-service-enforcement.md +++ b/docs/plans/wip-limited-task-creation/task-02-task-service-enforcement.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-atomic-repository-admission"] plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 02: Task-Service WIP Enforcement diff --git a/docs/plans/wip-limited-task-creation/task-03-conflict-adapters-and-watcher-deferral.md b/docs/plans/wip-limited-task-creation/task-03-conflict-adapters-and-watcher-deferral.md index e443e8a144..f8dcb3c593 100644 --- a/docs/plans/wip-limited-task-creation/task-03-conflict-adapters-and-watcher-deferral.md +++ b/docs/plans/wip-limited-task-creation/task-03-conflict-adapters-and-watcher-deferral.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-task-service-enforcement"] plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 03: Conflict Adapters and Review-Watch Deferral diff --git a/docs/plans/wip-limited-task-creation/task-04-public-documentation.md b/docs/plans/wip-limited-task-creation/task-04-public-documentation.md index b4eebb181c..222507ba9f 100644 --- a/docs/plans/wip-limited-task-creation/task-04-public-documentation.md +++ b/docs/plans/wip-limited-task-creation/task-04-public-documentation.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["03-conflict-adapters-and-watcher-deferral"] plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 04: Public WIP Documentation diff --git a/docs/plans/wip-overflow-queues/plan.md b/docs/plans/wip-overflow-queues/plan.md index a7506ba8d0..3c6d543853 100644 --- a/docs/plans/wip-overflow-queues/plan.md +++ b/docs/plans/wip-overflow-queues/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/tasks/wip-limit-pull-system.md +spec: docs/specs/tasks/system-design/wip-limit-pull-system.md created: 2026-07-28 status: in_progress --- diff --git a/docs/plans/wip-overflow-queues/task-01-persist-wip-admission-state.md b/docs/plans/wip-overflow-queues/task-01-persist-wip-admission-state.md index e7ca716a36..f22e61cc74 100644 --- a/docs/plans/wip-overflow-queues/task-01-persist-wip-admission-state.md +++ b/docs/plans/wip-overflow-queues/task-01-persist-wip-admission-state.md @@ -5,7 +5,7 @@ status: completed wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 01: Persist WIP Admission State diff --git a/docs/plans/wip-overflow-queues/task-02-implement-atomic-overflow-placement.md b/docs/plans/wip-overflow-queues/task-02-implement-atomic-overflow-placement.md index 9060d52c32..96434c63da 100644 --- a/docs/plans/wip-overflow-queues/task-02-implement-atomic-overflow-placement.md +++ b/docs/plans/wip-overflow-queues/task-02-implement-atomic-overflow-placement.md @@ -6,7 +6,7 @@ wave: 2 depends_on: - "01-persist-wip-admission-state" plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 02: Implement Atomic Overflow Placement diff --git a/docs/plans/wip-overflow-queues/task-03-reconcile-queued-promotion.md b/docs/plans/wip-overflow-queues/task-03-reconcile-queued-promotion.md index 241bd02415..01a806cd91 100644 --- a/docs/plans/wip-overflow-queues/task-03-reconcile-queued-promotion.md +++ b/docs/plans/wip-overflow-queues/task-03-reconcile-queued-promotion.md @@ -6,7 +6,7 @@ wave: 3 depends_on: - "02-implement-atomic-overflow-placement" plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 03: Reconcile Queued Promotion diff --git a/docs/plans/wip-overflow-queues/task-04-defer-queued-task-launch.md b/docs/plans/wip-overflow-queues/task-04-defer-queued-task-launch.md index 61fcccb8b3..1eaa5cd074 100644 --- a/docs/plans/wip-overflow-queues/task-04-defer-queued-task-launch.md +++ b/docs/plans/wip-overflow-queues/task-04-defer-queued-task-launch.md @@ -6,7 +6,7 @@ wave: 4 depends_on: - "03-reconcile-queued-promotion" plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 04: Defer Queued Task Launch diff --git a/docs/plans/wip-overflow-queues/task-05-adapt-integration-watchers.md b/docs/plans/wip-overflow-queues/task-05-adapt-integration-watchers.md index 3f12b094bf..882f604b32 100644 --- a/docs/plans/wip-overflow-queues/task-05-adapt-integration-watchers.md +++ b/docs/plans/wip-overflow-queues/task-05-adapt-integration-watchers.md @@ -6,7 +6,7 @@ wave: 5 depends_on: - "04-defer-queued-task-launch" plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 05: Adapt Integration Watchers diff --git a/docs/plans/wip-overflow-queues/task-06-expose-visible-queue-ux.md b/docs/plans/wip-overflow-queues/task-06-expose-visible-queue-ux.md index 4db9b4dec6..53200d6a87 100644 --- a/docs/plans/wip-overflow-queues/task-06-expose-visible-queue-ux.md +++ b/docs/plans/wip-overflow-queues/task-06-expose-visible-queue-ux.md @@ -6,7 +6,7 @@ wave: 6 depends_on: - "05-adapt-integration-watchers" plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 06: Expose Visible Queue UX diff --git a/docs/plans/wip-overflow-queues/task-07-verify-browser-flows-and-documentation.md b/docs/plans/wip-overflow-queues/task-07-verify-browser-flows-and-documentation.md index 69abe45cca..00e524ce5f 100644 --- a/docs/plans/wip-overflow-queues/task-07-verify-browser-flows-and-documentation.md +++ b/docs/plans/wip-overflow-queues/task-07-verify-browser-flows-and-documentation.md @@ -6,7 +6,7 @@ wave: 7 depends_on: - "06-expose-visible-queue-ux" plan: "plan.md" -spec: "../../specs/tasks/wip-limit-pull-system.md" +spec: "../../specs/tasks/system-design/wip-limit-pull-system.md" --- # Task 07: Verify Browser Flows and Documentation diff --git a/docs/plans/workflow-cycle-guardrails/plan.md b/docs/plans/workflow-cycle-guardrails/plan.md index 3ebd0b942a..a15a6647ef 100644 --- a/docs/plans/workflow-cycle-guardrails/plan.md +++ b/docs/plans/workflow-cycle-guardrails/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/workflow-cycle-guardrails/spec.md +spec: docs/specs/tasks/requirements/workflow-cycle-guardrails.md created: 2026-07-15 status: complete --- diff --git a/docs/plans/workflow-cycle-guardrails/task-01-cycle-analyzer.md b/docs/plans/workflow-cycle-guardrails/task-01-cycle-analyzer.md index 53b7e1c6f3..0ca27daa6b 100644 --- a/docs/plans/workflow-cycle-guardrails/task-01-cycle-analyzer.md +++ b/docs/plans/workflow-cycle-guardrails/task-01-cycle-analyzer.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/workflow-cycle-guardrails/spec.md" +spec: "../../specs/tasks/requirements/workflow-cycle-guardrails.md" --- # Task 01: Workflow replay cycle analyzer diff --git a/docs/plans/workflow-cycle-guardrails/task-02-mutation-guard.md b/docs/plans/workflow-cycle-guardrails/task-02-mutation-guard.md index 11340e4486..0496f2179f 100644 --- a/docs/plans/workflow-cycle-guardrails/task-02-mutation-guard.md +++ b/docs/plans/workflow-cycle-guardrails/task-02-mutation-guard.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-cycle-analyzer"] plan: "plan.md" -spec: "../../specs/workflow-cycle-guardrails/spec.md" +spec: "../../specs/tasks/requirements/workflow-cycle-guardrails.md" --- # Task 02: Workflow mutation guard diff --git a/docs/plans/workflow-cycle-guardrails/task-03-diagnostic-ui.md b/docs/plans/workflow-cycle-guardrails/task-03-diagnostic-ui.md index ee2ae0a9bd..fe181d6f4a 100644 --- a/docs/plans/workflow-cycle-guardrails/task-03-diagnostic-ui.md +++ b/docs/plans/workflow-cycle-guardrails/task-03-diagnostic-ui.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-mutation-guard"] plan: "plan.md" -spec: "../../specs/workflow-cycle-guardrails/spec.md" +spec: "../../specs/tasks/requirements/workflow-cycle-guardrails.md" --- # Task 03: Workflow cycle diagnostic UI diff --git a/docs/plans/workflow-cycle-guardrails/task-04-e2e-verification.md b/docs/plans/workflow-cycle-guardrails/task-04-e2e-verification.md index 005dfcefb7..2299a3ecac 100644 --- a/docs/plans/workflow-cycle-guardrails/task-04-e2e-verification.md +++ b/docs/plans/workflow-cycle-guardrails/task-04-e2e-verification.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["03-diagnostic-ui"] plan: "plan.md" -spec: "../../specs/workflow-cycle-guardrails/spec.md" +spec: "../../specs/tasks/requirements/workflow-cycle-guardrails.md" --- # Task 04: Workflow cycle guardrail E2E verification diff --git a/docs/plans/workflow-duplication/plan.md b/docs/plans/workflow-duplication/plan.md index 914b9073d9..6e7586e311 100644 --- a/docs/plans/workflow-duplication/plan.md +++ b/docs/plans/workflow-duplication/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/workflow-duplication/spec.md +spec: docs/specs/tasks/requirements/workflow-duplication.md created: 2026-08-11 status: complete --- diff --git a/docs/plans/workflow-duplication/task-01-build-duplication-drafts.md b/docs/plans/workflow-duplication/task-01-build-duplication-drafts.md index 8ca0f43569..74daf95b0f 100644 --- a/docs/plans/workflow-duplication/task-01-build-duplication-drafts.md +++ b/docs/plans/workflow-duplication/task-01-build-duplication-drafts.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/workflow-duplication/spec.md" +spec: "../../specs/tasks/requirements/workflow-duplication.md" --- # Task 01: Build Duplication Drafts diff --git a/docs/plans/workflow-duplication/task-02-expose-duplicate-action.md b/docs/plans/workflow-duplication/task-02-expose-duplicate-action.md index 9804ab407f..55f7d1dec9 100644 --- a/docs/plans/workflow-duplication/task-02-expose-duplicate-action.md +++ b/docs/plans/workflow-duplication/task-02-expose-duplicate-action.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-build-duplication-drafts"] plan: "plan.md" -spec: "../../specs/workflow-duplication/spec.md" +spec: "../../specs/tasks/requirements/workflow-duplication.md" --- # Task 02: Expose Duplicate Action diff --git a/docs/plans/workflow-duplication/task-03-prove-duplication-flows.md b/docs/plans/workflow-duplication/task-03-prove-duplication-flows.md index f0789e0cda..3fc2e330dd 100644 --- a/docs/plans/workflow-duplication/task-03-prove-duplication-flows.md +++ b/docs/plans/workflow-duplication/task-03-prove-duplication-flows.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["02-expose-duplicate-action"] plan: "plan.md" -spec: "../../specs/workflow-duplication/spec.md" +spec: "../../specs/tasks/requirements/workflow-duplication.md" --- # Task 03: Prove Duplication Flows diff --git a/docs/plans/workflow-session-settings/plan.md b/docs/plans/workflow-session-settings/plan.md index 3aa5bb129b..6cfbd50699 100644 --- a/docs/plans/workflow-session-settings/plan.md +++ b/docs/plans/workflow-session-settings/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/workflow-session-settings/spec.md +spec: docs/specs/tasks/requirements/workflow-session-settings.md decision: docs/decisions/2026-08-01-workflow-session-original-configuration.md created: 2026-08-01 status: complete diff --git a/docs/plans/workflow-session-settings/task-01-workflow-contract-and-validation.md b/docs/plans/workflow-session-settings/task-01-workflow-contract-and-validation.md index d5d6bb875f..fe8ff668ce 100644 --- a/docs/plans/workflow-session-settings/task-01-workflow-contract-and-validation.md +++ b/docs/plans/workflow-session-settings/task-01-workflow-contract-and-validation.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/workflow-session-settings/spec.md" +spec: "../../specs/tasks/requirements/workflow-session-settings.md" --- # Task 01: Add and Validate the Workflow Session-settings Contract diff --git a/docs/plans/workflow-session-settings/task-02-original-session-initialization.md b/docs/plans/workflow-session-settings/task-02-original-session-initialization.md index c15b565424..e6d97d9dc7 100644 --- a/docs/plans/workflow-session-settings/task-02-original-session-initialization.md +++ b/docs/plans/workflow-session-settings/task-02-original-session-initialization.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-workflow-contract-and-validation"] plan: "plan.md" -spec: "../../specs/workflow-session-settings/spec.md" +spec: "../../specs/tasks/requirements/workflow-session-settings.md" --- # Task 02: Capture the Original Session Configuration Before Workflow Overrides diff --git a/docs/plans/workflow-session-settings/task-03-runtime-rule-application.md b/docs/plans/workflow-session-settings/task-03-runtime-rule-application.md index b2a5e68d49..f333759f4a 100644 --- a/docs/plans/workflow-session-settings/task-03-runtime-rule-application.md +++ b/docs/plans/workflow-session-settings/task-03-runtime-rule-application.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["01-workflow-contract-and-validation", "02-original-session-initialization"] plan: "plan.md" -spec: "../../specs/workflow-session-settings/spec.md" +spec: "../../specs/tasks/requirements/workflow-session-settings.md" --- # Task 03: Apply Matching Workflow Rules to the Original Session diff --git a/docs/plans/workflow-session-settings/task-04-editor-and-carry-analysis.md b/docs/plans/workflow-session-settings/task-04-editor-and-carry-analysis.md index a46ab15701..251a0df507 100644 --- a/docs/plans/workflow-session-settings/task-04-editor-and-carry-analysis.md +++ b/docs/plans/workflow-session-settings/task-04-editor-and-carry-analysis.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-workflow-contract-and-validation"] plan: "plan.md" -spec: "../../specs/workflow-session-settings/spec.md" +spec: "../../specs/tasks/requirements/workflow-session-settings.md" --- # Task 04: Build the Conditional Rule Editor and Carry-forward Diagnostics diff --git a/docs/plans/workflow-session-settings/task-05-e2e-docs-and-verification.md b/docs/plans/workflow-session-settings/task-05-e2e-docs-and-verification.md index f2970cddd8..a9f43c927e 100644 --- a/docs/plans/workflow-session-settings/task-05-e2e-docs-and-verification.md +++ b/docs/plans/workflow-session-settings/task-05-e2e-docs-and-verification.md @@ -5,7 +5,7 @@ status: done wave: 4 depends_on: ["01-workflow-contract-and-validation", "02-original-session-initialization", "03-runtime-rule-application", "04-editor-and-carry-analysis"] plan: "plan.md" -spec: "../../specs/workflow-session-settings/spec.md" +spec: "../../specs/tasks/requirements/workflow-session-settings.md" --- # Task 05: Verify Workflow Session Settings and Document the Feature diff --git a/docs/plans/workflow-session-settings/task-06-workflow-session-options-toggle.md b/docs/plans/workflow-session-settings/task-06-workflow-session-options-toggle.md index d2d4129492..8069191092 100644 --- a/docs/plans/workflow-session-settings/task-06-workflow-session-options-toggle.md +++ b/docs/plans/workflow-session-settings/task-06-workflow-session-options-toggle.md @@ -5,7 +5,7 @@ status: done wave: 5 depends_on: ["04-editor-and-carry-analysis"] plan: "plan.md" -spec: "../../specs/workflow-session-settings/spec.md" +spec: "../../specs/tasks/requirements/workflow-session-settings.md" --- # Task 06: Refine the workflow session-options toggle and family choices diff --git a/docs/plans/workflow-settings-autosave/plan.md b/docs/plans/workflow-settings-autosave/plan.md index 681cdc6cdb..2f7f237a32 100644 --- a/docs/plans/workflow-settings-autosave/plan.md +++ b/docs/plans/workflow-settings-autosave/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/workflow-settings-autosave/spec.md +spec: docs/specs/tasks/requirements/workflow-settings-autosave.md created: 2026-07-14 status: superseded superseded_by: docs/plans/settings-manual-save/plan.md diff --git a/docs/plans/workflow-settings-autosave/task-01-autosave-state.md b/docs/plans/workflow-settings-autosave/task-01-autosave-state.md index 9626c5d2ed..bc514b22fa 100644 --- a/docs/plans/workflow-settings-autosave/task-01-autosave-state.md +++ b/docs/plans/workflow-settings-autosave/task-01-autosave-state.md @@ -5,7 +5,7 @@ status: done wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/workflow-settings-autosave/spec.md" +spec: "../../specs/tasks/requirements/workflow-settings-autosave.md" --- # Task 01: Autosave State diff --git a/docs/plans/workflow-settings-autosave/task-02-responsive-layout.md b/docs/plans/workflow-settings-autosave/task-02-responsive-layout.md index 9fb7553297..4665283e8b 100644 --- a/docs/plans/workflow-settings-autosave/task-02-responsive-layout.md +++ b/docs/plans/workflow-settings-autosave/task-02-responsive-layout.md @@ -5,7 +5,7 @@ status: done wave: 2 depends_on: ["01-autosave-state"] plan: "plan.md" -spec: "../../specs/workflow-settings-autosave/spec.md" +spec: "../../specs/tasks/requirements/workflow-settings-autosave.md" --- # Task 02: Responsive Workflow Editor Layout diff --git a/docs/plans/workflow-settings-autosave/task-03-e2e-coverage.md b/docs/plans/workflow-settings-autosave/task-03-e2e-coverage.md index 36fc1f923d..deb9b999b3 100644 --- a/docs/plans/workflow-settings-autosave/task-03-e2e-coverage.md +++ b/docs/plans/workflow-settings-autosave/task-03-e2e-coverage.md @@ -5,7 +5,7 @@ status: done wave: 3 depends_on: ["01-autosave-state", "02-responsive-layout"] plan: "plan.md" -spec: "../../specs/workflow-settings-autosave/spec.md" +spec: "../../specs/tasks/requirements/workflow-settings-autosave.md" --- # Task 03: Workflow Autosave E2E Coverage diff --git a/docs/plans/workflow-step-agent-start-ownership/plan.md b/docs/plans/workflow-step-agent-start-ownership/plan.md index 9c6b62a5ad..7999eb048d 100644 --- a/docs/plans/workflow-step-agent-start-ownership/plan.md +++ b/docs/plans/workflow-step-agent-start-ownership/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/workflow-step-agent-start-ownership/spec.md +spec: docs/specs/tasks/requirements/workflow-step-agent-start-ownership.md created: 2026-08-05 status: complete --- diff --git a/docs/plans/workflow-step-agent-start-ownership/task-01-reset-skips-created.md b/docs/plans/workflow-step-agent-start-ownership/task-01-reset-skips-created.md index 996b21e52b..06a352d3f9 100644 --- a/docs/plans/workflow-step-agent-start-ownership/task-01-reset-skips-created.md +++ b/docs/plans/workflow-step-agent-start-ownership/task-01-reset-skips-created.md @@ -6,7 +6,7 @@ wave: 1 parallelism: sequential depends_on: [] plan: "plan.md" -spec: "../../specs/workflow-step-agent-start-ownership/spec.md" +spec: "../../specs/tasks/requirements/workflow-step-agent-start-ownership.md" --- # Task 01: Context reset skips never-prompted sessions diff --git a/docs/plans/workflow-step-agent-start-ownership/task-02-queue-prompt-on-failed-launch.md b/docs/plans/workflow-step-agent-start-ownership/task-02-queue-prompt-on-failed-launch.md index 02c07677fc..d24db04877 100644 --- a/docs/plans/workflow-step-agent-start-ownership/task-02-queue-prompt-on-failed-launch.md +++ b/docs/plans/workflow-step-agent-start-ownership/task-02-queue-prompt-on-failed-launch.md @@ -6,7 +6,7 @@ wave: 2 parallelism: sequential depends_on: ["01-reset-skips-created"] plan: "plan.md" -spec: "../../specs/workflow-step-agent-start-ownership/spec.md" +spec: "../../specs/tasks/requirements/workflow-step-agent-start-ownership.md" --- # Task 02: Queue the auto-start prompt when a CREATED launch fails diff --git a/docs/plans/workflow-sync-workspace-authz/plan.md b/docs/plans/workflow-sync-workspace-authz/plan.md index b9e926bc1c..700964a46e 100644 --- a/docs/plans/workflow-sync-workspace-authz/plan.md +++ b/docs/plans/workflow-sync-workspace-authz/plan.md @@ -1,5 +1,5 @@ --- -spec: docs/specs/workflow-sync-workspace-authz/spec.md +spec: docs/specs/tasks/requirements/workflow-sync-workspace-authz.md created: 2026-08-06 status: building --- diff --git a/docs/plans/workflow-sync-workspace-authz/task-01-service-authorization.md b/docs/plans/workflow-sync-workspace-authz/task-01-service-authorization.md index 5a2f3e6cfa..752d05eda2 100644 --- a/docs/plans/workflow-sync-workspace-authz/task-01-service-authorization.md +++ b/docs/plans/workflow-sync-workspace-authz/task-01-service-authorization.md @@ -5,7 +5,7 @@ status: pending wave: 1 depends_on: [] plan: "plan.md" -spec: "../../specs/workflow-sync-workspace-authz/spec.md" +spec: "../../specs/tasks/requirements/workflow-sync-workspace-authz.md" --- # Task 01: Service-layer workspace authorization diff --git a/docs/plans/workflow-sync-workspace-authz/task-02-handler-error-mapping.md b/docs/plans/workflow-sync-workspace-authz/task-02-handler-error-mapping.md index bf4245e799..df3c172419 100644 --- a/docs/plans/workflow-sync-workspace-authz/task-02-handler-error-mapping.md +++ b/docs/plans/workflow-sync-workspace-authz/task-02-handler-error-mapping.md @@ -5,7 +5,7 @@ status: pending wave: 1 depends_on: ["01-service-authorization"] plan: "plan.md" -spec: "../../specs/workflow-sync-workspace-authz/spec.md" +spec: "../../specs/tasks/requirements/workflow-sync-workspace-authz.md" --- # Task 02: Handler error mapping + HTTP regression tests diff --git a/docs/specs/INDEX.md b/docs/specs/INDEX.md index f2dc7f55d6..7b90c633e8 100644 --- a/docs/specs/INDEX.md +++ b/docs/specs/INDEX.md @@ -75,60 +75,10 @@ Product-wide capabilities that are not tied to a single feature area. ## tasks/ — task & workflow model -Kandev's task model: documents, execution stages, labels, blocker escalation, subtask checklists, subtree controls, and the unification with the workflow engine. - -| Spec | Status | -|---|---| -| [documents](tasks/documents.md) | shipped | -| [execution-stages](tasks/execution-stages.md) | shipped | -| [interrupted-task-indicator](tasks/interrupted-task-indicator.md) | complete | -| [labels](tasks/labels.md) | shipped | -| [title-length-limit](tasks/title-length-limit.md) | complete | -| [rich-task-title-previews](tasks/rich-task-title-previews.md) | building | -| [active clarification lifecycle](clarification-active-lifecycle/spec.md) | approved | -| [model-unification](tasks/model-unification.md) | draft | -| [run-scheduling](tasks/run-scheduling.md) | building | -| [without-repositories](tasks/without-repositories.md) | draft | -| [attach-workspace-sources](tasks/attach-workspace-sources.md) | building | -| [remote-contribution-tasks](tasks/remote-contribution-tasks.md) | approved | -| [subtask-checklist](tasks/subtask-checklist.md) | shipped | -| [subtask-detachment](tasks/subtask-detachment.md) | shipped | -| [subtask-reparenting-drag-drop](tasks/subtask-reparenting-drag-drop.md) | building | -| [subtask-completion-trigger](tasks/subtask-completion-trigger.md) | draft | -| [task-dependencies](task-dependencies/spec.md) | draft | -| [task-dependencies - create dialog selector](task-dependencies/create-dialog-dependency-selector.md) | implemented | -| [task-dependencies - create dialog advanced settings](task-dependencies/create-dialog-advanced-settings.md) | shipped | -| [subtree-controls](tasks/subtree-controls.md) | shipped | -| [blocked-task-escalation](tasks/blocked-task-escalation.md) | draft | -| [runtime-cleanup](tasks/runtime-cleanup.md) | draft | -| [session-delete-resource-cleanup](session-delete-resource-cleanup/spec.md) | draft | -| [archive-confirmation](tasks/archive-confirmation.md) | shipped | -| [link-existing-task-github-issue](tasks/link-existing-task-github-issue.md) | building | -| [wip-limit-pull-system](tasks/wip-limit-pull-system.md) | draft | -| [multi-branch](tasks/multi-branch/spec.md) | shipped | -| [quick-chat-sessions](tasks/quick-chat-expiration.md) | shipped | -| [quick-chat-repository-context](tasks/quick-chat-repository-context.md) | shipped | -| [parent-child-message-interrupt](tasks/parent-child-message-interrupt.md) | shipped | -| [parent-child-task-stop](tasks/parent-child-task-stop.md) | shipped | -| [mcp-task-agent-profile-default](tasks/mcp-task-agent-profile-default/spec.md) | shipped | -| [spawn-session-effective-profile](spawn-session-effective-profile/spec.md) | shipped | -| [runtime-state-publication-order](tasks/runtime-state-publication-order.md) | shipped | -| [agent-generated-titles](tasks/agent-generated-titles.md) | approved | -| [task-create-executor-default](tasks/task-create-executor-default.md) | approved | -| [task-create-workflow-memory](tasks/task-create-workflow-memory.md) | approved | -| [task-create-escape-dismissal](tasks/task-create-escape-dismissal.md) | complete | -| [repository-sets](repository-sets/spec.md) | building | -| [external-id-idempotency](tasks/external-id-idempotency/spec.md) | draft | -| [prompt-attachments](tasks/prompt-attachments.md) | draft | -| [sidebar-task-edit](tasks/sidebar-task-edit.md) | approved | -| [autopilot-mode](tasks/autopilot-mode.md) | draft | -| [user-question-turn-boundary](tasks/user-question-turn-boundary.md) | shipped | -| [explicit-completion-signal](workflow/explicit-completion-signal/spec.md) | shipped | -| [cancelled-turn-completion](workflow/cancelled-turn-completion/spec.md) | building | -| [task-step-transition-ledger](workflow/task-step-transition-ledger/spec.md) | draft | -| [conditional-session-settings](workflow-session-settings/spec.md) | approved | -| [prevent-agent-autostart-on-open](prevent-agent-autostart-on-open/spec.md) | draft | -| [workflow-duplication](workflow-duplication/spec.md) | draft | +The task and workflow system has a system index and new authoritative +requirement and design paths in [tasks/README.md](tasks/README.md). +All task and workflow specifications are now indexed under that system +directory; this legacy catalog has no remaining task-owned editable sources. ## agents/ — agent governance @@ -303,8 +253,6 @@ System pages (Radarr/Sonarr-style) for status, disk usage, database maintenance, | [external-question-answering](external-question-answering/spec.md) | draft | | [auth](auth/spec.md) | building | | [create-local-repository](create-local-repository/spec.md) | shipped | -| [workflow-cycle-guardrails](workflow-cycle-guardrails/spec.md) | building | -| [workflow-quorum-decision-recording](workflow-quorum-decision-recording/spec.md) | draft | | [improve-kandev](improve-kandev/spec.md) | building | | [homebrew-core](homebrew-core/spec.md) | building | | [native-kandev-cli](native-kandev-cli/spec.md) | draft | @@ -315,7 +263,6 @@ System pages (Radarr/Sonarr-style) for status, disk usage, database maintenance, | [ssh-executor](ssh-executor/spec.md) | draft | | [cli-mode-parity](cli-mode-parity/spec.md) | draft | | [claude-fork-review-allowlist](claude-fork-review-allowlist/spec.md) | building | -| [workflow-settings-autosave](workflow-settings-autosave/spec.md) | archived; superseded by settings-manual-save | | [mobile-quick-chat-topbar](mobile-quick-chat-topbar/spec.md) | building | | [quick-chat-idle-dot](quick-chat-idle-dot/spec.md) | draft | | [native-code-review](native-code-review/spec.md) | building | @@ -331,7 +278,6 @@ System pages (Radarr/Sonarr-style) for status, disk usage, database maintenance, | [shutdown-turn-failure-suppression](shutdown-turn-failure-suppression/spec.md) | draft | | [executor-profile-env-precedence](executor-profile-env-precedence/spec.md) | building | | [automations-yaml-export](automations-yaml-export/spec.md) | building | -| [task-launch-failure-recovery](task-launch-failure-recovery/spec.md) | draft | | [pr-walkthrough](pr-walkthrough/spec.md) | building | --- diff --git a/docs/specs/clarification-active-lifecycle/spec.md b/docs/specs/clarification-active-lifecycle/spec.md deleted file mode 100644 index 547e2dab24..0000000000 --- a/docs/specs/clarification-active-lifecycle/spec.md +++ /dev/null @@ -1,462 +0,0 @@ ---- -status: approved -created: 2026-08-14 -owner: kandev ---- - -# Active Clarification Lifecycle - -## Why - -A clarification can outlive the agent wait that created it. Kandev keeps that detached question -answerable so a timeout or connection loss does not discard required user input. That durability must -not let a question from an older turn remain operational after the session has accepted newer work. - -One stale clarification currently can reappear in chat, restore a task-row question icon, and block a -workflow transition after the user has dismissed a newer question. A real question in a secondary -session can also produce a correct task-row icon while task navigation opens the clean primary session, -hiding the action the icon represents. - -## What - -- A clarification bundle is active only when at least one row in the bundle is pending and the bundle - belongs to the session's current turn. -- A detached bundle remains active and answerable while its turn remains current. Detachment sets - `agent_disconnected=true`; it does not by itself resolve the question. -- After a session successfully enters a terminal state, its current-turn pending clarification bundles - expire so no answerable overlay survives a completed, failed, or cancelled session. -- Acceptance of a newer turn supersedes every pending clarification from an older turn. Superseded - rows remain transcript history but cannot drive a chat overlay, task/session pending projection, - workflow guard, turn-completion detach pass, or late agent resume. -- Deleting every message from the newer turn does not move ownership backward or reactivate an older - clarification. -- All backend consumers derive active clarification state from one repository rule. Event payloads - trigger projection refreshes; they are not a second source of pending truth. -- Repeated detach/completion processing is a semantic no-op after a bundle is already detached. It - emits no duplicate `message.updated` occurrence. -- Detachment claims pending, non-detached rows from the current durable turn in one database update - and publishes only the rows that update returns. A concurrent answer or newer turn cannot be - overwritten by a stale read-modify-write detachment. -- Clarification-pause cancellation snapshots turn authority before detachment. With a wired turn - service, both a specific turn and the absence of a turn are explicit expectations; a first or - successor turn created during detachment cannot be cancelled by the stale pause. Installations - without a turn service retain the legacy unscoped fallback. -- Resolving, rejecting, cancelling, expiring, or deleting one bundle changes only that bundle. It - cannot clear or re-arm another bundle in the same session. -- The chat's Skip action rejects the exact visible bundle through the existing response endpoint. A - live waiter receives the rejection in the same turn. A detached current-turn bundle is persisted as - rejected without resuming the agent. -- An affirmative response to a detached current-turn bundle returns success only after the - orchestrator accepts one resume dispatch within a bounded wait. The response waits for prompt - acknowledgement, not agent-turn completion. Before dispatch, the successor is durably reserved but - marked unpublished so provider frames can reference it without making it current. Immediately before - the external executor call, Kandev durably marks the reservation attempted. That marker is the - at-most-once boundary: a crash can no longer prove whether agentctl accepted the prompt, so restart - preserves the successor and keeps the claimed bundle terminal. Acknowledgement publishes a - recovery-clean `turn.started` payload while the recovery metadata remains durable, then clears that - metadata after the event bus accepts the event. HTTP success requires both operations. A rejected - event publication therefore remains discoverable by startup reconciliation. Recovery atomically - replaces an accepted or ambiguous reservation with a durable start-event outbox marker. Before - admitting work, startup replays `turn.started`, followed by `turn.completed` when the turn is already - terminal, and clears the marker only after the event bus accepts every required event. Failed replay - fails startup and leaves the marker for the next attempt. Every public turn event, including a - completion racing live cleanup or emitted during recovery, strips the private prompt-dispatch fields. - If the predecessor's delayed ready event overlaps this private reservation, ready handling waits for the - reservation to resolve and then revalidates prompt generation before touching turn or workflow state. - It cannot complete the reserved successor or run predecessor completion actions against it. When the - reservation rolls back, a ready event whose predecessor generation still owns the session continues - through normal completion so the predecessor and its queue are not stranded. A generationless ready - event that overlapped the reservation is dropped after resolution because ownership cannot be proven. - If the attempt marker cannot be persisted, dispatch does not occur and a fresh bounded context rolls - back the reservation and session claim even when the transport context was cancelled, so the answer - remains retryable. - If agentctl synchronously rejects the prompt, the reservation is rolled back and the answer can be - restored. If agentctl accepts the prompt but publication or later transport handling fails, the - endpoint returns a server error, performs normal prompt-failure cleanup, and keeps the claimed bundle - terminal because retrying could dispatch the answer twice. Startup - deletes only an empty, unattempted unpublished reservation and restores the exact clarification rows - claimed for its dispatch; an attempt marker or message evidence instead proves dispatch ambiguity and - preserves the successor. Authority and recovery treat boolean `true`, strings `"true"` and `"1"`, - and numeric `1` as equivalent pending/attempted flags across SQLite and PostgreSQL. If reservation - reconciliation is unavailable or fails, orchestrator startup - fails before watcher, scheduler, or prompt admission starts; the next start retries recovery. A - production turn repository must provide this recovery capability through its compile-time contract. - A rejection persists terminal status without resuming the agent. -- Every response atomically claims current-turn ownership and persists a response-delivery recovery - intent before it can reach a live waiter or request a detached resume. A live waiter runs durable - delivery confirmation before returning the response to the agent; enqueue alone does not retire the - intent. Once confirmation starts, it owns its durable operation through completion even if the - responder's bounded wait expires. Its input claim remains immutable, its result remains local to the - callback, and any compensating restore serializes against finalization so only one durable outcome - wins. The detached path retires the intent only at its durable resume boundary. Startup first - reconciles prompt reservations, then restores an unhanded current-turn claim to pending; a terminal - session or newer authoritative turn instead retires the stale intent without reactivating history. - Terminal message updates are published only after delivery succeeds. If detached - resume acceptance fails, the endpoint returns an error and restores the still-current bundle to - pending so the same answer can be retried. Restored rows publish after commit even when synchronous - task-summary acknowledgement fails, preventing clients from retaining the terminal snapshot while the - endpoint still returns the acknowledgement error. A publication or summary-convergence error after - the database restore does not make that retry unsafe; durable pending state remains authoritative. - Once agentctl accepts the prompt, later publication or completion errors cannot roll back the - successor turn or reopen the answer. A primary-answer watchdog carries the clarification turn ID - and revalidates that ID both before fallback and inside serialized prompt admission, so it cannot - dispatch a stale answer into a successor turn. Its fallback keeps the watchdog cancellation context - through authority reads and prompt admission so session activity or service shutdown interrupts - in-flight recovery work. -- A current-turn bundle remains answerable while any sibling question is pending. Recovery claims only - those pending rows, preserves siblings already made terminal by an earlier partial write, and restores - only the claimed rows if detached delivery fails. Primary delivery events and detached recovery derive - one turn identity from the same bundle rule: legacy empty turn IDs do not mask a consistent non-empty - identity, while conflicting non-empty IDs invalidate the identity. -- Any response to a superseded or terminal bundle returns conflict, performs no message mutation, and - initiates no agent resume. Current clients close their obsolete local overlay through the existing - conflict handling. -- Persisted task status summaries reconcile `pending_action` against current-turn repository state on - source events and task-list/boot reads. Existing summaries are repaired, not only missing rows. -- When a task row advertises a pending action, desktop and phone task activation load the task's - sessions from the server and select the newest input-capable session whose `pending_action` matches - the task action. Before applying that response, activation revalidates the task-summary revision and - pending action. If either changes while the task remains present, activation discards the delayed - session choice and opens the task-only route; phone activation also closes the sheet. Overlapping - authoritative loads are generation-guarded per task, so an older response cannot replace a newer - session snapshot. This pending owner outranks remembered-session and primary-session preferences. If - the task still advertises pending input but no matching input-capable owner exists, activation releases - the outgoing session layout and fails closed to the task route without guessing a session. Normal - preference order returns only for a clean task. If the task projection disappears while that - authoritative load is in flight, desktop and phone leave the selection inert instead of navigating to - a deleted task, including tasks using the legacy pending-action projection. A forced load aborted by a - newer load is also inert; it is not treated as a request failure requiring task-only fallback. Each - mounted phone task sheet owns its selection generation, so simultaneous instances cannot invalidate - one another's in-flight task choice. - -## Data model - -No schema change. - -- Clarification questions remain `task_session_messages` rows with - `type = "clarification_request"`. -- Rows in one bundle share `metadata.pending_id`; terminal status remains in `metadata.status`. -- `task_session_messages.turn_id` associates a question with its turn. The newest authoritative durable - `task_session_turns` record for the session identifies the current turn; deleting messages does not - delete that parent turn or move ownership backward. -- A pre-acknowledgement successor carries `metadata.prompt_dispatch_pending=true` plus the source - clarification turn, pending ID, and exact claimed message IDs. An empty row carrying only the pending - marker is not turn authority. Immediately before external dispatch, - `metadata.prompt_dispatch_attempted=true` records the at-most-once boundary and makes the successor - authoritative if the process stops in the ambiguous window. A message referencing the successor is - independent durable evidence of ambiguous acceptance. -- `metadata.agent_disconnected=true` records that no in-memory waiter owns an otherwise active bundle. -- A superseded row may retain `metadata.status = "pending"`. Pending metadata is historical evidence, - not sufficient proof that the request is operational. -- A missing-status row in an older turn is superseded history. Turn ownership takes precedence over - the legacy rule that missing status means pending. -- `TaskSession.pending_action` and `TaskStatusSummary.pending_action` remain bounded derived fields. - They are reconstructable and never become independent clarification state. - -## API surface - -No new route. Task-session responses add `pending_action_revision` beside the existing -`pending_action` projection. - -- `GET /api/v1/tasks/:taskId/task-sessions` continues to expose each session's current derived - `pending_action`; task navigation uses this existing field. -- Semantic `session.message.added`, `session.message.updated`, and `session.message.deleted` - notifications include the authoritative per-session `pending_action` after mutations that can - change it. The field is explicit null when clean and omitted when projection fails or the event is - a replaceable content-only update; clients preserve the prior value when it is omitted. Each - projection includes a `pending_action_revision`, shared with REST task-session snapshots. Its - decimal epoch is a database-backed, monotonically allocated backend generation; sequence orders - reads within that generation. Clients compare both fields and reject any older result, including - an unseen pre-restart epoch delivered after client state has been rebuilt. -- `GET /api/v1/task-sessions/:sessionId/turns` continues to expose durable turn history; - unpublished reservations stay hidden until publication or durable message evidence, including while - an attempt marker makes them internal current-turn authority. Attempted reservations are preserved - across restart because their dispatch outcome is ambiguous. Visible history is ordered ascending by - `started_at`, `created_at`, then `id`, matching the reverse ordering used to select the current turn. -- Task list, workflow snapshot, and boot payloads continue to expose task-level `pending_action` in - the status summary and legacy fallback fields. -- `POST /api/v1/clarification/:pendingId/respond` uses one state-based contract: - - `active_live`: answer or rejection returns success and is delivered to the same-turn waiter. - - `active_detached`: an answer returns success only after the orchestrator acknowledges one resume - dispatch and durably publishes its successor within a bounded wait; it does not wait for the resumed - turn to complete. Rejection returns success and persists without resuming the agent. If resume - acceptance fails, an answer returns a server error and the still-current bundle remains answerable - for retry. If acceptance succeeds but successor publication fails, the endpoint returns a - non-retryable server error and keeps the bundle terminal. - - `superseded_history` or `terminal`: answer or rejection returns conflict, performs no write, and - initiates no agent resume. - - `delivery_claimed`: a concurrent second response returns conflict, performs no write, and - initiates no agent resume because the first response already claimed every pending row. -- `POST /api/v1/clarification/:pendingId/cancel` remains the low-level cancellation path for a request - still owned by the in-memory clarification store. The chat Skip control uses `/respond` with - `rejected=true`, including for detached requests. - -## State machine - -One clarification bundle has five operational states: - -1. `active_live`: rows are pending in the current turn and an in-memory waiter exists. -2. `active_detached`: rows are pending in the current turn, no waiter exists, and - `agent_disconnected=true` records deferred-answer behavior. -3. `delivery_claimed`: current-turn rows carry their provisional terminal answer plus a durable - response-delivery intent, but no handoff boundary has been acknowledged yet. -4. `terminal`: every actionable row is answered, rejected, cancelled, expired, or deleted, with no - outstanding delivery intent. -5. `superseded_history`: rows still carry pending history, but a newer turn is current. - -Transitions: - -- Request creation enters `active_live`. -- Wait timeout, disconnect, or turn teardown moves `active_live -> active_detached` once. -- A response first moves either active state to `delivery_claimed`. Successful answer delivery or Skip - then moves that exact `pending_id` to `terminal`. For a live response, the waiter must durably confirm - consumption before returning it to the agent. A backend restart before any handoff restores a - still-current `delivery_claimed` bundle to its prior active state; if a newer turn or terminal session - already superseded it, recovery retires the intent and preserves terminal history. Cancel, expiry, - or deletion moves an active state directly to `terminal`. A failed detached resume acceptance returns to - `active_detached` while the same turn remains current only when dispatch is known not to have been - accepted and rollback succeeds. A post-attempt crash or post-acceptance publication failure remains - terminal because the prompt may already be running. -- Acceptance of a newer turn moves any older pending bundle to `superseded_history` operationally; - no history rewrite is required. -- Neither `terminal` nor `superseded_history` can become active again. Only the provisional - `delivery_claimed` state is recoverable. A new request creates a new - bundle identity; message deletion cannot reverse this transition. - -## Permissions - -No authorization change. A user can see, answer, or dismiss only clarification data for a task and -session they can already access. Session selection does not broaden task visibility. - -## Failure modes - -- Active-state repository read fails: workflow guarding fails closed, and projections keep the last - known pending value. A later message event or list/boot read retries convergence. -- Terminal-session expiry persistence fails: the terminal session state still quarantines pending - history from task/session projections and interactive overlays; a stale response claim remains - rejected by the terminal-session predicate. -- A forced task-session list cannot project authoritative pending actions: fail the HTTP or WebSocket - request instead of returning a successful list with empty pending ownership. -- Summary compare-and-set loses a race: reload the newer summary, reapply authoritative pending state, - and retry within a bounded loop. Exhaustion is an error, so restored-state acknowledgement is withheld - until the pending action is durably confirmed. Task-list and boot reads explicitly invalidate that - task's stale summary on repair failure so an unchanged browser cache clears it and exposes the - authoritative coarse pending action. Ordinary summary omission remains a partial-response no-op, and - an invalidating response cannot erase a newer live summary received while that read was in flight. - Never overwrite unrelated newer summary fields. -- Summary repair persists but its WebSocket publication fails: the initiating response carries the - corrected summary; other clients converge on their next event or read. -- A stale browser submits an older-turn answer: return conflict, do not update runtime ownership, and - do not dispatch a prompt. -- Detached resume context resolution or orchestrator acceptance fails: use a non-cancelled context with - a finite deadline for acceptance and persistence, withhold terminal message events, restore the - still-current bundle to pending, and return a retryable server error instead of reporting false - success. Attempt to refresh and persist the task summary synchronously from authoritative pending - rows, and publish the committed restored messages even if that acknowledgement fails. A later event - or read repairs the summary cache; the response still reports that the durable answer can be retried. -- Persisting a successor turn or dispatch-attempt marker fails: use a fresh bounded context to roll back - the session claim and any reserved successor before making an external executor call, restore the - still-current bundle, and return a retryable server error. -- Reserved-successor rollback fails or has an ambiguous durable outcome: keep the live reservation - unresolved so ready handling waits and later prompt admission remains blocked until restart recovery - reconciles the row. -- Clarification detachment, expiry persistence, and terminal bundle publication ignore request - cancellation but always use a fresh bounded context, so a database lock or synchronous summary - refresh cannot hold the per-session pause or HTTP response indefinitely. -- Agentctl accepts a detached resume but durable successor publication fails: return a non-retryable - server error, keep the claimed bundle terminal, and make later rollback fail closed so the accepted - answer cannot be dispatched again in-process. -- Live waiter delivery fails unexpectedly: restore the durable claim only if its turn remains current, - report whether retry state was recovered, and never restore it after a successor turn is accepted. A - started confirmation may finish after the responder's bounded wait, but it cannot mutate the - responder's claim snapshot and its durable finalization serializes against that restore. -- Historical partial terminalization leaves pending and terminal siblings in one current-turn bundle: - complete the pending siblings without rewriting terminal history or returning a permanent conflict. -- A malformed persisted pending row has no matching durable turn: drain any live in-memory waiter, but - keep the row inert. If such pre-turn history is encountered, repair it through explicit data cleanup - rather than treating it as current input authority. -- Session loading fails during task activation: retain existing navigation fallback instead of - stranding the user in the task drawer or on an unchanged URL. -- A newer task-summary revision arrives while pending-owner loading is in flight: discard the delayed - owner-session result, open the requested task through the task-only fallback, and let the current - projection render the authoritative pending owner. -- Backend stops after reserving a detached-answer successor but before the attempt marker: startup - restores only the clarification rows claimed for that dispatch and removes the empty reservation. -- Backend stops after the attempt marker but before dispatch acknowledgement: startup fails closed, - preserves the successor as authoritative, and keeps its exact clarification claim terminal. Output - referencing the reservation provides the same conservative authority even without the marker. -- A delayed predecessor ready event arrives between the attempt marker and agentctl acknowledgement: - wait for the live reservation outcome, then reject the event if its prompt generation was superseded; - never complete the reserved successor or evaluate predecessor workflow completion against it. If the - reservation rolls back and the predecessor generation still owns the session, process the ready event - normally instead of stranding the predecessor turn. Drop a generationless event after the wait because - it cannot be correlated safely with the predecessor. -- Unpublished-reservation reconciliation fails during startup: fail startup before event processing or - prompt admission begins so no new turn can supersede an unrecovered clarification claim. -- Response-delivery intent reconciliation fails during startup: fail startup before event processing or - prompt admission begins. Never leave an unhanded terminal claim permanently unanswerable. -- Unpublished-reservation recovery metadata contains a non-string or empty claimed-message ID: fail the - recovery transaction and startup, preserving the reservation and claimed rows for diagnosis and retry. - -## Persistence guarantees - -- Message history remains durable and is not destructively rewritten merely because a newer turn - exists. -- Active clarification state is reconstructable after restart from message status plus the newest - authoritative durable turn. -- Current-turn ownership is reconstructable from durable turn rows even when a turn has no remaining - messages. -- Unpublished detached-answer reservations carry enough recovery identity to restore only their own - claimed rows. A durable attempt marker separates safe rollback from ambiguous dispatch, and startup - reconciliation runs even when no executor record remains. -- Provisional terminal response claims carry a durable delivery intent. Startup restores that exact - current-turn claim when no live or detached handoff became authoritative, while prompt-reservation - recovery owns any detached dispatch that reached its durable reservation boundary. -- Task summaries are caches. Boot and task-list reads correct a stale persisted `pending_action` with - a monotonic revision while preserving all unrelated summary fields. -- No one-off mutation or backfill of an existing installation database is required. Deploying the - corrected derivation makes historical older-turn pending rows inert and repairs their summaries on - normal reads. - -## Scenarios - -- **GIVEN** a clarification wait disconnects and no newer turn exists, **WHEN** the task is reloaded, - **THEN** the detached question remains visible and answerable, its session and task advertise - `clarification`, and workflow completion stays blocked. -- **GIVEN** an older turn retains a detached pending row, **WHEN** the session accepts a newer ordinary - turn, **THEN** the old row remains in history but no overlay, pending projection, detach event, or - workflow barrier derives from it. -- **GIVEN** a newer turn superseded an older pending question, **WHEN** every message in the newer turn - is deleted, **THEN** the durable newer turn remains current and the older question stays inert. -- **GIVEN** an attempted successor reservation is authoritative but intentionally hidden from client - turn history, **WHEN** the session projection explicitly reports no clarification action, **THEN** a - pending question on the visible predecessor stays inert in chat and pending-input indicators. -- **GIVEN** a browser cached an explicit clean session projection, **WHEN** a new current-turn - clarification message arrives, **THEN** the same event advances the session projection to - `clarification` before pending discovery so the question remains visible and answerable. -- **GIVEN** a task-session list request is in flight, **WHEN** a newer semantic message event updates - the same session before the HTTP response resolves, **THEN** the response's older projection - revision cannot replace the WebSocket projection (and the reverse delivery order is equally safe). -- **GIVEN** an old detached question and a newer clarification bundle, **WHEN** the user skips the - newer bundle and reloads, **THEN** neither bundle reappears, the task question icon is absent, and - later turn completion cannot re-arm the old bundle. -- **GIVEN** a persisted summary says `pending_action=clarification` while current-turn repository state - has no pending action, **WHEN** a boot or task-list payload is built, **THEN** the summary revision - advances with no pending action and all unrelated fields remain unchanged. -- **GIVEN** a task's secondary session owns a current clarification while the primary session is - clean, **WHEN** the user activates the task from the desktop sidebar or phone task drawer, **THEN** - Kandev bypasses any cached session list, selects the secondary session, closes the drawer when - applicable, and shows the question. -- **GIVEN** clarification pause observes no active turn, **WHEN** detachment creates the session's first - turn before cancellation registration completes, **THEN** the pause rejects its stale cancellation - and does not cancel or complete that new turn. -- **GIVEN** pending-owner loading began for one task-summary revision, **WHEN** a newer summary changes - the pending action before the session response is applied, **THEN** desktop and phone activation do - not navigate to the obsolete owner, open the selected task through the task-only fallback, and close - the phone drawer. -- **GIVEN** two forced session loads overlap for one task, **WHEN** the older response finishes last, - **THEN** it cannot overwrite the newer session snapshot or revive an obsolete pending owner. -- **GIVEN** a forced session load is pending in the phone task sheet, **WHEN** a responsive-layout - change unmounts it and mounts the tablet sheet before that load finishes, **THEN** the old - continuation cannot navigate, change the active selection, or close the replacement sheet. -- **GIVEN** a pending task has no loaded owner, **WHEN** activation falls back to its task route, - **THEN** the outgoing session layout is released before the active session is cleared. -- **GIVEN** a stale browser still displays a superseded question, **WHEN** it submits an answer, - **THEN** the server returns conflict and does not resume or otherwise prompt the agent. -- **GIVEN** a detached current-turn answer is not accepted by the orchestrator, **WHEN** the response - endpoint fails, **THEN** it returns a server error, keeps the question answerable, and a later retry - delivers exactly one accepted resume request without a failed-dispatch turn superseding the bundle. -- **GIVEN** agentctl accepts a detached answer but successor publication fails, **WHEN** the response - endpoint completes, **THEN** it returns an error, keeps the answer terminal, and does not make that - accepted successor eligible for rollback or in-process redispatch. -- **GIVEN** agentctl accepts a detached answer and later transport handling also fails, **WHEN** prompt - handling returns, **THEN** normal failure cleanup runs and the accepted-answer marker still prevents - the terminal clarification from being reopened. -- **GIVEN** the backend stops after terminalizing a detached answer and reserving its successor but - before marking dispatch attempted, **WHEN** it starts again, **THEN** it deletes the empty - reservation, restores the exact claimed rows to pending, and leaves pre-existing terminal siblings - unchanged. -- **GIVEN** the backend stops after marking a detached-answer dispatch attempted but before observing - acknowledgement, **WHEN** it starts again, **THEN** it preserves the successor as current and keeps - the exact claimed rows terminal rather than risking duplicate answer dispatch. -- **GIVEN** the backend stops after claiming a current-turn response but before either a live waiter or - detached resumer receives it, **WHEN** it starts again, **THEN** startup restores the exact claimed - rows to pending and the same answer can be submitted again. -- **GIVEN** a response is enqueued to a live waiter, **WHEN** durable confirmation has not completed, - **THEN** the waiter does not return the response and restart recovery may safely restore the claim. -- **GIVEN** durable confirmation starts but outlives the responder's bounded wait, **WHEN** the responder - attempts recovery, **THEN** the immutable claim is race-free and repository serialization lets either - finalization or restore win without reopening a successfully finalized response. -- **GIVEN** cancellation removes a live clarification after a responder loads it but before response - delivery is claimed, **WHEN** that responder continues, **THEN** it returns not-found immediately so - the caller can use detached recovery instead of waiting for an orphaned delivery confirmation. If - response delivery wins first, later cancellation cannot preempt its durable confirmation. -- **GIVEN** a primary-answer watchdog survives until another turn supersedes its clarification turn, - **WHEN** its fallback timer expires, **THEN** both the preflight and serialized prompt-admission checks - reject the stale answer without prompting or cancelling the successor. -- **GIVEN** a primary-answer watchdog has entered fallback authority or prompt work, **WHEN** session - activity or service shutdown cancels the watchdog, **THEN** that cancellation reaches the in-flight - repository and prompt calls. -- **GIVEN** a reserved successor is marked dispatch-attempted but remains unpublished, **WHEN** a client - loads turn history before agentctl accepts or rejects it, **THEN** the successor is omitted until - publication or durable message evidence prevents rollback from leaving stale client-only history. -- **GIVEN** a predecessor ready event arrives while a detached-answer successor is reserved, **WHEN** - the successor dispatch resolves, **THEN** the handler revalidates prompt generation and the stale - predecessor cannot complete the successor or run `on_turn_complete` against it; if reservation - rollback leaves that predecessor generation authoritative, its ready event completes normally. -- **GIVEN** reserved-successor deletion returns an error, **WHEN** rollback handling completes, **THEN** - the reservation waiter remains unresolved and another prompt cannot enter that session before - restart recovery. -- **GIVEN** an unpublished reservation stores an attempted marker as boolean `true`, string `"true"` or - `"1"`, or numeric `1`, **WHEN** startup recovery runs, **THEN** it preserves the ambiguous successor - and clears recovery metadata instead of restoring the answer and deleting the turn. -- **GIVEN** a generationless ready event waits on a live reservation, **WHEN** the reservation rolls - back, **THEN** Kandev drops the uncorrelatable event without changing session or workflow state. -- **GIVEN** a cancelled request triggers clarification detach or expiry, **WHEN** repository work - begins, **THEN** it uses a non-cancelled context with a finite deadline. -- **GIVEN** a session transition to completed, failed, or cancelled is durably accepted, **WHEN** the - transition publishes, **THEN** current-turn pending clarification bundles expire before a terminal - session can leave an interactive overlay behind. -- **GIVEN** a detached answer reaches synchronous orchestrator resume, **WHEN** the orchestrator does - not acknowledge it before the bounded deadline, **THEN** Kandev treats acceptance as failed and - restores the still-current bundle. -- **GIVEN** startup cannot reconcile an unpublished prompt reservation, **WHEN** the orchestrator starts, - **THEN** startup returns an error before watcher, scheduler, or prompt admission begins. -- **GIVEN** restart recovery preserves an accepted or ambiguous prompt reservation, **WHEN** its public - start event may have been missed, **THEN** startup durably retains and retries an ordered event replay - until `turn.started` and any already-required `turn.completed` event are accepted before clearing the - recovery marker. -- **GIVEN** an unpublished prompt reservation contains a malformed claimed-message ID list, **WHEN** - startup recovery decodes it, **THEN** recovery fails closed without deleting the reservation or - reopening only a subset of its claimed rows. -- **GIVEN** pending ownership changes while desktop or mobile task selection loads sessions, **WHEN** the - delayed load settles, **THEN** Kandev ignores its stale owner choice but still opens the selected task - through the task-only fallback, and the mobile sheet closes. -- **GIVEN** a task-list refresh observes a detached answer's temporary terminal claim, **WHEN** resume - rejection restores that claim, **THEN** Kandev acknowledges durable summary convergence before - reporting retryability and publishes the restored pending rows so other clients converge. -- **GIVEN** live publication or synchronous summary acknowledgement fails after restored clarification - rows commit, **WHEN** the detached resume returns, **THEN** Kandev reports the convergence error while - still identifying the durably restored answer as safe to retry. -- **GIVEN** the selected task projection disappears while desktop or mobile session loading is in - flight, **WHEN** the delayed load settles, **THEN** Kandev leaves task and session selection unchanged. -- **GIVEN** a newer desktop or mobile forced session load aborts an older load for the same task, - **WHEN** the older continuation handles `AbortError`, **THEN** it leaves the winning task and session - selection unchanged. -- **GIVEN** two phone task sheets are mounted, **WHEN** both have an in-flight task selection, **THEN** - closing or selecting within one sheet does not invalidate the other sheet's selection generation. -- **GIVEN** authoritative pending-action projection fails while listing a task's sessions, **WHEN** the - HTTP or WebSocket list request completes, **THEN** it returns an internal error rather than a false - clean-session response. -- **GIVEN** two request identities produce terminal and pending events close together, **WHEN** the - projector refreshes, **THEN** its result matches current repository state rather than event order. - -## Out of scope - -- Rewriting or deleting historical pending message rows solely to clean old data. -- A dedicated clarification table or schema migration. -- Redesigning the desktop sidebar, phone task drawer, chat cards, or clarification carousel. -- Changing permission-request lifecycle semantics; pending-session navigation reuses the shared - `pending_action` type for permission parity. -- Changing notification history for a clarification that was valid when its notification fired. -- Automatically choosing a non-primary session when the task has no current pending action. diff --git a/docs/specs/external-question-answering/spec.md b/docs/specs/external-question-answering/spec.md index e15380bf8d..b5bf73f6f4 100644 --- a/docs/specs/external-question-answering/spec.md +++ b/docs/specs/external-question-answering/spec.md @@ -7,10 +7,10 @@ owner: nova28 # External Question Answering — authorized, discoverable, idempotent clarification resolution -> **Revision note (2026-08-20, spec round 6).** This spec was first written against a +> **Revision note (2026-08-20, spec round 6).** This spec was first written against > `main` on which nothing made clarification resolution atomic. While it was in Build, > upstream commit `7e2c4ae84` ("fix: enforce active clarification lifecycle", #2669) merged -> its own approved spec, `docs/specs/clarification-active-lifecycle/spec.md`, which landed an +> its spec, `docs/specs/tasks/requirements/clarification-active-lifecycle.md`, which landed an > atomic current-turn claim and a turn-supersession lifecycle. That work **supersedes this > spec's entire claim mechanism** — the `clarification_resolutions` table, `ResolveBundle`'s > own claim SQL, and the workflow-guard changes are all retired here rather than merged diff --git a/docs/specs/gitlab-mr-task-list-badges/spec.md b/docs/specs/gitlab-mr-task-list-badges/spec.md index 9b46e69d8b..54a0688941 100644 --- a/docs/specs/gitlab-mr-task-list-badges/spec.md +++ b/docs/specs/gitlab-mr-task-list-badges/spec.md @@ -6,7 +6,7 @@ owner: tbd # GitLab MR Badge on the Sidebar and Tasks-List Rows -> **Amendment:** [Rich task title previews](../tasks/rich-task-title-previews.md) +> **Amendment:** [Rich titles](../tasks/requirements/rich-task-title-previews.md) > extends this draft. It replaces the requirements that `MRTaskIcon` stay > unmodified, add no copy, and have only one hydration owner. The badge > placement and provider data-model requirements in this document still apply. diff --git a/docs/specs/office/scheduler.md b/docs/specs/office/scheduler.md index cf3ada306c..8b10ea5b6a 100644 --- a/docs/specs/office/scheduler.md +++ b/docs/specs/office/scheduler.md @@ -10,7 +10,7 @@ owner: cfl 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. The shared ownership, scoping, and shutdown contract is defined by [queued run scheduling](../tasks/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). ## What diff --git a/docs/specs/session-delete-resource-cleanup/spec.md b/docs/specs/session-delete-resource-cleanup/spec.md index e7f2faa850..bdf62e6f4d 100644 --- a/docs/specs/session-delete-resource-cleanup/spec.md +++ b/docs/specs/session-delete-resource-cleanup/spec.md @@ -8,7 +8,7 @@ owner: cfl Decision: [Task-owned worktree lifetime](../../decisions/2026-08-08-task-owned-worktree-lifetime.md) -Runtime contract: [Task runtime cleanup](../tasks/runtime-cleanup.md) +Runtime contract: [Task runtime cleanup](../tasks/system-design/runtime-cleanup.md) ## Why diff --git a/docs/specs/spec-lint.json b/docs/specs/spec-lint.json index 195379860b..4d573e3204 100644 --- a/docs/specs/spec-lint.json +++ b/docs/specs/spec-lint.json @@ -15,9 +15,8 @@ "docs/specs/automations-yaml-export/spec.md": 111884, "docs/specs/azure-devops-integration/spec.md": 43510, "docs/specs/board-step-visibility-filter/spec.md": 39946, - "docs/specs/clarification-active-lifecycle/spec.md": 37498, "docs/specs/executor-profile-env-precedence/spec.md": 88243, - "docs/specs/external-question-answering/spec.md": 80622, + "docs/specs/external-question-answering/spec.md": 80621, "docs/specs/gitlab-integration/spec.md": 34079, "docs/specs/gitlab-mr-status-chip/spec.md": 77760, "docs/specs/gitlab-mr-task-list-badges/spec.md": 51962, @@ -29,7 +28,7 @@ "docs/specs/office/live-updates.md": 38602, "docs/specs/office/overview.md": 34375, "docs/specs/office/routing.md": 40782, - "docs/specs/office/scheduler.md": 50970, + "docs/specs/office/scheduler.md": 50967, "docs/specs/office/tasks.md": 42862, "docs/specs/office-agent-tier-routing/spec.md": 58448, "docs/specs/platform/diagnostic-logging.md": 40700, @@ -37,9 +36,6 @@ "docs/specs/plugins/spec.md": 77820, "docs/specs/subagent-context-persistence/spec.md": 160552, "docs/specs/system-page/storage-maintenance.md": 54320, - "docs/specs/tasks/external-id-idempotency/spec.md": 61162, - "docs/specs/ui/ci-pr-automation.md": 57643, - "docs/specs/workflow/task-step-transition-ledger/spec.md": 36373, - "docs/specs/workflow-quorum-decision-recording/spec.md": 76297 + "docs/specs/ui/ci-pr-automation.md": 57643 } } diff --git a/docs/specs/tasks/README.md b/docs/specs/tasks/README.md new file mode 100644 index 0000000000..15c11ffc45 --- /dev/null +++ b/docs/specs/tasks/README.md @@ -0,0 +1,131 @@ +--- +status: active +system: tasks +specification_version: 1 +migration: complete +owners: + - kandev +--- + +# Task and workflow system + +## Purpose + +The task and workflow system owns durable work items, task relationships, +execution lifecycle, workflow steps, queued launches, and the contracts that +move a task through its configured process. + +## Ownership + +This system owns task identity and metadata, task documents and attachments, +parent and dependency relationships, task creation and launch behavior, task +runtime state publication, workflow definitions and transitions, completion +signals, and task-scoped scheduling contracts. + +## Exclusions + +- Agent identity, permissions, and runtime profiles belong to the + [agent system](../agents/). +- Repository and worktree ownership belongs to the + [workspace system](../workspaces/). +- Office-specific autonomous agent identities and dashboards belong to the + [Office system](../office/). +- Presentation-only behavior belongs to the [UI specifications](../ui/). + +## Specification map + +### Requirements + +- [Active clarification lifecycle](requirements/clarification-active-lifecycle.md) +- [Active clarification lifecycle scenarios](requirements/clarification-active-lifecycle-scenarios.md) +- [Agent-generated titles](requirements/agent-generated-titles.md) +- [Attach workspace sources](requirements/attach-workspace-sources.md) +- [Archive confirmation](requirements/archive-confirmation.md) +- [Autopilot mode](requirements/autopilot-mode.md) +- [Blocked task escalation](requirements/blocked-task-escalation.md) +- [Cancelled turn completion](requirements/workflow-cancelled-turn-completion.md) +- [Create dialog advanced dependency settings](requirements/task-dependencies-create-dialog-advanced-settings.md) +- [Create dialog dependency selector](requirements/task-dependencies-create-dialog-dependency-selector.md) +- [Documents](requirements/documents.md) +- [Execution stages](requirements/execution-stages.md) +- [External task ID idempotency](requirements/external-id-idempotency.md) +- [External task ID idempotency boundaries](requirements/external-id-idempotency-boundaries.md) +- [External task ID idempotency scenarios](requirements/external-id-idempotency-scenarios.md) +- [Explicit workflow-step completion signal](requirements/workflow-explicit-completion-signal.md) +- [Interrupted task indicator](requirements/interrupted-task-indicator.md) +- [Labels](requirements/labels.md) +- [Link an existing task to a GitHub issue](requirements/link-existing-task-github-issue.md) +- [MCP task agent-profile default](requirements/mcp-task-agent-profile-default.md) +- [Model unification](requirements/model-unification.md) +- [Multi-branch tasks](requirements/multi-branch.md) +- [Parent-child message interrupt](requirements/parent-child-message-interrupt.md) +- [Parent-child task stop](requirements/parent-child-task-stop.md) +- [Prevent agent autostart on open](requirements/prevent-agent-autostart-on-open.md) +- [Prompt attachments](requirements/prompt-attachments.md) +- [Quick chat expiration](requirements/quick-chat-expiration.md) +- [Quick chat repository context](requirements/quick-chat-repository-context.md) +- [Remote contribution tasks](requirements/remote-contribution-tasks.md) +- [Rich task title previews](requirements/rich-task-title-previews.md) +- [Run scheduling](requirements/run-scheduling.md) +- [Runtime state publication order](requirements/runtime-state-publication-order.md) +- [Sidebar task edit](requirements/sidebar-task-edit.md) +- [Subtask checklist](requirements/subtask-checklist.md) +- [Subtask completion trigger](requirements/subtask-completion-trigger.md) +- [Subtask detachment](requirements/subtask-detachment.md) +- [Subtask reparenting](requirements/subtask-reparenting-drag-drop.md) +- [Subtree controls](requirements/subtree-controls.md) +- [Task create escape dismissal](requirements/task-create-escape-dismissal.md) +- [Task create executor default](requirements/task-create-executor-default.md) +- [Task create workflow memory](requirements/task-create-workflow-memory.md) +- [Task dependencies](requirements/task-dependencies.md) +- [Task launch failure recovery](requirements/task-launch-failure-recovery.md) +- [Task title length limit](requirements/title-length-limit.md) +- [User question turn boundary](requirements/user-question-turn-boundary.md) +- [Without repositories](requirements/without-repositories.md) +- [Runtime cleanup](requirements/runtime-cleanup.md) +- [Workflow cycle guardrails](requirements/workflow-cycle-guardrails.md) +- [Workflow duplication](requirements/workflow-duplication.md) +- [Workflow quorum decision recording](requirements/workflow-quorum-decision-recording.md) +- [Workflow quorum decision recording: agent surface](requirements/workflow-quorum-decision-recording-agent-surface.md) +- [Workflow quorum decision recording: concurrency](requirements/workflow-quorum-decision-recording-concurrency.md) +- [Workflow quorum decision recording: diagnostics](requirements/workflow-quorum-decision-recording-diagnostics.md) +- [Workflow quorum decision recording: participant slate](requirements/workflow-quorum-decision-recording-participant-slate.md) +- [Workflow quorum decision recording: re-evaluation](requirements/workflow-quorum-decision-recording-reevaluation.md) +- [Workflow quorum decision recording: regression](requirements/workflow-quorum-decision-recording-regression.md) +- [Workflow quorum decision recording: step binding](requirements/workflow-quorum-decision-recording-step-binding.md) +- [Workflow quorum decision recording: verdict vocabulary](requirements/workflow-quorum-decision-recording-verdict-vocabulary.md) +- [Workflow session settings](requirements/workflow-session-settings.md) +- [Workflow settings autosave](requirements/workflow-settings-autosave.md) +- [Workflow step agent-start ownership](requirements/workflow-step-agent-start-ownership.md) +- [Workflow sync workspace authorization](requirements/workflow-sync-workspace-authz.md) +- [Workflow task-step transition ledger](requirements/workflow-task-step-transition-ledger.md) +- [Workflow task-step transition ledger scenarios](requirements/workflow-task-step-transition-ledger-scenarios.md) +- [WIP limits and visible overflow queues](requirements/wip-limit-pull-system.md) + +### System design + +- [Active clarification lifecycle](system-design/clarification-active-lifecycle.md) +- [Attach workspace sources](system-design/attach-workspace-sources.md) +- [External task ID idempotency](system-design/external-id-idempotency.md) +- [External task ID idempotency operations](system-design/external-id-idempotency-operations.md) +- [Model unification](system-design/model-unification.md) +- [Remote contribution tasks](system-design/remote-contribution-tasks.md) +- [Runtime cleanup](system-design/runtime-cleanup.md) +- [Task dependencies](system-design/task-dependencies.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) +- [WIP limit and pull system](system-design/wip-limit-pull-system.md) + +## Migration status + +All task and workflow sources in this migration are now represented by +authoritative requirement and system-design documents under this directory. +The legacy editable sources were split by capability, lifecycle, and contract +boundary, then removed from the legacy tree. + +## Related systems + +- [Agents](../agents/): supplies agent identity and execution profiles. +- [Office](../office/): builds autonomous workflows on task primitives. +- [UI](../ui/): owns presentation-specific task surfaces. +- [Workspaces](../workspaces/): owns repositories and task worktrees. diff --git a/docs/specs/tasks/external-id-idempotency/spec.md b/docs/specs/tasks/external-id-idempotency/spec.md deleted file mode 100644 index 556c44b1d9..0000000000 --- a/docs/specs/tasks/external-id-idempotency/spec.md +++ /dev/null @@ -1,1103 +0,0 @@ ---- -status: draft -created: 2026-08-07 -updated: 2026-08-08 -owner: nova28 ---- - -# Task external ID (create idempotency) - -## Why - -An external system that creates Kandev tasks over the API has no way to ask -"did I already create this one?". Every create is unconditionally a new task, so -a webhook redelivery, a network timeout the caller retried, or a crash between -"task created" and "I recorded the task ID" produces a duplicate task — with a -duplicate worktree and, when `start_agent` is true, a duplicate agent burning -tokens on work already in flight. Callers today compensate with fragile -heuristics (scanning task titles for a prefix, treating an existing branch name -as a witness that the task exists), which break the moment a title is edited or -a branch is renamed. - -## What this feature is, and is not - -**It is durable duplicate suppression.** A create carrying an external ID never -produces a second task for that identity, and always returns the task that holds -it — including after the caller crashed before recording the task ID. That is -the unblock: the caller gets its task ID back and stops guessing. - -**It is not automated crash repair.** Kandev does not detect, adopt, resume, or -clean up a partially-created task, and callers MUST NOT automate doing so -either. The reason is not implementation cost — it is that the information does -not exist. A task whose creation has not finished is indistinguishable from one -whose creation is *still running*: both are simply "not settled yet". Without a -liveness signal — a lease, a heartbeat, an owner token — no observer can tell a -crashed create from a slow one. - -An earlier draft claimed a retry would never observe a half-created task, which -required exactly that machinery: fencing tokens, compare-and-swap on every -transition, a two-phase coordinator, and isolation of unsettled rows. It was -reviewed and rejected as unbuildable. This spec does not reintroduce it, and -therefore does not make the guarantee it would have bought. - -What callers get instead is an honest flag. - -**`creation_complete` has exactly one meaning, in every outcome and on every -surface: the creation of the returned task completed its required synchronous -work.** Nothing more. It says nothing about whether an agent is running, and -nothing about whether any process is currently alive. - -The diagnostic case is the specific tuple **`deduplicated: true` + -`creation_complete: false`** — another create claimed this identity and had not -finished when we looked; it may still be in progress. That combination is -diagnostic, not actionable. The safe response is to proceed with the returned -task ID, or to escalate to a human — never to release the identity and create -again. See *The one unsafe thing a caller can do*. - -No other tuple carries the "may still be running" caveat. - -## What - -- A task MAY carry an **external ID**: a caller-supplied string that identifies - the entity in the caller's own system (a Jira key, a webhook delivery ID, a - UUID the caller minted before its first attempt). -- An external ID SHALL be held by at most one task per workspace. -- A create carrying an external ID SHALL have exactly one of four outcomes, and - the response SHALL make which one unambiguous: - 1. **Created** — no task held that identity; a new task was created and holds - it. - 2. **Found, settled** — a task already holds it and its creation finished. - 3. **Found, unsettled** — a task already holds it and its creation had not - finished at observation time. It may still be running. - 4. **Created, identity lost** — this request created the task and finished its - work, but another actor released the identity in the interim, so the task - survives holding no external ID. Rare; see *Settlement*. -- Both **Found** outcomes SHALL have **no side effects**: no new task row, no - new agent session, no agent launch, no repository attachment, no attachment - claim, no workspace-policy write, no branch creation, and no `task.created` - event. - - **One bounded exception, on any concurrent-loser path.** When a Found - outcome is resolved by the step-3 lookup — which is every ordinary retry — - nothing whatsoever is consumed. When it is instead resolved *after* a - step-3 miss, the loser may already have allocated an office task-identifier - sequence number, and that number is not returned to the pool. This applies - to **both** late-resolution paths, because identifier allocation precedes - both of them: the unique-index backstop, and the pre-insert re-read that - catches an admission or capacity failure. This is the single permitted side - effect; it leaves a gap in the office identifier sequence, which is not - required to be contiguous, and no other durable change. -- The system SHALL NOT delete, repair, resume, or reclaim an unsettled task, and - SHALL NOT expire one. There is no duration after which behavior changes. -- **REST** callers SHALL have a side-effect-free way to ask what holds an - identity without risking a create: the lookup route. **MCP callers do not get - one in this iteration.** What MCP gets is an idempotent *create-if-absent* - operation — safe to repeat, but it creates a task when no holder exists, so it - is not a probe. This asymmetry is deliberate and is stated rather than papered - over; see *The probe, and what MCP has instead*. -- Callers SHALL be able to release an external ID from a task, freeing it for - reuse without deleting the task. This is an operator action, not a recovery - step — see *The one unsafe thing a caller can do*. -- Omitting the external ID SHALL leave task creation behaving exactly as it does - today. -- The external ID SHALL be accepted on the **REST create endpoint** and the - **`create_task_kandev` MCP tool**. The WebSocket `task.create` action and the - plugin host task-create API do not accept it in this iteration. -- The external ID SHALL appear on exactly the task representations listed in - *Task representations*. That table is the complete requirement; there is no - broader "everywhere tasks are read" obligation. -- The external ID identifies the **external entity**, not the request body. A - second create for a held external ID SHALL return the existing task unchanged - even when the rest of the payload differs; it SHALL NOT patch the existing - task. -- An external ID SHALL NOT be inherited by subtasks, copied on any task-cloning - path, or auto-generated by the system. - -## The one unsafe thing a caller can do - -**Do not automatically release an identity because a create reported -`creation_complete: false`, and then create again.** This is called out -explicitly because it is the intuitive "recovery" move and it is unsafe. - -The failure: - -1. Create A commits its task row for identity `E` and continues its remaining - work normally. -2. Retry B observes `E` held by an unsettled task and returns - `creation_complete: false`. -3. B releases `E` and creates again, producing task T2. -4. A finishes. Two tasks now exist for one external entity, and if both - requested an agent, two agents are running. - -The unique index prevents two *simultaneous holders*; it cannot prevent -duplicates across a release. Releasing an identity that another create is -actively using re-opens exactly the duplicate this feature exists to prevent. - -**Safe responses to `creation_complete: false`:** - -- Proceed with the returned task ID. It is a real task. -- Poll the lookup; if it settles, the original create finished on its own. -- Escalate to a human, who can inspect the task and decide. - -**Release is for a human or operator who has determined the task is abandoned** -— not for an automated retry loop. Client libraries and agent tooling SHOULD NOT -expose release as an automatic recovery action. - -## Data model - -Two columns on the existing `tasks` table. **There is no separate claim table.** - -``` -tasks - ...existing columns... - external_id TEXT nullable caller-supplied identity; - NULL when the task has none - external_id_settled_at TIMESTAMP nullable when creation finished; - NULL means not settled -``` - -- **Uniqueness:** partial unique index `uniq_tasks_external_id` on - `(workspace_id, external_id)` `WHERE external_id IS NOT NULL`. Supported by - both SQLite and PostgreSQL. Naming it matters — violation handling must be - attributable to this constraint specifically, never to "some unique index". -- **Settledness:** `external_id_settled_at IS NULL` means the create that - claimed this identity had not finished its required synchronous work. It does - **not** mean that create is dead. -- Empty or whitespace-only input normalizes to `NULL` (see *Validation*), so - tasks without an external ID never collide with each other. - -### Why the identity lives on the task row - -Putting the claim on the task itself, rather than in a side table, removes -whole classes of failure **by construction rather than by specification**: - -| Hazard | Why it cannot occur here | -|---|---| -| Claim and task disagreeing | They are the same row. Nothing to keep in sync. | -| Orphan claim after a task is deleted | The columns are deleted with the row, through *any* deletion path — including office handoff cascades that call the repository directly and bypass the service. | -| Non-atomic "mark deleted" transition | There is no transition. Deleting the row releases the identity. | -| E2E reset leaving stale claims | Reset already deletes tasks; the columns go with them. No reset change is needed. | -| A task with no claim, or a claim with no task | One row, one insert. Impossible. | - -### Validation and normalization - -Applied to the **raw** caller-supplied value, in this exact order. The order is -normative because it decides which error a malformed value produces. - -1. **Reject ASCII control characters** (U+0000–U+001F, U+007F) anywhere in the - raw value — before any trimming. Newline and tab are control characters and - are rejected here. Doing this before the trim is what makes a - trailing-newline value an error rather than something the trim silently - erases. -2. **Trim** leading and trailing Unicode whitespace (any code point with the - Unicode `White_Space` property). -3. **Empty after trim** → treat as absent (`NULL`). Not an error. -4. **Length** of the trimmed value in **UTF-8 bytes** MUST be ≤ 255. - -Everything surviving those rules is accepted verbatim: `jira:PROJ-1234`, -`gh-issue/kdlbs/kandev#2325`, a bare UUID, and non-ASCII letters are all valid. - -### Comparison semantics - -Matching is **byte-exact and case-sensitive** after normalization. `ext-1` and -`EXT-1` are different identities. - -To make that true on both dialects, `tasks.external_id` and its index MUST use a -binary/deterministic collation (SQLite `COLLATE BINARY`, the default; -PostgreSQL `COLLATE "C"` or another explicitly deterministic collation). An -unqualified `TEXT` column on PostgreSQL follows the database collation, which -may be case-insensitive or nondeterministic and would silently violate this -contract. No Unicode normalization is performed. - -### Lifecycle - -- **Set once at create.** No API changes an external ID on an existing task; - `PATCH /tasks/:id` and the update MCP/WS surfaces do not accept the field. -- **Settled once at create**, then never cleared or re-stamped except by release. -- **Archiving changes nothing.** An archived task still holds its identity. -- **Deleting frees the identity.** The row is gone. -- **Releasing frees the identity without deleting the task**, setting both - columns to `NULL`. - -### Idempotency is scoped to the task's lifetime - -Once the task holding an identity is deleted or released, that identity is free, -and a later create makes a new task. This is a real limitation and it is stated -without excuse: - -**A consumer that redelivers indefinitely must suppress redelivery for entities -it has itself deleted.** Kandev retains no record of a consumed identity, so it -cannot distinguish "never seen" from "seen and since deleted". - -An earlier draft argued that a caller could detect this because the returned -task ID differs from the one it recorded. That rationale is withdrawn: the -motivating scenario is a caller that crashed *before* recording any task ID, and -such a caller has nothing to compare against. - -The alternative — a durable tombstone of consumed identities — was rejected -because it requires the identity to outlive the task row, which reintroduces the -side table and every synchronization hazard the table above removes. If a real -consumer cannot suppress its own post-deletion redeliveries, that is the trigger -to revisit this, and it should be treated as a design change rather than a -tweak. - -**This limitation does not affect the motivating crash-recovery case**, where no -deletion occurs. - -## State machine - -Two observable states for a task holding an external ID: - -| State | Representation | Meaning | -|---|---|---| -| **unsettled** | `external_id` set, `external_id_settled_at IS NULL` | The create that claimed this identity had not finished its required synchronous work at observation time. It may still be running. | -| **settled** | both set | That work finished. | - -Transitions: - -| From | To | Trigger | -|---|---|---| -| (none) | unsettled | A create wins the unique index and commits the task row | -| unsettled | settled | That same create completes its required synchronous work | -| any | (none) | The task is deleted, or the identity is released | - -There is no reclaim, no lease, no timeout, and no ownership token, because -nothing in this design ever mutates another caller's task. - -### Create sequence (normative) - -``` -1. authorize workspace ← already first in CreateTask -2. validate + normalize external_id -3. LOOKUP by (workspace_id, external_id) ← before ANY write or allocation - └─ found → return Found outcome, stop. Nothing else runs. -4. required create-time validation -5. identifier allocation, WIP admission, task-row insert (incl. external_id) - └─ unique violation on uniq_tasks_external_id → roll back, re-read, Found -6. required synchronous post-create work -7. SETTLE (conditional UPDATE, see below) -8. asynchronous dispatch (agent launch, PR association) -``` - -**Step 3 is load-bearing and its position is normative.** It must precede -identifier allocation, WIP admission, and every other write. Two concrete -consequences of getting this wrong, both of which violate the no-side-effects -requirement: - -- `assignIdentifier` runs before persistence and calls `IncrementTaskSequence`, - an unconditional `UPDATE workspaces SET task_sequence = task_sequence + 1`. - A found outcome resolved after that point permanently burns a sequence number. -- WIP admission can reject a create before the insert is ever attempted, so a - retry for an already-held identity would fail with a capacity error instead of - returning the existing task. - -Step 5's unique-violation path remains as the **TOCTOU backstop** for the narrow -race between the step-3 lookup and the insert. It is not the primary mechanism. - -**The backstop is not reachable from every failure, and that gap must be closed -explicitly.** WIP admission runs *before* the insert and can return a capacity -error without ever attempting it. So a request that missed at step 3, then lost -the race, can surface a capacity failure instead of the Found outcome the -contract promises. - -Therefore: **after a step-3 miss, any pre-insert failure — capacity, admission, -or otherwise — MUST trigger a re-read by `(workspace_id, external_id)` before -that failure is returned.** If a task now holds the identity, the request -returns the Found outcome instead of the failure. Only if the re-read still -finds nothing does the original error surface. - -This is narrow but load-bearing: without it, "a retry always returns the -existing task" is false precisely when the destination step is at its WIP limit, -which is exactly when a task is most likely to already exist there. - -### Settlement (normative) - -Settlement is a single conditional UPDATE. The predicate MUST include -`external_id`: - -```sql -UPDATE tasks - SET external_id_settled_at = ? - WHERE id = ? - AND external_id = ? - AND external_id_settled_at IS NULL -``` - -`external_id` in the predicate is **required, not defensive**. Release sets both -`external_id` and `external_id_settled_at` to `NULL`; a predicate guarding only -on `id` and `external_id_settled_at IS NULL` would still match a released row -and stamp a settlement onto a task that no longer holds any identity. - -**The affected-row count MUST be checked.** - -- **One row** — settlement succeeded. Proceed to asynchronous dispatch. -- **Zero rows** — the task was deleted or its identity released while this - create was running. The implementation MUST re-read current state and MUST NOT - dispatch asynchronous work. It then splits by what it finds: - - **The task exists but no longer holds the identity** (it was released) → - return outcome `CreatedIdentityLost`: `200`, `deduplicated: false`, - `creation_complete: true`, and `external_id` absent from the body. The work - finished; only the stamp had nowhere to land. - - **The task no longer exists** (it was deleted) → return the surface's - existing not-found error. There is no task to describe. - - It MUST NOT report outcome `Created`, and MUST NOT include `external_id` in - the body — either would claim an identity the task no longer holds. Note that - `creation_complete: true` **is** correct here and is not what distinguishes - this outcome: the create's synchronous work did finish. The absent - `external_id` is the distinguishing signal. - -Zero rows is a legitimate outcome of a benign race, not an error to log and -ignore. - -### Settlement call site (normative, per surface) - -The service cannot settle, because required synchronous work continues in the -handlers after `CreateTask` returns. Settlement is therefore the **handler's** -responsibility, and the covered steps differ per surface: - -| Surface | Required synchronous steps that must succeed before settling | Settle after | -|---|---|---| -| REST | attachment claim, workspace-policy attach, fresh-branch commit | fresh-branch commit, and **before** any session dispatch | -| MCP | remote-contribution association, workspace-policy attach | policy attach, and **before** auto-start dispatch | - -**REST's session helper must be split.** It currently performs synchronous -preparation and asynchronous dispatch in one call, swallowing preparation -errors before dispatching the start goroutine. Settling after that helper would -place settlement after async dispatch has already begun, violating step 7 → 8 -ordering. The helper must expose preparation and dispatch separately so -settlement can sit between them. - -**Best-effort steps do not block settlement.** Task-create last-used recording -and PR association are best-effort today; their failure MUST NOT prevent -settlement and MUST NOT trigger compensation. Only the steps listed in the table -above gate it. - -### What "settled" does and does not mean - -`external_id_settled_at` means **the required synchronous setup finished**. It -does *not* mean an agent is running, and it does not mean the create's process -is alive or dead. - -Callers needing to know whether an agent is running read session state, which -already exists and is already authoritative. - -## API surface - -### Service contract - -`Service.CreateTask` currently returns `(*models.Task, error)` and carries no -outcome signal. It SHALL return: - -``` -CreateTaskResult { - Task *models.Task - Outcome enum { Created, FoundSettled, FoundUnsettled, CreatedIdentityLost } -} -``` - -`CreatedIdentityLost` is the fourth outcome, and it exists solely to represent -the settlement zero-row race: this request created the task and finished its -required synchronous work, but by the time settlement ran the identity had been -released by another actor, so the task survives holding no external ID. Without -it the contract has a reachable state with no representation — the handler would -have to claim an identity the task no longer holds, or invent an undocumented -response. See *Settlement*. - -It is deliberately distinct from `FoundUnsettled`: that one means *someone -else's* create is unfinished, while this one means *this* create finished but -lost its identity. Callers act differently — the first says "wait or escalate", -the second says "your task exists, but the identity you asked for is now free -and someone may claim it". - -**It carries `creation_complete: true`.** The work finished; only the settlement -stamp had nowhere to land. Reporting `false` would break the field's single -definition and falsely suggest in-progress work. The absent `external_id` is -what identifies this outcome. - -If the task was **deleted** rather than released during that window, there is no -task to return and the request surfaces the appropriate not-found error; that is -an error path, not this outcome. - -Callers MUST skip their post-create work on both `Found*` outcomes: - -| Caller | In scope? | Post-create work to skip | -|---|---|---| -| REST `httpCreateTask` | **Yes** | attachment claim, workspace-policy attach, fresh-branch commit, session prepare/start, task-create last-used recording, PR association | -| MCP `handleCreateTask` | **Yes** | remote-contribution association, workspace-policy attach, auto-start launch | -| WS `wsCreateTask` | Deferred | agent launch, last-used recording | -| Plugin host `Tasks().Create` | Deferred | `StartAgent` best-effort start | - -The deferred rows cannot misbehave in this iteration because their surfaces do -not accept an external ID, so the outcome is always `Created` for them. They are -listed so enabling one later starts from a complete inventory. - -### ⚠️ The MCP skip is a data-loss guard - -Running MCP's post-create steps on a found outcome can **delete the existing -task**. The handler resolves remote contributions from the *request* -(`handlers.go:658`), then indexes them against the *returned task's* -repositories (`:715-719`). A found outcome returns a task whose repository list -need not match the retry payload, and the index guard at `:719` calls -`DeleteTask` at `:720`. Two further rollback paths do the same at `:729` and -`:741`. The sequence: - -> retry a create with an external ID → found outcome returns the existing task → -> post-create steps wrongly run → repository index mismatch → rollback -> **deletes the task the caller was trying to recover** - -That turns crash recovery into data loss. Skipping these steps on a found -outcome is a **correctness requirement**, and its scenario is required coverage. - -### REST — create - -`POST /api/v1/tasks` gains one optional request field, `external_id`. - -Response additions, both **always present** on every create response: - -```jsonc -{ - "id": "…", - "external_id": "jira:PROJ-1234", // omitted when the task holds none - "deduplicated": false, // true for both Found outcomes - "creation_complete": true, // false ONLY for Found, unsettled - // …all other existing task DTO fields… -} -``` - -`deduplicated` and `creation_complete` are required booleans, not presence-only -markers. A presence-only field makes a serialization bug indistinguishable from -a genuine fresh create. - -The field is named `creation_complete`, not `complete`, because it describes one -narrow thing — that the create's required synchronous setup finished — and a -broader name invites callers to read it as "the task is done" or "the agent is -running". Every schema and tool description MUST carry that narrow meaning. - -The four success outcomes map to exhaustive, mutually exclusive tuples: - -| Outcome | Status | `deduplicated` | `creation_complete` | `external_id` in body | -|---|---|---|---|---| -| Created | `200` | `false` | `true` | present | -| Found, settled | `200` | `true` | `true` | present | -| Found, unsettled | `200` | `true` | `false` | present | -| Created, identity lost | `200` | `false` | `true` | **absent** | -| `external_id` fails validation | `400` | — | — | — | -| Caller not authorized for `workspace_id` | `404` `{"error": "task not created"}` | — | — | — | - -Reading the tuples: - -- **`creation_complete: false` occurs in exactly one outcome**, `Found, - unsettled`, and only ever alongside `deduplicated: true`. That is the one - tuple meaning "another create may still be in progress." -- **`Created, identity lost` carries `creation_complete: true`**, because this - create's required synchronous work genuinely did finish — only the settlement - stamp had nowhere to land, since the identity was released. Reporting `false` - would contradict the single definition of the field and would falsely imply - in-progress work. -- It is distinguished from `Created` by the **absent `external_id`**, which is - the literal truth: the task no longer holds one. A caller seeing this tuple - has a valid task ID and should treat the external identity as unclaimed. - -All four success outcomes are `200` with the task body. There is no `409` and -no `410`: an unsettled task is a fact to report, not a conflict to raise, and a -freed identity is indistinguishable from one never used. Keeping fresh creates -at `200` also avoids a breaking change for existing clients. - -### The probe, and what MCP has instead - -**REST callers get a true probe:** the lookup route below. It reads and returns; -it never creates. - -**MCP callers do not.** A create carrying an external ID is *idempotent* — safe -to repeat, no duplicates, no side effects when a holder exists — but it is not -side-effect-free, because when no holder exists it creates a task. Calling that -a probe would be wrong, and an MCP agent told it was one could use it to "check" -an identity and be surprised to find it had made something. - -The MCP tool description MUST therefore say plainly: this creates the task if -nothing holds the identity yet. - -No MCP lookup tool is added in this iteration because no in-scope MCP flow needs -to ask without being willing to create — an agent reaching for an identity is -reaching for the task. If a flow appears that genuinely needs to ask first, add -a read-only tool then; it is a small, additive change. - -### REST — lookup - -``` -GET /api/v1/workspaces/:id/tasks/by-external-id?external_id= -``` - -| Situation | Status | Body | -|---|---|---| -| A task holds it (including archived, including unsettled) | `200` | task DTO with `creation_complete` | -| No task holds it | `404` | `{"error": "task not found"}` | -| `external_id` missing or fails validation | `400` | `{"error": ""}` | -| Caller not authorized for the workspace | `404` | `{"error": "task not found"}` | - -Read-only; no side effects. - -### REST — release - -``` -DELETE /api/v1/workspaces/:id/tasks/by-external-id?external_id= -``` - -Sets `external_id` and `external_id_settled_at` to `NULL` on the task that holds -it. Does **not** delete or otherwise modify the task. Returns `204` on success, -`404` when no task holds it, `400` on validation failure, `404` when -unauthorized. - -Release is an operator action for an identity a human has determined is -abandoned. See *The one unsafe thing a caller can do* for why it must not be -automated in response to `creation_complete: false`. - -### Request ordering and error precedence - -Two orderings are normative: - -1. **Authorization precedes any external-ID work.** An unauthorized caller - receives the existing unauthorized response and SHALL NOT learn whether an - external ID exists there, whether it is held, or whether its task is settled. - `Service.CreateTask` already authorizes as its first statement. -2. **Validation of the external ID precedes the lookup**, so a malformed value - is a `400` rather than an accidental lookup. - -The step-3 lookup then precedes all remaining create-time work, per -*Create sequence*. - -**Honest limitation — a retry can still fail before dedupe.** Both in-scope -handlers perform validation *before* reaching the service: REST validates -attachments, launch-profile requirements, and repositories; MCP resolves -repositories, workflow, workspace, contributions, and launch metadata. This spec -does not require restructuring them. Two distinct causes follow: - -- **Payload drift** — the retry sends something different from the original. - Mitigated by replaying the original payload. -- **Server-state drift** — the retry is byte-identical, but the world changed: - a repository was deleted, a workflow or step was removed, an agent profile was - disabled, a parent task was archived, a credential expired. **Replaying the - original payload does not help here.** - -Server-state drift means the "a caller that crashed and came back a week later -still finds its task" property holds for the **lookup route**, which touches -none of that validation, but **not necessarily for the create path**. A caller -that wants recovery robust against long absences should use the lookup route -first and fall back to create only when the lookup returns `404`. - -This is a real weakening relative to an idealized idempotent create, and it is -stated rather than implied. Engineering around it would require a create -coordinator that resolves the identity before any handler validation, which was -judged not worth its cost. - -### MCP - -`create_task_kandev` gains one optional string parameter: - -- `external_id` — "A stable identifier from your own system (issue key, webhook - delivery ID, a UUID you generated). Creating a task twice with the same - `external_id` in the same workspace returns the first task instead of making a - duplicate — use it when a retry or restart could re-run this call. Replay the - same arguments you sent the first time." - -The tool result is the task as JSON carrying `external_id`, `deduplicated`, and -`creation_complete`. The tool description MUST state all three of these: - -1. **This creates the task when nothing holds the identity yet.** It is - create-if-absent, not a lookup. An agent must not call it merely to check. -2. **`deduplicated: true`** means the task already existed and was not created, - so the agent must not report having created something new. -3. **`deduplicated: true` together with `creation_complete: false`** means - another create claimed this identity and had not finished when observed, and - **may still be in progress** — so the agent must not assume the task is - ready, and must not release the identity and create again. - -Point 3 is scoped to that tuple deliberately. `creation_complete` appears as -`false` in no other outcome, so an agent that keys off the pair cannot -misinterpret it. - -The workspace is resolved before the identity is used: explicit `workspace_id`, -else the parent's when `parent_id` is set, else auto-resolution when exactly one -workspace exists. The identity resolves against that **effective** workspace. - -The pre-existing MCP-layer `title` check runs before any backend call, so an MCP -replay carrying an external ID but no title gets the MCP validation error rather -than a found outcome. This is existing behavior and is consistent with the -replay guidance above. - -### Write surfaces NOT changed - -The WebSocket `task.create` action and the plugin host `Tasks().Create` do not -accept `external_id`. A caller supplying it there has the field ignored, exactly -as any other unknown field is today; no error, no claim. - -No dedupe event suppression is needed on the WebSocket: the found outcomes -return early inside the service, so WS subscribers never see an event for a -create that did not happen. - -### Task representations - -`external_id` is added to exactly these. This table **is** the requirement. - -| Representation | Location | Carries it | -|---|---|---| -| `dto.TaskDTO` | `internal/task/dto/dto.go` | Yes — `omitempty`, like the sibling `identifier` field. Covers REST reads/lists and the MCP task tools, which project this DTO. | -| WS task lifecycle events | `publishTaskEventNow`'s hand-built map, `service_events.go` | Yes — added explicitly. **Not** built from `TaskDTO`, so it needs its own change. | -| Plugin `Task` + SDK type + mapper | `plugin.proto`, `pkg/pluginsdk/data_types.go`, `internal/plugins/host_data.go` | **No.** Deferred with the plugin surface. Adding it later is additive on the proto (ADR 0043) and not breaking. | -| `v1.Task` | `pkg/api/v1/task.go` | **No.** Separate legacy projection. | -| Task-context references | `pkg/api/v1/task_context.go` | **No.** Lightweight ref. | - -`creation_complete` is a **create-response and lookup-response field only**. It -is not added to the shared task DTO, because it is meaningful only in the -context of an idempotent create; putting it on every task read would leak a -create-time condition into unrelated surfaces. - -## Permissions - -- The external ID is workspace-scoped data and inherits the workspace's existing - authorization rules with no additions. -- Authorization precedes all external-ID work. An unauthorized caller receives - `404 {"error": "task not created"}` for a create and - `404 {"error": "task not found"}` for a lookup or release, and cannot learn - whether an identity exists in that workspace or whether its task is settled. -- Because uniqueness is workspace-scoped and workspaces are per-owner when auth - is enabled, two users cannot collide on each other's external IDs or probe - each other's namespaces. -- An in-session agent calling `create_task_kandev` is already scoped to its - task's workspace owner by the MCP identity scoper. -- Release is a workspace-scoped mutation requiring the same authorization as any - other write to that workspace. - -## Failure modes - -| Condition | Behavior | -|---|---| -| Two concurrent creates race past the step-3 lookup | The partial unique index admits exactly one. The loser's insert transaction rolls back — including its task row and runner participant — then it re-reads by `(workspace_id, external_id)` and returns that task as a Found outcome. Neither sees a `5xx`, and no orphan row survives. A loser that already allocated an office identifier has burned a sequence number; see the row below. | -| A concurrent loser burned a task-identifier sequence number | Accepted. The step-3 lookup makes this rare (it requires two creates to interleave within the window between lookup and insert), and the consequence is a gap in the office identifier sequence, not a correctness failure. Identifier sequences are not required to be contiguous. | -| Unique-violation classification across dialects | The violation MUST be attributed to `uniq_tasks_external_id` specifically — by constraint name on PostgreSQL via typed `pgconn.PgError` inspection, and by the constraint-bearing message on SQLite. A bare `23505` check is insufficient: it matches any unique violation, so a primary-key failure would be misread as a Found outcome. | -| Crash between the task insert and settlement | The task exists unsettled. A retry returns **Found, unsettled**. Nothing is deleted or repaired. | -| Crash after settlement, before the response reaches the caller | A retry returns **Found, settled**. | -| The task is deleted while its create is still running | Settlement affects zero rows. Async dispatch is suppressed, state is re-read, and the response reflects reality rather than claiming a create that no longer exists. | -| The identity is released while its create is still running | Settlement affects zero rows (the `external_id` predicate no longer matches). Same handling as above; the task survives without an identity. | -| Found outcome where `start_agent: true` was requested | No agent launched, no session created. This is the central safety property. | -| Found outcome on an archived task | The archived task is returned with `archived_at` set. Not auto-unarchived. | -| Found outcome where the request carried different repositories, attachments, parent, or policy | All ignored; the existing task is returned unmodified. | -| A retry fails handler validation because server state changed | The request fails with that handler's existing error. Documented, not worked around. Use the lookup route for drift-robust recovery. | -| Workspace deleted | Its tasks are deleted; their identities go with them. | -| A caller supplies an external ID on a subtask create | Accepted and applied to the subtask, scoped to the effective workspace. Never inherited from the parent. | - -## Persistence guarantees - -- Both columns live on `tasks` and survive restart with the row. Nothing is - cached, and there is no TTL. -- **There is no idempotency window.** A held identity resolves against the - current table however old the task is. -- An unsettled task persists indefinitely. Kandev never sweeps, expires, or - repairs one. It is visible and mutable like any other task. -- A released or deleted identity is gone irreversibly; no history is retained. - -## Scenarios - -### Golden path - -- **GIVEN** workspace `W` has no task holding `ext-1`, **WHEN** a client POSTs - `/api/v1/tasks` with `workspace_id: W` and `external_id: ext-1`, **THEN** a - task is created, the response carries `external_id: "ext-1"`, - `deduplicated: false`, `creation_complete: true`, a `task.created` event is - published, and the row has a non-NULL `external_id_settled_at`. -- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** a client POSTs the - same create again, **THEN** the response is `200` with `id` equal to `T.id`, - `deduplicated: true`, `creation_complete: true`, `W` still has exactly one - task holding `ext-1`, and no `task.created` event is published. -- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** a client GETs the - lookup route, **THEN** the response is `200` with `T`'s DTO and no task is - created. -- **GIVEN** no task holds `(W, ext-nope)`, **WHEN** a client GETs the lookup, - **THEN** the response is `404`. - -### Lookup-before-write ordering - -- **GIVEN** an office workspace `W` whose `task_sequence` is `N`, and a settled - task holding `(W, ext-1)`, **WHEN** a client POSTs a create with - `external_id: ext-1` and office-task parameters, **THEN** the existing task is - returned and `W.task_sequence` is **still `N`** — the step-3 lookup resolved - it before any allocation. -- **GIVEN** an office workspace `W` whose `task_sequence` is `N` and **no** task - holding `(W, ext-4)`, **WHEN** two office creates with `external_id: ext-4` - race such that both miss the step-3 lookup, **THEN** exactly one task is - created, the loser returns a Found outcome, and `W.task_sequence` may be - `N+2` — the loser's allocated identifier is not reclaimed. This is the single - permitted side effect of a Found outcome, and the sequence is not required to - be contiguous. -- **GIVEN** a workflow step at its WIP limit, and a settled task holding - `(W, ext-1)` on that step, **WHEN** a client POSTs a create with - `external_id: ext-1` targeting that step, **THEN** the existing task is - returned with `deduplicated: true` — **not** a WIP-capacity error. The step-3 - lookup resolves it before admission is consulted. -- **GIVEN** a workflow step with **exactly one remaining WIP slot** and **no** - task yet holding `(W, ext-2)`, **WHEN** two creates with `external_id: ext-2` - targeting that step race such that both miss the step-3 lookup, the winner - consumes the last slot and inserts, and the loser is then rejected by WIP - admission **before** reaching the insert, **THEN** the loser re-reads by - `(workspace_id, external_id)`, finds the winner's task, and returns it as a - Found outcome — **not** the capacity error. This is the admission-preemption - guard; without it the contract's "a retry always returns the existing task" - promise fails exactly when the step has just saturated. - - The one-remaining-slot precondition is load-bearing: if the step were - already full before either request, neither could insert, so there would be - no winner for the loser to find and the scenario would be unsatisfiable. -- **GIVEN** the same saturated step and **no** task holding `(W, ext-3)`, - **WHEN** a single create with `external_id: ext-3` is rejected by WIP - admission and the re-read still finds nothing, **THEN** the original capacity - error surfaces unchanged — the guard must not swallow genuine failures. -- **GIVEN** a settled task holding `(W, ext-1)`, **WHEN** a client POSTs a create - with `external_id: ext-1` and a `repositories` entry naming a repository that - no longer exists, **THEN** the outcome depends on where that validation lives: - if the handler rejects it pre-service the request fails (documented - limitation); if it reaches the service, the existing task is returned. The - implementation MUST state which, and a test MUST pin the actual behavior. - -### Unsettled outcome and the unsafe-recovery guard - -- **GIVEN** a task `T` holding `(W, ext-1)` with `external_id_settled_at IS NULL`, - **WHEN** a client POSTs a create with `external_id: ext-1`, **THEN** the - response is `200` with `T.id`, `deduplicated: true`, and - `creation_complete: false`; `T` is unmodified; no new task is created. -- **GIVEN** the same unsettled task `T`, **WHEN** a client POSTs that create ten - more times over any span of time, **THEN** every response is identical and `T` - is never deleted, repaired, or duplicated — there is no timeout after which - behavior changes. -- **GIVEN** the same unsettled task `T`, **WHEN** a client GETs the lookup for - `ext-1`, **THEN** the response is `200` with `creation_complete: false`. -- **GIVEN** a create for `ext-1` still running its required synchronous work, - **WHEN** a second caller releases `ext-1` and creates again, **THEN** two tasks - exist for `ext-1`'s entity. This is the documented unsafe path; the test - exists to pin the consequence, and the MCP tool description and REST docs MUST - warn against automating it. -- **GIVEN** an unsettled task `T` holding `(W, ext-1)`, **WHEN** an operator - DELETEs the release route and then a client POSTs the create again, **THEN** - the release returns `204`, `T` still exists with NULL `external_id`, and the - create produces a **new** task with `deduplicated: false`. - -### Settlement predicate and zero-row handling - -- **GIVEN** a create for `ext-1` that has committed its task row and finished its - required synchronous work, **WHEN** settlement runs, **THEN** exactly one row - is updated and asynchronous dispatch proceeds. -- **GIVEN** a create for `ext-1` whose identity is **released** by another actor - after the row commit but before settlement, **WHEN** settlement runs, **THEN** - it affects **zero** rows — the `external_id` predicate no longer matches — no - asynchronous work is dispatched, and the response is `200` with - `deduplicated: false`, `creation_complete: true`, and **no** `external_id` - field, i.e. outcome `CreatedIdentityLost`. -- **GIVEN** the same released-before-settlement race, **WHEN** the caller - inspects the response, **THEN** it distinguishes this from `Created` by the - absent `external_id`, and from `Found, unsettled` by `deduplicated: false`; - **AND** `creation_complete` is `true`, because this create's synchronous work - did finish. -- **GIVEN** every success outcome across the whole contract, **WHEN** their - response tuples are enumerated, **THEN** `creation_complete: false` appears in - exactly one — `Found, unsettled` — and always alongside `deduplicated: true`. -- **GIVEN** a create for `ext-1` whose **task is deleted** by another actor - before settlement, **WHEN** settlement runs, **THEN** it affects zero rows, no - asynchronous work is dispatched, and the response is the surface's existing - not-found error rather than a fabricated success. -- **GIVEN** any settlement that affects zero rows, **WHEN** the handler - continues, **THEN** no agent is launched and no PR association is started. - -### No side effects on a found outcome - -- **GIVEN** a settled task `T` holding `(W, ext-1)` with exactly one agent - session, **WHEN** a client POSTs a create with `external_id: ext-1` and - `start_agent: true`, **THEN** `T` still has exactly one session and no new - agent execution is started. -- **GIVEN** an **unsettled** task `T` holding `(W, ext-1)` with no session, - **WHEN** a client POSTs a create with `external_id: ext-1` and - `start_agent: true`, **THEN** `T` still has no session and no agent is - launched — an unsettled task is reported, never resumed. -- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** a client POSTs a - create with `external_id: ext-1`, `start_agent: true`, one attachment, and a - `repositories` entry naming a different repository with `fresh_branch: true` - (the flag is per-repository, not top-level), **THEN** `T` still has exactly its - original repositories, no new worktree or branch exists, the attachment is not - claimed, and no last-used task-create settings are recorded. -- **GIVEN** a settled task `T` holding `(W, ext-1)` and title `"Original"`, - **WHEN** a client POSTs a create with `external_id: ext-1` and title - `"Changed"`, **THEN** `T`'s stored title is still `"Original"`. - -### MCP - -- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** an agent calls - `create_task_kandev` with `external_id: ext-1` resolving to `W` and - `start_agent: true`, **THEN** the tool result JSON carries `T.id`, - `deduplicated: true`, `creation_complete: true`, and no new task or session is - created. -- **GIVEN** an **unsettled** task `T` holding `(W, ext-1)`, **WHEN** an agent - calls `create_task_kandev` with `external_id: ext-1`, **THEN** the result - carries `T.id` with `creation_complete: false` and no task is created or - started. -- **GIVEN** a settled task `T` holding `(W, ext-1)` whose repository list differs - from the retry payload, **WHEN** an agent calls `create_task_kandev` with - `external_id: ext-1` and a `repository_url` that would resolve to a remote - contribution, **THEN** `T` is returned, no remote contribution is associated, - **and `T` still exists**. This is the data-loss guard; required coverage. -- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** an agent calls - `create_task_kandev` with `external_id: ext-1` and a `workspace_mode` needing - a workspace-policy attach, **THEN** no policy is attached and `T` still exists. -- **GIVEN** a settled task holding `(W, ext-1)` and `W` is the only workspace, - **WHEN** an agent calls `create_task_kandev` with `external_id: ext-1` and no - `workspace_id`, **THEN** that task is returned with `deduplicated: true`. -- **GIVEN** a settled task holding `(W, ext-1)`, **WHEN** an agent calls - `create_task_kandev` with `parent_id` naming a task in a different workspace - `W2` and `external_id: ext-1`, **THEN** a new subtask is created in `W2` - holding `ext-1`, because the effective workspace is `W2`. -- **GIVEN** an agent whose session belongs to a task in workspace `W`, and a task - holding `(W2, ext-1)` in a workspace the agent's task does not belong to, - **WHEN** the agent calls `create_task_kandev` targeting `W2` with - `external_id: ext-1`, **THEN** the call is denied by MCP identity scoping and - reveals nothing about `W2`. - -### Uniqueness and concurrency - -- **GIVEN** a task holding `(W1, ext-1)` and nothing in `W2`, **WHEN** a client - POSTs a create for `workspace_id: W2` with `external_id: ext-1`, **THEN** a new - task is created in `W2` with `deduplicated: false`. -- **GIVEN** two tasks in `W` with no external IDs, **WHEN** a third is created - without one, **THEN** the create succeeds — absent identities never collide. -- **GIVEN** a task `P` in `W` holding `ext-parent`, **WHEN** a client creates a - subtask of `P` with no `external_id`, **THEN** the subtask holds no identity. -- **GIVEN** no task holds `(W, ext-race)`, **WHEN** two creates with - `external_id: ext-race` are issued concurrently and both pass the step-3 - lookup, **THEN** exactly one task in `W` holds `ext-race`, both requests return - `200`, the outcomes are (`deduplicated: false`, `deduplicated: true`), and - **no orphan task row exists from the loser**. -- **GIVEN** the same race against **PostgreSQL**, **THEN** the same assertions - hold — an environment-gated PostgreSQL test, since the SQLite path passing is - not evidence for it. -- **GIVEN** a create whose task-row insert collides on the `tasks` primary key - rather than on `uniq_tasks_external_id`, **THEN** the response is an error and - **not** a Found outcome. - -### Validation - -- **GIVEN** any workspace, **WHEN** a client POSTs a create with an - `external_id` whose trimmed UTF-8 length is 256 bytes, **THEN** the response - is `400`, no task is created, and no agent is launched. -- **GIVEN** any workspace, **WHEN** a client POSTs a create with `external_id` - `"ext-1\n"`, **THEN** the response is `400` — the control character is - rejected before trimming. -- **GIVEN** any workspace, **WHEN** a client POSTs a create with `external_id` - `"\t"`, **THEN** the response is `400`, not "absent". -- **GIVEN** any workspace, **WHEN** a client POSTs a create with `external_id` - `" "`, **THEN** the task is created with a NULL `external_id`, and the - response omits `external_id`. -- **GIVEN** a task holding `(W, ext-1)`, **WHEN** a client POSTs a create with - `external_id: " ext-1 "`, **THEN** that task is returned with - `deduplicated: true` — trimming precedes the lookup. -- **GIVEN** a task holding `(W, ext-1)`, **WHEN** a client POSTs a create with - `external_id: "EXT-1"`, **THEN** a new task is created — case-sensitive. -- **GIVEN** a PostgreSQL install whose database collation is case-insensitive, - **WHEN** the previous scenario runs, **THEN** it still produces two distinct - tasks — the column's deterministic collation overrides the database default. - -### Lifecycle - -- **GIVEN** a settled task `T` holding `(W, ext-1)` that has been archived, - **WHEN** a client POSTs a create with `external_id: ext-1`, **THEN** `T` is - returned with `deduplicated: true` and a non-null `archived_at`, and `T` - remains archived. -- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** `T` is deleted and - a client POSTs the create again, **THEN** a new task is created with - `deduplicated: false` — idempotency is scoped to the task's lifetime. -- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** `T` is deleted by - an office handoff cascade that calls the repository directly, **THEN** the - identity is free and a subsequent create succeeds — no code path can leave a - stale identity behind. -- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** a client DELETEs - the release route, **THEN** the response is `204`, `T` still exists with NULL - `external_id` and NULL `external_id_settled_at`, and the lookup returns `404`. -- **GIVEN** a task `T` holding `ext-1`, **WHEN** a client PATCHes - `/api/v1/tasks/T` with an `external_id` field in the body, **THEN** `T`'s - identity is unchanged. - -### Permissions - -- **GIVEN** auth is enabled, a settled task holding `(W, ext-1)` owned by user - `A`, and user `B` who does not own `W`, **WHEN** `B` POSTs a create for - `workspace_id: W` with `external_id: ext-1`, **THEN** `B` receives - `404 {"error": "task not created"}`, the response carries no `deduplicated`, - no `creation_complete`, and no field of `A`'s task, and no task is created. -- **GIVEN** the same setup but an **unsettled** task, **WHEN** `B` POSTs the - same create, **THEN** `B` receives `404` — settledness is not observable - across an authorization boundary. -- **GIVEN** the same setup, **WHEN** `B` GETs or DELETEs the by-external-id route - for `W`, **THEN** the response is `404`. -- **GIVEN** user `B` unauthorized for `W`, **WHEN** `B` POSTs a create for `W` - with a 300-byte `external_id`, **THEN** the response is `404`, not `400` — - authorization precedes validation. - -### Read surfaces and the deferred write surfaces - -- **GIVEN** a task holding `ext-1`, **WHEN** it is returned by a REST read, a - REST list, an MCP task tool, or a WS task lifecycle event, **THEN** each of - those four carries `external_id: "ext-1"`. -- **GIVEN** a task holding no identity, **WHEN** any of those four returns it, - **THEN** the representation omits `external_id` rather than sending `null` or - `""`. -- **GIVEN** a task holding `ext-1`, **WHEN** it is returned by an ordinary task - read, **THEN** the representation does **not** carry `creation_complete`. -- **GIVEN** a task holding `(W, ext-1)`, **WHEN** a client sends the - `task.create` WS action with an `external_id` field, **THEN** a new task is - created and the field is ignored. -- **GIVEN** a task holding `(W, ext-1)`, **WHEN** a plugin calls - `Tasks().Create` with `StartAgent: true`, **THEN** a new task is created and - started normally. - -### Migration - -- **GIVEN** an existing database whose tasks predate this feature, **WHEN** the - backend starts, **THEN** both columns exist and are NULL for every - pre-existing task, `uniq_tasks_external_id` exists, and creating tasks without - an external ID continues to succeed. -- **GIVEN** the same database, **WHEN** the backend starts a second time, - **THEN** the migration replays without error. - -## Out of scope - -### Deferred write surfaces (designed, not built) - -Neither has a named consumer today, and they are **not equally cheap** to -enable. Each is additive. - -| Surface | Cost to enable later | -|---|---| -| WebSocket `task.create` | **Low.** One request-struct field, one line in the service-call literal, and skip the launch and last-used recording on a found outcome. One file. Deferred because it is the UI's own path: nothing retries on the UI's behalf. | -| Plugin host | **Meaningfully higher.** `plugin.pb.go` and `plugin_grpc.pb.go` are generated and committed, so a proto edit requires regenerating them through the Makefile proto target (installs `protoc-gen-go`, needs network). It also needs a new SDK method — **do not change the existing `TaskReader.Create` signature**, which is source-breaking for shipped plugins — a mapper change, and a release in each separate plugin repo. | - -### Other non-goals - -- **Detecting whether an unsettled create is still alive.** No lease, heartbeat, - or owner token. This is the deliberate boundary of the feature. -- **Repairing, resuming, adopting, or garbage-collecting unsettled tasks.** -- **Any timeout or expiry on an unsettled task.** -- **A tombstone for deleted identities.** See *Idempotency is scoped to the - task's lifetime*. -- **An MCP lookup tool.** MCP gets an idempotent create-if-absent, not a probe; - no in-scope MCP flow needs to ask without being willing to create. Adding a - read-only tool later is small and additive. -- **Changing an external ID in place.** Write-once; re-key via release + create. -- **System-generated external IDs.** -- **Idempotency for anything other than task creation.** `spawn_session_kandev` - is not covered. -- **Restructuring the create handlers** so identity resolution precedes - handler-level validation. The consequence — payload and server-state drift can - fail a retry before dedupe — is documented, not engineered around. -- **Retiring the existing integration dedupe tables.** -- **Replacing the office `runs.idempotency_key` mechanism.** -- **The office runtime `create_task` HTTP action.** -- **Request-payload fingerprinting.** -- **Cross-workspace uniqueness or a global namespace.** -- **A UI surface** for entering, displaying, or releasing external IDs. -- **Bulk lookup or bulk release.** -- **Backfilling external IDs onto existing tasks.** - -## Open questions - -None. Every decision is settled; downstream steps implement as written and route -back here if a change is needed. - -## Appendix: verified current state - -Claims checked against the code at `09fa82144` (a fast-forward of `origin/main` -at `cba3838c8`). - -**In-scope entry points.** REST `POST /api/v1/tasks` (`task_handlers.go:140` → -`httpCreateTask` at `task_http_handlers.go:826`, body struct at `:730`) and MCP -`create_task_kandev` (tool schema `internal/mcp/server/server.go:805`, client -handler `internal/mcp/server/handlers.go:142`, backend handler at -`internal/mcp/handlers/handlers.go:559`). Both converge on -`taskservice.Service.CreateTask` (`internal/task/service/service_tasks.go:124`), -which authorizes first at `:125`. - -**Why the lookup must precede allocation and admission.** `assignIdentifier` -runs at `service_tasks.go:160`, before `createTaskWithCapacity` at `:166`, and -calls `IncrementTaskSequence` — an unconditional -`UPDATE workspaces SET task_sequence = task_sequence + 1` -(`repository/sqlite/task.go:2328`). Admission checks precede the insert -(`task.go:215`) and can return before it (`:257`). Both were confirmed by -adversarial review. - -**Why settlement needs the `external_id` predicate.** Release sets both columns -to `NULL`, so a predicate of `id` plus `external_id_settled_at IS NULL` still -matches a released row. - -**Why settlement lives in the handlers.** The service publishes `task.created` -and returns before caller-specific work (`service_tasks.go:171`, `:190`). REST -then claims attachments (`task_http_handlers.go:912`), attaches policy (`:933`), -commits a fresh branch (`:947`), and prepares/starts a session (`:954`); MCP -associates remote contributions (`handlers.go:715`), attaches policy (`:737`), -and launches (`:749`). - -**Why REST's session helper must be split.** Preparation errors are swallowed at -`task_http_handlers.go:1237` while the start goroutine is dispatched at `:1289`, -inside the same helper. PR association is background work at `:1201`. MCP -auto-start is a goroutine at `handlers.go:1303`. - -**The MCP data-loss path.** `resolveMCPRemoteContributions` derives contributions -from the request at `handlers.go:658`; the handler indexes them against the -returned task's repositories at `:715-719`; the guard at `:719` calls -`DeleteTask` at `:720`. Further rollback-delete paths at `:729` and `:741`. -Independently confirmed. - -**Schema placement.** The `tasks` DDL is -`internal/task/repository/sqlite/base_schema.go:295`. Per -`apps/backend/AGENTS.md`, a column added to an existing table must come from an -idempotent `ADD COLUMN` in `runMigrations()`, with anything referencing it -following there, not in schema init. Precedents at `base_migrations.go:165` and -`:1086`. The insert to extend is `insertTaskTx` (`repository/sqlite/task.go:316`), -whose task insert and runner participant are genuinely transactional — a unique -loser leaves no row and does not double-count WIP. - -**Response shape.** `createTaskResponse` (`task_http_handlers.go:761`) embeds -`dto.TaskDTO` (`internal/task/dto/dto.go:143`); the current success path returns -`http.StatusOK`, not `201` (`:960`). `TaskDTO` already carries the analogous -optional `Identifier` at `dto.go:195`. Unauthorized/not-found maps to `404` via -`handleNotFound` (`internal/task/handlers/errors.go:20`). - -**Representations are not all TaskDTO.** WS task lifecycle events are a -hand-built map in `publishTaskEventNow` (`service_events.go:367`). Plugins have -an independent proto message, SDK type, and mapper (`plugin.proto:216`, -`pkg/pluginsdk/data_types.go:92`, `internal/plugins/host_data.go:878`); -`CreateTaskResponse` holds only `Task task = 1` (`plugin.proto:456`) and SDK -`TaskReader.Create` returns `(*Task, error)` (`pkg/pluginsdk/host.go:138`). - -**Dialect handling.** Partial unique indexes work on both dialects. A typed -`pgconn.PgError` precedent exists at `repository/sqlite/task.go:2238`. Every -existing *unique*-violation helper matches SQLite text only -(`internal/office/repository/sqlite/wakeup_requests.go:246`, -`internal/github/store.go:1433`, `internal/prompts/service/service.go:64`, -`internal/sentry/store.go:948`), and per `apps/backend/AGENTS.md` this package -also runs against PostgreSQL. - -**Deletion shadow paths.** Office handoff cascades delete tasks through the -repository directly (`handoff_cascade.go:332`). This design is immune: the -identity is a column on the deleted row. - -**Design provenance.** Three independent adversarial reviews (OpenAI Codex, -read-only). Pass 1 (unique index): 10 P1s, including a crash between the row -commit and post-create work leaving a half-created task returned as complete. -Pass 2 (reservation table + lease + reclaim): NOT READY, 7 P1s — no fencing -token, destructive release of live claims, non-atomic tombstoning across -deletion shadow paths, an unimplementable completion protocol given async work, -unsettled rows visible to ordinary reads, unsatisfiable handler ordering. -Pass 3 (identity on the task row, honest reporting): NOT READY, 7 P1s — the -decisive one being that `complete: false` cannot distinguish a live create from -an abandoned one, so the release-and-recreate "recovery" it implied could -produce duplicate tasks and agents. - -This version responds by **removing the recovery claim rather than the -information**. The flag remains, explicitly diagnostic; the unsafe automated -action is named and forbidden; the settlement predicate, its zero-row handling, -its call site, and the lookup-before-write ordering are now specified concretely. -All factual findings from every pass were independently re-verified against -source before acceptance. diff --git a/docs/specs/tasks/agent-generated-titles.md b/docs/specs/tasks/requirements/agent-generated-titles.md similarity index 94% rename from docs/specs/tasks/agent-generated-titles.md rename to docs/specs/tasks/requirements/agent-generated-titles.md index 5c4b37ef59..274f6cb633 100644 --- a/docs/specs/tasks/agent-generated-titles.md +++ b/docs/specs/tasks/requirements/agent-generated-titles.md @@ -1,11 +1,28 @@ --- -status: approved +status: active +system: tasks created: 2026-07-31 updated: 2026-08-04 -owner: kandev +owners: + - kandev --- +# Agent-Generated Task Titles Requirements -# Agent-Generated Task Titles +## 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. + +## Requirements + +### REQ-TASKS-AGENT-GENERATED-TITLES-001: Agent-Generated Task Titles + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-AGENT-GENERATED-TITLES-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -14,9 +31,9 @@ prefer prompt-first creation need tasks and subtasks to appear immediately with letting the assigned agent replace that label with a concise title. Decisions: -[ADR-2026-07-31-agent-generated-task-titles](../../decisions/2026-07-31-agent-generated-task-titles.md) +[ADR-2026-07-31-agent-generated-task-titles](../../../decisions/2026-07-31-agent-generated-task-titles.md) and -[ADR-2026-08-02-single-owner-agent-task-titles](../../decisions/2026-08-02-single-owner-agent-task-titles.md). +[ADR-2026-08-02-single-owner-agent-task-titles](../../../decisions/2026-08-02-single-owner-agent-task-titles.md). ## What @@ -276,5 +293,5 @@ provisional-title name. ## Implementation plan -- [Agent-Generated Task Titles](../../plans/agent-generated-task-titles/plan.md) -- [Agent Title Branch Renaming](../../plans/agent-title-branch-renaming/plan.md) +- [Agent-Generated Task Titles](../../../plans/agent-generated-task-titles/plan.md) +- [Agent Title Branch Renaming](../../../plans/agent-title-branch-renaming/plan.md) \ No newline at end of file diff --git a/docs/specs/tasks/archive-confirmation.md b/docs/specs/tasks/requirements/archive-confirmation.md similarity index 84% rename from docs/specs/tasks/archive-confirmation.md rename to docs/specs/tasks/requirements/archive-confirmation.md index c13abeb1c3..03ce35c874 100644 --- a/docs/specs/tasks/archive-confirmation.md +++ b/docs/specs/tasks/requirements/archive-confirmation.md @@ -1,11 +1,28 @@ --- -status: shipped +status: active +system: tasks created: 2026-07-15 updated: 2026-08-12 -owner: kandev +owners: + - kandev --- +# Task Archive Confirmation Requirements -# Task Archive Confirmation +## 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. + +## Requirements + +### REQ-TASKS-ARCHIVE-CONFIRMATION-001: Task Archive Confirmation + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### 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 @@ -72,5 +89,5 @@ The preference survives backend and client restarts as part of the existing user ## Implementation plan -- [Archive Confirmation Preference](../../plans/archive-confirmation-preference/plan.md) -- [Cascade Archive Navigation](../../plans/cascade-archive-navigation/plan.md) +- [Archive Confirmation Preference](../../../plans/archive-confirmation-preference/plan.md) +- [Cascade Archive Navigation](../../../plans/cascade-archive-navigation/plan.md) \ No newline at end of file diff --git a/docs/specs/tasks/requirements/attach-workspace-sources.md b/docs/specs/tasks/requirements/attach-workspace-sources.md new file mode 100644 index 0000000000..de123c90a4 --- /dev/null +++ b/docs/specs/tasks/requirements/attach-workspace-sources.md @@ -0,0 +1,30 @@ +--- +status: active +system: tasks +created: 2026-07-22 +owners: + - kandev +--- +# Attach Workspace Sources Requirements + +## Overview + +Tasks can add repositories and folders after creation without losing their existing conversation, +state, or repository attachments. The operation is validated and materialized as one task-workspace +change. + +## Requirements + +### REQ-TASKS-ATTACH-WORKSPACE-SOURCES-001: Attach Workspace Sources + +**Intent:** Let users and agents attach supported workspace sources while preserving task state and +providing atomic validation and materialization. + +#### Acceptance criteria + +- **AC-TASKS-ATTACH-WORKSPACE-SOURCES-001.1:** When an idle task opens workspace actions, the system shall offer source attachment and workspace-folder actions on desktop and touch surfaces, and shall preserve configured source rows while the user builds a batch. +- **AC-TASKS-ATTACH-WORKSPACE-SOURCES-001.2:** When a task submits valid repository and folder sources, the system shall persist and materialize every source, expose repository sources in repository-aware task surfaces, and keep folders file-only. +- **AC-TASKS-ATTACH-WORKSPACE-SOURCES-001.3:** When a submission contains an invalid, inaccessible, contradictory, cross-workspace, or executor-incompatible source, the system shall reject it before changing the task or executor workspace. +- **AC-TASKS-ATTACH-WORKSPACE-SOURCES-001.4:** When any source in a multi-source submission fails during materialization, the system shall roll back all new attachments and restore any pre-existing Kandev-owned entry that the submission repointed. +- **AC-TASKS-ATTACH-WORKSPACE-SOURCES-001.5:** When attachment changes the effective task root, the system shall preserve task state, plans, conversations, sessions, and existing attachments, publish the updated workspace projection, and either retain or rehydrate the provider session according to executor capabilities. +- **AC-TASKS-ATTACH-WORKSPACE-SOURCES-001.6:** When an agent uses the legacy worktree-only add-branch action, the system shall create the new worktree as a task-root sibling, return its exact paths, refresh task projections, and leave the running agent working directory unchanged. diff --git a/docs/specs/tasks/autopilot-mode.md b/docs/specs/tasks/requirements/autopilot-mode.md similarity index 93% rename from docs/specs/tasks/autopilot-mode.md rename to docs/specs/tasks/requirements/autopilot-mode.md index 2a4362b18d..1100a2f3f8 100644 --- a/docs/specs/tasks/autopilot-mode.md +++ b/docs/specs/tasks/requirements/autopilot-mode.md @@ -1,10 +1,27 @@ --- -status: implemented +status: active +system: tasks created: 2026-08-08 -owner: cfl +owners: + - cfl --- +# Task Autopilot Mode Requirements -# Task Autopilot Mode +## 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. + +## Requirements + +### REQ-TASKS-AUTOPILOT-MODE-001: Task Autopilot Mode + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-AUTOPILOT-MODE-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -18,8 +35,8 @@ Autopilot makes autonomy an explicit task contract. It is visible to the operato changes the agent's instructions and available MCP tools, and gives an exceptional question a non-blocking, parent-owned lifecycle. -Decision: [ADR-2026-08-08-task-autopilot-contract](../../decisions/2026-08-08-task-autopilot-contract.md) -MCP profile decision: [ADR-2026-08-08-mcp-tool-profiles](../../decisions/2026-08-08-mcp-tool-profiles.md) +Decision: [ADR-2026-08-08-task-autopilot-contract](../../../decisions/2026-08-08-task-autopilot-contract.md) +MCP profile decision: [ADR-2026-08-08-mcp-tool-profiles](../../../decisions/2026-08-08-mcp-tool-profiles.md) ## User experience @@ -310,4 +327,4 @@ interaction. - Asking the operator from a top-level autopilot task. - Applying autopilot to Office, quick chat, utility/config sessions, or runtimes that cannot enforce the Kandev task MCP contract. -- General-purpose approvals, permissions, or multi-party voting. +- General-purpose approvals, permissions, or multi-party voting. \ No newline at end of file diff --git a/docs/specs/tasks/blocked-task-escalation.md b/docs/specs/tasks/requirements/blocked-task-escalation.md similarity index 85% rename from docs/specs/tasks/blocked-task-escalation.md rename to docs/specs/tasks/requirements/blocked-task-escalation.md index 565fc31940..e637bec03b 100644 --- a/docs/specs/tasks/blocked-task-escalation.md +++ b/docs/specs/tasks/requirements/blocked-task-escalation.md @@ -1,10 +1,27 @@ --- status: draft +system: tasks created: 2026-04-28 -owner: cfl +owners: + - cfl --- +# Blocked Task Escalation Requirements -# Blocked Task Escalation +## 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. + +## Requirements + +### REQ-TASKS-BLOCKED-TASK-ESCALATION-001: Blocked Task Escalation + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-BLOCKED-TASK-ESCALATION-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -39,4 +56,4 @@ When an agent hits a decision point it cannot resolve on its own — a missing c ## Open questions -- Should the human task be left unassigned (relies on the human scanning the board) or should a `KANDEV_HUMAN_USER_ID` workspace setting be introduced so the task is explicitly assigned? The unassigned approach works today; explicit assignment requires adding the setting. +- Should the human task be left unassigned (relies on the human scanning the board) or should a `KANDEV_HUMAN_USER_ID` workspace setting be introduced so the task is explicitly assigned? The unassigned approach works today; explicit assignment requires adding the setting. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/clarification-active-lifecycle-scenarios.md b/docs/specs/tasks/requirements/clarification-active-lifecycle-scenarios.md new file mode 100644 index 0000000000..e8bcfd1669 --- /dev/null +++ b/docs/specs/tasks/requirements/clarification-active-lifecycle-scenarios.md @@ -0,0 +1,183 @@ +--- +status: active +system: tasks +created: 2026-08-14 +owners: + - kandev +--- + + +# Active clarification lifecycle scenarios Requirements + + + +## Overview + + + +The lifecycle contract covers timeout, supersession, response, cleanup, and recovery scenarios across live and detached sessions. + + + +## Scenarios + +- **GIVEN** a clarification wait disconnects and no newer turn exists, **WHEN** the task is reloaded, + **THEN** the detached question remains visible and answerable, its session and task advertise + `clarification`, and workflow completion stays blocked. +- **GIVEN** an older turn retains a detached pending row, **WHEN** the session accepts a newer ordinary + turn, **THEN** the old row remains in history but no overlay, pending projection, detach event, or + workflow barrier derives from it. +- **GIVEN** a newer turn superseded an older pending question, **WHEN** every message in the newer turn + is deleted, **THEN** the durable newer turn remains current and the older question stays inert. +- **GIVEN** an attempted successor reservation is authoritative but intentionally hidden from client + turn history, **WHEN** the session projection explicitly reports no clarification action, **THEN** a + pending question on the visible predecessor stays inert in chat and pending-input indicators. +- **GIVEN** a browser cached an explicit clean session projection, **WHEN** a new current-turn + clarification message arrives, **THEN** the same event advances the session projection to + `clarification` before pending discovery so the question remains visible and answerable. +- **GIVEN** a task-session list request is in flight, **WHEN** a newer semantic message event updates + the same session before the HTTP response resolves, **THEN** the response's older projection + revision cannot replace the WebSocket projection (and the reverse delivery order is equally safe). +- **GIVEN** an old detached question and a newer clarification bundle, **WHEN** the user skips the + newer bundle and reloads, **THEN** neither bundle reappears, the task question icon is absent, and + later turn completion cannot re-arm the old bundle. +- **GIVEN** a persisted summary says `pending_action=clarification` while current-turn repository state + has no pending action, **WHEN** a boot or task-list payload is built, **THEN** the summary revision + advances with no pending action and all unrelated fields remain unchanged. +- **GIVEN** a task's secondary session owns a current clarification while the primary session is + clean, **WHEN** the user activates the task from the desktop sidebar or phone task drawer, **THEN** + Kandev bypasses any cached session list, selects the secondary session, closes the drawer when + applicable, and shows the question. +- **GIVEN** clarification pause observes no active turn, **WHEN** detachment creates the session's first + turn before cancellation registration completes, **THEN** the pause rejects its stale cancellation + and does not cancel or complete that new turn. +- **GIVEN** pending-owner loading began for one task-summary revision, **WHEN** a newer summary changes + the pending action before the session response is applied, **THEN** desktop and phone activation do + not navigate to the obsolete owner, open the selected task through the task-only fallback, and close + the phone drawer. +- **GIVEN** two forced session loads overlap for one task, **WHEN** the older response finishes last, + **THEN** it cannot overwrite the newer session snapshot or revive an obsolete pending owner. +- **GIVEN** a forced session load is pending in the phone task sheet, **WHEN** a responsive-layout + change unmounts it and mounts the tablet sheet before that load finishes, **THEN** the old + continuation cannot navigate, change the active selection, or close the replacement sheet. +- **GIVEN** a pending task has no loaded owner, **WHEN** activation falls back to its task route, + **THEN** the outgoing session layout is released before the active session is cleared. +- **GIVEN** a stale browser still displays a superseded question, **WHEN** it submits an answer, + **THEN** the server returns conflict and does not resume or otherwise prompt the agent. +- **GIVEN** a detached current-turn answer is not accepted by the orchestrator, **WHEN** the response + endpoint fails, **THEN** it returns a server error, keeps the question answerable, and a later retry + delivers exactly one accepted resume request without a failed-dispatch turn superseding the bundle. +- **GIVEN** agentctl accepts a detached answer but successor publication fails, **WHEN** the response + endpoint completes, **THEN** it returns an error, keeps the answer terminal, and does not make that + accepted successor eligible for rollback or in-process redispatch. +- **GIVEN** agentctl accepts a detached answer and later transport handling also fails, **WHEN** prompt + handling returns, **THEN** normal failure cleanup runs and the accepted-answer marker still prevents + the terminal clarification from being reopened. +- **GIVEN** the backend stops after terminalizing a detached answer and reserving its successor but + before marking dispatch attempted, **WHEN** it starts again, **THEN** it deletes the empty + reservation, restores the exact claimed rows to pending, and leaves pre-existing terminal siblings + unchanged. +- **GIVEN** the backend stops after marking a detached-answer dispatch attempted but before observing + acknowledgement, **WHEN** it starts again, **THEN** it preserves the successor as current and keeps + the exact claimed rows terminal rather than risking duplicate answer dispatch. +- **GIVEN** the backend stops after claiming a current-turn response but before either a live waiter or + detached resumer receives it, **WHEN** it starts again, **THEN** startup restores the exact claimed + rows to pending and the same answer can be submitted again. +- **GIVEN** a response is enqueued to a live waiter, **WHEN** durable confirmation has not completed, + **THEN** the waiter does not return the response and restart recovery may safely restore the claim. +- **GIVEN** durable confirmation starts but outlives the responder's bounded wait, **WHEN** the responder + attempts recovery, **THEN** the immutable claim is race-free and repository serialization lets either + finalization or restore win without reopening a successfully finalized response. +- **GIVEN** cancellation removes a live clarification after a responder loads it but before response + delivery is claimed, **WHEN** that responder continues, **THEN** it returns not-found immediately so + the caller can use detached recovery instead of waiting for an orphaned delivery confirmation. If + response delivery wins first, later cancellation cannot preempt its durable confirmation. +- **GIVEN** a primary-answer watchdog survives until another turn supersedes its clarification turn, + **WHEN** its fallback timer expires, **THEN** both the preflight and serialized prompt-admission checks + reject the stale answer without prompting or cancelling the successor. +- **GIVEN** a primary-answer watchdog has entered fallback authority or prompt work, **WHEN** session + activity or service shutdown cancels the watchdog, **THEN** that cancellation reaches the in-flight + repository and prompt calls. +- **GIVEN** a reserved successor is marked dispatch-attempted but remains unpublished, **WHEN** a client + loads turn history before agentctl accepts or rejects it, **THEN** the successor is omitted until + publication or durable message evidence prevents rollback from leaving stale client-only history. +- **GIVEN** a predecessor ready event arrives while a detached-answer successor is reserved, **WHEN** + the successor dispatch resolves, **THEN** the handler revalidates prompt generation and the stale + predecessor cannot complete the successor or run `on_turn_complete` against it; if reservation + rollback leaves that predecessor generation authoritative, its ready event completes normally. +- **GIVEN** reserved-successor deletion returns an error, **WHEN** rollback handling completes, **THEN** + the reservation waiter remains unresolved and another prompt cannot enter that session before + restart recovery. +- **GIVEN** an unpublished reservation stores an attempted marker as boolean `true`, string `"true"` or + `"1"`, or numeric `1`, **WHEN** startup recovery runs, **THEN** it preserves the ambiguous successor + and clears recovery metadata instead of restoring the answer and deleting the turn. +- **GIVEN** a generationless ready event waits on a live reservation, **WHEN** the reservation rolls + back, **THEN** Kandev drops the uncorrelatable event without changing session or workflow state. +- **GIVEN** a cancelled request triggers clarification detach or expiry, **WHEN** repository work + begins, **THEN** it uses a non-cancelled context with a finite deadline. +- **GIVEN** a session transition to completed, failed, or cancelled is durably accepted, **WHEN** the + transition publishes, **THEN** current-turn pending clarification bundles expire before a terminal + session can leave an interactive overlay behind. +- **GIVEN** a detached answer reaches synchronous orchestrator resume, **WHEN** the orchestrator does + not acknowledge it before the bounded deadline, **THEN** Kandev treats acceptance as failed and + restores the still-current bundle. +- **GIVEN** startup cannot reconcile an unpublished prompt reservation, **WHEN** the orchestrator starts, + **THEN** startup returns an error before watcher, scheduler, or prompt admission begins. +- **GIVEN** restart recovery preserves an accepted or ambiguous prompt reservation, **WHEN** its public + start event may have been missed, **THEN** startup durably retains and retries an ordered event replay + until `turn.started` and any already-required `turn.completed` event are accepted before clearing the + recovery marker. +- **GIVEN** an unpublished prompt reservation contains a malformed claimed-message ID list, **WHEN** + startup recovery decodes it, **THEN** recovery fails closed without deleting the reservation or + reopening only a subset of its claimed rows. +- **GIVEN** pending ownership changes while desktop or mobile task selection loads sessions, **WHEN** the + delayed load settles, **THEN** Kandev ignores its stale owner choice but still opens the selected task + through the task-only fallback, and the mobile sheet closes. +- **GIVEN** a task-list refresh observes a detached answer's temporary terminal claim, **WHEN** resume + rejection restores that claim, **THEN** Kandev acknowledges durable summary convergence before + reporting retryability and publishes the restored pending rows so other clients converge. +- **GIVEN** live publication or synchronous summary acknowledgement fails after restored clarification + rows commit, **WHEN** the detached resume returns, **THEN** Kandev reports the convergence error while + still identifying the durably restored answer as safe to retry. +- **GIVEN** the selected task projection disappears while desktop or mobile session loading is in + flight, **WHEN** the delayed load settles, **THEN** Kandev leaves task and session selection unchanged. +- **GIVEN** a newer desktop or mobile forced session load aborts an older load for the same task, + **WHEN** the older continuation handles `AbortError`, **THEN** it leaves the winning task and session + selection unchanged. +- **GIVEN** two phone task sheets are mounted, **WHEN** both have an in-flight task selection, **THEN** + closing or selecting within one sheet does not invalidate the other sheet's selection generation. +- **GIVEN** authoritative pending-action projection fails while listing a task's sessions, **WHEN** the + HTTP or WebSocket list request completes, **THEN** it returns an internal error rather than a false + clean-session response. +- **GIVEN** two request identities produce terminal and pending events close together, **WHEN** the + projector refreshes, **THEN** its result matches current repository state rather than event order. + +## Requirements + + + +### REQ-TASKS-CLARIFICATION-SCENARIOS-001: Active clarification lifecycle scenarios + + + +**Intent:** The lifecycle contract covers timeout, supersession, response, cleanup, and recovery scenarios across live and detached sessions. + + + +#### Acceptance criteria + + + +- **AC-TASKS-CLARIFICATION-SCENARIOS-001.1:** When any listed lifecycle scenario occurs, the system shall produce the observable state and response outcome described by that scenario. + + + +## Out of scope + +- Rewriting or deleting historical pending message rows solely to clean old data. +- A dedicated clarification table or schema migration. +- Redesigning the desktop sidebar, phone task drawer, chat cards, or clarification carousel. +- Changing permission-request lifecycle semantics; pending-session navigation reuses the shared + `pending_action` type for permission parity. +- Changing notification history for a clarification that was valid when its notification fired. +- Automatically choosing a non-primary session when the task has no current pending action. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/clarification-active-lifecycle.md b/docs/specs/tasks/requirements/clarification-active-lifecycle.md new file mode 100644 index 0000000000..9b27891f43 --- /dev/null +++ b/docs/specs/tasks/requirements/clarification-active-lifecycle.md @@ -0,0 +1,171 @@ +--- +status: active +system: tasks +created: 2026-08-14 +owners: + - kandev +--- + + +# Active clarification lifecycle Requirements + + + +## Overview + + + +Clarification bundles remain answerable only for the session turn that owns them, while detached and recovery paths preserve the response contract. + + + +## Why + +A clarification can outlive the agent wait that created it. Kandev keeps that detached question +answerable so a timeout or connection loss does not discard required user input. That durability must +not let a question from an older turn remain operational after the session has accepted newer work. + +One stale clarification currently can reappear in chat, restore a task-row question icon, and block a +workflow transition after the user has dismissed a newer question. A real question in a secondary +session can also produce a correct task-row icon while task navigation opens the clean primary session, +hiding the action the icon represents. + +## What + +- A clarification bundle is active only when at least one row in the bundle is pending and the bundle + belongs to the session's current turn. +- A detached bundle remains active and answerable while its turn remains current. Detachment sets + `agent_disconnected=true`; it does not by itself resolve the question. +- After a session successfully enters a terminal state, its current-turn pending clarification bundles + expire so no answerable overlay survives a completed, failed, or cancelled session. +- Acceptance of a newer turn supersedes every pending clarification from an older turn. Superseded + rows remain transcript history but cannot drive a chat overlay, task/session pending projection, + workflow guard, turn-completion detach pass, or late agent resume. +- Deleting every message from the newer turn does not move ownership backward or reactivate an older + clarification. +- All backend consumers derive active clarification state from one repository rule. Event payloads + trigger projection refreshes; they are not a second source of pending truth. +- Repeated detach/completion processing is a semantic no-op after a bundle is already detached. It + emits no duplicate `message.updated` occurrence. +- Detachment claims pending, non-detached rows from the current durable turn in one database update + and publishes only the rows that update returns. A concurrent answer or newer turn cannot be + overwritten by a stale read-modify-write detachment. +- Clarification-pause cancellation snapshots turn authority before detachment. With a wired turn + service, both a specific turn and the absence of a turn are explicit expectations; a first or + successor turn created during detachment cannot be cancelled by the stale pause. Installations + without a turn service retain the legacy unscoped fallback. +- Resolving, rejecting, cancelling, expiring, or deleting one bundle changes only that bundle. It + cannot clear or re-arm another bundle in the same session. +- The chat's Skip action rejects the exact visible bundle through the existing response endpoint. A + live waiter receives the rejection in the same turn. A detached current-turn bundle is persisted as + rejected without resuming the agent. +- An affirmative response to a detached current-turn bundle returns success only after the + orchestrator accepts one resume dispatch within a bounded wait. The response waits for prompt + acknowledgement, not agent-turn completion. Before dispatch, the successor is durably reserved but + marked unpublished so provider frames can reference it without making it current. Immediately before + the external executor call, Kandev durably marks the reservation attempted. That marker is the + at-most-once boundary: a crash can no longer prove whether agentctl accepted the prompt, so restart + preserves the successor and keeps the claimed bundle terminal. Acknowledgement publishes a + recovery-clean `turn.started` payload while the recovery metadata remains durable, then clears that + metadata after the event bus accepts the event. HTTP success requires both operations. A rejected + event publication therefore remains discoverable by startup reconciliation. Recovery atomically + replaces an accepted or ambiguous reservation with a durable start-event outbox marker. Before + admitting work, startup replays `turn.started`, followed by `turn.completed` when the turn is already + terminal, and clears the marker only after the event bus accepts every required event. Failed replay + fails startup and leaves the marker for the next attempt. Every public turn event, including a + completion racing live cleanup or emitted during recovery, strips the private prompt-dispatch fields. + If the predecessor's delayed ready event overlaps this private reservation, ready handling waits for the + reservation to resolve and then revalidates prompt generation before touching turn or workflow state. + It cannot complete the reserved successor or run predecessor completion actions against it. When the + reservation rolls back, a ready event whose predecessor generation still owns the session continues + through normal completion so the predecessor and its queue are not stranded. A generationless ready + event that overlapped the reservation is dropped after resolution because ownership cannot be proven. + If the attempt marker cannot be persisted, dispatch does not occur and a fresh bounded context rolls + back the reservation and session claim even when the transport context was cancelled, so the answer + remains retryable. + If agentctl synchronously rejects the prompt, the reservation is rolled back and the answer can be + restored. If agentctl accepts the prompt but publication or later transport handling fails, the + endpoint returns a server error, performs normal prompt-failure cleanup, and keeps the claimed bundle + terminal because retrying could dispatch the answer twice. Startup + deletes only an empty, unattempted unpublished reservation and restores the exact clarification rows + claimed for its dispatch; an attempt marker or message evidence instead proves dispatch ambiguity and + preserves the successor. Authority and recovery treat boolean `true`, strings `"true"` and `"1"`, + and numeric `1` as equivalent pending/attempted flags across SQLite and PostgreSQL. If reservation + reconciliation is unavailable or fails, orchestrator startup + fails before watcher, scheduler, or prompt admission starts; the next start retries recovery. A + production turn repository must provide this recovery capability through its compile-time contract. + A rejection persists terminal status without resuming the agent. +- Every response atomically claims current-turn ownership and persists a response-delivery recovery + intent before it can reach a live waiter or request a detached resume. A live waiter runs durable + delivery confirmation before returning the response to the agent; enqueue alone does not retire the + intent. Once confirmation starts, it owns its durable operation through completion even if the + responder's bounded wait expires. Its input claim remains immutable, its result remains local to the + callback, and any compensating restore serializes against finalization so only one durable outcome + wins. The detached path retires the intent only at its durable resume boundary. Startup first + reconciles prompt reservations, then restores an unhanded current-turn claim to pending; a terminal + session or newer authoritative turn instead retires the stale intent without reactivating history. + Terminal message updates are published only after delivery succeeds. If detached + resume acceptance fails, the endpoint returns an error and restores the still-current bundle to + pending so the same answer can be retried. Restored rows publish after commit even when synchronous + task-summary acknowledgement fails, preventing clients from retaining the terminal snapshot while the + endpoint still returns the acknowledgement error. A publication or summary-convergence error after + the database restore does not make that retry unsafe; durable pending state remains authoritative. + Once agentctl accepts the prompt, later publication or completion errors cannot roll back the + successor turn or reopen the answer. A primary-answer watchdog carries the clarification turn ID + and revalidates that ID both before fallback and inside serialized prompt admission, so it cannot + dispatch a stale answer into a successor turn. Its fallback keeps the watchdog cancellation context + through authority reads and prompt admission so session activity or service shutdown interrupts + in-flight recovery work. +- A current-turn bundle remains answerable while any sibling question is pending. Recovery claims only + those pending rows, preserves siblings already made terminal by an earlier partial write, and restores + only the claimed rows if detached delivery fails. Primary delivery events and detached recovery derive + one turn identity from the same bundle rule: legacy empty turn IDs do not mask a consistent non-empty + identity, while conflicting non-empty IDs invalidate the identity. +- Any response to a superseded or terminal bundle returns conflict, performs no message mutation, and + initiates no agent resume. Current clients close their obsolete local overlay through the existing + conflict handling. +- Persisted task status summaries reconcile `pending_action` against current-turn repository state on + source events and task-list/boot reads. Existing summaries are repaired, not only missing rows. +- When a task row advertises a pending action, desktop and phone task activation load the task's + sessions from the server and select the newest input-capable session whose `pending_action` matches + the task action. Before applying that response, activation revalidates the task-summary revision and + pending action. If either changes while the task remains present, activation discards the delayed + session choice and opens the task-only route; phone activation also closes the sheet. Overlapping + authoritative loads are generation-guarded per task, so an older response cannot replace a newer + session snapshot. This pending owner outranks remembered-session and primary-session preferences. If + the task still advertises pending input but no matching input-capable owner exists, activation releases + the outgoing session layout and fails closed to the task route without guessing a session. Normal + preference order returns only for a clean task. If the task projection disappears while that + authoritative load is in flight, desktop and phone leave the selection inert instead of navigating to + a deleted task, including tasks using the legacy pending-action projection. A forced load aborted by a + newer load is also inert; it is not treated as a request failure requiring task-only fallback. Each + mounted phone task sheet owns its selection generation, so simultaneous instances cannot invalidate + one another's in-flight task choice. + +## Requirements + + + +### REQ-TASKS-CLARIFICATION-LIFECYCLE-001: Active clarification lifecycle + + + +**Intent:** Clarification bundles remain answerable only for the session turn that owns them, while detached and recovery paths preserve the response contract. + + + +#### Acceptance criteria + + + +- **AC-TASKS-CLARIFICATION-LIFECYCLE-001.1:** When a clarification is pending, the system shall expose and process only the bundle owned by the session's current turn. +- **AC-TASKS-CLARIFICATION-LIFECYCLE-001.2:** When a newer turn or terminal session state supersedes a bundle, the system shall keep its transcript history without leaving it answerable or able to block workflow progress. +- **AC-TASKS-CLARIFICATION-LIFECYCLE-001.3:** When detached clarification recovery succeeds or fails, the system shall return the bounded response and durable recovery outcome defined by the lifecycle contract. + + + +## Out of scope + + + +Clarification presentation details belong to the UI system; agent identity and provider runtime behavior belong to the agent system. \ No newline at end of file diff --git a/docs/specs/tasks/documents.md b/docs/specs/tasks/requirements/documents.md similarity index 90% rename from docs/specs/tasks/documents.md rename to docs/specs/tasks/requirements/documents.md index ca91b18878..01182082b3 100644 --- a/docs/specs/tasks/documents.md +++ b/docs/specs/tasks/requirements/documents.md @@ -1,10 +1,27 @@ --- -status: shipped +status: active +system: tasks created: 2026-04-29 -owner: cfl +owners: + - cfl --- +# Task Documents Requirements -# Task Documents +## 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. + +## Requirements + +### REQ-TASKS-DOCUMENTS-001: Task Documents + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-DOCUMENTS-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -92,4 +109,4 @@ POST /tasks/:id/documents/:key/revisions/:revId/restore → restore from prior - Document templates (e.g. auto-populate spec template on creation) - Cross-task document linking (documents belong to one task) - Attachment revision history (attachments are replace-only, not versioned) -- Image/file preview rendering in the UI (download link only for now) +- Image/file preview rendering in the UI (download link only for now) \ No newline at end of file diff --git a/docs/specs/tasks/execution-stages.md b/docs/specs/tasks/requirements/execution-stages.md similarity index 91% rename from docs/specs/tasks/execution-stages.md rename to docs/specs/tasks/requirements/execution-stages.md index ff42f1683f..c059c7ae65 100644 --- a/docs/specs/tasks/execution-stages.md +++ b/docs/specs/tasks/requirements/execution-stages.md @@ -1,10 +1,27 @@ --- -status: shipped +status: active +system: tasks created: 2026-04-29 -owner: cfl +owners: + - cfl --- +# Task Execution Stages Requirements -# Task Execution Stages +## 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. + +## Requirements + +### REQ-TASKS-EXECUTION-STAGES-001: Task Execution Stages + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-EXECUTION-STAGES-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -97,4 +114,4 @@ Agents signal stage outcomes by updating the task status. The server applies exe - Sequential reviewers within a single stage (reviewers always run in parallel) - Custom stage types beyond work/review/approval/ship - Automatic retry limits on the rejection loop (loops until approved or human intervenes) -- Stage-level timeouts or SLAs +- Stage-level timeouts or SLAs \ No newline at end of file diff --git a/docs/specs/tasks/requirements/external-id-idempotency-boundaries.md b/docs/specs/tasks/requirements/external-id-idempotency-boundaries.md new file mode 100644 index 0000000000..cedd1c8147 --- /dev/null +++ b/docs/specs/tasks/requirements/external-id-idempotency-boundaries.md @@ -0,0 +1,82 @@ +--- +status: draft +system: tasks +created: 2026-08-07 +owners: + - nova28 +--- + + +# External task ID idempotency boundaries Requirements + + + +## Overview + + + +The external identity contract has explicit exclusions and records unresolved questions without silently expanding its guarantee. + + + +## Out of scope + +### Deferred write surfaces (designed, not built) + +Neither has a named consumer today, and they are **not equally cheap** to +enable. Each is additive. + +| Surface | Cost to enable later | +|---|---| +| WebSocket `task.create` | **Low.** One request-struct field, one line in the service-call literal, and skip the launch and last-used recording on a found outcome. One file. Deferred because it is the UI's own path: nothing retries on the UI's behalf. | +| Plugin host | **Meaningfully higher.** `plugin.pb.go` and `plugin_grpc.pb.go` are generated and committed, so a proto edit requires regenerating them through the Makefile proto target (installs `protoc-gen-go`, needs network). It also needs a new SDK method — **do not change the existing `TaskReader.Create` signature**, which is source-breaking for shipped plugins — a mapper change, and a release in each separate plugin repo. | + +### Other non-goals + +- **Detecting whether an unsettled create is still alive.** No lease, heartbeat, + or owner token. This is the deliberate boundary of the feature. +- **Repairing, resuming, adopting, or garbage-collecting unsettled tasks.** +- **Any timeout or expiry on an unsettled task.** +- **A tombstone for deleted identities.** See *Idempotency is scoped to the + task's lifetime*. +- **An MCP lookup tool.** MCP gets an idempotent create-if-absent, not a probe; + no in-scope MCP flow needs to ask without being willing to create. Adding a + read-only tool later is small and additive. +- **Changing an external ID in place.** Write-once; re-key via release + create. +- **System-generated external IDs.** +- **Idempotency for anything other than task creation.** `spawn_session_kandev` + is not covered. +- **Restructuring the create handlers** so identity resolution precedes + handler-level validation. The consequence — payload and server-state drift can + fail a retry before dedupe — is documented, not engineered around. +- **Retiring the existing integration dedupe tables.** +- **Replacing the office `runs.idempotency_key` mechanism.** +- **The office runtime `create_task` HTTP action.** +- **Request-payload fingerprinting.** +- **Cross-workspace uniqueness or a global namespace.** +- **A UI surface** for entering, displaying, or releasing external IDs. +- **Bulk lookup or bulk release.** +- **Backfilling external IDs onto existing tasks.** + +## Open questions + +None. Every decision is settled; downstream steps implement as written and route +back here if a change is needed. + +## Requirements + + + +### REQ-TASKS-EXTERNAL-ID-BOUNDARIES-001: External task ID idempotency boundaries + + + +**Intent:** The external identity contract has explicit exclusions and records unresolved questions without silently expanding its guarantee. + + + +#### Acceptance criteria + + + +- **AC-TASKS-EXTERNAL-ID-BOUNDARIES-001.1:** When a caller relies on creation idempotency, the system shall apply only the guarantees listed in scope and shall not infer crash repair or liveness. diff --git a/docs/specs/tasks/requirements/external-id-idempotency-scenarios.md b/docs/specs/tasks/requirements/external-id-idempotency-scenarios.md new file mode 100644 index 0000000000..ccb233d491 --- /dev/null +++ b/docs/specs/tasks/requirements/external-id-idempotency-scenarios.md @@ -0,0 +1,310 @@ +--- +status: draft +system: tasks +created: 2026-08-07 +owners: + - nova28 +--- + + +# External task ID idempotency scenarios Requirements + + + +## Overview + + + +Creation idempotency is defined by observable outcomes for first creates, retries, concurrent callers, and identity settlement races. + + + +## Scenarios + +### Golden path + +- **GIVEN** workspace `W` has no task holding `ext-1`, **WHEN** a client POSTs + `/api/v1/tasks` with `workspace_id: W` and `external_id: ext-1`, **THEN** a + task is created, the response carries `external_id: "ext-1"`, + `deduplicated: false`, `creation_complete: true`, a `task.created` event is + published, and the row has a non-NULL `external_id_settled_at`. +- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** a client POSTs the + same create again, **THEN** the response is `200` with `id` equal to `T.id`, + `deduplicated: true`, `creation_complete: true`, `W` still has exactly one + task holding `ext-1`, and no `task.created` event is published. +- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** a client GETs the + lookup route, **THEN** the response is `200` with `T`'s DTO and no task is + created. +- **GIVEN** no task holds `(W, ext-nope)`, **WHEN** a client GETs the lookup, + **THEN** the response is `404`. + +### Lookup-before-write ordering + +- **GIVEN** an office workspace `W` whose `task_sequence` is `N`, and a settled + task holding `(W, ext-1)`, **WHEN** a client POSTs a create with + `external_id: ext-1` and office-task parameters, **THEN** the existing task is + returned and `W.task_sequence` is **still `N`** — the step-3 lookup resolved + it before any allocation. +- **GIVEN** an office workspace `W` whose `task_sequence` is `N` and **no** task + holding `(W, ext-4)`, **WHEN** two office creates with `external_id: ext-4` + race such that both miss the step-3 lookup, **THEN** exactly one task is + created, the loser returns a Found outcome, and `W.task_sequence` may be + `N+2` — the loser's allocated identifier is not reclaimed. This is the single + permitted side effect of a Found outcome, and the sequence is not required to + be contiguous. +- **GIVEN** a workflow step at its WIP limit, and a settled task holding + `(W, ext-1)` on that step, **WHEN** a client POSTs a create with + `external_id: ext-1` targeting that step, **THEN** the existing task is + returned with `deduplicated: true` — **not** a WIP-capacity error. The step-3 + lookup resolves it before admission is consulted. +- **GIVEN** a workflow step with **exactly one remaining WIP slot** and **no** + task yet holding `(W, ext-2)`, **WHEN** two creates with `external_id: ext-2` + targeting that step race such that both miss the step-3 lookup, the winner + consumes the last slot and inserts, and the loser is then rejected by WIP + admission **before** reaching the insert, **THEN** the loser re-reads by + `(workspace_id, external_id)`, finds the winner's task, and returns it as a + Found outcome — **not** the capacity error. This is the admission-preemption + guard; without it the contract's "a retry always returns the existing task" + promise fails exactly when the step has just saturated. + - The one-remaining-slot precondition is load-bearing: if the step were + already full before either request, neither could insert, so there would be + no winner for the loser to find and the scenario would be unsatisfiable. +- **GIVEN** the same saturated step and **no** task holding `(W, ext-3)`, + **WHEN** a single create with `external_id: ext-3` is rejected by WIP + admission and the re-read still finds nothing, **THEN** the original capacity + error surfaces unchanged — the guard must not swallow genuine failures. +- **GIVEN** a settled task holding `(W, ext-1)`, **WHEN** a client POSTs a create + with `external_id: ext-1` and a `repositories` entry naming a repository that + no longer exists, **THEN** the outcome depends on where that validation lives: + if the handler rejects it pre-service the request fails (documented + limitation); if it reaches the service, the existing task is returned. The + implementation MUST state which, and a test MUST pin the actual behavior. + +### Unsettled outcome and the unsafe-recovery guard + +- **GIVEN** a task `T` holding `(W, ext-1)` with `external_id_settled_at IS NULL`, + **WHEN** a client POSTs a create with `external_id: ext-1`, **THEN** the + response is `200` with `T.id`, `deduplicated: true`, and + `creation_complete: false`; `T` is unmodified; no new task is created. +- **GIVEN** the same unsettled task `T`, **WHEN** a client POSTs that create ten + more times over any span of time, **THEN** every response is identical and `T` + is never deleted, repaired, or duplicated — there is no timeout after which + behavior changes. +- **GIVEN** the same unsettled task `T`, **WHEN** a client GETs the lookup for + `ext-1`, **THEN** the response is `200` with `creation_complete: false`. +- **GIVEN** a create for `ext-1` still running its required synchronous work, + **WHEN** a second caller releases `ext-1` and creates again, **THEN** two tasks + exist for `ext-1`'s entity. This is the documented unsafe path; the test + exists to pin the consequence, and the MCP tool description and REST docs MUST + warn against automating it. +- **GIVEN** an unsettled task `T` holding `(W, ext-1)`, **WHEN** an operator + DELETEs the release route and then a client POSTs the create again, **THEN** + the release returns `204`, `T` still exists with NULL `external_id`, and the + create produces a **new** task with `deduplicated: false`. + +### Settlement predicate and zero-row handling + +- **GIVEN** a create for `ext-1` that has committed its task row and finished its + required synchronous work, **WHEN** settlement runs, **THEN** exactly one row + is updated and asynchronous dispatch proceeds. +- **GIVEN** a create for `ext-1` whose identity is **released** by another actor + after the row commit but before settlement, **WHEN** settlement runs, **THEN** + it affects **zero** rows — the `external_id` predicate no longer matches — no + asynchronous work is dispatched, and the response is `200` with + `deduplicated: false`, `creation_complete: true`, and **no** `external_id` + field, i.e. outcome `CreatedIdentityLost`. +- **GIVEN** the same released-before-settlement race, **WHEN** the caller + inspects the response, **THEN** it distinguishes this from `Created` by the + absent `external_id`, and from `Found, unsettled` by `deduplicated: false`; + **AND** `creation_complete` is `true`, because this create's synchronous work + did finish. +- **GIVEN** every success outcome across the whole contract, **WHEN** their + response tuples are enumerated, **THEN** `creation_complete: false` appears in + exactly one — `Found, unsettled` — and always alongside `deduplicated: true`. +- **GIVEN** a create for `ext-1` whose **task is deleted** by another actor + before settlement, **WHEN** settlement runs, **THEN** it affects zero rows, no + asynchronous work is dispatched, and the response is the surface's existing + not-found error rather than a fabricated success. +- **GIVEN** any settlement that affects zero rows, **WHEN** the handler + continues, **THEN** no agent is launched and no PR association is started. + +### No side effects on a found outcome + +- **GIVEN** a settled task `T` holding `(W, ext-1)` with exactly one agent + session, **WHEN** a client POSTs a create with `external_id: ext-1` and + `start_agent: true`, **THEN** `T` still has exactly one session and no new + agent execution is started. +- **GIVEN** an **unsettled** task `T` holding `(W, ext-1)` with no session, + **WHEN** a client POSTs a create with `external_id: ext-1` and + `start_agent: true`, **THEN** `T` still has no session and no agent is + launched — an unsettled task is reported, never resumed. +- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** a client POSTs a + create with `external_id: ext-1`, `start_agent: true`, one attachment, and a + `repositories` entry naming a different repository with `fresh_branch: true` + (the flag is per-repository, not top-level), **THEN** `T` still has exactly its + original repositories, no new worktree or branch exists, the attachment is not + claimed, and no last-used task-create settings are recorded. +- **GIVEN** a settled task `T` holding `(W, ext-1)` and title `"Original"`, + **WHEN** a client POSTs a create with `external_id: ext-1` and title + `"Changed"`, **THEN** `T`'s stored title is still `"Original"`. + +### MCP + +- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** an agent calls + `create_task_kandev` with `external_id: ext-1` resolving to `W` and + `start_agent: true`, **THEN** the tool result JSON carries `T.id`, + `deduplicated: true`, `creation_complete: true`, and no new task or session is + created. +- **GIVEN** an **unsettled** task `T` holding `(W, ext-1)`, **WHEN** an agent + calls `create_task_kandev` with `external_id: ext-1`, **THEN** the result + carries `T.id` with `creation_complete: false` and no task is created or + started. +- **GIVEN** a settled task `T` holding `(W, ext-1)` whose repository list differs + from the retry payload, **WHEN** an agent calls `create_task_kandev` with + `external_id: ext-1` and a `repository_url` that would resolve to a remote + contribution, **THEN** `T` is returned, no remote contribution is associated, + **and `T` still exists**. This is the data-loss guard; required coverage. +- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** an agent calls + `create_task_kandev` with `external_id: ext-1` and a `workspace_mode` needing + a workspace-policy attach, **THEN** no policy is attached and `T` still exists. +- **GIVEN** a settled task holding `(W, ext-1)` and `W` is the only workspace, + **WHEN** an agent calls `create_task_kandev` with `external_id: ext-1` and no + `workspace_id`, **THEN** that task is returned with `deduplicated: true`. +- **GIVEN** a settled task holding `(W, ext-1)`, **WHEN** an agent calls + `create_task_kandev` with `parent_id` naming a task in a different workspace + `W2` and `external_id: ext-1`, **THEN** a new subtask is created in `W2` + holding `ext-1`, because the effective workspace is `W2`. +- **GIVEN** an agent whose session belongs to a task in workspace `W`, and a task + holding `(W2, ext-1)` in a workspace the agent's task does not belong to, + **WHEN** the agent calls `create_task_kandev` targeting `W2` with + `external_id: ext-1`, **THEN** the call is denied by MCP identity scoping and + reveals nothing about `W2`. + +### Uniqueness and concurrency + +- **GIVEN** a task holding `(W1, ext-1)` and nothing in `W2`, **WHEN** a client + POSTs a create for `workspace_id: W2` with `external_id: ext-1`, **THEN** a new + task is created in `W2` with `deduplicated: false`. +- **GIVEN** two tasks in `W` with no external IDs, **WHEN** a third is created + without one, **THEN** the create succeeds — absent identities never collide. +- **GIVEN** a task `P` in `W` holding `ext-parent`, **WHEN** a client creates a + subtask of `P` with no `external_id`, **THEN** the subtask holds no identity. +- **GIVEN** no task holds `(W, ext-race)`, **WHEN** two creates with + `external_id: ext-race` are issued concurrently and both pass the step-3 + lookup, **THEN** exactly one task in `W` holds `ext-race`, both requests return + `200`, the outcomes are (`deduplicated: false`, `deduplicated: true`), and + **no orphan task row exists from the loser**. +- **GIVEN** the same race against **PostgreSQL**, **THEN** the same assertions + hold — an environment-gated PostgreSQL test, since the SQLite path passing is + not evidence for it. +- **GIVEN** a create whose task-row insert collides on the `tasks` primary key + rather than on `uniq_tasks_external_id`, **THEN** the response is an error and + **not** a Found outcome. + +### Validation + +- **GIVEN** any workspace, **WHEN** a client POSTs a create with an + `external_id` whose trimmed UTF-8 length is 256 bytes, **THEN** the response + is `400`, no task is created, and no agent is launched. +- **GIVEN** any workspace, **WHEN** a client POSTs a create with `external_id` + `"ext-1\n"`, **THEN** the response is `400` — the control character is + rejected before trimming. +- **GIVEN** any workspace, **WHEN** a client POSTs a create with `external_id` + `"\t"`, **THEN** the response is `400`, not "absent". +- **GIVEN** any workspace, **WHEN** a client POSTs a create with `external_id` + `" "`, **THEN** the task is created with a NULL `external_id`, and the + response omits `external_id`. +- **GIVEN** a task holding `(W, ext-1)`, **WHEN** a client POSTs a create with + `external_id: " ext-1 "`, **THEN** that task is returned with + `deduplicated: true` — trimming precedes the lookup. +- **GIVEN** a task holding `(W, ext-1)`, **WHEN** a client POSTs a create with + `external_id: "EXT-1"`, **THEN** a new task is created — case-sensitive. +- **GIVEN** a PostgreSQL install whose database collation is case-insensitive, + **WHEN** the previous scenario runs, **THEN** it still produces two distinct + tasks — the column's deterministic collation overrides the database default. + +### Lifecycle + +- **GIVEN** a settled task `T` holding `(W, ext-1)` that has been archived, + **WHEN** a client POSTs a create with `external_id: ext-1`, **THEN** `T` is + returned with `deduplicated: true` and a non-null `archived_at`, and `T` + remains archived. +- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** `T` is deleted and + a client POSTs the create again, **THEN** a new task is created with + `deduplicated: false` — idempotency is scoped to the task's lifetime. +- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** `T` is deleted by + an office handoff cascade that calls the repository directly, **THEN** the + identity is free and a subsequent create succeeds — no code path can leave a + stale identity behind. +- **GIVEN** a settled task `T` holding `(W, ext-1)`, **WHEN** a client DELETEs + the release route, **THEN** the response is `204`, `T` still exists with NULL + `external_id` and NULL `external_id_settled_at`, and the lookup returns `404`. +- **GIVEN** a task `T` holding `ext-1`, **WHEN** a client PATCHes + `/api/v1/tasks/T` with an `external_id` field in the body, **THEN** `T`'s + identity is unchanged. + +### Permissions + +- **GIVEN** auth is enabled, a settled task holding `(W, ext-1)` owned by user + `A`, and user `B` who does not own `W`, **WHEN** `B` POSTs a create for + `workspace_id: W` with `external_id: ext-1`, **THEN** `B` receives + `404 {"error": "task not created"}`, the response carries no `deduplicated`, + no `creation_complete`, and no field of `A`'s task, and no task is created. +- **GIVEN** the same setup but an **unsettled** task, **WHEN** `B` POSTs the + same create, **THEN** `B` receives `404` — settledness is not observable + across an authorization boundary. +- **GIVEN** the same setup, **WHEN** `B` GETs or DELETEs the by-external-id route + for `W`, **THEN** the response is `404`. +- **GIVEN** user `B` unauthorized for `W`, **WHEN** `B` POSTs a create for `W` + with a 300-byte `external_id`, **THEN** the response is `404`, not `400` — + authorization precedes validation. + +### Read surfaces and the deferred write surfaces + +- **GIVEN** a task holding `ext-1`, **WHEN** it is returned by a REST read, a + REST list, an MCP task tool, or a WS task lifecycle event, **THEN** each of + those four carries `external_id: "ext-1"`. +- **GIVEN** a task holding no identity, **WHEN** any of those four returns it, + **THEN** the representation omits `external_id` rather than sending `null` or + `""`. +- **GIVEN** a task holding `ext-1`, **WHEN** it is returned by an ordinary task + read, **THEN** the representation does **not** carry `creation_complete`. +- **GIVEN** a task holding `(W, ext-1)`, **WHEN** a client sends the + `task.create` WS action with an `external_id` field, **THEN** a new task is + created and the field is ignored. +- **GIVEN** a task holding `(W, ext-1)`, **WHEN** a plugin calls + `Tasks().Create` with `StartAgent: true`, **THEN** a new task is created and + started normally. + +### Migration + +- **GIVEN** an existing database whose tasks predate this feature, **WHEN** the + backend starts, **THEN** both columns exist and are NULL for every + pre-existing task, `uniq_tasks_external_id` exists, and creating tasks without + an external ID continues to succeed. +- **GIVEN** the same database, **WHEN** the backend starts a second time, + **THEN** the migration replays without error. + +## Requirements + + + +### REQ-TASKS-EXTERNAL-ID-SCENARIOS-001: External task ID idempotency scenarios + + + +**Intent:** Creation idempotency is defined by observable outcomes for first creates, retries, concurrent callers, and identity settlement races. + + + +#### Acceptance criteria + + + +- **AC-TASKS-EXTERNAL-ID-SCENARIOS-001.1:** When a creation scenario occurs, the system shall return the outcome, task identity, and side-effect behavior described by that scenario. + + + +## Out of scope + +- Boundary exclusions are recorded in [External task ID idempotency boundaries](external-id-idempotency-boundaries.md). diff --git a/docs/specs/tasks/requirements/external-id-idempotency.md b/docs/specs/tasks/requirements/external-id-idempotency.md new file mode 100644 index 0000000000..9c7cabf378 --- /dev/null +++ b/docs/specs/tasks/requirements/external-id-idempotency.md @@ -0,0 +1,183 @@ +--- +status: draft +system: tasks +created: 2026-08-07 +owners: + - nova28 +--- + + +# External task ID idempotency Requirements + + + +## Overview + + + +A caller can retry task creation with an external identity and receive the one task that owns that identity without creating duplicate work. + + + +## Why + +An external system that creates Kandev tasks over the API has no way to ask +"did I already create this one?". Every create is unconditionally a new task, so +a webhook redelivery, a network timeout the caller retried, or a crash between +"task created" and "I recorded the task ID" produces a duplicate task — with a +duplicate worktree and, when `start_agent` is true, a duplicate agent burning +tokens on work already in flight. Callers today compensate with fragile +heuristics (scanning task titles for a prefix, treating an existing branch name +as a witness that the task exists), which break the moment a title is edited or +a branch is renamed. + +## What this feature is, and is not + +**It is durable duplicate suppression.** A create carrying an external ID never +produces a second task for that identity, and always returns the task that holds +it — including after the caller crashed before recording the task ID. That is +the unblock: the caller gets its task ID back and stops guessing. + +**It is not automated crash repair.** Kandev does not detect, adopt, resume, or +clean up a partially-created task, and callers MUST NOT automate doing so +either. The reason is not implementation cost — it is that the information does +not exist. A task whose creation has not finished is indistinguishable from one +whose creation is *still running*: both are simply "not settled yet". Without a +liveness signal — a lease, a heartbeat, an owner token — no observer can tell a +crashed create from a slow one. + +An earlier draft claimed a retry would never observe a half-created task, which +required exactly that machinery: fencing tokens, compare-and-swap on every +transition, a two-phase coordinator, and isolation of unsettled rows. It was +reviewed and rejected as unbuildable. This spec does not reintroduce it, and +therefore does not make the guarantee it would have bought. + +What callers get instead is an honest flag. + +**`creation_complete` has exactly one meaning, in every outcome and on every +surface: the creation of the returned task completed its required synchronous +work.** Nothing more. It says nothing about whether an agent is running, and +nothing about whether any process is currently alive. + +The diagnostic case is the specific tuple **`deduplicated: true` + +`creation_complete: false`** — another create claimed this identity and had not +finished when we looked; it may still be in progress. That combination is +diagnostic, not actionable. The safe response is to proceed with the returned +task ID, or to escalate to a human — never to release the identity and create +again. See *The one unsafe thing a caller can do*. + +No other tuple carries the "may still be running" caveat. + +## What + +- A task MAY carry an **external ID**: a caller-supplied string that identifies + the entity in the caller's own system (a Jira key, a webhook delivery ID, a + UUID the caller minted before its first attempt). +- An external ID SHALL be held by at most one task per workspace. +- A create carrying an external ID SHALL have exactly one of four outcomes, and + the response SHALL make which one unambiguous: + 1. **Created** — no task held that identity; a new task was created and holds + it. + 2. **Found, settled** — a task already holds it and its creation finished. + 3. **Found, unsettled** — a task already holds it and its creation had not + finished at observation time. It may still be running. + 4. **Created, identity lost** — this request created the task and finished its + work, but another actor released the identity in the interim, so the task + survives holding no external ID. Rare; see *Settlement*. +- Both **Found** outcomes SHALL have **no side effects**: no new task row, no + new agent session, no agent launch, no repository attachment, no attachment + claim, no workspace-policy write, no branch creation, and no `task.created` + event. + - **One bounded exception, on any concurrent-loser path.** When a Found + outcome is resolved by the step-3 lookup — which is every ordinary retry — + nothing whatsoever is consumed. When it is instead resolved *after* a + step-3 miss, the loser may already have allocated an office task-identifier + sequence number, and that number is not returned to the pool. This applies + to **both** late-resolution paths, because identifier allocation precedes + both of them: the unique-index backstop, and the pre-insert re-read that + catches an admission or capacity failure. This is the single permitted side + effect; it leaves a gap in the office identifier sequence, which is not + required to be contiguous, and no other durable change. +- The system SHALL NOT delete, repair, resume, or reclaim an unsettled task, and + SHALL NOT expire one. There is no duration after which behavior changes. +- **REST** callers SHALL have a side-effect-free way to ask what holds an + identity without risking a create: the lookup route. **MCP callers do not get + one in this iteration.** What MCP gets is an idempotent *create-if-absent* + operation — safe to repeat, but it creates a task when no holder exists, so it + is not a probe. This asymmetry is deliberate and is stated rather than papered + over; see *The probe, and what MCP has instead*. +- Callers SHALL be able to release an external ID from a task, freeing it for + reuse without deleting the task. This is an operator action, not a recovery + step — see *The one unsafe thing a caller can do*. +- Omitting the external ID SHALL leave task creation behaving exactly as it does + today. +- The external ID SHALL be accepted on the **REST create endpoint** and the + **`create_task_kandev` MCP tool**. The WebSocket `task.create` action and the + plugin host task-create API do not accept it in this iteration. +- The external ID SHALL appear on exactly the task representations listed in + *Task representations*. That table is the complete requirement; there is no + broader "everywhere tasks are read" obligation. +- The external ID identifies the **external entity**, not the request body. A + second create for a held external ID SHALL return the existing task unchanged + even when the rest of the payload differs; it SHALL NOT patch the existing + task. +- An external ID SHALL NOT be inherited by subtasks, copied on any task-cloning + path, or auto-generated by the system. + +## The one unsafe thing a caller can do + +**Do not automatically release an identity because a create reported +`creation_complete: false`, and then create again.** This is called out +explicitly because it is the intuitive "recovery" move and it is unsafe. + +The failure: + +1. Create A commits its task row for identity `E` and continues its remaining + work normally. +2. Retry B observes `E` held by an unsettled task and returns + `creation_complete: false`. +3. B releases `E` and creates again, producing task T2. +4. A finishes. Two tasks now exist for one external entity, and if both + requested an agent, two agents are running. + +The unique index prevents two *simultaneous holders*; it cannot prevent +duplicates across a release. Releasing an identity that another create is +actively using re-opens exactly the duplicate this feature exists to prevent. + +**Safe responses to `creation_complete: false`:** + +- Proceed with the returned task ID. It is a real task. +- Poll the lookup; if it settles, the original create finished on its own. +- Escalate to a human, who can inspect the task and decide. + +**Release is for a human or operator who has determined the task is abandoned** +— not for an automated retry loop. Client libraries and agent tooling SHOULD NOT +expose release as an automatic recovery action. + +## Requirements + + + +### REQ-TASKS-EXTERNAL-ID-001: External task ID idempotency + + + +**Intent:** A caller can retry task creation with an external identity and receive the one task that owns that identity without creating duplicate work. + + + +#### Acceptance criteria + + + +- **AC-TASKS-EXTERNAL-ID-001.1:** When a create request carries an external ID, the system shall return the single task associated with that identity and shall not create a duplicate task. +- **AC-TASKS-EXTERNAL-ID-001.2:** When the returned task's creation is unsettled, the response shall identify that diagnostic state without authorizing identity release or duplicate creation. +- **AC-TASKS-EXTERNAL-ID-001.3:** When a request finds an existing identity, the system shall preserve the no-side-effect contract defined by the requirement. + + + +## Out of scope + + + +Crash repair, adoption, resume, cleanup, and caller-side identity release are excluded from this capability. \ No newline at end of file diff --git a/docs/specs/tasks/interrupted-task-indicator.md b/docs/specs/tasks/requirements/interrupted-task-indicator.md similarity index 88% rename from docs/specs/tasks/interrupted-task-indicator.md rename to docs/specs/tasks/requirements/interrupted-task-indicator.md index 1bfe1cd509..47bafbdae8 100644 --- a/docs/specs/tasks/interrupted-task-indicator.md +++ b/docs/specs/tasks/requirements/interrupted-task-indicator.md @@ -1,11 +1,28 @@ --- -status: complete -updated: 2026-08-09 +status: active +system: tasks created: 2026-08-02 -owner: kandev +updated: 2026-08-09 +owners: + - kandev --- +# Interrupted Task Indicator Requirements -# Interrupted Task Indicator +## 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. + +## Requirements + +### REQ-TASKS-INTERRUPTED-TASK-INDICATOR-001: Interrupted Task Indicator + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-INTERRUPTED-TASK-INDICATOR-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -167,9 +184,9 @@ no effect beyond the icon. ## Related -- [Task Runtime Cleanup](runtime-cleanup.md) — startup reconciliation semantics +- [Task Runtime Cleanup](../system-design/runtime-cleanup.md) — startup reconciliation semantics the marker hooks into. -- [Sidebar Task Completion Icons](../ui/sidebar-task-completion-icons.md) — +- [Sidebar Task Completion Icons](../../ui/sidebar-task-completion-icons.md) — the shared task-row icon precedence this feature extends. -- [Backend Runtime-State Ownership](../../plans/backend-runtime-state-ownership/plan.md) — - startup publication that clears stale generating activity. +- [Backend Runtime-State Ownership](../../../plans/backend-runtime-state-ownership/plan.md) — + startup publication that clears stale generating activity. \ No newline at end of file diff --git a/docs/specs/tasks/labels.md b/docs/specs/tasks/requirements/labels.md similarity index 81% rename from docs/specs/tasks/labels.md rename to docs/specs/tasks/requirements/labels.md index 157ac2b59e..78e9ede078 100644 --- a/docs/specs/tasks/labels.md +++ b/docs/specs/tasks/requirements/labels.md @@ -1,10 +1,27 @@ --- -status: shipped +status: active +system: tasks created: 2026-04-29 -owner: cfl +owners: + - cfl --- +# Task Labels Requirements -# Task Labels +## 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. + +## Requirements + +### REQ-TASKS-LABELS-001: Task Labels + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-LABELS-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -53,4 +70,4 @@ Tasks have a `labels` field stored as an inline JSON array (`["bug","urgent"]`). - Label groups or categories (e.g. "priority: high" vs "type: bug") - Label descriptions - Per-project label scoping (all labels are workspace-wide) -- Label-based automation triggers (e.g. "when label X added, assign to agent Y") +- Label-based automation triggers (e.g. "when label X added, assign to agent Y") \ No newline at end of file diff --git a/docs/specs/tasks/link-existing-task-github-issue.md b/docs/specs/tasks/requirements/link-existing-task-github-issue.md similarity index 93% rename from docs/specs/tasks/link-existing-task-github-issue.md rename to docs/specs/tasks/requirements/link-existing-task-github-issue.md index 6dc4d74dea..94128dfd31 100644 --- a/docs/specs/tasks/link-existing-task-github-issue.md +++ b/docs/specs/tasks/requirements/link-existing-task-github-issue.md @@ -1,10 +1,27 @@ --- -status: building +status: active +system: tasks created: 2026-06-23 -owner: product +owners: + - product --- +# Link Existing Task to External References Requirements -# Link Existing Task to External References +## 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. + +## Requirements + +### REQ-TASKS-LINK-EXISTING-TASK-GITHUB-ISSUE-001: Link Existing Task to External References + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-LINK-EXISTING-TASK-GITHUB-ISSUE-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -201,4 +218,4 @@ THEN Kandev renames the task to `ENG-20: Fix login` instead of stacking prefixes - Desktop pointer and coarse-pointer users can unlink one PR when at least two are associated, and the remaining association still renders after reload. - Jira, Linear, and Sentry actions are hidden when the corresponding integration is disabled or unauthenticated. -- GitHub issue links are visible only in their task workspace, including after page reload. +- GitHub issue links are visible only in their task workspace, including after page reload. \ No newline at end of file diff --git a/docs/specs/tasks/mcp-task-agent-profile-default/spec.md b/docs/specs/tasks/requirements/mcp-task-agent-profile-default.md similarity index 92% rename from docs/specs/tasks/mcp-task-agent-profile-default/spec.md rename to docs/specs/tasks/requirements/mcp-task-agent-profile-default.md index 0285434e20..ac9e228d35 100644 --- a/docs/specs/tasks/mcp-task-agent-profile-default/spec.md +++ b/docs/specs/tasks/requirements/mcp-task-agent-profile-default.md @@ -1,10 +1,27 @@ --- -status: shipped +status: active +system: tasks created: 2026-07-19 -owner: kandev +owners: + - kandev --- +# MCP-Created Task Agent Profile Default Requirements -# MCP-Created Task Agent Profile Default +## 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. + +## Requirements + +### REQ-TASKS-MCP-TASK-AGENT-PROFILE-DEFAULT-001: MCP-Created Task Agent Profile Default + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-MCP-TASK-AGENT-PROFILE-DEFAULT-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -178,4 +195,4 @@ Later sessions do not copy the initial-session value. ## Implementation plan -See [`../../../plans/creator-session-task-inheritance/plan.md`](../../../plans/creator-session-task-inheritance/plan.md). +See [`../../../plans/creator-session-task-inheritance/plan.md`](../../../plans/creator-session-task-inheritance/plan.md). \ No newline at end of file diff --git a/docs/specs/tasks/requirements/model-unification.md b/docs/specs/tasks/requirements/model-unification.md new file mode 100644 index 0000000000..20fb44238b --- /dev/null +++ b/docs/specs/tasks/requirements/model-unification.md @@ -0,0 +1,29 @@ +--- +status: draft +system: tasks +created: 2026-05-05 +owners: + - cfl +--- +# Task model unification Requirements + +## Overview + +Kanban and Office execution use one workflow-driven task model. Workflow style changes default +presentation and configured triggers, not the task lifecycle owner. + +## Requirements + +### REQ-TASKS-MODEL-UNIFICATION-001: Task model unification + +**Intent:** Unify task execution around workflow steps while keeping existing Kanban behavior +unchanged and making event-driven, multi-agent workflows observable through the same task surfaces. + +#### Acceptance criteria + +- **AC-TASKS-MODEL-UNIFICATION-001.1:** When a task exists, the system shall associate it with a workflow and workflow step, and shall use the workflow's style only as a presentation hint while preserving the existing Kanban Default lifecycle. +- **AC-TASKS-MODEL-UNIFICATION-001.2:** When a step configures comment, blocker, child-completion, approval, heartbeat, budget, or exhausted-agent-error triggers, the system shall dispatch the corresponding trigger through the workflow engine with its event context; steps without those triggers shall retain current behavior. +- **AC-TASKS-MODEL-UNIFICATION-001.3:** When a workflow step has participants, the system shall support role-targeted runs, participant fan-out, decision-required participants, and guarded quorum transitions; re-entering a decision step shall begin a fresh decision round. +- **AC-TASKS-MODEL-UNIFICATION-001.4:** When the workflow engine emits an automated run, the system shall place it in the shared runs queue with coalescing, idempotency, per-agent serialization, cooldown, and one-agent-per-task checkout guarantees; an explicit user start shall remain direct. +- **AC-TASKS-MODEL-UNIFICATION-001.5:** When a run exhausts its retry policy, the system shall fire the error trigger on the failing task with failure context and preserve an inbox signal when no coordination agent can be queued. +- **AC-TASKS-MODEL-UNIFICATION-001.6:** When users view or delegate tasks across workflow styles, the system shall render shared task content, preserve per-workflow Kanban swimlanes, and create child tasks with the selected workflow without losing parent identity or comments. diff --git a/docs/specs/tasks/multi-branch/spec.md b/docs/specs/tasks/requirements/multi-branch.md similarity index 95% rename from docs/specs/tasks/multi-branch/spec.md rename to docs/specs/tasks/requirements/multi-branch.md index 816333a522..d6b258b792 100644 --- a/docs/specs/tasks/multi-branch/spec.md +++ b/docs/specs/tasks/requirements/multi-branch.md @@ -1,9 +1,31 @@ -# Multi-branch tasks +--- +status: draft +system: tasks +owners: + - kandev +--- +# Multi-branch tasks 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. + +## Requirements + +### REQ-TASKS-MULTI-BRANCH-001: Multi-branch tasks + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-MULTI-BRANCH-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail **Status:** shipped **Owner:** Kandev backend **Date:** 2026-06-01 -**Related:** [ADR 0013](../../decisions/0013-multi-branch-tasks.md) +**Related:** [ADR 0013](../../../decisions/0013-multi-branch-tasks.md) ## What diff --git a/docs/specs/tasks/parent-child-message-interrupt.md b/docs/specs/tasks/requirements/parent-child-message-interrupt.md similarity index 94% rename from docs/specs/tasks/parent-child-message-interrupt.md rename to docs/specs/tasks/requirements/parent-child-message-interrupt.md index b113fab8d7..48719a3d0b 100644 --- a/docs/specs/tasks/parent-child-message-interrupt.md +++ b/docs/specs/tasks/requirements/parent-child-message-interrupt.md @@ -1,10 +1,27 @@ --- -status: shipped +status: active +system: tasks created: 2026-07-13 -owner: cfl +owners: + - cfl --- +# Parent-Child Message Interrupt Requirements -# Parent-Child Message Interrupt +## 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. + +## Requirements + +### REQ-TASKS-PARENT-CHILD-MESSAGE-INTERRUPT-001: Parent-Child Message Interrupt + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-PARENT-CHILD-MESSAGE-INTERRUPT-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -91,7 +108,7 @@ failure is still logged server-side. ### Composition with the FIFO queue and turn-safety guarantees -Decision: [ADR-0035](../../decisions/0035-version-agent-ready-events-by-prompt-generation.md) +Decision: [ADR-0035](../../../decisions/0035-version-agent-ready-events-by-prompt-generation.md) `delivery_mode="interrupt"` queues the message and cancels-and-takes it in one atomic orchestrator call, not two separate steps: queuing followed by a separate interrupt call @@ -243,4 +260,4 @@ itself via the ordinary FIFO drain — the message is not left stranded. preferred control when a direct parent has replacement instructions. A parent that instead needs to halt child work without sending a replacement prompt uses the separate [Parent-Child Task Stop](parent-child-task-stop.md) - capability. + capability. \ No newline at end of file diff --git a/docs/specs/tasks/parent-child-task-stop.md b/docs/specs/tasks/requirements/parent-child-task-stop.md similarity index 93% rename from docs/specs/tasks/parent-child-task-stop.md rename to docs/specs/tasks/requirements/parent-child-task-stop.md index 6b718702d1..f6dee7963d 100644 --- a/docs/specs/tasks/parent-child-task-stop.md +++ b/docs/specs/tasks/requirements/parent-child-task-stop.md @@ -1,10 +1,27 @@ --- -status: shipped +status: active +system: tasks created: 2026-07-19 -owner: cfl +owners: + - cfl --- +# Parent-Child Task Stop Requirements -# Parent-Child Task Stop +## 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. + +## Requirements + +### REQ-TASKS-PARENT-CHILD-TASK-STOP-001: Parent-Child Task Stop + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-PARENT-CHILD-TASK-STOP-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -56,7 +73,7 @@ for a turn boundary, queuing a message, or starting another agent turn. ## API surface Decision: -[ADR-2026-07-19-reject-mcp-actions-on-raw-websocket](../../decisions/2026-07-19-reject-mcp-actions-on-raw-websocket.md) +[ADR-2026-07-19-reject-mcp-actions-on-raw-websocket](../../../decisions/2026-07-19-reject-mcp-actions-on-raw-websocket.md) Task-mode MCP tool request: @@ -237,4 +254,4 @@ dispatcher routing, so it cannot forge the task-mode server's injected identity. that registers its lifecycle execution after its candidate session was inspected. - Clearing or delivering existing queued messages. -- External/admin MCP stop access. +- External/admin MCP stop access. \ No newline at end of file diff --git a/docs/specs/prevent-agent-autostart-on-open/spec.md b/docs/specs/tasks/requirements/prevent-agent-autostart-on-open.md similarity index 93% rename from docs/specs/prevent-agent-autostart-on-open/spec.md rename to docs/specs/tasks/requirements/prevent-agent-autostart-on-open.md index bae1ab306a..1f107dc969 100644 --- a/docs/specs/prevent-agent-autostart-on-open/spec.md +++ b/docs/specs/tasks/requirements/prevent-agent-autostart-on-open.md @@ -1,9 +1,28 @@ --- status: draft +system: tasks created: 2026-08-11 updated: 2026-08-13 -owner: platform +owners: + - platform --- +# Prevent Agent Auto-Start On Open 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. + +## Requirements + +### REQ-TASKS-PREVENT-AGENT-AUTOSTART-ON-OPEN-001: Prevent Agent Auto-Start On Open + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-PREVENT-AGENT-AUTOSTART-ON-OPEN-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail > Follow-up (2026-08-13): for recovered-idle sessions the in-chat Start agent > button was replaced by a hint in the chat composer ("sending a message will @@ -12,8 +31,6 @@ owner: platform > way down. The never-started (CREATED) task-description Start agent button is > unchanged. The sections below describe the current behavior. -# Prevent Agent Auto-Start On Open - ## Why Opening a task after a kandev restart silently starts (or resumes) the agent: @@ -204,4 +221,4 @@ per-task variant in this feature. - Whether the option should also gate the office advanced mode's `ensureExecution` resume once that WS field is actually wired. Currently a - no-op; intentionally out of scope until the office flow auto-starts. + no-op; intentionally out of scope until the office flow auto-starts. \ No newline at end of file diff --git a/docs/specs/tasks/prompt-attachments.md b/docs/specs/tasks/requirements/prompt-attachments.md similarity index 93% rename from docs/specs/tasks/prompt-attachments.md rename to docs/specs/tasks/requirements/prompt-attachments.md index ffc9b54005..babaaea87d 100644 --- a/docs/specs/tasks/prompt-attachments.md +++ b/docs/specs/tasks/requirements/prompt-attachments.md @@ -1,12 +1,29 @@ --- status: draft +system: tasks created: 2026-08-04 -owner: Kandev team +owners: + - Kandev team --- +# Prompt attachments Requirements -# Prompt attachments +## Overview -Decision: [ADR-2026-08-04-file-backed-prompt-attachments](../../decisions/2026-08-04-file-backed-prompt-attachments.md) +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. + +## Requirements + +### REQ-TASKS-PROMPT-ATTACHMENTS-001: Prompt attachments + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-PROMPT-ATTACHMENTS-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail + +Decision: [ADR-2026-08-04-file-backed-prompt-attachments](../../../decisions/2026-08-04-file-backed-prompt-attachments.md) ## Why @@ -186,4 +203,4 @@ request paths never determine ownership. - Per-user or per-workspace configurable attachment limits. - External object storage or direct browser-to-executor uploads. - Changing the separate 10 MiB task-document attachment contract. -- Changing the queue's existing compact thumbnail dimensions or crop policy. +- Changing the queue's existing compact thumbnail dimensions or crop policy. \ No newline at end of file diff --git a/docs/specs/tasks/quick-chat-expiration.md b/docs/specs/tasks/requirements/quick-chat-expiration.md similarity index 94% rename from docs/specs/tasks/quick-chat-expiration.md rename to docs/specs/tasks/requirements/quick-chat-expiration.md index de47896dea..7a069f59dd 100644 --- a/docs/specs/tasks/quick-chat-expiration.md +++ b/docs/specs/tasks/requirements/quick-chat-expiration.md @@ -1,10 +1,27 @@ --- -status: shipped +status: active +system: tasks created: 2026-07-02 -owner: kandev +owners: + - kandev --- +# Quick Chat Sessions, Persistence, and Expiration Requirements -# Quick Chat Sessions, Persistence, and Expiration +## 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. + +## Requirements + +### REQ-TASKS-QUICK-CHAT-EXPIRATION-001: Quick Chat Sessions, Persistence, and Expiration + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-QUICK-CHAT-EXPIRATION-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -60,8 +77,8 @@ while retaining the elevated tools that distinguish them from ordinary quick cha - Ordinary and configuration sessions can coexist without sharing setup state, initial prompts, repository choices, or workspace state. -Decision: [ADR-2026-07-14-typed-utility-chat-sessions](../../decisions/2026-07-14-typed-utility-chat-sessions.md). Repository-backed ordinary -chats additionally follow [ADR 0038](../../decisions/0038-quick-chat-repository-isolation.md). +Decision: [ADR-2026-07-14-typed-utility-chat-sessions](../../../decisions/2026-07-14-typed-utility-chat-sessions.md). Repository-backed ordinary +chats additionally follow [ADR 0038](../../../decisions/0038-quick-chat-repository-isolation.md). ## Data model @@ -272,4 +289,4 @@ deletion. - A general task/chat navigation redesign. - Multiple configuration sessions per workspace. - Repository context for configuration sessions. -- A user-configurable ordinary-chat retention window or automatic expiration for config sessions. +- A user-configurable ordinary-chat retention window or automatic expiration for config sessions. \ No newline at end of file diff --git a/docs/specs/tasks/quick-chat-repository-context.md b/docs/specs/tasks/requirements/quick-chat-repository-context.md similarity index 87% rename from docs/specs/tasks/quick-chat-repository-context.md rename to docs/specs/tasks/requirements/quick-chat-repository-context.md index 0deaed6c32..a926ab1860 100644 --- a/docs/specs/tasks/quick-chat-repository-context.md +++ b/docs/specs/tasks/requirements/quick-chat-repository-context.md @@ -1,10 +1,27 @@ --- -status: shipped +status: active +system: tasks created: 2026-07-14 -owner: kandev +owners: + - kandev --- +# Quick Chat Repository Context Requirements -# Quick Chat Repository Context +## 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. + +## Requirements + +### REQ-TASKS-QUICK-CHAT-REPOSITORY-CONTEXT-001: Quick Chat Repository Context + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-QUICK-CHAT-REPOSITORY-CONTEXT-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -43,7 +60,7 @@ context without changing their checked-out branches or creating a kanban task. - The setup flow supports desktop and mobile layouts without horizontal page scrolling or hover-only actions. Mobile quick chat remains full-screen and does not expose resize handles. -Decision: [ADR 0038](../../decisions/0038-quick-chat-repository-isolation.md). +Decision: [ADR 0038](../../../decisions/0038-quick-chat-repository-isolation.md). ## API surface @@ -115,4 +132,4 @@ them through the existing quick-chat task deletion path. - Remote repository URL entry or arbitrary local folders. - Including uncommitted working-directory changes. - Selecting the same repository more than once in a single quick chat. -- Choosing an executor in the quick-chat setup. +- Choosing an executor in the quick-chat setup. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/remote-contribution-tasks.md b/docs/specs/tasks/requirements/remote-contribution-tasks.md new file mode 100644 index 0000000000..f8f2c0741c --- /dev/null +++ b/docs/specs/tasks/requirements/remote-contribution-tasks.md @@ -0,0 +1,30 @@ +--- +status: active +system: tasks +created: 2026-08-04 +updated: 2026-08-19 +owners: + - product +--- +# Remote Contribution Tasks Requirements + +## Overview + +Maintainers can create a task from an existing GitHub pull request or GitLab merge request and keep +the task checkout synchronized with the contributor's published change without losing local work. + +## Requirements + +### REQ-TASKS-REMOTE-CONTRIBUTION-TASKS-001: Remote Contribution Tasks + +**Intent:** Prepare and operate on a remote contribution using provider-validated identity, explicit +user intent for destructive replacement, and evidence-based version comparison. + +#### Acceptance criteria + +- **AC-TASKS-REMOTE-CONTRIBUTION-TASKS-001.1:** When task creation receives a supported repository, pull-request, or merge-request URL, the system shall validate the provider identity, source branch, head SHA, target branch, and collaboration permission instead of trusting caller-supplied repository or branch metadata. +- **AC-TASKS-REMOTE-CONTRIBUTION-TASKS-001.2:** When a contribution is accepted, the system shall attach the task to the target repository, prepare the checkout at the provider-reported head SHA, configure a dedicated source remote for contributor pushes, and associate the existing provider change before agent launch. +- **AC-TASKS-REMOTE-CONTRIBUTION-TASKS-001.3:** When provider content is used to prepare a task, the system shall keep provider title and body outside trusted task text and prompts, and shall preserve ordinary repository URLs and ordinary task-created pull requests with their existing behavior. +- **AC-TASKS-REMOTE-CONTRIBUTION-TASKS-001.4:** When the checkout and provider history diverge, the system shall classify versions by repository, branch, commit identity, and ancestry evidence; it shall preserve the local task version and expose distinct provider/local actions without treating message or patch similarity as equality. +- **AC-TASKS-REMOTE-CONTRIBUTION-TASKS-001.5:** When a user chooses to replace the provider branch, the system shall require explicit confirmation and an exact provider-head lease; if the provider head changed, it shall leave both versions unchanged and request a fresh review. +- **AC-TASKS-REMOTE-CONTRIBUTION-TASKS-001.6:** When a user chooses the provider version, the system shall require a clean working tree, create a local recovery branch at the current task head, and reset to the confirmed provider head while reporting the recovery branch. diff --git a/docs/specs/tasks/rich-task-title-previews.md b/docs/specs/tasks/requirements/rich-task-title-previews.md similarity index 79% rename from docs/specs/tasks/rich-task-title-previews.md rename to docs/specs/tasks/requirements/rich-task-title-previews.md index 75943ac4c9..90ae0dd21c 100644 --- a/docs/specs/tasks/rich-task-title-previews.md +++ b/docs/specs/tasks/requirements/rich-task-title-previews.md @@ -1,10 +1,27 @@ --- -status: building +status: active +system: tasks created: 2026-08-18 -owner: tbd +owners: + - tbd --- +# Rich task title previews Requirements -# Rich task title previews +## 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. + +## Requirements + +### REQ-TASKS-RICH-TASK-TITLE-PREVIEWS-001: Rich task title previews + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -64,4 +81,4 @@ Provider-specific code supplies labels, icons, test identifiers, and status rows ## Implementation plan -See [the implementation plan](../../plans/rich-task-title-previews/plan.md). +See [the implementation plan](../../../plans/rich-task-title-previews/plan.md). \ No newline at end of file diff --git a/docs/specs/tasks/run-scheduling.md b/docs/specs/tasks/requirements/run-scheduling.md similarity index 89% rename from docs/specs/tasks/run-scheduling.md rename to docs/specs/tasks/requirements/run-scheduling.md index 261c701e98..cc45a6f1fb 100644 --- a/docs/specs/tasks/run-scheduling.md +++ b/docs/specs/tasks/requirements/run-scheduling.md @@ -1,10 +1,27 @@ --- -status: building +status: active +system: tasks created: 2026-08-01 -owner: cfl +owners: + - cfl --- +# Queued run scheduling Requirements -# Queued run scheduling +## 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. + +## Requirements + +### REQ-TASKS-RUN-SCHEDULING-001: Queued run scheduling + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### 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 @@ -47,9 +64,9 @@ errors during a normal shutdown. 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). +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). +Implementation plan: [run-scheduler-lifecycle](../../../plans/run-scheduler-lifecycle/plan.md). ## Data model @@ -145,4 +162,4 @@ existing stale-claim policy after restart. - 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. + existing queue-recovery contract. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/runtime-cleanup.md b/docs/specs/tasks/requirements/runtime-cleanup.md new file mode 100644 index 0000000000..5e65301e10 --- /dev/null +++ b/docs/specs/tasks/requirements/runtime-cleanup.md @@ -0,0 +1,30 @@ +--- +status: draft +system: tasks +created: 2026-06-22 +updated: 2026-08-08 +owners: + - cfl +--- +# Task Runtime Cleanup Requirements + +## Overview + +Task lifecycle operations release runtime resources without deleting reusable task workspaces or +discarding the durable evidence needed to retry cleanup safely. + +## Requirements + +### REQ-TASKS-RUNTIME-CLEANUP-001: Task Runtime Cleanup + +**Intent:** Make archive, delete, shutdown, and startup cleanup ownership-aware, durable, idempotent, +and safe when runtimes or task rows are already gone. + +#### Acceptance criteria + +- **AC-TASKS-RUNTIME-CLEANUP-001.1:** When a task is archived or deleted, the system shall stop every runtime recorded for that task before destructive workspace cleanup, using `executors_running` ownership rather than terminal session state alone. +- **AC-TASKS-RUNTIME-CLEANUP-001.2:** When a session is deleted, the system shall remove only that session and its references; the task-owned workspace, Git worktrees, branches, and reusable files shall remain until a task lifecycle operation requests cleanup. +- **AC-TASKS-RUNTIME-CLEANUP-001.3:** When a task environment is shared, cleanup shall stop the deleting task's runtimes but shall defer destructive environment or worktree teardown until no active session holds the shared environment, and shall never remove borrowed resources. +- **AC-TASKS-RUNTIME-CLEANUP-001.4:** When an agent or agentctl process does not stop within its grace period, the system shall terminate the complete process group and shall not report shutdown complete while descendants remain attached to PID 1. +- **AC-TASKS-RUNTIME-CLEANUP-001.5:** When startup reconciliation sees a stale runtime row, the system shall remove only a confirmed-dead local runtime, preserve alive, unknown, remote, or generically failed rows for retry, and emit bounded diagnostics for fail-closed outcomes. +- **AC-TASKS-RUNTIME-CLEANUP-001.6:** When lifecycle cleanup begins, the system shall persist an operation snapshot and retry state before mutating task state; repeated delivery shall reuse the durable job, and unarchiving shall prevent a pending archive retry from deleting active resources. diff --git a/docs/specs/tasks/runtime-state-publication-order.md b/docs/specs/tasks/requirements/runtime-state-publication-order.md similarity index 86% rename from docs/specs/tasks/runtime-state-publication-order.md rename to docs/specs/tasks/requirements/runtime-state-publication-order.md index 14ac4f318e..46753b4921 100644 --- a/docs/specs/tasks/runtime-state-publication-order.md +++ b/docs/specs/tasks/requirements/runtime-state-publication-order.md @@ -1,11 +1,27 @@ --- -status: shipped +status: active +system: tasks created: 2026-07-30 -owner: kandev -decision: docs/decisions/2026-07-30-runtime-task-state-before-running-event.md +owners: + - kandev --- +# Runtime Task-State Publication Order Requirements -# Runtime Task-State Publication Order +## 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. + +## Requirements + +### REQ-TASKS-RUNTIME-STATE-PUBLICATION-ORDER-001: Runtime Task-State Publication Order + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-RUNTIME-STATE-PUBLICATION-ORDER-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -58,7 +74,7 @@ The existing WebSocket event names and payloads remain unchanged: - `session.state_changed` The ordering contract is defined by -[ADR-2026-07-30-runtime-task-state-before-running-event](../../decisions/2026-07-30-runtime-task-state-before-running-event.md). +[ADR-2026-07-30-runtime-task-state-before-running-event](../../../decisions/2026-07-30-runtime-task-state-before-running-event.md). ## State machine @@ -121,4 +137,4 @@ The ordering contract is defined by - Inferring persisted task state in the frontend from session events. - Changing workflow-step transitions, session-state enums, WebSocket payloads, Office task-state ownership, or task status icons. -- Adding new desktop or mobile layout, navigation, or touch behavior. +- Adding new desktop or mobile layout, navigation, or touch behavior. \ No newline at end of file diff --git a/docs/specs/tasks/sidebar-task-edit.md b/docs/specs/tasks/requirements/sidebar-task-edit.md similarity index 86% rename from docs/specs/tasks/sidebar-task-edit.md rename to docs/specs/tasks/requirements/sidebar-task-edit.md index e142363aaa..ccf9e9ac86 100644 --- a/docs/specs/tasks/sidebar-task-edit.md +++ b/docs/specs/tasks/requirements/sidebar-task-edit.md @@ -1,10 +1,27 @@ --- -status: approved +status: active +system: tasks created: 2026-08-03 -owner: Kandev +owners: + - Kandev --- +# Sidebar Task Editing Requirements -# Sidebar Task Editing +## 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. + +## Requirements + +### REQ-TASKS-SIDEBAR-TASK-EDIT-001: Sidebar Task Editing + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-SIDEBAR-TASK-EDIT-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -58,4 +75,4 @@ Edit follows the same access rules as editing from a Kanban card. The sidebar do ## Implementation plan -See [`../../plans/sidebar-task-edit/plan.md`](../../plans/sidebar-task-edit/plan.md). +See [`../../plans/sidebar-task-edit/plan.md`](../../../plans/sidebar-task-edit/plan.md). \ No newline at end of file diff --git a/docs/specs/tasks/subtask-checklist.md b/docs/specs/tasks/requirements/subtask-checklist.md similarity index 83% rename from docs/specs/tasks/subtask-checklist.md rename to docs/specs/tasks/requirements/subtask-checklist.md index 45e93c7b8c..7456f9f2d3 100644 --- a/docs/specs/tasks/subtask-checklist.md +++ b/docs/specs/tasks/requirements/subtask-checklist.md @@ -1,10 +1,27 @@ --- -status: shipped +status: active +system: tasks created: 2026-04-28 -owner: cfl +owners: + - cfl --- +# Subtasks as Workflow Checklist Requirements -# Subtasks as Workflow Checklist +## 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. + +## Requirements + +### REQ-TASKS-SUBTASK-CHECKLIST-001: Subtasks as Workflow Checklist + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-SUBTASK-CHECKLIST-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -79,4 +96,4 @@ to a sibling. If no child references a sibling blocker, render as a flat list. (that lives on the parent task header; see `StageProgressBar`). - Creating or deleting blocker relationships from this view. - A `position` field on `Task` — ordering derives entirely from `blocked_by` chains and - creation time; no new DB column is needed. + creation time; no new DB column is needed. \ No newline at end of file diff --git a/docs/specs/tasks/subtask-completion-trigger.md b/docs/specs/tasks/requirements/subtask-completion-trigger.md similarity index 93% rename from docs/specs/tasks/subtask-completion-trigger.md rename to docs/specs/tasks/requirements/subtask-completion-trigger.md index 9b4da51537..6700d806d0 100644 --- a/docs/specs/tasks/subtask-completion-trigger.md +++ b/docs/specs/tasks/requirements/subtask-completion-trigger.md @@ -1,10 +1,27 @@ --- status: draft +system: tasks created: 2026-06-23 -owner: cfl +owners: + - cfl --- +# Subtask Completion Trigger Requirements -# Subtask Completion Trigger +## 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. + +## Requirements + +### REQ-TASKS-SUBTASK-COMPLETION-TRIGGER-001: Subtask Completion Trigger + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-SUBTASK-COMPLETION-TRIGGER-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -235,4 +252,4 @@ This feature adds no new permission boundary. - Recursive descendant aggregation. - Creating parent sessions automatically when no parent session exists. - Adding a new MCP wait primitive or long-poll API. -- Requiring every child agent to know whether it is the last sibling. +- Requiring every child agent to know whether it is the last sibling. \ No newline at end of file diff --git a/docs/specs/tasks/subtask-detachment.md b/docs/specs/tasks/requirements/subtask-detachment.md similarity index 88% rename from docs/specs/tasks/subtask-detachment.md rename to docs/specs/tasks/requirements/subtask-detachment.md index 746c6120b2..e0676ad4db 100644 --- a/docs/specs/tasks/subtask-detachment.md +++ b/docs/specs/tasks/requirements/subtask-detachment.md @@ -1,10 +1,27 @@ --- -status: shipped +status: active +system: tasks created: 2026-07-18 -owner: kandev +owners: + - kandev --- +# Subtask detachment Requirements -# Subtask detachment +## 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. + +## Requirements + +### REQ-TASKS-SUBTASK-DETACHMENT-001: Subtask detachment + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-SUBTASK-DETACHMENT-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -88,4 +105,4 @@ The cleared parent relationship and normalized workspace mode survive backend re ## Implementation plan -See [the implementation plan](../../plans/subtask-detachment/plan.md). +See [the implementation plan](../../../plans/subtask-detachment/plan.md). \ No newline at end of file diff --git a/docs/specs/tasks/subtask-reparenting-drag-drop.md b/docs/specs/tasks/requirements/subtask-reparenting-drag-drop.md similarity index 91% rename from docs/specs/tasks/subtask-reparenting-drag-drop.md rename to docs/specs/tasks/requirements/subtask-reparenting-drag-drop.md index cbdb0f895f..bf5d950d93 100644 --- a/docs/specs/tasks/subtask-reparenting-drag-drop.md +++ b/docs/specs/tasks/requirements/subtask-reparenting-drag-drop.md @@ -1,10 +1,27 @@ --- -status: building +status: active +system: tasks created: 2026-08-04 -owner: kandev +owners: + - kandev --- +# Subtask re-parenting by drag and drop Requirements -# Subtask re-parenting by drag and drop +## 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. + +## Requirements + +### REQ-TASKS-SUBTASK-REPARENTING-DRAG-DROP-001: Subtask re-parenting by drag and drop + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-SUBTASK-REPARENTING-DRAG-DROP-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -74,4 +91,4 @@ No new endpoint. Reuses and extends existing contracts: ## Implementation plan -See [the implementation plan](../../plans/subtask-reparenting-drag-drop/plan.md). +See [the implementation plan](../../../plans/subtask-reparenting-drag-drop/plan.md). \ No newline at end of file diff --git a/docs/specs/tasks/subtree-controls.md b/docs/specs/tasks/requirements/subtree-controls.md similarity index 93% rename from docs/specs/tasks/subtree-controls.md rename to docs/specs/tasks/requirements/subtree-controls.md index aaa9f03797..c1386c8102 100644 --- a/docs/specs/tasks/subtree-controls.md +++ b/docs/specs/tasks/requirements/subtree-controls.md @@ -1,11 +1,28 @@ --- -status: shipped +status: active +system: tasks created: 2026-04-28 updated: 2026-08-14 -owner: cfl +owners: + - cfl --- +# Task Subtree Controls Requirements -# Task Subtree Controls +## 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. + +## Requirements + +### REQ-TASKS-SUBTREE-CONTROLS-001: Task Subtree Controls + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-SUBTREE-CONTROLS-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -117,7 +134,7 @@ Pending clarification bundles can be skipped from task chat. publishes the existing cancellation event. It is not the chat Skip path. The current-turn ownership and restart behavior are defined by -[Active Clarification Lifecycle](../clarification-active-lifecycle/spec.md). +[Active Clarification Lifecycle](clarification-active-lifecycle.md). --- @@ -222,4 +239,4 @@ Index: `idx_tree_hold_members_task` on `(task_id)` — supports "is this task ga - Bulk operations across multiple unrelated trees in a single API call. - Cascading hold creation on tasks that are spawned after the hold is created (holds are point-in-time snapshots of the tree at creation time; new children are not - automatically gated). + automatically gated). \ No newline at end of file diff --git a/docs/specs/tasks/task-create-escape-dismissal.md b/docs/specs/tasks/requirements/task-create-escape-dismissal.md similarity index 72% rename from docs/specs/tasks/task-create-escape-dismissal.md rename to docs/specs/tasks/requirements/task-create-escape-dismissal.md index 626e04e253..ba3c0b5ee9 100644 --- a/docs/specs/tasks/task-create-escape-dismissal.md +++ b/docs/specs/tasks/requirements/task-create-escape-dismissal.md @@ -1,10 +1,27 @@ --- -status: complete +status: active +system: tasks created: 2026-08-19 -owner: kandev +owners: + - kandev --- +# Create Task Escape Dismissal Requirements -# Create Task Escape Dismissal +## 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. + +## Requirements + +### REQ-TASKS-TASK-CREATE-ESCAPE-DISMISSAL-001: Create Task Escape Dismissal + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-TASK-CREATE-ESCAPE-DISMISSAL-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -39,4 +56,4 @@ The same key must not close the Create Task dialog and discard the active task-c ## Implementation plan -- [Create Task Escape Dismissal](../../plans/task-create-escape-dismissal/plan.md) +- [Create Task Escape Dismissal](../../../plans/task-create-escape-dismissal/plan.md) \ No newline at end of file diff --git a/docs/specs/tasks/task-create-executor-default.md b/docs/specs/tasks/requirements/task-create-executor-default.md similarity index 83% rename from docs/specs/tasks/task-create-executor-default.md rename to docs/specs/tasks/requirements/task-create-executor-default.md index 2eaf3cd6ab..91222e8b8a 100644 --- a/docs/specs/tasks/task-create-executor-default.md +++ b/docs/specs/tasks/requirements/task-create-executor-default.md @@ -1,12 +1,28 @@ --- -status: approved +status: active +system: tasks created: 2026-08-01 updated: 2026-08-18 -owner: Kandev -decision: ADR-2026-08-01-repository-task-executor-defaults +owners: + - Kandev --- +# Task Create Executor Default Requirements -# Task Create Executor Default +## 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. + +## Requirements + +### REQ-TASKS-TASK-CREATE-EXECUTOR-DEFAULT-001: Task Create Executor Default + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-TASK-CREATE-EXECUTOR-DEFAULT-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -50,8 +66,8 @@ repository-backed default again. ## Persistence guarantees `users.settings.task_create_last_used.executor_profile_id` remains a portable backend-owned user -preference as defined by [ADR 0028](../../decisions/0028-task-create-last-used-source-of-truth.md) -and [ADR 0041](../../decisions/0041-backend-owned-portable-user-settings.md). No browser storage, +preference as defined by [ADR 0028](../../../decisions/0028-task-create-last-used-source-of-truth.md) +and [ADR 0041](../../../decisions/0041-backend-owned-portable-user-settings.md). No browser storage, schema migration, or new persistence field is introduced. The stored profile is a convenience within the executor selected by policy, not a portable override of that policy. @@ -83,4 +99,4 @@ within the executor selected by policy, not a portable override of that policy. - Adding per-executor last-used profile history. - Preventing users from manually choosing Local for an ordinary task. - Changing workspace executor settings or their explicit precedence. -- Changing task-create dialog layout or mobile interaction patterns. +- Changing task-create dialog layout or mobile interaction patterns. \ No newline at end of file diff --git a/docs/specs/tasks/task-create-workflow-memory.md b/docs/specs/tasks/requirements/task-create-workflow-memory.md similarity index 85% rename from docs/specs/tasks/task-create-workflow-memory.md rename to docs/specs/tasks/requirements/task-create-workflow-memory.md index 4928bb4c3e..c7590bc7c1 100644 --- a/docs/specs/tasks/task-create-workflow-memory.md +++ b/docs/specs/tasks/requirements/task-create-workflow-memory.md @@ -1,11 +1,27 @@ --- -status: approved +status: active +system: tasks created: 2026-08-08 -owner: Kandev -decision: ADR-2026-08-08-workspace-scoped-task-create-workflow-memory +owners: + - Kandev --- +# Task Create Workflow Memory Requirements -# Task Create Workflow Memory +## 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. + +## Requirements + +### REQ-TASKS-TASK-CREATE-WORKFLOW-MEMORY-001: Task Create Workflow Memory + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-TASK-CREATE-WORKFLOW-MEMORY-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -78,9 +94,9 @@ profile, and executor profile fields. ## Persistence guarantees Workflow memory is a portable backend-owned user preference under -[ADR 0028](../../decisions/0028-task-create-last-used-source-of-truth.md), -[ADR 0041](../../decisions/0041-backend-owned-portable-user-settings.md), and -[ADR-2026-08-08-workspace-scoped-task-create-workflow-memory](../../decisions/2026-08-08-workspace-scoped-task-create-workflow-memory.md). +[ADR 0028](../../../decisions/0028-task-create-last-used-source-of-truth.md), +[ADR 0041](../../../decisions/0041-backend-owned-portable-user-settings.md), and +[ADR-2026-08-08-workspace-scoped-task-create-workflow-memory](../../../decisions/2026-08-08-workspace-scoped-task-create-workflow-memory.md). It survives backend restarts, browser changes, and workspace switches. The frontend's queued overlay is transient and only bridges the interval between a successful create response and the authoritative settings update. @@ -117,4 +133,4 @@ successful create response and the authoritative settings update. - Remembering workflow choices from cancelled or failed task creation. - Exposing workflow history in Settings. - Changing hidden-workflow rules or feature-specific locked task-create flows. -- Changing the task-create dialog's desktop or mobile composition. +- Changing the task-create dialog's desktop or mobile composition. \ No newline at end of file diff --git a/docs/specs/task-dependencies/create-dialog-advanced-settings.md b/docs/specs/tasks/requirements/task-dependencies-create-dialog-advanced-settings.md similarity index 90% rename from docs/specs/task-dependencies/create-dialog-advanced-settings.md rename to docs/specs/tasks/requirements/task-dependencies-create-dialog-advanced-settings.md index 99a3a5f448..0ab45b2d81 100644 --- a/docs/specs/task-dependencies/create-dialog-advanced-settings.md +++ b/docs/specs/tasks/requirements/task-dependencies-create-dialog-advanced-settings.md @@ -1,10 +1,27 @@ --- -status: shipped +status: active +system: tasks created: 2026-08-13 -owner: kandev +owners: + - kandev --- +# Task-create advanced settings disclosure Requirements -# Task-create advanced settings disclosure +## 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. + +## Requirements + +### REQ-TASKS-TASK-DEPENDENCIES-CREATE-DIALOG-ADVANCED-SETTINGS-001: Task-create advanced settings disclosure + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-TASK-DEPENDENCIES-CREATE-DIALOG-ADVANCED-SETTINGS-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -132,4 +149,4 @@ place for dependency selection and future less-common options. - Adding dependency editing to task detail, Kanban card menus, or MCP. - Adding or designing future advanced controls beyond the extensible content region. -- Changing workflow, agent, executor, repository, or branch selector behavior. +- Changing workflow, agent, executor, repository, or branch selector behavior. \ No newline at end of file diff --git a/docs/specs/task-dependencies/create-dialog-dependency-selector.md b/docs/specs/tasks/requirements/task-dependencies-create-dialog-dependency-selector.md similarity index 87% rename from docs/specs/task-dependencies/create-dialog-dependency-selector.md rename to docs/specs/tasks/requirements/task-dependencies-create-dialog-dependency-selector.md index 4fd6c34c4d..0ac121cc85 100644 --- a/docs/specs/task-dependencies/create-dialog-dependency-selector.md +++ b/docs/specs/tasks/requirements/task-dependencies-create-dialog-dependency-selector.md @@ -1,10 +1,27 @@ --- -status: implemented +status: active +system: tasks created: 2026-08-13 -owner: kandev +owners: + - kandev --- +# Task-create dependency selector refinement Requirements -# Task-create dependency selector refinement +## 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. + +## Requirements + +### REQ-TASKS-TASK-DEPENDENCIES-CREATE-DIALOG-DEPENDENCY-SELECTOR-001: Task-create dependency selector refinement + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-TASK-DEPENDENCIES-CREATE-DIALOG-DEPENDENCY-SELECTOR-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -102,4 +119,4 @@ set of tasks. remain supported. - Adding dependency editing to task detail, Kanban card menus, or MCP. - Adding a new mobile navigation surface or replacing the task-create dialog. -- Changing workflow, agent, executor, repository, or branch selector behavior. +- Changing workflow, agent, executor, repository, or branch selector behavior. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/task-dependencies.md b/docs/specs/tasks/requirements/task-dependencies.md new file mode 100644 index 0000000000..90c2867973 --- /dev/null +++ b/docs/specs/tasks/requirements/task-dependencies.md @@ -0,0 +1,22 @@ +--- +status: active +system: tasks +created: 2026-08-09 +owners: + - kandev +--- +# Task Dependencies and Auto-Start Chains Requirements + +## Overview + +This requirement record defines the observable task-system contract for the companion system design. + +## Requirements + +### REQ-TASKS-TASK-DEPENDENCIES-001: Task Dependencies and Auto-Start Chains + +**Intent:** Preserve the observable behavior of the migrated capability. + +#### Acceptance criteria + +- **AC-TASKS-TASK-DEPENDENCIES-001.1:** When a consumer uses this capability, the system shall satisfy the behavior described by the companion system design. diff --git a/docs/specs/task-launch-failure-recovery/spec.md b/docs/specs/tasks/requirements/task-launch-failure-recovery.md similarity index 94% rename from docs/specs/task-launch-failure-recovery/spec.md rename to docs/specs/tasks/requirements/task-launch-failure-recovery.md index 13cf2fb82c..d4019d3213 100644 --- a/docs/specs/task-launch-failure-recovery/spec.md +++ b/docs/specs/tasks/requirements/task-launch-failure-recovery.md @@ -1,10 +1,27 @@ --- status: draft +system: tasks created: 2026-08-19 -owner: cfl12 +owners: + - cfl12 --- +# Task Launch Failure Recovery Requirements -# Task Launch Failure Recovery +## 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. + +## 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. + +#### 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 @@ -272,4 +289,4 @@ The picker owns its internal list scroll and clears the bottom safe area. - 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. +- A different self-heal target for stacked PRs. \ No newline at end of file diff --git a/docs/specs/tasks/title-length-limit.md b/docs/specs/tasks/requirements/title-length-limit.md similarity index 89% rename from docs/specs/tasks/title-length-limit.md rename to docs/specs/tasks/requirements/title-length-limit.md index 3ebc46f1e5..5d3fdc1d5d 100644 --- a/docs/specs/tasks/title-length-limit.md +++ b/docs/specs/tasks/requirements/title-length-limit.md @@ -1,11 +1,28 @@ --- -status: complete +status: active +system: tasks created: 2026-08-01 updated: 2026-08-11 -owner: Kandev +owners: + - Kandev --- +# Task Title Length Limit Requirements -# Task Title Length Limit +## 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. + +## Requirements + +### REQ-TASKS-TITLE-LENGTH-LIMIT-001: Task Title Length Limit + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-TITLE-LENGTH-LIMIT-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -58,4 +75,4 @@ Long task titles crowd navigation and task chrome, especially when a remote pull ## Implementation plan -See [the implementation plan](../../plans/task-title-length-limit/plan.md) and the [caret-preservation fix plan](../../plans/task-title-caret/plan.md). +See [the implementation plan](../../../plans/task-title-length-limit/plan.md) and the [caret-preservation fix plan](../../../plans/task-title-caret/plan.md). \ No newline at end of file diff --git a/docs/specs/tasks/user-question-turn-boundary.md b/docs/specs/tasks/requirements/user-question-turn-boundary.md similarity index 81% rename from docs/specs/tasks/user-question-turn-boundary.md rename to docs/specs/tasks/requirements/user-question-turn-boundary.md index 0397077996..6c49b0719f 100644 --- a/docs/specs/tasks/user-question-turn-boundary.md +++ b/docs/specs/tasks/requirements/user-question-turn-boundary.md @@ -1,10 +1,27 @@ --- -status: shipped +status: active +system: tasks created: 2026-08-22 -owner: kandev +owners: + - kandev --- +# User Question Turn Boundary Requirements -# User Question Turn Boundary +## 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. + +## Requirements + +### REQ-TASKS-USER-QUESTION-TURN-BOUNDARY-001: User Question Turn Boundary + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-USER-QUESTION-TURN-BOUNDARY-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -69,4 +86,4 @@ question wait timed out or disconnected. keepalive, persistence, or resume behavior. - Changing configuration-mode, Office-mode, or external MCP prompt surfaces. - Adding model-specific prompt variants or runtime enforcement that cancels an - agent immediately after the tool call. + agent immediately after the tool call. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/wip-limit-pull-system.md b/docs/specs/tasks/requirements/wip-limit-pull-system.md new file mode 100644 index 0000000000..7c94c5be41 --- /dev/null +++ b/docs/specs/tasks/requirements/wip-limit-pull-system.md @@ -0,0 +1,30 @@ +--- +status: active +system: tasks +created: 2026-07-27 +updated: 2026-08-12 +owners: + - kandev +--- +# WIP Limits and Visible Overflow Queues Requirements + +## Overview + +Workflow WIP limits control active admission, not visibility. Work that cannot run immediately remains +durable and visible in a deterministic queue. + +## Requirements + +### REQ-TASKS-WIP-LIMIT-PULL-SYSTEM-001: WIP Limits and Visible Overflow Queues + +**Intent:** Apply one visible overflow and admission contract to task creation, task moves, +integrations, and workflow transitions without exceeding configured WIP capacity. + +#### Acceptance criteria + +- **AC-TASKS-WIP-LIMIT-PULL-SYSTEM-001.1:** When a task is created for a limited step, the system shall admit it when capacity exists; otherwise it shall keep the task durable and visible in the destination queue or its configured one-hop feeder, with `wip_limit: 0` remaining unlimited. +- **AC-TASKS-WIP-LIMIT-PULL-SYSTEM-001.2:** When a queued task is not admitted, the system shall not start an agent, create a session, prepare an executor, or consume a WIP slot; when capacity opens, it shall promote queued work in destination-first deterministic order and run destination entry behavior only after admission. +- **AC-TASKS-WIP-LIMIT-PULL-SYSTEM-001.3:** When a task is moved by UI, HTTP, WebSocket, MCP, drag/drop, bulk action, or workflow transition into a full limited step, the system shall complete the move as queued in that destination without routing through its feeder, and shall defer destination entry effects until promotion. +- **AC-TASKS-WIP-LIMIT-PULL-SYSTEM-001.4:** When a task is created or moved through any supported surface, the system shall return and publish its actual placement, admission, queue destination, and queue time so the Kanban board can show admitted and queued cards separately with correct WIP counts. +- **AC-TASKS-WIP-LIMIT-PULL-SYSTEM-001.5:** When a configured feeder is full, invalid, or belongs to another workflow, the system shall return a typed conflict and persist no task or launch intent; it shall not walk a second feeder or create a hidden task. +- **AC-TASKS-WIP-LIMIT-PULL-SYSTEM-001.6:** When direct creation and event-driven promotion can both auto-start the same admitted task, the system shall use one race-safe claim so at most one session starts, and shall release the claim when launch fails so retry remains possible. diff --git a/docs/specs/tasks/without-repositories.md b/docs/specs/tasks/requirements/without-repositories.md similarity index 86% rename from docs/specs/tasks/without-repositories.md rename to docs/specs/tasks/requirements/without-repositories.md index a00786aa9d..6f0fd61288 100644 --- a/docs/specs/tasks/without-repositories.md +++ b/docs/specs/tasks/requirements/without-repositories.md @@ -1,10 +1,27 @@ --- status: draft +system: tasks created: 2026-05-05 -owner: tbd +owners: + - tbd --- +# Tasks Without Repositories Requirements -# Tasks Without Repositories +## 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. + +## Requirements + +### REQ-TASKS-WITHOUT-REPOSITORIES-001: Tasks Without Repositories + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-WITHOUT-REPOSITORIES-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -43,4 +60,4 @@ Not every piece of agent work is tied to a codebase. Users want to brainstorm a - **Scratch workspace location and lifecycle.** Per-task scratch dir under the kandev data directory (e.g. `~/.kandev/data/scratch//`)? Wiped on task delete, or retained until manual cleanup? Per-executor — does `local_docker` get a named volume, or a tmpfs? - **Folder picker mechanism.** Backend endpoint that lists local directories (browse from `$HOME`)? Or a free-text path input with server-side validation? Does kandev's existing repo-add flow already have a directory browser we can reuse? -- **Storage of the picked folder.** New nullable `task_sessions.workspace_path` column, or reuse an existing field? +- **Storage of the picked folder.** New nullable `task_sessions.workspace_path` column, or reuse an existing field? \ No newline at end of file diff --git a/docs/specs/workflow/cancelled-turn-completion/spec.md b/docs/specs/tasks/requirements/workflow-cancelled-turn-completion.md similarity index 95% rename from docs/specs/workflow/cancelled-turn-completion/spec.md rename to docs/specs/tasks/requirements/workflow-cancelled-turn-completion.md index 4835124bec..7c2929bb12 100644 --- a/docs/specs/workflow/cancelled-turn-completion/spec.md +++ b/docs/specs/tasks/requirements/workflow-cancelled-turn-completion.md @@ -1,10 +1,27 @@ --- -status: building +status: active +system: tasks created: 2026-08-02 -owner: kandev +owners: + - kandev --- +# Cancelled Turn Completion Requirements -# Cancelled Turn Completion +## 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. + +## Requirements + +### REQ-TASKS-WORKFLOW-CANCELLED-TURN-COMPLETION-001: Cancelled Turn Completion + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-WORKFLOW-CANCELLED-TURN-COMPLETION-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail Decision: [ADR-2026-08-02-explicit-user-cancel-completion](../../../decisions/2026-08-02-explicit-user-cancel-completion.md) @@ -123,4 +140,4 @@ Template defaults apply when a workflow's steps are instantiated. Changing the e - Changing queued-message drain policy after cancellation. - Treating agent errors, provider failures, crashes, task stops, or clarification pauses as successful completion. - Adding cancellation-cause metadata to workflow transition history. -- Reducing the existing lifecycle timeout for agents that acknowledge cancellation but never finish their prompt, or synthesizing success before their terminal path settles. +- Reducing the existing lifecycle timeout for agents that acknowledge cancellation but never finish their prompt, or synthesizing success before their terminal path settles. \ No newline at end of file diff --git a/docs/specs/workflow-cycle-guardrails/spec.md b/docs/specs/tasks/requirements/workflow-cycle-guardrails.md similarity index 93% rename from docs/specs/workflow-cycle-guardrails/spec.md rename to docs/specs/tasks/requirements/workflow-cycle-guardrails.md index 5f107de990..9b1566d7ae 100644 --- a/docs/specs/workflow-cycle-guardrails/spec.md +++ b/docs/specs/tasks/requirements/workflow-cycle-guardrails.md @@ -1,10 +1,27 @@ --- -status: building +status: active +system: tasks created: 2026-07-15 -owner: kandev +owners: + - kandev --- +# Workflow Cycle Guardrails Requirements -# Workflow Cycle Guardrails +## 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. + +## Requirements + +### REQ-TASKS-WORKFLOW-CYCLE-GUARDRAILS-001: Workflow Cycle Guardrails + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-WORKFLOW-CYCLE-GUARDRAILS-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -193,4 +210,4 @@ deterministic and has no persisted state. ## Implementation Plan -[Implementation plan](../../plans/workflow-cycle-guardrails/plan.md) +[Implementation plan](../../../plans/workflow-cycle-guardrails/plan.md) \ No newline at end of file diff --git a/docs/specs/workflow-duplication/spec.md b/docs/specs/tasks/requirements/workflow-duplication.md similarity index 86% rename from docs/specs/workflow-duplication/spec.md rename to docs/specs/tasks/requirements/workflow-duplication.md index b239dd56dd..5f7d53e984 100644 --- a/docs/specs/workflow-duplication/spec.md +++ b/docs/specs/tasks/requirements/workflow-duplication.md @@ -1,10 +1,27 @@ --- status: draft +system: tasks created: 2026-08-11 -owner: kandev +owners: + - kandev --- +# Workflow Duplication Requirements -# Workflow Duplication +## 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. + +## Requirements + +### REQ-TASKS-WORKFLOW-DUPLICATION-001: Workflow Duplication + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-WORKFLOW-DUPLICATION-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -25,7 +42,7 @@ Workflow authors often need a new pipeline that differs only slightly from an ex - Duplicate is unavailable for a new unsaved workflow or a workflow with unsaved metadata or step changes. The UI explains that the source must be saved before it can be duplicated. - Duplicate is unavailable in the read-only Improve Kandev workspace. It remains available for a sync-managed workflow in a normal editable workspace because creating the independent manual draft does not mutate the source. - The source workflow remains unchanged. Editing, saving, deleting, or reordering the duplicate after creation has no ongoing effect on the source. -- Discarding or removing the duplicate draft creates no durable workflow. Reloading before Save also discards it, following the existing [Settings Manual Save](../ui/settings-manual-save.md) contract. +- Discarding or removing the duplicate draft creates no durable workflow. Reloading before Save also discards it, following the existing [Settings Manual Save](../../ui/settings-manual-save.md) contract. ## Permissions @@ -68,4 +85,4 @@ The user must be able to create workflows in the source workspace. A sync-manage ## Implementation Plan -See [the implementation plan](../../plans/workflow-duplication/plan.md). +See [the implementation plan](../../../plans/workflow-duplication/plan.md). \ No newline at end of file diff --git a/docs/specs/workflow/explicit-completion-signal/spec.md b/docs/specs/tasks/requirements/workflow-explicit-completion-signal.md similarity index 84% rename from docs/specs/workflow/explicit-completion-signal/spec.md rename to docs/specs/tasks/requirements/workflow-explicit-completion-signal.md index bd0ec1d577..a4be942ee3 100644 --- a/docs/specs/workflow/explicit-completion-signal/spec.md +++ b/docs/specs/tasks/requirements/workflow-explicit-completion-signal.md @@ -1,10 +1,27 @@ --- -status: shipped +status: active +system: tasks created: 2026-07-22 -owner: kandev +owners: + - kandev --- +# Explicit Workflow-Step Completion Signal Requirements -# Explicit Workflow-Step Completion Signal +## 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. + +## Requirements + +### REQ-TASKS-WORKFLOW-EXPLICIT-COMPLETION-SIGNAL-001: Explicit Workflow-Step Completion Signal + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-WORKFLOW-EXPLICIT-COMPLETION-SIGNAL-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -20,7 +37,7 @@ Signal-gated Kanban workflows need a reliable distinction between an agent endin - Creating or loading an ACP session supplies the local Kandev MCP server. After a transient MCP reconnect, the client can list and call `step_complete_kandev` again without a user message or process restart. - Kandev does not pin the Claude ACP bridge version as part of this feature. Bridge selection retains the existing unversioned package behavior; diagnostics continue to report the version returned during ACP initialization. - Existing idempotency, clarification barriers, and re-open semantics remain unchanged. ADR 0015's planned manual "Mark complete & advance" fallback is not currently implemented and is outside this reliability fix. -- Explicit user-cancel completion is owned by the [Cancelled Turn Completion](../cancelled-turn-completion/spec.md) spec. When a step opts into that policy, the user's cancel action is a human completion decision and may run `on_turn_complete` without an agent-emitted signal; internal cancellation paths remain non-completing. +- Explicit user-cancel completion is owned by the [Cancelled Turn Completion](workflow-cancelled-turn-completion.md) spec. When a step opts into that policy, the user's cancel action is a human completion decision and may run `on_turn_complete` without an agent-emitted signal; internal cancellation paths remain non-completing. ## API Surface @@ -77,4 +94,4 @@ The pending completion signal continues to use `TaskSession.Metadata` as specifi - Exposing `step_complete_kandev` to Office agents. - Changing completion-signal persistence or ordinary halt semantics, or implementing ADR 0015's planned manual fallback UI. Explicit user-cancel behavior is defined separately by the cancelled-turn completion spec. - Adding vendor-specific eager-load metadata; agents are expected to use normal MCP tool discovery. -- Implementing a generic MCP proxy or replacing the ACP bridge. +- Implementing a generic MCP proxy or replacing the ACP bridge. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/workflow-quorum-decision-recording-agent-surface.md b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-agent-surface.md new file mode 100644 index 0000000000..d7721a26bc --- /dev/null +++ b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-agent-surface.md @@ -0,0 +1,104 @@ +--- +status: draft +system: tasks +created: 2026-08-19 +owners: + - kandev +--- + + +# Agent decision recording Requirements + + + +## Overview + + + +An Office participant can submit a validated verdict with a reason, and the result is tied to the task and workflow step used for validation. + +## Requirements + + +### REQ-TASKS-QUORUM-RECORDING-001: Agent decision recording + + + +**Intent:** An Office participant can submit a validated verdict with a reason, and the result is tied to the task and workflow step used for validation. + +#### Acceptance criteria + +#### Recording a decision (agent surface) + +- **AC-TASKS-QUORUM-RECORDING-001.1:** WHEN an Office agent session is attached to the task's current step + with role `reviewer` or `approver` and `decision_required = 1`, THE SYSTEM + SHALL expose an MCP tool that records a decision, accepting a verdict of + `approved` or `rejected` and a required non-empty reason. +- **AC-TASKS-QUORUM-RECORDING-001.2:** WHEN that tool is called with a valid verdict, THE SYSTEM SHALL write + one row to `workflow_step_decisions` with `task_id` = the calling session's + task, `step_id` = that task's current `workflow_step_id`, `role` = the + caller's resolved role, `decider_type` = `agent`, `decider_id` = the caller's + agent profile id, and `participant_id` = the id of the **seat** the participant occupies, as + defined by AC-TASKS-QUORUM-SLATE-001.9 — that is, AC-TASKS-QUORUM-SLATE-001.3 canonicalization followed by AC-TASKS-QUORUM-SLATE-001.5 collapse, + not AC-TASKS-QUORUM-SLATE-001.3 alone. The written `participant_id` SHALL be the same id + the quorum evaluator uses for that participant in the required slate; a + decision whose `participant_id` is not the canonical id is a defect, because + `latestDecisionsPerParticipant` silently discards it. +- **AC-TASKS-QUORUM-RECORDING-001.3:** WHEN the caller is not a participant on the task with + `decision_required = 1` for `reviewer` or `approver`, THE SYSTEM SHALL reject + the call with a permission error and write no row. +- **AC-TASKS-QUORUM-RECORDING-001.4:** WHEN the caller holds both `reviewer` and `approver`, THE SYSTEM + SHALL record the decision under `approver`, matching the existing + approver-wins precedence in `resolveDeciderRole` + (`office/dashboard/decisions.go`). +- **AC-TASKS-QUORUM-RECORDING-001.5:** THE SYSTEM SHALL resolve the caller's role over the same + participant population that AC-TASKS-QUORUM-SLATE-001.9 builds, not over rows scoped to the + task's current step alone. `ListAllTaskParticipants` is step-scoped today, so + an approver attached at an earlier step is currently unresolvable at the step + where the approval is due; role resolution and slate membership SHALL NOT + disagree about who participates. +- **AC-TASKS-QUORUM-RECORDING-001.6:** WHEN the verdict is any value other than `approved` or `rejected`, + THE SYSTEM SHALL reject the call with a validation error and write no row. +- **AC-TASKS-QUORUM-RECORDING-001.7:** WHEN the reason is empty or whitespace-only, THE SYSTEM SHALL reject + the call with a validation error and write no row. +- **AC-TASKS-QUORUM-RECORDING-001.8:** WHEN the task has no `workflow_step_id` bound, THE SYSTEM SHALL + reject the call with an error naming the unbound step and write no row. +- **AC-TASKS-QUORUM-RECORDING-001.9:** THE SYSTEM SHALL NOT expose the decision tool to sessions whose + profile does not enable `SurfaceOfficeTask`. + +- **AC-TASKS-QUORUM-RECORDING-001.10:** THE SYSTEM SHALL validate the decision tool's preconditions in this + order: (1) the task has a bound `workflow_step_id` (AC-TASKS-QUORUM-RECORDING-001.8), (2) the caller + resolves to a participant with `decision_required = 1` for `reviewer` or + `approver` (AC-TASKS-QUORUM-RECORDING-001.3), (3) the verdict is `approved` or `rejected` (AC-TASKS-QUORUM-RECORDING-001.6), (4) the + reason is non-empty (AC-TASKS-QUORUM-RECORDING-001.7). The step check precedes the permission check + because `AddTaskParticipant` silently no-ops when a task has no bound step + (`office/repository/sqlite/participants.go`), so an unbound-step task has zero + participant rows **by construction**; a permission-first implementation would + report AC-TASKS-QUORUM-RECORDING-001.3's opaque permission error on the most likely real trigger for AC-TASKS-QUORUM-RECORDING-001.8, + a task created before a workflow step was assigned. The order is stated because + both preconditions are false at once in that case and the two errors differ in + diagnostic value. + +- **AC-TASKS-QUORUM-RECORDING-001.11:** WHEN the decision tool returns successfully, THE SYSTEM SHALL return + the seven fields enumerated in `## Tool contract` — `decision`, `role`, + `step_id`, `decision_id`, `decided_at`, `transition_applied` and `guards` — + and SHALL NOT return a scalar `required_count` or `received_count`. + `guards` SHALL equal the AC-TASKS-QUORUM-REEVALUATION-001.14 snapshot's `Guards` for the validated step, + in the same order, so the tool and the AC-TASKS-QUORUM-DIAGNOSTICS-001.4 endpoint report the same + arithmetic. `guards` SHALL be an empty list, not an error and not omitted, + when the validated step configures no guarded transition. + WHEN the post-write AC-TASKS-QUORUM-REEVALUATION-001.14 snapshot cannot be computed — the AC-TASKS-QUORUM-REEVALUATION-001.3 case, where + the write succeeded and the re-evaluation errored — THE SYSTEM SHALL return + `guards` as an empty list and `transition_applied` as `false`, and SHALL still + report the call as successful per AC-TASKS-QUORUM-REEVALUATION-001.3, the failure being surfaced through + AC-TASKS-QUORUM-DIAGNOSTICS-001.2 rather than to the agent. A diagnostic read that failed SHALL NOT put the + recorded verdict at risk. + This AC exists because without it only two of the returned fields — `step_id` + and `transition_applied`, via AC-TASKS-QUORUM-BINDING-001.2 and AC-TASKS-QUORUM-CONCURRENCY-001.4 — were observable by any + acceptance criterion, so a wrong `role` or a wrong count could ship untested. + +## Out of scope + + + +Behavior outside this acceptance-criteria group is defined by the core quorum requirement and the other quorum capability requirements. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/workflow-quorum-decision-recording-concurrency.md b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-concurrency.md new file mode 100644 index 0000000000..220f30a0d4 --- /dev/null +++ b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-concurrency.md @@ -0,0 +1,131 @@ +--- +status: draft +system: tasks +created: 2026-08-19 +owners: + - kandev +--- + + +# Quorum ordering and concurrency Requirements + + + +## Overview + + + +Concurrent verdicts and transition races preserve decision history and apply a guarded transition at most once. + +## Requirements + + +### REQ-TASKS-QUORUM-CONCURRENCY-001: Quorum ordering and concurrency + + + +**Intent:** Concurrent verdicts and transition races preserve decision history and apply a guarded transition at most once. + +#### Acceptance criteria + +#### Ordering, idempotency, concurrency + +- **AC-TASKS-QUORUM-CONCURRENCY-001.1:** THE SYSTEM SHALL order decisions for threshold evaluation by + `decided_at` ascending, tie-broken by `id` ascending — the existing + `ListStepDecisions` contract (`phase2_sqlite.go:594`) — and SHALL count only + the last row per **AC-TASKS-QUORUM-SLATE-001.9 seat** under that ordering, decisions being matched to + seats by AC-TASKS-QUORUM-SLATE-001.10. This clause governs the approve-style thresholds + (`all_approve`, `all_decide`, `majority_approve`, `n_approve:`) ONLY; + `any_reject` is keyed on decider identity and needs no seat, per AC-TASKS-QUORUM-VERDICT-001.3 and + AC-TASKS-QUORUM-VERDICT-001.4. The scoping is explicit because an unqualified "last row per + `participant_id`" is precisely the reading that reintroduces the + discarded-human-rejection defect AC-TASKS-QUORUM-VERDICT-001.3 exists to fix — production funnels every + threshold through one `latestDecisionsPerParticipant` call today, so reusing + this rule for `any_reject` is the path of least resistance. +- **AC-TASKS-QUORUM-CONCURRENCY-001.2:** WHEN the same participant records a second decision for the same + `(task_id, step_id)`, THE SYSTEM SHALL mark the prior row `superseded_at` and + insert the new one in a single transaction, and the new verdict SHALL replace + the old one for threshold purposes. +- **AC-TASKS-QUORUM-CONCURRENCY-001.3:** WHEN two decisions are recorded concurrently for the same + `(task_id, step_id)` by different participants, THE SYSTEM SHALL persist both, + and at most one guarded transition SHALL be applied for the resulting step + change. The engine's operation-id mechanism CANNOT provide this: + `HandleTrigger` dedupes by exact `OperationID` match only + (`isOperationAlreadyApplied`) and offers no mutual exclusion across distinct + ids, and two concurrent decisions necessarily observe different decision sets + and so compute different ids. Mutual exclusion SHALL therefore be enforced at + apply time per AC-TASKS-QUORUM-CONCURRENCY-001.4, not by the operation id. +- **AC-TASKS-QUORUM-CONCURRENCY-001.4:** WHEN a transition is applied after a quorum re-evaluation, THE + SYSTEM SHALL apply it only if the task's `workflow_step_id` still equals the + step the guard was evaluated against, and SHALL abandon the apply otherwise. + AC-TASKS-QUORUM-CONCURRENCY-001.10 defines the port this needs and why the existing one cannot express it. + The loser of a concurrent race SHALL report `transition_applied = false` with + its decision still persisted, and SHALL NOT be reported as an error: losing + the race is a normal outcome, not a failure. +- **AC-TASKS-QUORUM-CONCURRENCY-001.5:** WHEN two decisions are recorded concurrently by the *same* + participant for the same `(task_id, step_id)`, THE SYSTEM SHALL leave exactly + one non-superseded row for that participant. +- **AC-TASKS-QUORUM-CONCURRENCY-001.6:** WHEN a re-evaluation is triggered twice for the same recorded + decision, THE SYSTEM SHALL apply the transition at most once, keyed on a + **deterministic** operation id of the form + `decision:::`. Determinism is the whole + point: `Engine.RecordParticipantDecision` currently builds its operation id + with a `time.Now().UnixNano()` suffix, which yields a fresh id on every call + and therefore deduplicates nothing. That existing construction SHALL NOT be + reused as-is. The repo's convention for this primitive is a deterministic + content-derived key — see `childCompletionOperationID` + (`orchestrator/event_handlers_children_completed.go`). +- **AC-TASKS-QUORUM-CONCURRENCY-001.7:** THE SYSTEM SHALL NOT accept a client-supplied idempotency key on the + decision tool. A repeated call is a new verdict and supersedes the previous one + per AC-TASKS-QUORUM-CONCURRENCY-001.2. Consequence, stated so a builder does not quietly invent a key: an + agent that retries after a timeout on a call that actually succeeded records a + second verdict rather than a no-op. This is acceptable because the verdict is + the same value and AC-TASKS-QUORUM-CONCURRENCY-001.2 leaves exactly one non-superseded row; it is NOT + acceptable to paper over it with a synthesized key derived from the arguments, + which would make a deliberate change-of-mind silently fail. +- **AC-TASKS-QUORUM-CONCURRENCY-001.8:** THE SYSTEM SHALL generate the decision row id BEFORE the write and + pass it in `DecisionInfo.ID`, following the existing office precedent + (`recordTaskDecision` sets `ID: uuid.New().String()` on the row it hands to + `RecordStepDecision`). It SHALL NOT follow `Engine.RecordParticipantDecision`, + which leaves `ID` blank and lets the repository generate one. The + `DecisionStore.RecordStepDecision(ctx, d DecisionInfo) error` signature is + UNCHANGED by this feature. + The two in-tree precedents disagree and only one is usable: the port returns + no id, so a blank-`ID` write leaves AC-TASKS-QUORUM-CONCURRENCY-001.6's operation id + `decision:::` unconstructible, and AC-TASKS-QUORUM-REEVALUATION-001.11 + unsatisfiable since office would have no id to publish. Recovering the id by + reading the row back after the write is NOT permitted — under the AC-TASKS-QUORUM-CONCURRENCY-001.3 + concurrency this must coexist with, a read-back can select a different + decider's row for the same `(task_id, step_id)`. Pre-generation also makes the + operation id available before the write, which is what lets AC-TASKS-QUORUM-REEVALUATION-001.7 mark it + applied strictly after the apply. +- **AC-TASKS-QUORUM-CONCURRENCY-001.9:** WHEN a task re-enters a guarded step, THE SYSTEM SHALL clear that + step's decisions before the round's participants are queued, so a new round + starts with zero decisions — the existing `clear_decisions` ordering on + `on_enter`. AC-TASKS-QUORUM-REGRESSION-001.6 makes this observable rather than assumed, and names what it + means when it does not hold. + +- **AC-TASKS-QUORUM-CONCURRENCY-001.10:** THE SYSTEM SHALL add exactly one method to the engine's transition + port — `ApplyTransitionIfAtStep(ctx, taskID, sessionID, expectedStepID, + toStepID, trigger) (applied bool, err error)` — used ONLY by the AC-TASKS-QUORUM-CONCURRENCY-001.4 quorum + apply, and SHALL leave the existing `ApplyTransition(...) error` and every + transition that uses it semantically unchanged. `HandleResult` SHALL gain an + additive `TransitionAbandoned` field so an abandoned apply is distinguishable + from both a transition and an error. This is required because AC-TASKS-QUORUM-CONCURRENCY-001.4's outcome + is not expressible today: `ApplyTransition` receives `fromStepID` and ignores + it, its orchestrator implementation unconditionally sets + `task.WorkflowStepID = toStepID`, and a single `error` return cannot separate + "lost the race" from "the write failed". The compare-and-swap SHALL read the + task's `workflow_step_id` inside the same transaction that performs the update + — `updateTaskWithWorkflowStepAdmission` already opens one and already takes the + workspace and workflow-step locks — so no new locking primitive is introduced + and no non-quorum transition changes behavior. `applied = true` means the task + left `expectedStepID`; it does NOT mean the task was WIP-admitted at the + target, because a step at capacity queues the task there and that is still a + completed transition. + +## Out of scope + + + +Behavior outside this acceptance-criteria group is defined by the core quorum requirement and the other quorum capability requirements. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/workflow-quorum-decision-recording-diagnostics.md b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-diagnostics.md new file mode 100644 index 0000000000..e0a21ec635 --- /dev/null +++ b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-diagnostics.md @@ -0,0 +1,195 @@ +--- +status: draft +system: tasks +created: 2026-08-19 +owners: + - kandev +--- + + +# Quorum diagnostics Requirements + + + +## Overview + + + +A task exposes enough live quorum state to distinguish an unmet threshold from a guard that cannot succeed. + +## Requirements + + +### REQ-TASKS-QUORUM-DIAGNOSTICS-001: Quorum diagnostics + + + +**Intent:** A task exposes enough live quorum state to distinguish an unmet threshold from a guard that cannot succeed. + +#### Acceptance criteria + +#### Distinguishing waiting from stuck + +- **AC-TASKS-QUORUM-DIAGNOSTICS-001.1:** WHEN a guarded transition does not fire, THE SYSTEM SHALL record + exactly one reason code from this closed set: + `threshold_not_met`, `slate_empty`, `decision_store_unwired`, + `participant_store_unwired`, `guard_variant_unrecognized`, + `threshold_unrecognized`, `threshold_unsatisfiable`, `evaluation_error`, and + `session_unresolvable`. + `decision_store_unwired` and `participant_store_unwired` are separate because + `evaluateWaitForQuorum` SHALL nil-check them separately — today it does not, + the current code being a single combined + `if e.decisions == nil || e.participants == nil { return false, nil }`, so + splitting that branch in two is a REQUIRED change of this feature and not an + existing property to be relied on. Left unsplit, the two codes are + indistinguishable in practice and AC-TASKS-QUORUM-DIAGNOSTICS-001.1's taxonomy silently loses a member; + `threshold_unsatisfiable` carries AC-TASKS-QUORUM-BINDING-001.5, the case where the threshold can + never be met by any future decision and the card is stuck rather than waiting; + `evaluation_error` carries a genuine error returned by + `ListStepParticipants` or `ListStepDecisions`; `session_unresolvable` carries + AC-TASKS-QUORUM-REEVALUATION-001.5. `threshold_unrecognized` carries AC-TASKS-QUORUM-DIAGNOSTICS-001.8, a threshold string + the evaluator does not recognize at all. The set is closed rather than "at + minimum" so that AC-TASKS-QUORUM-DIAGNOSTICS-001.3's expvar keys are enumerable and a new reason cannot + appear unlabelled. More than one condition can hold at once; AC-TASKS-QUORUM-DIAGNOSTICS-001.7 fixes which + single code is reported. +- **AC-TASKS-QUORUM-DIAGNOSTICS-001.2:** THE SYSTEM SHALL emit, each time a guarded transition does not fire + **on the engine's transition-evaluation path**, a structured log record + carrying the task id, step id, guard role, + threshold, required-participant count, decisions-received count, the AC-TASKS-QUORUM-DIAGNOSTICS-001.1 + reason code, and an `error` field which is populated when and only when the + reason is `evaluation_error`. The engine holds no logger today, so wiring one + is part of this feature. +- **AC-TASKS-QUORUM-DIAGNOSTICS-001.3:** THE SYSTEM SHALL expose counts of not-fired guard evaluations under + `/debug/vars`, keyed by the AC-TASKS-QUORUM-DIAGNOSTICS-001.1 reason, following the existing `workflow_*` + expvar convention documented in the root `CLAUDE.md`. Like AC-TASKS-QUORUM-DIAGNOSTICS-001.2, this counts + **engine-path evaluations only**: the AC-TASKS-QUORUM-DIAGNOSTICS-001.4 diagnostic endpoint evaluates + guards read-only and SHALL emit no log record and increment no counter. Stated + because AC-TASKS-QUORUM-DIAGNOSTICS-001.9 requires that endpoint to evaluate every guard live, so the two + ACs otherwise overlap: were diagnostic reads counted, these counters would + measure UI polling rather than workflow health, and a card left open in a + browser would out-count a genuinely stuck one. +- **AC-TASKS-QUORUM-DIAGNOSTICS-001.4:** THE SYSTEM SHALL expose guard state for a task at + `GET /api/v1/office/tasks/:id/quorum`, a sibling of the existing + `GET /api/v1/office/tasks/:id/decisions` route + (`office/dashboard/handler.go`) and carrying the same authorization as that + route. The response SHALL contain one entry per guarded `on_turn_complete` + transition configured at the task's current step, each with: `target_step_id`, + `role`, `threshold`, `required_count`, `received_count`, `satisfied` (a + boolean), `reason` (an AC-TASKS-QUORUM-DIAGNOSTICS-001.1 code, present if and only if `satisfied` is + `false` — see AC-TASKS-QUORUM-DIAGNOSTICS-001.10), and `error` (populated only for `evaluation_error`). A + task at a step with no guarded transition SHALL return an empty list, not a + 404. Entries are computed live per AC-TASKS-QUORUM-DIAGNOSTICS-001.9, which also defines the payload's + top-level `reevaluation_blocked` field. The payload SHALL be a direct + projection of the AC-TASKS-QUORUM-REEVALUATION-001.14 snapshot — that is the only path by which this + handler may reach the slate machinery, since AC-TASKS-QUORUM-REEVALUATION-001.9 forbids it from carrying + its own copy — preserving AC-TASKS-QUORUM-REEVALUATION-001.14's ordering. AC-TASKS-QUORUM-DIAGNOSTICS-001.11 defines how the card + renders when entries disagree. +- **AC-TASKS-QUORUM-DIAGNOSTICS-001.5:** WHEN the task has no bound `workflow_step_id`, the AC-TASKS-QUORUM-DIAGNOSTICS-001.4 endpoint + SHALL return an empty list rather than an error, so a diagnostic read never + fails on the state it exists to diagnose. +- **AC-TASKS-QUORUM-DIAGNOSTICS-001.6:** WHEN the task's AC-TASKS-QUORUM-DIAGNOSTICS-001.4 entries aggregate to `awaiting` under AC-TASKS-QUORUM-DIAGNOSTICS-001.11, + the Office task detail view SHALL render the task as awaiting decisions — + showing `received_count` of `required_count` for the role of the entry that + AC-TASKS-QUORUM-DIAGNOSTICS-001.11 selects — and SHALL NOT render it as failed or errored. When they + aggregate to `stuck`, the view SHALL render a visually distinct stuck state + naming the selected entry's AC-TASKS-QUORUM-DIAGNOSTICS-001.1 reason. This is the whole diagnostic gap: + the two states are byte-identical today. AC-TASKS-QUORUM-DIAGNOSTICS-001.11 exists because AC-TASKS-QUORUM-DIAGNOSTICS-001.4 returns a + LIST and `Office Default`'s Review configures two guards, so "the reason" is + not well defined without an aggregation rule. + +- **AC-TASKS-QUORUM-DIAGNOSTICS-001.7:** WHEN more than one AC-TASKS-QUORUM-DIAGNOSTICS-001.1 condition holds at once, THE SYSTEM SHALL + report the first that applies in this order, which mirrors the order the code + discovers them: `guard_variant_unrecognized`, `decision_store_unwired`, + `participant_store_unwired`, `evaluation_error`, `slate_empty`, + `threshold_unrecognized`, `threshold_unsatisfiable`, `threshold_not_met`. + AC-TASKS-QUORUM-DIAGNOSTICS-001.1 requires exactly one code, and AC-TASKS-QUORUM-SLATE-001.6 and AC-TASKS-QUORUM-BINDING-001.5 both fire for an empty + slate with any positive `n_approve:`, since every positive N exceeds a slate + of zero. `slate_empty` wins: it is the more actionable diagnosis, and + `evaluateWaitForQuorum` already returns at `len(required) == 0` before the + threshold is examined. The ninth code, `session_unresolvable`, is not in this + ordering because it is not a guard-evaluation outcome at all — it is the + recording-time skip of AC-TASKS-QUORUM-REEVALUATION-001.5, surfaced through AC-TASKS-QUORUM-DIAGNOSTICS-001.2/AC-TASKS-QUORUM-DIAGNOSTICS-001.3 and, on the + AC-TASKS-QUORUM-DIAGNOSTICS-001.4 payload, through the separate field AC-TASKS-QUORUM-DIAGNOSTICS-001.9 defines. `slate_empty` is + reachable only for approve-style thresholds, per AC-TASKS-QUORUM-SLATE-001.6 as scoped and AC-TASKS-QUORUM-SLATE-001.11; an + `any_reject` guard over an empty slate reports `threshold_not_met`, because a + seatless rejection can still arrive and so the card is waiting, not stuck. +- **AC-TASKS-QUORUM-DIAGNOSTICS-001.8:** WHEN the guard's threshold is a string that is neither one of the + five recognized thresholds nor prefixed `n_approve:`, THE SYSTEM SHALL not fire + the transition and SHALL report AC-TASKS-QUORUM-DIAGNOSTICS-001.1 reason `threshold_unrecognized`, which + AC-TASKS-QUORUM-DIAGNOSTICS-001.6 renders as stuck. `applyThreshold` returns false from its final + fall-through for such a value today, indistinguishably from + `threshold_not_met`, so a mistyped threshold would render as "awaiting + decisions" forever — the exact diagnostic gap this feature exists to close. + `guard_variant_unrecognized` is reserved for a guard whose variant is not + `wait_for_quorum` at all, matching `evaluateTransitionGuard`'s fail-closed + branch. +- **AC-TASKS-QUORUM-DIAGNOSTICS-001.9:** THE SYSTEM SHALL compute AC-TASKS-QUORUM-DIAGNOSTICS-001.4's per-guard entries by evaluating + each guarded `on_turn_complete` transition configured at the task's current + step **live, at request time, and independently** — not by replaying the + engine's transition loop, and not by reading a stored evaluation. Independent + evaluation is required because `evaluateActions` short-circuits, evaluating a + guard only while `targetStepID == ""`, so a replay cannot enumerate every + guard the way AC-TASKS-QUORUM-DIAGNOSTICS-001.4 requires. A per-guard `reason` SHALL therefore never be + `session_unresolvable`, which is a recording-time skip rather than an + evaluation outcome; the AC-TASKS-QUORUM-DIAGNOSTICS-001.4 payload SHALL instead carry a top-level + `reevaluation_blocked` boolean, defined by AC-TASKS-QUORUM-DIAGNOSTICS-001.12, and AC-TASKS-QUORUM-DIAGNOSTICS-001.11 SHALL treat it as + stuck-distinct. The field SHALL be `false` when the task has no decisions at + its current step, so an untouched task is never rendered as stuck. + +- **AC-TASKS-QUORUM-DIAGNOSTICS-001.10:** THE SYSTEM SHALL set an AC-TASKS-QUORUM-DIAGNOSTICS-001.4 entry's `satisfied` to `true` when + that guard's threshold is met at request time, and SHALL omit `reason` + entirely for such an entry. AC-TASKS-QUORUM-DIAGNOSTICS-001.1's set is a taxonomy of why a guard did NOT + fire and SHALL NOT gain a "satisfied" member, so AC-TASKS-QUORUM-DIAGNOSTICS-001.3's expvar keys stay + enumerable. The state is reachable rather than theoretical: under AC-TASKS-QUORUM-REEVALUATION-001.5 a + verdict is recorded, re-evaluation is skipped, and the task sits at the step + with its quorum already met. AC-TASKS-QUORUM-REEVALUATION-001.5 is the whole of it. An earlier draft also + claimed the AC-TASKS-QUORUM-CONCURRENCY-001.4 abandon as a second route; it is not one, and the claim is + removed rather than left to send a builder hunting: AC-TASKS-QUORUM-CONCURRENCY-001.4 abandons PRECISELY + when the task's `workflow_step_id` no longer equals the evaluated step — the + race winner has already moved it — and AC-TASKS-QUORUM-DIAGNOSTICS-001.4 evaluates at the task's CURRENT + step, so the abandoned guard is not in the returned list at all. +- **AC-TASKS-QUORUM-DIAGNOSTICS-001.11:** THE SYSTEM SHALL aggregate a task's AC-TASKS-QUORUM-DIAGNOSTICS-001.4 entries to exactly one + card-level state: `stuck` when `reevaluation_blocked` is `true` or when ANY + entry is unsatisfied with a `reason` other than `threshold_not_met`; + otherwise `awaiting` when any entry is unsatisfied; otherwise `clear`. When + the state is `stuck` the SELECTED entry is the first such entry in the step's + configured `on_turn_complete` action order, matching AC-TASKS-QUORUM-REEVALUATION-001.6's + first-transition-wins; when `awaiting`, it is the first unsatisfied entry in + that same order. Stuck outranks awaiting because a stuck guard is the + actionable diagnosis and MUST NOT be masked by a sibling that is merely + waiting — a mistyped `n_approve:` reporting `threshold_unsatisfiable` + beside a healthy `any_reject` reporting `threshold_not_met` is the concrete + case, and rendering that card as "awaiting decisions" forever is the exact + defect AC-TASKS-QUORUM-DIAGNOSTICS-001.8 was added to close. Boundary: an EMPTY entry list aggregates to + `clear`, which is the correct rendering for both a step with no guarded + transition and the AC-TASKS-QUORUM-DIAGNOSTICS-001.5 unbound-step case — neither is stuck, and neither is + waiting on a decision. +- **AC-TASKS-QUORUM-DIAGNOSTICS-001.12:** THE SYSTEM SHALL compute `reevaluation_blocked` live at request time + as the conjunction of: the task has at least one non-superseded decision at + its current step, AND the AC-TASKS-QUORUM-REEVALUATION-001.4 session query returns no row. It SHALL NOT be + read from a stored flag. + The first conjunct SHALL be evaluated as "`ListStepDecisions` returns a + non-empty list for (task, current step)". That is equivalent, and it is the + form the engine can actually express: a row is superseded only when a + replacement is inserted for the same decider, so a step holding at least one + row always holds at least one non-superseded row. Stating the equivalence + matters because the engine's `DecisionInfo` projection carries no + `superseded_at` — the same limitation AC-TASKS-QUORUM-VERDICT-001.4 turns into last-row-wins — so a + literal reading of "non-superseded" would be a predicate the evaluator cannot + compute. + This is a CURRENT-STATE predicate, not a history of AC-TASKS-QUORUM-REEVALUATION-001.5 skips, and the + difference is deliberate. A stored "the last recording skipped" flag would keep + rendering a card as stuck after a session returned and the card became healthy, + and reading a stored evaluation is the very thing AC-TASKS-QUORUM-DIAGNOSTICS-001.9 forbids for the + per-guard entries. The live predicate also needs no ordering rule, no tiebreak + and no new column, so the "most recent decision" ambiguity does not arise; two + concurrent reads compute the same value from committed state. + Naming: the field is `reevaluation_blocked`, NOT `last_reevaluation_skipped`, + because it asserts something different from the original and a reader must not + carry the historical reading across the rename. + +## Out of scope + + + +Behavior outside this acceptance-criteria group is defined by the core quorum requirement and the other quorum capability requirements. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/workflow-quorum-decision-recording-participant-slate.md b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-participant-slate.md new file mode 100644 index 0000000000..8ff9e95780 --- /dev/null +++ b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-participant-slate.md @@ -0,0 +1,137 @@ +--- +status: draft +system: tasks +created: 2026-08-19 +owners: + - kandev +--- + + +# Quorum participant slate Requirements + + + +## Overview + + + +The quorum evaluator uses the canonical required participants for the task and evaluating step. + +## Requirements + + +### REQ-TASKS-QUORUM-SLATE-001: Quorum participant slate + + + +**Intent:** The quorum evaluator uses the canonical required participants for the task and evaluating step. + +#### Acceptance criteria + +#### Participant slate + +- **AC-TASKS-QUORUM-SLATE-001.1:** WHEN a quorum guard evaluates at a step, THE SYSTEM SHALL count a + **per-task** participant row (`task_id != ''`) with `decision_required = 1` + and a matching role even when that row was attached under a different step of + the same workflow. +- **AC-TASKS-QUORUM-SLATE-001.2:** THE SYSTEM SHALL NOT extend cross-step counting to template-level + rows (`task_id = ''`). A template row is bound to one step by workflow design, + so it remains visible only at its own step. Without this, a Work-step template + reviewer would be pulled into a Review-step quorum. +- **AC-TASKS-QUORUM-SLATE-001.3:** THE SYSTEM SHALL identify a participant by the natural key + `(task_id, role, agent_profile_id)`, and SHALL select one **canonical row** + per key: the row whose `step_id` equals the evaluating step when such a row + exists, otherwise the row with the lowest `id` in ASCII order among that key's + rows. `workflow_step_participants` carries no timestamp column, so `id` ASC is + the tiebreak — "most recent" is not expressible here. The same rule SHALL be + used by slate construction (AC-TASKS-QUORUM-SLATE-001.1), by role resolution (AC-TASKS-QUORUM-RECORDING-001.5), and by the + `participant_id` written on a decision (AC-TASKS-QUORUM-RECORDING-001.2), so that the read side and the + write side cannot disagree. AC-TASKS-QUORUM-SLATE-001.3 is step 3 of AC-TASKS-QUORUM-SLATE-001.9; the id that finally + identifies a participant is the one surviving AC-TASKS-QUORUM-SLATE-001.9 step 4, because AC-TASKS-QUORUM-SLATE-001.5's + template-versus-per-task collapse keys on `(role, agent_profile_id)` and can + still discard an AC-TASKS-QUORUM-SLATE-001.3 canonical row. +- **AC-TASKS-QUORUM-SLATE-001.4:** WHEN both a step-specific row and a differently-stepped row exist + for the same `(task_id, role, agent_profile_id)`, THE SYSTEM SHALL count the + participant exactly once, as the AC-TASKS-QUORUM-SLATE-001.3 canonical row. This shape is produced + in normal operation: `AddTaskParticipant`'s idempotency probe is scoped to + `step_id`, so re-attaching the same agent after a step change inserts a second + row rather than matching the first. +- **AC-TASKS-QUORUM-SLATE-001.5:** THE SYSTEM SHALL continue to honor template-level rows + (`task_id = ''`) with per-task rows taking precedence on + `(role, agent_profile_id)`, per `phase2_sqlite.go:20-27`. +- **AC-TASKS-QUORUM-SLATE-001.6:** WHEN the required slate for the guard's role is empty AND the + guard's threshold is an approve-style one (`all_approve`, `all_decide`, + `majority_approve`, `n_approve:`), THE SYSTEM SHALL NOT fire the + transition, and SHALL report the empty slate distinctly from an unmet + threshold per AC-TASKS-QUORUM-DIAGNOSTICS-001.1. The scoping to approve-style thresholds is required by + AC-TASKS-QUORUM-SLATE-001.11; an unscoped empty-slate short-circuit makes the AC-TASKS-QUORUM-VERDICT-001.3 veto unreachable. +- **AC-TASKS-QUORUM-SLATE-001.7:** WHEN a decision is on file for a participant who WAS in the required + slate and has since been removed, THE SYSTEM SHALL ignore that decision for + the counting thresholds, preserving the documented mid-flight removal + behavior in `applyThreshold`. This governs removed slate members only; it does + NOT govern deciders who never held a seat, whose rejections are handled by + AC-TASKS-QUORUM-VERDICT-001.3 and whose approvals are excluded by AC-TASKS-QUORUM-VERDICT-001.6. + +- **AC-TASKS-QUORUM-SLATE-001.8:** THE SYSTEM SHALL extend the engine's `ParticipantStore` port with a + task-scoped read — `ListTaskParticipants(ctx, taskID)`, returning every + per-task row (`task_id = ?`) for that task irrespective of `step_id` — and + SHALL leave `ListStepParticipants(ctx, stepID, taskID)` unchanged. AC-TASKS-QUORUM-SLATE-001.1's + cross-step counting is not expressible through the existing port, whose only + production query is `WHERE step_id = ? AND (task_id = '' OR task_id = ?)`; + widening that predicate in place would also pull template rows across steps, + which AC-TASKS-QUORUM-SLATE-001.2 forbids. A task with no per-task rows SHALL yield an empty list, not an + error, matching `ListStepParticipants`'s documented empty-is-valid contract. + This is the participant-side twin of AC-TASKS-QUORUM-VERDICT-001.5 — the projection and the port both + have to be named, or the builder invents them. +- **AC-TASKS-QUORUM-SLATE-001.9:** THE SYSTEM SHALL build the required slate for a guard, at the + evaluating step, in exactly this order: + 1. **Gather** — per-task rows for the task via AC-TASKS-QUORUM-SLATE-001.8 (any step), plus + template rows (`task_id = ''`) at the evaluating step only, per AC-TASKS-QUORUM-SLATE-001.2. + 2. **Filter** — keep rows whose `role` matches the guard and whose + `decision_required` is true, per `requiredParticipants`. + 3. **Canonicalize** — collapse to one row per `(task_id, role, + agent_profile_id)` by AC-TASKS-QUORUM-SLATE-001.3. + 4. **Collapse** — collapse across `task_id` to one row per + `(role, agent_profile_id)`, the per-task row winning, per AC-TASKS-QUORUM-SLATE-001.5. + + The row surviving step 4 is THE **seat**; its `id` is the slate id that AC-TASKS-QUORUM-RECORDING-001.2 + writes and AC-TASKS-QUORUM-CONCURRENCY-001.1 counts. The order is stated because AC-TASKS-QUORUM-SLATE-001.3 keys on + `(task_id, role, agent_profile_id)` while AC-TASKS-QUORUM-SLATE-001.5 keys on + `(role, agent_profile_id)`: running AC-TASKS-QUORUM-SLATE-001.3 alone seats one agent twice when it + holds both a template row and a per-task row, inflating `totalRequired` so + `all_approve` can never be met, and running AC-TASKS-QUORUM-SLATE-001.5 alone discards AC-TASKS-QUORUM-SLATE-001.3's + cross-step canonicalization. Boundary: a row whose `agent_profile_id` is empty + has no identity to canonicalize on and SHALL be kept as its own seat, keyed by + its row `id`, at both step 3 and step 4 — collapsing two such rows into one + seat would under-count `totalRequired` and let `all_approve` fire on a single + approval. +- **AC-TASKS-QUORUM-SLATE-001.10:** WHEN counting decisions against the slate, THE SYSTEM SHALL map each + decision to at most one AC-TASKS-QUORUM-SLATE-001.9 seat: by `participant_id` when that id is a seat + id, otherwise by the seat whose `(role, agent_profile_id)` equals the + decision's `(role, decider_id)` when `decider_type` = `agent`. A decision that + maps to no seat is not counted toward any approve-style threshold, per AC-TASKS-QUORUM-VERDICT-001.6; + `any_reject` does not use this mapping at all, per AC-TASKS-QUORUM-VERDICT-001.3. The fallback exists + because a participant's AC-TASKS-QUORUM-SLATE-001.3 canonical row can CHANGE after that participant + has already decided — AC-TASKS-QUORUM-SLATE-001.4 states the two-row shape arises in normal + operation, so attaching a current-step row for an agent who decided while only + an earlier-step row existed flips the seat id, and an id-only match would + silently discard a real verdict, which is the discard class AC-TASKS-QUORUM-RECORDING-001.2 exists to + prevent. AC-TASKS-QUORUM-SLATE-001.7 is unaffected: a participant removed from the slate entirely has + no seat under either match, so its decision is still ignored. +- **AC-TASKS-QUORUM-SLATE-001.11:** WHEN the required slate is empty, THE SYSTEM SHALL still evaluate an + `any_reject` guard and SHALL NOT short-circuit it. The empty-slate fail-closed + of AC-TASKS-QUORUM-SLATE-001.6 exists because `all_approve` is `approveCount == totalRequired`, + which is vacuously TRUE at `0 == 0` — an empty slate would otherwise advance a + task nobody reviewed. `any_reject` is `rejectCount > 0` and cannot fire + vacuously, so the same guard rail is unnecessary there and actively harmful: + it makes the AC-TASKS-QUORUM-VERDICT-001.3 seatless veto unreachable in exactly the case where every + decider is seatless. Implementation consequence, stated because this is not a + free change: `evaluateWaitForQuorum` returns at `len(required) == 0` before + `ListStepDecisions` is called, so the empty-slate check must become + threshold-aware rather than remaining a precondition. + +## Out of scope + + + +Behavior outside this acceptance-criteria group is defined by the core quorum requirement and the other quorum capability requirements. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/workflow-quorum-decision-recording-reevaluation.md b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-reevaluation.md new file mode 100644 index 0000000000..637e1aca20 --- /dev/null +++ b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-reevaluation.md @@ -0,0 +1,238 @@ +--- +status: draft +system: tasks +created: 2026-08-19 +owners: + - kandev +--- + + +# Decision-triggered quorum re-evaluation Requirements + + + +## Overview + + + +Recording a verdict causes the current step's guarded transitions to be evaluated with clear success, skip, and failure outcomes. + +## Requirements + + +### REQ-TASKS-QUORUM-REEVALUATION-001: Decision-triggered quorum re-evaluation + + + +**Intent:** Recording a verdict causes the current step's guarded transitions to be evaluated with clear success, skip, and failure outcomes. + +#### Acceptance criteria + +#### Re-evaluation on record + +- **AC-TASKS-QUORUM-REEVALUATION-001.1:** WHEN a decision is recorded through any surface, THE SYSTEM SHALL + re-evaluate the current step's `on_turn_complete` guarded transitions before + returning to the caller, using the task's active session as defined in AC-TASKS-QUORUM-REEVALUATION-001.4 + — including on the human HTTP path, which has no calling session of its own. +- **AC-TASKS-QUORUM-REEVALUATION-001.2:** WHEN that re-evaluation finds a satisfied guard, THE SYSTEM SHALL + apply the transition, and the recording call SHALL still report success. +- **AC-TASKS-QUORUM-REEVALUATION-001.3:** WHEN that re-evaluation fails or errors, THE SYSTEM SHALL keep the + recorded decision, report the recording as successful, and surface the + re-evaluation failure through the observability required by AC-TASKS-QUORUM-DIAGNOSTICS-001.2. +- **AC-TASKS-QUORUM-REEVALUATION-001.4:** THE SYSTEM SHALL resolve the re-evaluation session as + `GetActiveTaskSessionByTaskID` already defines it + (`task/repository/sqlite/session.go`): the task's most recently started + session whose state is one of `CREATED`, `STARTING`, `RUNNING`, or + `WAITING_FOR_INPUT`, ordered by `started_at` descending, limit one. A task is + "unresolvable" for this purpose exactly when that query returns no row. +- **AC-TASKS-QUORUM-REEVALUATION-001.5:** WHEN no session is resolvable under AC-TASKS-QUORUM-REEVALUATION-001.4, THE SYSTEM SHALL record + the decision and skip re-evaluation without erroring, matching the existing + blank-session behavior in `Engine.RecordParticipantDecision`, and SHALL report + the skip under AC-TASKS-QUORUM-DIAGNOSTICS-001.1 so a card that recorded a verdict but never re-evaluated + is distinguishable from one that re-evaluated and found the threshold unmet. +- **AC-TASKS-QUORUM-REEVALUATION-001.6:** WHEN two guarded transitions are both satisfied, THE SYSTEM SHALL + apply the first in the step's configured `on_turn_complete` action order, + preserving first-transition-wins (`workflow/engine/engine.go:268-283`). + +- **AC-TASKS-QUORUM-REEVALUATION-001.7:** THE SYSTEM SHALL run the AC-TASKS-QUORUM-REEVALUATION-001.1 re-evaluation with the engine in + **committing mode** (`EvaluateOnly = false`), so the engine applies a satisfied + transition itself rather than returning a payload for a transport to apply. + Consequently the AC-TASKS-QUORUM-CONCURRENCY-001.6 operation id SHALL be marked applied only after the + transition has been applied, or deliberately abandoned under AC-TASKS-QUORUM-CONCURRENCY-001.4 — never + before. This is stated because the two precedents in tree disagree and only one + is safe: `Engine.RecordParticipantDecision` passes `EvaluateOnly: true` today + while `office/engine_dispatcher` does not, and `HandleTrigger` calls + `markOperationApplied` on its success path only. Under committing mode an apply + failure returns before the mark, so a retry can still land; under evaluate-only + the id is marked while no transition has been applied, and every retry then + returns `Idempotent: true` with the transition lost permanently — the exact + stuck-card failure this feature exists to eliminate. The existing + `EvaluateOnly: true` on `RecordParticipantDecision` SHALL therefore change. + Keeping the apply inside the engine is also what `## What` means by ownership + following the engine. "Committing mode" here names APPLY-OWNERSHIP — the + engine performs the apply and marks the operation id after it — and NOT reuse + of the full trigger path; AC-TASKS-QUORUM-REEVALUATION-001.8 bounds what the re-evaluation is allowed to + run. + +- **AC-TASKS-QUORUM-REEVALUATION-001.8:** THE SYSTEM SHALL scope the AC-TASKS-QUORUM-REEVALUATION-001.1 re-evaluation to the step's guarded + transition actions ONLY. The AC-TASKS-QUORUM-REEVALUATION-001.10 entry point SHALL evaluate those + `on_turn_complete` actions for which `isTransitionAction` holds and which + carry a `wait_for_quorum` guard, in configured order, and apply at most one, + per AC-TASKS-QUORUM-REEVALUATION-001.6 and AC-TASKS-QUORUM-CONCURRENCY-001.4. It SHALL NOT execute non-transition action callbacks, and + it SHALL NOT be implemented as a plain + `HandleTrigger(TriggerOnTurnComplete)` call. + This is stated because the obvious implementation is exactly the wrong one. + `evaluateActions` guards only actions for which `isTransitionAction` holds and + routes EVERY other action to `executeCallback` — and that call is not gated on + `EvaluateOnly`, unlike `applyTransition` and `PersistData`. So a + HandleTrigger-based re-evaluation re-runs whatever else the step configures on + `on_turn_complete` — `queue_run`, `clear_decisions`, + `queue_run_for_each_participant`, `run_code_review`, `create_child_task`, + `switch_workflow`, `set_workflow_data` are all registered kinds — once per + RECORDED DECISION rather than once per completed turn. AC-TASKS-QUORUM-CONCURRENCY-001.6's operation id is + deliberately unique per decision, so the id-dedup does not suppress the repeat + either. `Office Default` configures only two `move_to_step` actions on + `on_turn_complete`, so this is invisible on the shipped workflow and on every + regression in `### Regression`; it would first appear on a workflow that pairs + a quorum guard with a side-effect action, as `clear_decisions` and + `queue_run_for_each_participant` already are elsewhere in this same workflow's + `on_enter`. A reviewer vote is not a turn completion, and this AC is what keeps + the two from being conflated. + Boundary: when the current step configures no guarded transition at all, the + re-evaluation SHALL apply nothing and report `transition_applied = false` + without erroring. A decision is legitimately recordable at such a step — AC-TASKS-QUORUM-RECORDING-001.3 + gates on participation, not on the presence of a guard — and treating "nothing + to evaluate" as a failure would reject valid verdicts. + +- **AC-TASKS-QUORUM-REEVALUATION-001.9:** THE SYSTEM SHALL implement the AC-TASKS-QUORUM-SLATE-001.9 slate construction, the AC-TASKS-QUORUM-SLATE-001.10 + seat matching, the AC-TASKS-QUORUM-RECORDING-001.4/AC-TASKS-QUORUM-RECORDING-001.5 role resolution and the decision write EXACTLY + ONCE, in the engine, against the engine's `ParticipantStore` and + `DecisionStore` ports, and SHALL expose them to both transports through a + single engine decision entry point. Neither the MCP tool nor + `office/dashboard` SHALL carry its own copy. This is stated because the + ownership sentence in `## What` is not satisfiable as written today: + `DashboardService`'s only engine connection is + `engineDispatcher shared.WorkflowEngineDispatcher`, whose entire interface + surface is one `HandleTrigger` method — no slate read, no role resolution, no + decision write. The office decision path therefore runs through + `recordTaskDecision` -> `resolveDeciderRole` -> `s.repo.ListAllTaskParticipants` + in `office/repository/sqlite`, a DIFFERENT Go package from the + `workflow/repository` one backing the engine's `ParticipantStore`, while both + read and write the same `workflow_step_participants` table. Left unstated, a + builder may reasonably implement AC-TASKS-QUORUM-SLATE-001.9's four steps a second time on the + office side — two canonicalizations over one table, which is precisely the + read-side/write-side divergence AC-TASKS-QUORUM-SLATE-001.3 exists to prevent. +- **AC-TASKS-QUORUM-REEVALUATION-001.10:** THE SYSTEM SHALL extend `shared.WorkflowEngineDispatcher` with an + additive **write-side** method that records a decision and performs the AC-TASKS-QUORUM-REEVALUATION-001.1 + re-evaluation, and SHALL wire it at the existing dispatcher construction site + (`backendapp/main.go`, where `officeenginedispatcher.New` is already built + from the shared engine instance and the task repo). `HandleTrigger` and every + existing caller of it SHALL be unchanged. This is the write half of the + minimum port change that makes AC-TASKS-QUORUM-REEVALUATION-001.9 buildable; without naming it, "call into + the engine's decision API" has no referent. It is the FIRST of exactly two + additive methods this feature adds to that interface — AC-TASKS-QUORUM-REEVALUATION-001.14 names the + second, read-side one. Earlier wording said "one additive method", which read + as forbidding the read-side method that AC-TASKS-QUORUM-DIAGNOSTICS-001.4 cannot be built without. +- **AC-TASKS-QUORUM-REEVALUATION-001.11:** THE AC-TASKS-QUORUM-REEVALUATION-001.10 entry point's return SHALL carry the decision row id + and the persisted `decided_at`, because AC-TASKS-QUORUM-REEVALUATION-001.12 promises + `publishDecisionRecorded` is preserved unchanged and that function publishes + `"decision_id": d.ID` and `"created_at": d.CreatedAt` off the + `DecisionRecord`. Once the persist moves behind the entry point, office can no + longer source those two values itself. Without this the builder must either + widen the entry point's return undocumented or locally synthesise an id and a + timestamp, and a locally taken timestamp need not equal the row's stored + `decided_at` — publishing an event whose `created_at` disagrees with the row + it describes. AC-TASKS-QUORUM-RECORDING-001.11 observes both fields on the tool surface and they are the + same two values. + Concretely: the entry point SHALL stamp `decided_at` once, persist that value, + and return THAT value — never re-read the clock to build the response. Two + clock reads around one write are the defect this clause exists to forbid; they + would put `created_at` on the published event ahead of the row every time, + which is invisible in review and wrong in every timeline that joins the two. +- **AC-TASKS-QUORUM-REEVALUATION-001.12:** THE SYSTEM SHALL confine the office-side change to the resolve, + persist and re-evaluate core of `recordTaskDecision`. Its surrounding + behavior — `publishDecisionRecorded`, `logDecisionActivity`, + `runReactivityForDecision`, and the `DecisionRecord` shape returned to the + handler — SHALL be preserved, and every other reader of + `ListAllTaskParticipants` (task inbox, scheduler reactivity, the task-detail + handler) SHALL keep today's step-scoped behavior, as `## Out of scope` + already states. This bounds the refactor: the card repoints one function's + core, it does not migrate `office/dashboard` onto the engine wholesale. +- **AC-TASKS-QUORUM-REEVALUATION-001.13:** WHEN the engine decision entry point is not wired — `main.go` + already returns early and logs "workflow engine not initialised; office engine + dispatcher disabled" when `orchestratorSvc.WorkflowEngine()` is nil, leaving + `DashboardService.engineDispatcher` nil — THE SYSTEM SHALL reject the decision + call with an error and write no row, on BOTH transports. It SHALL NOT fall back + to a second office-side implementation of AC-TASKS-QUORUM-SLATE-001.9, which would resurrect exactly + the divergence AC-TASKS-QUORUM-REEVALUATION-001.9 forbids, and it SHALL NOT silently persist a decision that + can never be counted. This mirrors the existing `decisionStoreNotWiredErr` + guard in `recordTaskDecision` rather than inventing a new failure mode. +- **AC-TASKS-QUORUM-REEVALUATION-001.14:** THE SYSTEM SHALL extend `shared.WorkflowEngineDispatcher` with a + second additive method, **read-only**, that evaluates every guarded + `on_turn_complete` transition configured at a task's current step and returns + one entry per guard together with the AC-TASKS-QUORUM-DIAGNOSTICS-001.12 `reevaluation_blocked` value: + + `EvaluateStepQuorum(ctx, taskID) (QuorumSnapshot, error)`, where + `QuorumSnapshot` carries `StepID`, `ReevaluationBlocked bool`, and + `Guards []QuorumGuardState`, and each `QuorumGuardState` carries + `TargetStepID`, `Role`, `Threshold`, `RequiredCount`, `ReceivedCount`, + `Satisfied`, `Reason` and `Error`. + + This method SHALL apply no transition, write no row, execute no action + callback, emit no AC-TASKS-QUORUM-DIAGNOSTICS-001.2 log record and increment no AC-TASKS-QUORUM-DIAGNOSTICS-001.3 counter. It SHALL + reuse the AC-TASKS-QUORUM-SLATE-001.9 slate construction, the AC-TASKS-QUORUM-SLATE-001.10 seat matching and the AC-TASKS-QUORUM-DIAGNOSTICS-001.7 + reason precedence — it is a second CALLER of that logic, never a second + implementation of it, per AC-TASKS-QUORUM-REEVALUATION-001.9. + + **Ordering.** `Guards` SHALL be returned in the step's configured + `on_turn_complete` action order, so that AC-TASKS-QUORUM-DIAGNOSTICS-001.11's selection rule and AC-TASKS-QUORUM-REEVALUATION-001.6's + first-transition-wins read off one and the same order. This is the named + ordering AC-TASKS-QUORUM-DIAGNOSTICS-001.11 depends on; without it AC-TASKS-QUORUM-DIAGNOSTICS-001.11's "first such entry" has no + referent. + + **Nil / empty / error.** A task with no bound `workflow_step_id` SHALL return + an empty `Guards`, `ReevaluationBlocked = false` and no error, which is what + AC-TASKS-QUORUM-DIAGNOSTICS-001.5 requires. A step with a bound id but no guarded transition SHALL + likewise return an empty `Guards` and no error; AC-TASKS-QUORUM-DIAGNOSTICS-001.11 aggregates both to + `clear`. A store error SHALL surface as a per-guard `evaluation_error` entry + per AC-TASKS-QUORUM-DIAGNOSTICS-001.1, not as a method-level error, so one failing guard does not blank + the others. A method-level error is reserved for the not-wired case. + + **Not wired.** When the dispatcher is nil, per the AC-TASKS-QUORUM-REEVALUATION-001.13 condition, the + AC-TASKS-QUORUM-DIAGNOSTICS-001.4 handler SHALL return an error rather than falling back to an + office-side evaluation — the read side carries the same no-fallback rule as + the write side, and for the same reason. + + **Unknown task.** An unresolvable `taskID` SHALL surface as a method-level + error, distinct from the unbound-step case of AC-TASKS-QUORUM-DIAGNOSTICS-001.5 which is a successful + empty result; the AC-TASKS-QUORUM-DIAGNOSTICS-001.4 handler SHALL map it exactly as the sibling + `GET /tasks/:id/decisions` route already maps an unknown task. Stated because + AC-TASKS-QUORUM-DIAGNOSTICS-001.5's "return empty rather than error" is easy to over-apply to a task that + does not exist at all, which would report a missing task as a healthy one. + + **Idempotency and concurrency.** The method is a pure read: two concurrent + calls compute independently from committed state and neither observes nor + mutates the other. It needs no operation id. + + AC-TASKS-QUORUM-DIAGNOSTICS-001.4's HTTP payload SHALL be a direct projection of this snapshot, so the + endpoint and the tool's `guards` field cannot drift apart. + + Without this AC there is no legal way to build AC-TASKS-QUORUM-DIAGNOSTICS-001.4 at all: AC-TASKS-QUORUM-REEVALUATION-001.9 forbids + `office/dashboard` from carrying its own copy of the slate machinery, and + `DashboardService`'s only engine connection is + `engineDispatcher shared.WorkflowEngineDispatcher`, whose declared surface is + `HandleTrigger` alone. AC-TASKS-QUORUM-DIAGNOSTICS-001.4, AC-TASKS-QUORUM-DIAGNOSTICS-001.5, AC-TASKS-QUORUM-DIAGNOSTICS-001.6, AC-TASKS-QUORUM-DIAGNOSTICS-001.10, AC-TASKS-QUORUM-DIAGNOSTICS-001.11 and AC-TASKS-QUORUM-DIAGNOSTICS-001.12 all + depend on this method existing. + + **Implementation note, not a requirement:** the in-tree precedent for adding a + dispatcher capability is a separate optional interface plus a type assertion + (`handledWorkflowEngineDispatcher` in `office/dashboard/service_tasks.go` + reaching `HandleTriggerHandled`), rather than widening the shared interface. + Either shape satisfies this AC; the requirement is that the method exist and + be reachable from the AC-TASKS-QUORUM-DIAGNOSTICS-001.4 handler, not which of the two idioms carries it. + +## Out of scope + + + +Behavior outside this acceptance-criteria group is defined by the core quorum requirement and the other quorum capability requirements. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/workflow-quorum-decision-recording-regression.md b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-regression.md new file mode 100644 index 0000000000..dc02438427 --- /dev/null +++ b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-regression.md @@ -0,0 +1,69 @@ +--- +status: draft +system: tasks +created: 2026-08-19 +owners: + - kandev +--- + + +# Quorum regression behavior Requirements + + + +## Overview + + + +Representative approval, rejection, waiting, and human request-changes flows produce the expected task states. + +## Requirements + + +### REQ-TASKS-QUORUM-REGRESSION-001: Quorum regression behavior + + + +**Intent:** Representative approval, rejection, waiting, and human request-changes flows produce the expected task states. + +#### Acceptance criteria + +#### Regression + +- **AC-TASKS-QUORUM-REGRESSION-001.1:** GIVEN a task at Review with exactly one reviewer participant + (`decision_required = 1`) and one recorded `approved` decision from that + reviewer, WHEN quorum is evaluated, THE SYSTEM SHALL move the task to + Approval. +- **AC-TASKS-QUORUM-REGRESSION-001.2:** GIVEN the same task with one recorded rejection instead, WHEN quorum + is evaluated, THE SYSTEM SHALL move the task to Work. +- **AC-TASKS-QUORUM-REGRESSION-001.3:** GIVEN two reviewer participants and one approval, WHEN quorum is + evaluated under `all_approve`, THE SYSTEM SHALL leave the task at Review and + report it as awaiting one further decision. +- **AC-TASKS-QUORUM-REGRESSION-001.4:** GIVEN a workflow whose steps carry no guard, THE SYSTEM SHALL + transition exactly as it does today. +- **AC-TASKS-QUORUM-REGRESSION-001.5:** GIVEN a task at Review, whose guards name `role: reviewer`, and a + human recording **Request changes** through `POST /tasks/:id/request-changes`, + WHEN quorum is evaluated, THE SYSTEM SHALL move the task to Work — even though + that decision is stored with `role = approver`. This is the regression that + makes AC-TASKS-QUORUM-VERDICT-001.2 and AC-TASKS-QUORUM-VERDICT-001.7 observable end to end, and it is the exact path that is + broken in production today. + +- **AC-TASKS-QUORUM-REGRESSION-001.6:** GIVEN a task moved off a guarded step by `any_reject` under AC-TASKS-QUORUM-REGRESSION-001.2 + and later returned to that step, WHEN quorum is next evaluated there, THE + SYSTEM SHALL evaluate against zero decisions from the prior round. This AC + exists to make AC-TASKS-QUORUM-CONCURRENCY-001.9's premise observable rather than assumed: `clear_decisions` + on `on_enter` is depended upon but verified by no other AC, and the only + `on_enter` trigger dispatch in the tree is inside `SwitchWorkflowCallback` — + the orchestrator's callback registry wires a `DispatchTriggerFn` for + `switch_workflow` only. If this AC fails, this card is blocked on the sibling + `on_enter` action-dispatch feature and SHALL be reported as blocked; it SHALL + NOT be closed by adding a second decision-clearing path inside this feature. + Without it, a rejection persists at the guarded step and re-fires `any_reject` + on the next completed turn, so AC-TASKS-QUORUM-REGRESSION-001.1 and AC-TASKS-QUORUM-REGRESSION-001.2 cannot both hold across a round + trip. + +## Out of scope + + + +Behavior outside this acceptance-criteria group is defined by the core quorum requirement and the other quorum capability requirements. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/workflow-quorum-decision-recording-step-binding.md b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-step-binding.md new file mode 100644 index 0000000000..f189de4f14 --- /dev/null +++ b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-step-binding.md @@ -0,0 +1,71 @@ +--- +status: draft +system: tasks +created: 2026-08-19 +owners: + - kandev +--- + + +# Quorum step binding and thresholds Requirements + + + +## Overview + + + +A verdict remains bound to the validated step and threshold arithmetic stays explicit at boundary conditions. + +## Requirements + + +### REQ-TASKS-QUORUM-BINDING-001: Quorum step binding and thresholds + + + +**Intent:** A verdict remains bound to the validated step and threshold arithmetic stays explicit at boundary conditions. + +#### Acceptance criteria + +#### Step binding and threshold boundaries + +- **AC-TASKS-QUORUM-BINDING-001.1:** THE SYSTEM SHALL bind a decision to the task's `workflow_step_id` + read during validation, and SHALL write the row inside the same transaction + that supersedes the participant's prior verdict for that step. +- **AC-TASKS-QUORUM-BINDING-001.2:** WHEN the task's step changes between validation and write, THE + SYSTEM SHALL persist the decision against the **validated** step — not reject + it, and not re-target it at the new step. The recorded row SHALL NOT be + counted at the new step, the call SHALL report success, `transition_applied` + SHALL be `false`, and the returned `step_id` SHALL be the validated step so + the caller can see its verdict landed on a step the task has left. Persisting + is chosen over rejecting because the reviewer's verdict is real work and + discarding it would silently lose a review; the returned `step_id` is what + makes the outcome detectable. +- **AC-TASKS-QUORUM-BINDING-001.3:** THE SYSTEM SHALL write the caller's reason to + `workflow_step_decisions.comment`. +- **AC-TASKS-QUORUM-BINDING-001.4:** THE SYSTEM SHALL satisfy `majority_approve` only when approvals + strictly exceed half the required slate, so a slate of 2 requires 2 approvals + and a slate of 3 requires 2 — preserving `approveCount*2 > totalRequired` + (`workflow/engine/quorum.go:113`). +- **AC-TASKS-QUORUM-BINDING-001.5:** WHEN `n_approve:` names an N greater than the required slate size, + THE SYSTEM SHALL never fire that transition and SHALL report AC-TASKS-QUORUM-DIAGNOSTICS-001.1 reason + `threshold_unsatisfiable` rather than an error. It is deliberately NOT + `threshold_not_met`: no future decision can satisfy it, so it is a stuck card, + and AC-TASKS-QUORUM-DIAGNOSTICS-001.6 requires stuck to look different from waiting. +- **AC-TASKS-QUORUM-BINDING-001.6:** WHEN `n_approve:` names a non-numeric or non-positive N, THE + SYSTEM SHALL not fire the transition and SHALL report AC-TASKS-QUORUM-DIAGNOSTICS-001.1 reason + `threshold_unrecognized`. The threshold family is recognized here and only its + parameter is malformed, so this is not a guard-variant failure; + `guard_variant_unrecognized` is reserved for the case AC-TASKS-QUORUM-DIAGNOSTICS-001.8 names. +- **AC-TASKS-QUORUM-BINDING-001.7:** WHEN a decision exists for a role other than the guard's role at the + same step, THE SYSTEM SHALL ignore it for that guard — EXCEPT for the + `decider_type = user` veto of AC-TASKS-QUORUM-VERDICT-001.7, which is deliberately role-agnostic. + AC-TASKS-QUORUM-BINDING-001.7 governs seated, role-scoped quorum contributions; the human veto is not + one. + +## Out of scope + + + +Behavior outside this acceptance-criteria group is defined by the core quorum requirement and the other quorum capability requirements. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/workflow-quorum-decision-recording-verdict-vocabulary.md b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-verdict-vocabulary.md new file mode 100644 index 0000000000..0470480b4a --- /dev/null +++ b/docs/specs/tasks/requirements/workflow-quorum-decision-recording-verdict-vocabulary.md @@ -0,0 +1,103 @@ +--- +status: draft +system: tasks +created: 2026-08-19 +owners: + - kandev +--- + + +# Quorum verdict vocabulary Requirements + + + +## Overview + + + +Human and agent verdicts use compatible meanings so approvals and rejections produce the expected quorum results. + +## Requirements + + +### REQ-TASKS-QUORUM-VERDICT-001: Quorum verdict vocabulary + + + +**Intent:** Human and agent verdicts use compatible meanings so approvals and rejections produce the expected quorum results. + +#### Acceptance criteria + +#### Verdict vocabulary + +- **AC-TASKS-QUORUM-VERDICT-001.1:** THE SYSTEM SHALL treat `changes_requested` as a rejection wherever + `rejected` is treated as one, so that `any_reject` is satisfied by either. +- **AC-TASKS-QUORUM-VERDICT-001.2:** WHEN a human records a decision through + `POST /tasks/:id/request-changes`, THE SYSTEM SHALL produce a stored verdict + that satisfies `any_reject` for the guard evaluating at that step, whatever + role that guard names. AC-TASKS-QUORUM-VERDICT-001.1 alone does not achieve this; AC-TASKS-QUORUM-VERDICT-001.3 and AC-TASKS-QUORUM-VERDICT-001.7 are + the other halves. +- **AC-TASKS-QUORUM-VERDICT-001.3:** THE SYSTEM SHALL satisfy `any_reject` when, among decisions + recorded at the evaluating step for the guard's role, the LAST row per decider + identity `(decider_type, decider_id)` under the AC-TASKS-QUORUM-CONCURRENCY-001.1 ordering is a rejection + — whether or not that decider's `participant_id` is in the required slate. A + rejection is a veto: one is sufficient and it does not require a quorum seat. + This is what makes a singleton-user rejection countable, since that decider + has no `agent_profile_id` and therefore no participant row, and is written + with the `userParticipantSentinel` participant id. +- **AC-TASKS-QUORUM-VERDICT-001.4:** THE SYSTEM SHALL key the AC-TASKS-QUORUM-VERDICT-001.3 veto scan on the latest verdict per + decider, NOT on the presence of any rejection row. A decider who rejects and + then records an approval at the same step SHALL NOT continue to veto. Stated + because the naive reading — scan for a rejection — reintroduces the defect + named under `## Persistence guarantees`: the engine's `DecisionInfo` + projection carries no `superseded_at`, so "non-superseded" is not a predicate + the evaluator can express, and only last-row-wins is. +- **AC-TASKS-QUORUM-VERDICT-001.5:** THE SYSTEM SHALL extend the engine's `DecisionInfo` projection to + carry `decider_type`, `decider_id`, `role`, and `comment`. AC-TASKS-QUORUM-VERDICT-001.3 needs decider + identity, AC-TASKS-QUORUM-BINDING-001.7 needs `role`, and AC-TASKS-QUORUM-BINDING-001.3 needs `comment`; the projection today + carries only `participant_id`, `decision`, and `note`, so none of the three is + satisfiable without this. The underlying columns already exist and + `ListStepDecisions` already selects them. +- **AC-TASKS-QUORUM-VERDICT-001.6:** THE SYSTEM SHALL count only slate members, per AC-TASKS-QUORUM-SLATE-001.9, toward + `all_approve`, `all_decide`, `majority_approve`, and `n_approve:`. An + approval is a quorum contribution and does require a seat. AC-TASKS-QUORUM-VERDICT-001.3 is scoped to + `any_reject` alone, so a non-slate approval never advances a task. +- **AC-TASKS-QUORUM-VERDICT-001.7:** THE SYSTEM SHALL evaluate the AC-TASKS-QUORUM-VERDICT-001.3 veto over decisions with + `decider_type = user` WITHOUT filtering on the guard's role, while continuing + to filter agent decisions by role per AC-TASKS-QUORUM-BINDING-001.7. The singleton human's stored + `role` SHALL remain whatever `resolveDeciderRole` returns for a user caller — + today the constant `approver`, which `## Out of scope` leaves unchanged — and + the veto SHALL NOT depend on it. + Without this, AC-TASKS-QUORUM-VERDICT-001.2 is unsatisfiable at every reviewer-guarded step, which is + the step the motivating card in `## Why` is stuck at: `resolveDeciderRole` + sets `hasApprover = true` unconditionally for `decider_type = user` and + returns on the approver-wins branch, so a human clicking **Request changes** + at `Office Default`'s Review — whose guards name `role: reviewer` — writes + `role = approver`, and AC-TASKS-QUORUM-BINDING-001.7 discards it before AC-TASKS-QUORUM-VERDICT-001.3 ever sees it. + The alternative, stamping the human's decision with the evaluating guard's + role, is rejected rather than merely unchosen: a decision is recorded once + while a step may configure several guards, so "the guard's role" is not well + defined at write time. The human is the operator of the board rather than a + seated participant, so a human rejection is a veto over the step, not a + role-scoped quorum contribution. AC-TASKS-QUORUM-VERDICT-001.6 already denies a seatless approval any + effect, so this asymmetry touches rejections only. + Boundary, stated so it is a decision rather than an accident: when a step + configures `any_reject` guards under two DIFFERENT roles, one human rejection + satisfies both at once, because the veto is role-agnostic by construction. + That is intended — the human is the operator of the board and is rejecting the + step, not a role's share of it — and AC-TASKS-QUORUM-REEVALUATION-001.6 still means only the first + satisfied transition in configured order is applied. `Office Default` does not + configure this shape; nothing forbids it. +- **AC-TASKS-QUORUM-VERDICT-001.8:** THE SYSTEM SHALL continue to accept and store verdict strings + outside the recognized set without error, counting them toward `all_decide` + and toward neither `all_approve` nor `any_reject` — preserving the documented + free-form behavior at `workflow/engine/quorum.go:22-24`. +- **AC-TASKS-QUORUM-VERDICT-001.9:** THE SYSTEM SHALL preserve the existing wire value written by the + human `request-changes` path, so the frontend DTO union + `"approved" | "changes_requested"` continues to typecheck unchanged. + +## Out of scope + + + +Behavior outside this acceptance-criteria group is defined by the core quorum requirement and the other quorum capability requirements. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/workflow-quorum-decision-recording.md b/docs/specs/tasks/requirements/workflow-quorum-decision-recording.md new file mode 100644 index 0000000000..4f592b572f --- /dev/null +++ b/docs/specs/tasks/requirements/workflow-quorum-decision-recording.md @@ -0,0 +1,95 @@ +--- +status: draft +system: tasks +created: 2026-08-19 +owners: + - kandev +--- + + +# Workflow quorum decision recording Requirements + + + +## Overview + + + +Office workflows can record participant decisions and move a task through guarded quorum transitions with diagnosable outcomes. + + + +## Why + +A `move_to_step` guarded by `wait_for_quorum` never fires. An Office task +driven through Work → Review with a reviewer attached runs the reviewer, gets a +substantive review back, and then sits at Review permanently. It is +indistinguishable, from every surface a human can see, from a hung card. + +Verified 2026-08-19 against task `7611f8da-4fe3-4bb2-a5a2-ebe6f90130f2` +(workspace `95542bf3`) in `~/.kandev/data/kandev.db`: + +``` +sqlite> SELECT COUNT(*) FROM workflow_step_decisions; +0 +``` + +Zero rows in the entire store, ever. + +## What + +Five behaviors, matching the five defects. + +1. An agent holding `reviewer` or `approver` on the current step can record an + approve or reject verdict with a reason, via an MCP tool, and it persists to + `workflow_step_decisions`. +2. A rejection recorded through any surface — agent tool or human UI — is + counted by `any_reject`. This needs both a vocabulary fix and a seat fix: the + human decider has no participant row at all, so a rejection counts as a veto + without requiring a quorum seat, while approvals still require one. +3. Recording a verdict through any surface immediately re-evaluates the guarded + transitions for that task's current step. +4. A participant with `decision_required` is counted at the step where the + decision is being taken, not only at the step where they were attached. +5. A guarded transition that does not fire says why, distinguishably, on a + surface a human reads. + +Ownership follows the engine. The engine already owns guard semantics, +threshold arithmetic, and the required-participant slate; this feature adds no +second implementation in the orchestrator. The MCP tool and the HTTP handler are +transports that call into the engine's decision API. AC-TASKS-QUORUM-REGRESSION-001.1 names that API and +the ports that make it reachable, because "call into the engine" is not today a +thing `office/dashboard` can do. There are two such ports and they are named +separately: AC-TASKS-QUORUM-REGRESSION-001.2 for the write side (record a decision, re-evaluate) and +AC-TASKS-QUORUM-REGRESSION-001.6 for the read side (evaluate every guard read-only, for the AC-TASKS-QUORUM-DIAGNOSTICS-001.4 +diagnostic surface). Behavior 5 is unbuildable without the second one. + +## Requirements + + + +### REQ-TASKS-QUORUM-CORE-001: Workflow quorum decision recording + + + +**Intent:** Office workflows can record participant decisions and move a task through guarded quorum transitions with diagnosable outcomes. + + + +#### Acceptance criteria + + + +- **AC-TASKS-QUORUM-CORE-001.1:** When a qualified agent records a verdict, the system shall persist it for the task's current workflow decision round. +- **AC-TASKS-QUORUM-CORE-001.2:** When a human or agent records a verdict, the system shall evaluate the guarded transition for the task's current step. +- **AC-TASKS-QUORUM-CORE-001.3:** When a quorum guard does not advance a task, the system shall expose a reason that distinguishes waiting from an unfulfillable or failed evaluation. +- **AC-TASKS-QUORUM-CORE-001.4:** When a task is evaluated, the system shall use the workflow engine as the single owner of quorum semantics. +- **AC-TASKS-QUORUM-CORE-001.5:** When the task is re-entered for another decision round, the system shall preserve the round boundary defined by the workflow configuration. + + + +## Out of scope + + + +Workspace approvals, new thresholds or guard variants, decision revocation, timeouts, delegation, Kanban completion, and the separate close-approval gate remain outside this capability. \ No newline at end of file diff --git a/docs/specs/workflow-session-settings/spec.md b/docs/specs/tasks/requirements/workflow-session-settings.md similarity index 94% rename from docs/specs/workflow-session-settings/spec.md rename to docs/specs/tasks/requirements/workflow-session-settings.md index 67ce43650b..4f193b685f 100644 --- a/docs/specs/workflow-session-settings/spec.md +++ b/docs/specs/tasks/requirements/workflow-session-settings.md @@ -1,10 +1,27 @@ --- -status: approved +status: active +system: tasks created: 2026-08-01 -owner: kandev +owners: + - kandev --- +# Conditional Workflow Session Settings Requirements -# Conditional Workflow Session Settings +## 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. + +## Requirements + +### REQ-TASKS-WORKFLOW-SESSION-SETTINGS-001: Conditional Workflow Session Settings + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-WORKFLOW-SESSION-SETTINGS-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -38,7 +55,7 @@ Workflow authors can currently select a different agent profile for a step, but - Agent-family choices in conditional rules come only from agent families represented by configured agent profiles; capability data for an unconfigured family does not create a new choice. - Read-only synced workflows display their rules and carry-forward warnings but do not allow edits, matching existing workflow settings behavior. -Decision: [ADR-2026-08-01-workflow-session-original-configuration](../../decisions/2026-08-01-workflow-session-original-configuration.md). +Decision: [ADR-2026-08-01-workflow-session-original-configuration](../../../decisions/2026-08-01-workflow-session-original-configuration.md). ## Data model @@ -116,7 +133,7 @@ The backend validates the action on workflow-step create/update and import. Inva Existing agent capability responses provide the editor's family names, models, and ACP select options. Existing task-session model/configuration mechanisms apply and persist runtime changes. -When an ACP provider's options depend on the selected model, the editor uses the model-aware capability contract in [Dynamic Provider Model Options](../agents/dynamic-provider-options.md) to replace the baseline option snapshot before saving a rule. +When an ACP provider's options depend on the selected model, the editor uses the model-aware capability contract in [Dynamic Provider Model Options](../../agents/dynamic-provider-options.md) to replace the baseline option snapshot before saving a rule. ## State machine @@ -184,4 +201,4 @@ The same users who can edit a workspace workflow can create, change, or remove c - Changing agent credentials, CLI flags, permission mode, executor, environment, MCP configuration, or profile settings. - Automatically restoring settings merely because a task leaves a step. - Predicting arbitrary manual card moves in the editor's carry-forward analysis. -- Inferring, translating, or aliasing provider-specific model and option identifiers. +- Inferring, translating, or aliasing provider-specific model and option identifiers. \ No newline at end of file diff --git a/docs/specs/workflow-settings-autosave/spec.md b/docs/specs/tasks/requirements/workflow-settings-autosave.md similarity index 76% rename from docs/specs/workflow-settings-autosave/spec.md rename to docs/specs/tasks/requirements/workflow-settings-autosave.md index e38ff4d86d..e1afe3c75a 100644 --- a/docs/specs/workflow-settings-autosave/spec.md +++ b/docs/specs/tasks/requirements/workflow-settings-autosave.md @@ -1,13 +1,29 @@ --- -status: archived +status: deprecated +system: tasks created: 2026-07-14 -owner: kandev -superseded_by: docs/specs/ui/settings-manual-save.md +owners: + - kandev --- +# Workflow Settings Autosave Requirements -# Workflow Settings Autosave +## Overview -> Superseded by [Settings Manual Save](../ui/settings-manual-save.md). This file records the previously shipped autosave behavior and is no longer the product contract. +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. + +## Requirements + +### REQ-TASKS-WORKFLOW-SETTINGS-AUTOSAVE-001: Workflow Settings Autosave + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-WORKFLOW-SETTINGS-AUTOSAVE-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail + +> Superseded by [Settings Manual Save](../../ui/settings-manual-save.md). This file records the previously shipped autosave behavior and is no longer the product contract. ## Why @@ -50,4 +66,4 @@ Only successful backend writes survive reload or restart. Save-status UI state i ## Implementation Plan -See [the completed implementation plan](../../plans/workflow-settings-autosave/plan.md). +See [the completed implementation plan](../../../plans/workflow-settings-autosave/plan.md). \ No newline at end of file diff --git a/docs/specs/workflow-step-agent-start-ownership/spec.md b/docs/specs/tasks/requirements/workflow-step-agent-start-ownership.md similarity index 89% rename from docs/specs/workflow-step-agent-start-ownership/spec.md rename to docs/specs/tasks/requirements/workflow-step-agent-start-ownership.md index 2a02db76d3..37d06a37d0 100644 --- a/docs/specs/workflow-step-agent-start-ownership/spec.md +++ b/docs/specs/tasks/requirements/workflow-step-agent-start-ownership.md @@ -1,10 +1,27 @@ --- status: draft +system: tasks created: 2026-08-05 -owner: Kandev +owners: + - Kandev --- +# Workflow Step Agent Start Ownership Requirements -# Workflow Step Agent Start Ownership +## 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. + +## Requirements + +### REQ-TASKS-WORKFLOW-STEP-AGENT-START-OWNERSHIP-001: Workflow Step Agent Start Ownership + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-WORKFLOW-STEP-AGENT-START-OWNERSHIP-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why @@ -121,4 +138,4 @@ read, which is a design change beyond this repair. - The `-32002 Resource not found` resume failure, which is a consequence of the force-kill rather than an independent defect. - Changing task `FAILED` semantics, the reconciliation path, or the - lazy-recovery boot that follows a backend restart. + lazy-recovery boot that follows a backend restart. \ No newline at end of file diff --git a/docs/specs/workflow-sync-workspace-authz/spec.md b/docs/specs/tasks/requirements/workflow-sync-workspace-authz.md similarity index 89% rename from docs/specs/workflow-sync-workspace-authz/spec.md rename to docs/specs/tasks/requirements/workflow-sync-workspace-authz.md index 5f8dedddc3..9163c2f595 100644 --- a/docs/specs/workflow-sync-workspace-authz/spec.md +++ b/docs/specs/tasks/requirements/workflow-sync-workspace-authz.md @@ -1,10 +1,27 @@ --- -status: building +status: active +system: tasks created: 2026-08-06 -owner: nova28 +owners: + - nova28 --- +# Workflow Sync — Per-User Workspace Authorization Requirements -# Workflow Sync — Per-User Workspace Authorization +## 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. + +## Requirements + +### REQ-TASKS-WORKFLOW-SYNC-WORKSPACE-AUTHZ-001: Workflow Sync — Per-User Workspace Authorization + +**Intent:** Preserve the observable task or workflow behavior recorded by the legacy specification. + +#### Acceptance criteria + +- **AC-TASKS-WORKFLOW-SYNC-WORKSPACE-AUTHZ-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below. + +## Migrated source detail ## Why Every other per-workspace integration service (GitHub, Jira, Linear, Slack, Azure DevOps, @@ -93,4 +110,4 @@ missed it. — this spec is strictly about the `workspace_id` ownership boundary on the workflow-sync HTTP surface. - Adding workspace authorization to any other integration package — all of them already have it. -- New data model or schema changes — no persistent state changes. +- New data model or schema changes — no persistent state changes. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/workflow-task-step-transition-ledger-scenarios.md b/docs/specs/tasks/requirements/workflow-task-step-transition-ledger-scenarios.md new file mode 100644 index 0000000000..845b7885f7 --- /dev/null +++ b/docs/specs/tasks/requirements/workflow-task-step-transition-ledger-scenarios.md @@ -0,0 +1,209 @@ +--- +status: draft +system: tasks +created: 2026-08-12 +owners: + - nova28 +--- + + +# Workflow task-step transition ledger scenarios Requirements + + + +## Overview + + + +The ledger contract is observable through turn starts, committed transitions, rollback behavior, and downstream attribution scenarios. + + + +## Scenarios + +### Slice 1 — turn-start step stamp + +- **GIVEN** a session whose task sits in workflow step `S`, **WHEN** a turn + starts, **THEN** the turn's metadata contains `workflow_step_id_at_start` = `S`. +- **GIVEN** a session whose task has no workflow step, **WHEN** a turn starts, + **THEN** the turn's metadata contains no `workflow_step_id_at_start` key — not + an empty string, not `null`, not `0`. +- **GIVEN** a session with no runtime config to snapshot, **WHEN** a turn starts + while its task sits in step `S`, **THEN** the metadata contains + `workflow_step_id_at_start` = `S` and no `runtime_config_snapshot`. +- **GIVEN** a turn stamped with step `S`, **WHEN** the task moves to step `T` + before the turn completes, **THEN** the turn's stamp is still `S`. +- **GIVEN** a turn created before activation, **WHEN** it is read after + activation, **THEN** it carries no stamp and is not backfilled. +- **GIVEN** the task row cannot be read, **WHEN** a turn starts, **THEN** the + turn is created without a stamp and turn creation does not fail. +- **GIVEN** a synthetic completed turn created for a lifecycle message, **WHEN** + it is written while its task sits in step `S`, **THEN** it carries the same + stamp as a normal turn. + +### Slice 2 — ledger writes + +- **GIVEN** a task in step `A`, **WHEN** a user moves it to step `B`, **THEN** + exactly one ledger row exists with `from_workflow_step_id` = `A`, + `to_workflow_step_id` = `B`, trigger `manual_move`, actor kind `human`, and + `actor_id` equal to the acting user's ID. +- **GIVEN** a task in step `A` with no session at all, **WHEN** a bulk move + sends it to step `B`, **THEN** one row is written with `session_id` NULL and + trigger `bulk_move`. +- **GIVEN** a task in step `A` with two live sessions, **WHEN** a WIP pull + promotes it into step `B`, **THEN** one row is written with `session_id` NULL, + trigger `wip_pull`, and actor kind `system`. +- **GIVEN** an agent session on a task in step `A`, **WHEN** the agent calls + `move_task_kandev` and it applies immediately, **THEN** one row is written + with trigger `mcp_move`, actor kind `agent`, and `actor_id` equal to that + session's ID. +- **GIVEN** an agent's deferred `move_task_kandev`, **WHEN** it applies at turn + end, **THEN** one row is written with trigger `mcp_deferred_move`. +- **GIVEN** a workflow step whose `on_turn_complete` transitions to step `B`, + **WHEN** the turn completes, **THEN** one row is written with trigger + `engine_transition`. +- **GIVEN** a task in step `A`, **WHEN** a move to step `A` is issued, **THEN** + no row is written. +- **GIVEN** a task in step `A`, **WHEN** only its position within `A` changes, + **THEN** no row is written. +- **GIVEN** a WIP-limited target step at capacity, **WHEN** a move into it is + rejected, **THEN** no row is written and the task's step is unchanged. +- **GIVEN** the transaction that changes the step is rolled back for any reason, + **WHEN** it completes, **THEN** no row exists for it. +- **GIVEN** a task created into step `A`, **WHEN** creation commits, **THEN** one + row exists with `from_workflow_id` and `from_workflow_step_id` NULL and + trigger `task_created`. +- **GIVEN** a task created targeting a full step `B` with feeder step `A`, + **WHEN** creation commits and places it in `A`, **THEN** the genesis row's + `to_workflow_step_id` is `A`, the step it was actually placed in. +- **GIVEN** a task attached to a workflow, **WHEN** it is later removed from + that workflow, **THEN** a row is written with `to_workflow_id` and + `to_workflow_step_id` NULL and trigger `workflow_detached`. +- **GIVEN** a task whose step changed via a task update rather than the move + path, **WHEN** the update commits, **THEN** one row is written with trigger + `task_update`. +- **GIVEN** a caller that declares no trigger or actor, **WHEN** its step change + commits, **THEN** the row records trigger `unknown`, actor kind `unknown`, and + `actor_id` NULL. +- **GIVEN** a task created with no workflow, **WHEN** creation commits, **THEN** + no row is written; **WHEN** it is later attached to a workflow, **THEN** its + first row has trigger `workflow_attached` and `from_*` NULL. +- **GIVEN** an ephemeral (quick-chat) task in step `A`, **WHEN** it moves to + step `B`, **THEN** a row is written exactly as for any other task. +- **GIVEN** a task in step `A`, **WHEN** it is archived, unarchived in place, + cascade-archived, or deleted without its step changing, **THEN** no row is + written for that action. +- **GIVEN** an engine action that switches a task to a step in a **different** + workflow, **WHEN** the transition commits, **THEN** one row is written whose + `from_workflow_id` and `to_workflow_id` differ, with trigger + `engine_transition`. +- **GIVEN** a task whose step is stored as the empty string, **WHEN** it moves + into step `B`, **THEN** the row's `from_workflow_step_id` is NULL, not `""`. +- **GIVEN** a row referencing a workflow step that is later deleted, **WHEN** + the step is deleted, **THEN** the row survives unchanged with its step ID + intact. + +### Ordering, chain, and concurrency + +- **GIVEN** a task moved `A`→`B`→`A`→`B`, **WHEN** its rows are read ordered by + `(occurred_at, id)`, **THEN** four rows appear and each row's + `from_workflow_step_id` equals the previous row's `to_workflow_step_id`. +- **GIVEN** two rows for one task sharing an identical `occurred_at`, **WHEN** + they are ordered by `(occurred_at, id)`, **THEN** the order is total and + matches the order the transitions committed. +- **GIVEN** two callers concurrently moving the same task, one to `B` and one to + `C`, **WHEN** both transactions commit, **THEN** exactly two rows exist, their + chain is intact, and the later row's `to_workflow_step_id` equals the task's + final `workflow_step_id`. +- **GIVEN** an engine trigger that is retried with the same `OperationID`, + **WHEN** the retry is handled, **THEN** no second row is written. +- **GIVEN** any task, **WHEN** its last row is read, **THEN** its + `to_workflow_step_id` equals `tasks.workflow_step_id`. +- **GIVEN** two rows for one task whose `occurred_at` values run backwards + because the host clock was corrected, **WHEN** they are ordered by `id`, + **THEN** the chain invariant still holds and the ledger is not repaired or + reordered by timestamp. +- **GIVEN** a turn stamped with step `S` and a ledger row moving the task to `T` + a moment after that turn started, **WHEN** both are read, **THEN** they + disagree, and neither is treated as an error. + +### Actor and privacy + +- **GIVEN** authentication is disabled, **WHEN** the single synthetic user moves + a card, **THEN** actor kind is `human` and `actor_id` is that identity's user + ID. +- **GIVEN** a Jira or Linear watcher that moves a card, **WHEN** the move + commits, **THEN** actor kind is `integration` and `actor_id` is the watch ID. +- **GIVEN** any row, **WHEN** it is inspected, **THEN** `actor_id` holds an + identifier and no display name, email, title, or prompt text appears anywhere + in the row. + +### Migration and activation + +- **GIVEN** a database created before this feature, **WHEN** the backend boots, + **THEN** the ledger table and `telemetry_activations` exist, pre-existing + tasks have no ledger rows, and pre-existing turns carry no stamp. +- **GIVEN** a database already migrated, **WHEN** the migration runner is run + twice more, **THEN** it succeeds both times and changes nothing. +- **GIVEN** a Postgres deployment, **WHEN** the same fresh-then-replay sequence + runs, **THEN** it behaves identically to SQLite. +- **GIVEN** a first boot on which a contract activates, **WHEN** boot completes, + **THEN** `telemetry_activations` holds one row for that key with + `contract_version` = 1 and that boot's UTC time. +- **GIVEN** a subsequent boot, **WHEN** it completes, **THEN** the existing + activation row is unchanged. +- **GIVEN** an install where the ledger table is absent because its migration + silently failed, **WHEN** a step change is attempted, **THEN** the transaction + fails rather than committing a step change with no row, and the boot health + line reports the table as absent. + +### Writer health + +- **GIVEN** a new production statement that mutates `tasks.workflow_step_id`, + **WHEN** the test suite runs, **THEN** the pinning test fails until that + statement is registered. +- **GIVEN** a ledger row is written, **WHEN** metrics are read, **THEN** the + expvar counter for that trigger has incremented and a `telemetry.metric.*` + log line names the event. +- **GIVEN** any boot, **WHEN** startup completes, **THEN** one health line per + registered contract key reports object existence, activation time, row count, + and most recent `occurred_at`. + +## Requirements + + + +### REQ-TASKS-TRANSITION-LEDGER-SCENARIOS-001: Workflow task-step transition ledger scenarios + + + +**Intent:** The ledger contract is observable through turn starts, committed transitions, rollback behavior, and downstream attribution scenarios. + + + +#### Acceptance criteria + + + +- **AC-TASKS-TRANSITION-LEDGER-SCENARIOS-001.1:** When a ledger scenario occurs, the system shall preserve the committed, ordered, and bounded evidence described by the scenario. + + + +## Out of scope + +- **Backfilling history.** No pre-activation transition is reconstructed, from + the message timeline or otherwise. +- **Dividing a cost event across a mid-turn step change.** Explicitly not + attempted; the "dominant of two steps" bucket remains. +- **`workflow_step_decisions`.** A different mechanism with a live writer. Not + merged, not extended, not read here. +- **Wiring, changing, or removing `session_step_history`** and its endpoint. + Separate work, lands first. +- **Any read API.** No HTTP route, no WS event, no MCP tool, no UI. Consumers + read the database. +- **The downstream extract and dashboard.** The consumer contract above is + recorded for the analysis side; implementing it is not part of this feature. +- **Step-name capture or step-rename history.** IDs only. +- **Retention, pruning, or archival of ledger rows.** +- **A `parent_session_id` / subagent grain.** Separate card. +- **Per-turn cost columns.** Separate card. \ No newline at end of file diff --git a/docs/specs/tasks/requirements/workflow-task-step-transition-ledger.md b/docs/specs/tasks/requirements/workflow-task-step-transition-ledger.md new file mode 100644 index 0000000000..1966dbaf61 --- /dev/null +++ b/docs/specs/tasks/requirements/workflow-task-step-transition-ledger.md @@ -0,0 +1,131 @@ +--- +status: draft +system: tasks +created: 2026-08-12 +owners: + - nova28 +--- + + +# Workflow task-step transition ledger Requirements + + + +## Overview + + + +The task system records forward-looking workflow-step boundaries so downstream consumers can attribute work to a step without reconstructing history. + + + +## Why + +Kandev records *when a card was worked on* but not *which workflow step it was +in at the time*. Cost, token, and duration events can be joined to a task and a +session, but attributing them to a step requires reconstructing step boundaries +from the message timeline. On the reference store that reconstruction leaves +**47.0%** of spend cleanly inside one step, **30.5%** merely "dominant of two +steps in the window", and **22.5%** unattributable — so "what does Review cost" +cannot be answered without a caveat larger than the answer. + +The one table that looks like it should answer this, `session_step_history`, is +keyed by **session** while Kandev tracks step state on the **task** +(`base_migrations.go` removed `task_sessions.workflow_step_id` for exactly that +reason). A card can change step with no active session, or with several, so +attributing a task-level transition to one session manufactures ownership that +does not exist. + +## What this feature is, and is not + +**It is a forward-looking record of step boundaries.** From its activation point +onward, every durable change to a task's workflow step is recorded with its +timestamp, its cause, and its actor, so downstream analysis can bound a step +interval from data rather than infer it from chat. + +**It cannot repair the past.** No historical transition is reconstructed or +backfilled. Rows before the activation point do not exist and MUST NOT be +synthesised. + +**It does not make per-step cost exact.** Cost events bill windows that cross +step boundaries; better boundaries improve attribution but do not decide how one +window's cost divides between two steps. The "dominant of two steps" bucket +shrinks; it does not disappear. + +**It is not `workflow_step_decisions`.** That table has a live writer on the +Office approval path; its zero rows mean an unused feature, not a missing one. +The two mechanisms MUST NOT be merged. + +**It is not a replacement for `session_step_history`.** This spec neither wires, +removes, nor changes that table or the `GET /api/v1/sessions/:id/workflow/history` +endpoint that reads it. See *Relationship to `session_step_history`*. + +## What + +The feature ships as two independently valuable slices. **Slice 1 SHALL ship and +be measured before Slice 2 is built** (see *Gate between slices*). + +### Slice 1 — turn-start step stamp + +- A turn created for a session records the workflow step its task was in at the + moment the turn started. +- The stamp is present on every turn created after activation whose task has a + workflow step, and **absent** — never empty, never `0` — when the task has + none. +- The stamp is immutable once written. A step change during the turn does not + rewrite it. +- No column is added to `task_session_turns`. + +### Slice 2 — task-level transition ledger + +- Every committed change to a task's workflow step produces exactly one ledger + row, written in the same database transaction as the change. +- A row that is not committed with its transition does not exist: a rolled-back + or WIP-rejected move leaves no row. +- A move that does not change the step (position-only reorder, re-issued move to + the current step) produces **no** row. +- Each row names the task, the step it left and the step it entered, when, why + (trigger), what kind of actor caused it, and that actor's **identifier** — + never a display name. +- The initiating session is recorded when one exists and is genuinely the + initiator; it is `NULL` otherwise. A transition with no session, or with + several candidate sessions and no single initiator, MUST record `NULL` rather + than pick one. +- Each row carries the collection-contract version it was written under. + +### Cross-cutting + +- Legacy rows and unstamped turns get `NULL` / absent — **never `0`, never + `""`**. +- Both slices publish a machine-readable activation point, because the + downstream extract is a point-in-time snapshot with no schema versioning: a + column whose meaning changes mid-series is silently discontinuous. +- Both writers are observable at runtime, and the set of code paths that may + mutate a task's workflow step is pinned by a test (see *Writer health*). + +## Requirements + + + +### REQ-TASKS-TRANSITION-LEDGER-001: Workflow task-step transition ledger + + + +**Intent:** The task system records forward-looking workflow-step boundaries so downstream consumers can attribute work to a step without reconstructing history. + + + +#### Acceptance criteria + + + +- **AC-TASKS-TRANSITION-LEDGER-001.1:** When a task turn or workflow-step change occurs after activation, the system shall record the applicable step boundary and its provenance. +- **AC-TASKS-TRANSITION-LEDGER-001.2:** When consumers inspect the ledger, the system shall distinguish post-activation evidence from historical intervals that were not backfilled. + + + +## Out of scope + + + +Historical reconstruction, exact per-step cost allocation, and replacement of session_step_history are excluded. \ No newline at end of file diff --git a/docs/specs/tasks/attach-workspace-sources.md b/docs/specs/tasks/system-design/attach-workspace-sources.md similarity index 94% rename from docs/specs/tasks/attach-workspace-sources.md rename to docs/specs/tasks/system-design/attach-workspace-sources.md index 4b614d3f37..a3e3022826 100644 --- a/docs/specs/tasks/attach-workspace-sources.md +++ b/docs/specs/tasks/system-design/attach-workspace-sources.md @@ -1,10 +1,25 @@ --- -status: building +status: current +system: tasks +requirements: + - REQ-TASKS-ATTACH-WORKSPACE-SOURCES-001 created: 2026-07-22 -owner: kandev +owners: + - kandev --- +# Attach Workspace Sources System Design -# Attach Workspace Sources +## Purpose and boundaries + +This design record preserves the technical source for the capability mapped to REQ-TASKS-ATTACH-WORKSPACE-SOURCES-001 while the task system completes its migration. + +## Requirement mapping + +| Requirement | Design source | +| --- | --- | +| REQ-TASKS-ATTACH-WORKSPACE-SOURCES-001 | Migrated legacy design detail below | + +## Migrated design source ## Why @@ -73,9 +88,9 @@ manually moving files into the task workspace. not change the running agent or terminal working directory. Its MCP result returns the exact new worktree path, the promoted task workspace path, and that the agent CWD did not change. -Decisions: [ADR-2026-07-22-runtime-mutable-task-workspace-sources](../../decisions/2026-07-22-runtime-mutable-task-workspace-sources.md) -[ADR-2026-07-23-workspace-source-root-move-boundary](../../decisions/2026-07-23-workspace-source-root-move-boundary.md), -and [ADR-2026-07-27-legacy-add-branch-live-rescan](../../decisions/2026-07-27-legacy-add-branch-live-rescan.md). +Decisions: [ADR-2026-07-22-runtime-mutable-task-workspace-sources](../../../decisions/2026-07-22-runtime-mutable-task-workspace-sources.md) +[ADR-2026-07-23-workspace-source-root-move-boundary](../../../decisions/2026-07-23-workspace-source-root-move-boundary.md), +and [ADR-2026-07-27-legacy-add-branch-live-rescan](../../../decisions/2026-07-27-legacy-add-branch-live-rescan.md). ## Data model @@ -151,7 +166,7 @@ task. The caller may target only itself or a same-workspace direct child. The ta the bound caller task/session provenance and the backend verifies it before mutation; provenance is not an agent-supplied tool argument. Exact normalized repository and folder retries are idempotent, while contradictory source identities remain conflicts. See ADR -[`2026-08-19-parent-authorized-child-workspace-sources`](../../decisions/2026-08-19-parent-authorized-child-workspace-sources.md). +[`2026-08-19-parent-authorized-child-workspace-sources`](../../../decisions/2026-08-19-parent-authorized-child-workspace-sources.md). `add_branch_to_task_kandev` preserves its existing request arguments and returns: @@ -341,7 +356,7 @@ persisted; every relaunch and resume of that task reuses the persisted name. ## Implementation plan -See [Attach Workspace Sources plan](../../plans/attach-workspace-sources/plan.md) and the -[live add-branch compatibility repair plan](../../plans/restore-live-add-branch/plan.md), plus the -[multi-repository chat file-link repair plan](../../plans/multi-repo-chat-file-links/plan.md) and the -[owned link target mismatch repair plan](../../plans/owned-link-target-mismatch-repair/plan.md). +See [Attach Workspace Sources plan](../../../plans/attach-workspace-sources/plan.md) and the +[live add-branch compatibility repair plan](../../../plans/restore-live-add-branch/plan.md), plus the +[multi-repository chat file-link repair plan](../../../plans/multi-repo-chat-file-links/plan.md) and the +[owned link target mismatch repair plan](../../../plans/owned-link-target-mismatch-repair/plan.md). \ No newline at end of file diff --git a/docs/specs/tasks/system-design/clarification-active-lifecycle.md b/docs/specs/tasks/system-design/clarification-active-lifecycle.md new file mode 100644 index 0000000000..7acfe2714b --- /dev/null +++ b/docs/specs/tasks/system-design/clarification-active-lifecycle.md @@ -0,0 +1,217 @@ +--- +status: current +system: tasks +requirements: + - REQ-TASKS-CLARIFICATION-LIFECYCLE-001 + - REQ-TASKS-CLARIFICATION-SCENARIOS-001 +--- + + +# Active clarification lifecycle System Design + + + +## Purpose and boundaries + + + +The task system owns clarification bundle authority, turn scoping, response persistence, and detached-resume recovery. The UI and agent systems consume the resulting projections and events. + + + +## Requirement mapping + +| Requirement | Design source | +| --- | --- | +| `REQ-TASKS-CLARIFICATION-LIFECYCLE-001` | Extracted from the legacy design sections below. | +| `REQ-TASKS-CLARIFICATION-SCENARIOS-001` | Extracted from the legacy design sections below. | + + +## Data model + +No schema change. + +- Clarification questions remain `task_session_messages` rows with + `type = "clarification_request"`. +- Rows in one bundle share `metadata.pending_id`; terminal status remains in `metadata.status`. +- `task_session_messages.turn_id` associates a question with its turn. The newest authoritative durable + `task_session_turns` record for the session identifies the current turn; deleting messages does not + delete that parent turn or move ownership backward. +- A pre-acknowledgement successor carries `metadata.prompt_dispatch_pending=true` plus the source + clarification turn, pending ID, and exact claimed message IDs. An empty row carrying only the pending + marker is not turn authority. Immediately before external dispatch, + `metadata.prompt_dispatch_attempted=true` records the at-most-once boundary and makes the successor + authoritative if the process stops in the ambiguous window. A message referencing the successor is + independent durable evidence of ambiguous acceptance. +- `metadata.agent_disconnected=true` records that no in-memory waiter owns an otherwise active bundle. +- A superseded row may retain `metadata.status = "pending"`. Pending metadata is historical evidence, + not sufficient proof that the request is operational. +- A missing-status row in an older turn is superseded history. Turn ownership takes precedence over + the legacy rule that missing status means pending. +- `TaskSession.pending_action` and `TaskStatusSummary.pending_action` remain bounded derived fields. + They are reconstructable and never become independent clarification state. + +## API surface + +No new route. Task-session responses add `pending_action_revision` beside the existing +`pending_action` projection. + +- `GET /api/v1/tasks/:taskId/task-sessions` continues to expose each session's current derived + `pending_action`; task navigation uses this existing field. +- Semantic `session.message.added`, `session.message.updated`, and `session.message.deleted` + notifications include the authoritative per-session `pending_action` after mutations that can + change it. The field is explicit null when clean and omitted when projection fails or the event is + a replaceable content-only update; clients preserve the prior value when it is omitted. Each + projection includes a `pending_action_revision`, shared with REST task-session snapshots. Its + decimal epoch is a database-backed, monotonically allocated backend generation; sequence orders + reads within that generation. Clients compare both fields and reject any older result, including + an unseen pre-restart epoch delivered after client state has been rebuilt. +- `GET /api/v1/task-sessions/:sessionId/turns` continues to expose durable turn history; + unpublished reservations stay hidden until publication or durable message evidence, including while + an attempt marker makes them internal current-turn authority. Attempted reservations are preserved + across restart because their dispatch outcome is ambiguous. Visible history is ordered ascending by + `started_at`, `created_at`, then `id`, matching the reverse ordering used to select the current turn. +- Task list, workflow snapshot, and boot payloads continue to expose task-level `pending_action` in + the status summary and legacy fallback fields. +- `POST /api/v1/clarification/:pendingId/respond` uses one state-based contract: + - `active_live`: answer or rejection returns success and is delivered to the same-turn waiter. + - `active_detached`: an answer returns success only after the orchestrator acknowledges one resume + dispatch and durably publishes its successor within a bounded wait; it does not wait for the resumed + turn to complete. Rejection returns success and persists without resuming the agent. If resume + acceptance fails, an answer returns a server error and the still-current bundle remains answerable + for retry. If acceptance succeeds but successor publication fails, the endpoint returns a + non-retryable server error and keeps the bundle terminal. + - `superseded_history` or `terminal`: answer or rejection returns conflict, performs no write, and + initiates no agent resume. + - `delivery_claimed`: a concurrent second response returns conflict, performs no write, and + initiates no agent resume because the first response already claimed every pending row. +- `POST /api/v1/clarification/:pendingId/cancel` remains the low-level cancellation path for a request + still owned by the in-memory clarification store. The chat Skip control uses `/respond` with + `rejected=true`, including for detached requests. + +## State machine + +One clarification bundle has five operational states: + +1. `active_live`: rows are pending in the current turn and an in-memory waiter exists. +2. `active_detached`: rows are pending in the current turn, no waiter exists, and + `agent_disconnected=true` records deferred-answer behavior. +3. `delivery_claimed`: current-turn rows carry their provisional terminal answer plus a durable + response-delivery intent, but no handoff boundary has been acknowledged yet. +4. `terminal`: every actionable row is answered, rejected, cancelled, expired, or deleted, with no + outstanding delivery intent. +5. `superseded_history`: rows still carry pending history, but a newer turn is current. + +Transitions: + +- Request creation enters `active_live`. +- Wait timeout, disconnect, or turn teardown moves `active_live -> active_detached` once. +- A response first moves either active state to `delivery_claimed`. Successful answer delivery or Skip + then moves that exact `pending_id` to `terminal`. For a live response, the waiter must durably confirm + consumption before returning it to the agent. A backend restart before any handoff restores a + still-current `delivery_claimed` bundle to its prior active state; if a newer turn or terminal session + already superseded it, recovery retires the intent and preserves terminal history. Cancel, expiry, + or deletion moves an active state directly to `terminal`. A failed detached resume acceptance returns to + `active_detached` while the same turn remains current only when dispatch is known not to have been + accepted and rollback succeeds. A post-attempt crash or post-acceptance publication failure remains + terminal because the prompt may already be running. +- Acceptance of a newer turn moves any older pending bundle to `superseded_history` operationally; + no history rewrite is required. +- Neither `terminal` nor `superseded_history` can become active again. Only the provisional + `delivery_claimed` state is recoverable. A new request creates a new + bundle identity; message deletion cannot reverse this transition. + +## Permissions + +No authorization change. A user can see, answer, or dismiss only clarification data for a task and +session they can already access. Session selection does not broaden task visibility. + +## Failure modes + +- Active-state repository read fails: workflow guarding fails closed, and projections keep the last + known pending value. A later message event or list/boot read retries convergence. +- Terminal-session expiry persistence fails: the terminal session state still quarantines pending + history from task/session projections and interactive overlays; a stale response claim remains + rejected by the terminal-session predicate. +- A forced task-session list cannot project authoritative pending actions: fail the HTTP or WebSocket + request instead of returning a successful list with empty pending ownership. +- Summary compare-and-set loses a race: reload the newer summary, reapply authoritative pending state, + and retry within a bounded loop. Exhaustion is an error, so restored-state acknowledgement is withheld + until the pending action is durably confirmed. Task-list and boot reads explicitly invalidate that + task's stale summary on repair failure so an unchanged browser cache clears it and exposes the + authoritative coarse pending action. Ordinary summary omission remains a partial-response no-op, and + an invalidating response cannot erase a newer live summary received while that read was in flight. + Never overwrite unrelated newer summary fields. +- Summary repair persists but its WebSocket publication fails: the initiating response carries the + corrected summary; other clients converge on their next event or read. +- A stale browser submits an older-turn answer: return conflict, do not update runtime ownership, and + do not dispatch a prompt. +- Detached resume context resolution or orchestrator acceptance fails: use a non-cancelled context with + a finite deadline for acceptance and persistence, withhold terminal message events, restore the + still-current bundle to pending, and return a retryable server error instead of reporting false + success. Attempt to refresh and persist the task summary synchronously from authoritative pending + rows, and publish the committed restored messages even if that acknowledgement fails. A later event + or read repairs the summary cache; the response still reports that the durable answer can be retried. +- Persisting a successor turn or dispatch-attempt marker fails: use a fresh bounded context to roll back + the session claim and any reserved successor before making an external executor call, restore the + still-current bundle, and return a retryable server error. +- Reserved-successor rollback fails or has an ambiguous durable outcome: keep the live reservation + unresolved so ready handling waits and later prompt admission remains blocked until restart recovery + reconciles the row. +- Clarification detachment, expiry persistence, and terminal bundle publication ignore request + cancellation but always use a fresh bounded context, so a database lock or synchronous summary + refresh cannot hold the per-session pause or HTTP response indefinitely. +- Agentctl accepts a detached resume but durable successor publication fails: return a non-retryable + server error, keep the claimed bundle terminal, and make later rollback fail closed so the accepted + answer cannot be dispatched again in-process. +- Live waiter delivery fails unexpectedly: restore the durable claim only if its turn remains current, + report whether retry state was recovered, and never restore it after a successor turn is accepted. A + started confirmation may finish after the responder's bounded wait, but it cannot mutate the + responder's claim snapshot and its durable finalization serializes against that restore. +- Historical partial terminalization leaves pending and terminal siblings in one current-turn bundle: + complete the pending siblings without rewriting terminal history or returning a permanent conflict. +- A malformed persisted pending row has no matching durable turn: drain any live in-memory waiter, but + keep the row inert. If such pre-turn history is encountered, repair it through explicit data cleanup + rather than treating it as current input authority. +- Session loading fails during task activation: retain existing navigation fallback instead of + stranding the user in the task drawer or on an unchanged URL. +- A newer task-summary revision arrives while pending-owner loading is in flight: discard the delayed + owner-session result, open the requested task through the task-only fallback, and let the current + projection render the authoritative pending owner. +- Backend stops after reserving a detached-answer successor but before the attempt marker: startup + restores only the clarification rows claimed for that dispatch and removes the empty reservation. +- Backend stops after the attempt marker but before dispatch acknowledgement: startup fails closed, + preserves the successor as authoritative, and keeps its exact clarification claim terminal. Output + referencing the reservation provides the same conservative authority even without the marker. +- A delayed predecessor ready event arrives between the attempt marker and agentctl acknowledgement: + wait for the live reservation outcome, then reject the event if its prompt generation was superseded; + never complete the reserved successor or evaluate predecessor workflow completion against it. If the + reservation rolls back and the predecessor generation still owns the session, process the ready event + normally instead of stranding the predecessor turn. Drop a generationless event after the wait because + it cannot be correlated safely with the predecessor. +- Unpublished-reservation reconciliation fails during startup: fail startup before event processing or + prompt admission begins so no new turn can supersede an unrecovered clarification claim. +- Response-delivery intent reconciliation fails during startup: fail startup before event processing or + prompt admission begins. Never leave an unhanded terminal claim permanently unanswerable. +- Unpublished-reservation recovery metadata contains a non-string or empty claimed-message ID: fail the + recovery transaction and startup, preserving the reservation and claimed rows for diagnosis and retry. + +## Persistence guarantees + +- Message history remains durable and is not destructively rewritten merely because a newer turn + exists. +- Active clarification state is reconstructable after restart from message status plus the newest + authoritative durable turn. +- Current-turn ownership is reconstructable from durable turn rows even when a turn has no remaining + messages. +- Unpublished detached-answer reservations carry enough recovery identity to restore only their own + claimed rows. A durable attempt marker separates safe rollback from ambiguous dispatch, and startup + reconciliation runs even when no executor record remains. +- Provisional terminal response claims carry a durable delivery intent. Startup restores that exact + current-turn claim when no live or detached handoff became authoritative, while prompt-reservation + recovery owns any detached dispatch that reached its durable reservation boundary. +- Task summaries are caches. Boot and task-list reads correct a stale persisted `pending_action` with + a monotonic revision while preserving all unrelated summary fields. +- No one-off mutation or backfill of an existing installation database is required. Deploying the + corrected derivation makes historical older-turn pending rows inert and repairs their summaries on + normal reads. diff --git a/docs/specs/tasks/system-design/external-id-idempotency-operations.md b/docs/specs/tasks/system-design/external-id-idempotency-operations.md new file mode 100644 index 0000000000..ef2bd7cefb --- /dev/null +++ b/docs/specs/tasks/system-design/external-id-idempotency-operations.md @@ -0,0 +1,170 @@ +--- +status: draft +system: tasks +requirements: + - REQ-TASKS-EXTERNAL-ID-001 + - REQ-TASKS-EXTERNAL-ID-SCENARIOS-001 + - REQ-TASKS-EXTERNAL-ID-BOUNDARIES-001 +--- + + +# External task ID idempotency operations System Design + + + +## Purpose and boundaries + + + +This design records the operational, failure, persistence, and current-state evidence needed to preserve the idempotency contract. + + + +## Requirement mapping + +| Requirement | Design source | +| --- | --- | +| `REQ-TASKS-EXTERNAL-ID-001` | Extracted from the legacy design sections below. | +| `REQ-TASKS-EXTERNAL-ID-SCENARIOS-001` | Extracted from the legacy design sections below. | +| `REQ-TASKS-EXTERNAL-ID-BOUNDARIES-001` | Extracted from the legacy design sections below. | + + +## Permissions + +- The external ID is workspace-scoped data and inherits the workspace's existing + authorization rules with no additions. +- Authorization precedes all external-ID work. An unauthorized caller receives + `404 {"error": "task not created"}` for a create and + `404 {"error": "task not found"}` for a lookup or release, and cannot learn + whether an identity exists in that workspace or whether its task is settled. +- Because uniqueness is workspace-scoped and workspaces are per-owner when auth + is enabled, two users cannot collide on each other's external IDs or probe + each other's namespaces. +- An in-session agent calling `create_task_kandev` is already scoped to its + task's workspace owner by the MCP identity scoper. +- Release is a workspace-scoped mutation requiring the same authorization as any + other write to that workspace. + +## Failure modes + +| Condition | Behavior | +|---|---| +| Two concurrent creates race past the step-3 lookup | The partial unique index admits exactly one. The loser's insert transaction rolls back — including its task row and runner participant — then it re-reads by `(workspace_id, external_id)` and returns that task as a Found outcome. Neither sees a `5xx`, and no orphan row survives. A loser that already allocated an office identifier has burned a sequence number; see the row below. | +| A concurrent loser burned a task-identifier sequence number | Accepted. The step-3 lookup makes this rare (it requires two creates to interleave within the window between lookup and insert), and the consequence is a gap in the office identifier sequence, not a correctness failure. Identifier sequences are not required to be contiguous. | +| Unique-violation classification across dialects | The violation MUST be attributed to `uniq_tasks_external_id` specifically — by constraint name on PostgreSQL via typed `pgconn.PgError` inspection, and by the constraint-bearing message on SQLite. A bare `23505` check is insufficient: it matches any unique violation, so a primary-key failure would be misread as a Found outcome. | +| Crash between the task insert and settlement | The task exists unsettled. A retry returns **Found, unsettled**. Nothing is deleted or repaired. | +| Crash after settlement, before the response reaches the caller | A retry returns **Found, settled**. | +| The task is deleted while its create is still running | Settlement affects zero rows. Async dispatch is suppressed, state is re-read, and the response reflects reality rather than claiming a create that no longer exists. | +| The identity is released while its create is still running | Settlement affects zero rows (the `external_id` predicate no longer matches). Same handling as above; the task survives without an identity. | +| Found outcome where `start_agent: true` was requested | No agent launched, no session created. This is the central safety property. | +| Found outcome on an archived task | The archived task is returned with `archived_at` set. Not auto-unarchived. | +| Found outcome where the request carried different repositories, attachments, parent, or policy | All ignored; the existing task is returned unmodified. | +| A retry fails handler validation because server state changed | The request fails with that handler's existing error. Documented, not worked around. Use the lookup route for drift-robust recovery. | +| Workspace deleted | Its tasks are deleted; their identities go with them. | +| A caller supplies an external ID on a subtask create | Accepted and applied to the subtask, scoped to the effective workspace. Never inherited from the parent. | + +## Persistence guarantees + +- Both columns live on `tasks` and survive restart with the row. Nothing is + cached, and there is no TTL. +- **There is no idempotency window.** A held identity resolves against the + current table however old the task is. +- An unsettled task persists indefinitely. Kandev never sweeps, expires, or + repairs one. It is visible and mutable like any other task. +- A released or deleted identity is gone irreversibly; no history is retained. + +## Appendix: verified current state + +Claims checked against the code at `09fa82144` (a fast-forward of `origin/main` +at `cba3838c8`). + +**In-scope entry points.** REST `POST /api/v1/tasks` (`task_handlers.go:140` → +`httpCreateTask` at `task_http_handlers.go:826`, body struct at `:730`) and MCP +`create_task_kandev` (tool schema `internal/mcp/server/server.go:805`, client +handler `internal/mcp/server/handlers.go:142`, backend handler at +`internal/mcp/handlers/handlers.go:559`). Both converge on +`taskservice.Service.CreateTask` (`internal/task/service/service_tasks.go:124`), +which authorizes first at `:125`. + +**Why the lookup must precede allocation and admission.** `assignIdentifier` +runs at `service_tasks.go:160`, before `createTaskWithCapacity` at `:166`, and +calls `IncrementTaskSequence` — an unconditional +`UPDATE workspaces SET task_sequence = task_sequence + 1` +(`repository/sqlite/task.go:2328`). Admission checks precede the insert +(`task.go:215`) and can return before it (`:257`). Both were confirmed by +adversarial review. + +**Why settlement needs the `external_id` predicate.** Release sets both columns +to `NULL`, so a predicate of `id` plus `external_id_settled_at IS NULL` still +matches a released row. + +**Why settlement lives in the handlers.** The service publishes `task.created` +and returns before caller-specific work (`service_tasks.go:171`, `:190`). REST +then claims attachments (`task_http_handlers.go:912`), attaches policy (`:933`), +commits a fresh branch (`:947`), and prepares/starts a session (`:954`); MCP +associates remote contributions (`handlers.go:715`), attaches policy (`:737`), +and launches (`:749`). + +**Why REST's session helper must be split.** Preparation errors are swallowed at +`task_http_handlers.go:1237` while the start goroutine is dispatched at `:1289`, +inside the same helper. PR association is background work at `:1201`. MCP +auto-start is a goroutine at `handlers.go:1303`. + +**The MCP data-loss path.** `resolveMCPRemoteContributions` derives contributions +from the request at `handlers.go:658`; the handler indexes them against the +returned task's repositories at `:715-719`; the guard at `:719` calls +`DeleteTask` at `:720`. Further rollback-delete paths at `:729` and `:741`. +Independently confirmed. + +**Schema placement.** The `tasks` DDL is +`internal/task/repository/sqlite/base_schema.go:295`. Per +`apps/backend/AGENTS.md`, a column added to an existing table must come from an +idempotent `ADD COLUMN` in `runMigrations()`, with anything referencing it +following there, not in schema init. Precedents at `base_migrations.go:165` and +`:1086`. The insert to extend is `insertTaskTx` (`repository/sqlite/task.go:316`), +whose task insert and runner participant are genuinely transactional — a unique +loser leaves no row and does not double-count WIP. + +**Response shape.** `createTaskResponse` (`task_http_handlers.go:761`) embeds +`dto.TaskDTO` (`internal/task/dto/dto.go:143`); the current success path returns +`http.StatusOK`, not `201` (`:960`). `TaskDTO` already carries the analogous +optional `Identifier` at `dto.go:195`. Unauthorized/not-found maps to `404` via +`handleNotFound` (`internal/task/handlers/errors.go:20`). + +**Representations are not all TaskDTO.** WS task lifecycle events are a +hand-built map in `publishTaskEventNow` (`service_events.go:367`). Plugins have +an independent proto message, SDK type, and mapper (`plugin.proto:216`, +`pkg/pluginsdk/data_types.go:92`, `internal/plugins/host_data.go:878`); +`CreateTaskResponse` holds only `Task task = 1` (`plugin.proto:456`) and SDK +`TaskReader.Create` returns `(*Task, error)` (`pkg/pluginsdk/host.go:138`). + +**Dialect handling.** Partial unique indexes work on both dialects. A typed +`pgconn.PgError` precedent exists at `repository/sqlite/task.go:2238`. Every +existing *unique*-violation helper matches SQLite text only +(`internal/office/repository/sqlite/wakeup_requests.go:246`, +`internal/github/store.go:1433`, `internal/prompts/service/service.go:64`, +`internal/sentry/store.go:948`), and per `apps/backend/AGENTS.md` this package +also runs against PostgreSQL. + +**Deletion shadow paths.** Office handoff cascades delete tasks through the +repository directly (`handoff_cascade.go:332`). This design is immune: the +identity is a column on the deleted row. + +**Design provenance.** Three independent adversarial reviews (OpenAI Codex, +read-only). Pass 1 (unique index): 10 P1s, including a crash between the row +commit and post-create work leaving a half-created task returned as complete. +Pass 2 (reservation table + lease + reclaim): NOT READY, 7 P1s — no fencing +token, destructive release of live claims, non-atomic tombstoning across +deletion shadow paths, an unimplementable completion protocol given async work, +unsettled rows visible to ordinary reads, unsatisfiable handler ordering. +Pass 3 (identity on the task row, honest reporting): NOT READY, 7 P1s — the +decisive one being that `complete: false` cannot distinguish a live create from +an abandoned one, so the release-and-recreate "recovery" it implied could +produce duplicate tasks and agents. + +This version responds by **removing the recovery claim rather than the +information**. The flag remains, explicitly diagnostic; the unsafe automated +action is named and forbidden; the settlement predicate, its zero-row handling, +its call site, and the lookup-before-write ordering are now specified concretely. +All factual findings from every pass were independently re-verified against +source before acceptance. diff --git a/docs/specs/tasks/system-design/external-id-idempotency.md b/docs/specs/tasks/system-design/external-id-idempotency.md new file mode 100644 index 0000000000..dc34121eac --- /dev/null +++ b/docs/specs/tasks/system-design/external-id-idempotency.md @@ -0,0 +1,540 @@ +--- +status: draft +system: tasks +requirements: + - REQ-TASKS-EXTERNAL-ID-001 + - REQ-TASKS-EXTERNAL-ID-SCENARIOS-001 + - REQ-TASKS-EXTERNAL-ID-BOUNDARIES-001 +--- + + +# External task ID idempotency System Design + + + +## Purpose and boundaries + + + +The task system owns external identity uniqueness, creation settlement, and the create response contract. Integrations own the caller's identity and retry policy. + + + +## Requirement mapping + +| Requirement | Design source | +| --- | --- | +| `REQ-TASKS-EXTERNAL-ID-001` | Extracted from the legacy design sections below. | +| `REQ-TASKS-EXTERNAL-ID-SCENARIOS-001` | Extracted from the legacy design sections below. | +| `REQ-TASKS-EXTERNAL-ID-BOUNDARIES-001` | Extracted from the legacy design sections below. | + + +## Data model + +Two columns on the existing `tasks` table. **There is no separate claim table.** + +``` +tasks + ...existing columns... + external_id TEXT nullable caller-supplied identity; + NULL when the task has none + external_id_settled_at TIMESTAMP nullable when creation finished; + NULL means not settled +``` + +- **Uniqueness:** partial unique index `uniq_tasks_external_id` on + `(workspace_id, external_id)` `WHERE external_id IS NOT NULL`. Supported by + both SQLite and PostgreSQL. Naming it matters — violation handling must be + attributable to this constraint specifically, never to "some unique index". +- **Settledness:** `external_id_settled_at IS NULL` means the create that + claimed this identity had not finished its required synchronous work. It does + **not** mean that create is dead. +- Empty or whitespace-only input normalizes to `NULL` (see *Validation*), so + tasks without an external ID never collide with each other. + +### Why the identity lives on the task row + +Putting the claim on the task itself, rather than in a side table, removes +whole classes of failure **by construction rather than by specification**: + +| Hazard | Why it cannot occur here | +|---|---| +| Claim and task disagreeing | They are the same row. Nothing to keep in sync. | +| Orphan claim after a task is deleted | The columns are deleted with the row, through *any* deletion path — including office handoff cascades that call the repository directly and bypass the service. | +| Non-atomic "mark deleted" transition | There is no transition. Deleting the row releases the identity. | +| E2E reset leaving stale claims | Reset already deletes tasks; the columns go with them. No reset change is needed. | +| A task with no claim, or a claim with no task | One row, one insert. Impossible. | + +### Validation and normalization + +Applied to the **raw** caller-supplied value, in this exact order. The order is +normative because it decides which error a malformed value produces. + +1. **Reject ASCII control characters** (U+0000–U+001F, U+007F) anywhere in the + raw value — before any trimming. Newline and tab are control characters and + are rejected here. Doing this before the trim is what makes a + trailing-newline value an error rather than something the trim silently + erases. +2. **Trim** leading and trailing Unicode whitespace (any code point with the + Unicode `White_Space` property). +3. **Empty after trim** → treat as absent (`NULL`). Not an error. +4. **Length** of the trimmed value in **UTF-8 bytes** MUST be ≤ 255. + +Everything surviving those rules is accepted verbatim: `jira:PROJ-1234`, +`gh-issue/kdlbs/kandev#2325`, a bare UUID, and non-ASCII letters are all valid. + +### Comparison semantics + +Matching is **byte-exact and case-sensitive** after normalization. `ext-1` and +`EXT-1` are different identities. + +To make that true on both dialects, `tasks.external_id` and its index MUST use a +binary/deterministic collation (SQLite `COLLATE BINARY`, the default; +PostgreSQL `COLLATE "C"` or another explicitly deterministic collation). An +unqualified `TEXT` column on PostgreSQL follows the database collation, which +may be case-insensitive or nondeterministic and would silently violate this +contract. No Unicode normalization is performed. + +### Lifecycle + +- **Set once at create.** No API changes an external ID on an existing task; + `PATCH /tasks/:id` and the update MCP/WS surfaces do not accept the field. +- **Settled once at create**, then never cleared or re-stamped except by release. +- **Archiving changes nothing.** An archived task still holds its identity. +- **Deleting frees the identity.** The row is gone. +- **Releasing frees the identity without deleting the task**, setting both + columns to `NULL`. + +### Idempotency is scoped to the task's lifetime + +Once the task holding an identity is deleted or released, that identity is free, +and a later create makes a new task. This is a real limitation and it is stated +without excuse: + +**A consumer that redelivers indefinitely must suppress redelivery for entities +it has itself deleted.** Kandev retains no record of a consumed identity, so it +cannot distinguish "never seen" from "seen and since deleted". + +An earlier draft argued that a caller could detect this because the returned +task ID differs from the one it recorded. That rationale is withdrawn: the +motivating scenario is a caller that crashed *before* recording any task ID, and +such a caller has nothing to compare against. + +The alternative — a durable tombstone of consumed identities — was rejected +because it requires the identity to outlive the task row, which reintroduces the +side table and every synchronization hazard the table above removes. If a real +consumer cannot suppress its own post-deletion redeliveries, that is the trigger +to revisit this, and it should be treated as a design change rather than a +tweak. + +**This limitation does not affect the motivating crash-recovery case**, where no +deletion occurs. + +## State machine + +Two observable states for a task holding an external ID: + +| State | Representation | Meaning | +|---|---|---| +| **unsettled** | `external_id` set, `external_id_settled_at IS NULL` | The create that claimed this identity had not finished its required synchronous work at observation time. It may still be running. | +| **settled** | both set | That work finished. | + +Transitions: + +| From | To | Trigger | +|---|---|---| +| (none) | unsettled | A create wins the unique index and commits the task row | +| unsettled | settled | That same create completes its required synchronous work | +| any | (none) | The task is deleted, or the identity is released | + +There is no reclaim, no lease, no timeout, and no ownership token, because +nothing in this design ever mutates another caller's task. + +### Create sequence (normative) + +``` +1. authorize workspace ← already first in CreateTask +2. validate + normalize external_id +3. LOOKUP by (workspace_id, external_id) ← before ANY write or allocation + └─ found → return Found outcome, stop. Nothing else runs. +4. required create-time validation +5. identifier allocation, WIP admission, task-row insert (incl. external_id) + └─ unique violation on uniq_tasks_external_id → roll back, re-read, Found +6. required synchronous post-create work +7. SETTLE (conditional UPDATE, see below) +8. asynchronous dispatch (agent launch, PR association) +``` + +**Step 3 is load-bearing and its position is normative.** It must precede +identifier allocation, WIP admission, and every other write. Two concrete +consequences of getting this wrong, both of which violate the no-side-effects +requirement: + +- `assignIdentifier` runs before persistence and calls `IncrementTaskSequence`, + an unconditional `UPDATE workspaces SET task_sequence = task_sequence + 1`. + A found outcome resolved after that point permanently burns a sequence number. +- WIP admission can reject a create before the insert is ever attempted, so a + retry for an already-held identity would fail with a capacity error instead of + returning the existing task. + +Step 5's unique-violation path remains as the **TOCTOU backstop** for the narrow +race between the step-3 lookup and the insert. It is not the primary mechanism. + +**The backstop is not reachable from every failure, and that gap must be closed +explicitly.** WIP admission runs *before* the insert and can return a capacity +error without ever attempting it. So a request that missed at step 3, then lost +the race, can surface a capacity failure instead of the Found outcome the +contract promises. + +Therefore: **after a step-3 miss, any pre-insert failure — capacity, admission, +or otherwise — MUST trigger a re-read by `(workspace_id, external_id)` before +that failure is returned.** If a task now holds the identity, the request +returns the Found outcome instead of the failure. Only if the re-read still +finds nothing does the original error surface. + +This is narrow but load-bearing: without it, "a retry always returns the +existing task" is false precisely when the destination step is at its WIP limit, +which is exactly when a task is most likely to already exist there. + +### Settlement (normative) + +Settlement is a single conditional UPDATE. The predicate MUST include +`external_id`: + +```sql +UPDATE tasks + SET external_id_settled_at = ? + WHERE id = ? + AND external_id = ? + AND external_id_settled_at IS NULL +``` + +`external_id` in the predicate is **required, not defensive**. Release sets both +`external_id` and `external_id_settled_at` to `NULL`; a predicate guarding only +on `id` and `external_id_settled_at IS NULL` would still match a released row +and stamp a settlement onto a task that no longer holds any identity. + +**The affected-row count MUST be checked.** + +- **One row** — settlement succeeded. Proceed to asynchronous dispatch. +- **Zero rows** — the task was deleted or its identity released while this + create was running. The implementation MUST re-read current state and MUST NOT + dispatch asynchronous work. It then splits by what it finds: + - **The task exists but no longer holds the identity** (it was released) → + return outcome `CreatedIdentityLost`: `200`, `deduplicated: false`, + `creation_complete: true`, and `external_id` absent from the body. The work + finished; only the stamp had nowhere to land. + - **The task no longer exists** (it was deleted) → return the surface's + existing not-found error. There is no task to describe. + + It MUST NOT report outcome `Created`, and MUST NOT include `external_id` in + the body — either would claim an identity the task no longer holds. Note that + `creation_complete: true` **is** correct here and is not what distinguishes + this outcome: the create's synchronous work did finish. The absent + `external_id` is the distinguishing signal. + +Zero rows is a legitimate outcome of a benign race, not an error to log and +ignore. + +### Settlement call site (normative, per surface) + +The service cannot settle, because required synchronous work continues in the +handlers after `CreateTask` returns. Settlement is therefore the **handler's** +responsibility, and the covered steps differ per surface: + +| Surface | Required synchronous steps that must succeed before settling | Settle after | +|---|---|---| +| REST | attachment claim, workspace-policy attach, fresh-branch commit | fresh-branch commit, and **before** any session dispatch | +| MCP | remote-contribution association, workspace-policy attach | policy attach, and **before** auto-start dispatch | + +**REST's session helper must be split.** It currently performs synchronous +preparation and asynchronous dispatch in one call, swallowing preparation +errors before dispatching the start goroutine. Settling after that helper would +place settlement after async dispatch has already begun, violating step 7 → 8 +ordering. The helper must expose preparation and dispatch separately so +settlement can sit between them. + +**Best-effort steps do not block settlement.** Task-create last-used recording +and PR association are best-effort today; their failure MUST NOT prevent +settlement and MUST NOT trigger compensation. Only the steps listed in the table +above gate it. + +### What "settled" does and does not mean + +`external_id_settled_at` means **the required synchronous setup finished**. It +does *not* mean an agent is running, and it does not mean the create's process +is alive or dead. + +Callers needing to know whether an agent is running read session state, which +already exists and is already authoritative. + +## API surface + +### Service contract + +`Service.CreateTask` currently returns `(*models.Task, error)` and carries no +outcome signal. It SHALL return: + +``` +CreateTaskResult { + Task *models.Task + Outcome enum { Created, FoundSettled, FoundUnsettled, CreatedIdentityLost } +} +``` + +`CreatedIdentityLost` is the fourth outcome, and it exists solely to represent +the settlement zero-row race: this request created the task and finished its +required synchronous work, but by the time settlement ran the identity had been +released by another actor, so the task survives holding no external ID. Without +it the contract has a reachable state with no representation — the handler would +have to claim an identity the task no longer holds, or invent an undocumented +response. See *Settlement*. + +It is deliberately distinct from `FoundUnsettled`: that one means *someone +else's* create is unfinished, while this one means *this* create finished but +lost its identity. Callers act differently — the first says "wait or escalate", +the second says "your task exists, but the identity you asked for is now free +and someone may claim it". + +**It carries `creation_complete: true`.** The work finished; only the settlement +stamp had nowhere to land. Reporting `false` would break the field's single +definition and falsely suggest in-progress work. The absent `external_id` is +what identifies this outcome. + +If the task was **deleted** rather than released during that window, there is no +task to return and the request surfaces the appropriate not-found error; that is +an error path, not this outcome. + +Callers MUST skip their post-create work on both `Found*` outcomes: + +| Caller | In scope? | Post-create work to skip | +|---|---|---| +| REST `httpCreateTask` | **Yes** | attachment claim, workspace-policy attach, fresh-branch commit, session prepare/start, task-create last-used recording, PR association | +| MCP `handleCreateTask` | **Yes** | remote-contribution association, workspace-policy attach, auto-start launch | +| WS `wsCreateTask` | Deferred | agent launch, last-used recording | +| Plugin host `Tasks().Create` | Deferred | `StartAgent` best-effort start | + +The deferred rows cannot misbehave in this iteration because their surfaces do +not accept an external ID, so the outcome is always `Created` for them. They are +listed so enabling one later starts from a complete inventory. + +### ⚠️ The MCP skip is a data-loss guard + +Running MCP's post-create steps on a found outcome can **delete the existing +task**. The handler resolves remote contributions from the *request* +(`handlers.go:658`), then indexes them against the *returned task's* +repositories (`:715-719`). A found outcome returns a task whose repository list +need not match the retry payload, and the index guard at `:719` calls +`DeleteTask` at `:720`. Two further rollback paths do the same at `:729` and +`:741`. The sequence: + +> retry a create with an external ID → found outcome returns the existing task → +> post-create steps wrongly run → repository index mismatch → rollback +> **deletes the task the caller was trying to recover** + +That turns crash recovery into data loss. Skipping these steps on a found +outcome is a **correctness requirement**, and its scenario is required coverage. + +### REST — create + +`POST /api/v1/tasks` gains one optional request field, `external_id`. + +Response additions, both **always present** on every create response: + +```jsonc +{ + "id": "…", + "external_id": "jira:PROJ-1234", // omitted when the task holds none + "deduplicated": false, // true for both Found outcomes + "creation_complete": true, // false ONLY for Found, unsettled + // …all other existing task DTO fields… +} +``` + +`deduplicated` and `creation_complete` are required booleans, not presence-only +markers. A presence-only field makes a serialization bug indistinguishable from +a genuine fresh create. + +The field is named `creation_complete`, not `complete`, because it describes one +narrow thing — that the create's required synchronous setup finished — and a +broader name invites callers to read it as "the task is done" or "the agent is +running". Every schema and tool description MUST carry that narrow meaning. + +The four success outcomes map to exhaustive, mutually exclusive tuples: + +| Outcome | Status | `deduplicated` | `creation_complete` | `external_id` in body | +|---|---|---|---|---| +| Created | `200` | `false` | `true` | present | +| Found, settled | `200` | `true` | `true` | present | +| Found, unsettled | `200` | `true` | `false` | present | +| Created, identity lost | `200` | `false` | `true` | **absent** | +| `external_id` fails validation | `400` | — | — | — | +| Caller not authorized for `workspace_id` | `404` `{"error": "task not created"}` | — | — | — | + +Reading the tuples: + +- **`creation_complete: false` occurs in exactly one outcome**, `Found, + unsettled`, and only ever alongside `deduplicated: true`. That is the one + tuple meaning "another create may still be in progress." +- **`Created, identity lost` carries `creation_complete: true`**, because this + create's required synchronous work genuinely did finish — only the settlement + stamp had nowhere to land, since the identity was released. Reporting `false` + would contradict the single definition of the field and would falsely imply + in-progress work. +- It is distinguished from `Created` by the **absent `external_id`**, which is + the literal truth: the task no longer holds one. A caller seeing this tuple + has a valid task ID and should treat the external identity as unclaimed. + +All four success outcomes are `200` with the task body. There is no `409` and +no `410`: an unsettled task is a fact to report, not a conflict to raise, and a +freed identity is indistinguishable from one never used. Keeping fresh creates +at `200` also avoids a breaking change for existing clients. + +### The probe, and what MCP has instead + +**REST callers get a true probe:** the lookup route below. It reads and returns; +it never creates. + +**MCP callers do not.** A create carrying an external ID is *idempotent* — safe +to repeat, no duplicates, no side effects when a holder exists — but it is not +side-effect-free, because when no holder exists it creates a task. Calling that +a probe would be wrong, and an MCP agent told it was one could use it to "check" +an identity and be surprised to find it had made something. + +The MCP tool description MUST therefore say plainly: this creates the task if +nothing holds the identity yet. + +No MCP lookup tool is added in this iteration because no in-scope MCP flow needs +to ask without being willing to create — an agent reaching for an identity is +reaching for the task. If a flow appears that genuinely needs to ask first, add +a read-only tool then; it is a small, additive change. + +### REST — lookup + +``` +GET /api/v1/workspaces/:id/tasks/by-external-id?external_id= +``` + +| Situation | Status | Body | +|---|---|---| +| A task holds it (including archived, including unsettled) | `200` | task DTO with `creation_complete` | +| No task holds it | `404` | `{"error": "task not found"}` | +| `external_id` missing or fails validation | `400` | `{"error": ""}` | +| Caller not authorized for the workspace | `404` | `{"error": "task not found"}` | + +Read-only; no side effects. + +### REST — release + +``` +DELETE /api/v1/workspaces/:id/tasks/by-external-id?external_id= +``` + +Sets `external_id` and `external_id_settled_at` to `NULL` on the task that holds +it. Does **not** delete or otherwise modify the task. Returns `204` on success, +`404` when no task holds it, `400` on validation failure, `404` when +unauthorized. + +Release is an operator action for an identity a human has determined is +abandoned. See *The one unsafe thing a caller can do* for why it must not be +automated in response to `creation_complete: false`. + +### Request ordering and error precedence + +Two orderings are normative: + +1. **Authorization precedes any external-ID work.** An unauthorized caller + receives the existing unauthorized response and SHALL NOT learn whether an + external ID exists there, whether it is held, or whether its task is settled. + `Service.CreateTask` already authorizes as its first statement. +2. **Validation of the external ID precedes the lookup**, so a malformed value + is a `400` rather than an accidental lookup. + +The step-3 lookup then precedes all remaining create-time work, per +*Create sequence*. + +**Honest limitation — a retry can still fail before dedupe.** Both in-scope +handlers perform validation *before* reaching the service: REST validates +attachments, launch-profile requirements, and repositories; MCP resolves +repositories, workflow, workspace, contributions, and launch metadata. This spec +does not require restructuring them. Two distinct causes follow: + +- **Payload drift** — the retry sends something different from the original. + Mitigated by replaying the original payload. +- **Server-state drift** — the retry is byte-identical, but the world changed: + a repository was deleted, a workflow or step was removed, an agent profile was + disabled, a parent task was archived, a credential expired. **Replaying the + original payload does not help here.** + +Server-state drift means the "a caller that crashed and came back a week later +still finds its task" property holds for the **lookup route**, which touches +none of that validation, but **not necessarily for the create path**. A caller +that wants recovery robust against long absences should use the lookup route +first and fall back to create only when the lookup returns `404`. + +This is a real weakening relative to an idealized idempotent create, and it is +stated rather than implied. Engineering around it would require a create +coordinator that resolves the identity before any handler validation, which was +judged not worth its cost. + +### MCP + +`create_task_kandev` gains one optional string parameter: + +- `external_id` — "A stable identifier from your own system (issue key, webhook + delivery ID, a UUID you generated). Creating a task twice with the same + `external_id` in the same workspace returns the first task instead of making a + duplicate — use it when a retry or restart could re-run this call. Replay the + same arguments you sent the first time." + +The tool result is the task as JSON carrying `external_id`, `deduplicated`, and +`creation_complete`. The tool description MUST state all three of these: + +1. **This creates the task when nothing holds the identity yet.** It is + create-if-absent, not a lookup. An agent must not call it merely to check. +2. **`deduplicated: true`** means the task already existed and was not created, + so the agent must not report having created something new. +3. **`deduplicated: true` together with `creation_complete: false`** means + another create claimed this identity and had not finished when observed, and + **may still be in progress** — so the agent must not assume the task is + ready, and must not release the identity and create again. + +Point 3 is scoped to that tuple deliberately. `creation_complete` appears as +`false` in no other outcome, so an agent that keys off the pair cannot +misinterpret it. + +The workspace is resolved before the identity is used: explicit `workspace_id`, +else the parent's when `parent_id` is set, else auto-resolution when exactly one +workspace exists. The identity resolves against that **effective** workspace. + +The pre-existing MCP-layer `title` check runs before any backend call, so an MCP +replay carrying an external ID but no title gets the MCP validation error rather +than a found outcome. This is existing behavior and is consistent with the +replay guidance above. + +### Write surfaces NOT changed + +The WebSocket `task.create` action and the plugin host `Tasks().Create` do not +accept `external_id`. A caller supplying it there has the field ignored, exactly +as any other unknown field is today; no error, no claim. + +No dedupe event suppression is needed on the WebSocket: the found outcomes +return early inside the service, so WS subscribers never see an event for a +create that did not happen. + +### Task representations + +`external_id` is added to exactly these. This table **is** the requirement. + +| Representation | Location | Carries it | +|---|---|---| +| `dto.TaskDTO` | `internal/task/dto/dto.go` | Yes — `omitempty`, like the sibling `identifier` field. Covers REST reads/lists and the MCP task tools, which project this DTO. | +| WS task lifecycle events | `publishTaskEventNow`'s hand-built map, `service_events.go` | Yes — added explicitly. **Not** built from `TaskDTO`, so it needs its own change. | +| Plugin `Task` + SDK type + mapper | `plugin.proto`, `pkg/pluginsdk/data_types.go`, `internal/plugins/host_data.go` | **No.** Deferred with the plugin surface. Adding it later is additive on the proto (ADR 0043) and not breaking. | +| `v1.Task` | `pkg/api/v1/task.go` | **No.** Separate legacy projection. | +| Task-context references | `pkg/api/v1/task_context.go` | **No.** Lightweight ref. | + +`creation_complete` is a **create-response and lookup-response field only**. It +is not added to the shared task DTO, because it is meaningful only in the +context of an idempotent create; putting it on every task read would leak a +create-time condition into unrelated surfaces. diff --git a/docs/specs/tasks/model-unification.md b/docs/specs/tasks/system-design/model-unification.md similarity index 96% rename from docs/specs/tasks/model-unification.md rename to docs/specs/tasks/system-design/model-unification.md index db131eb8e0..eedb679f1b 100644 --- a/docs/specs/tasks/model-unification.md +++ b/docs/specs/tasks/system-design/model-unification.md @@ -1,10 +1,25 @@ --- status: draft +system: tasks +requirements: + - REQ-TASKS-MODEL-UNIFICATION-001 created: 2026-05-05 -owner: cfl +owners: + - cfl --- +# Task model unification System Design -# Task model unification +## Purpose and boundaries + +This design record preserves the technical source for the capability mapped to REQ-TASKS-MODEL-UNIFICATION-001 while the task system completes its migration. + +## Requirement mapping + +| Requirement | Design source | +| --- | --- | +| REQ-TASKS-MODEL-UNIFICATION-001 | Migrated legacy design detail below | + +## Migrated design source ## Why @@ -184,8 +199,8 @@ an ordinary Kanban task autonomous. Office assignment recovery uses the authoritative Office-task identity, while a workflow of any style can opt into queued automation explicitly with `queue_run`. Shared scheduler lifecycle, Office maintenance activation, and shutdown -ordering are specified in [queued run scheduling](run-scheduling.md) -and [ADR-2026-08-01-global-run-scheduler-ownership](../../decisions/2026-08-01-global-run-scheduler-ownership.md). +ordering are specified in [queued run scheduling](../requirements/run-scheduling.md) +and [ADR-2026-08-01-global-run-scheduler-ownership](../../../decisions/2026-08-01-global-run-scheduler-ownership.md). `run_events` (the audit log per run) keeps its current shape and event-streaming behaviour. WS event subjects (`office.run.queued`, @@ -436,4 +451,4 @@ These are flagged in the spec; the plan resolves them. After unification, it must instead trigger `Engine.HandleTrigger("on_comment", task)` and let the engine emit `queue_run`. The plan tracks this rewiring carefully because the - office UI's reactive paths depend on the resulting WS events. + office UI's reactive paths depend on the resulting WS events. \ No newline at end of file diff --git a/docs/specs/tasks/remote-contribution-tasks.md b/docs/specs/tasks/system-design/remote-contribution-tasks.md similarity index 95% rename from docs/specs/tasks/remote-contribution-tasks.md rename to docs/specs/tasks/system-design/remote-contribution-tasks.md index 10ab5c06ef..aa6839364e 100644 --- a/docs/specs/tasks/remote-contribution-tasks.md +++ b/docs/specs/tasks/system-design/remote-contribution-tasks.md @@ -1,11 +1,26 @@ --- -status: approved +status: current +system: tasks +requirements: + - REQ-TASKS-REMOTE-CONTRIBUTION-TASKS-001 created: 2026-08-04 updated: 2026-08-19 -owner: product +owners: + - product --- +# Remote Contribution Tasks System Design -# Remote Contribution Tasks +## Purpose and boundaries + +This design record preserves the technical source for the capability mapped to REQ-TASKS-REMOTE-CONTRIBUTION-TASKS-001 while the task system completes its migration. + +## Requirement mapping + +| Requirement | Design source | +| --- | --- | +| REQ-TASKS-REMOTE-CONTRIBUTION-TASKS-001 | Migrated legacy design detail below | + +## Migrated design source ## Why @@ -97,11 +112,11 @@ Kandev must preserve both versions and ask for user intent before one version re head may offer a normal Push for exactly `remote_ahead` commits. Decisions: -[ADR-2026-08-04-remote-contribution-bindings](../../decisions/2026-08-04-remote-contribution-bindings.md), -[ADR-2026-08-10-remote-contribution-head-drift](../../decisions/2026-08-10-remote-contribution-head-drift.md), -[ADR-2026-08-12-local-first-contribution-replacement](../../decisions/2026-08-12-local-first-contribution-replacement.md), +[ADR-2026-08-04-remote-contribution-bindings](../../../decisions/2026-08-04-remote-contribution-bindings.md), +[ADR-2026-08-10-remote-contribution-head-drift](../../../decisions/2026-08-10-remote-contribution-head-drift.md), +[ADR-2026-08-12-local-first-contribution-replacement](../../../decisions/2026-08-12-local-first-contribution-replacement.md), and -[ADR-2026-08-13-provider-history-changes-enrichment](../../decisions/2026-08-13-provider-history-changes-enrichment.md). +[ADR-2026-08-13-provider-history-changes-enrichment](../../../decisions/2026-08-13-provider-history-changes-enrichment.md). ## Data model @@ -165,7 +180,7 @@ request is associated: The binding is credential-free and server-authored from provider data. It is stored only when the head identity and normalized branch resolve to exactly one task-repository attachment. See -[ADR-2026-08-19-repository-qualified-comparison-targets](../../decisions/2026-08-19-repository-qualified-comparison-targets.md). +[ADR-2026-08-19-repository-qualified-comparison-targets](../../../decisions/2026-08-19-repository-qualified-comparison-targets.md). ## API surface @@ -400,4 +415,4 @@ mentions pull and merge request URLs evidence. The backend replacement operations remain provider-neutral. - Copying remote titles, bodies, comments, or diffs into trusted prompts. - Guaranteeing write access after credentials or provider permissions change during a running session. -- Expanding task Git credentials so an otherwise unreadable private target can be fetched. +- Expanding task Git credentials so an otherwise unreadable private target can be fetched. \ No newline at end of file diff --git a/docs/specs/tasks/runtime-cleanup.md b/docs/specs/tasks/system-design/runtime-cleanup.md similarity index 97% rename from docs/specs/tasks/runtime-cleanup.md rename to docs/specs/tasks/system-design/runtime-cleanup.md index 85b0a90595..2ed036aba6 100644 --- a/docs/specs/tasks/runtime-cleanup.md +++ b/docs/specs/tasks/system-design/runtime-cleanup.md @@ -1,13 +1,28 @@ --- status: draft +system: tasks +requirements: + - REQ-TASKS-RUNTIME-CLEANUP-001 created: 2026-06-22 updated: 2026-08-08 -owner: cfl +owners: + - cfl --- +# Task Runtime Cleanup System Design -# Task Runtime Cleanup +## Purpose and boundaries -Decision: [ADR-2026-08-08-task-owned-worktree-lifetime](../../decisions/2026-08-08-task-owned-worktree-lifetime.md) +This design record preserves the technical source for the capability mapped to REQ-TASKS-RUNTIME-CLEANUP-001 while the task system completes its migration. + +## Requirement mapping + +| Requirement | Design source | +| --- | --- | +| REQ-TASKS-RUNTIME-CLEANUP-001 | Migrated legacy design detail below | + +## Migrated design source + +Decision: [ADR-2026-08-08-task-owned-worktree-lifetime](../../../decisions/2026-08-08-task-owned-worktree-lifetime.md) ## Why @@ -477,4 +492,4 @@ The durable cleanup job wraps that resource lifecycle: ## Implementation plan -[Backend failure containment](../../plans/backend-failure-containment/plan.md) +[Backend failure containment](../../../plans/backend-failure-containment/plan.md) \ No newline at end of file diff --git a/docs/specs/task-dependencies/spec.md b/docs/specs/tasks/system-design/task-dependencies.md similarity index 97% rename from docs/specs/task-dependencies/spec.md rename to docs/specs/tasks/system-design/task-dependencies.md index 8ad8761ff5..ddba583052 100644 --- a/docs/specs/task-dependencies/spec.md +++ b/docs/specs/tasks/system-design/task-dependencies.md @@ -1,10 +1,25 @@ --- -status: shipped +status: current +system: tasks +requirements: + - REQ-TASKS-TASK-DEPENDENCIES-001 created: 2026-08-09 -owner: kandev +owners: + - kandev --- +# Task Dependencies and Auto-Start Chains System Design -# Task Dependencies and Auto-Start Chains +## Purpose and boundaries + +This design record preserves the technical source for the capability mapped to REQ-TASKS-TASK-DEPENDENCIES-001 while the task system completes its migration. + +## Requirement mapping + +| Requirement | Design source | +| --- | --- | +| REQ-TASKS-TASK-DEPENDENCIES-001 | Migrated legacy design detail below | + +## Migrated design source ## Why @@ -87,11 +102,11 @@ parallel ones. The reuse is part of the contract because the guarantees below | `task_blockers` table + BFS cycle detection (`office/dashboard.AddTaskBlocker`) | The dependency edge store and cycle validator, promoted from Office-only to a core task relationship. | | Deferred launch intent (`tasks.metadata.deferred_launch`, claimed atomically) | Storage and idempotent consumption of "start automatically when unblocked". | | The single auto-start chokepoint (`orchestrator.autoStartTaskForStep`) | Where the dependency gate lives and where dependency-triggered launches enter, so the existing auto-start claim guard prevents double launches. | -| WIP admission and queue promotion ([`tasks/wip-limit-pull-system`](../tasks/wip-limit-pull-system.md)) | Decides whether an unblocked, auto-start-intent task launches now or on promotion. | +| WIP admission and queue promotion ([`tasks/wip-limit-pull-system`](wip-limit-pull-system.md)) | Decides whether an unblocked, auto-start-intent task launches now or on promotion. | Two existing behaviors are intentionally *not* changed: -- **Parent/child completion signalling** ([`tasks/subtask-completion-trigger`](../tasks/subtask-completion-trigger.md)) +- **Parent/child completion signalling** ([`tasks/subtask-completion-trigger`](../requirements/subtask-completion-trigger.md)) fires `on_children_completed` when all children reach any terminal state, including `FAILED`. Dependency resolution uses a stricter definition (success only). The two must not be unified. @@ -533,7 +548,7 @@ MCP: single chain. - **Office mode changes.** Office is feature-flagged and not in the live - feature inventory ([`docs/features.md`](../../features.md), "In Progress: + feature inventory ([`docs/features.md`](../../../features.md), "In Progress: Office Mode"), and it already has its own blocker reaction and run queue. v1 makes the relationship and the Kanban reaction work in Kanban mode and leaves `cascadeBlockersResolved`, Office agent assignment, and Office task @@ -561,4 +576,4 @@ MCP: ## Implementation plan -See [`../../plans/task-dependencies/plan.md`](../../plans/task-dependencies/plan.md). +See [`../../plans/task-dependencies/plan.md`](../../../plans/task-dependencies/plan.md). \ No newline at end of file diff --git a/docs/specs/tasks/wip-limit-pull-system.md b/docs/specs/tasks/system-design/wip-limit-pull-system.md similarity index 95% rename from docs/specs/tasks/wip-limit-pull-system.md rename to docs/specs/tasks/system-design/wip-limit-pull-system.md index 06f40b3d6d..c25315c53a 100644 --- a/docs/specs/tasks/wip-limit-pull-system.md +++ b/docs/specs/tasks/system-design/wip-limit-pull-system.md @@ -1,11 +1,26 @@ --- -status: accepted +status: current +system: tasks +requirements: + - REQ-TASKS-WIP-LIMIT-PULL-SYSTEM-001 created: 2026-07-27 updated: 2026-08-12 -owner: kandev +owners: + - kandev --- +# WIP Limits and Visible Overflow Queues System Design -# WIP Limits and Visible Overflow Queues +## Purpose and boundaries + +This design record preserves the technical source for the capability mapped to REQ-TASKS-WIP-LIMIT-PULL-SYSTEM-001 while the task system completes its migration. + +## Requirement mapping + +| Requirement | Design source | +| --- | --- | +| REQ-TASKS-WIP-LIMIT-PULL-SYSTEM-001 | Migrated legacy design detail below | + +## Migrated design source ## Why @@ -376,18 +391,18 @@ promotion. ## Decision See -[`../../decisions/2026-07-28-visible-wip-overflow-queues.md`](../../decisions/2026-07-28-visible-wip-overflow-queues.md). +[`../../decisions/2026-07-28-visible-wip-overflow-queues.md`](../../../decisions/2026-07-28-visible-wip-overflow-queues.md). Destination queue admission for task moves: -[`../../decisions/2026-08-12-queue-task-moves-at-wip-capacity.md`](../../decisions/2026-08-12-queue-task-moves-at-wip-capacity.md). +[`../../decisions/2026-08-12-queue-task-moves-at-wip-capacity.md`](../../../decisions/2026-08-12-queue-task-moves-at-wip-capacity.md). ## Implementation plan See -[`../../plans/wip-overflow-queues/plan.md`](../../plans/wip-overflow-queues/plan.md). +[`../../plans/wip-overflow-queues/plan.md`](../../../plans/wip-overflow-queues/plan.md). Auto-start idempotency repair (single agent per review task): -[`../../plans/review-watcher-double-autostart/plan.md`](../../plans/review-watcher-double-autostart/plan.md). +[`../../plans/review-watcher-double-autostart/plan.md`](../../../plans/review-watcher-double-autostart/plan.md). Queued task moves and queue presentation: -[`../../plans/queued-task-moves/plan.md`](../../plans/queued-task-moves/plan.md). +[`../../plans/queued-task-moves/plan.md`](../../../plans/queued-task-moves/plan.md). \ No newline at end of file diff --git a/docs/specs/tasks/system-design/workflow-quorum-decision-recording.md b/docs/specs/tasks/system-design/workflow-quorum-decision-recording.md new file mode 100644 index 0000000000..8c5db1855f --- /dev/null +++ b/docs/specs/tasks/system-design/workflow-quorum-decision-recording.md @@ -0,0 +1,398 @@ +--- +status: draft +system: tasks +requirements: + - REQ-TASKS-QUORUM-CORE-001 + - REQ-TASKS-QUORUM-RECORDING-001 + - REQ-TASKS-QUORUM-VERDICT-001 + - REQ-TASKS-QUORUM-REEVALUATION-001 + - REQ-TASKS-QUORUM-SLATE-001 + - REQ-TASKS-QUORUM-DIAGNOSTICS-001 + - REQ-TASKS-QUORUM-CONCURRENCY-001 + - REQ-TASKS-QUORUM-BINDING-001 + - REQ-TASKS-QUORUM-REGRESSION-001 +--- + + +# Workflow quorum decision recording System Design + + + +## Purpose and boundaries + + + +The task system owns quorum semantics, participant canonicalization, decision persistence, guarded transition application, and diagnostic projections. Office and MCP surfaces are transports over that engine-owned contract. + + + +## Requirement mapping + +| Requirement | Design source | +| --- | --- | +| `REQ-TASKS-QUORUM-CORE-001` | Extracted from the legacy design sections below. | +| `REQ-TASKS-QUORUM-RECORDING-001` | Extracted from the legacy design sections below. | +| `REQ-TASKS-QUORUM-VERDICT-001` | Extracted from the legacy design sections below. | +| `REQ-TASKS-QUORUM-REEVALUATION-001` | Extracted from the legacy design sections below. | +| `REQ-TASKS-QUORUM-SLATE-001` | Extracted from the legacy design sections below. | +| `REQ-TASKS-QUORUM-DIAGNOSTICS-001` | Extracted from the legacy design sections below. | +| `REQ-TASKS-QUORUM-CONCURRENCY-001` | Extracted from the legacy design sections below. | +| `REQ-TASKS-QUORUM-BINDING-001` | Extracted from the legacy design sections below. | +| `REQ-TASKS-QUORUM-REGRESSION-001` | Extracted from the legacy design sections below. | + +## Verified current state + +The originating report named two causes. Only the first survives inspection. +Investigation surfaced four defects, three of which the report did not name. + +### The evaluation path is intact (originally reported as broken) + +`wait_for_quorum` **is** evaluated on the path that runs these workflows. The +claim that the orchestrator ignores the guard is false; the orchestrator +delegates to the engine, which owns the semantics. + +| Link | Evidence | +|---|---| +| Guard evaluated on the transition loop | `apps/backend/internal/workflow/engine/engine.go:275` | +| All five thresholds implemented | `apps/backend/internal/workflow/engine/quorum.go:88-119` | +| Office dispatches into that engine | `apps/backend/internal/office/engine_dispatcher/dispatcher.go:108` | +| Office shares the orchestrator's engine instance | `apps/backend/internal/backendapp/main.go:1520,1527` | +| Participant + decision stores wired in production | `apps/backend/internal/backendapp/main.go:1514-1515` | +| `clear_decisions` / `queue_run_for_each_participant` callbacks wired | `apps/backend/internal/orchestrator/workflow_callbacks.go:49,56` | + +The persistence layer is likewise complete: `RecordStepDecision` supersedes +prior verdicts inside a transaction (`phase2_sqlite.go:522-578`), +`ListStepDecisions` returns oldest-first (`:583-600`), and `AddTaskParticipant` +writes `decision_required = 1` for every reviewer and approver +(`office/repository/sqlite/participants.go:84-87`). + +The engine's evaluation is therefore correct **given its inputs**. With zero +decision rows, `all_approve` and `any_reject` both correctly evaluate false and +the card correctly stays put. The defects are all on the input side. + +### Defect 1 — an agent has no tool to record a decision + +The MCP tool registry (`apps/backend/internal/mcp/server/server.go:892-923`) +enables exactly four groups for `SurfaceOfficeTask`: `plan` (4 tools), +`related-tasks` (1), `office-documents` (3), and the capability-gated +`user-question` (1). Nine tools, none of which records a decision. + +`POST /api/v1/office/tasks/:id/approve` and `.../request-changes` exist +(`office/dashboard/handler.go:91-92`) and are reachable from the human UI +(`apps/web/components/task/simple/components/approval-action-bar.tsx:122-123`), +but no agent can reach either. A reviewer agent's only means of expressing a +verdict is a free-text comment, which nothing parses. + +### Defect 2 — the human path writes a verdict the evaluator cannot read + +Office records `changes_requested` (`office/models/models.go:653`), surfaced in +the frontend contract as `"approved" | "changes_requested"` +(`apps/web/lib/api/domains/office-extended-api.ts:297`). + +The evaluator counts only `approved` and `rejected` +(`workflow/engine/quorum.go:25-26,100-103`). + +A human clicking **Request changes** therefore writes a row that satisfies +neither `all_approve` (it is not an approval) nor `any_reject` (it is not +literally `rejected`). The task stays at Review with a rejection on file. This +is live today on the human path and is independent of Defect 1. + +The vocabulary is only half of it. `resolveParticipantID` +(`office/dashboard/decisions.go`) stamps every human decision with the constant +`userParticipantSentinel = "user"`, because the singleton user has no +`agent_profile_id` and therefore no `workflow_step_participants` row. +`latestDecisionsPerParticipant` (`workflow/engine/quorum.go`) discards any +decision whose `participant_id` is absent from the required slate, and that +slate is built from participant-row UUIDs. **A human decision is therefore +discarded before its verdict string is ever inspected.** Correcting the +vocabulary alone changes nothing on the human path; AC-TASKS-QUORUM-VERDICT-001.3 is what makes AC-TASKS-QUORUM-VERDICT-001.2 +reachable. + +### Defect 3 — recording a decision does not re-evaluate the transition + +`recordTaskDecision` (`office/dashboard/decisions.go:225-265`) persists the row, +publishes `OfficeTaskDecisionRecorded`, logs activity, and queues follow-up +agent runs. It never re-evaluates the guarded transition. + +`Engine.RecordParticipantDecision` (`workflow/engine/quorum.go:152-200`) does +exactly that re-evaluation and has **zero production callers** — only tests. + +So even once a verdict is recorded with a vocabulary the evaluator understands, +the transition fires only if some unrelated turn later completes on that task. +When the reviewer has already finished, nothing re-triggers. + +### Defect 4 — participant rows are pinned to the step they were attached at + +`AddTaskParticipant` (`office/repository/sqlite/participants.go:59-88`) writes +`step_id` = the task's step *at attach time*. No code ever updates a +participant's `step_id`; the only `UPDATE workflow_step_participants` statements +in the tree touch `agent_profile_id`. + +`requiredParticipants` filters on `state.CurrentStepID` +(`workflow/engine/quorum.go:70-83`). A participant attached at one step is +invisible at every other step, and an empty slate fails closed +(`quorum.go:57-60`). + +Live evidence from the verified task — the approver exists only at Work: + +| participant | step | role | decision_required | +|---|---|---|---| +| `afbd72cd` | Review | reviewer | 1 | +| `8eefba93` | **Work** | approver | 1 | +| `3cb7cd33` | Work | reviewer | 1 | + +Review's reviewer slate is non-empty, so Defects 1–3 are what block this task +today. But Approval's approver slate is **empty**, so fixing Review alone moves +the card one step and strands it again. + +Two further facts make this wider than the slate. First, `AddTaskParticipant`'s +idempotency probe is itself scoped to `step_id`, so re-attaching the same agent +after a step change **creates a second row** for the same +`(task_id, role, agent_profile_id)` — the two-row shape is how the live data +above came to exist, not a hypothetical. Second, `ListAllTaskParticipants` +(`office/repository/sqlite/participants.go`) is *also* step-scoped, so +`resolveDeciderRole` — the role-resolution precedent AC-TASKS-QUORUM-RECORDING-001.4 cites — is blind in +exactly the same way: an approver whose row sits at Work cannot be resolved as +an approver while the task is at Approval. Slate membership, role resolution, +and the `participant_id` written on a decision must therefore all use one +canonical-row rule, which AC-TASKS-QUORUM-SLATE-001.3 defines. + +### Defect 5 — every failure is silent and identical + +The engine holds no logger (`workflow/engine/engine.go:131-144`) and `quorum.go` +emits nothing. `evaluateTransitionGuard` returns `(false, nil)` for all of: +store not wired, empty required slate, threshold genuinely unmet, and unknown +guard variant. From outside, a card legitimately awaiting two more approvals is +byte-for-byte identical to a card whose guard can never be satisfied. This is +the diagnostic gap that let the bug survive. + +### Live workflow configuration + +`Office Default` (`7561d51f`), steps Backlog(0) Work(1) Review(2) Approval(3) +Done(4). Both guarded steps carry the guard nested inside the action's `config`, +which `ConfigTransitionGuard` accepts (`workflow/engine/types.go:533`): + +```json +"on_turn_complete": [ + {"type": "move_to_step", "config": {"step_id": "", + "wait_for_quorum": {"role": "reviewer", "threshold": "all_approve"}}}, + {"type": "move_to_step", "config": {"step_id": "", + "wait_for_quorum": {"role": "reviewer", "threshold": "any_reject"}}} +] +``` + +Approval is the same shape with `role: approver`. + +## Tool contract + +Pinned here because Build would otherwise invent it and Review would relitigate it. + +**Name.** `record_step_decision_kandev`, registered under a new +`office-decisions` group in the profile registry +(`apps/backend/internal/mcp/server/server.go:892-923`), enabled for +`SurfaceOfficeTask` only. + +**Arguments.** + +| Argument | Required | Shape | +|---|---|---| +| `decision` | yes | `"approved"` or `"rejected"` | +| `reason` | yes | non-empty string | + +No idempotency argument. A retry is a new verdict that supersedes the previous +one under AC-TASKS-QUORUM-CONCURRENCY-001.2; the tool is deliberately NOT idempotent across calls, and +AC-TASKS-QUORUM-CONCURRENCY-001.7 states this as contract so a builder does not invent a client-supplied +key. Idempotency in this feature is confined to transition application +(AC-TASKS-QUORUM-CONCURRENCY-001.6, AC-TASKS-QUORUM-CONCURRENCY-001.4). + +**Scoping.** The tool takes no `task_id`. It resolves the task and step from the +calling session, matching every other Office tool. An agent cannot record a +decision on a task other than the one its session is bound to. + +**Return.** On success the tool returns exactly these seven fields, observed by +AC-TASKS-QUORUM-RECORDING-001.11: + +| Field | Meaning | +|---|---| +| `decision` | the recorded verdict | +| `role` | the caller's resolved role, per AC-TASKS-QUORUM-RECORDING-001.4 | +| `step_id` | the validated step the row landed on, per AC-TASKS-QUORUM-BINDING-001.2 | +| `decision_id` | the decision row id, per AC-TASKS-QUORUM-CONCURRENCY-001.8 | +| `decided_at` | the row's `decided_at` value as persisted | +| `transition_applied` | `true` meaning the task left the step, per AC-TASKS-QUORUM-CONCURRENCY-001.10 — NOT that it was admitted at the target | +| `guards` | the AC-TASKS-QUORUM-REEVALUATION-001.14 per-guard entries for the validated step, identical in shape to AC-TASKS-QUORUM-DIAGNOSTICS-001.4's | + +The agent needs `transition_applied` to know whether its work at this step is +finished, and `step_id` to detect the AC-TASKS-QUORUM-BINDING-001.2 race in which its decision landed on +a step the task has since left. `decision_id` and `decided_at` are returned +because AC-TASKS-QUORUM-REEVALUATION-001.12 requires `publishDecisionRecorded` to keep publishing +`decision_id` and `created_at` off the `DecisionRecord`, and once the persist +moves behind the AC-TASKS-QUORUM-REEVALUATION-001.10 entry point those two values are no longer office's to +generate. + +There is deliberately NO scalar `required_count` / `received_count` pair. A step +may configure several guards, and AC-TASKS-QUORUM-RECORDING-001.4's approver-wins precedence can resolve the +caller to `approver` while every guard at that step names `reviewer` — which is +`Office Default`'s Review exactly, per `## Live workflow configuration`. In that +case AC-TASKS-QUORUM-BINDING-001.7 means the decision is never counted by that guard at all, so a single +pair of counts is either ambiguous or actively misleading. `guards` gives the +agent the count against each guard that exists, which is the question it was +actually asking. + +**Reason column.** The reason is written to `workflow_step_decisions.comment`, +the column every Office reader already projects (`office/dashboard/decisions.go:141,428`). +It is NOT written to `note`, which `Engine.RecordParticipantDecision` currently +uses (`workflow/engine/quorum.go:180`) and which no Office surface reads. Any +engine-side decision API used by this feature writes `comment` for the +human-facing reason. A reason written only to `note` would be invisible in the +task timeline — this is the concrete bug this paragraph exists to prevent. + +## Failure modes + +| Condition | Behavior | +|---|---| +| Agent not a participant | Permission error; no row written (AC-TASKS-QUORUM-RECORDING-001.3) | +| Verdict outside `approved`/`rejected` on the tool | Validation error; no row (AC-TASKS-QUORUM-RECORDING-001.6) | +| Empty reason | Validation error; no row (AC-TASKS-QUORUM-RECORDING-001.7) | +| Task has no bound step | Error naming the unbound step; no row (AC-TASKS-QUORUM-RECORDING-001.8) | +| Decision store not wired | Guard does not fire; reason recorded (AC-TASKS-QUORUM-DIAGNOSTICS-001.1) | +| Required slate empty, approve-style threshold | Guard does not fire; reason `slate_empty` (AC-TASKS-QUORUM-SLATE-001.6, AC-TASKS-QUORUM-DIAGNOSTICS-001.1) | +| Required slate empty, `any_reject` | Guard still evaluated, not short-circuited; a seatless rejection vetoes (AC-TASKS-QUORUM-SLATE-001.11, AC-TASKS-QUORUM-VERDICT-001.3) | +| Unknown guard variant | Guard does not fire; reason recorded (AC-TASKS-QUORUM-DIAGNOSTICS-001.1) | +| Re-evaluation errors after a successful write | Decision kept; success reported; failure surfaced (AC-TASKS-QUORUM-REEVALUATION-001.3) | +| No resolvable session | Decision kept; re-evaluation skipped, no error (AC-TASKS-QUORUM-REEVALUATION-001.4) | +| Duplicate decision from one participant | Prior superseded transactionally (AC-TASKS-QUORUM-CONCURRENCY-001.2) | +| Human rejection, no participant row | Counted by `any_reject` as a veto (AC-TASKS-QUORUM-VERDICT-001.3) | +| Non-slate approval | Not counted toward any approve threshold (AC-TASKS-QUORUM-VERDICT-001.6) | +| Participant has rows at two steps | Canonical row chosen by AC-TASKS-QUORUM-SLATE-001.3; one seat (AC-TASKS-QUORUM-SLATE-001.4) | +| Task step changed between validate and write | Persisted against validated step; `transition_applied=false`; returned `step_id` is the validated step (AC-TASKS-QUORUM-BINDING-001.2) | +| Concurrent transition race lost | `transition_applied=false`, decision kept, not an error (AC-TASKS-QUORUM-CONCURRENCY-001.4) | +| Participant store not wired | Guard does not fire; reason `participant_store_unwired` (AC-TASKS-QUORUM-DIAGNOSTICS-001.1) | +| `ListStepDecisions` / `ListStepParticipants` errors | Guard does not fire; reason `evaluation_error` with `error` field (AC-TASKS-QUORUM-DIAGNOSTICS-001.1, AC-TASKS-QUORUM-DIAGNOSTICS-001.2) | +| No active session for re-evaluation | Decision kept, re-eval skipped, reason `session_unresolvable` (AC-TASKS-QUORUM-REEVALUATION-001.5) | +| Unrecognized threshold string | Guard does not fire; reason `threshold_unrecognized`; rendered as stuck (AC-TASKS-QUORUM-DIAGNOSTICS-001.8) | +| Empty slate AND `n_approve:` unsatisfiable | Single reason `slate_empty` wins by AC-TASKS-QUORUM-DIAGNOSTICS-001.7 precedence | +| Seat id changed after a verdict was recorded | Verdict still counted, matched by decider identity (AC-TASKS-QUORUM-SLATE-001.10) | +| Re-evaluation apply fails | Operation id left unmarked so a retry can still land (AC-TASKS-QUORUM-REEVALUATION-001.7) | +| Quorum apply loses the step compare-and-swap | `applied = false`, not an error; `TransitionAbandoned` set (AC-TASKS-QUORUM-CONCURRENCY-001.10, AC-TASKS-QUORUM-CONCURRENCY-001.4) | +| Target step at WIP capacity | Transition still applied; task queued at target (AC-TASKS-QUORUM-CONCURRENCY-001.10) | +| Decision on file and no resolvable session | AC-TASKS-QUORUM-DIAGNOSTICS-001.4 reports `reevaluation_blocked`; card renders stuck (AC-TASKS-QUORUM-DIAGNOSTICS-001.12, AC-TASKS-QUORUM-DIAGNOSTICS-001.11) | +| Human rejects at a reviewer-guarded step | Veto counts; role filter waived for `decider_type = user` (AC-TASKS-QUORUM-VERDICT-001.7, AC-TASKS-QUORUM-VERDICT-001.2) | +| Guard's threshold already met at request time | Entry reports `satisfied = true` and omits `reason` (AC-TASKS-QUORUM-DIAGNOSTICS-001.10) | +| One guard stuck, a sibling merely awaiting | Card renders stuck; stuck outranks awaiting (AC-TASKS-QUORUM-DIAGNOSTICS-001.11) | +| Diagnostic `GET /quorum` read | No AC-TASKS-QUORUM-DIAGNOSTICS-001.2 log record, no AC-TASKS-QUORUM-DIAGNOSTICS-001.3 counter increment (AC-TASKS-QUORUM-DIAGNOSTICS-001.2, AC-TASKS-QUORUM-DIAGNOSTICS-001.3) | +| Engine decision entry point not wired | Both transports error; no row written; no office-side fallback (AC-TASKS-QUORUM-REEVALUATION-001.13) | +| Task at a step with no guarded transition | AC-TASKS-QUORUM-DIAGNOSTICS-001.4 returns empty list; card aggregates to `clear` (AC-TASKS-QUORUM-DIAGNOSTICS-001.11, AC-TASKS-QUORUM-DIAGNOSTICS-001.5) | +| Task returns to a guarded step with prior decisions live | AC-TASKS-QUORUM-REGRESSION-001.6 fails; card is blocked on the `on_enter` card, not patched here | +| AC-TASKS-QUORUM-DIAGNOSTICS-001.4 read while the engine dispatcher is not wired | Endpoint errors; no office-side fallback evaluation (AC-TASKS-QUORUM-REEVALUATION-001.14, AC-TASKS-QUORUM-REEVALUATION-001.13) | +| One guard's store read errors, siblings healthy | That entry alone reports `evaluation_error`; the others are still returned (AC-TASKS-QUORUM-REEVALUATION-001.14, AC-TASKS-QUORUM-DIAGNOSTICS-001.1) | +| Decision recorded at a step that also configures a side-effect action on `on_turn_complete` | Only guarded transitions are evaluated; no callback re-fires (AC-TASKS-QUORUM-REEVALUATION-001.8) | +| Tool called at a step with no guarded transition | `guards` is an empty list, not an error (AC-TASKS-QUORUM-RECORDING-001.11) | +| Decision row id needed for the AC-TASKS-QUORUM-CONCURRENCY-001.6 operation id | Pre-generated caller-side into `DecisionInfo.ID`; never read back after the write (AC-TASKS-QUORUM-CONCURRENCY-001.8) | +| Office needs `decision_id` / `created_at` to publish | Both returned by the AC-TASKS-QUORUM-REEVALUATION-001.10 entry point (AC-TASKS-QUORUM-REEVALUATION-001.11) | + +## Persistence guarantees + +- A decision write and its supersede of the prior verdict are one transaction; a + reader never observes zero non-superseded rows for a participant that has + decided (existing `RecordStepDecision` behavior). +- A recorded decision survives a re-evaluation failure (AC-TASKS-QUORUM-REEVALUATION-001.3). +- Decisions cleared by `clear_decisions` on step re-entry are hard-deleted; + decisions superseded by a new verdict are retained with `superseded_at` set — + the existing split at `phase2_sqlite.go:686-697`. +- Threshold evaluation counts, per AC-TASKS-QUORUM-SLATE-001.9 seat, the LAST + row under the AC-TASKS-QUORUM-CONCURRENCY-001.1 ordering — it does not filter on `superseded_at`. + `ListStepDecisions` deliberately returns superseded rows so timelines can + render full history, and the engine's `DecisionInfo` projection carries no + `SupersededAt` field, so the engine could not filter on it even if it wanted + to. Last-row-wins and non-superseded coincide whenever the supersede predicate + and the `participant_id` key agree; AC-TASKS-QUORUM-SLATE-001.3 is what makes them agree, and + without it the two diverge exactly in the multi-row case of AC-TASKS-QUORUM-SLATE-001.4. + +## Out of scope + +Each of the following is a deliberate exclusion, not an oversight. + +- **Workspace `approvals` subsystem.** `POST /approvals/:id/decide` and its + `"approved" | "rejected"` shape (`apps/web/lib/api/domains/office-api.ts:534`) + are a separate feature from task step decisions. Untouched. +- **New thresholds.** `all_approve`, `all_decide`, `any_reject`, + `majority_approve`, `n_approve:` are the complete set. No new threshold. +- **Guard variants beyond `wait_for_quorum`.** `TransitionGuard` stays + single-variant. +- **`on_enter` action dispatch.** Tracked by its own card, and this feature does + not change it. It is a **hard dependency**, not merely an adjacent card: + AC-TASKS-QUORUM-CONCURRENCY-001.9's decision-clearing is what stops a rejection recorded at a guarded step + from re-firing `any_reject` every time the task returns there, and the only + `on_enter` trigger dispatch in the tree today is inside + `SwitchWorkflowCallback`. AC-TASKS-QUORUM-REGRESSION-001.6 turns that dependency into an observable + regression so it fails loudly instead of shipping as an unbounded + reject/return loop. This feature SHALL NOT grow a second decision-clearing + path to compensate. +- **Decision revocation.** A participant may supersede a verdict by recording a + new one; there is no withdraw-to-undecided operation. +- **Timeouts and escalation.** A quorum that is never reached waits + indefinitely. No auto-advance, no reminder, no deadline. +- **Delegating or reassigning a pending decision.** Not addressed. +- **Kanban-workflow decisions.** The decision tool is Office-surface only + (AC-TASKS-QUORUM-RECORDING-001.9); Kanban keeps `step_complete_kandev`. +- **Backfilling the zero existing decision rows.** There are none to migrate. +- **Client-supplied idempotency keys on the decision tool.** Excluded by AC-TASKS-QUORUM-CONCURRENCY-001.7. + Transition application is idempotent (AC-TASKS-QUORUM-CONCURRENCY-001.6); recording is not. +- **Giving the singleton human user a participant row.** AC-TASKS-QUORUM-VERDICT-001.3 makes a human + rejection count without one. Seating the user in the slate would change who + `all_approve` waits for on every existing task, which is a larger contract + change than this card carries. +- **The `allApproversApproved` / `pendingApprovers` close gate.** These drive the + `task_ready_to_close` reactivity run and the `ApprovalsPendingError` 409 on + `UpdateTaskStatus`, and they read through the same step-scoped + `office/repository/sqlite` query, so they carry Defect 4's blindness: an + approver attached at an earlier step is invisible to them. Excluded + deliberately, and the consequence is stated rather than hidden — after this + card, quorum can advance a task while that separate gate still blocks its + close on an approver it cannot see. Folding it in would extend AC-TASKS-QUORUM-SLATE-001.9's slate + to a second consumer with different semantics (per-role current-approval, not + threshold arithmetic) on a card that AC-TASKS-QUORUM-REEVALUATION-001.9 has already grown. It needs its own + card. + The residual is sharper than "quorum can advance a task while that gate still + blocks", and is recorded in full so the follow-up card is scoped from fact: a + task whose AC-TASKS-QUORUM-DIAGNOSTICS-001.4 entries all report `satisfied` renders as `clear` under + AC-TASKS-QUORUM-DIAGNOSTICS-001.11 — the healthy rendering — and can still take a 409 + `ApprovalsPendingError` on close, naming an approver that nothing this card + touches can help the operator locate, because that gate reads through the same + step-scoped `ListAllTaskParticipants` query Defect 4 diagnoses. This card + therefore converts one invisible stuck class into one visible-but-confusing + class, which is an improvement but not a completion. The follow-up should be + tracked, not deferred indefinitely. +- **Repointing `resolveDeciderRole` for non-decision callers.** AC-TASKS-QUORUM-RECORDING-001.5 fixes role + resolution on the decision path only. Other readers of + `ListAllTaskParticipants` keep today's step-scoped behavior. +- **A generic transition-lock primitive.** AC-TASKS-QUORUM-CONCURRENCY-001.10 adds exactly one additive port + method, used only by the AC-TASKS-QUORUM-CONCURRENCY-001.4 quorum apply; `ApplyTransition` and every + transition that uses it keep today's semantics, and the compare-and-swap reuses + the transaction and the workspace/step locks + `updateTaskWithWorkflowStepAdmission` already takes. No engine-wide locking + mechanism, and no behavior change to any non-quorum transition. +- **Repointing already-attached participant rows.** AC-TASKS-QUORUM-SLATE-001.1 changes how the slate + is *read*; whether existing rows are also rewritten is an implementation + choice the plan may make either way, provided AC-TASKS-QUORUM-SLATE-001.1–AC-TASKS-QUORUM-SLATE-001.7 hold. + +## E2E surfaces + +User-visible surfaces this touches: + +- Office task detail approval action bar (`approval-action-bar.tsx`) — the + Request-changes path changes behavior under AC-TASKS-QUORUM-VERDICT-001.2. +- Office task board — a card at Review now advances on quorum (AC-TASKS-QUORUM-REGRESSION-001.1, AC-TASKS-QUORUM-REGRESSION-001.2). +- `GET /api/v1/office/tasks/:id/quorum` — new read endpoint (AC-TASKS-QUORUM-DIAGNOSTICS-001.4, AC-TASKS-QUORUM-DIAGNOSTICS-001.5), + projecting the AC-TASKS-QUORUM-REEVALUATION-001.14 engine snapshot. +- Office task detail awaiting-decisions rendering (AC-TASKS-QUORUM-DIAGNOSTICS-001.6), including the + stuck-distinct rendering of `reevaluation_blocked` (AC-TASKS-QUORUM-DIAGNOSTICS-001.12) and the AC-TASKS-QUORUM-DIAGNOSTICS-001.11 + aggregation when two guards disagree. + +E2E recommendation: one Playwright spec driving Work → Review → Approval with a +reviewer and an approver attached, asserting advance-on-approve, return-on-reject, +and the awaiting-decisions presentation of AC-TASKS-QUORUM-DIAGNOSTICS-001.6. The same spec covers AC-TASKS-QUORUM-REGRESSION-001.6 at +no extra cost: after the return-on-reject leg, drive the task back to the guarded +step and assert it does not immediately bounce again. Backend quorum arithmetic, +vocabulary, and concurrency are Go-test territory, not E2E. diff --git a/docs/specs/workflow/task-step-transition-ledger/spec.md b/docs/specs/tasks/system-design/workflow-task-step-transition-ledger.md similarity index 61% rename from docs/specs/workflow/task-step-transition-ledger/spec.md rename to docs/specs/tasks/system-design/workflow-task-step-transition-ledger.md index 2eb37a1e16..000c542b12 100644 --- a/docs/specs/workflow/task-step-transition-ledger/spec.md +++ b/docs/specs/tasks/system-design/workflow-task-step-transition-ledger.md @@ -1,94 +1,31 @@ --- status: draft -created: 2026-08-12 -owner: nova28 +system: tasks +requirements: + - REQ-TASKS-TRANSITION-LEDGER-001 + - REQ-TASKS-TRANSITION-LEDGER-SCENARIOS-001 --- -# Task step transition ledger -## Why +# Workflow task-step transition ledger System Design -Kandev records *when a card was worked on* but not *which workflow step it was -in at the time*. Cost, token, and duration events can be joined to a task and a -session, but attributing them to a step requires reconstructing step boundaries -from the message timeline. On the reference store that reconstruction leaves -**47.0%** of spend cleanly inside one step, **30.5%** merely "dominant of two -steps in the window", and **22.5%** unattributable — so "what does Review cost" -cannot be answered without a caveat larger than the answer. -The one table that looks like it should answer this, `session_step_history`, is -keyed by **session** while Kandev tracks step state on the **task** -(`base_migrations.go` removed `task_sessions.workflow_step_id` for exactly that -reason). A card can change step with no active session, or with several, so -attributing a task-level transition to one session manufactures ownership that -does not exist. -## What this feature is, and is not +## Purpose and boundaries -**It is a forward-looking record of step boundaries.** From its activation point -onward, every durable change to a task's workflow step is recorded with its -timestamp, its cause, and its actor, so downstream analysis can bound a step -interval from data rather than infer it from chat. -**It cannot repair the past.** No historical transition is reconstructed or -backfilled. Rows before the activation point do not exist and MUST NOT be -synthesised. -**It does not make per-step cost exact.** Cost events bill windows that cross -step boundaries; better boundaries improve attribution but do not decide how one -window's cost divides between two steps. The "dominant of two steps" bucket -shrinks; it does not disappear. +The task system owns the task-level transition ledger and its writer boundaries. Session history remains owned by the session lifecycle contract; downstream cost and analytics consumers read the ledger without changing its authority. -**It is not `workflow_step_decisions`.** That table has a live writer on the -Office approval path; its zero rows mean an unused feature, not a missing one. -The two mechanisms MUST NOT be merged. -**It is not a replacement for `session_step_history`.** This spec neither wires, -removes, nor changes that table or the `GET /api/v1/sessions/:id/workflow/history` -endpoint that reads it. See *Relationship to `session_step_history`*. -## What +## Requirement mapping -The feature ships as two independently valuable slices. **Slice 1 SHALL ship and -be measured before Slice 2 is built** (see *Gate between slices*). +| Requirement | Design source | +| --- | --- | +| `REQ-TASKS-TRANSITION-LEDGER-001` | Extracted from the legacy design sections below. | +| `REQ-TASKS-TRANSITION-LEDGER-SCENARIOS-001` | Extracted from the legacy design sections below. | -### Slice 1 — turn-start step stamp - -- A turn created for a session records the workflow step its task was in at the - moment the turn started. -- The stamp is present on every turn created after activation whose task has a - workflow step, and **absent** — never empty, never `0` — when the task has - none. -- The stamp is immutable once written. A step change during the turn does not - rewrite it. -- No column is added to `task_session_turns`. - -### Slice 2 — task-level transition ledger - -- Every committed change to a task's workflow step produces exactly one ledger - row, written in the same database transaction as the change. -- A row that is not committed with its transition does not exist: a rolled-back - or WIP-rejected move leaves no row. -- A move that does not change the step (position-only reorder, re-issued move to - the current step) produces **no** row. -- Each row names the task, the step it left and the step it entered, when, why - (trigger), what kind of actor caused it, and that actor's **identifier** — - never a display name. -- The initiating session is recorded when one exists and is genuinely the - initiator; it is `NULL` otherwise. A transition with no session, or with - several candidate sessions and no single initiator, MUST record `NULL` rather - than pick one. -- Each row carries the collection-contract version it was written under. - -### Cross-cutting - -- Legacy rows and unstamped turns get `NULL` / absent — **never `0`, never - `""`**. -- Both slices publish a machine-readable activation point, because the - downstream extract is a point-in-time snapshot with no schema versioning: a - column whose meaning changes mid-series is silently discontinuous. -- Both writers are observable at runtime, and the set of code paths that may - mutate a task's workflow step is pinned by a test (see *Writer health*). ## Data model @@ -490,172 +427,3 @@ and can be superseded by a move that commits moments later, which the ledger records and the stamp does not. That is expected, not an inconsistency, and is why `task_history` outranks `turn_start` in the basis precedence. Neither writer corrects the other. - -## Scenarios - -### Slice 1 — turn-start step stamp - -- **GIVEN** a session whose task sits in workflow step `S`, **WHEN** a turn - starts, **THEN** the turn's metadata contains `workflow_step_id_at_start` = `S`. -- **GIVEN** a session whose task has no workflow step, **WHEN** a turn starts, - **THEN** the turn's metadata contains no `workflow_step_id_at_start` key — not - an empty string, not `null`, not `0`. -- **GIVEN** a session with no runtime config to snapshot, **WHEN** a turn starts - while its task sits in step `S`, **THEN** the metadata contains - `workflow_step_id_at_start` = `S` and no `runtime_config_snapshot`. -- **GIVEN** a turn stamped with step `S`, **WHEN** the task moves to step `T` - before the turn completes, **THEN** the turn's stamp is still `S`. -- **GIVEN** a turn created before activation, **WHEN** it is read after - activation, **THEN** it carries no stamp and is not backfilled. -- **GIVEN** the task row cannot be read, **WHEN** a turn starts, **THEN** the - turn is created without a stamp and turn creation does not fail. -- **GIVEN** a synthetic completed turn created for a lifecycle message, **WHEN** - it is written while its task sits in step `S`, **THEN** it carries the same - stamp as a normal turn. - -### Slice 2 — ledger writes - -- **GIVEN** a task in step `A`, **WHEN** a user moves it to step `B`, **THEN** - exactly one ledger row exists with `from_workflow_step_id` = `A`, - `to_workflow_step_id` = `B`, trigger `manual_move`, actor kind `human`, and - `actor_id` equal to the acting user's ID. -- **GIVEN** a task in step `A` with no session at all, **WHEN** a bulk move - sends it to step `B`, **THEN** one row is written with `session_id` NULL and - trigger `bulk_move`. -- **GIVEN** a task in step `A` with two live sessions, **WHEN** a WIP pull - promotes it into step `B`, **THEN** one row is written with `session_id` NULL, - trigger `wip_pull`, and actor kind `system`. -- **GIVEN** an agent session on a task in step `A`, **WHEN** the agent calls - `move_task_kandev` and it applies immediately, **THEN** one row is written - with trigger `mcp_move`, actor kind `agent`, and `actor_id` equal to that - session's ID. -- **GIVEN** an agent's deferred `move_task_kandev`, **WHEN** it applies at turn - end, **THEN** one row is written with trigger `mcp_deferred_move`. -- **GIVEN** a workflow step whose `on_turn_complete` transitions to step `B`, - **WHEN** the turn completes, **THEN** one row is written with trigger - `engine_transition`. -- **GIVEN** a task in step `A`, **WHEN** a move to step `A` is issued, **THEN** - no row is written. -- **GIVEN** a task in step `A`, **WHEN** only its position within `A` changes, - **THEN** no row is written. -- **GIVEN** a WIP-limited target step at capacity, **WHEN** a move into it is - rejected, **THEN** no row is written and the task's step is unchanged. -- **GIVEN** the transaction that changes the step is rolled back for any reason, - **WHEN** it completes, **THEN** no row exists for it. -- **GIVEN** a task created into step `A`, **WHEN** creation commits, **THEN** one - row exists with `from_workflow_id` and `from_workflow_step_id` NULL and - trigger `task_created`. -- **GIVEN** a task created targeting a full step `B` with feeder step `A`, - **WHEN** creation commits and places it in `A`, **THEN** the genesis row's - `to_workflow_step_id` is `A`, the step it was actually placed in. -- **GIVEN** a task attached to a workflow, **WHEN** it is later removed from - that workflow, **THEN** a row is written with `to_workflow_id` and - `to_workflow_step_id` NULL and trigger `workflow_detached`. -- **GIVEN** a task whose step changed via a task update rather than the move - path, **WHEN** the update commits, **THEN** one row is written with trigger - `task_update`. -- **GIVEN** a caller that declares no trigger or actor, **WHEN** its step change - commits, **THEN** the row records trigger `unknown`, actor kind `unknown`, and - `actor_id` NULL. -- **GIVEN** a task created with no workflow, **WHEN** creation commits, **THEN** - no row is written; **WHEN** it is later attached to a workflow, **THEN** its - first row has trigger `workflow_attached` and `from_*` NULL. -- **GIVEN** an ephemeral (quick-chat) task in step `A`, **WHEN** it moves to - step `B`, **THEN** a row is written exactly as for any other task. -- **GIVEN** a task in step `A`, **WHEN** it is archived, unarchived in place, - cascade-archived, or deleted without its step changing, **THEN** no row is - written for that action. -- **GIVEN** an engine action that switches a task to a step in a **different** - workflow, **WHEN** the transition commits, **THEN** one row is written whose - `from_workflow_id` and `to_workflow_id` differ, with trigger - `engine_transition`. -- **GIVEN** a task whose step is stored as the empty string, **WHEN** it moves - into step `B`, **THEN** the row's `from_workflow_step_id` is NULL, not `""`. -- **GIVEN** a row referencing a workflow step that is later deleted, **WHEN** - the step is deleted, **THEN** the row survives unchanged with its step ID - intact. - -### Ordering, chain, and concurrency - -- **GIVEN** a task moved `A`→`B`→`A`→`B`, **WHEN** its rows are read ordered by - `(occurred_at, id)`, **THEN** four rows appear and each row's - `from_workflow_step_id` equals the previous row's `to_workflow_step_id`. -- **GIVEN** two rows for one task sharing an identical `occurred_at`, **WHEN** - they are ordered by `(occurred_at, id)`, **THEN** the order is total and - matches the order the transitions committed. -- **GIVEN** two callers concurrently moving the same task, one to `B` and one to - `C`, **WHEN** both transactions commit, **THEN** exactly two rows exist, their - chain is intact, and the later row's `to_workflow_step_id` equals the task's - final `workflow_step_id`. -- **GIVEN** an engine trigger that is retried with the same `OperationID`, - **WHEN** the retry is handled, **THEN** no second row is written. -- **GIVEN** any task, **WHEN** its last row is read, **THEN** its - `to_workflow_step_id` equals `tasks.workflow_step_id`. -- **GIVEN** two rows for one task whose `occurred_at` values run backwards - because the host clock was corrected, **WHEN** they are ordered by `id`, - **THEN** the chain invariant still holds and the ledger is not repaired or - reordered by timestamp. -- **GIVEN** a turn stamped with step `S` and a ledger row moving the task to `T` - a moment after that turn started, **WHEN** both are read, **THEN** they - disagree, and neither is treated as an error. - -### Actor and privacy - -- **GIVEN** authentication is disabled, **WHEN** the single synthetic user moves - a card, **THEN** actor kind is `human` and `actor_id` is that identity's user - ID. -- **GIVEN** a Jira or Linear watcher that moves a card, **WHEN** the move - commits, **THEN** actor kind is `integration` and `actor_id` is the watch ID. -- **GIVEN** any row, **WHEN** it is inspected, **THEN** `actor_id` holds an - identifier and no display name, email, title, or prompt text appears anywhere - in the row. - -### Migration and activation - -- **GIVEN** a database created before this feature, **WHEN** the backend boots, - **THEN** the ledger table and `telemetry_activations` exist, pre-existing - tasks have no ledger rows, and pre-existing turns carry no stamp. -- **GIVEN** a database already migrated, **WHEN** the migration runner is run - twice more, **THEN** it succeeds both times and changes nothing. -- **GIVEN** a Postgres deployment, **WHEN** the same fresh-then-replay sequence - runs, **THEN** it behaves identically to SQLite. -- **GIVEN** a first boot on which a contract activates, **WHEN** boot completes, - **THEN** `telemetry_activations` holds one row for that key with - `contract_version` = 1 and that boot's UTC time. -- **GIVEN** a subsequent boot, **WHEN** it completes, **THEN** the existing - activation row is unchanged. -- **GIVEN** an install where the ledger table is absent because its migration - silently failed, **WHEN** a step change is attempted, **THEN** the transaction - fails rather than committing a step change with no row, and the boot health - line reports the table as absent. - -### Writer health - -- **GIVEN** a new production statement that mutates `tasks.workflow_step_id`, - **WHEN** the test suite runs, **THEN** the pinning test fails until that - statement is registered. -- **GIVEN** a ledger row is written, **WHEN** metrics are read, **THEN** the - expvar counter for that trigger has incremented and a `telemetry.metric.*` - log line names the event. -- **GIVEN** any boot, **WHEN** startup completes, **THEN** one health line per - registered contract key reports object existence, activation time, row count, - and most recent `occurred_at`. - -## Out of scope - -- **Backfilling history.** No pre-activation transition is reconstructed, from - the message timeline or otherwise. -- **Dividing a cost event across a mid-turn step change.** Explicitly not - attempted; the "dominant of two steps" bucket remains. -- **`workflow_step_decisions`.** A different mechanism with a live writer. Not - merged, not extended, not read here. -- **Wiring, changing, or removing `session_step_history`** and its endpoint. - Separate work, lands first. -- **Any read API.** No HTTP route, no WS event, no MCP tool, no UI. Consumers - read the database. -- **The downstream extract and dashboard.** The consumer contract above is - recorded for the analysis side; implementing it is not part of this feature. -- **Step-name capture or step-rename history.** IDs only. -- **Retention, pruning, or archival of ledger rows.** -- **A `parent_session_id` / subagent grain.** Separate card. -- **Per-turn cost columns.** Separate card. diff --git a/docs/specs/ui/acp-model-configuration-summary.md b/docs/specs/ui/acp-model-configuration-summary.md index aec2be5baf..7eaa3e95c9 100644 --- a/docs/specs/ui/acp-model-configuration-summary.md +++ b/docs/specs/ui/acp-model-configuration-summary.md @@ -14,7 +14,7 @@ ACP agents can advertise an arbitrary ordered set of model-adjacent session conf - Kandev records a write-once baseline of the initial ACP select-option values advertised by the provider before agent-profile or runtime overrides are applied. The baseline is stored in the task-session database metadata and survives backend restart, process recreation, and ACP session resume. - The provider-default baseline is separate from mutable runtime configuration and is never used to restore provider state. A profile-selected value that differs from the provider default is therefore shown as changed as soon as the task session starts. -- Workflow restoration uses a separate immutable original effective configuration captured after profile settings settle. It does not change the comparison-only ownership of the provider-default baseline. See [Conditional Workflow Session Settings](../workflow-session-settings/spec.md) and [ADR-2026-08-01-workflow-session-original-configuration](../../decisions/2026-08-01-workflow-session-original-configuration.md). +- Workflow restoration uses a separate immutable original effective configuration captured after profile settings settle. It does not change the comparison-only ownership of the provider-default baseline. See [Conditional Workflow Session Settings](../tasks/requirements/workflow-session-settings.md) and [ADR-2026-08-01-workflow-session-original-configuration](../../decisions/2026-08-01-workflow-session-original-configuration.md). - In the task chat input and task context surfaces, the closed model selector always shows the current model name followed by every non-model config value whose raw current value differs from its baseline value, in ACP-provided option order. Values are joined by a slash with surrounding spaces; option names are omitted. Example: `GPT-5.6-Sol / Low / On`. - Until a session baseline is available, the closed task selector shows every current non-model value rather than hiding options whose changed state cannot yet be determined. - A value that returns to its baseline disappears from the closed summary. A currently advertised option with no baseline entry is treated as changed. Baseline entries for options the provider no longer advertises are ignored. diff --git a/docs/specs/ui/settings-manual-save.md b/docs/specs/ui/settings-manual-save.md index e4645b8cdf..515f902e81 100644 --- a/docs/specs/ui/settings-manual-save.md +++ b/docs/specs/ui/settings-manual-save.md @@ -3,7 +3,7 @@ status: shipped created: 2026-07-14 updated: 2026-08-09 owner: kandev -supersedes: docs/specs/workflow-settings-autosave/spec.md +supersedes: docs/specs/tasks/requirements/workflow-settings-autosave.md --- # Settings Manual Save diff --git a/docs/specs/workflow-quorum-decision-recording/spec.md b/docs/specs/workflow-quorum-decision-recording/spec.md deleted file mode 100644 index e2afdb946c..0000000000 --- a/docs/specs/workflow-quorum-decision-recording/spec.md +++ /dev/null @@ -1,1179 +0,0 @@ ---- -status: draft -created: 2026-08-19 -owner: Kandev ---- - -# Workflow Quorum Decision Recording - -## Why - -A `move_to_step` guarded by `wait_for_quorum` never fires. An Office task -driven through Work → Review with a reviewer attached runs the reviewer, gets a -substantive review back, and then sits at Review permanently. It is -indistinguishable, from every surface a human can see, from a hung card. - -Verified 2026-08-19 against task `7611f8da-4fe3-4bb2-a5a2-ebe6f90130f2` -(workspace `95542bf3`) in `~/.kandev/data/kandev.db`: - -``` -sqlite> SELECT COUNT(*) FROM workflow_step_decisions; -0 -``` - -Zero rows in the entire store, ever. - -## Verified current state - -The originating report named two causes. Only the first survives inspection. -Investigation surfaced four defects, three of which the report did not name. - -### The evaluation path is intact (originally reported as broken) - -`wait_for_quorum` **is** evaluated on the path that runs these workflows. The -claim that the orchestrator ignores the guard is false; the orchestrator -delegates to the engine, which owns the semantics. - -| Link | Evidence | -|---|---| -| Guard evaluated on the transition loop | `apps/backend/internal/workflow/engine/engine.go:275` | -| All five thresholds implemented | `apps/backend/internal/workflow/engine/quorum.go:88-119` | -| Office dispatches into that engine | `apps/backend/internal/office/engine_dispatcher/dispatcher.go:108` | -| Office shares the orchestrator's engine instance | `apps/backend/internal/backendapp/main.go:1520,1527` | -| Participant + decision stores wired in production | `apps/backend/internal/backendapp/main.go:1514-1515` | -| `clear_decisions` / `queue_run_for_each_participant` callbacks wired | `apps/backend/internal/orchestrator/workflow_callbacks.go:49,56` | - -The persistence layer is likewise complete: `RecordStepDecision` supersedes -prior verdicts inside a transaction (`phase2_sqlite.go:522-578`), -`ListStepDecisions` returns oldest-first (`:583-600`), and `AddTaskParticipant` -writes `decision_required = 1` for every reviewer and approver -(`office/repository/sqlite/participants.go:84-87`). - -The engine's evaluation is therefore correct **given its inputs**. With zero -decision rows, `all_approve` and `any_reject` both correctly evaluate false and -the card correctly stays put. The defects are all on the input side. - -### Defect 1 — an agent has no tool to record a decision - -The MCP tool registry (`apps/backend/internal/mcp/server/server.go:892-923`) -enables exactly four groups for `SurfaceOfficeTask`: `plan` (4 tools), -`related-tasks` (1), `office-documents` (3), and the capability-gated -`user-question` (1). Nine tools, none of which records a decision. - -`POST /api/v1/office/tasks/:id/approve` and `.../request-changes` exist -(`office/dashboard/handler.go:91-92`) and are reachable from the human UI -(`apps/web/components/task/simple/components/approval-action-bar.tsx:122-123`), -but no agent can reach either. A reviewer agent's only means of expressing a -verdict is a free-text comment, which nothing parses. - -### Defect 2 — the human path writes a verdict the evaluator cannot read - -Office records `changes_requested` (`office/models/models.go:653`), surfaced in -the frontend contract as `"approved" | "changes_requested"` -(`apps/web/lib/api/domains/office-extended-api.ts:297`). - -The evaluator counts only `approved` and `rejected` -(`workflow/engine/quorum.go:25-26,100-103`). - -A human clicking **Request changes** therefore writes a row that satisfies -neither `all_approve` (it is not an approval) nor `any_reject` (it is not -literally `rejected`). The task stays at Review with a rejection on file. This -is live today on the human path and is independent of Defect 1. - -The vocabulary is only half of it. `resolveParticipantID` -(`office/dashboard/decisions.go`) stamps every human decision with the constant -`userParticipantSentinel = "user"`, because the singleton user has no -`agent_profile_id` and therefore no `workflow_step_participants` row. -`latestDecisionsPerParticipant` (`workflow/engine/quorum.go`) discards any -decision whose `participant_id` is absent from the required slate, and that -slate is built from participant-row UUIDs. **A human decision is therefore -discarded before its verdict string is ever inspected.** Correcting the -vocabulary alone changes nothing on the human path; AC-43 is what makes AC-10 -reachable. - -### Defect 3 — recording a decision does not re-evaluate the transition - -`recordTaskDecision` (`office/dashboard/decisions.go:225-265`) persists the row, -publishes `OfficeTaskDecisionRecorded`, logs activity, and queues follow-up -agent runs. It never re-evaluates the guarded transition. - -`Engine.RecordParticipantDecision` (`workflow/engine/quorum.go:152-200`) does -exactly that re-evaluation and has **zero production callers** — only tests. - -So even once a verdict is recorded with a vocabulary the evaluator understands, -the transition fires only if some unrelated turn later completes on that task. -When the reviewer has already finished, nothing re-triggers. - -### Defect 4 — participant rows are pinned to the step they were attached at - -`AddTaskParticipant` (`office/repository/sqlite/participants.go:59-88`) writes -`step_id` = the task's step *at attach time*. No code ever updates a -participant's `step_id`; the only `UPDATE workflow_step_participants` statements -in the tree touch `agent_profile_id`. - -`requiredParticipants` filters on `state.CurrentStepID` -(`workflow/engine/quorum.go:70-83`). A participant attached at one step is -invisible at every other step, and an empty slate fails closed -(`quorum.go:57-60`). - -Live evidence from the verified task — the approver exists only at Work: - -| participant | step | role | decision_required | -|---|---|---|---| -| `afbd72cd` | Review | reviewer | 1 | -| `8eefba93` | **Work** | approver | 1 | -| `3cb7cd33` | Work | reviewer | 1 | - -Review's reviewer slate is non-empty, so Defects 1–3 are what block this task -today. But Approval's approver slate is **empty**, so fixing Review alone moves -the card one step and strands it again. - -Two further facts make this wider than the slate. First, `AddTaskParticipant`'s -idempotency probe is itself scoped to `step_id`, so re-attaching the same agent -after a step change **creates a second row** for the same -`(task_id, role, agent_profile_id)` — the two-row shape is how the live data -above came to exist, not a hypothetical. Second, `ListAllTaskParticipants` -(`office/repository/sqlite/participants.go`) is *also* step-scoped, so -`resolveDeciderRole` — the role-resolution precedent AC-4 cites — is blind in -exactly the same way: an approver whose row sits at Work cannot be resolved as -an approver while the task is at Approval. Slate membership, role resolution, -and the `participant_id` written on a decision must therefore all use one -canonical-row rule, which AC-44 defines. - -### Defect 5 — every failure is silent and identical - -The engine holds no logger (`workflow/engine/engine.go:131-144`) and `quorum.go` -emits nothing. `evaluateTransitionGuard` returns `(false, nil)` for all of: -store not wired, empty required slate, threshold genuinely unmet, and unknown -guard variant. From outside, a card legitimately awaiting two more approvals is -byte-for-byte identical to a card whose guard can never be satisfied. This is -the diagnostic gap that let the bug survive. - -### Live workflow configuration - -`Office Default` (`7561d51f`), steps Backlog(0) Work(1) Review(2) Approval(3) -Done(4). Both guarded steps carry the guard nested inside the action's `config`, -which `ConfigTransitionGuard` accepts (`workflow/engine/types.go:533`): - -```json -"on_turn_complete": [ - {"type": "move_to_step", "config": {"step_id": "", - "wait_for_quorum": {"role": "reviewer", "threshold": "all_approve"}}}, - {"type": "move_to_step", "config": {"step_id": "", - "wait_for_quorum": {"role": "reviewer", "threshold": "any_reject"}}} -] -``` - -Approval is the same shape with `role: approver`. - -## What - -Five behaviors, matching the five defects. - -1. An agent holding `reviewer` or `approver` on the current step can record an - approve or reject verdict with a reason, via an MCP tool, and it persists to - `workflow_step_decisions`. -2. A rejection recorded through any surface — agent tool or human UI — is - counted by `any_reject`. This needs both a vocabulary fix and a seat fix: the - human decider has no participant row at all, so a rejection counts as a veto - without requiring a quorum seat, while approvals still require one. -3. Recording a verdict through any surface immediately re-evaluates the guarded - transitions for that task's current step. -4. A participant with `decision_required` is counted at the step where the - decision is being taken, not only at the step where they were attached. -5. A guarded transition that does not fire says why, distinguishably, on a - surface a human reads. - -Ownership follows the engine. The engine already owns guard semantics, -threshold arithmetic, and the required-participant slate; this feature adds no -second implementation in the orchestrator. The MCP tool and the HTTP handler are -transports that call into the engine's decision API. AC-57 names that API and -the ports that make it reachable, because "call into the engine" is not today a -thing `office/dashboard` can do. There are two such ports and they are named -separately: AC-57a for the write side (record a decision, re-evaluate) and -AC-57d for the read side (evaluate every guard read-only, for the AC-24b -diagnostic surface). Behavior 5 is unbuildable without the second one. - -## Tool contract - -Pinned here because Build would otherwise invent it and Review would relitigate it. - -**Name.** `record_step_decision_kandev`, registered under a new -`office-decisions` group in the profile registry -(`apps/backend/internal/mcp/server/server.go:892-923`), enabled for -`SurfaceOfficeTask` only. - -**Arguments.** - -| Argument | Required | Shape | -|---|---|---| -| `decision` | yes | `"approved"` or `"rejected"` | -| `reason` | yes | non-empty string | - -No idempotency argument. A retry is a new verdict that supersedes the previous -one under AC-27; the tool is deliberately NOT idempotent across calls, and -AC-45 states this as contract so a builder does not invent a client-supplied -key. Idempotency in this feature is confined to transition application -(AC-30, AC-46). - -**Scoping.** The tool takes no `task_id`. It resolves the task and step from the -calling session, matching every other Office tool. An agent cannot record a -decision on a task other than the one its session is bound to. - -**Return.** On success the tool returns exactly these seven fields, observed by -AC-64: - -| Field | Meaning | -|---|---| -| `decision` | the recorded verdict | -| `role` | the caller's resolved role, per AC-4 | -| `step_id` | the validated step the row landed on, per AC-37 | -| `decision_id` | the decision row id, per AC-66 | -| `decided_at` | the row's `decided_at` value as persisted | -| `transition_applied` | `true` meaning the task left the step, per AC-48 — NOT that it was admitted at the target | -| `guards` | the AC-57d per-guard entries for the validated step, identical in shape to AC-24b's | - -The agent needs `transition_applied` to know whether its work at this step is -finished, and `step_id` to detect the AC-37 race in which its decision landed on -a step the task has since left. `decision_id` and `decided_at` are returned -because AC-57b requires `publishDecisionRecorded` to keep publishing -`decision_id` and `created_at` off the `DecisionRecord`, and once the persist -moves behind the AC-57a entry point those two values are no longer office's to -generate. - -There is deliberately NO scalar `required_count` / `received_count` pair. A step -may configure several guards, and AC-4's approver-wins precedence can resolve the -caller to `approver` while every guard at that step names `reviewer` — which is -`Office Default`'s Review exactly, per `## Live workflow configuration`. In that -case AC-42 means the decision is never counted by that guard at all, so a single -pair of counts is either ambiguous or actively misleading. `guards` gives the -agent the count against each guard that exists, which is the question it was -actually asking. - -**Reason column.** The reason is written to `workflow_step_decisions.comment`, -the column every Office reader already projects (`office/dashboard/decisions.go:141,428`). -It is NOT written to `note`, which `Engine.RecordParticipantDecision` currently -uses (`workflow/engine/quorum.go:180`) and which no Office surface reads. Any -engine-side decision API used by this feature writes `comment` for the -human-facing reason. A reason written only to `note` would be invisible in the -task timeline — this is the concrete bug this paragraph exists to prevent. - -## Acceptance criteria - -### Recording a decision (agent surface) - -- **AC-1** WHEN an Office agent session is attached to the task's current step - with role `reviewer` or `approver` and `decision_required = 1`, THE SYSTEM - SHALL expose an MCP tool that records a decision, accepting a verdict of - `approved` or `rejected` and a required non-empty reason. -- **AC-2** WHEN that tool is called with a valid verdict, THE SYSTEM SHALL write - one row to `workflow_step_decisions` with `task_id` = the calling session's - task, `step_id` = that task's current `workflow_step_id`, `role` = the - caller's resolved role, `decider_type` = `agent`, `decider_id` = the caller's - agent profile id, and `participant_id` = the id of the **seat** the participant occupies, as - defined by AC-50 — that is, AC-44 canonicalization followed by AC-20 collapse, - not AC-44 alone. The written `participant_id` SHALL be the same id - the quorum evaluator uses for that participant in the required slate; a - decision whose `participant_id` is not the canonical id is a defect, because - `latestDecisionsPerParticipant` silently discards it. -- **AC-3** WHEN the caller is not a participant on the task with - `decision_required = 1` for `reviewer` or `approver`, THE SYSTEM SHALL reject - the call with a permission error and write no row. -- **AC-4** WHEN the caller holds both `reviewer` and `approver`, THE SYSTEM - SHALL record the decision under `approver`, matching the existing - approver-wins precedence in `resolveDeciderRole` - (`office/dashboard/decisions.go`). -- **AC-4a** THE SYSTEM SHALL resolve the caller's role over the same - participant population that AC-50 builds, not over rows scoped to the - task's current step alone. `ListAllTaskParticipants` is step-scoped today, so - an approver attached at an earlier step is currently unresolvable at the step - where the approval is due; role resolution and slate membership SHALL NOT - disagree about who participates. -- **AC-5** WHEN the verdict is any value other than `approved` or `rejected`, - THE SYSTEM SHALL reject the call with a validation error and write no row. -- **AC-6** WHEN the reason is empty or whitespace-only, THE SYSTEM SHALL reject - the call with a validation error and write no row. -- **AC-7** WHEN the task has no `workflow_step_id` bound, THE SYSTEM SHALL - reject the call with an error naming the unbound step and write no row. -- **AC-8** THE SYSTEM SHALL NOT expose the decision tool to sessions whose - profile does not enable `SurfaceOfficeTask`. - -- **AC-55** THE SYSTEM SHALL validate the decision tool's preconditions in this - order: (1) the task has a bound `workflow_step_id` (AC-7), (2) the caller - resolves to a participant with `decision_required = 1` for `reviewer` or - `approver` (AC-3), (3) the verdict is `approved` or `rejected` (AC-5), (4) the - reason is non-empty (AC-6). The step check precedes the permission check - because `AddTaskParticipant` silently no-ops when a task has no bound step - (`office/repository/sqlite/participants.go`), so an unbound-step task has zero - participant rows **by construction**; a permission-first implementation would - report AC-3's opaque permission error on the most likely real trigger for AC-7, - a task created before a workflow step was assigned. The order is stated because - both preconditions are false at once in that case and the two errors differ in - diagnostic value. - -- **AC-64** WHEN the decision tool returns successfully, THE SYSTEM SHALL return - the seven fields enumerated in `## Tool contract` — `decision`, `role`, - `step_id`, `decision_id`, `decided_at`, `transition_applied` and `guards` — - and SHALL NOT return a scalar `required_count` or `received_count`. - `guards` SHALL equal the AC-57d snapshot's `Guards` for the validated step, - in the same order, so the tool and the AC-24b endpoint report the same - arithmetic. `guards` SHALL be an empty list, not an error and not omitted, - when the validated step configures no guarded transition. - WHEN the post-write AC-57d snapshot cannot be computed — the AC-15 case, where - the write succeeded and the re-evaluation errored — THE SYSTEM SHALL return - `guards` as an empty list and `transition_applied` as `false`, and SHALL still - report the call as successful per AC-15, the failure being surfaced through - AC-24 rather than to the agent. A diagnostic read that failed SHALL NOT put the - recorded verdict at risk. - This AC exists because without it only two of the returned fields — `step_id` - and `transition_applied`, via AC-37 and AC-46 — were observable by any - acceptance criterion, so a wrong `role` or a wrong count could ship untested. - -### Verdict vocabulary - -- **AC-9** THE SYSTEM SHALL treat `changes_requested` as a rejection wherever - `rejected` is treated as one, so that `any_reject` is satisfied by either. -- **AC-10** WHEN a human records a decision through - `POST /tasks/:id/request-changes`, THE SYSTEM SHALL produce a stored verdict - that satisfies `any_reject` for the guard evaluating at that step, whatever - role that guard names. AC-9 alone does not achieve this; AC-43 and AC-58 are - the other halves. -- **AC-43** THE SYSTEM SHALL satisfy `any_reject` when, among decisions - recorded at the evaluating step for the guard's role, the LAST row per decider - identity `(decider_type, decider_id)` under the AC-26 ordering is a rejection - — whether or not that decider's `participant_id` is in the required slate. A - rejection is a veto: one is sufficient and it does not require a quorum seat. - This is what makes a singleton-user rejection countable, since that decider - has no `agent_profile_id` and therefore no participant row, and is written - with the `userParticipantSentinel` participant id. -- **AC-43b** THE SYSTEM SHALL key the AC-43 veto scan on the latest verdict per - decider, NOT on the presence of any rejection row. A decider who rejects and - then records an approval at the same step SHALL NOT continue to veto. Stated - because the naive reading — scan for a rejection — reintroduces the defect - named under `## Persistence guarantees`: the engine's `DecisionInfo` - projection carries no `superseded_at`, so "non-superseded" is not a predicate - the evaluator can express, and only last-row-wins is. -- **AC-43c** THE SYSTEM SHALL extend the engine's `DecisionInfo` projection to - carry `decider_type`, `decider_id`, `role`, and `comment`. AC-43 needs decider - identity, AC-42 needs `role`, and AC-38 needs `comment`; the projection today - carries only `participant_id`, `decision`, and `note`, so none of the three is - satisfiable without this. The underlying columns already exist and - `ListStepDecisions` already selects them. -- **AC-43a** THE SYSTEM SHALL count only slate members, per AC-50, toward - `all_approve`, `all_decide`, `majority_approve`, and `n_approve:`. An - approval is a quorum contribution and does require a seat. AC-43 is scoped to - `any_reject` alone, so a non-slate approval never advances a task. -- **AC-58** THE SYSTEM SHALL evaluate the AC-43 veto over decisions with - `decider_type = user` WITHOUT filtering on the guard's role, while continuing - to filter agent decisions by role per AC-42. The singleton human's stored - `role` SHALL remain whatever `resolveDeciderRole` returns for a user caller — - today the constant `approver`, which `## Out of scope` leaves unchanged — and - the veto SHALL NOT depend on it. - Without this, AC-10 is unsatisfiable at every reviewer-guarded step, which is - the step the motivating card in `## Why` is stuck at: `resolveDeciderRole` - sets `hasApprover = true` unconditionally for `decider_type = user` and - returns on the approver-wins branch, so a human clicking **Request changes** - at `Office Default`'s Review — whose guards name `role: reviewer` — writes - `role = approver`, and AC-42 discards it before AC-43 ever sees it. - The alternative, stamping the human's decision with the evaluating guard's - role, is rejected rather than merely unchosen: a decision is recorded once - while a step may configure several guards, so "the guard's role" is not well - defined at write time. The human is the operator of the board rather than a - seated participant, so a human rejection is a veto over the step, not a - role-scoped quorum contribution. AC-43a already denies a seatless approval any - effect, so this asymmetry touches rejections only. - Boundary, stated so it is a decision rather than an accident: when a step - configures `any_reject` guards under two DIFFERENT roles, one human rejection - satisfies both at once, because the veto is role-agnostic by construction. - That is intended — the human is the operator of the board and is rejecting the - step, not a role's share of it — and AC-17 still means only the first - satisfied transition in configured order is applied. `Office Default` does not - configure this shape; nothing forbids it. -- **AC-11** THE SYSTEM SHALL continue to accept and store verdict strings - outside the recognized set without error, counting them toward `all_decide` - and toward neither `all_approve` nor `any_reject` — preserving the documented - free-form behavior at `workflow/engine/quorum.go:22-24`. -- **AC-12** THE SYSTEM SHALL preserve the existing wire value written by the - human `request-changes` path, so the frontend DTO union - `"approved" | "changes_requested"` continues to typecheck unchanged. - -### Re-evaluation on record - -- **AC-13** WHEN a decision is recorded through any surface, THE SYSTEM SHALL - re-evaluate the current step's `on_turn_complete` guarded transitions before - returning to the caller, using the task's active session as defined in AC-16 - — including on the human HTTP path, which has no calling session of its own. -- **AC-14** WHEN that re-evaluation finds a satisfied guard, THE SYSTEM SHALL - apply the transition, and the recording call SHALL still report success. -- **AC-15** WHEN that re-evaluation fails or errors, THE SYSTEM SHALL keep the - recorded decision, report the recording as successful, and surface the - re-evaluation failure through the observability required by AC-24. -- **AC-16** THE SYSTEM SHALL resolve the re-evaluation session as - `GetActiveTaskSessionByTaskID` already defines it - (`task/repository/sqlite/session.go`): the task's most recently started - session whose state is one of `CREATED`, `STARTING`, `RUNNING`, or - `WAITING_FOR_INPUT`, ordered by `started_at` descending, limit one. A task is - "unresolvable" for this purpose exactly when that query returns no row. -- **AC-16a** WHEN no session is resolvable under AC-16, THE SYSTEM SHALL record - the decision and skip re-evaluation without erroring, matching the existing - blank-session behavior in `Engine.RecordParticipantDecision`, and SHALL report - the skip under AC-23 so a card that recorded a verdict but never re-evaluated - is distinguishable from one that re-evaluated and found the threshold unmet. -- **AC-17** WHEN two guarded transitions are both satisfied, THE SYSTEM SHALL - apply the first in the step's configured `on_turn_complete` action order, - preserving first-transition-wins (`workflow/engine/engine.go:268-283`). - -- **AC-47** THE SYSTEM SHALL run the AC-13 re-evaluation with the engine in - **committing mode** (`EvaluateOnly = false`), so the engine applies a satisfied - transition itself rather than returning a payload for a transport to apply. - Consequently the AC-30 operation id SHALL be marked applied only after the - transition has been applied, or deliberately abandoned under AC-46 — never - before. This is stated because the two precedents in tree disagree and only one - is safe: `Engine.RecordParticipantDecision` passes `EvaluateOnly: true` today - while `office/engine_dispatcher` does not, and `HandleTrigger` calls - `markOperationApplied` on its success path only. Under committing mode an apply - failure returns before the mark, so a retry can still land; under evaluate-only - the id is marked while no transition has been applied, and every retry then - returns `Idempotent: true` with the transition lost permanently — the exact - stuck-card failure this feature exists to eliminate. The existing - `EvaluateOnly: true` on `RecordParticipantDecision` SHALL therefore change. - Keeping the apply inside the engine is also what `## What` means by ownership - following the engine. "Committing mode" here names APPLY-OWNERSHIP — the - engine performs the apply and marks the operation id after it — and NOT reuse - of the full trigger path; AC-65 bounds what the re-evaluation is allowed to - run. - -- **AC-65** THE SYSTEM SHALL scope the AC-13 re-evaluation to the step's guarded - transition actions ONLY. The AC-57a entry point SHALL evaluate those - `on_turn_complete` actions for which `isTransitionAction` holds and which - carry a `wait_for_quorum` guard, in configured order, and apply at most one, - per AC-17 and AC-46. It SHALL NOT execute non-transition action callbacks, and - it SHALL NOT be implemented as a plain - `HandleTrigger(TriggerOnTurnComplete)` call. - This is stated because the obvious implementation is exactly the wrong one. - `evaluateActions` guards only actions for which `isTransitionAction` holds and - routes EVERY other action to `executeCallback` — and that call is not gated on - `EvaluateOnly`, unlike `applyTransition` and `PersistData`. So a - HandleTrigger-based re-evaluation re-runs whatever else the step configures on - `on_turn_complete` — `queue_run`, `clear_decisions`, - `queue_run_for_each_participant`, `run_code_review`, `create_child_task`, - `switch_workflow`, `set_workflow_data` are all registered kinds — once per - RECORDED DECISION rather than once per completed turn. AC-30's operation id is - deliberately unique per decision, so the id-dedup does not suppress the repeat - either. `Office Default` configures only two `move_to_step` actions on - `on_turn_complete`, so this is invisible on the shipped workflow and on every - regression in `### Regression`; it would first appear on a workflow that pairs - a quorum guard with a side-effect action, as `clear_decisions` and - `queue_run_for_each_participant` already are elsewhere in this same workflow's - `on_enter`. A reviewer vote is not a turn completion, and this AC is what keeps - the two from being conflated. - Boundary: when the current step configures no guarded transition at all, the - re-evaluation SHALL apply nothing and report `transition_applied = false` - without erroring. A decision is legitimately recordable at such a step — AC-3 - gates on participation, not on the presence of a guard — and treating "nothing - to evaluate" as a failure would reject valid verdicts. - -- **AC-57** THE SYSTEM SHALL implement the AC-50 slate construction, the AC-51 - seat matching, the AC-4/AC-4a role resolution and the decision write EXACTLY - ONCE, in the engine, against the engine's `ParticipantStore` and - `DecisionStore` ports, and SHALL expose them to both transports through a - single engine decision entry point. Neither the MCP tool nor - `office/dashboard` SHALL carry its own copy. This is stated because the - ownership sentence in `## What` is not satisfiable as written today: - `DashboardService`'s only engine connection is - `engineDispatcher shared.WorkflowEngineDispatcher`, whose entire interface - surface is one `HandleTrigger` method — no slate read, no role resolution, no - decision write. The office decision path therefore runs through - `recordTaskDecision` -> `resolveDeciderRole` -> `s.repo.ListAllTaskParticipants` - in `office/repository/sqlite`, a DIFFERENT Go package from the - `workflow/repository` one backing the engine's `ParticipantStore`, while both - read and write the same `workflow_step_participants` table. Left unstated, a - builder may reasonably implement AC-50's four steps a second time on the - office side — two canonicalizations over one table, which is precisely the - read-side/write-side divergence AC-44 exists to prevent. -- **AC-57a** THE SYSTEM SHALL extend `shared.WorkflowEngineDispatcher` with an - additive **write-side** method that records a decision and performs the AC-13 - re-evaluation, and SHALL wire it at the existing dispatcher construction site - (`backendapp/main.go`, where `officeenginedispatcher.New` is already built - from the shared engine instance and the task repo). `HandleTrigger` and every - existing caller of it SHALL be unchanged. This is the write half of the - minimum port change that makes AC-57 buildable; without naming it, "call into - the engine's decision API" has no referent. It is the FIRST of exactly two - additive methods this feature adds to that interface — AC-57d names the - second, read-side one. Earlier wording said "one additive method", which read - as forbidding the read-side method that AC-24b cannot be built without. -- **AC-57b-i** THE AC-57a entry point's return SHALL carry the decision row id - and the persisted `decided_at`, because AC-57b promises - `publishDecisionRecorded` is preserved unchanged and that function publishes - `"decision_id": d.ID` and `"created_at": d.CreatedAt` off the - `DecisionRecord`. Once the persist moves behind the entry point, office can no - longer source those two values itself. Without this the builder must either - widen the entry point's return undocumented or locally synthesise an id and a - timestamp, and a locally taken timestamp need not equal the row's stored - `decided_at` — publishing an event whose `created_at` disagrees with the row - it describes. AC-64 observes both fields on the tool surface and they are the - same two values. - Concretely: the entry point SHALL stamp `decided_at` once, persist that value, - and return THAT value — never re-read the clock to build the response. Two - clock reads around one write are the defect this clause exists to forbid; they - would put `created_at` on the published event ahead of the row every time, - which is invisible in review and wrong in every timeline that joins the two. -- **AC-57b** THE SYSTEM SHALL confine the office-side change to the resolve, - persist and re-evaluate core of `recordTaskDecision`. Its surrounding - behavior — `publishDecisionRecorded`, `logDecisionActivity`, - `runReactivityForDecision`, and the `DecisionRecord` shape returned to the - handler — SHALL be preserved, and every other reader of - `ListAllTaskParticipants` (task inbox, scheduler reactivity, the task-detail - handler) SHALL keep today's step-scoped behavior, as `## Out of scope` - already states. This bounds the refactor: the card repoints one function's - core, it does not migrate `office/dashboard` onto the engine wholesale. -- **AC-57c** WHEN the engine decision entry point is not wired — `main.go` - already returns early and logs "workflow engine not initialised; office engine - dispatcher disabled" when `orchestratorSvc.WorkflowEngine()` is nil, leaving - `DashboardService.engineDispatcher` nil — THE SYSTEM SHALL reject the decision - call with an error and write no row, on BOTH transports. It SHALL NOT fall back - to a second office-side implementation of AC-50, which would resurrect exactly - the divergence AC-57 forbids, and it SHALL NOT silently persist a decision that - can never be counted. This mirrors the existing `decisionStoreNotWiredErr` - guard in `recordTaskDecision` rather than inventing a new failure mode. -- **AC-57d** THE SYSTEM SHALL extend `shared.WorkflowEngineDispatcher` with a - second additive method, **read-only**, that evaluates every guarded - `on_turn_complete` transition configured at a task's current step and returns - one entry per guard together with the AC-62 `reevaluation_blocked` value: - - `EvaluateStepQuorum(ctx, taskID) (QuorumSnapshot, error)`, where - `QuorumSnapshot` carries `StepID`, `ReevaluationBlocked bool`, and - `Guards []QuorumGuardState`, and each `QuorumGuardState` carries - `TargetStepID`, `Role`, `Threshold`, `RequiredCount`, `ReceivedCount`, - `Satisfied`, `Reason` and `Error`. - - This method SHALL apply no transition, write no row, execute no action - callback, emit no AC-24 log record and increment no AC-24a counter. It SHALL - reuse the AC-50 slate construction, the AC-51 seat matching and the AC-52 - reason precedence — it is a second CALLER of that logic, never a second - implementation of it, per AC-57. - - **Ordering.** `Guards` SHALL be returned in the step's configured - `on_turn_complete` action order, so that AC-61's selection rule and AC-17's - first-transition-wins read off one and the same order. This is the named - ordering AC-61 depends on; without it AC-61's "first such entry" has no - referent. - - **Nil / empty / error.** A task with no bound `workflow_step_id` SHALL return - an empty `Guards`, `ReevaluationBlocked = false` and no error, which is what - AC-24c requires. A step with a bound id but no guarded transition SHALL - likewise return an empty `Guards` and no error; AC-61 aggregates both to - `clear`. A store error SHALL surface as a per-guard `evaluation_error` entry - per AC-23, not as a method-level error, so one failing guard does not blank - the others. A method-level error is reserved for the not-wired case. - - **Not wired.** When the dispatcher is nil, per the AC-57c condition, the - AC-24b handler SHALL return an error rather than falling back to an - office-side evaluation — the read side carries the same no-fallback rule as - the write side, and for the same reason. - - **Unknown task.** An unresolvable `taskID` SHALL surface as a method-level - error, distinct from the unbound-step case of AC-24c which is a successful - empty result; the AC-24b handler SHALL map it exactly as the sibling - `GET /tasks/:id/decisions` route already maps an unknown task. Stated because - AC-24c's "return empty rather than error" is easy to over-apply to a task that - does not exist at all, which would report a missing task as a healthy one. - - **Idempotency and concurrency.** The method is a pure read: two concurrent - calls compute independently from committed state and neither observes nor - mutates the other. It needs no operation id. - - AC-24b's HTTP payload SHALL be a direct projection of this snapshot, so the - endpoint and the tool's `guards` field cannot drift apart. - - Without this AC there is no legal way to build AC-24b at all: AC-57 forbids - `office/dashboard` from carrying its own copy of the slate machinery, and - `DashboardService`'s only engine connection is - `engineDispatcher shared.WorkflowEngineDispatcher`, whose declared surface is - `HandleTrigger` alone. AC-24b, AC-24c, AC-25, AC-60, AC-61 and AC-62 all - depend on this method existing. - - **Implementation note, not a requirement:** the in-tree precedent for adding a - dispatcher capability is a separate optional interface plus a type assertion - (`handledWorkflowEngineDispatcher` in `office/dashboard/service_tasks.go` - reaching `HandleTriggerHandled`), rather than widening the shared interface. - Either shape satisfies this AC; the requirement is that the method exist and - be reachable from the AC-24b handler, not which of the two idioms carries it. - -### Participant slate - -- **AC-18** WHEN a quorum guard evaluates at a step, THE SYSTEM SHALL count a - **per-task** participant row (`task_id != ''`) with `decision_required = 1` - and a matching role even when that row was attached under a different step of - the same workflow. -- **AC-18a** THE SYSTEM SHALL NOT extend cross-step counting to template-level - rows (`task_id = ''`). A template row is bound to one step by workflow design, - so it remains visible only at its own step. Without this, a Work-step template - reviewer would be pulled into a Review-step quorum. -- **AC-44** THE SYSTEM SHALL identify a participant by the natural key - `(task_id, role, agent_profile_id)`, and SHALL select one **canonical row** - per key: the row whose `step_id` equals the evaluating step when such a row - exists, otherwise the row with the lowest `id` in ASCII order among that key's - rows. `workflow_step_participants` carries no timestamp column, so `id` ASC is - the tiebreak — "most recent" is not expressible here. The same rule SHALL be - used by slate construction (AC-18), by role resolution (AC-4a), and by the - `participant_id` written on a decision (AC-2), so that the read side and the - write side cannot disagree. AC-44 is step 3 of AC-50; the id that finally - identifies a participant is the one surviving AC-50 step 4, because AC-20's - template-versus-per-task collapse keys on `(role, agent_profile_id)` and can - still discard an AC-44 canonical row. -- **AC-19** WHEN both a step-specific row and a differently-stepped row exist - for the same `(task_id, role, agent_profile_id)`, THE SYSTEM SHALL count the - participant exactly once, as the AC-44 canonical row. This shape is produced - in normal operation: `AddTaskParticipant`'s idempotency probe is scoped to - `step_id`, so re-attaching the same agent after a step change inserts a second - row rather than matching the first. -- **AC-20** THE SYSTEM SHALL continue to honor template-level rows - (`task_id = ''`) with per-task rows taking precedence on - `(role, agent_profile_id)`, per `phase2_sqlite.go:20-27`. -- **AC-21** WHEN the required slate for the guard's role is empty AND the - guard's threshold is an approve-style one (`all_approve`, `all_decide`, - `majority_approve`, `n_approve:`), THE SYSTEM SHALL NOT fire the - transition, and SHALL report the empty slate distinctly from an unmet - threshold per AC-23. The scoping to approve-style thresholds is required by - AC-59; an unscoped empty-slate short-circuit makes the AC-43 veto unreachable. -- **AC-22** WHEN a decision is on file for a participant who WAS in the required - slate and has since been removed, THE SYSTEM SHALL ignore that decision for - the counting thresholds, preserving the documented mid-flight removal - behavior in `applyThreshold`. This governs removed slate members only; it does - NOT govern deciders who never held a seat, whose rejections are handled by - AC-43 and whose approvals are excluded by AC-43a. - -- **AC-49** THE SYSTEM SHALL extend the engine's `ParticipantStore` port with a - task-scoped read — `ListTaskParticipants(ctx, taskID)`, returning every - per-task row (`task_id = ?`) for that task irrespective of `step_id` — and - SHALL leave `ListStepParticipants(ctx, stepID, taskID)` unchanged. AC-18's - cross-step counting is not expressible through the existing port, whose only - production query is `WHERE step_id = ? AND (task_id = '' OR task_id = ?)`; - widening that predicate in place would also pull template rows across steps, - which AC-18a forbids. A task with no per-task rows SHALL yield an empty list, not an - error, matching `ListStepParticipants`'s documented empty-is-valid contract. - This is the participant-side twin of AC-43c — the projection and the port both - have to be named, or the builder invents them. -- **AC-50** THE SYSTEM SHALL build the required slate for a guard, at the - evaluating step, in exactly this order: - 1. **Gather** — per-task rows for the task via AC-49 (any step), plus - template rows (`task_id = ''`) at the evaluating step only, per AC-18a. - 2. **Filter** — keep rows whose `role` matches the guard and whose - `decision_required` is true, per `requiredParticipants`. - 3. **Canonicalize** — collapse to one row per `(task_id, role, - agent_profile_id)` by AC-44. - 4. **Collapse** — collapse across `task_id` to one row per - `(role, agent_profile_id)`, the per-task row winning, per AC-20. - - The row surviving step 4 is THE **seat**; its `id` is the slate id that AC-2 - writes and AC-26 counts. The order is stated because AC-44 keys on - `(task_id, role, agent_profile_id)` while AC-20 keys on - `(role, agent_profile_id)`: running AC-44 alone seats one agent twice when it - holds both a template row and a per-task row, inflating `totalRequired` so - `all_approve` can never be met, and running AC-20 alone discards AC-44's - cross-step canonicalization. Boundary: a row whose `agent_profile_id` is empty - has no identity to canonicalize on and SHALL be kept as its own seat, keyed by - its row `id`, at both step 3 and step 4 — collapsing two such rows into one - seat would under-count `totalRequired` and let `all_approve` fire on a single - approval. -- **AC-51** WHEN counting decisions against the slate, THE SYSTEM SHALL map each - decision to at most one AC-50 seat: by `participant_id` when that id is a seat - id, otherwise by the seat whose `(role, agent_profile_id)` equals the - decision's `(role, decider_id)` when `decider_type` = `agent`. A decision that - maps to no seat is not counted toward any approve-style threshold, per AC-43a; - `any_reject` does not use this mapping at all, per AC-43. The fallback exists - because a participant's AC-44 canonical row can CHANGE after that participant - has already decided — AC-19 states the two-row shape arises in normal - operation, so attaching a current-step row for an agent who decided while only - an earlier-step row existed flips the seat id, and an id-only match would - silently discard a real verdict, which is the discard class AC-2 exists to - prevent. AC-22 is unaffected: a participant removed from the slate entirely has - no seat under either match, so its decision is still ignored. -- **AC-59** WHEN the required slate is empty, THE SYSTEM SHALL still evaluate an - `any_reject` guard and SHALL NOT short-circuit it. The empty-slate fail-closed - of AC-21 exists because `all_approve` is `approveCount == totalRequired`, - which is vacuously TRUE at `0 == 0` — an empty slate would otherwise advance a - task nobody reviewed. `any_reject` is `rejectCount > 0` and cannot fire - vacuously, so the same guard rail is unnecessary there and actively harmful: - it makes the AC-43 seatless veto unreachable in exactly the case where every - decider is seatless. Implementation consequence, stated because this is not a - free change: `evaluateWaitForQuorum` returns at `len(required) == 0` before - `ListStepDecisions` is called, so the empty-slate check must become - threshold-aware rather than remaining a precondition. - -### Distinguishing waiting from stuck - -- **AC-23** WHEN a guarded transition does not fire, THE SYSTEM SHALL record - exactly one reason code from this closed set: - `threshold_not_met`, `slate_empty`, `decision_store_unwired`, - `participant_store_unwired`, `guard_variant_unrecognized`, - `threshold_unrecognized`, `threshold_unsatisfiable`, `evaluation_error`, and - `session_unresolvable`. - `decision_store_unwired` and `participant_store_unwired` are separate because - `evaluateWaitForQuorum` SHALL nil-check them separately — today it does not, - the current code being a single combined - `if e.decisions == nil || e.participants == nil { return false, nil }`, so - splitting that branch in two is a REQUIRED change of this feature and not an - existing property to be relied on. Left unsplit, the two codes are - indistinguishable in practice and AC-23's taxonomy silently loses a member; - `threshold_unsatisfiable` carries AC-40, the case where the threshold can - never be met by any future decision and the card is stuck rather than waiting; - `evaluation_error` carries a genuine error returned by - `ListStepParticipants` or `ListStepDecisions`; `session_unresolvable` carries - AC-16a. `threshold_unrecognized` carries AC-53, a threshold string - the evaluator does not recognize at all. The set is closed rather than "at - minimum" so that AC-24a's expvar keys are enumerable and a new reason cannot - appear unlabelled. More than one condition can hold at once; AC-52 fixes which - single code is reported. -- **AC-24** THE SYSTEM SHALL emit, each time a guarded transition does not fire - **on the engine's transition-evaluation path**, a structured log record - carrying the task id, step id, guard role, - threshold, required-participant count, decisions-received count, the AC-23 - reason code, and an `error` field which is populated when and only when the - reason is `evaluation_error`. The engine holds no logger today, so wiring one - is part of this feature. -- **AC-24a** THE SYSTEM SHALL expose counts of not-fired guard evaluations under - `/debug/vars`, keyed by the AC-23 reason, following the existing `workflow_*` - expvar convention documented in the root `CLAUDE.md`. Like AC-24, this counts - **engine-path evaluations only**: the AC-24b diagnostic endpoint evaluates - guards read-only and SHALL emit no log record and increment no counter. Stated - because AC-54 requires that endpoint to evaluate every guard live, so the two - ACs otherwise overlap: were diagnostic reads counted, these counters would - measure UI polling rather than workflow health, and a card left open in a - browser would out-count a genuinely stuck one. -- **AC-24b** THE SYSTEM SHALL expose guard state for a task at - `GET /api/v1/office/tasks/:id/quorum`, a sibling of the existing - `GET /api/v1/office/tasks/:id/decisions` route - (`office/dashboard/handler.go`) and carrying the same authorization as that - route. The response SHALL contain one entry per guarded `on_turn_complete` - transition configured at the task's current step, each with: `target_step_id`, - `role`, `threshold`, `required_count`, `received_count`, `satisfied` (a - boolean), `reason` (an AC-23 code, present if and only if `satisfied` is - `false` — see AC-60), and `error` (populated only for `evaluation_error`). A - task at a step with no guarded transition SHALL return an empty list, not a - 404. Entries are computed live per AC-54, which also defines the payload's - top-level `reevaluation_blocked` field. The payload SHALL be a direct - projection of the AC-57d snapshot — that is the only path by which this - handler may reach the slate machinery, since AC-57 forbids it from carrying - its own copy — preserving AC-57d's ordering. AC-61 defines how the card - renders when entries disagree. -- **AC-24c** WHEN the task has no bound `workflow_step_id`, the AC-24b endpoint - SHALL return an empty list rather than an error, so a diagnostic read never - fails on the state it exists to diagnose. -- **AC-25** WHEN the task's AC-24b entries aggregate to `awaiting` under AC-61, - the Office task detail view SHALL render the task as awaiting decisions — - showing `received_count` of `required_count` for the role of the entry that - AC-61 selects — and SHALL NOT render it as failed or errored. When they - aggregate to `stuck`, the view SHALL render a visually distinct stuck state - naming the selected entry's AC-23 reason. This is the whole diagnostic gap: - the two states are byte-identical today. AC-61 exists because AC-24b returns a - LIST and `Office Default`'s Review configures two guards, so "the reason" is - not well defined without an aggregation rule. - -- **AC-52** WHEN more than one AC-23 condition holds at once, THE SYSTEM SHALL - report the first that applies in this order, which mirrors the order the code - discovers them: `guard_variant_unrecognized`, `decision_store_unwired`, - `participant_store_unwired`, `evaluation_error`, `slate_empty`, - `threshold_unrecognized`, `threshold_unsatisfiable`, `threshold_not_met`. - AC-23 requires exactly one code, and AC-21 and AC-40 both fire for an empty - slate with any positive `n_approve:`, since every positive N exceeds a slate - of zero. `slate_empty` wins: it is the more actionable diagnosis, and - `evaluateWaitForQuorum` already returns at `len(required) == 0` before the - threshold is examined. The ninth code, `session_unresolvable`, is not in this - ordering because it is not a guard-evaluation outcome at all — it is the - recording-time skip of AC-16a, surfaced through AC-24/AC-24a and, on the - AC-24b payload, through the separate field AC-54 defines. `slate_empty` is - reachable only for approve-style thresholds, per AC-21 as scoped and AC-59; an - `any_reject` guard over an empty slate reports `threshold_not_met`, because a - seatless rejection can still arrive and so the card is waiting, not stuck. -- **AC-53** WHEN the guard's threshold is a string that is neither one of the - five recognized thresholds nor prefixed `n_approve:`, THE SYSTEM SHALL not fire - the transition and SHALL report AC-23 reason `threshold_unrecognized`, which - AC-25 renders as stuck. `applyThreshold` returns false from its final - fall-through for such a value today, indistinguishably from - `threshold_not_met`, so a mistyped threshold would render as "awaiting - decisions" forever — the exact diagnostic gap this feature exists to close. - `guard_variant_unrecognized` is reserved for a guard whose variant is not - `wait_for_quorum` at all, matching `evaluateTransitionGuard`'s fail-closed - branch. -- **AC-54** THE SYSTEM SHALL compute AC-24b's per-guard entries by evaluating - each guarded `on_turn_complete` transition configured at the task's current - step **live, at request time, and independently** — not by replaying the - engine's transition loop, and not by reading a stored evaluation. Independent - evaluation is required because `evaluateActions` short-circuits, evaluating a - guard only while `targetStepID == ""`, so a replay cannot enumerate every - guard the way AC-24b requires. A per-guard `reason` SHALL therefore never be - `session_unresolvable`, which is a recording-time skip rather than an - evaluation outcome; the AC-24b payload SHALL instead carry a top-level - `reevaluation_blocked` boolean, defined by AC-62, and AC-61 SHALL treat it as - stuck-distinct. The field SHALL be `false` when the task has no decisions at - its current step, so an untouched task is never rendered as stuck. - -- **AC-60** THE SYSTEM SHALL set an AC-24b entry's `satisfied` to `true` when - that guard's threshold is met at request time, and SHALL omit `reason` - entirely for such an entry. AC-23's set is a taxonomy of why a guard did NOT - fire and SHALL NOT gain a "satisfied" member, so AC-24a's expvar keys stay - enumerable. The state is reachable rather than theoretical: under AC-16a a - verdict is recorded, re-evaluation is skipped, and the task sits at the step - with its quorum already met. AC-16a is the whole of it. An earlier draft also - claimed the AC-46 abandon as a second route; it is not one, and the claim is - removed rather than left to send a builder hunting: AC-46 abandons PRECISELY - when the task's `workflow_step_id` no longer equals the evaluated step — the - race winner has already moved it — and AC-24b evaluates at the task's CURRENT - step, so the abandoned guard is not in the returned list at all. -- **AC-61** THE SYSTEM SHALL aggregate a task's AC-24b entries to exactly one - card-level state: `stuck` when `reevaluation_blocked` is `true` or when ANY - entry is unsatisfied with a `reason` other than `threshold_not_met`; - otherwise `awaiting` when any entry is unsatisfied; otherwise `clear`. When - the state is `stuck` the SELECTED entry is the first such entry in the step's - configured `on_turn_complete` action order, matching AC-17's - first-transition-wins; when `awaiting`, it is the first unsatisfied entry in - that same order. Stuck outranks awaiting because a stuck guard is the - actionable diagnosis and MUST NOT be masked by a sibling that is merely - waiting — a mistyped `n_approve:` reporting `threshold_unsatisfiable` - beside a healthy `any_reject` reporting `threshold_not_met` is the concrete - case, and rendering that card as "awaiting decisions" forever is the exact - defect AC-53 was added to close. Boundary: an EMPTY entry list aggregates to - `clear`, which is the correct rendering for both a step with no guarded - transition and the AC-24c unbound-step case — neither is stuck, and neither is - waiting on a decision. -- **AC-62** THE SYSTEM SHALL compute `reevaluation_blocked` live at request time - as the conjunction of: the task has at least one non-superseded decision at - its current step, AND the AC-16 session query returns no row. It SHALL NOT be - read from a stored flag. - The first conjunct SHALL be evaluated as "`ListStepDecisions` returns a - non-empty list for (task, current step)". That is equivalent, and it is the - form the engine can actually express: a row is superseded only when a - replacement is inserted for the same decider, so a step holding at least one - row always holds at least one non-superseded row. Stating the equivalence - matters because the engine's `DecisionInfo` projection carries no - `superseded_at` — the same limitation AC-43b turns into last-row-wins — so a - literal reading of "non-superseded" would be a predicate the evaluator cannot - compute. - This is a CURRENT-STATE predicate, not a history of AC-16a skips, and the - difference is deliberate. A stored "the last recording skipped" flag would keep - rendering a card as stuck after a session returned and the card became healthy, - and reading a stored evaluation is the very thing AC-54 forbids for the - per-guard entries. The live predicate also needs no ordering rule, no tiebreak - and no new column, so the "most recent decision" ambiguity does not arise; two - concurrent reads compute the same value from committed state. - Naming: the field is `reevaluation_blocked`, NOT `last_reevaluation_skipped`, - because it asserts something different from the original and a reader must not - carry the historical reading across the rename. - -### Ordering, idempotency, concurrency - -- **AC-26** THE SYSTEM SHALL order decisions for threshold evaluation by - `decided_at` ascending, tie-broken by `id` ascending — the existing - `ListStepDecisions` contract (`phase2_sqlite.go:594`) — and SHALL count only - the last row per **AC-50 seat** under that ordering, decisions being matched to - seats by AC-51. This clause governs the approve-style thresholds - (`all_approve`, `all_decide`, `majority_approve`, `n_approve:`) ONLY; - `any_reject` is keyed on decider identity and needs no seat, per AC-43 and - AC-43b. The scoping is explicit because an unqualified "last row per - `participant_id`" is precisely the reading that reintroduces the - discarded-human-rejection defect AC-43 exists to fix — production funnels every - threshold through one `latestDecisionsPerParticipant` call today, so reusing - this rule for `any_reject` is the path of least resistance. -- **AC-27** WHEN the same participant records a second decision for the same - `(task_id, step_id)`, THE SYSTEM SHALL mark the prior row `superseded_at` and - insert the new one in a single transaction, and the new verdict SHALL replace - the old one for threshold purposes. -- **AC-28** WHEN two decisions are recorded concurrently for the same - `(task_id, step_id)` by different participants, THE SYSTEM SHALL persist both, - and at most one guarded transition SHALL be applied for the resulting step - change. The engine's operation-id mechanism CANNOT provide this: - `HandleTrigger` dedupes by exact `OperationID` match only - (`isOperationAlreadyApplied`) and offers no mutual exclusion across distinct - ids, and two concurrent decisions necessarily observe different decision sets - and so compute different ids. Mutual exclusion SHALL therefore be enforced at - apply time per AC-46, not by the operation id. -- **AC-46** WHEN a transition is applied after a quorum re-evaluation, THE - SYSTEM SHALL apply it only if the task's `workflow_step_id` still equals the - step the guard was evaluated against, and SHALL abandon the apply otherwise. - AC-48 defines the port this needs and why the existing one cannot express it. - The loser of a concurrent race SHALL report `transition_applied = false` with - its decision still persisted, and SHALL NOT be reported as an error: losing - the race is a normal outcome, not a failure. -- **AC-29** WHEN two decisions are recorded concurrently by the *same* - participant for the same `(task_id, step_id)`, THE SYSTEM SHALL leave exactly - one non-superseded row for that participant. -- **AC-30** WHEN a re-evaluation is triggered twice for the same recorded - decision, THE SYSTEM SHALL apply the transition at most once, keyed on a - **deterministic** operation id of the form - `decision:::`. Determinism is the whole - point: `Engine.RecordParticipantDecision` currently builds its operation id - with a `time.Now().UnixNano()` suffix, which yields a fresh id on every call - and therefore deduplicates nothing. That existing construction SHALL NOT be - reused as-is. The repo's convention for this primitive is a deterministic - content-derived key — see `childCompletionOperationID` - (`orchestrator/event_handlers_children_completed.go`). -- **AC-45** THE SYSTEM SHALL NOT accept a client-supplied idempotency key on the - decision tool. A repeated call is a new verdict and supersedes the previous one - per AC-27. Consequence, stated so a builder does not quietly invent a key: an - agent that retries after a timeout on a call that actually succeeded records a - second verdict rather than a no-op. This is acceptable because the verdict is - the same value and AC-27 leaves exactly one non-superseded row; it is NOT - acceptable to paper over it with a synthesized key derived from the arguments, - which would make a deliberate change-of-mind silently fail. -- **AC-66** THE SYSTEM SHALL generate the decision row id BEFORE the write and - pass it in `DecisionInfo.ID`, following the existing office precedent - (`recordTaskDecision` sets `ID: uuid.New().String()` on the row it hands to - `RecordStepDecision`). It SHALL NOT follow `Engine.RecordParticipantDecision`, - which leaves `ID` blank and lets the repository generate one. The - `DecisionStore.RecordStepDecision(ctx, d DecisionInfo) error` signature is - UNCHANGED by this feature. - The two in-tree precedents disagree and only one is usable: the port returns - no id, so a blank-`ID` write leaves AC-30's operation id - `decision:::` unconstructible, and AC-57b-i - unsatisfiable since office would have no id to publish. Recovering the id by - reading the row back after the write is NOT permitted — under the AC-28 - concurrency this must coexist with, a read-back can select a different - decider's row for the same `(task_id, step_id)`. Pre-generation also makes the - operation id available before the write, which is what lets AC-47 mark it - applied strictly after the apply. -- **AC-31** WHEN a task re-enters a guarded step, THE SYSTEM SHALL clear that - step's decisions before the round's participants are queued, so a new round - starts with zero decisions — the existing `clear_decisions` ordering on - `on_enter`. AC-56 makes this observable rather than assumed, and names what it - means when it does not hold. - -- **AC-48** THE SYSTEM SHALL add exactly one method to the engine's transition - port — `ApplyTransitionIfAtStep(ctx, taskID, sessionID, expectedStepID, - toStepID, trigger) (applied bool, err error)` — used ONLY by the AC-46 quorum - apply, and SHALL leave the existing `ApplyTransition(...) error` and every - transition that uses it semantically unchanged. `HandleResult` SHALL gain an - additive `TransitionAbandoned` field so an abandoned apply is distinguishable - from both a transition and an error. This is required because AC-46's outcome - is not expressible today: `ApplyTransition` receives `fromStepID` and ignores - it, its orchestrator implementation unconditionally sets - `task.WorkflowStepID = toStepID`, and a single `error` return cannot separate - "lost the race" from "the write failed". The compare-and-swap SHALL read the - task's `workflow_step_id` inside the same transaction that performs the update - — `updateTaskWithWorkflowStepAdmission` already opens one and already takes the - workspace and workflow-step locks — so no new locking primitive is introduced - and no non-quorum transition changes behavior. `applied = true` means the task - left `expectedStepID`; it does NOT mean the task was WIP-admitted at the - target, because a step at capacity queues the task there and that is still a - completed transition. - -### Step binding and threshold boundaries - -- **AC-36** THE SYSTEM SHALL bind a decision to the task's `workflow_step_id` - read during validation, and SHALL write the row inside the same transaction - that supersedes the participant's prior verdict for that step. -- **AC-37** WHEN the task's step changes between validation and write, THE - SYSTEM SHALL persist the decision against the **validated** step — not reject - it, and not re-target it at the new step. The recorded row SHALL NOT be - counted at the new step, the call SHALL report success, `transition_applied` - SHALL be `false`, and the returned `step_id` SHALL be the validated step so - the caller can see its verdict landed on a step the task has left. Persisting - is chosen over rejecting because the reviewer's verdict is real work and - discarding it would silently lose a review; the returned `step_id` is what - makes the outcome detectable. -- **AC-38** THE SYSTEM SHALL write the caller's reason to - `workflow_step_decisions.comment`. -- **AC-39** THE SYSTEM SHALL satisfy `majority_approve` only when approvals - strictly exceed half the required slate, so a slate of 2 requires 2 approvals - and a slate of 3 requires 2 — preserving `approveCount*2 > totalRequired` - (`workflow/engine/quorum.go:113`). -- **AC-40** WHEN `n_approve:` names an N greater than the required slate size, - THE SYSTEM SHALL never fire that transition and SHALL report AC-23 reason - `threshold_unsatisfiable` rather than an error. It is deliberately NOT - `threshold_not_met`: no future decision can satisfy it, so it is a stuck card, - and AC-25 requires stuck to look different from waiting. -- **AC-41** WHEN `n_approve:` names a non-numeric or non-positive N, THE - SYSTEM SHALL not fire the transition and SHALL report AC-23 reason - `threshold_unrecognized`. The threshold family is recognized here and only its - parameter is malformed, so this is not a guard-variant failure; - `guard_variant_unrecognized` is reserved for the case AC-53 names. -- **AC-42** WHEN a decision exists for a role other than the guard's role at the - same step, THE SYSTEM SHALL ignore it for that guard — EXCEPT for the - `decider_type = user` veto of AC-58, which is deliberately role-agnostic. - AC-42 governs seated, role-scoped quorum contributions; the human veto is not - one. - -### Regression - -- **AC-32** GIVEN a task at Review with exactly one reviewer participant - (`decision_required = 1`) and one recorded `approved` decision from that - reviewer, WHEN quorum is evaluated, THE SYSTEM SHALL move the task to - Approval. -- **AC-33** GIVEN the same task with one recorded rejection instead, WHEN quorum - is evaluated, THE SYSTEM SHALL move the task to Work. -- **AC-34** GIVEN two reviewer participants and one approval, WHEN quorum is - evaluated under `all_approve`, THE SYSTEM SHALL leave the task at Review and - report it as awaiting one further decision. -- **AC-35** GIVEN a workflow whose steps carry no guard, THE SYSTEM SHALL - transition exactly as it does today. -- **AC-63** GIVEN a task at Review, whose guards name `role: reviewer`, and a - human recording **Request changes** through `POST /tasks/:id/request-changes`, - WHEN quorum is evaluated, THE SYSTEM SHALL move the task to Work — even though - that decision is stored with `role = approver`. This is the regression that - makes AC-10 and AC-58 observable end to end, and it is the exact path that is - broken in production today. - -- **AC-56** GIVEN a task moved off a guarded step by `any_reject` under AC-33 - and later returned to that step, WHEN quorum is next evaluated there, THE - SYSTEM SHALL evaluate against zero decisions from the prior round. This AC - exists to make AC-31's premise observable rather than assumed: `clear_decisions` - on `on_enter` is depended upon but verified by no other AC, and the only - `on_enter` trigger dispatch in the tree is inside `SwitchWorkflowCallback` — - the orchestrator's callback registry wires a `DispatchTriggerFn` for - `switch_workflow` only. If this AC fails, this card is blocked on the sibling - `on_enter` action-dispatch feature and SHALL be reported as blocked; it SHALL - NOT be closed by adding a second decision-clearing path inside this feature. - Without it, a rejection persists at the guarded step and re-fires `any_reject` - on the next completed turn, so AC-32 and AC-33 cannot both hold across a round - trip. - -## Failure modes - -| Condition | Behavior | -|---|---| -| Agent not a participant | Permission error; no row written (AC-3) | -| Verdict outside `approved`/`rejected` on the tool | Validation error; no row (AC-5) | -| Empty reason | Validation error; no row (AC-6) | -| Task has no bound step | Error naming the unbound step; no row (AC-7) | -| Decision store not wired | Guard does not fire; reason recorded (AC-23) | -| Required slate empty, approve-style threshold | Guard does not fire; reason `slate_empty` (AC-21, AC-23) | -| Required slate empty, `any_reject` | Guard still evaluated, not short-circuited; a seatless rejection vetoes (AC-59, AC-43) | -| Unknown guard variant | Guard does not fire; reason recorded (AC-23) | -| Re-evaluation errors after a successful write | Decision kept; success reported; failure surfaced (AC-15) | -| No resolvable session | Decision kept; re-evaluation skipped, no error (AC-16) | -| Duplicate decision from one participant | Prior superseded transactionally (AC-27) | -| Human rejection, no participant row | Counted by `any_reject` as a veto (AC-43) | -| Non-slate approval | Not counted toward any approve threshold (AC-43a) | -| Participant has rows at two steps | Canonical row chosen by AC-44; one seat (AC-19) | -| Task step changed between validate and write | Persisted against validated step; `transition_applied=false`; returned `step_id` is the validated step (AC-37) | -| Concurrent transition race lost | `transition_applied=false`, decision kept, not an error (AC-46) | -| Participant store not wired | Guard does not fire; reason `participant_store_unwired` (AC-23) | -| `ListStepDecisions` / `ListStepParticipants` errors | Guard does not fire; reason `evaluation_error` with `error` field (AC-23, AC-24) | -| No active session for re-evaluation | Decision kept, re-eval skipped, reason `session_unresolvable` (AC-16a) | -| Unrecognized threshold string | Guard does not fire; reason `threshold_unrecognized`; rendered as stuck (AC-53) | -| Empty slate AND `n_approve:` unsatisfiable | Single reason `slate_empty` wins by AC-52 precedence | -| Seat id changed after a verdict was recorded | Verdict still counted, matched by decider identity (AC-51) | -| Re-evaluation apply fails | Operation id left unmarked so a retry can still land (AC-47) | -| Quorum apply loses the step compare-and-swap | `applied = false`, not an error; `TransitionAbandoned` set (AC-48, AC-46) | -| Target step at WIP capacity | Transition still applied; task queued at target (AC-48) | -| Decision on file and no resolvable session | AC-24b reports `reevaluation_blocked`; card renders stuck (AC-62, AC-61) | -| Human rejects at a reviewer-guarded step | Veto counts; role filter waived for `decider_type = user` (AC-58, AC-10) | -| Guard's threshold already met at request time | Entry reports `satisfied = true` and omits `reason` (AC-60) | -| One guard stuck, a sibling merely awaiting | Card renders stuck; stuck outranks awaiting (AC-61) | -| Diagnostic `GET /quorum` read | No AC-24 log record, no AC-24a counter increment (AC-24, AC-24a) | -| Engine decision entry point not wired | Both transports error; no row written; no office-side fallback (AC-57c) | -| Task at a step with no guarded transition | AC-24b returns empty list; card aggregates to `clear` (AC-61, AC-24c) | -| Task returns to a guarded step with prior decisions live | AC-56 fails; card is blocked on the `on_enter` card, not patched here | -| AC-24b read while the engine dispatcher is not wired | Endpoint errors; no office-side fallback evaluation (AC-57d, AC-57c) | -| One guard's store read errors, siblings healthy | That entry alone reports `evaluation_error`; the others are still returned (AC-57d, AC-23) | -| Decision recorded at a step that also configures a side-effect action on `on_turn_complete` | Only guarded transitions are evaluated; no callback re-fires (AC-65) | -| Tool called at a step with no guarded transition | `guards` is an empty list, not an error (AC-64) | -| Decision row id needed for the AC-30 operation id | Pre-generated caller-side into `DecisionInfo.ID`; never read back after the write (AC-66) | -| Office needs `decision_id` / `created_at` to publish | Both returned by the AC-57a entry point (AC-57b-i) | - -## Persistence guarantees - -- A decision write and its supersede of the prior verdict are one transaction; a - reader never observes zero non-superseded rows for a participant that has - decided (existing `RecordStepDecision` behavior). -- A recorded decision survives a re-evaluation failure (AC-15). -- Decisions cleared by `clear_decisions` on step re-entry are hard-deleted; - decisions superseded by a new verdict are retained with `superseded_at` set — - the existing split at `phase2_sqlite.go:686-697`. -- Threshold evaluation counts, per AC-50 seat, the LAST - row under the AC-26 ordering — it does not filter on `superseded_at`. - `ListStepDecisions` deliberately returns superseded rows so timelines can - render full history, and the engine's `DecisionInfo` projection carries no - `SupersededAt` field, so the engine could not filter on it even if it wanted - to. Last-row-wins and non-superseded coincide whenever the supersede predicate - and the `participant_id` key agree; AC-44 is what makes them agree, and - without it the two diverge exactly in the multi-row case of AC-19. - -## Out of scope - -Each of the following is a deliberate exclusion, not an oversight. - -- **Workspace `approvals` subsystem.** `POST /approvals/:id/decide` and its - `"approved" | "rejected"` shape (`apps/web/lib/api/domains/office-api.ts:534`) - are a separate feature from task step decisions. Untouched. -- **New thresholds.** `all_approve`, `all_decide`, `any_reject`, - `majority_approve`, `n_approve:` are the complete set. No new threshold. -- **Guard variants beyond `wait_for_quorum`.** `TransitionGuard` stays - single-variant. -- **`on_enter` action dispatch.** Tracked by its own card, and this feature does - not change it. It is a **hard dependency**, not merely an adjacent card: - AC-31's decision-clearing is what stops a rejection recorded at a guarded step - from re-firing `any_reject` every time the task returns there, and the only - `on_enter` trigger dispatch in the tree today is inside - `SwitchWorkflowCallback`. AC-56 turns that dependency into an observable - regression so it fails loudly instead of shipping as an unbounded - reject/return loop. This feature SHALL NOT grow a second decision-clearing - path to compensate. -- **Decision revocation.** A participant may supersede a verdict by recording a - new one; there is no withdraw-to-undecided operation. -- **Timeouts and escalation.** A quorum that is never reached waits - indefinitely. No auto-advance, no reminder, no deadline. -- **Delegating or reassigning a pending decision.** Not addressed. -- **Kanban-workflow decisions.** The decision tool is Office-surface only - (AC-8); Kanban keeps `step_complete_kandev`. -- **Backfilling the zero existing decision rows.** There are none to migrate. -- **Client-supplied idempotency keys on the decision tool.** Excluded by AC-45. - Transition application is idempotent (AC-30); recording is not. -- **Giving the singleton human user a participant row.** AC-43 makes a human - rejection count without one. Seating the user in the slate would change who - `all_approve` waits for on every existing task, which is a larger contract - change than this card carries. -- **The `allApproversApproved` / `pendingApprovers` close gate.** These drive the - `task_ready_to_close` reactivity run and the `ApprovalsPendingError` 409 on - `UpdateTaskStatus`, and they read through the same step-scoped - `office/repository/sqlite` query, so they carry Defect 4's blindness: an - approver attached at an earlier step is invisible to them. Excluded - deliberately, and the consequence is stated rather than hidden — after this - card, quorum can advance a task while that separate gate still blocks its - close on an approver it cannot see. Folding it in would extend AC-50's slate - to a second consumer with different semantics (per-role current-approval, not - threshold arithmetic) on a card that AC-57 has already grown. It needs its own - card. - The residual is sharper than "quorum can advance a task while that gate still - blocks", and is recorded in full so the follow-up card is scoped from fact: a - task whose AC-24b entries all report `satisfied` renders as `clear` under - AC-61 — the healthy rendering — and can still take a 409 - `ApprovalsPendingError` on close, naming an approver that nothing this card - touches can help the operator locate, because that gate reads through the same - step-scoped `ListAllTaskParticipants` query Defect 4 diagnoses. This card - therefore converts one invisible stuck class into one visible-but-confusing - class, which is an improvement but not a completion. The follow-up should be - tracked, not deferred indefinitely. -- **Repointing `resolveDeciderRole` for non-decision callers.** AC-4a fixes role - resolution on the decision path only. Other readers of - `ListAllTaskParticipants` keep today's step-scoped behavior. -- **A generic transition-lock primitive.** AC-48 adds exactly one additive port - method, used only by the AC-46 quorum apply; `ApplyTransition` and every - transition that uses it keep today's semantics, and the compare-and-swap reuses - the transaction and the workspace/step locks - `updateTaskWithWorkflowStepAdmission` already takes. No engine-wide locking - mechanism, and no behavior change to any non-quorum transition. -- **Repointing already-attached participant rows.** AC-18 changes how the slate - is *read*; whether existing rows are also rewritten is an implementation - choice the plan may make either way, provided AC-18–AC-22 hold. - -## E2E surfaces - -User-visible surfaces this touches: - -- Office task detail approval action bar (`approval-action-bar.tsx`) — the - Request-changes path changes behavior under AC-10. -- Office task board — a card at Review now advances on quorum (AC-32, AC-33). -- `GET /api/v1/office/tasks/:id/quorum` — new read endpoint (AC-24b, AC-24c), - projecting the AC-57d engine snapshot. -- Office task detail awaiting-decisions rendering (AC-25), including the - stuck-distinct rendering of `reevaluation_blocked` (AC-62) and the AC-61 - aggregation when two guards disagree. - -E2E recommendation: one Playwright spec driving Work → Review → Approval with a -reviewer and an approver attached, asserting advance-on-approve, return-on-reject, -and the awaiting-decisions presentation of AC-25. The same spec covers AC-56 at -no extra cost: after the return-on-reject leg, drive the task back to the guarded -step and assert it does not immediately bounce again. Backend quorum arithmetic, -vocabulary, and concurrency are Go-test territory, not E2E.