codexspec 0.7.16__tar.gz → 0.7.18__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 (106) hide show
  1. {codexspec-0.7.16 → codexspec-0.7.18}/PKG-INFO +9 -1
  2. {codexspec-0.7.16 → codexspec-0.7.18}/README.md +8 -0
  3. {codexspec-0.7.16 → codexspec-0.7.18}/pyproject.toml +1 -1
  4. {codexspec-0.7.16 → codexspec-0.7.18}/scripts/bash/create-new-feature.sh +11 -0
  5. {codexspec-0.7.16 → codexspec-0.7.18}/scripts/powershell/create-new-feature.ps1 +28 -0
  6. {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/__init__.py +254 -3
  7. {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/automation.py +17 -7
  8. codexspec-0.7.18/src/codexspec/distill_review/__init__.py +13 -0
  9. codexspec-0.7.18/src/codexspec/distill_review/assets/app.js +938 -0
  10. codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/ar.json +112 -0
  11. codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/de.json +112 -0
  12. codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/en.json +112 -0
  13. codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/es.json +112 -0
  14. codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/fr.json +112 -0
  15. codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/hi.json +112 -0
  16. codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/it.json +112 -0
  17. codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/ja.json +112 -0
  18. codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/ko.json +112 -0
  19. codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/pt.json +112 -0
  20. codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/ru.json +112 -0
  21. codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/zh-CN.json +112 -0
  22. codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/zh-TW.json +112 -0
  23. codexspec-0.7.18/src/codexspec/distill_review/assets/index.html +43 -0
  24. codexspec-0.7.18/src/codexspec/distill_review/assets/styles.css +824 -0
  25. codexspec-0.7.18/src/codexspec/distill_review/domain.py +585 -0
  26. codexspec-0.7.18/src/codexspec/distill_review/lease.py +101 -0
  27. codexspec-0.7.18/src/codexspec/distill_review/models.py +133 -0
  28. codexspec-0.7.18/src/codexspec/distill_review/records.py +399 -0
  29. codexspec-0.7.18/src/codexspec/distill_review/server.py +386 -0
  30. codexspec-0.7.18/src/codexspec/distill_review/session.py +323 -0
  31. codexspec-0.7.18/src/codexspec/distill_review/terminal.py +259 -0
  32. codexspec-0.7.18/src/codexspec/distill_review/transaction.py +932 -0
  33. codexspec-0.7.18/src/codexspec/worktrees.py +477 -0
  34. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/analyze.md +72 -0
  35. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/auto-dev.md +72 -0
  36. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/blueprint.md +72 -0
  37. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/checklist.md +72 -0
  38. codexspec-0.7.18/templates/commands/clarify.md +159 -0
  39. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/commit-staged.md +72 -0
  40. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/config.md +83 -0
  41. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/constitution.md +72 -0
  42. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/debug.md +72 -0
  43. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/distill.md +120 -4
  44. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/evolve.md +72 -0
  45. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/generate-spec.md +72 -0
  46. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/implement-tasks.md +72 -0
  47. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/onboard.md +72 -0
  48. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/plan-to-tasks.md +72 -0
  49. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/pr.md +72 -0
  50. codexspec-0.7.18/templates/commands/quick.md +141 -0
  51. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/release-notes.md +72 -0
  52. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/reverse-spec.md +79 -7
  53. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/review-code.md +72 -0
  54. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/review-design.md +72 -0
  55. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/review-plan.md +72 -0
  56. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/review-spec.md +72 -0
  57. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/review-tasks.md +72 -0
  58. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/spec-to-design.md +72 -0
  59. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/spec-to-plan.md +72 -0
  60. {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/specify.md +76 -1
  61. codexspec-0.7.18/templates/commands/tasks-to-issues.md +216 -0
  62. codexspec-0.7.16/templates/commands/clarify.md +0 -87
  63. codexspec-0.7.16/templates/commands/quick.md +0 -69
  64. codexspec-0.7.16/templates/commands/tasks-to-issues.md +0 -144
  65. {codexspec-0.7.16 → codexspec-0.7.18}/.gitignore +0 -0
  66. {codexspec-0.7.16 → codexspec-0.7.18}/LICENSE +0 -0
  67. {codexspec-0.7.16 → codexspec-0.7.18}/codexspec-icon.svg +0 -0
  68. {codexspec-0.7.16 → codexspec-0.7.18}/codexspec-logo-dark.svg +0 -0
  69. {codexspec-0.7.16 → codexspec-0.7.18}/codexspec-logo-light.svg +0 -0
  70. {codexspec-0.7.16 → codexspec-0.7.18}/scripts/bash/check-i18n-completeness.sh +0 -0
  71. {codexspec-0.7.16 → codexspec-0.7.18}/scripts/bash/check-i18n-structure.sh +0 -0
  72. {codexspec-0.7.16 → codexspec-0.7.18}/scripts/bash/check-prerequisites.sh +0 -0
  73. {codexspec-0.7.16 → codexspec-0.7.18}/scripts/bash/common.sh +0 -0
  74. {codexspec-0.7.16 → codexspec-0.7.18}/scripts/bash/review-context.sh +0 -0
  75. {codexspec-0.7.16 → codexspec-0.7.18}/scripts/powershell/check-prerequisites.ps1 +0 -0
  76. {codexspec-0.7.16 → codexspec-0.7.18}/scripts/powershell/common.ps1 +0 -0
  77. {codexspec-0.7.16 → codexspec-0.7.18}/scripts/powershell/review-context.ps1 +0 -0
  78. {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/blueprint.py +0 -0
  79. {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/commands/__init__.py +0 -0
  80. {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/commands/installer.py +0 -0
  81. {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/i18n.py +0 -0
  82. {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/idea.md +0 -0
  83. {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/integrations/__init__.py +0 -0
  84. {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/integrations/base.py +0 -0
  85. {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/integrations/claude.py +0 -0
  86. {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/integrations/codex.py +0 -0
  87. {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/profile.py +0 -0
  88. {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/translator.py +0 -0
  89. {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/checklist-template.md +0 -0
  90. {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/constitution-template.md +0 -0
  91. {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/design-template.md +0 -0
  92. {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/plan-template-detailed.md +0 -0
  93. {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/plan-template-simple.md +0 -0
  94. {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/requirements-template.md +0 -0
  95. {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/spec-template-detailed.md +0 -0
  96. {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/spec-template-simple.md +0 -0
  97. {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/tasks-template-detailed.md +0 -0
  98. {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/tasks-template-simple.md +0 -0
  99. {codexspec-0.7.16 → codexspec-0.7.18}/templates/translations/de.json +0 -0
  100. {codexspec-0.7.16 → codexspec-0.7.18}/templates/translations/en.json +0 -0
  101. {codexspec-0.7.16 → codexspec-0.7.18}/templates/translations/es.json +0 -0
  102. {codexspec-0.7.16 → codexspec-0.7.18}/templates/translations/fr.json +0 -0
  103. {codexspec-0.7.16 → codexspec-0.7.18}/templates/translations/ja.json +0 -0
  104. {codexspec-0.7.16 → codexspec-0.7.18}/templates/translations/ko.json +0 -0
  105. {codexspec-0.7.16 → codexspec-0.7.18}/templates/translations/pt-BR.json +0 -0
  106. {codexspec-0.7.16 → codexspec-0.7.18}/templates/translations/zh-CN.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codexspec
3
- Version: 0.7.16
3
+ Version: 0.7.18
4
4
  Summary: CodexSpec - A Requirements-First SDD toolkit for Claude Code
5
5
  Project-URL: Homepage, https://github.com/Zts0hg/codexspec
6
6
  Project-URL: Repository, https://github.com/Zts0hg/codexspec
@@ -565,6 +565,7 @@ Implementation follows **conditional TDD workflow**:
565
565
  | `--list-langs` | List all supported languages |
566
566
  | `--auto-next` | Toggle/set `workflow.auto_next` (bare toggles; or on/off) |
567
567
  | `--auto-distill` | Toggle/set `workflow.auto_distill` (default on; bare toggles; or on/off) |
568
+ | `--worktrees` | Toggle/set checkout-local worktree isolation (default on; bare toggles; or on/off) |
568
569
 
569
570
  </details>
570
571
 
@@ -837,3 +838,10 @@ MIT License - see [LICENSE](LICENSE) for details.
837
838
 
838
839
  - Inspired by [GitHub spec-kit](https://github.com/github/spec-kit)
839
840
  - Built for [Claude Code](https://claude.ai/code)
841
+
842
+ Ordinary features develop in separate worktrees under the main checkout's sibling
843
+ `<repository-name>-codexspec-worktrees/`, starting before the first requirements write.
844
+ Blueprint and auto-dev retain their fixed shared worktree. Standalone project edits
845
+ use a maintenance worktree; settings take effect per checkout and reach other checkouts
846
+ through Git integration. See [worktree development](docs/en/user-guide/workflow.md#worktree-development)
847
+ and [configuration](docs/en/reference/configuration.md#workflowworktrees).
@@ -520,6 +520,7 @@ Implementation follows **conditional TDD workflow**:
520
520
  | `--list-langs` | List all supported languages |
521
521
  | `--auto-next` | Toggle/set `workflow.auto_next` (bare toggles; or on/off) |
522
522
  | `--auto-distill` | Toggle/set `workflow.auto_distill` (default on; bare toggles; or on/off) |
523
+ | `--worktrees` | Toggle/set checkout-local worktree isolation (default on; bare toggles; or on/off) |
523
524
 
524
525
  </details>
525
526
 
@@ -792,3 +793,10 @@ MIT License - see [LICENSE](LICENSE) for details.
792
793
 
793
794
  - Inspired by [GitHub spec-kit](https://github.com/github/spec-kit)
794
795
  - Built for [Claude Code](https://claude.ai/code)
796
+
797
+ Ordinary features develop in separate worktrees under the main checkout's sibling
798
+ `<repository-name>-codexspec-worktrees/`, starting before the first requirements write.
799
+ Blueprint and auto-dev retain their fixed shared worktree. Standalone project edits
800
+ use a maintenance worktree; settings take effect per checkout and reach other checkouts
801
+ through Git integration. See [worktree development](docs/en/user-guide/workflow.md#worktree-development)
802
+ and [configuration](docs/en/reference/configuration.md#workflowworktrees).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codexspec"
3
- version = "0.7.16"
3
+ version = "0.7.18"
4
4
  description = "CodexSpec - A Requirements-First SDD toolkit for Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -50,6 +50,17 @@ if [ -z "$FEATURE_SUFFIX" ]; then
50
50
  exit 1
51
51
  fi
52
52
 
53
+ # Resolve the switch before creating directories or changing Git state.
54
+ if ! command_exists codexspec; then
55
+ log_error "CodexSpec runtime is unavailable. Install codexspec before creating a feature."
56
+ exit 1
57
+ fi
58
+ WORKTREE_SETTING=$(codexspec _worktree-helper setting) || exit $?
59
+ case "$WORKTREE_SETTING" in
60
+ *'"enabled": false'*) ;; # Explicit opt-out retains the original in-place flow below.
61
+ *) exec codexspec _worktree-helper create --name "$FEATURE_SUFFIX" ;;
62
+ esac
63
+
53
64
  TIMESTAMP=$(date +"%Y-%m%d-%H%M")
54
65
  RANDOM_SUFFIX=$(LC_ALL=C tr -dc 'a-z0-9' </dev/urandom | head -c 2)
55
66
  FEATURE_ID="${TIMESTAMP}${RANDOM_SUFFIX}"
@@ -132,6 +132,34 @@ if (-not $branchSuffix) {
132
132
  exit 1
133
133
  }
134
134
 
135
+ # Resolve isolation before any feature directory or branch mutation.
136
+ if (-not (Get-Command codexspec -ErrorAction SilentlyContinue)) {
137
+ Write-Error "CodexSpec runtime is unavailable. Install codexspec before creating a feature."
138
+ exit 1
139
+ }
140
+ $settingOutput = & codexspec _worktree-helper setting
141
+ if ($LASTEXITCODE -ne 0) { Write-Output $settingOutput; exit $LASTEXITCODE }
142
+ $setting = $settingOutput | ConvertFrom-Json
143
+ if ($setting.enabled) {
144
+ $workspaceOutput = & codexspec _worktree-helper create --name $branchSuffix
145
+ $workspaceExit = $LASTEXITCODE
146
+ if ($workspaceExit -ne 0) { Write-Output $workspaceOutput; exit $workspaceExit }
147
+ $workspace = $workspaceOutput | ConvertFrom-Json
148
+ $env:CODEXSPEC_FEATURE = $workspace.branch
149
+ $result = [PSCustomObject]@{
150
+ BRANCH_NAME = $workspace.branch
151
+ REQUIREMENTS_FILE = $workspace.requirements_file
152
+ FEATURE_ID = $workspace.feature_id
153
+ WORKTREE_PATH = $workspace.workspace
154
+ HAS_GIT = $true
155
+ STATUS = $workspace.status
156
+ WARNINGS = $workspace.warnings
157
+ }
158
+ if ($Json) { $result | ConvertTo-Json -Compress }
159
+ else { $result | Format-List | Out-String | Write-Output }
160
+ exit 0
161
+ }
162
+
135
163
  $specsDir = Join-Path $repoRoot '.codexspec/specs'
136
164
  New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
137
165
 
@@ -8,11 +8,13 @@ executable specifications that guide AI-assisted implementation.
8
8
 
9
9
  import json
10
10
  import re
11
+ import secrets
11
12
  import subprocess
12
13
  import sys
14
+ import webbrowser
13
15
  from datetime import datetime
14
16
  from pathlib import Path
15
- from typing import Optional
17
+ from typing import Any, Optional
16
18
 
17
19
  import typer
18
20
  from rich.console import Console
@@ -43,6 +45,12 @@ from .commands.installer import (
43
45
  should_update_commands,
44
46
  update_installed_command_frontmatter,
45
47
  )
48
+ from .distill_review.domain import ReviewError, ReviewService
49
+ from .distill_review.lease import ActiveSessionError, ReviewLease
50
+ from .distill_review.server import create_server
51
+ from .distill_review.session import SessionError, SessionStore, ensure_runtime_ignored
52
+ from .distill_review.terminal import localized_error, localized_result, run_text_review, safe_terminal
53
+ from .distill_review.transaction import ProfileTransaction, TransactionError
46
54
  from .i18n import (
47
55
  generate_config_content,
48
56
  get_explicit_language_key,
@@ -56,9 +64,18 @@ from .i18n import (
56
64
  from .integrations import get_integrations
57
65
  from .profile import ensure_profile_scaffold, inject_profile_block
58
66
  from .translator import SUPPORTED_LANGUAGES, translate
67
+ from .worktrees import (
68
+ MAINTENANCE_NAME,
69
+ WorkspaceManager,
70
+ feature_name,
71
+ read_worktrees,
72
+ validate_write_path,
73
+ write_destination,
74
+ write_worktrees,
75
+ )
59
76
 
60
77
  # Version info
61
- __version__ = "0.7.16"
78
+ __version__ = "0.7.18"
62
79
  __author__ = "CodexSpec Team"
63
80
 
64
81
  # Constitution file path constants
@@ -74,6 +91,153 @@ app = typer.Typer(
74
91
  console = Console()
75
92
 
76
93
 
94
+ @app.command("_worktree-helper", hidden=True)
95
+ def worktree_helper(
96
+ action: str = typer.Argument(...),
97
+ name: Optional[str] = typer.Option(None, "--name"),
98
+ feature: Optional[str] = typer.Option(None, "--feature"),
99
+ verification: Optional[Path] = typer.Option(None, "--verification"),
100
+ ) -> None:
101
+ """Return structured workspace facts; never emit shell code for callers to evaluate."""
102
+ recovery: dict[str, Any] = {}
103
+ result: dict[str, Any]
104
+ try:
105
+ from .worktrees import checkout_root
106
+
107
+ root = checkout_root(Path.cwd())
108
+ if action == "setting":
109
+ result = {"enabled": read_worktrees(root / ".codexspec/config.yml")}
110
+ elif action == "route":
111
+ result = {"status": "ready", "workspace": str(write_destination(root))}
112
+ elif action == "create" and not read_worktrees(root / ".codexspec/config.yml"):
113
+ result = {"status": "disabled", "workspace": str(root.resolve())}
114
+ else:
115
+ manager = WorkspaceManager(root)
116
+ if action == "create":
117
+ if bool(name) == bool(feature):
118
+ raise AutomationError("expected_name_or_feature")
119
+ identity = feature if feature else feature_name(name or "")
120
+ branch, path, _ = manager._identity(identity)
121
+ recovery = {"branch": branch, "workspace": str(path)}
122
+ result = manager.create(identity, reuse=feature is not None)
123
+ elif action == "resolve" and feature:
124
+ result = manager.resolve(feature)
125
+ elif action == "maintenance":
126
+ result = manager.create(MAINTENANCE_NAME)
127
+ elif action == "finish" and feature and verification:
128
+ result = manager.finish(feature, json.loads(verification.read_text(encoding="utf-8")))
129
+ else:
130
+ raise AutomationError("invalid_workspace_action", action)
131
+ print(json.dumps(result, ensure_ascii=False))
132
+ if result.get("status") in {"creating", "merge_requires_resolution", "merge_requires_verification"}:
133
+ raise typer.Exit(3)
134
+ except (AutomationError, OSError, ValueError) as exc:
135
+ print(json.dumps({**recovery, "status": "blocked", "error": str(exc)}, ensure_ascii=False))
136
+ raise typer.Exit(1) from exc
137
+
138
+
139
+ @app.command("_distill-review-helper", hidden=True)
140
+ def distill_review_helper(
141
+ project_root: Path = typer.Option(Path("."), "--project-root"),
142
+ mode: str = typer.Option("html", "--mode"),
143
+ manifest: Optional[Path] = typer.Option(None, "--manifest"),
144
+ no_open: bool = typer.Option(False, "--no-open"),
145
+ discard_draft: bool = typer.Option(False, "--discard-draft"),
146
+ ) -> None:
147
+ """Run the deterministic local carrier used by the distill command."""
148
+ root = project_root.resolve()
149
+ language = get_interaction_language(root / ".codexspec" / "config.yml")
150
+ if mode not in {"html", "text"}:
151
+ _review_result(language, {"status": "invalid_input", "error": "invalid_mode"})
152
+ raise typer.Exit(2)
153
+ if not root.is_dir() or not (root / ".codexspec" / "profile").is_dir():
154
+ _review_result(language, {"status": "invalid_input", "error": "invalid_project_root"})
155
+ raise typer.Exit(2)
156
+ try:
157
+ root = write_destination(root)
158
+ except (AutomationError, OSError) as exc:
159
+ _review_result(language, {"status": "invalid_input", "error": str(exc)})
160
+ raise typer.Exit(2) from exc
161
+ language = get_interaction_language(root / ".codexspec" / "config.yml")
162
+ if not (root / ".codexspec" / "profile").is_dir():
163
+ _review_result(
164
+ language, {"status": "invalid_input", "error": "missing_destination_profile", "project_root": str(root)}
165
+ )
166
+ raise typer.Exit(2)
167
+ store = SessionStore(root)
168
+ lease = ReviewLease(root)
169
+ try:
170
+ lease.acquire({"state": "starting", "mode": mode})
171
+ except ActiveSessionError as exc:
172
+ # Machine output reports the running session, but its capability token and
173
+ # token-bearing URL belong to the session that printed them.
174
+ session = {key: value for key, value in exc.metadata.items() if key not in {"capability", "url"}}
175
+ _review_result(language, {"status": "active_session", "session": session})
176
+ return
177
+ except (AutomationError, SessionError, OSError) as exc:
178
+ _review_result(language, {"status": "invalid_input", "error": str(exc)})
179
+ raise typer.Exit(2) from exc
180
+ server = None
181
+ try:
182
+ try:
183
+ ProfileTransaction.recover(root, store)
184
+ except (TransactionError, SessionError, OSError):
185
+ if not discard_draft:
186
+ raise
187
+ # A failed recovery must not brick the documented escape hatch: an
188
+ # explicit discard abandons the draft and the stuck journal together.
189
+ if discard_draft:
190
+ store.discard()
191
+ store.discard_transaction()
192
+ _review_result(language, {"status": "discarded"})
193
+ return
194
+ draft = store.load()
195
+ service = ReviewService.from_project(
196
+ root,
197
+ manifest_path=manifest,
198
+ draft=draft,
199
+ interaction_language=language,
200
+ )
201
+ except (ReviewError, SessionError, TransactionError, OSError) as exc:
202
+ _review_result(language, {"status": "invalid_input", "error": str(exc)})
203
+ raise typer.Exit(2) from exc
204
+ if not service.records and not service.clusters and not (service.draft.decisions or service.draft.deferred):
205
+ _review_result(language, {"status": "nothing_to_review"})
206
+ return
207
+ try:
208
+ if mode == "text":
209
+ lease.update({"mode": "text"})
210
+ result = run_text_review(service, store)
211
+ typer.echo()
212
+ _machine_json(result)
213
+ return
214
+ token = secrets.token_urlsafe(32)
215
+ server = create_server(service, store, token=token)
216
+ url = f"http://127.0.0.1:{server.server_port}/#token={token}"
217
+ lease.update({"mode": "html", "port": server.server_port, "capability": token, "url": url})
218
+ try:
219
+ opened = False if no_open else webbrowser.open(url)
220
+ except (OSError, webbrowser.Error):
221
+ opened = False
222
+ if not opened:
223
+ typer.echo(url)
224
+ server.timeout = 0.5
225
+ while not server.stop_event.is_set():
226
+ server.handle_request()
227
+ _review_result(language, server.final_result or {"status": "finished"})
228
+ except (ReviewError, SessionError, TransactionError, OSError) as exc:
229
+ failure: dict[str, object] = {"status": "invalid_input", "error": str(exc)}
230
+ if isinstance(exc, TransactionError):
231
+ failure["records"] = exc.records
232
+ failure["failures"] = exc.failures
233
+ _review_result(language, failure)
234
+ raise typer.Exit(2) from exc
235
+ finally:
236
+ if server is not None:
237
+ server.server_close()
238
+ lease.release()
239
+
240
+
77
241
  def get_version() -> str:
78
242
  """Return the current version of codexspec."""
79
243
  return __version__
@@ -192,6 +356,25 @@ def _machine_json(value: object) -> None:
192
356
  sys.stdout.write(json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n")
193
357
 
194
358
 
359
+ def _review_result(language: str, value: dict[str, object]) -> None:
360
+ status = str(value.get("status", "invalid_input"))
361
+ if status in {"invalid_input", "conflict"}:
362
+ raw_records = value.get("records")
363
+ raw_failures = value.get("failures")
364
+ records = [str(item) for item in raw_records] if isinstance(raw_records, list) else None
365
+ failures = [str(item) for item in raw_failures] if isinstance(raw_failures, list) else None
366
+ message = localized_error(
367
+ language,
368
+ str(value.get("error", status)),
369
+ records=records,
370
+ failures=failures,
371
+ )
372
+ else:
373
+ message = localized_result(language, status)
374
+ typer.echo(safe_terminal(message))
375
+ _machine_json(value)
376
+
377
+
195
378
  def _stdin_json() -> dict:
196
379
  try:
197
380
  value = json.loads(sys.stdin.read() or "{}")
@@ -408,6 +591,9 @@ def config(
408
591
  "--auto-distill",
409
592
  help="Toggle workflow.auto_distill (bare), or set it (on/off|true/false|1/0|yes/no).",
410
593
  ),
594
+ worktrees: Optional[str] = typer.Option(
595
+ None, "--worktrees", help="Toggle checkout-local worktree isolation (default on), or set on/off."
596
+ ),
411
597
  ) -> None:
412
598
  """
413
599
  View or modify CodexSpec project configuration.
@@ -443,6 +629,58 @@ def config(
443
629
  console.print("Run [cyan]codexspec init[/cyan] to create a new project.")
444
630
  raise typer.Exit(1)
445
631
 
632
+ # Validate values before creating any destination workspace.
633
+ for value, sentinel, option in (
634
+ (auto_next, _AUTO_NEXT_SENTINEL, "--auto-next"),
635
+ (auto_distill, _AUTO_DISTILL_SENTINEL, "--auto-distill"),
636
+ (worktrees, "__toggle_worktrees__", "--worktrees"),
637
+ ):
638
+ if value is not None and value != sentinel:
639
+ try:
640
+ parse_auto_next_value(value)
641
+ except ValueError:
642
+ console.print(f"[red]Invalid {option} value:[/red] {value!r}")
643
+ raise typer.Exit(1)
644
+ modifying = any(
645
+ value is not None
646
+ for value in (
647
+ auto_next,
648
+ auto_distill,
649
+ worktrees,
650
+ set_lang,
651
+ set_commit_lang,
652
+ set_interaction_lang,
653
+ set_document_lang,
654
+ )
655
+ )
656
+ if modifying:
657
+ try:
658
+ isolated = read_worktrees(config_file)
659
+ destination = write_destination(config_file.parent.parent)
660
+ config_file = destination / ".codexspec" / "config.yml"
661
+ if isolated:
662
+ validate_write_path(destination, config_file)
663
+ if set_lang is not None or set_interaction_lang is not None:
664
+ _validate_command_frontmatter(destination)
665
+ if not config_file.is_file():
666
+ raise AutomationError("missing_destination_config", str(config_file))
667
+ console.print(f"Configuration destination: {config_file}", markup=False, soft_wrap=True)
668
+ console.print("Changes apply to this checkout; other checkouts receive them through Git integration.")
669
+ except (AutomationError, OSError) as exc:
670
+ console.print(str(exc), markup=False)
671
+ raise typer.Exit(1) from exc
672
+ if worktrees is not None:
673
+ target = (
674
+ not read_worktrees(config_file) if worktrees == "__toggle_worktrees__" else parse_auto_next_value(worktrees)
675
+ )
676
+ try:
677
+ write_worktrees(config_file, target)
678
+ except (AutomationError, OSError) as exc:
679
+ console.print(str(exc), markup=False)
680
+ raise typer.Exit(1) from exc
681
+ console.print(f"workflow.worktrees = {str(target).lower()}")
682
+ return
683
+
446
684
  # Handle auto-next toggle/set
447
685
  if auto_next is not None:
448
686
  if auto_next == _AUTO_NEXT_SENTINEL:
@@ -622,6 +860,7 @@ def config(
622
860
  return
623
861
 
624
862
  # Display current configuration
863
+ console.print(f"Effective workflow.worktrees: {str(read_worktrees(config_file)).lower()}")
625
864
  console.print(
626
865
  Panel(
627
866
  config_file.read_text(encoding="utf-8"),
@@ -885,6 +1124,7 @@ def init(
885
1124
  # so knowledge distilled later is effective immediately with no re-init and
886
1125
  # no dangling reference. Non-destructive: existing profile files are kept.
887
1126
  ensure_profile_scaffold(target_dir)
1127
+ ensure_runtime_ignored(target_dir, install_managed_file=True)
888
1128
 
889
1129
  # Copy helper scripts based on platform
890
1130
  scripts_source_dir = get_scripts_dir()
@@ -1109,6 +1349,15 @@ def init(
1109
1349
  console.print(f"[yellow]{_important_action(integrations, normalized_lang)}[/yellow]")
1110
1350
 
1111
1351
 
1352
+ def _validate_command_frontmatter(root: Path) -> None:
1353
+ commands = root / ".claude" / "commands" / COMMANDS_SUBDIR
1354
+ validate_write_path(root, commands)
1355
+ if commands.exists() and not commands.is_dir():
1356
+ raise AutomationError("unsafe_write_path", str(commands))
1357
+ for command in commands.glob("*.md"):
1358
+ validate_write_path(root, command)
1359
+
1360
+
1112
1361
  def _rerender_command_frontmatter(config_file: Path) -> None:
1113
1362
  """Re-render installed command frontmatter in the current interaction language.
1114
1363
 
@@ -1118,7 +1367,7 @@ def _rerender_command_frontmatter(config_file: Path) -> None:
1118
1367
  pre-translated cache are not re-rendered here (that path would invoke the
1119
1368
  ``claude`` CLI once per command); the user is advised to run ``codexspec init``.
1120
1369
  """
1121
- commands_subdir = Path.cwd() / ".claude" / "commands" / COMMANDS_SUBDIR
1370
+ commands_subdir = config_file.parent.parent / ".claude" / "commands" / COMMANDS_SUBDIR
1122
1371
  if not commands_subdir.exists():
1123
1372
  return
1124
1373
  language = get_interaction_language(config_file) # re-read AFTER the config write
@@ -1800,6 +2049,8 @@ def main() -> None:
1800
2049
  sys.argv = _normalize_auto_next_argv(sys.argv)
1801
2050
  if "--auto-distill" in sys.argv:
1802
2051
  sys.argv = _normalize_auto_distill_argv(sys.argv)
2052
+ if "--worktrees" in sys.argv:
2053
+ sys.argv = _normalize_optional_value_argv(sys.argv, "--worktrees", "__toggle_worktrees__")
1803
2054
  app()
1804
2055
 
1805
2056
 
@@ -173,8 +173,12 @@ def locate_repository(cwd: Path | str, *, runner: GitRunner | None = None) -> Re
173
173
  current = Path(cwd).resolve()
174
174
  top = git.run(current, "rev-parse", "--show-toplevel", check=False)
175
175
  if top.returncode != 0:
176
- raise AutomationError("not_git_repository")
177
- invoking_root = Path(top.stdout.strip()).resolve()
176
+ bare = git.run(current, "rev-parse", "--is-bare-repository", check=False)
177
+ if bare.returncode != 0 or bare.stdout.strip() != "true":
178
+ raise AutomationError("not_git_repository")
179
+ invoking_root = Path(git.run(current, "rev-parse", "--absolute-git-dir").stdout.strip()).resolve()
180
+ else:
181
+ invoking_root = Path(top.stdout.strip()).resolve()
178
182
  worktrees = _parse_worktrees(git.run(invoking_root, "worktree", "list", "--porcelain").stdout)
179
183
  if not worktrees:
180
184
  raise AutomationError("worktree_registry_empty")
@@ -188,7 +192,7 @@ def locate_repository(cwd: Path | str, *, runner: GitRunner | None = None) -> Re
188
192
  default_branch = remote_branch or _local_default_branch(git, primary_root, worktrees[0].get("branch"))
189
193
  if remote_name and remote_ref is None:
190
194
  remote_ref = f"refs/remotes/{remote_name}/{default_branch}"
191
- worktree_path = Path(f"{primary_root}-worktrees") / WORKTREE_BASENAME
195
+ worktree_path = Path(f"{primary_root}-codexspec-worktrees") / WORKTREE_BASENAME
192
196
  return RepositoryContext(
193
197
  repository_root=primary_root,
194
198
  common_git_dir=common_git_dir,
@@ -303,16 +307,22 @@ def _remote_default(git: GitRunner, root: Path) -> tuple[str | None, str | None,
303
307
  if symbolic.returncode != 0:
304
308
  return remote, None, None
305
309
  ref = symbolic.stdout.strip()
306
- return remote, ref, ref.rsplit("/", 1)[-1]
310
+ return remote, ref, ref.removeprefix(f"refs/remotes/{remote}/")
307
311
 
308
312
 
309
313
  def _local_default_branch(git: GitRunner, root: Path, primary_branch_ref: str | None) -> str:
310
314
  for candidate in ("main", "master"):
311
315
  if git.run(root, "show-ref", "--verify", "--quiet", f"refs/heads/{candidate}", check=False).returncode == 0:
312
316
  return candidate
313
- if primary_branch_ref and primary_branch_ref.startswith("refs/heads/"):
314
- return primary_branch_ref.removeprefix("refs/heads/")
315
- raise AutomationError("default_branch_not_found")
317
+ configured = git.run(root, "config", "--get", "init.defaultBranch", check=False).stdout.strip()
318
+ if (
319
+ configured
320
+ and git.run(root, "show-ref", "--verify", "--quiet", f"refs/heads/{configured}", check=False).returncode == 0
321
+ ):
322
+ return configured
323
+ raise AutomationError(
324
+ "default_branch_not_found", "Configure the remote default branch or a valid init.defaultBranch."
325
+ )
316
326
 
317
327
 
318
328
  def _parse_worktrees(output: str) -> list[dict[str, Any]]:
@@ -0,0 +1,13 @@
1
+ """Deterministic local review runtime for distilled profile knowledge."""
2
+
3
+ from .domain import ReviewError, ReviewService
4
+ from .records import RecordDocument, RecordError, discover_records, parse_record
5
+
6
+ __all__ = [
7
+ "RecordDocument",
8
+ "RecordError",
9
+ "ReviewError",
10
+ "ReviewService",
11
+ "discover_records",
12
+ "parse_record",
13
+ ]