Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22c035984e | |||
| d35037df01 |
@@ -0,0 +1,104 @@
|
||||
"""add config profiles, includes, mounts, and tool instance profile selection
|
||||
|
||||
Revision ID: 0013_add_config_profiles
|
||||
Revises: 0012_default_port_req
|
||||
Create Date: 2026-05-24 12:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "0013_add_config_profiles"
|
||||
down_revision: Union[str, None] = "0012_default_port_req"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create config_profiles table
|
||||
op.create_table(
|
||||
"config_profiles",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("name", sa.String(length=255), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("user_id", "name", name="uq_config_profiles_user_name"),
|
||||
)
|
||||
op.create_index("idx_config_profiles_user", "config_profiles", ["user_id"])
|
||||
|
||||
# Create config_includes table
|
||||
op.create_table(
|
||||
"config_includes",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("profile_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("included_profile_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("order_index", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False),
|
||||
sa.ForeignKeyConstraint(["profile_id"], ["config_profiles.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["included_profile_id"], ["config_profiles.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("profile_id", "included_profile_id", name="uq_config_includes_pair"),
|
||||
)
|
||||
op.create_index("idx_config_includes_profile", "config_includes", ["profile_id"])
|
||||
op.create_index("idx_config_includes_included", "config_includes", ["included_profile_id"])
|
||||
|
||||
# Create config_mounts table
|
||||
op.create_table(
|
||||
"config_mounts",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("profile_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("mount_path", sa.String(length=1024), nullable=False),
|
||||
sa.Column("content", sa.Text(), nullable=True),
|
||||
sa.Column("source_profile_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column("order_index", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False),
|
||||
sa.ForeignKeyConstraint(["profile_id"], ["config_profiles.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["source_profile_id"], ["config_profiles.id"], ondelete="SET NULL"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("idx_config_mounts_profile", "config_mounts", ["profile_id"])
|
||||
|
||||
# Add selected_profile_id to tool_instances
|
||||
op.add_column(
|
||||
"tool_instances",
|
||||
sa.Column("selected_profile_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"fk_tool_instances_selected_profile",
|
||||
"tool_instances",
|
||||
"config_profiles",
|
||||
["selected_profile_id"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
op.create_index("idx_tool_instances_selected_profile", "tool_instances", ["selected_profile_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Remove selected_profile_id from tool_instances
|
||||
op.drop_index("idx_tool_instances_selected_profile", table_name="tool_instances")
|
||||
op.drop_constraint("fk_tool_instances_selected_profile", "tool_instances", type_="foreignkey")
|
||||
op.drop_column("tool_instances", "selected_profile_id")
|
||||
|
||||
# Drop config_mounts
|
||||
op.drop_index("idx_config_mounts_profile", table_name="config_mounts")
|
||||
op.drop_table("config_mounts")
|
||||
|
||||
# Drop config_includes
|
||||
op.drop_index("idx_config_includes_included", table_name="config_includes")
|
||||
op.drop_index("idx_config_includes_profile", table_name="config_includes")
|
||||
op.drop_table("config_includes")
|
||||
|
||||
# Drop config_profiles
|
||||
op.drop_index("idx_config_profiles_user", table_name="config_profiles")
|
||||
op.drop_table("config_profiles")
|
||||
@@ -1,5 +1,8 @@
|
||||
from src.models.base import Base
|
||||
from src.models.config_folder import ConfigFolder
|
||||
from src.models.config_include import ConfigInclude
|
||||
from src.models.config_mount import ConfigMount
|
||||
from src.models.config_profile import ConfigProfile
|
||||
from src.models.git_repository import GitRepository
|
||||
from src.models.project import Project
|
||||
from src.models.ssh_key import SSHKey
|
||||
@@ -8,4 +11,17 @@ from src.models.tool_type import ToolType
|
||||
from src.models.user import User
|
||||
from src.models.user_config import UserConfig
|
||||
|
||||
__all__ = ["Base", "ConfigFolder", "GitRepository", "Project", "SSHKey", "ToolInstance", "ToolType", "User", "UserConfig"]
|
||||
__all__ = [
|
||||
"Base",
|
||||
"ConfigFolder",
|
||||
"ConfigInclude",
|
||||
"ConfigMount",
|
||||
"ConfigProfile",
|
||||
"GitRepository",
|
||||
"Project",
|
||||
"SSHKey",
|
||||
"ToolInstance",
|
||||
"ToolType",
|
||||
"User",
|
||||
"UserConfig",
|
||||
]
|
||||
|
||||
@@ -26,6 +26,8 @@ class ConfigFolder(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
||||
project_overrides: Mapped[dict | None] = mapped_column(
|
||||
JSON, default=dict, nullable=True
|
||||
) # {"project_id": {"mount_path": "...", "files": {...}}}
|
||||
# DEPRECATED: Legacy auto-mounting flag. No longer used for launch-time
|
||||
# auto-mounting. Use ConfigProfile and ToolInstance.selected_profile_id instead.
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
|
||||
user: Mapped["User"] = relationship()
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, UniqueConstraint
|
||||
from sqlalchemy import Uuid as UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.models.config_profile import ConfigProfile
|
||||
|
||||
|
||||
class ConfigInclude(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
||||
__tablename__ = "config_includes"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("profile_id", "included_profile_id", name="uq_config_includes_pair"),
|
||||
)
|
||||
|
||||
profile_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(), ForeignKey("config_profiles.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
included_profile_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(), ForeignKey("config_profiles.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
order_index: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
|
||||
profile: Mapped["ConfigProfile"] = relationship(
|
||||
"ConfigProfile",
|
||||
foreign_keys=[profile_id],
|
||||
back_populates="includes",
|
||||
)
|
||||
included_profile: Mapped["ConfigProfile"] = relationship(
|
||||
"ConfigProfile",
|
||||
foreign_keys=[included_profile_id],
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, String, Text
|
||||
from sqlalchemy import Uuid as UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.models.config_profile import ConfigProfile
|
||||
|
||||
|
||||
class ConfigMount(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
||||
__tablename__ = "config_mounts"
|
||||
|
||||
profile_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(), ForeignKey("config_profiles.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
mount_path: Mapped[str] = mapped_column(String(1024), nullable=False)
|
||||
content: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
source_profile_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(), ForeignKey("config_profiles.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
order_index: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
|
||||
profile: Mapped["ConfigProfile"] = relationship(
|
||||
"ConfigProfile",
|
||||
foreign_keys=[profile_id],
|
||||
back_populates="mounts",
|
||||
)
|
||||
source_profile: Mapped["ConfigProfile | None"] = relationship(
|
||||
"ConfigProfile",
|
||||
foreign_keys=[source_profile_id],
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, String, Text, UniqueConstraint
|
||||
from sqlalchemy import Uuid as UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.models.user import User
|
||||
|
||||
|
||||
class ConfigProfile(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
||||
__tablename__ = "config_profiles"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("user_id", "name", name="uq_config_profiles_user_name"),
|
||||
)
|
||||
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
user: Mapped["User"] = relationship()
|
||||
includes: Mapped[list["ConfigInclude"]] = relationship(
|
||||
"ConfigInclude",
|
||||
foreign_keys="ConfigInclude.profile_id",
|
||||
back_populates="profile",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="ConfigInclude.order_index",
|
||||
)
|
||||
mounts: Mapped[list["ConfigMount"]] = relationship(
|
||||
"ConfigMount",
|
||||
back_populates="profile",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="ConfigMount.order_index",
|
||||
)
|
||||
@@ -9,6 +9,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from src.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.models.config_profile import ConfigProfile
|
||||
from src.models.git_repository import GitRepository
|
||||
from src.models.project import Project
|
||||
from src.models.tool_type import ToolType
|
||||
@@ -62,8 +63,12 @@ class ToolInstance(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
||||
last_stopped_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
selected_profile_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(), ForeignKey("config_profiles.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
tool_type: Mapped["ToolType"] = relationship()
|
||||
repository: Mapped["GitRepository"] = relationship()
|
||||
project: Mapped["Project"] = relationship()
|
||||
owner: Mapped["User"] = relationship()
|
||||
selected_profile: Mapped["ConfigProfile | None"] = relationship()
|
||||
|
||||
@@ -18,3 +18,23 @@ class UserConfig(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
||||
config: Mapped[dict[str, object]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
|
||||
user: Mapped["User"] = relationship(back_populates="user_config")
|
||||
|
||||
@property
|
||||
def default_profile_id(self) -> uuid.UUID | None:
|
||||
profile_id = self.config.get("default_profile_id")
|
||||
return uuid.UUID(profile_id) if profile_id else None
|
||||
|
||||
@default_profile_id.setter
|
||||
def default_profile_id(self, value: uuid.UUID | None) -> None:
|
||||
if value is not None:
|
||||
self.config["default_profile_id"] = str(value)
|
||||
elif "default_profile_id" in self.config:
|
||||
del self.config["default_profile_id"]
|
||||
|
||||
@property
|
||||
def default_profiles(self) -> dict[str, str]:
|
||||
return self.config.get("default_profiles", {})
|
||||
|
||||
@default_profiles.setter
|
||||
def default_profiles(self, value: dict[str, str]) -> None:
|
||||
self.config["default_profiles"] = value
|
||||
|
||||
@@ -39,3 +39,18 @@ def test_refresh_tokens_migration_has_expected_revision_chain() -> None:
|
||||
|
||||
assert module.revision == "0002_refresh_tokens"
|
||||
assert module.down_revision == "0001_initial_schema"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_config_profiles_migration_has_expected_revision_chain() -> None:
|
||||
migration_path = Path(__file__).resolve().parents[2] / "alembic" / "versions" / "0013_add_config_profiles.py"
|
||||
spec = spec_from_file_location("add_config_profiles", migration_path)
|
||||
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
|
||||
module = module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
assert module.revision == "0013_add_config_profiles"
|
||||
assert module.down_revision == "0012_default_port_req"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-24
|
||||
@@ -0,0 +1,45 @@
|
||||
## Context
|
||||
|
||||
The current system uses `config_folders` with a flat `files` JSONB and an `is_active` flag for auto-mounting at tool launch time. This design is inflexible: only one folder can be active, there's no ordering of includes, no explicit per-tool-instance selection, and mount definitions are mixed with file contents in a single blob.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Provide structured config profiles with named collections of mounts and includes
|
||||
- Support ordered include lists so profiles can reference other profiles in sequence
|
||||
- Allow per-tool-instance profile selection with fallback to user/tool-type defaults
|
||||
- Remove implicit auto-mounting behavior at launch time
|
||||
- Maintain backward compatibility for existing `config_folders` data during migration
|
||||
|
||||
**Non-Goals:**
|
||||
- Frontend UI for profile management (separate change)
|
||||
- Real-time profile switching on running instances
|
||||
- Profile versioning or history
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. New `config_profiles` table replaces the semantic role of `config_folders`
|
||||
- Rationale: A profile is a higher-level concept than a folder; it includes mounts, includes, and metadata
|
||||
- `config_folders` remains for data migration but is no longer used for auto-mounting
|
||||
|
||||
### 2. `config_includes` provides ordered many-to-many self-reference on `config_profiles`
|
||||
- Rationale: Profiles need to include other profiles (e.g., a "base" profile included by "project-specific")
|
||||
- `order_index` column controls application order
|
||||
|
||||
### 3. `config_mounts` stores individual mount/file entries
|
||||
- Rationale: Normalizing mounts allows querying, ordering, and validation per mount
|
||||
- Each mount has a `mount_path`, optional `content` text, and optional `source_profile_id` for transitive includes
|
||||
|
||||
### 4. Default profile stored on `user_configs.config` JSONB
|
||||
- Rationale: Avoids schema changes to `users`; the existing `user_configs` table already stores per-user JSON
|
||||
- Key: `default_profile_id` (global default) and `default_profiles` map for per-tool-type defaults
|
||||
|
||||
### 5. `tool_instances.selected_profile_id` for explicit selection
|
||||
- Rationale: Clear, direct foreign key; nullable to allow fallback to defaults
|
||||
- Null means "use default resolution"
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Risk] Existing `config_folders` data becomes orphaned if not migrated → Mitigation: keep table, stop auto-mount behavior only
|
||||
- [Risk] Profile include cycles could cause infinite loops → Mitigation: validate at write time, detect cycles in include graph
|
||||
- [Risk] Multiple includes with overlapping mount paths → Mitigation: last-include-wins based on order_index
|
||||
@@ -0,0 +1,30 @@
|
||||
## Why
|
||||
|
||||
The current `config_folders` table provides basic file mounting but lacks structured profile management, ordering, and per-tool-instance selection. We need a proper config profile system that supports ordered includes, mount/file definitions, default selection, and explicit profile assignment per tool instance.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `ConfigProfile` model to replace the legacy `config_folders` concept with structured profiles
|
||||
- Add `ConfigInclude` model for ordered include lists within profiles
|
||||
- Add `ConfigMount` model for mount/file definitions (replacing the flat `files` JSONB on `config_folders`)
|
||||
- Add default profile selection per user and tool type
|
||||
- Add `selected_profile_id` to `ToolInstance` for per-instance profile selection
|
||||
- Remove launch-time reliance on legacy active config folder auto-mounting (mark `config_folders.is_active` as deprecated, stop auto-mounting at launch)
|
||||
- Create database migrations for all new tables
|
||||
- **BREAKING**: Legacy `config_folders` auto-mounting behavior will be removed; tool instances must explicitly select a profile
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `config-profile-management`: CRUD operations for config profiles, includes, and mounts
|
||||
- `tool-instance-profile-selection`: Assign and switch config profiles per tool instance
|
||||
|
||||
### Modified Capabilities
|
||||
- `tool-instance-launch`: Change launch behavior to use explicit profile selection instead of auto-mounting active config folder
|
||||
|
||||
## Impact
|
||||
|
||||
- New database tables: `config_profiles`, `config_includes`, `config_mounts`
|
||||
- Modified tables: `tool_instances` (add `selected_profile_id`), `users` or `user_configs` (add default profile selection)
|
||||
- API endpoints for profile management and instance profile assignment
|
||||
- Tool launch logic changes (remove auto-mount, use explicit profile)
|
||||
@@ -0,0 +1,29 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: User can create config profiles
|
||||
The system SHALL allow users to create named config profiles containing mounts and includes.
|
||||
|
||||
#### Scenario: Successful profile creation
|
||||
- **WHEN** user creates a profile with name, description, and mount list
|
||||
- **THEN** the profile is stored with a unique ID and associated mounts
|
||||
|
||||
### Requirement: Profile includes are ordered
|
||||
The system SHALL support ordered includes where profiles can reference other profiles with a defined application sequence.
|
||||
|
||||
#### Scenario: Include with order
|
||||
- **WHEN** user adds an include to a profile with order_index 1
|
||||
- **THEN** the included profile's mounts are applied after order_index 0 includes
|
||||
|
||||
### Requirement: Config mounts define files and paths
|
||||
The system SHALL store individual mount entries with mount_path, optional content, and optional source profile reference.
|
||||
|
||||
#### Scenario: Add mount to profile
|
||||
- **WHEN** user adds a mount with mount_path "/app/config.json" and content "{}"
|
||||
- **THEN** the mount is stored and linked to the profile
|
||||
|
||||
### Requirement: Cycle detection in includes
|
||||
The system SHALL prevent creation of include cycles.
|
||||
|
||||
#### Scenario: Attempt cyclic include
|
||||
- **WHEN** user tries to include profile B in profile A where A is already included in B
|
||||
- **THEN** the system rejects the request with an error
|
||||
@@ -0,0 +1,22 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Tool instance can have selected profile
|
||||
The system SHALL allow setting an explicit config profile on a tool instance.
|
||||
|
||||
#### Scenario: Assign profile to instance
|
||||
- **WHEN** user sets selected_profile_id on a tool instance
|
||||
- **THEN** the instance stores the profile ID and uses it at launch time
|
||||
|
||||
### Requirement: Tool instance uses default profile when none selected
|
||||
The system SHALL resolve a default profile for a tool instance when no explicit profile is selected.
|
||||
|
||||
#### Scenario: Fallback to user default
|
||||
- **WHEN** a tool instance has no selected_profile_id
|
||||
- **THEN** the system uses the user's default profile for that tool type, or the global default
|
||||
|
||||
### Requirement: Remove legacy auto-mount behavior
|
||||
The system SHALL no longer auto-mount the active config folder at tool launch time.
|
||||
|
||||
#### Scenario: Launch without active folder
|
||||
- **WHEN** a tool instance launches with no selected profile and no default
|
||||
- **THEN** the instance starts without mounting any config folder
|
||||
@@ -0,0 +1,15 @@
|
||||
## 1. Data Models and Migrations
|
||||
|
||||
- [x] 1.1 Create ConfigProfile model with user ownership, name, description
|
||||
- [x] 1.2 Create ConfigInclude model for ordered profile self-references
|
||||
- [x] 1.3 Create ConfigMount model for mount/file definitions
|
||||
- [x] 1.4 Add selected_profile_id to ToolInstance model
|
||||
- [x] 1.5 Add default profile fields to UserConfig model
|
||||
- [x] 1.6 Create Alembic migration for new tables and columns
|
||||
- [x] 1.7 Register new models in models/__init__.py
|
||||
- [x] 1.8 Add migration metadata and test
|
||||
|
||||
## 2. Legacy Deprecation
|
||||
|
||||
- [x] 2.1 Mark config_folders.is_active as deprecated in model
|
||||
- [ ] 2.2 Remove auto-mounting logic from tool launch (separate change)
|
||||
@@ -0,0 +1,213 @@
|
||||
# OpenSpec Status and Implementation Checklist Review
|
||||
|
||||
**Review Date:** 2026-05-24
|
||||
**Reviewer:** Worker el-2i1s
|
||||
**Task:** 6.3 Final OpenSpec status and checklist review
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This review covers all active OpenSpec changes in the `openspec/changes/` directory. Out of **11 active changes** with **345 total tasks**, **66 tasks (19.1%) are complete** and **279 tasks remain**.
|
||||
|
||||
### Key Findings
|
||||
|
||||
- **2 changes are near completion** (git-repo-working-clones at 87.5%, opencode-web-terminal at 72.7%)
|
||||
- **2 changes have partial progress** (session-management-fixes at 32%, tool-workshop at 24.6%)
|
||||
- **7 changes have not started** (0% complete)
|
||||
- **1 new change was recently created** (add-config-profiles) with initial model work already implemented
|
||||
|
||||
---
|
||||
|
||||
## Active Changes Status
|
||||
|
||||
### Near Completion (>50%)
|
||||
|
||||
#### 1. git-repo-working-clones (87.5% complete)
|
||||
- **Completed:** 7/8 tasks
|
||||
- **Remaining:** Task 4.1 (Run targeted API tests)
|
||||
- **Status:** All implementation complete, only testing remains
|
||||
- **Recommendation:** Complete the remaining test task and archive
|
||||
|
||||
#### 2. opencode-web-terminal (72.7% complete)
|
||||
- **Completed:** 16/22 tasks
|
||||
- **Remaining:** Tasks 6.1-6.4 (testing and quality gates)
|
||||
- **Status:** Phases 1-5 complete (models, API, frontend, migrations)
|
||||
- **Recommendation:** Run backend tests, typecheck, and lint to complete
|
||||
|
||||
### In Progress (20-50%)
|
||||
|
||||
#### 3. session-management-fixes (32% complete)
|
||||
- **Completed:** 8/25 tasks
|
||||
- **Remaining:** All frontend work (phases 3-4, 5.3-5.6) and quality gates
|
||||
- **Status:** Backend tunnel work complete; frontend confirmation dialogs, health polling, and UI updates pending
|
||||
- **Blockers:** Frontend tasks depend on backend being deployed
|
||||
|
||||
#### 4. tool-workshop (24.6% complete)
|
||||
- **Completed:** 35/142 tasks
|
||||
- **Remaining:** 107 tasks across phases 2-5
|
||||
- **Status:** Phase 1 (Backend Foundation) nearly complete (35/37 tasks)
|
||||
- **Blockers:** Phase 2 (Instance Creation Enhancement) not started; includes docker build service, compose generation, config folder mounting, readiness probes
|
||||
|
||||
### Not Started (0%)
|
||||
|
||||
#### 5. cloudflare-tunnel-instances (0% complete)
|
||||
- **Tasks:** 28 across 6 phases
|
||||
- **Status:** No work started
|
||||
- **Dependencies:** May depend on instance-proxy being complete
|
||||
|
||||
#### 6. git-repo-ssh-clone-check (0% complete)
|
||||
- **Tasks:** 11 across 4 phases
|
||||
- **Status:** No work started
|
||||
- **Relationship:** Related to git-repo-working-clones
|
||||
|
||||
#### 7. instance-proxy (0% complete)
|
||||
- **Tasks:** 15 across 4 phases
|
||||
- **Status:** No work started
|
||||
- **Note:** May be superseded by cloudflare-tunnel-instances approach
|
||||
|
||||
#### 8. sessions-hub (0% complete)
|
||||
- **Tasks:** 18 across 6 phases
|
||||
- **Status:** No work started
|
||||
- **Dependencies:** Frontend foundation, session management APIs
|
||||
|
||||
#### 9. tool-config-management (0% complete)
|
||||
- **Tasks:** 22 across 7 phases
|
||||
- **Status:** No work started
|
||||
- **Relationship:** Related to tool-config-ui-rework and tool-workshop
|
||||
|
||||
#### 10. tool-config-ui-rework (0% complete)
|
||||
- **Tasks:** 36 across 8 phases
|
||||
- **Status:** No work started
|
||||
- **Relationship:** Related to tool-config-management
|
||||
|
||||
#### 11. ui-redesign-home-settings (0% complete)
|
||||
- **Tasks:** 18 across 5 phases
|
||||
- **Status:** No work started
|
||||
- **Dependencies:** Sessions hub, settings pages
|
||||
|
||||
### Newly Created
|
||||
|
||||
#### 12. add-config-profiles (partially implemented, not tracked)
|
||||
- **Tasks:** 10 across 2 sections
|
||||
- **Completed:** ~5/10 tasks (models created, migrations pending)
|
||||
- **Status:** Models implemented but not checked off in tasks.md
|
||||
- **Work Done:**
|
||||
- ConfigProfile model created with user ownership, name, description
|
||||
- ConfigInclude model created for ordered profile self-references
|
||||
- ConfigMount model created for mount/file definitions
|
||||
- selected_profile_id added to ToolInstance model
|
||||
- default profile fields added to UserConfig model
|
||||
- Models registered in models/__init__.py
|
||||
- **Remaining:**
|
||||
- Alembic migration
|
||||
- Migration metadata and testing
|
||||
- Legacy deprecation markings
|
||||
|
||||
---
|
||||
|
||||
## Archived Changes
|
||||
|
||||
**25 changes** have been successfully archived in `openspec/changes/archive/`, including:
|
||||
- auth-oauth, database-models, frontend-foundation
|
||||
- tool-instances, tool-terminal, git-control
|
||||
- api-documentation, workspace-visual-overhaul
|
||||
- And others
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Immediate Actions (This Sprint)
|
||||
|
||||
- [ ] **Complete git-repo-working-clones**: Run task 4.1 (targeted API tests)
|
||||
- [ ] **Complete opencode-web-terminal**: Run tasks 6.1-6.4 (tests and quality gates)
|
||||
- [ ] **Archive completed changes**: Move git-repo-working-clones and opencode-web-terminal to archive once tests pass
|
||||
|
||||
### Short-Term (Next 1-2 Sprints)
|
||||
|
||||
- [ ] **session-management-fixes frontend**: Implement confirmation dialogs, health polling, recreate tunnel button
|
||||
- [ ] **tool-workshop Phase 2**: Begin docker build service, compose generation, config folder mounting
|
||||
- [ ] **add-config-profiles**: Create Alembic migration, test models, mark legacy deprecation
|
||||
|
||||
### Medium-Term (Next 3-4 Sprints)
|
||||
|
||||
- [ ] **cloudflare-tunnel-instances**: Evaluate dependency on instance-proxy; decide approach
|
||||
- [ ] **sessions-hub**: Implement after session-management-fixes is complete
|
||||
- [ ] **ui-redesign-home-settings**: Coordinate with sessions-hub completion
|
||||
|
||||
### Backlog / Needs Prioritization
|
||||
|
||||
- [ ] **git-repo-ssh-clone-check**: Determine if still needed after git-repo-working-clones
|
||||
- [ ] **instance-proxy**: Determine if superseded by cloudflare-tunnel-instances
|
||||
- [ ] **tool-config-management**: Evaluate overlap with tool-workshop and tool-config-ui-rework
|
||||
- [ ] **tool-config-ui-rework**: Evaluate overlap with tool-config-management
|
||||
|
||||
---
|
||||
|
||||
## Quality Gates Status
|
||||
|
||||
### Backend
|
||||
|
||||
| Gate | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| ruff (linting) | Unknown | Not run in this review |
|
||||
| mypy (type checking) | Unknown | Not run in this review |
|
||||
| pytest (tests) | Unknown | Not run in this review |
|
||||
| bandit (security) | Unknown | Not run in this review |
|
||||
|
||||
### Frontend
|
||||
|
||||
| Gate | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| TypeScript typecheck | Unknown | Not run in this review |
|
||||
| ESLint | Unknown | Not run in this review |
|
||||
| Build | Unknown | Not run in this review |
|
||||
| Vitest tests | Unknown | Not run in this review |
|
||||
|
||||
**Note:** Tasks 6.1 (Backend quality gates) and 6.2 (Frontend quality gates) are dependencies for this review but are currently blocked. A follow-up task should run these gates and report results.
|
||||
|
||||
---
|
||||
|
||||
## Risks and Blockers
|
||||
|
||||
1. **Testing Bottleneck**: Both near-complete changes are blocked on test execution
|
||||
2. **Frontend Lag**: session-management-fixes has complete backend but all frontend work pending
|
||||
3. **Massive Scope**: tool-workshop is 41% of all active tasks with most work not started
|
||||
4. **Parallel Unstarted Work**: 7 of 11 changes have 0% progress
|
||||
5. **Dependency Confusion**: instance-proxy and cloudflare-tunnel-instances may be competing approaches
|
||||
6. **Legacy Migration**: add-config-profiles introduces breaking changes to config_folders behavior
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **Focus on completions**: Finish git-repo-working-clones and opencode-web-terminal first
|
||||
2. **Archive promptly**: Move completed changes to archive to reduce cognitive load
|
||||
3. **Clarify proxy approach**: Decide between instance-proxy and cloudflare-tunnel-instances
|
||||
4. **Merge overlapping changes**: Consider consolidating tool-config-management, tool-config-ui-rework, and tool-workshop
|
||||
5. **Run quality gates**: Execute tasks 6.1 and 6.2 before claiming any change is complete
|
||||
6. **Document breaking changes**: Ensure add-config-profiles migration plan is well-documented
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Task Count by Change
|
||||
|
||||
| Change | Total | Complete | Remaining | % |
|
||||
|--------|-------|----------|-----------|---|
|
||||
| cloudflare-tunnel-instances | 28 | 0 | 28 | 0.0% |
|
||||
| git-repo-ssh-clone-check | 11 | 0 | 11 | 0.0% |
|
||||
| git-repo-working-clones | 8 | 7 | 1 | 87.5% |
|
||||
| instance-proxy | 15 | 0 | 15 | 0.0% |
|
||||
| opencode-web-terminal | 22 | 16 | 6 | 72.7% |
|
||||
| session-management-fixes | 25 | 8 | 17 | 32.0% |
|
||||
| sessions-hub | 18 | 0 | 18 | 0.0% |
|
||||
| tool-config-management | 22 | 0 | 22 | 0.0% |
|
||||
| tool-config-ui-rework | 36 | 0 | 36 | 0.0% |
|
||||
| tool-workshop | 142 | 35 | 107 | 24.6% |
|
||||
| ui-redesign-home-settings | 18 | 0 | 18 | 0.0% |
|
||||
| **TOTAL** | **345** | **66** | **279** | **19.1%** |
|
||||
|
||||
---
|
||||
|
||||
*Review completed. Recommend archiving this document in the workspace documentation.*
|
||||
Reference in New Issue
Block a user