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.
- {codexspec-0.7.16 → codexspec-0.7.18}/PKG-INFO +9 -1
- {codexspec-0.7.16 → codexspec-0.7.18}/README.md +8 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/pyproject.toml +1 -1
- {codexspec-0.7.16 → codexspec-0.7.18}/scripts/bash/create-new-feature.sh +11 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/scripts/powershell/create-new-feature.ps1 +28 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/__init__.py +254 -3
- {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/automation.py +17 -7
- codexspec-0.7.18/src/codexspec/distill_review/__init__.py +13 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/app.js +938 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/ar.json +112 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/de.json +112 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/en.json +112 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/es.json +112 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/fr.json +112 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/hi.json +112 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/it.json +112 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/ja.json +112 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/ko.json +112 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/pt.json +112 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/ru.json +112 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/zh-CN.json +112 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/i18n/zh-TW.json +112 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/index.html +43 -0
- codexspec-0.7.18/src/codexspec/distill_review/assets/styles.css +824 -0
- codexspec-0.7.18/src/codexspec/distill_review/domain.py +585 -0
- codexspec-0.7.18/src/codexspec/distill_review/lease.py +101 -0
- codexspec-0.7.18/src/codexspec/distill_review/models.py +133 -0
- codexspec-0.7.18/src/codexspec/distill_review/records.py +399 -0
- codexspec-0.7.18/src/codexspec/distill_review/server.py +386 -0
- codexspec-0.7.18/src/codexspec/distill_review/session.py +323 -0
- codexspec-0.7.18/src/codexspec/distill_review/terminal.py +259 -0
- codexspec-0.7.18/src/codexspec/distill_review/transaction.py +932 -0
- codexspec-0.7.18/src/codexspec/worktrees.py +477 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/analyze.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/auto-dev.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/blueprint.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/checklist.md +72 -0
- codexspec-0.7.18/templates/commands/clarify.md +159 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/commit-staged.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/config.md +83 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/constitution.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/debug.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/distill.md +120 -4
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/evolve.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/generate-spec.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/implement-tasks.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/onboard.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/plan-to-tasks.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/pr.md +72 -0
- codexspec-0.7.18/templates/commands/quick.md +141 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/release-notes.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/reverse-spec.md +79 -7
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/review-code.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/review-design.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/review-plan.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/review-spec.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/review-tasks.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/spec-to-design.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/spec-to-plan.md +72 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/commands/specify.md +76 -1
- codexspec-0.7.18/templates/commands/tasks-to-issues.md +216 -0
- codexspec-0.7.16/templates/commands/clarify.md +0 -87
- codexspec-0.7.16/templates/commands/quick.md +0 -69
- codexspec-0.7.16/templates/commands/tasks-to-issues.md +0 -144
- {codexspec-0.7.16 → codexspec-0.7.18}/.gitignore +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/LICENSE +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/codexspec-icon.svg +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/codexspec-logo-dark.svg +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/codexspec-logo-light.svg +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/scripts/bash/check-i18n-completeness.sh +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/scripts/bash/check-i18n-structure.sh +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/scripts/bash/check-prerequisites.sh +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/scripts/bash/common.sh +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/scripts/bash/review-context.sh +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/scripts/powershell/check-prerequisites.ps1 +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/scripts/powershell/common.ps1 +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/scripts/powershell/review-context.ps1 +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/blueprint.py +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/commands/__init__.py +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/commands/installer.py +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/i18n.py +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/idea.md +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/integrations/__init__.py +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/integrations/base.py +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/integrations/claude.py +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/integrations/codex.py +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/profile.py +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/src/codexspec/translator.py +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/checklist-template.md +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/constitution-template.md +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/design-template.md +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/plan-template-detailed.md +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/plan-template-simple.md +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/requirements-template.md +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/spec-template-detailed.md +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/spec-template-simple.md +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/tasks-template-detailed.md +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/docs/tasks-template-simple.md +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/translations/de.json +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/translations/en.json +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/translations/es.json +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/translations/fr.json +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/translations/ja.json +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/translations/ko.json +0 -0
- {codexspec-0.7.16 → codexspec-0.7.18}/templates/translations/pt-BR.json +0 -0
- {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.
|
|
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).
|
|
@@ -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.
|
|
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 =
|
|
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
|
-
|
|
177
|
-
|
|
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.
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
]
|