codexspec 0.6.5__tar.gz → 0.7.1__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.6.5 → codexspec-0.7.1}/PKG-INFO +9 -3
- {codexspec-0.6.5 → codexspec-0.7.1}/README.md +8 -2
- {codexspec-0.6.5 → codexspec-0.7.1}/pyproject.toml +1 -1
- {codexspec-0.6.5 → codexspec-0.7.1}/src/codexspec/__init__.py +162 -79
- codexspec-0.7.1/src/codexspec/integrations/__init__.py +20 -0
- codexspec-0.7.1/src/codexspec/integrations/base.py +20 -0
- codexspec-0.7.1/src/codexspec/integrations/claude.py +25 -0
- codexspec-0.7.1/src/codexspec/integrations/codex.py +170 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/plan-to-tasks.md +10 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/.gitignore +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/LICENSE +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/codexspec-icon.svg +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/codexspec-logo-dark.svg +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/codexspec-logo-light.svg +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/scripts/bash/check-i18n-completeness.sh +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/scripts/bash/check-i18n-structure.sh +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/scripts/bash/check-prerequisites.sh +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/scripts/bash/common.sh +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/scripts/bash/create-new-feature.sh +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/scripts/powershell/check-prerequisites.ps1 +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/scripts/powershell/common.ps1 +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/scripts/powershell/create-new-feature.ps1 +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/src/codexspec/commands/__init__.py +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/src/codexspec/commands/installer.py +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/src/codexspec/i18n.py +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/src/codexspec/idea.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/src/codexspec/translator.py +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/analyze.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/checklist.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/clarify.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/commit-staged.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/config.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/constitution.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/generate-spec.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/implement-tasks.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/pr.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/quick.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/review-code.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/review-plan.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/review-spec.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/review-tasks.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/spec-to-plan.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/specify.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/tasks-to-issues.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/checklist-template.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/constitution-template.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/plan-template-detailed.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/plan-template-simple.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/requirements-template.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/spec-template-detailed.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/spec-template-simple.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/tasks-template-detailed.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/tasks-template-simple.md +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/translations/de.json +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/translations/en.json +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/translations/es.json +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/translations/fr.json +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/translations/ja.json +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/translations/ko.json +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/translations/pt-BR.json +0 -0
- {codexspec-0.6.5 → codexspec-0.7.1}/templates/translations/zh-CN.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codexspec
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
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
|
|
@@ -251,6 +251,9 @@ uvx codexspec init my-project
|
|
|
251
251
|
# Initialize in existing project
|
|
252
252
|
cd your-existing-project
|
|
253
253
|
uvx codexspec init . --ai claude
|
|
254
|
+
|
|
255
|
+
# Initialize for Codex CLI
|
|
256
|
+
uvx codexspec init . --ai codex
|
|
254
257
|
```
|
|
255
258
|
|
|
256
259
|
#### Install Development Version from GitHub
|
|
@@ -530,7 +533,7 @@ Implementation follows **conditional TDD workflow**:
|
|
|
530
533
|
| --------------- | ------------------------------------- |
|
|
531
534
|
| `PROJECT_NAME` | Project directory name (`.` or `--here` for current dir) |
|
|
532
535
|
| `--here`, `-h` | Initialize in the current directory |
|
|
533
|
-
| `--ai`, `-a` | AI assistant to use (default: claude) |
|
|
536
|
+
| `--ai`, `-a` | AI assistant to use: `claude`, `codex`, or `both` (default: claude) |
|
|
534
537
|
| `--lang`, `-l` | Output (base) language; interaction/document/commit fall back to it (e.g., en, zh-CN, ja) |
|
|
535
538
|
| `--interaction-lang` | Interaction language (LLM dialogue + CLI output); overrides `--lang` |
|
|
536
539
|
| `--document-lang` | Document language (generated spec/plan/tasks); overrides `--lang` |
|
|
@@ -738,7 +741,10 @@ my-project/
|
|
|
738
741
|
│ └── extensions/ # Custom extensions
|
|
739
742
|
├── .claude/
|
|
740
743
|
│ └── commands/ # Claude Code slash commands
|
|
741
|
-
|
|
744
|
+
├── .agents/
|
|
745
|
+
│ └── skills/ # Codex skills (when initialized with --ai codex or both)
|
|
746
|
+
├── CLAUDE.md # Claude Code context
|
|
747
|
+
└── AGENTS.md # Codex context
|
|
742
748
|
```
|
|
743
749
|
|
|
744
750
|
---
|
|
@@ -206,6 +206,9 @@ uvx codexspec init my-project
|
|
|
206
206
|
# Initialize in existing project
|
|
207
207
|
cd your-existing-project
|
|
208
208
|
uvx codexspec init . --ai claude
|
|
209
|
+
|
|
210
|
+
# Initialize for Codex CLI
|
|
211
|
+
uvx codexspec init . --ai codex
|
|
209
212
|
```
|
|
210
213
|
|
|
211
214
|
#### Install Development Version from GitHub
|
|
@@ -485,7 +488,7 @@ Implementation follows **conditional TDD workflow**:
|
|
|
485
488
|
| --------------- | ------------------------------------- |
|
|
486
489
|
| `PROJECT_NAME` | Project directory name (`.` or `--here` for current dir) |
|
|
487
490
|
| `--here`, `-h` | Initialize in the current directory |
|
|
488
|
-
| `--ai`, `-a` | AI assistant to use (default: claude) |
|
|
491
|
+
| `--ai`, `-a` | AI assistant to use: `claude`, `codex`, or `both` (default: claude) |
|
|
489
492
|
| `--lang`, `-l` | Output (base) language; interaction/document/commit fall back to it (e.g., en, zh-CN, ja) |
|
|
490
493
|
| `--interaction-lang` | Interaction language (LLM dialogue + CLI output); overrides `--lang` |
|
|
491
494
|
| `--document-lang` | Document language (generated spec/plan/tasks); overrides `--lang` |
|
|
@@ -693,7 +696,10 @@ my-project/
|
|
|
693
696
|
│ └── extensions/ # Custom extensions
|
|
694
697
|
├── .claude/
|
|
695
698
|
│ └── commands/ # Claude Code slash commands
|
|
696
|
-
|
|
699
|
+
├── .agents/
|
|
700
|
+
│ └── skills/ # Codex skills (when initialized with --ai codex or both)
|
|
701
|
+
├── CLAUDE.md # Claude Code context
|
|
702
|
+
└── AGENTS.md # Codex context
|
|
697
703
|
```
|
|
698
704
|
|
|
699
705
|
---
|
|
@@ -6,6 +6,7 @@ This toolkit provides a structured approach to software development using
|
|
|
6
6
|
executable specifications that guide AI-assisted implementation.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import re
|
|
9
10
|
import subprocess
|
|
10
11
|
import sys
|
|
11
12
|
from datetime import datetime
|
|
@@ -38,10 +39,11 @@ from .i18n import (
|
|
|
38
39
|
normalize_locale,
|
|
39
40
|
update_language_field,
|
|
40
41
|
)
|
|
42
|
+
from .integrations import get_integrations
|
|
41
43
|
from .translator import SUPPORTED_LANGUAGES, translate
|
|
42
44
|
|
|
43
45
|
# Version info
|
|
44
|
-
__version__ = "0.
|
|
46
|
+
__version__ = "0.7.0"
|
|
45
47
|
__author__ = "CodexSpec Team"
|
|
46
48
|
|
|
47
49
|
# Constitution file path constants
|
|
@@ -512,6 +514,14 @@ def init(
|
|
|
512
514
|
interaction_lang = interaction_lang if isinstance(interaction_lang, str) else None
|
|
513
515
|
document_lang = document_lang if isinstance(document_lang, str) else None
|
|
514
516
|
commit_lang = commit_lang if isinstance(commit_lang, str) else None
|
|
517
|
+
ai = ai if isinstance(ai, str) else "claude"
|
|
518
|
+
ai = ai.lower().strip()
|
|
519
|
+
try:
|
|
520
|
+
integrations = get_integrations(ai)
|
|
521
|
+
except ValueError as exc:
|
|
522
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
523
|
+
raise typer.Exit(1) from exc
|
|
524
|
+
integration_keys = {integration.key for integration in integrations}
|
|
515
525
|
|
|
516
526
|
# Determine target directory first — needed to detect an existing config.yml
|
|
517
527
|
# before resolving the language base (REQ-007).
|
|
@@ -661,65 +671,74 @@ def init(
|
|
|
661
671
|
else:
|
|
662
672
|
console.print("[yellow]Warning: Docs templates directory not found[/yellow]")
|
|
663
673
|
|
|
664
|
-
# Create .claude/commands directory for slash commands
|
|
665
|
-
claude_dir = target_dir / ".claude"
|
|
666
|
-
claude_commands_dir = claude_dir / "commands"
|
|
667
|
-
claude_commands_dir.mkdir(parents=True, exist_ok=True)
|
|
668
|
-
|
|
669
|
-
# Target subdirectory for CodexSpec commands
|
|
670
|
-
codexspec_commands_dir = claude_commands_dir / COMMANDS_SUBDIR
|
|
671
|
-
|
|
672
674
|
# Get templates directory
|
|
673
675
|
templates_dir = get_templates_dir() / "commands"
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
migration_happened = False
|
|
678
|
-
if old_files:
|
|
679
|
-
console.print()
|
|
676
|
+
if not templates_dir.exists():
|
|
677
|
+
# Templates directory is part of the wheel; if it's missing the
|
|
678
|
+
# install is broken and we cannot recover.
|
|
680
679
|
console.print(
|
|
681
|
-
f"[
|
|
680
|
+
f"[red]{translate('cli.init.error_templates_missing', normalized_lang, path=str(templates_dir))}[/red]"
|
|
682
681
|
)
|
|
683
|
-
|
|
684
|
-
console.print(f"[dim]{translate('cli.init.migration_new_structure', normalized_lang)}[/dim]")
|
|
682
|
+
raise typer.Exit(1)
|
|
685
683
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
if
|
|
699
|
-
|
|
700
|
-
codexspec_commands_dir, templates_dir, force=True, language=normalized_lang
|
|
701
|
-
)
|
|
702
|
-
console.print(f"[green]{translate('cli.init.commands_updated', normalized_lang, count=count)}[/green]")
|
|
703
|
-
elif should_update_commands(codexspec_dir):
|
|
704
|
-
console.print()
|
|
705
|
-
if force or Confirm.ask(translate("cli.init.update_confirm", normalized_lang), default=True):
|
|
706
|
-
count = install_commands_to_subdir(
|
|
707
|
-
codexspec_commands_dir, templates_dir, force=True, language=normalized_lang
|
|
708
|
-
)
|
|
709
|
-
console.print(f"[green]{translate('cli.init.commands_updated', normalized_lang, count=count)}[/green]")
|
|
710
|
-
else:
|
|
711
|
-
if not templates_dir.exists():
|
|
712
|
-
# Templates directory is part of the wheel; if it's missing the
|
|
713
|
-
# install is broken and we cannot recover.
|
|
684
|
+
if "claude" in integration_keys:
|
|
685
|
+
# Create .claude/commands directory for slash commands
|
|
686
|
+
claude_dir = target_dir / ".claude"
|
|
687
|
+
claude_commands_dir = claude_dir / "commands"
|
|
688
|
+
claude_commands_dir.mkdir(parents=True, exist_ok=True)
|
|
689
|
+
|
|
690
|
+
# Target subdirectory for CodexSpec commands
|
|
691
|
+
codexspec_commands_dir = claude_commands_dir / COMMANDS_SUBDIR
|
|
692
|
+
|
|
693
|
+
# Check for old structure and migrate if needed
|
|
694
|
+
old_files = detect_old_structure(claude_dir)
|
|
695
|
+
migration_happened = False
|
|
696
|
+
if old_files:
|
|
697
|
+
console.print()
|
|
714
698
|
console.print(
|
|
715
|
-
f"[
|
|
699
|
+
f"[yellow]{translate('cli.init.migration_found', normalized_lang, count=len(old_files))}[/yellow]"
|
|
716
700
|
)
|
|
717
|
-
|
|
701
|
+
console.print(f"[dim]{translate('cli.init.migration_old_structure', normalized_lang)}[/dim]")
|
|
702
|
+
console.print(f"[dim]{translate('cli.init.migration_new_structure', normalized_lang)}[/dim]")
|
|
703
|
+
|
|
704
|
+
if force or Confirm.ask(translate("cli.init.migration_confirm", normalized_lang), default=True):
|
|
705
|
+
if migrate_old_commands(claude_dir, old_files):
|
|
706
|
+
console.print(f"[green]{translate('cli.init.migration_complete', normalized_lang)}[/green]")
|
|
707
|
+
migration_happened = True
|
|
708
|
+
else:
|
|
709
|
+
console.print(f"[red]{translate('cli.init.migration_failed', normalized_lang)}[/red]")
|
|
710
|
+
else:
|
|
711
|
+
console.print(f"[dim]{translate('cli.init.migration_skipped', normalized_lang)}[/dim]")
|
|
712
|
+
|
|
713
|
+
# Install or update commands (translate after migration if user wants)
|
|
714
|
+
if migration_happened:
|
|
715
|
+
# Migration moved files, ask if user wants to update/translate them
|
|
716
|
+
if force or Confirm.ask(translate("cli.init.update_confirm", normalized_lang), default=True):
|
|
717
|
+
count = install_commands_to_subdir(
|
|
718
|
+
codexspec_commands_dir, templates_dir, force=True, language=normalized_lang
|
|
719
|
+
)
|
|
720
|
+
console.print(f"[green]{translate('cli.init.commands_updated', normalized_lang, count=count)}[/green]")
|
|
721
|
+
elif should_update_commands(codexspec_dir):
|
|
722
|
+
console.print()
|
|
723
|
+
if force or Confirm.ask(translate("cli.init.update_confirm", normalized_lang), default=True):
|
|
724
|
+
count = install_commands_to_subdir(
|
|
725
|
+
codexspec_commands_dir, templates_dir, force=True, language=normalized_lang
|
|
726
|
+
)
|
|
727
|
+
console.print(f"[green]{translate('cli.init.commands_updated', normalized_lang, count=count)}[/green]")
|
|
728
|
+
else:
|
|
729
|
+
count = install_commands_to_subdir(codexspec_commands_dir, templates_dir, language=normalized_lang)
|
|
730
|
+
cmd_path = f".claude/commands/{COMMANDS_SUBDIR}/"
|
|
731
|
+
msg = translate("cli.init.commands_installed", normalized_lang, count=count, path=cmd_path)
|
|
732
|
+
console.print(f"[green]{msg}[/green]")
|
|
718
733
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
734
|
+
for integration in integrations:
|
|
735
|
+
if integration.key == "claude":
|
|
736
|
+
continue
|
|
737
|
+
count = integration.install(target_dir, templates_dir, force=force, language=normalized_lang)
|
|
738
|
+
console.print(
|
|
739
|
+
f"[green]{translate('cli.init.commands_installed', normalized_lang, count=count, path='.agents/skills/')}"
|
|
740
|
+
"[/green]"
|
|
741
|
+
)
|
|
723
742
|
|
|
724
743
|
# Create constitution template
|
|
725
744
|
constitution_file = codexspec_dir / "memory" / "constitution.md"
|
|
@@ -749,6 +768,7 @@ def init(
|
|
|
749
768
|
commit=keys_to_write.get("commit"),
|
|
750
769
|
created=datetime.now().strftime("%Y-%m-%d"),
|
|
751
770
|
)
|
|
771
|
+
config_content = _set_config_ai_content(config_content, ai)
|
|
752
772
|
config_file.write_text(config_content, encoding="utf-8")
|
|
753
773
|
wrote_keys = list(keys_to_write.keys())
|
|
754
774
|
lang_name = get_language_name(normalized_lang)
|
|
@@ -763,6 +783,8 @@ def init(
|
|
|
763
783
|
for key, value in keys_to_write.items():
|
|
764
784
|
if update_language_field(config_file, key, value):
|
|
765
785
|
wrote_keys.append(key)
|
|
786
|
+
if config_file.exists():
|
|
787
|
+
_update_project_ai(config_file, ai)
|
|
766
788
|
|
|
767
789
|
# Non-blocking notice of which language keys were set (REQ-010).
|
|
768
790
|
if wrote_keys:
|
|
@@ -773,22 +795,23 @@ def init(
|
|
|
773
795
|
if prompted_base:
|
|
774
796
|
console.print(f"[dim]{translate('cli.init.language_dimensions_hint', normalized_lang)}[/dim]")
|
|
775
797
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
if
|
|
790
|
-
|
|
791
|
-
|
|
798
|
+
if "claude" in integration_keys:
|
|
799
|
+
# Create CLAUDE.md
|
|
800
|
+
# CLAUDE.md is user-authored content (like the constitution), not a
|
|
801
|
+
# regenerable artifact (like commands/scripts). Never overwrite an existing
|
|
802
|
+
# body -- even under --force. --force only auto-confirms the one safe,
|
|
803
|
+
# idempotent change: prepending the constitution @import if it is missing.
|
|
804
|
+
claude_md = target_dir / "CLAUDE.md"
|
|
805
|
+
if not claude_md.exists():
|
|
806
|
+
project_name = target_dir.name
|
|
807
|
+
claude_md.write_text(_get_claude_md_content(project_name), encoding="utf-8")
|
|
808
|
+
console.print(f"[green]{translate('cli.init.created_file', normalized_lang, file='CLAUDE.md')}[/green]")
|
|
809
|
+
else:
|
|
810
|
+
# Existing CLAUDE.md: ensure the compliance @import, never clobber the body.
|
|
811
|
+
if not has_compliance_section(claude_md):
|
|
812
|
+
if force or confirm_add_compliance(normalized_lang):
|
|
813
|
+
prepend_compliance_section(claude_md)
|
|
814
|
+
console.print(f"[green]{translate('cli.init.compliance_added', normalized_lang)}[/green]")
|
|
792
815
|
|
|
793
816
|
# Initialize git if requested
|
|
794
817
|
if not no_git and not (target_dir / ".git").exists():
|
|
@@ -800,7 +823,7 @@ def init(
|
|
|
800
823
|
|
|
801
824
|
# Print success message with command summary
|
|
802
825
|
console.print()
|
|
803
|
-
_print_command_summary(normalized_lang)
|
|
826
|
+
_print_command_summary(normalized_lang, ai=ai)
|
|
804
827
|
|
|
805
828
|
project_nav = project_name if project_name and project_name != "." else "."
|
|
806
829
|
console.print(
|
|
@@ -809,9 +832,9 @@ def init(
|
|
|
809
832
|
f"{translate('cli.init.success_project_dir', normalized_lang, path=str(target_dir))}\n\n"
|
|
810
833
|
f"[bold]{translate('cli.init.next_steps', normalized_lang)}[/bold]\n"
|
|
811
834
|
f"1. {translate('cli.init.next_step_navigate', normalized_lang, path=project_nav)}\n"
|
|
812
|
-
f"2. {
|
|
813
|
-
f"3. {
|
|
814
|
-
f"4. {
|
|
835
|
+
f"2. {_next_step_start(integration_keys, normalized_lang)}\n"
|
|
836
|
+
f"3. {_next_step_invocation(integrations, 'constitution')}\n"
|
|
837
|
+
f"4. {_next_step_invocation(integrations, 'specify')}",
|
|
815
838
|
title=translate("cli.init.success_title", normalized_lang),
|
|
816
839
|
)
|
|
817
840
|
)
|
|
@@ -819,7 +842,7 @@ def init(
|
|
|
819
842
|
# Git management tip
|
|
820
843
|
console.print()
|
|
821
844
|
console.print(f"[bold]{translate('cli.init.tips_header', normalized_lang)}[/bold]")
|
|
822
|
-
console.print(f" - {
|
|
845
|
+
console.print(f" - {_git_tip(integration_keys, normalized_lang)}")
|
|
823
846
|
console.print(f" - {translate('cli.init.tips_list_commands', normalized_lang)}")
|
|
824
847
|
console.print(f" - {translate('cli.init.tips_edit', normalized_lang)}")
|
|
825
848
|
|
|
@@ -828,7 +851,7 @@ def init(
|
|
|
828
851
|
important_header = translate("cli.init.important_header", normalized_lang)
|
|
829
852
|
important_msg = translate("cli.init.important_message", normalized_lang)
|
|
830
853
|
console.print(f"[yellow]{important_header}[/yellow] {important_msg}")
|
|
831
|
-
console.print(f"[yellow]{
|
|
854
|
+
console.print(f"[yellow]{_important_action(integrations, normalized_lang)}[/yellow]")
|
|
832
855
|
|
|
833
856
|
|
|
834
857
|
def _rerender_command_frontmatter(config_file: Path) -> None:
|
|
@@ -854,9 +877,65 @@ def _rerender_command_frontmatter(config_file: Path) -> None:
|
|
|
854
877
|
console.print(f"[dim]Updated command descriptions to: {get_language_name(language)}[/dim]")
|
|
855
878
|
|
|
856
879
|
|
|
857
|
-
def
|
|
880
|
+
def _set_config_ai_content(content: str, ai: str) -> str:
|
|
881
|
+
"""Set project.ai in freshly generated config content."""
|
|
882
|
+
return content.replace(' ai: "claude"', f' ai: "{ai}"')
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _update_project_ai(config_file: Path, ai: str) -> None:
|
|
886
|
+
"""Update the project.ai field in config.yml when present."""
|
|
887
|
+
try:
|
|
888
|
+
content = config_file.read_text(encoding="utf-8")
|
|
889
|
+
updated, count = re.subn(
|
|
890
|
+
r'^(\s*ai:\s*)["\']?[^"\'\n]+["\']?\s*$',
|
|
891
|
+
rf'\g<1>"{ai}"',
|
|
892
|
+
content,
|
|
893
|
+
flags=re.MULTILINE,
|
|
894
|
+
)
|
|
895
|
+
if count:
|
|
896
|
+
config_file.write_text(updated, encoding="utf-8")
|
|
897
|
+
except OSError:
|
|
898
|
+
return
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def _next_step_start(integration_keys: set[str], language: str) -> str:
|
|
902
|
+
"""Return a target-aware start instruction."""
|
|
903
|
+
if integration_keys == {"codex"}:
|
|
904
|
+
return "Start Codex in this project directory: codex"
|
|
905
|
+
if integration_keys == {"claude", "codex"}:
|
|
906
|
+
return "Start Claude Code or Codex in this project directory"
|
|
907
|
+
return translate("cli.init.next_step_start_claude", language)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _next_step_invocation(integrations: list, command_name: str) -> str:
|
|
911
|
+
"""Return a target-aware next-step command invocation."""
|
|
912
|
+
invocation = integrations[0].invocation_for(command_name)
|
|
913
|
+
if command_name == "constitution":
|
|
914
|
+
return f"Use {invocation} to establish project principles"
|
|
915
|
+
return f"Use {invocation} to create your first specification"
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _git_tip(integration_keys: set[str], language: str) -> str:
|
|
919
|
+
"""Return a target-aware git management tip."""
|
|
920
|
+
if integration_keys == {"codex"}:
|
|
921
|
+
return "Add .agents/ to Git if you want Codex skills versioned: git add .agents/"
|
|
922
|
+
if integration_keys == {"claude", "codex"}:
|
|
923
|
+
return "Add .claude/ and .agents/ to Git if you want AI entrypoints versioned"
|
|
924
|
+
return translate("cli.init.tips_git", language)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def _important_action(integrations: list, language: str) -> str:
|
|
928
|
+
"""Return target-aware constitution command reminder."""
|
|
929
|
+
if integrations and integrations[0].key == "codex":
|
|
930
|
+
return f"Run {integrations[0].invocation_for('constitution')} to customize it for your project."
|
|
931
|
+
return translate("cli.init.important_action", language)
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def _print_command_summary(language: str = "en", ai: str = "claude") -> None:
|
|
858
935
|
"""Print a summary of installed commands grouped by category."""
|
|
859
936
|
metadata = get_commands_metadata()
|
|
937
|
+
integrations = get_integrations(ai)
|
|
938
|
+
primary = integrations[0]
|
|
860
939
|
|
|
861
940
|
# Group by category
|
|
862
941
|
categories: dict[str, list] = {
|
|
@@ -871,16 +950,20 @@ def _print_command_summary(language: str = "en") -> None:
|
|
|
871
950
|
if cat in categories:
|
|
872
951
|
categories[cat].append(cmd)
|
|
873
952
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
953
|
+
if primary.key == "claude":
|
|
954
|
+
summary = translate("cli.init.commands_summary", language, count=len(metadata), path=COMMANDS_SUBDIR)
|
|
955
|
+
console.print(f"[bold]{summary}[/bold]")
|
|
956
|
+
elif ai == "both":
|
|
957
|
+
console.print(f"[bold]Installed {len(metadata)} CodexSpec entrypoints for Claude Code and Codex CLI[/bold]")
|
|
958
|
+
else:
|
|
959
|
+
console.print(f"[bold]Installed {len(metadata)} CodexSpec skills to .agents/skills/[/bold]")
|
|
877
960
|
console.print()
|
|
878
961
|
|
|
879
962
|
for cat, commands in categories.items():
|
|
880
963
|
if commands:
|
|
881
964
|
console.print(f" [bold]{translate(f'cli.init.category_{cat}', language, count=len(commands))}[/bold]")
|
|
882
965
|
for cmd in commands:
|
|
883
|
-
console.print(f" [cyan]{cmd['
|
|
966
|
+
console.print(f" [cyan]{primary.invocation_for(cmd['name'])}[/cyan]")
|
|
884
967
|
console.print()
|
|
885
968
|
|
|
886
969
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""AI coding tool integrations for CodexSpec."""
|
|
2
|
+
|
|
3
|
+
from .base import Integration
|
|
4
|
+
from .claude import ClaudeIntegration
|
|
5
|
+
from .codex import CodexIntegration
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_integrations(ai: str) -> list[Integration]:
|
|
9
|
+
"""Resolve the requested AI target into concrete integrations."""
|
|
10
|
+
normalized = ai.strip().lower()
|
|
11
|
+
if normalized == "claude":
|
|
12
|
+
return [ClaudeIntegration()]
|
|
13
|
+
if normalized == "codex":
|
|
14
|
+
return [CodexIntegration()]
|
|
15
|
+
if normalized == "both":
|
|
16
|
+
return [ClaudeIntegration(), CodexIntegration()]
|
|
17
|
+
raise ValueError(f"Unsupported AI assistant: {ai}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ["ClaudeIntegration", "CodexIntegration", "Integration", "get_integrations"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Shared integration protocol."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Integration(Protocol):
|
|
8
|
+
"""Protocol implemented by each AI coding tool integration."""
|
|
9
|
+
|
|
10
|
+
key: str
|
|
11
|
+
display_name: str
|
|
12
|
+
context_file: str
|
|
13
|
+
|
|
14
|
+
def invocation_for(self, command_name: str) -> str:
|
|
15
|
+
"""Return the user-facing invocation for a CodexSpec command."""
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
def install(self, target_dir: Path, templates_dir: Path, *, force: bool = False, language: str = "en") -> int:
|
|
19
|
+
"""Install this integration into target_dir."""
|
|
20
|
+
...
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Claude Code integration."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from codexspec.commands.installer import COMMANDS_SUBDIR, install_commands_to_subdir
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ClaudeIntegration:
|
|
9
|
+
"""Installs CodexSpec as Claude Code slash commands."""
|
|
10
|
+
|
|
11
|
+
key = "claude"
|
|
12
|
+
display_name = "Claude Code"
|
|
13
|
+
context_file = "CLAUDE.md"
|
|
14
|
+
|
|
15
|
+
def invocation_for(self, command_name: str) -> str:
|
|
16
|
+
"""Return the Claude slash command invocation."""
|
|
17
|
+
return f"/codexspec:{command_name}"
|
|
18
|
+
|
|
19
|
+
def commands_dir(self, target_dir: Path) -> Path:
|
|
20
|
+
"""Return the Claude command destination."""
|
|
21
|
+
return target_dir / ".claude" / "commands" / COMMANDS_SUBDIR
|
|
22
|
+
|
|
23
|
+
def install(self, target_dir: Path, templates_dir: Path, *, force: bool = False, language: str = "en") -> int:
|
|
24
|
+
"""Install Claude slash command templates."""
|
|
25
|
+
return install_commands_to_subdir(self.commands_dir(target_dir), templates_dir, force=force, language=language)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Codex CLI integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from codexspec.commands.installer import get_commands_metadata
|
|
13
|
+
from codexspec.translator import load_translation_cache, translate_template_frontmatter
|
|
14
|
+
|
|
15
|
+
CODEXSPEC_CONTEXT_START = "<!-- CODEXSPEC START -->"
|
|
16
|
+
CODEXSPEC_CONTEXT_END = "<!-- CODEXSPEC END -->"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CodexIntegration:
|
|
20
|
+
"""Installs CodexSpec as Codex skills."""
|
|
21
|
+
|
|
22
|
+
key = "codex"
|
|
23
|
+
display_name = "Codex CLI"
|
|
24
|
+
context_file = "AGENTS.md"
|
|
25
|
+
|
|
26
|
+
def invocation_for(self, command_name: str) -> str:
|
|
27
|
+
"""Return the Codex skill mention invocation."""
|
|
28
|
+
return f"$codexspec:{command_name}"
|
|
29
|
+
|
|
30
|
+
def skills_dir(self, target_dir: Path) -> Path:
|
|
31
|
+
"""Return the Codex skills destination."""
|
|
32
|
+
return target_dir / ".agents" / "skills"
|
|
33
|
+
|
|
34
|
+
def install(self, target_dir: Path, templates_dir: Path, *, force: bool = False, language: str = "en") -> int:
|
|
35
|
+
"""Install Codex skills and the Codex context file."""
|
|
36
|
+
count = self.install_skills(target_dir, templates_dir, force=force, language=language)
|
|
37
|
+
self.ensure_context_file(target_dir)
|
|
38
|
+
return count
|
|
39
|
+
|
|
40
|
+
def install_skills(
|
|
41
|
+
self,
|
|
42
|
+
target_dir: Path,
|
|
43
|
+
templates_dir: Path,
|
|
44
|
+
*,
|
|
45
|
+
force: bool = False,
|
|
46
|
+
language: str = "en",
|
|
47
|
+
) -> int:
|
|
48
|
+
"""Render command templates into Codex SKILL.md files."""
|
|
49
|
+
if not templates_dir.exists():
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
descriptions = {cmd["file_name"]: cmd["description"] for cmd in get_commands_metadata()}
|
|
53
|
+
translation_cache = None
|
|
54
|
+
if language != "en":
|
|
55
|
+
translation_cache = load_translation_cache(language)
|
|
56
|
+
|
|
57
|
+
installed_count = 0
|
|
58
|
+
for template_file in sorted(templates_dir.glob("*.md")):
|
|
59
|
+
command_name = template_file.stem
|
|
60
|
+
skill_dir = self.skills_dir(target_dir) / f"codexspec-{command_name}"
|
|
61
|
+
skill_file = skill_dir / "SKILL.md"
|
|
62
|
+
if skill_file.exists() and not force:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
content = template_file.read_text(encoding="utf-8")
|
|
66
|
+
content = translate_template_frontmatter(content, command_name, language, translation_cache)
|
|
67
|
+
skill_content = self.render_skill(command_name, content, descriptions.get(template_file.name, ""))
|
|
68
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
skill_file.write_text(skill_content, encoding="utf-8")
|
|
70
|
+
installed_count += 1
|
|
71
|
+
|
|
72
|
+
return installed_count
|
|
73
|
+
|
|
74
|
+
def ensure_context_file(self, target_dir: Path) -> None:
|
|
75
|
+
"""Create or update AGENTS.md with a managed CodexSpec section."""
|
|
76
|
+
context_path = target_dir / self.context_file
|
|
77
|
+
existing = context_path.read_text(encoding="utf-8") if context_path.exists() else ""
|
|
78
|
+
section = self._context_section()
|
|
79
|
+
|
|
80
|
+
pattern = re.compile(
|
|
81
|
+
rf"{re.escape(CODEXSPEC_CONTEXT_START)}.*?{re.escape(CODEXSPEC_CONTEXT_END)}",
|
|
82
|
+
re.DOTALL,
|
|
83
|
+
)
|
|
84
|
+
if pattern.search(existing):
|
|
85
|
+
updated = pattern.sub(section.strip(), existing)
|
|
86
|
+
elif existing.strip():
|
|
87
|
+
updated = existing.rstrip() + "\n\n" + section
|
|
88
|
+
else:
|
|
89
|
+
updated = "# AGENTS.md\n\n" + section
|
|
90
|
+
|
|
91
|
+
context_path.write_text(updated, encoding="utf-8")
|
|
92
|
+
|
|
93
|
+
def render_skill(self, command_name: str, content: str, fallback_description: str = "") -> str:
|
|
94
|
+
"""Render one command template into a Codex SKILL.md."""
|
|
95
|
+
frontmatter, body = _split_frontmatter(content)
|
|
96
|
+
description = str(frontmatter.get("description") or fallback_description or f"CodexSpec {command_name}")
|
|
97
|
+
body = _render_script_placeholders(body, frontmatter)
|
|
98
|
+
body = _render_codex_body(command_name, body)
|
|
99
|
+
|
|
100
|
+
return f"---\nname: codexspec:{command_name}\ndescription: {_yaml_scalar(description)}\n---\n\n{body.strip()}\n"
|
|
101
|
+
|
|
102
|
+
def _context_section(self) -> str:
|
|
103
|
+
return f"""{CODEXSPEC_CONTEXT_START}
|
|
104
|
+
## CodexSpec
|
|
105
|
+
|
|
106
|
+
This project uses CodexSpec for requirements-first spec-driven development.
|
|
107
|
+
|
|
108
|
+
Use these Codex skills when working on CodexSpec workflows:
|
|
109
|
+
|
|
110
|
+
- `$codexspec:constitution` to create or update project principles.
|
|
111
|
+
- `$codexspec:specify` to capture confirmed requirements.
|
|
112
|
+
- `$codexspec:generate-spec` to produce `spec.md`.
|
|
113
|
+
- `$codexspec:spec-to-plan` to produce `plan.md`.
|
|
114
|
+
- `$codexspec:plan-to-tasks` to produce `tasks.md`.
|
|
115
|
+
- `$codexspec:implement-tasks` to implement approved tasks.
|
|
116
|
+
|
|
117
|
+
Before making workflow decisions, read `.codexspec/memory/constitution.md`.
|
|
118
|
+
{CODEXSPEC_CONTEXT_END}
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _split_frontmatter(content: str) -> tuple[dict[str, Any], str]:
|
|
123
|
+
if not content.startswith("---"):
|
|
124
|
+
return {}, content
|
|
125
|
+
parts = content.split("---", 2)
|
|
126
|
+
if len(parts) < 3:
|
|
127
|
+
return {}, content
|
|
128
|
+
try:
|
|
129
|
+
parsed = yaml.safe_load(parts[1]) or {}
|
|
130
|
+
except yaml.YAMLError:
|
|
131
|
+
parsed = {}
|
|
132
|
+
if not isinstance(parsed, dict):
|
|
133
|
+
parsed = {}
|
|
134
|
+
return parsed, parts[2]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _render_script_placeholders(body: str, frontmatter: dict[str, Any]) -> str:
|
|
138
|
+
scripts = frontmatter.get("scripts")
|
|
139
|
+
if not isinstance(scripts, dict):
|
|
140
|
+
return body
|
|
141
|
+
preferred = "ps" if sys.platform == "win32" else "sh"
|
|
142
|
+
default_command = str(scripts.get(preferred) or scripts.get("sh") or scripts.get("ps") or "")
|
|
143
|
+
sh_command = str(scripts.get("sh") or default_command)
|
|
144
|
+
ps_command = str(scripts.get("ps") or default_command)
|
|
145
|
+
|
|
146
|
+
lines: list[str] = []
|
|
147
|
+
for line in body.splitlines(keepends=True):
|
|
148
|
+
if "{SCRIPT}" not in line:
|
|
149
|
+
lines.append(line)
|
|
150
|
+
elif "PowerShell:" in line:
|
|
151
|
+
lines.append(line.replace("{SCRIPT}", ps_command))
|
|
152
|
+
elif "Bash:" in line:
|
|
153
|
+
lines.append(line.replace("{SCRIPT}", sh_command))
|
|
154
|
+
else:
|
|
155
|
+
lines.append(line.replace("{SCRIPT}", default_command))
|
|
156
|
+
return "".join(lines)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _render_codex_body(command_name: str, body: str) -> str:
|
|
160
|
+
arg_text = f"the text after the $codexspec:{command_name} skill mention"
|
|
161
|
+
body = body.replace("$ARGUMENTS", arg_text)
|
|
162
|
+
body = body.replace("{ARGS}", arg_text)
|
|
163
|
+
return re.sub(r"/codexspec:([A-Za-z0-9-]+)", r"$codexspec:\1", body)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _yaml_scalar(value: str) -> str:
|
|
167
|
+
if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9 _.,:/()'\"-]*", value):
|
|
168
|
+
return value
|
|
169
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
170
|
+
return f'"{escaped}"'
|
|
@@ -92,6 +92,16 @@ Invoke `/codexspec:review-tasks <feature-dir>/tasks.md`.
|
|
|
92
92
|
- Run a maximum of two automatic fix-and-review rounds.
|
|
93
93
|
- Stop if defects repeat, remain unresolved, or require a user or architecture decision.
|
|
94
94
|
|
|
95
|
+
## Automatic Cross-Artifact Analysis
|
|
96
|
+
|
|
97
|
+
When the review loop above concludes in a passing state — the final `/codexspec:review-tasks` Overall Status is `PASS` or `PASS_WITH_WARNINGS` — invoke `/codexspec:analyze <feature-dir>` exactly once.
|
|
98
|
+
|
|
99
|
+
- Do not invoke analyze when the review loop stopped at `NEEDS_REVISION` or `BLOCKED`, or stopped early per the conditions above; in those cases end here, handing control back to the user as the review loop already does.
|
|
100
|
+
- analyze runs once and is read-only. Present its output as-is; do not auto-fix its findings and do not run a fix-and-reanalyze loop.
|
|
101
|
+
- If `requirements.md` is absent, analyze still runs and discloses its legacy limitation (it starts at `spec.md` and cannot verify fidelity to the original discussion) per its own behavior.
|
|
102
|
+
- analyze's results are informational only. They do not change whether tasks are ready for implementation and do not add a gate before `/codexspec:implement-tasks`.
|
|
103
|
+
- Do not modify the Output Summary for analyze, and do not save an additional analyze report file; analyze's own output is the report.
|
|
104
|
+
|
|
95
105
|
## Output Summary
|
|
96
106
|
|
|
97
107
|
Report the tasks path, plan/requirement coverage, dependency summary, unresolved items, and auto-review status.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|