gitstow 0.2.6__tar.gz → 0.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. {gitstow-0.2.6 → gitstow-0.2.8}/.github/workflows/publish.yml +19 -0
  2. gitstow-0.2.8/AGENTS.md +150 -0
  3. {gitstow-0.2.6 → gitstow-0.2.8}/CHANGELOG.md +29 -0
  4. {gitstow-0.2.6 → gitstow-0.2.8}/CLAUDE.md +9 -0
  5. {gitstow-0.2.6 → gitstow-0.2.8}/PKG-INFO +1 -1
  6. gitstow-0.2.8/docs/building/audit-2026-07-06.md +149 -0
  7. gitstow-0.2.8/docs/building/plans/2026-07-06-wave-1-correctness-safety.md +1301 -0
  8. gitstow-0.2.8/docs/building/plans/2026-07-06-wave-2-status-model.md +1097 -0
  9. gitstow-0.2.8/docs/building/plans/2026-07-06-wave-3-structure-polish.md +1690 -0
  10. {gitstow-0.2.6 → gitstow-0.2.8}/pyproject.toml +1 -1
  11. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/__init__.py +1 -1
  12. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/config_cmd.py +39 -38
  13. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/fetch.py +5 -5
  14. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/manage.py +38 -33
  15. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/onboard.py +15 -6
  16. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/pull.py +12 -11
  17. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/remove.py +11 -0
  18. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/setup_ai.py +4 -4
  19. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/workspace_cmd.py +15 -0
  20. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/git.py +9 -1
  21. gitstow-0.2.8/src/gitstow/core/locking.py +63 -0
  22. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/repo.py +50 -34
  23. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/url_parser.py +33 -10
  24. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/server.py +30 -1
  25. gitstow-0.2.8/tests/test_cli.py +266 -0
  26. {gitstow-0.2.6 → gitstow-0.2.8}/tests/test_config.py +63 -0
  27. {gitstow-0.2.6 → gitstow-0.2.8}/tests/test_git.py +14 -0
  28. gitstow-0.2.8/tests/test_locking.py +64 -0
  29. gitstow-0.2.8/tests/test_onboard.py +122 -0
  30. {gitstow-0.2.6 → gitstow-0.2.8}/tests/test_repo.py +2 -2
  31. {gitstow-0.2.6 → gitstow-0.2.8}/tests/test_serve.py +27 -1
  32. gitstow-0.2.8/tests/test_setup_ai.py +49 -0
  33. {gitstow-0.2.6 → gitstow-0.2.8}/tests/test_url_parser.py +35 -0
  34. gitstow-0.2.6/tests/test_cli.py +0 -80
  35. {gitstow-0.2.6 → gitstow-0.2.8}/.claude/settings.json +0 -0
  36. {gitstow-0.2.6 → gitstow-0.2.8}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  37. {gitstow-0.2.6 → gitstow-0.2.8}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  38. {gitstow-0.2.6 → gitstow-0.2.8}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  39. {gitstow-0.2.6 → gitstow-0.2.8}/.github/workflows/ci.yml +0 -0
  40. {gitstow-0.2.6 → gitstow-0.2.8}/.gitignore +0 -0
  41. {gitstow-0.2.6 → gitstow-0.2.8}/BACKLOG.md +0 -0
  42. {gitstow-0.2.6 → gitstow-0.2.8}/CODE_OF_CONDUCT.md +0 -0
  43. {gitstow-0.2.6 → gitstow-0.2.8}/CONTRIBUTING.md +0 -0
  44. {gitstow-0.2.6 → gitstow-0.2.8}/LICENSE +0 -0
  45. {gitstow-0.2.6 → gitstow-0.2.8}/README.md +0 -0
  46. {gitstow-0.2.6 → gitstow-0.2.8}/SECURITY.md +0 -0
  47. {gitstow-0.2.6 → gitstow-0.2.8}/demo.gif +0 -0
  48. {gitstow-0.2.6 → gitstow-0.2.8}/demo.tape +0 -0
  49. {gitstow-0.2.6 → gitstow-0.2.8}/docs/building/implementation-plan.md +0 -0
  50. {gitstow-0.2.6 → gitstow-0.2.8}/docs/user/commands.md +0 -0
  51. {gitstow-0.2.6 → gitstow-0.2.8}/docs/user/concepts.md +0 -0
  52. {gitstow-0.2.6 → gitstow-0.2.8}/docs/user/configuration.md +0 -0
  53. {gitstow-0.2.6 → gitstow-0.2.8}/docs/user/getting-started.md +0 -0
  54. {gitstow-0.2.6 → gitstow-0.2.8}/scripts/release.sh +0 -0
  55. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/__main__.py +0 -0
  56. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/__init__.py +0 -0
  57. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/add.py +0 -0
  58. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/doctor.py +0 -0
  59. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/exec_cmd.py +0 -0
  60. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/export_cmd.py +0 -0
  61. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/helpers.py +0 -0
  62. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/list_cmd.py +0 -0
  63. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/main.py +0 -0
  64. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/migrate.py +0 -0
  65. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/open_cmd.py +0 -0
  66. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/search.py +0 -0
  67. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/serve.py +0 -0
  68. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/shell.py +0 -0
  69. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/skill_cmd.py +0 -0
  70. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/stats.py +0 -0
  71. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/status.py +0 -0
  72. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/tui.py +0 -0
  73. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/cli/update.py +0 -0
  74. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/__init__.py +0 -0
  75. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/config.py +0 -0
  76. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/discovery.py +0 -0
  77. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/parallel.py +0 -0
  78. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/core/paths.py +0 -0
  79. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/mcp/__init__.py +0 -0
  80. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/mcp/server.py +0 -0
  81. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/skill/SKILL.md +0 -0
  82. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/tui/__init__.py +0 -0
  83. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/tui/app.py +0 -0
  84. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/__init__.py +0 -0
  85. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/routes/__init__.py +0 -0
  86. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/routes/collection.py +0 -0
  87. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/routes/dashboard.py +0 -0
  88. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/routes/pages.py +0 -0
  89. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/routes/repos.py +0 -0
  90. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/routes/system.py +0 -0
  91. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/routes/workspaces.py +0 -0
  92. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/static/app.css +0 -0
  93. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/_repo_drawer.html +0 -0
  94. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/add_repo.html +0 -0
  95. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/base.html +0 -0
  96. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/dashboard.html +0 -0
  97. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/partials/dashboard_rows.html +0 -0
  98. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/partials/fetch_summary.html +0 -0
  99. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/partials/pull_summary.html +0 -0
  100. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/partials/repo_row.html +0 -0
  101. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/settings.html +0 -0
  102. {gitstow-0.2.6 → gitstow-0.2.8}/src/gitstow/web/templates/workspaces.html +0 -0
  103. {gitstow-0.2.6 → gitstow-0.2.8}/tests/__init__.py +0 -0
  104. {gitstow-0.2.6 → gitstow-0.2.8}/tests/conftest.py +0 -0
@@ -5,7 +5,26 @@ on:
5
5
  tags: ["v*"]
6
6
 
7
7
  jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+
17
+ - name: Install dependencies
18
+ run: pip install -e ".[dev]"
19
+
20
+ - name: Lint
21
+ run: ruff check src/
22
+
23
+ - name: Test
24
+ run: pytest --tb=short
25
+
8
26
  publish:
27
+ needs: test
9
28
  runs-on: ubuntu-latest
10
29
  environment: pypi
11
30
  permissions:
@@ -0,0 +1,150 @@
1
+ # gitstow - AI Developer Guide
2
+
3
+ ## What is this?
4
+
5
+ A CLI tool that manages collections of git repositories across multiple workspaces. Think "package manager for repos" - whether they're open-source projects you learn from or active projects you work on.
6
+
7
+ ## Operating Contract
8
+
9
+ - Prefer proper long-term solutions over shortcut patches. If a feature is incomplete in one surface, build the feature into that surface instead of papering over it with wording, partial conditionals, or one-off display logic.
10
+ - Do not recommend quick fixes, temporary patches, or narrow workarounds unless the user explicitly asks for a shortcut.
11
+ - Read the relevant existing code before proposing or editing. Let the current architecture decide where the change belongs.
12
+ - Keep changes scoped to the requested behavior. Avoid unrelated refactors, formatting churn, or drive-by cleanups.
13
+ - Do not revert user changes or generated local state unless the user explicitly asks.
14
+
15
+ ## Product Standards
16
+
17
+ - Keep CLI, TUI, web dashboard, JSON output, docs, and tests semantically aligned when changing user-facing status behavior.
18
+ - For repo state presentation, avoid using "dirty" as a broad user-facing bucket for every local change. Present it as local/uncommitted changes with the composition visible: modified, staged, and untracked counts.
19
+ - Keep local working-tree state separate from remote relationship state. For example: local changes, clean, ahead, behind, diverged, frozen, missing.
20
+ - When improving the web dashboard, implement the actual missing dashboard feature and shared classification/model behavior instead of copying a CLI-only assumption into the template.
21
+
22
+ ## Key Concept: Workspaces
23
+
24
+ Repos are organized into **workspaces** - directories with a label and layout mode:
25
+ - **structured** layout: `workspace/owner/repo/` (open-source collections)
26
+ - **flat** layout: `workspace/repo/` (active projects, no owner subdirectory)
27
+
28
+ Each workspace can have auto-tags applied to all repos it discovers.
29
+
30
+ ## Architecture
31
+
32
+ ```
33
+ src/gitstow/
34
+ ├── cli/ # Typer commands (thin — delegates to core)
35
+ │ ├── main.py # App entry, command registration, global --workspace flag
36
+ │ ├── helpers.py # Shared workspace resolution, repo lookup, iteration
37
+ │ ├── add.py, pull.py, list_cmd.py, status.py, remove.py # Core commands
38
+ │ ├── workspace_cmd.py # workspace list/add/remove/scan subcommands
39
+ │ ├── manage.py # freeze/unfreeze/tag/untag/info subcommands
40
+ │ ├── exec_cmd.py, search.py, open_cmd.py, stats.py # Power commands
41
+ │ ├── export_cmd.py # export/import collection
42
+ │ ├── shell.py # Shell integration (fzf, aliases)
43
+ │ ├── tui.py # TUI launcher
44
+ │ ├── serve.py # Web dashboard launcher
45
+ │ ├── migrate.py, config_cmd.py, onboard.py, doctor.py, skill_cmd.py
46
+ │ └── __init__.py
47
+ ├── core/ # Business logic (git ops, URL parsing, state)
48
+ │ ├── paths.py # Path constants, central repos.yaml resolution
49
+ │ ├── config.py # Workspace + Settings dataclasses, load/save
50
+ │ ├── repo.py # Repo dataclass + RepoStore (nested YAML CRUD)
51
+ │ ├── url_parser.py # URL → (host, owner, repo) extraction
52
+ │ ├── git.py # All git subprocess calls
53
+ │ ├── discovery.py # Walk directory tree (structured + flat), reconcile
54
+ │ ├── parallel.py # Async execution with semaphore
55
+ │ └── __init__.py
56
+ ├── tui/ # Textual interactive dashboard
57
+ │ ├── app.py # Main TUI application
58
+ │ └── __init__.py
59
+ ├── web/ # FastAPI browser dashboard (gitstow ui)
60
+ │ ├── server.py # FastAPI app, uvicorn runner, app.state.server stash
61
+ │ ├── static/app.css # Dark theme, Bricolage Grotesque + JetBrains Mono
62
+ │ ├── templates/ # Jinja2 — base.html + page templates + partials/
63
+ │ └── routes/ # dashboard.py, repos.py, workspaces.py, collection.py, pages.py, system.py
64
+ └── skill/ # Codex skill (SKILL.md)
65
+ └── SKILL.md
66
+ ```
67
+
68
+ **Key rules:**
69
+ 1. CLI never touches git directly - it calls `core/git.py`
70
+ 2. `core/repo.py` (RepoStore) is the only module that reads/writes `repos.yaml`
71
+ 3. `core/config.py` defines Workspace dataclass - all workspace logic flows from here
72
+ 4. `cli/helpers.py` provides `resolve_workspaces()`, `resolve_repo()`, `iter_repos_with_workspace()`
73
+
74
+ ## Key Files
75
+
76
+ - `core/config.py` - Workspace + Settings dataclasses. `get_workspaces()` with legacy migration shim.
77
+ - `core/repo.py` - Repo with workspace field, RepoStore with nested YAML format, legacy auto-migration.
78
+ - `core/discovery.py` - `discover_repos(root, layout)` supports structured and flat layouts.
79
+ - `core/url_parser.py` - URL parsing (the hardest part). Test changes here thoroughly.
80
+ - `core/git.py` - All git subprocess calls. Uses `git status --porcelain=v2 --branch` for single-call efficiency.
81
+ - `core/parallel.py` - Async execution with semaphore (max 6 concurrent).
82
+ - `cli/helpers.py` - Shared workspace resolution used by all CLI commands.
83
+ - `cli/workspace_cmd.py` - workspace list/add/remove/scan subcommands.
84
+ - `cli/main.py` - Typer app, global `-w/--workspace` option, command registration.
85
+ - `tui/app.py` - Textual dashboard with DataTable, filter, pull, freeze toggle.
86
+
87
+ ## Data Files
88
+
89
+ - `~/.gitstow/config.yaml` - Settings (workspaces list, default host, SSH pref).
90
+ - `~/.gitstow/repos.yaml` - Repo metadata nested by workspace label. Central location.
91
+
92
+ ### repos.yaml format
93
+ ```yaml
94
+ oss:
95
+ anthropic/Codex:
96
+ remote_url: https://github.com/anthropic/Codex.git
97
+ tags: [ai]
98
+ active:
99
+ gitstow:
100
+ remote_url: https://github.com/rishmadaan/gitstow.git
101
+ tags: [active]
102
+ ```
103
+
104
+ ## All Commands (32)
105
+
106
+ **Core:** `add`, `pull`, `fetch`, `list`, `status`, `remove`, `migrate`
107
+ **Workspace:** `workspace list`, `workspace add`, `workspace remove`, `workspace scan`
108
+ **Repo management:** `repo freeze`, `repo unfreeze`, `repo tag`, `repo untag`, `repo tags`, `repo info`
109
+ **Power:** `exec`, `search`, `open`, `stats`
110
+ **Sharing:** `collection export`, `collection import`
111
+ **Shell:** `shell pick`, `shell init`, `shell completions`, `shell setup`, `tui`, `ui`
112
+ **Setup:** `onboard`, `config show/set/path/migrate-root`, `doctor`, `install-skill`, `setup-ai`, `update`
113
+
114
+ ## Development
115
+
116
+ ```bash
117
+ cd ~/labs/projects/gitstow
118
+ pip install -e ".[dev]"
119
+ pytest # 44 tests
120
+ ruff check src/
121
+ pip install -e ".[tui]" # For TUI development
122
+ ```
123
+
124
+ ## Patterns
125
+
126
+ - `--json -j` and `--quiet -q` on all main commands
127
+ - Global `-w/--workspace` flag filters all commands to a single workspace
128
+ - `cli/helpers.py` for workspace resolution (don't repeat in each command)
129
+ - Rich console for stdout, err_console for stderr
130
+ - Typer with `rich_markup_mode="rich"`, `typer.Context` for global options
131
+ - YAML for persistence (not JSON, not SQLite)
132
+ - asyncio with semaphore for parallel git ops
133
+ - `git status --porcelain=v2 --branch` for single-call status (vs gita's 4-5 calls)
134
+ - Repo.global_key (`workspace:key`) for unique identification across workspaces
135
+ - Legacy format auto-migration (flat repos.yaml -> nested, root_path -> workspaces)
136
+
137
+ ## AI Integration
138
+
139
+ **Primary: Codex skill** (`src/gitstow/skill/SKILL.md`)
140
+ - Installed to `~/.Codex/skills/gitstow/` via `gitstow install-skill` or `gitstow onboard`
141
+ - Auto-updates on version bumps (checks `.version` marker on every CLI invocation)
142
+ - Zero context cost when inactive - only loaded when task matches the skill description
143
+ - Codex runs gitstow CLI commands via Bash - full access to all commands
144
+
145
+ **Optional: MCP server** (`src/gitstow/mcp/server.py`)
146
+ - For non-Codex AI tools (Codex Desktop, Cursor, Windsurf)
147
+ - Install: `pip install gitstow[mcp]`, entry point: `gitstow-mcp`
148
+ - 13 tools + 3 resources, wraps same `core/` modules as CLI
149
+ - **Tradeoff:** MCP tools are always loaded into context (costs tokens even when idle).
150
+ The skill has no such cost. Only use MCP for dedicated repo-management setups.
@@ -4,6 +4,35 @@ All notable changes to gitstow will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.2.8] - 2026-07-12
8
+
9
+ ### Fixed
10
+
11
+ - **Pasted GitHub browse URLs now resolve correctly.** URLs like `/tree/...`, `/blob/...`, `/pull/...` now resolve to the repo root instead of being misparsed.
12
+ - **`config migrate-root`** now actually updates workspace paths, and honors the global `-w/--workspace` flag.
13
+ - **`repo freeze/unfreeze/tag/untag`** now honor `-w/--workspace` and report cross-workspace ambiguity clearly instead of guessing.
14
+ - **`pull --json` and `fetch --json`** always emit pure JSON on stdout — no more banners or progress lines interleaved with the payload.
15
+ - **Pull summary** keeps per-workspace identity for same-named frozen repos in different workspaces.
16
+ - **Bulk git operations** no longer hang on credential prompts and are now locale-independent.
17
+ - **`remove --delete`** refuses to delete paths that resolve outside the workspace root.
18
+ - **Workspace labels** are now validated (lowercase alphanumeric, dash, underscore only).
19
+
20
+ ### Added
21
+
22
+ - **Atomic, cross-process-locked writes** to `repos.yaml`, safe for concurrent CLI and web UI use.
23
+ - **Test gate before PyPI publish** — releases now require a passing test suite.
24
+
25
+ ### Security
26
+
27
+ - **Web dashboard CSRF protection.** POST routes now reject cross-origin and DNS-rebinding requests on the localhost UI.
28
+
29
+ ## [0.2.7] - 2026-05-28
30
+
31
+ ### Fixed
32
+
33
+ - **`gitstow onboard`** now uses Beaupy's `default_is_yes` confirmation parameter, fixing first-run setup crashes on confirmation prompts.
34
+ - **AI integration setup** now renders MCP warning text with balanced Rich markup during onboarding and `gitstow setup-ai`.
35
+
7
36
  ## [0.2.5] - 2026-04-19
8
37
 
9
38
  ### Added
@@ -119,6 +119,15 @@ pip install -e ".[tui]" # For TUI development
119
119
  - Repo.global_key (`workspace:key`) for unique identification across workspaces
120
120
  - Legacy format auto-migration (flat repos.yaml → nested, root_path → workspaces)
121
121
 
122
+ ## Product & Implementation Standards
123
+
124
+ - Prefer proper long-term solutions over shortcut patches. If a feature is incomplete in one surface, build the feature into that surface instead of papering over it with wording, partial conditionals, or one-off display logic.
125
+ - Do not recommend quick fixes, temporary patches, or narrow workarounds unless the user explicitly asks for a shortcut.
126
+ - Keep CLI, TUI, web dashboard, JSON output, docs, and tests semantically aligned when changing user-facing status behavior.
127
+ - For repo state presentation, avoid using "dirty" as a broad user-facing bucket for every local change. Present it as local/uncommitted changes with the composition visible: modified, staged, and untracked counts.
128
+ - Keep local working-tree state separate from remote relationship state. For example: local changes, clean, ahead, behind, diverged, frozen, missing.
129
+ - When improving the web dashboard, implement the actual missing dashboard feature and shared classification/model behavior instead of copying a CLI-only assumption into the template.
130
+
122
131
  ## AI Integration
123
132
 
124
133
  **Primary: Claude Code skill** (`src/gitstow/skill/SKILL.md`)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitstow
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: A git repository library manager — clone, organize, and maintain collections of repos you learn from
5
5
  Project-URL: Homepage, https://github.com/rishmadaan/gitstow
6
6
  Project-URL: Repository, https://github.com/rishmadaan/gitstow
@@ -0,0 +1,149 @@
1
+ # gitstow — Full Audit (v0.2.7)
2
+
3
+ **Date:** 2026-07-06
4
+ **Auditor:** Claude (Fable 5), full read of all source (~9.5k lines Python), planning docs, tests, CI, packaging
5
+ **Baseline:** commit `1aefe9a`, 139 tests passing
6
+ **Status:** Awaiting greenlight — see [Implementation Waves](#implementation-waves)
7
+
8
+ This document is the working tracker for the post-audit improvement effort. Each finding
9
+ has an ID for reference. Check items off as waves land.
10
+
11
+ ---
12
+
13
+ ## 1. Planned but never done
14
+
15
+ Commitments in `docs/building/implementation-plan.md` and `CLAUDE.md` that the code does not fulfill.
16
+
17
+ | ID | Finding | Where |
18
+ |----|---------|-------|
19
+ | P1 | **Parallel `add` clones.** Plan: "Multiple URLs are cloned concurrently (semaphore = parallel_limit)." Implementation is a sequential for-loop. | `cli/add.py:105` |
20
+ | P2 | **Remote-mismatch conflict detection.** Plan: existing dir with a different remote → "error, explain conflict". Instead `add` silently registers whatever is on disk, even when its remote differs from the requested URL. | `cli/add.py:141` |
21
+ | P3 | **Reconciliation on `list`/`status`/`pull`.** Plan design decision #2: reconcile disk-vs-store on these commands and show untracked repos as "untracked" with a hint. Only `doctor` reconciles. | `cli/list_cmd.py`, `cli/status.py`, `cli/pull.py` |
22
+ | P4 | **The status-presentation standard (CLAUDE.md "Product & Implementation Standards") is implemented on no surface.** The standard: don't use "dirty" as a broad bucket; show modified/staged/untracked composition; keep local working-tree state separate from remote relationship state. Reality: CLI shows combined `dirty(N)`; web `_classify` only reads `status.dirty` so **staged-only or untracked-only repos display as "clean"**; TUI computes dirty as `total − clean − frozen`. Three divergent classifiers. | `cli/status.py:168`, `web/routes/dashboard.py:62`, `tui/app.py:224` |
23
+ | P5 | **TUI acknowledged broken** (BACKLOG.md) and parked. Decision needed: triage or retire. | `tui/app.py` |
24
+ | P6 | **CHANGELOG missing the 0.2.6 entry.** `release.sh` bumps versions but never enforces a changelog entry. | `CHANGELOG.md`, `scripts/release.sh` |
25
+
26
+ ## 2. Correctness bugs
27
+
28
+ | ID | Severity | Finding | Where |
29
+ |----|----------|---------|-------|
30
+ | B1 | High | **Deep URLs parse to garbage** (verified live). `github.com/owner/repo/tree/main/src` → owner `owner/repo/tree/main`, repo `src`, clone URL `.../tree/main/src.git`. Any pasted `/tree/`, `/blob/`, `/pull/`, `/issues/` link — the most common real paste — creates a bogus nested dir and failing clone. Nested-group (GitLab) handling needs host-aware guards. | `core/url_parser.py:134` |
31
+ | B2 | High | **`config migrate-root` broken post-workspaces.** Sets legacy `settings.root_path`, but `Settings.to_dict()` drops `root_path` whenever workspaces exist, and workspace paths are never updated. Files move; config still points at old location; all repos show "missing". `config set` docstring still advertises `root_path`, which the command rejects. | `cli/config_cmd.py:223`, `core/config.py:86` |
32
+ | B3 | High | **repos.yaml writes are neither atomic nor locked.** Advertised workflow is CLI + web UI running simultaneously; concurrent load-modify-save loses updates; crash mid-write corrupts the file. Needs temp-file + `os.replace` + file lock. | `core/repo.py:149` |
33
+ | B4 | High | **No `GIT_TERMINAL_PROMPT=0` / `LC_ALL=C` on git subprocesses.** One auth-prompting repo hangs every bulk pull for the full timeout; "Already up to date" string-match breaks on non-English locales. | `core/git.py:83`, `core/git.py:160` |
34
+ | B5 | Med | **`repo freeze/unfreeze/tag/untag` ignore `-w` and mishandle ambiguity.** They call `store.get(key)` directly; same key in two workspaces → `_resolve_global_key` returns `None` → misleading "not tracked". `freeze --tag` updates by bare key and can hit the wrong workspace's repo. | `cli/manage.py:62,54` |
35
+ | B6 | Med | **Web pull-all pulls dirty repos; CLI never does.** Cross-surface semantic mismatch (ff-only limits damage). | `web/routes/repos.py:153` vs `cli/pull.py:35` |
36
+ | B7 | Med | **`collection import` loses workspace assignment and fails silently on newer versions.** Exports carry per-repo `workspace`; import dumps everything into one workspace. Newer-version file → bare `typer.Exit(1)` with a "caller should print error" comment; nothing is printed. | `cli/export_cmd.py:189,258` |
37
+ | B8 | Med | **`workspace remove` silently orphans repos.** Default `--keep-repos` leaves repos.yaml entries pointing at a nonexistent workspace — invisible to `list`/`status`/web; `doctor` doesn't flag them either. | `cli/workspace_cmd.py:118` |
38
+ | B9 | Low | **`open --editor` is a dead flag**; editor detection prefers `code`/`cursor` over `$EDITOR`; terminal `$EDITOR` (vim) launched via `Popen` with no TTY. | `cli/open_cmd.py:24,116` |
39
+ | B10 | Low | **`pull` frozen bookkeeping uses `r.key` not `global_key`** — same-named repos in two workspaces collapse into one "frozen" summary line. | `cli/pull.py:112` |
40
+ | B11 | Low | **No label validation on `workspace add`** — `:` or `/` in a label breaks `global_key` parsing and web routes. | `cli/workspace_cmd.py:67` |
41
+ | B12 | Low | **CLI `remove --delete` has no path-containment check**; web delete route defensively verifies path is under workspace root. CLI should get the same guard. | `cli/remove.py:66` vs `web/routes/repos.py:377` |
42
+ | B13 | Low | **`_resolve_global_key` dead code**: final line re-tests `len(matches) == 1` after that branch already returned; ambiguous vs missing indistinguishable to callers. | `core/repo.py:215` |
43
+
44
+ ## 3. Efficiency
45
+
46
+ | ID | Finding | Where |
47
+ |----|---------|-------|
48
+ | E1 | **Web dashboard status is sequential AND blocks the event loop.** `_build_repos_data` runs `get_status` (subprocess) per repo, serially, inside `async def`, on every render + every 30s auto-refresh. 100 repos ≈ multi-second loads. `core/parallel.run_parallel` exists and is used for pulls — not for status. | `web/routes/dashboard.py:122` |
49
+ | E2 | **`store.update()` rewrites the whole YAML per repo.** Pull-all over 50 repos = 50 full-file rewrites (CLI, web, fetch all do this in loops). Needs batch-update / deferred save. | `core/repo.py:259` + callers |
50
+ | E3 | **`add` clones sequentially** (also P1). | `cli/add.py:105` |
51
+ | E4 | **`search` is sequential** across repos; `exec` is parallel. | `cli/search.py:82` |
52
+ | E5 | **`get_disk_size` rglobs every file in Python** (`stats`, `repo info`) — very slow on large repos. Use `du -sk` or parallelize. | `core/git.py:280` |
53
+ | E6 | **Clone timeout hardcoded 300s** — kernel-sized repos fail by design; not configurable. | `core/git.py:141` |
54
+
55
+ ## 4. Architecture
56
+
57
+ | ID | Finding | Notes |
58
+ |----|---------|-------|
59
+ | A1 | **Status classification exists in 3 divergent copies** (CLI, web `_classify`, TUI). Fix for P4 is a single `core/status_model.py` classifier consumed by all surfaces + JSON. Highest-leverage refactor in the codebase. | |
60
+ | A2 | **Web stack (fastapi, uvicorn, jinja2, python-multipart) ships as core deps** for every install, including pure-CLI users. `cli/serve.py` already handles ImportError gracefully, so a `[ui]` extra is nearly free. Deliberate v0.2.0 choice — revisit (tradeoff: onboarding friction vs install weight). | Decision needed |
61
+ | A3 | **MCP server (704 lines) reimplements add/pull/list** instead of sharing an operations layer → drift risk (already lacks `last_fetched`; `list --json` omits it too, so no JSON consumer can see fetch state). | `mcp/server.py`, `cli/list_cmd.py:91` |
62
+ | A4 | **~300 lines of copy-paste across CLI commands**: `pull.py`/`fetch.py` retry+progress+summary loops nearly identical; the same tag/owner/frozen filter block appears in six commands. Natural home: `core/operations.py` (filters + bulk-runner), which is also what web/MCP should call. | |
63
+
64
+ ## 5. Security
65
+
66
+ | ID | Severity | Finding | Where |
67
+ |----|----------|---------|-------|
68
+ | S1 | High | **Web UI has zero CSRF protection.** All mutations are POSTs; any website can fire cross-origin form POSTs at `http://127.0.0.1:7853` (delete repos from disk, add attacker-URL repos, shutdown). Localhost binding does not prevent this. Fix: reject requests whose `Origin`/`Host` isn't localhost (or session token). The stated design goal "arbitrary git execution must not be LAN-reachable" has this gap. | `web/routes/*.py` |
69
+
70
+ ## 6. Deployment & CI
71
+
72
+ | ID | Finding | Where |
73
+ |----|---------|-------|
74
+ | D1 | **`publish.yml` ships to PyPI without running tests.** Tag push → build → publish; CI is not a gate. Add test job as dependency of publish. | `.github/workflows/publish.yml` |
75
+ | D2 | **CI ubuntu-only** while classifiers claim macOS + Windows and development happens on macOS. Add macOS to matrix at minimum. | `.github/workflows/ci.yml` |
76
+ | D3 | **`release.sh` doesn't require a CHANGELOG entry** (proven by missing 0.2.6). | `scripts/release.sh` |
77
+ | D4 | **`typer[all]` is a deprecated extra** in modern Typer (harmless noise). | `pyproject.toml:33` |
78
+ | D5 | **No tests for `fetch` CLI, MCP server, or TUI.** | `tests/` |
79
+
80
+ ## 7. Docs drift
81
+
82
+ | ID | Finding |
83
+ |----|---------|
84
+ | DOC1 | CLAUDE.md says "44 tests" (actual 139); command count worth re-verifying after changes. |
85
+ | DOC2 | BACKLOG.md references `.github/workflows/release.yml`; actual file is `publish.yml`. |
86
+ | DOC3 | `list --json` omits `last_fetched` though tracked (also A3). |
87
+ | DOC4 | `config set` help/docstring advertises `root_path`, which is rejected (also B2). |
88
+
89
+ ---
90
+
91
+ ## Implementation Waves
92
+
93
+ Execution model per repo convention: Fable designs/reviews; implementation delegated to
94
+ Opus (substantial) / Sonnet (mechanical) subagents.
95
+
96
+ **Detailed task-by-task plans (TDD steps, code, commands):**
97
+ - Wave 1: [plans/2026-07-06-wave-1-correctness-safety.md](plans/2026-07-06-wave-1-correctness-safety.md)
98
+ - Wave 2: [plans/2026-07-06-wave-2-status-model.md](plans/2026-07-06-wave-2-status-model.md)
99
+ - Wave 3: [plans/2026-07-06-wave-3-structure-polish.md](plans/2026-07-06-wave-3-structure-polish.md)
100
+
101
+ **Product decisions locked 2026-07-06:**
102
+ 1. Web stack moves to an optional `[ui]` extra (ships in 0.3.0) — A2.
103
+ 2. The broken Textual TUI is retired, not repaired (moved to Wave 2 Task 1 so the
104
+ status model only needs two consumer surfaces) — P5.
105
+ 3. Bulk pull skips only modified/staged repos; untracked-only repos ARE pulled
106
+ (CLI + web adopt the same rule) — informs B6/P4.
107
+
108
+ **Release strategy:** Wave 1 → 0.2.8 (patch). Waves 2 + 3 → 0.3.0 together
109
+ (TUI removal + `[ui]` extra are the breaking changes justifying the minor bump).
110
+
111
+ ### Wave 1 — Correctness & safety (Opus) — COMPLETE (2026-07-12)
112
+ - [x] B1 deep-URL parsing guards (host-aware; GitHub/known hosts reject >2 path segments, strip `/tree|blob|pull|issues/...`)
113
+ - [x] B3 atomic writes (temp + `os.replace`) + file locking on repos.yaml
114
+ - [x] B4 `GIT_TERMINAL_PROMPT=0`, `LC_ALL=C` in `_run_git`; drop locale-dependent string matching where possible
115
+ - [x] B2 fix or explicitly retire `config migrate-root` for the workspace era (+ DOC4)
116
+ - [x] B5 route `repo freeze/unfreeze/tag/untag` through `resolve_repo` with `-w` support; distinguish ambiguous from missing (B13)
117
+ - [x] S1 Origin/Host check middleware on all POST routes
118
+ - [x] D1 test gate before PyPI publish
119
+ - [x] B12 path-containment check in CLI `remove --delete`
120
+ - [x] B11 workspace label validation
121
+
122
+ ### Wave 2 — Shared status model (Opus) — NOT STARTED
123
+ - [ ] P5 retire the Textual TUI (decision: retire; clears the third surface before the refactor)
124
+ - [ ] A1/P4 `core/status_model.py`: one classifier separating local composition (modified/staged/untracked counts) from remote relationship (in-sync/ahead/behind/diverged) + frozen/missing overlays
125
+ - [ ] Consume from CLI `status`, web dashboard, and `--json` payloads (per CLAUDE.md standards)
126
+ - [ ] B6 one pull rule on both surfaces: modified/staged skip, untracked-only pulls (decision locked)
127
+ - [ ] E1 parallelize web status gathering via `run_parallel` (off the event loop)
128
+ - [ ] E2 batch RepoStore updates — `bulk()` (single save per bulk op)
129
+ - [ ] Tests covering the classifier matrix
130
+
131
+ ### Wave 3 — Structure & polish (Sonnet-heavy, Opus for A3/A4) — NOT STARTED
132
+ - [ ] P1/E3 parallel `add` clones; P2 remote-mismatch conflict error
133
+ - [ ] E4 parallel `search`
134
+ - [ ] A4 `core/operations.py` extraction (filters + bulk runner); collapse pull/fetch duplication
135
+ - [ ] A3 MCP server consumes operations layer; add `last_fetched` everywhere (DOC3); new `fetch_repos` tool
136
+ - [ ] P3 reconciliation hints surfacing untracked repos in `list`/`status` (human output only; JSON shapes unchanged)
137
+ - [ ] B7 import preserves workspace; loud version error. B8 orphaned-workspace detection in doctor + workspace remove warning
138
+ - [ ] B9 `$VISUAL`/`$EDITOR`-first open logic; terminal editors run foreground
139
+ - [ ] A2 web stack → `[ui]` extra (decision locked; ships 0.3.0)
140
+ - [ ] E5 `du`-backed disk sizing; E6 configurable `clone_timeout` setting
141
+ - [ ] D2 macOS CI; D3 changelog gate in release.sh; D4 typer extra; D5 fetch/MCP tests
142
+ - [ ] DOC1/DOC2 docs sync
143
+
144
+ ## Verification notes
145
+
146
+ - All 139 tests pass at baseline (`pytest -q`, 2.82s).
147
+ - B1 verified by executing the parser against deep URLs.
148
+ - B2 verified by reading `Settings.to_dict()` serialization guard (`core/config.py:86`).
149
+ - B3/B4/S1 verified by code inspection; no runtime exploit performed.