Files
kandev/docs/specs/create-local-repository/spec.md
T
2026-08-05 22:34:26 +01:00

221 lines
13 KiB
Markdown

---
status: shipped
created: 2026-07-21
updated: 2026-08-05
owner: kandev
---
# Create a Local Repository During Task Creation
## Why
Users can create tasks from an existing local or remote repository, but a new project requires
leaving the task flow to initialize and register a Git repository first. Users need to create that
repository on the machine running Kandev, select it, and continue composing the same task.
## What
- The local repository selector in **Create New Task** exposes a visible **Create new repository**
action while the task has exactly one repository row.
- The creation form asks for a repository name and an absolute parent path. It shows the resulting
target path before confirmation. The name is one platform-native path segment, not a path.
- A manually entered parent path may contain missing trailing directories. Confirmation creates
those directories beneath the nearest accessible, trusted ancestor before initializing the
repository.
- The parent-folder browser lists the filesystem of the machine running Kandev and shares the same
directory browsing behavior as the **None** source mode's starting-folder picker. It also lets
the user create and enter one new child folder in the currently displayed directory.
- Confirmation creates `<parent>/<name>` only when that target does not already exist, initializes a
local Git repository with default branch `main`, and creates one empty initial commit on that
branch. The working tree contains no user files.
- Git initialization remains bound to the exact request-owned directory identity that the backend
opened and verified. Replacing its pathname before or during initialization must not cause Git to
modify the replacement path.
- A successfully initialized repository is persisted as a local repository in the active workspace,
added to the in-memory workspace repository list, selected in the originating row, and displays
`main` as that row's branch. Other task fields and repository rows are unchanged.
- The task-create flow continues to select a compatible `local` or `local_pc` executor profile for
the first task when the selected profile cannot run the newly created local repository, and
explains the change. Executor selection is unchanged by the initial baseline commit.
- Multi-repository task creation does not expose the action because those tasks require the worktree
executor. Users can add the repository to a multi-repository task after it has its first commit.
- Initialization is an explicit local-path trust grant for the exact canonical repository path. It
does not widen automatic discovery roots or grant access to sibling paths.
- The form remains open with the entered name and parent folder when initialization fails, surfaces
an actionable error, and does not select a repository. Editing either input or navigating to
another folder clears the stale submission error immediately.
- Closing the creation form before confirmation does not create a directory or repository record.
- Desktop and mobile provide the same capability. Mobile uses a touch-native picker surface with no
hover-only action and keeps repository name, target path, errors, and the primary action reachable.
The explicit local repository trust contract remains defined by
[Local Workspace Repositories](../workspaces/local-repositories.md) and
[ADR-2026-07-20](../../decisions/2026-07-20-explicit-local-repository-trust.md).
## Data Model
No new database entity is introduced. A successful initialization creates one existing
`repositories` record:
| Field | Contract |
| --- | --- |
| `workspace_id` | The active task-creation workspace. |
| `name` | The user-supplied repository name after trimming. |
| `source_type` | `local`. |
| `local_path` | The canonical absolute `<parent>/<name>` path. |
| `default_branch` | `main`. |
The filesystem repository and the `repositories` row are created as one product operation when the
repository form is confirmed, before the task is submitted. The created Git repository contains one
empty root commit reachable from `refs/heads/main`, but no user files in the working tree. A
successful response means both exist; a failed response must not leave a repository row. The
filesystem cleanup behavior for a partial failure is described under Failure modes.
## API Surface
`POST /api/v1/workspaces/:id/repositories/initialize-local`
Request:
```json
{
"name": "new-project",
"parent_path": "/home/user/projects"
}
```
Response: `201 Created` with the existing `Repository` DTO. The response has `source_type: "local"`,
the canonical `local_path`, and `default_branch: "main"`.
The backend, not the browser, is authoritative for validation and filesystem changes:
- `400 Bad Request`: missing/invalid name, relative parent path, inaccessible parent, parent is not a
directory, or invalid request body.
- `404 Not Found`: workspace does not exist.
- `409 Conflict`: `<parent>/<name>` already exists. Existing directories are never converted or
overwritten by this operation.
- `500 Internal Server Error`: Git initialization or repository persistence fails.
`POST /api/v1/fs/create-dir`
Request:
```json
{
"parent_path": "/home/user/projects",
"name": "team"
}
```
Response: `201 Created` with the same directory-listing shape as `GET /api/v1/fs/list-dir`, rooted
at the new folder. Invalid names or inaccessible parents return `400 Bad Request`; an existing child
returns `409 Conflict`.
## Permissions
This follows Kandev's trusted-local-user model. The user may initialize a repository under any
explicitly selected parent path whose nearest existing ancestor the Kandev process can trust and
write. Missing trailing directories are created as process-owned directories. The grant is scoped
to the created canonical target path and the active workspace.
## Failure Modes
| Condition | Observable behavior |
| --- | --- |
| Name is empty, `.`/`..`, or contains a host path separator | Confirmation is disabled client-side and the backend rejects a forged request. |
| The nearest existing parent ancestor cannot be trusted, read, or written | The form stays open, shows an error, and no repository is selected or persisted. |
| Target already exists, including an empty directory | The request returns conflict and does not modify the target. |
| The request-owned staging pathname is replaced during Git initialization | The operation fails without modifying the replacement path. Cleanup remains limited to a request-owned directory whose identity can still be verified. |
| `git` is unavailable, initialization fails, or the initial commit fails | The request fails, no repository row is persisted, and Kandev removes only the target directory created by this request when cleanup is safe. A cleanup failure is logged and the error remains visible. |
| Repository persistence fails after Git initialization or the initial commit | The request fails and performs the same best-effort cleanup of the request-owned target; no repository row is returned. |
| Frontend state refresh fails after a successful response | The returned repository is still selected directly and merged into the active workspace cache without waiting for a second list request. |
| No compatible direct local executor profile is available for the existing task-create policy | The form explains the requirement and does not call the initialization endpoint. |
## Persistence Guarantees
The initialized Git repository, including its empty baseline commit, is ordinary user-owned
filesystem content and is never tied to task deletion. Its existing `repositories` record survives
backend restart. Canceling, archiving, or deleting the task does not delete the repository or its
workspace registration.
## Mobile Design Contract
- **Entry and outcome:** the repository chip opens the same local repository picker as desktop; a
visible, at-least-44px **Create new repository** row opens the creation flow and returns the new
selection to the originating chip.
- **Nearest exemplar:** `MobilePickerSheet` supplies the inset bottom-drawer geometry, fixed header,
internal scrolling, and predictable dismissal used by task-workbench pickers.
- **Hierarchy and primary action:** repository name and current target path stay above the directory
list; **Create repository** is the single primary action in the safe-area-aware footer.
- **Presentation rationale:** repository creation is a short temporary choice inside task creation,
so mobile uses an inset bottom drawer rather than a new route or a compressed desktop dialog.
- **Geometry:** the drawer uses one `min-h-0` vertical scroll owner, dynamic viewport sizing, bottom
safe-area clearance, no document horizontal overflow, and 44px directory/action rows. Keyboard
focus remains visible and dismissal returns focus to the repository selector.
- **Shared logic:** validation, target-path derivation, directory listing, API mutation, workspace
cache update, row selection, and compatible local-executor selection are shared. Only
Dialog-versus-Drawer composition is responsive.
## Scenarios
- **GIVEN** the task dialog has a local repository row, **WHEN** the user chooses **Create new
repository**, selects `/work/projects`, enters `alpha`, and confirms, **THEN**
`/work/projects/alpha` contains a Git repository with one empty initial commit on `main` and no
user files, the workspace has a matching local repository record, the branch endpoint returns a
local `main` option, and the originating task row selects `alpha` / `main`.
- **GIVEN** a worktree or container executor profile is selected and a direct local profile is
available, **WHEN** repository initialization succeeds, **THEN** the form selects the compatible
direct local profile and tells the user why the executor changed.
- **GIVEN** no direct local executor profile is available, **WHEN** the user opens the creation form,
**THEN** the primary action explains the requirement and no directory or repository is created.
- **GIVEN** a task with multiple repository rows, **WHEN** the user opens any repository picker,
**THEN** **Create new repository** is not offered because the action remains limited to the
existing single-row task-create flow.
- **GIVEN** `/work/projects/alpha` already exists, **WHEN** the user tries to create `alpha` under
`/work/projects`, **THEN** the UI reports the conflict and Kandev does not modify or register the
existing path.
- **GIVEN** the backend has opened its request-owned staging directory, **WHEN** that directory is
renamed and another directory is installed at the staging pathname before Git starts, **THEN**
Git operates only on the originally opened directory, the replacement remains unchanged, and the
request fails because the original identity is no longer publishable at that pathname.
- **GIVEN** `/work` exists but `/work/team/projects` does not, **WHEN** the user enters
`/work/team/projects` as the parent and creates `alpha`, **THEN** Kandev creates both missing
parent directories and initializes `/work/team/projects/alpha`.
- **GIVEN** the folder navigator displays `/work`, **WHEN** the user creates a child named `team`,
**THEN** `/work/team` is created and the navigator enters that folder on desktop and mobile.
- **GIVEN** an initialization error is visible, **WHEN** the user edits the name or parent path or
navigates to another folder, **THEN** the previous error disappears before the next submission.
- **GIVEN** an invalid name or unwritable parent, **WHEN** the user confirms, **THEN** the form stays
open with its inputs intact and no repository is selected.
- **GIVEN** the creation form is open, **WHEN** the user dismisses it without confirming, **THEN** no
filesystem or database change occurs and their task draft remains intact.
- **GIVEN** a repository was initialized and selected, **WHEN** the user later cancels or deletes the
task, **THEN** the repository directory and workspace repository record remain.
- **GIVEN** a Pixel 5 viewport, **WHEN** the user completes the same flow, **THEN** all actions are
reachable by touch, the directory list scrolls inside the drawer, the footer clears the safe area,
and the page has no horizontal overflow.
## Out of Scope
- Creating or publishing a remote Git hosting repository.
- Cloning, importing, or converting an existing directory; existing paths continue through the
existing local repository selection and settings flows.
- README, license, `.gitignore`, language template, remote, visibility, or hosting-provider setup.
- Choosing the initial branch name; new repositories always start on `main` with the empty baseline
commit described above.
- Worktree, container, or remote execution before the repository has its first commit.
- Creating a new repository as part of a multi-repository task.
- Adding the creation action to Quick Chat, repository settings, project repository pickers, or MCP
tools in this iteration.
- Automatically deleting a successfully created repository when its originating task is canceled or
deleted.
- Adding local-repository initialization support to BSD platforms that do not currently implement
trusted directory ownership checks and exclusive publication.
## Implementation Plan
See [Create Local Repository plan](../../plans/create-local-repository/plan.md).
The descriptor-bound macOS repair is tracked in the
[local repository descriptor initialization plan](../../plans/fix-local-repository-darwin-init/plan.md).