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.
Files changed (61) hide show
  1. {codexspec-0.6.5 → codexspec-0.7.1}/PKG-INFO +9 -3
  2. {codexspec-0.6.5 → codexspec-0.7.1}/README.md +8 -2
  3. {codexspec-0.6.5 → codexspec-0.7.1}/pyproject.toml +1 -1
  4. {codexspec-0.6.5 → codexspec-0.7.1}/src/codexspec/__init__.py +162 -79
  5. codexspec-0.7.1/src/codexspec/integrations/__init__.py +20 -0
  6. codexspec-0.7.1/src/codexspec/integrations/base.py +20 -0
  7. codexspec-0.7.1/src/codexspec/integrations/claude.py +25 -0
  8. codexspec-0.7.1/src/codexspec/integrations/codex.py +170 -0
  9. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/plan-to-tasks.md +10 -0
  10. {codexspec-0.6.5 → codexspec-0.7.1}/.gitignore +0 -0
  11. {codexspec-0.6.5 → codexspec-0.7.1}/LICENSE +0 -0
  12. {codexspec-0.6.5 → codexspec-0.7.1}/codexspec-icon.svg +0 -0
  13. {codexspec-0.6.5 → codexspec-0.7.1}/codexspec-logo-dark.svg +0 -0
  14. {codexspec-0.6.5 → codexspec-0.7.1}/codexspec-logo-light.svg +0 -0
  15. {codexspec-0.6.5 → codexspec-0.7.1}/scripts/bash/check-i18n-completeness.sh +0 -0
  16. {codexspec-0.6.5 → codexspec-0.7.1}/scripts/bash/check-i18n-structure.sh +0 -0
  17. {codexspec-0.6.5 → codexspec-0.7.1}/scripts/bash/check-prerequisites.sh +0 -0
  18. {codexspec-0.6.5 → codexspec-0.7.1}/scripts/bash/common.sh +0 -0
  19. {codexspec-0.6.5 → codexspec-0.7.1}/scripts/bash/create-new-feature.sh +0 -0
  20. {codexspec-0.6.5 → codexspec-0.7.1}/scripts/powershell/check-prerequisites.ps1 +0 -0
  21. {codexspec-0.6.5 → codexspec-0.7.1}/scripts/powershell/common.ps1 +0 -0
  22. {codexspec-0.6.5 → codexspec-0.7.1}/scripts/powershell/create-new-feature.ps1 +0 -0
  23. {codexspec-0.6.5 → codexspec-0.7.1}/src/codexspec/commands/__init__.py +0 -0
  24. {codexspec-0.6.5 → codexspec-0.7.1}/src/codexspec/commands/installer.py +0 -0
  25. {codexspec-0.6.5 → codexspec-0.7.1}/src/codexspec/i18n.py +0 -0
  26. {codexspec-0.6.5 → codexspec-0.7.1}/src/codexspec/idea.md +0 -0
  27. {codexspec-0.6.5 → codexspec-0.7.1}/src/codexspec/translator.py +0 -0
  28. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/analyze.md +0 -0
  29. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/checklist.md +0 -0
  30. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/clarify.md +0 -0
  31. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/commit-staged.md +0 -0
  32. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/config.md +0 -0
  33. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/constitution.md +0 -0
  34. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/generate-spec.md +0 -0
  35. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/implement-tasks.md +0 -0
  36. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/pr.md +0 -0
  37. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/quick.md +0 -0
  38. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/review-code.md +0 -0
  39. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/review-plan.md +0 -0
  40. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/review-spec.md +0 -0
  41. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/review-tasks.md +0 -0
  42. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/spec-to-plan.md +0 -0
  43. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/specify.md +0 -0
  44. {codexspec-0.6.5 → codexspec-0.7.1}/templates/commands/tasks-to-issues.md +0 -0
  45. {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/checklist-template.md +0 -0
  46. {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/constitution-template.md +0 -0
  47. {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/plan-template-detailed.md +0 -0
  48. {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/plan-template-simple.md +0 -0
  49. {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/requirements-template.md +0 -0
  50. {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/spec-template-detailed.md +0 -0
  51. {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/spec-template-simple.md +0 -0
  52. {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/tasks-template-detailed.md +0 -0
  53. {codexspec-0.6.5 → codexspec-0.7.1}/templates/docs/tasks-template-simple.md +0 -0
  54. {codexspec-0.6.5 → codexspec-0.7.1}/templates/translations/de.json +0 -0
  55. {codexspec-0.6.5 → codexspec-0.7.1}/templates/translations/en.json +0 -0
  56. {codexspec-0.6.5 → codexspec-0.7.1}/templates/translations/es.json +0 -0
  57. {codexspec-0.6.5 → codexspec-0.7.1}/templates/translations/fr.json +0 -0
  58. {codexspec-0.6.5 → codexspec-0.7.1}/templates/translations/ja.json +0 -0
  59. {codexspec-0.6.5 → codexspec-0.7.1}/templates/translations/ko.json +0 -0
  60. {codexspec-0.6.5 → codexspec-0.7.1}/templates/translations/pt-BR.json +0 -0
  61. {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.6.5
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
- └── CLAUDE.md # Claude Code context
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
- └── CLAUDE.md # Claude Code context
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
  ---
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codexspec"
3
- version = "0.6.5"
3
+ version = "0.7.1"
4
4
  description = "CodexSpec - A Requirements-First SDD toolkit for Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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.6.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
- # Check for old structure and migrate if needed
676
- old_files = detect_old_structure(claude_dir)
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"[yellow]{translate('cli.init.migration_found', normalized_lang, count=len(old_files))}[/yellow]"
680
+ f"[red]{translate('cli.init.error_templates_missing', normalized_lang, path=str(templates_dir))}[/red]"
682
681
  )
683
- console.print(f"[dim]{translate('cli.init.migration_old_structure', normalized_lang)}[/dim]")
684
- console.print(f"[dim]{translate('cli.init.migration_new_structure', normalized_lang)}[/dim]")
682
+ raise typer.Exit(1)
685
683
 
686
- if force or Confirm.ask(translate("cli.init.migration_confirm", normalized_lang), default=True):
687
- if migrate_old_commands(claude_dir, old_files):
688
- console.print(f"[green]{translate('cli.init.migration_complete', normalized_lang)}[/green]")
689
- migration_happened = True
690
- else:
691
- console.print(f"[red]{translate('cli.init.migration_failed', normalized_lang)}[/red]")
692
- else:
693
- console.print(f"[dim]{translate('cli.init.migration_skipped', normalized_lang)}[/dim]")
694
-
695
- # Install or update commands (translate after migration if user wants)
696
- if migration_happened:
697
- # Migration moved files, ask if user wants to update/translate them
698
- if force or Confirm.ask(translate("cli.init.update_confirm", normalized_lang), default=True):
699
- count = install_commands_to_subdir(
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"[red]{translate('cli.init.error_templates_missing', normalized_lang, path=str(templates_dir))}[/red]"
699
+ f"[yellow]{translate('cli.init.migration_found', normalized_lang, count=len(old_files))}[/yellow]"
716
700
  )
717
- raise typer.Exit(1)
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
- count = install_commands_to_subdir(codexspec_commands_dir, templates_dir, language=normalized_lang)
720
- cmd_path = f".claude/commands/{COMMANDS_SUBDIR}/"
721
- msg = translate("cli.init.commands_installed", normalized_lang, count=count, path=cmd_path)
722
- console.print(f"[green]{msg}[/green]")
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
- # Create CLAUDE.md
777
- # CLAUDE.md is user-authored content (like the constitution), not a
778
- # regenerable artifact (like commands/scripts). Never overwrite an existing
779
- # body -- even under --force. --force only auto-confirms the one safe,
780
- # idempotent change: prepending the constitution @import if it is missing.
781
- claude_md = target_dir / "CLAUDE.md"
782
- if not claude_md.exists():
783
- project_name = target_dir.name
784
- claude_md.write_text(_get_claude_md_content(project_name), encoding="utf-8")
785
- console.print(f"[green]{translate('cli.init.created_file', normalized_lang, file='CLAUDE.md')}[/green]")
786
- else:
787
- # Existing CLAUDE.md: ensure the compliance @import, never clobber the body.
788
- if not has_compliance_section(claude_md):
789
- if force or confirm_add_compliance(normalized_lang):
790
- prepend_compliance_section(claude_md)
791
- console.print(f"[green]{translate('cli.init.compliance_added', normalized_lang)}[/green]")
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. {translate('cli.init.next_step_start_claude', normalized_lang)}\n"
813
- f"3. {translate('cli.init.next_step_constitution', normalized_lang)}\n"
814
- f"4. {translate('cli.init.next_step_specify', normalized_lang)}",
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" - {translate('cli.init.tips_git', normalized_lang)}")
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]{translate('cli.init.important_action', normalized_lang)}[/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 _print_command_summary(language: str = "en") -> None:
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
- console.print(
875
- f"[bold]{translate('cli.init.commands_summary', language, count=len(metadata), path=COMMANDS_SUBDIR)}[/bold]"
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['display_name']}[/cyan]")
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