codexspec 0.6.4__tar.gz → 0.7.0__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.4 → codexspec-0.7.0}/PKG-INFO +9 -3
  2. {codexspec-0.6.4 → codexspec-0.7.0}/README.md +8 -2
  3. {codexspec-0.6.4 → codexspec-0.7.0}/pyproject.toml +1 -1
  4. {codexspec-0.6.4 → codexspec-0.7.0}/src/codexspec/__init__.py +167 -78
  5. codexspec-0.7.0/src/codexspec/integrations/__init__.py +20 -0
  6. codexspec-0.7.0/src/codexspec/integrations/base.py +20 -0
  7. codexspec-0.7.0/src/codexspec/integrations/claude.py +25 -0
  8. codexspec-0.7.0/src/codexspec/integrations/codex.py +170 -0
  9. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/implement-tasks.md +98 -5
  10. {codexspec-0.6.4 → codexspec-0.7.0}/.gitignore +0 -0
  11. {codexspec-0.6.4 → codexspec-0.7.0}/LICENSE +0 -0
  12. {codexspec-0.6.4 → codexspec-0.7.0}/codexspec-icon.svg +0 -0
  13. {codexspec-0.6.4 → codexspec-0.7.0}/codexspec-logo-dark.svg +0 -0
  14. {codexspec-0.6.4 → codexspec-0.7.0}/codexspec-logo-light.svg +0 -0
  15. {codexspec-0.6.4 → codexspec-0.7.0}/scripts/bash/check-i18n-completeness.sh +0 -0
  16. {codexspec-0.6.4 → codexspec-0.7.0}/scripts/bash/check-i18n-structure.sh +0 -0
  17. {codexspec-0.6.4 → codexspec-0.7.0}/scripts/bash/check-prerequisites.sh +0 -0
  18. {codexspec-0.6.4 → codexspec-0.7.0}/scripts/bash/common.sh +0 -0
  19. {codexspec-0.6.4 → codexspec-0.7.0}/scripts/bash/create-new-feature.sh +0 -0
  20. {codexspec-0.6.4 → codexspec-0.7.0}/scripts/powershell/check-prerequisites.ps1 +0 -0
  21. {codexspec-0.6.4 → codexspec-0.7.0}/scripts/powershell/common.ps1 +0 -0
  22. {codexspec-0.6.4 → codexspec-0.7.0}/scripts/powershell/create-new-feature.ps1 +0 -0
  23. {codexspec-0.6.4 → codexspec-0.7.0}/src/codexspec/commands/__init__.py +0 -0
  24. {codexspec-0.6.4 → codexspec-0.7.0}/src/codexspec/commands/installer.py +0 -0
  25. {codexspec-0.6.4 → codexspec-0.7.0}/src/codexspec/i18n.py +0 -0
  26. {codexspec-0.6.4 → codexspec-0.7.0}/src/codexspec/idea.md +0 -0
  27. {codexspec-0.6.4 → codexspec-0.7.0}/src/codexspec/translator.py +0 -0
  28. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/analyze.md +0 -0
  29. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/checklist.md +0 -0
  30. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/clarify.md +0 -0
  31. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/commit-staged.md +0 -0
  32. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/config.md +0 -0
  33. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/constitution.md +0 -0
  34. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/generate-spec.md +0 -0
  35. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/plan-to-tasks.md +0 -0
  36. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/pr.md +0 -0
  37. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/quick.md +0 -0
  38. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/review-code.md +0 -0
  39. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/review-plan.md +0 -0
  40. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/review-spec.md +0 -0
  41. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/review-tasks.md +0 -0
  42. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/spec-to-plan.md +0 -0
  43. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/specify.md +0 -0
  44. {codexspec-0.6.4 → codexspec-0.7.0}/templates/commands/tasks-to-issues.md +0 -0
  45. {codexspec-0.6.4 → codexspec-0.7.0}/templates/docs/checklist-template.md +0 -0
  46. {codexspec-0.6.4 → codexspec-0.7.0}/templates/docs/constitution-template.md +0 -0
  47. {codexspec-0.6.4 → codexspec-0.7.0}/templates/docs/plan-template-detailed.md +0 -0
  48. {codexspec-0.6.4 → codexspec-0.7.0}/templates/docs/plan-template-simple.md +0 -0
  49. {codexspec-0.6.4 → codexspec-0.7.0}/templates/docs/requirements-template.md +0 -0
  50. {codexspec-0.6.4 → codexspec-0.7.0}/templates/docs/spec-template-detailed.md +0 -0
  51. {codexspec-0.6.4 → codexspec-0.7.0}/templates/docs/spec-template-simple.md +0 -0
  52. {codexspec-0.6.4 → codexspec-0.7.0}/templates/docs/tasks-template-detailed.md +0 -0
  53. {codexspec-0.6.4 → codexspec-0.7.0}/templates/docs/tasks-template-simple.md +0 -0
  54. {codexspec-0.6.4 → codexspec-0.7.0}/templates/translations/de.json +0 -0
  55. {codexspec-0.6.4 → codexspec-0.7.0}/templates/translations/en.json +0 -0
  56. {codexspec-0.6.4 → codexspec-0.7.0}/templates/translations/es.json +0 -0
  57. {codexspec-0.6.4 → codexspec-0.7.0}/templates/translations/fr.json +0 -0
  58. {codexspec-0.6.4 → codexspec-0.7.0}/templates/translations/ja.json +0 -0
  59. {codexspec-0.6.4 → codexspec-0.7.0}/templates/translations/ko.json +0 -0
  60. {codexspec-0.6.4 → codexspec-0.7.0}/templates/translations/pt-BR.json +0 -0
  61. {codexspec-0.6.4 → codexspec-0.7.0}/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.4
3
+ Version: 0.7.0
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.4"
3
+ version = "0.7.0"
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
@@ -471,9 +473,11 @@ def init(
471
473
  "--force",
472
474
  "-f",
473
475
  help=(
474
- "Overwrite existing files and auto-confirm prompts (skips the language "
475
- "prompt and command-update/migration confirms). Language config is updated "
476
- "surgically, never fully regenerated."
476
+ "Overwrite regenerable files (commands/scripts) and auto-confirm prompts "
477
+ "(skips the language prompt and command-update/migration confirms). "
478
+ "User-authored content is preserved: language config is updated surgically "
479
+ "and an existing CLAUDE.md body is never overwritten (only the constitution "
480
+ "@import is prepended if missing)."
477
481
  ),
478
482
  ),
479
483
  no_git: bool = typer.Option(
@@ -510,6 +514,14 @@ def init(
510
514
  interaction_lang = interaction_lang if isinstance(interaction_lang, str) else None
511
515
  document_lang = document_lang if isinstance(document_lang, str) else None
512
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}
513
525
 
514
526
  # Determine target directory first — needed to detect an existing config.yml
515
527
  # before resolving the language base (REQ-007).
@@ -659,65 +671,74 @@ def init(
659
671
  else:
660
672
  console.print("[yellow]Warning: Docs templates directory not found[/yellow]")
661
673
 
662
- # Create .claude/commands directory for slash commands
663
- claude_dir = target_dir / ".claude"
664
- claude_commands_dir = claude_dir / "commands"
665
- claude_commands_dir.mkdir(parents=True, exist_ok=True)
666
-
667
- # Target subdirectory for CodexSpec commands
668
- codexspec_commands_dir = claude_commands_dir / COMMANDS_SUBDIR
669
-
670
674
  # Get templates directory
671
675
  templates_dir = get_templates_dir() / "commands"
672
-
673
- # Check for old structure and migrate if needed
674
- old_files = detect_old_structure(claude_dir)
675
- migration_happened = False
676
- if old_files:
677
- 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.
678
679
  console.print(
679
- 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]"
680
681
  )
681
- console.print(f"[dim]{translate('cli.init.migration_old_structure', normalized_lang)}[/dim]")
682
- console.print(f"[dim]{translate('cli.init.migration_new_structure', normalized_lang)}[/dim]")
682
+ raise typer.Exit(1)
683
683
 
684
- if force or Confirm.ask(translate("cli.init.migration_confirm", normalized_lang), default=True):
685
- if migrate_old_commands(claude_dir, old_files):
686
- console.print(f"[green]{translate('cli.init.migration_complete', normalized_lang)}[/green]")
687
- migration_happened = True
688
- else:
689
- console.print(f"[red]{translate('cli.init.migration_failed', normalized_lang)}[/red]")
690
- else:
691
- console.print(f"[dim]{translate('cli.init.migration_skipped', normalized_lang)}[/dim]")
692
-
693
- # Install or update commands (translate after migration if user wants)
694
- if migration_happened:
695
- # Migration moved files, ask if user wants to update/translate them
696
- if force or Confirm.ask(translate("cli.init.update_confirm", normalized_lang), default=True):
697
- count = install_commands_to_subdir(
698
- codexspec_commands_dir, templates_dir, force=True, language=normalized_lang
699
- )
700
- console.print(f"[green]{translate('cli.init.commands_updated', normalized_lang, count=count)}[/green]")
701
- elif should_update_commands(codexspec_dir):
702
- console.print()
703
- if force or Confirm.ask(translate("cli.init.update_confirm", normalized_lang), default=True):
704
- count = install_commands_to_subdir(
705
- codexspec_commands_dir, templates_dir, force=True, language=normalized_lang
706
- )
707
- console.print(f"[green]{translate('cli.init.commands_updated', normalized_lang, count=count)}[/green]")
708
- else:
709
- if not templates_dir.exists():
710
- # Templates directory is part of the wheel; if it's missing the
711
- # 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()
712
698
  console.print(
713
- 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]"
714
700
  )
715
- 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]")
716
733
 
717
- count = install_commands_to_subdir(codexspec_commands_dir, templates_dir, language=normalized_lang)
718
- cmd_path = f".claude/commands/{COMMANDS_SUBDIR}/"
719
- msg = translate("cli.init.commands_installed", normalized_lang, count=count, path=cmd_path)
720
- 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
+ )
721
742
 
722
743
  # Create constitution template
723
744
  constitution_file = codexspec_dir / "memory" / "constitution.md"
@@ -747,6 +768,7 @@ def init(
747
768
  commit=keys_to_write.get("commit"),
748
769
  created=datetime.now().strftime("%Y-%m-%d"),
749
770
  )
771
+ config_content = _set_config_ai_content(config_content, ai)
750
772
  config_file.write_text(config_content, encoding="utf-8")
751
773
  wrote_keys = list(keys_to_write.keys())
752
774
  lang_name = get_language_name(normalized_lang)
@@ -761,6 +783,8 @@ def init(
761
783
  for key, value in keys_to_write.items():
762
784
  if update_language_field(config_file, key, value):
763
785
  wrote_keys.append(key)
786
+ if config_file.exists():
787
+ _update_project_ai(config_file, ai)
764
788
 
765
789
  # Non-blocking notice of which language keys were set (REQ-010).
766
790
  if wrote_keys:
@@ -771,18 +795,23 @@ def init(
771
795
  if prompted_base:
772
796
  console.print(f"[dim]{translate('cli.init.language_dimensions_hint', normalized_lang)}[/dim]")
773
797
 
774
- # Create CLAUDE.md
775
- claude_md = target_dir / "CLAUDE.md"
776
- if not claude_md.exists() or force:
777
- project_name = target_dir.name
778
- claude_md.write_text(_get_claude_md_content(project_name), encoding="utf-8")
779
- console.print(f"[green]{translate('cli.init.created_file', normalized_lang, file='CLAUDE.md')}[/green]")
780
- else:
781
- # Check if existing CLAUDE.md has compliance section
782
- if not has_compliance_section(claude_md):
783
- if confirm_add_compliance(normalized_lang):
784
- prepend_compliance_section(claude_md)
785
- 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]")
786
815
 
787
816
  # Initialize git if requested
788
817
  if not no_git and not (target_dir / ".git").exists():
@@ -794,7 +823,7 @@ def init(
794
823
 
795
824
  # Print success message with command summary
796
825
  console.print()
797
- _print_command_summary(normalized_lang)
826
+ _print_command_summary(normalized_lang, ai=ai)
798
827
 
799
828
  project_nav = project_name if project_name and project_name != "." else "."
800
829
  console.print(
@@ -803,9 +832,9 @@ def init(
803
832
  f"{translate('cli.init.success_project_dir', normalized_lang, path=str(target_dir))}\n\n"
804
833
  f"[bold]{translate('cli.init.next_steps', normalized_lang)}[/bold]\n"
805
834
  f"1. {translate('cli.init.next_step_navigate', normalized_lang, path=project_nav)}\n"
806
- f"2. {translate('cli.init.next_step_start_claude', normalized_lang)}\n"
807
- f"3. {translate('cli.init.next_step_constitution', normalized_lang)}\n"
808
- 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')}",
809
838
  title=translate("cli.init.success_title", normalized_lang),
810
839
  )
811
840
  )
@@ -813,7 +842,7 @@ def init(
813
842
  # Git management tip
814
843
  console.print()
815
844
  console.print(f"[bold]{translate('cli.init.tips_header', normalized_lang)}[/bold]")
816
- console.print(f" - {translate('cli.init.tips_git', normalized_lang)}")
845
+ console.print(f" - {_git_tip(integration_keys, normalized_lang)}")
817
846
  console.print(f" - {translate('cli.init.tips_list_commands', normalized_lang)}")
818
847
  console.print(f" - {translate('cli.init.tips_edit', normalized_lang)}")
819
848
 
@@ -822,7 +851,7 @@ def init(
822
851
  important_header = translate("cli.init.important_header", normalized_lang)
823
852
  important_msg = translate("cli.init.important_message", normalized_lang)
824
853
  console.print(f"[yellow]{important_header}[/yellow] {important_msg}")
825
- console.print(f"[yellow]{translate('cli.init.important_action', normalized_lang)}[/yellow]")
854
+ console.print(f"[yellow]{_important_action(integrations, normalized_lang)}[/yellow]")
826
855
 
827
856
 
828
857
  def _rerender_command_frontmatter(config_file: Path) -> None:
@@ -848,9 +877,65 @@ def _rerender_command_frontmatter(config_file: Path) -> None:
848
877
  console.print(f"[dim]Updated command descriptions to: {get_language_name(language)}[/dim]")
849
878
 
850
879
 
851
- 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:
852
935
  """Print a summary of installed commands grouped by category."""
853
936
  metadata = get_commands_metadata()
937
+ integrations = get_integrations(ai)
938
+ primary = integrations[0]
854
939
 
855
940
  # Group by category
856
941
  categories: dict[str, list] = {
@@ -865,16 +950,20 @@ def _print_command_summary(language: str = "en") -> None:
865
950
  if cat in categories:
866
951
  categories[cat].append(cmd)
867
952
 
868
- console.print(
869
- f"[bold]{translate('cli.init.commands_summary', language, count=len(metadata), path=COMMANDS_SUBDIR)}[/bold]"
870
- )
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]")
871
960
  console.print()
872
961
 
873
962
  for cat, commands in categories.items():
874
963
  if commands:
875
964
  console.print(f" [bold]{translate(f'cli.init.category_{cat}', language, count=len(commands))}[/bold]")
876
965
  for cmd in commands:
877
- console.print(f" [cyan]{cmd['display_name']}[/cyan]")
966
+ console.print(f" [cyan]{primary.invocation_for(cmd['name'])}[/cyan]")
878
967
  console.print()
879
968
 
880
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}"'
@@ -133,10 +133,103 @@ When encountering problems, create/update `issues.md` in the same directory as `
133
133
  - **Status**: Blocked / Workaround Found / Needs Discussion
134
134
  ```
135
135
 
136
- ### 6. Completion
136
+ ### 6. Pre-Review Baseline
137
137
 
138
- After all tasks:
138
+ After all tasks are implemented:
139
139
 
140
- - Run full test suite (if applicable)
141
- - Final commit if needed
142
- - Report completion summary with files modified
140
+ - Run the full test suite.
141
+ - Confirm the suite is green. This green state is the baseline for the Final
142
+ Code Review Loop; no auto-fix may regress it. If the suite is already red at
143
+ this point, stop and fix the implementation before reviewing.
144
+
145
+ ### 7. Final Code Review Loop
146
+
147
+ After the baseline is green, review the implemented code and auto-fix verified
148
+ defects. This mirrors the `review-spec` / `review-plan` / `review-tasks` loops
149
+ the sibling generation commands run.
150
+
151
+ #### 7.1 Determine the Review Target
152
+
153
+ Review only the analyzable code changed by this implementation, not the whole
154
+ repository and not per task. Compute the candidate file set:
155
+
156
+ ```
157
+ git diff --name-only $(git merge-base HEAD <main>)..HEAD
158
+ git diff --name-only # uncommitted tracked changes
159
+ git ls-files --others --exclude-standard # untracked files
160
+ ```
161
+
162
+ `<main>` is the project's default branch, resolved from `git.main_branches` in
163
+ `.codexspec/config.yml` (default: `main`, `master`, `develop`).
164
+
165
+ Then filter the candidate set:
166
+
167
+ - Keep only analyzable source extensions: `.py .ts .tsx .js .jsx .go .rs .java
168
+ .kt .kts .rb .sh .bash .zsh .c .h .cpp .hpp .cc .cxx .cs .swift .php`.
169
+ - Exclude `.codexspec/specs/` and generated/vendored paths (e.g. `dist/`,
170
+ lockfiles, `.venv/`).
171
+
172
+ If the filtered set is empty (e.g. the implementation produced only docs,
173
+ config, or assets), report "no code to review", skip this loop, and proceed to
174
+ step 8.
175
+
176
+ Fallback: if git is unavailable or the current branch is not a feature branch
177
+ (so the diff base cannot be determined), review the project's primary source
178
+ directory as `/codexspec:review-code` would by default, and explicitly note the
179
+ degraded fidelity (the review may include pre-existing code).
180
+
181
+ #### 7.2 Invoke the Review
182
+
183
+ Invoke `/codexspec:review-code <filtered-paths>`.
184
+
185
+ `/codexspec:review-code` is **review-only**; it produces findings and scores but
186
+ does not edit code. Apply every fix **yourself**, under this command's tool
187
+ scope (which includes `Edit`/`Write` and running the test suite via `Bash`).
188
+
189
+ #### 7.3 Auto-Fix Scope
190
+
191
+ - Auto-fix **CRITICAL, HIGH, and MEDIUM** findings only.
192
+ - **LOW** (suggestion) findings are report-only; never auto-fix them.
193
+ - A **MEDIUM** finding is auto-fixed only when it is grounded in
194
+ `.codexspec/memory/constitution.md` and the confirmed requirements/spec, and
195
+ it concerns **maintainability, readability, or testability**. Ungrounded or
196
+ purely stylistic MEDIUM findings are report-only.
197
+ - Do not auto-fix advisories or design opportunities, and do not introduce any
198
+ new product decision.
199
+
200
+ #### 7.4 Test-Safe Fixes (never ship red)
201
+
202
+ Every fix must be test-safe. A fix that breaks tests is, by definition, an
203
+ incorrect change — it is never shipped and never silently skipped.
204
+
205
+ - **Functional defects** (logic, correctness, security): follow TDD — add a
206
+ failing test that reproduces the defect (red), apply the fix until that test
207
+ passes (green), then refactor while tests stay green.
208
+ - **Non-functional fixes** (refactors for maintainability, readability,
209
+ testability): run the suite before and after the change. If the change turns
210
+ any test red, revert it, confirm the suite is green again, and re-attempt the
211
+ refactor.
212
+ - If a fix cannot be made green after retry, treat it as **unresolved** and stop
213
+ (see 7.5).
214
+
215
+ #### 7.5 Loop Bounds and Stop Conditions
216
+
217
+ - Run at most **two** fix-and-review rounds. (The per-fix TDD retry in 7.4
218
+ happens within a round; the two-round limit bounds overall effort.)
219
+ - Stop when a defect repeats, remains unresolved, or requires a user or
220
+ architecture decision.
221
+ - After each green round, commit the fixes (see step 8).
222
+
223
+ #### 7.6 Terminal Status
224
+
225
+ - If CRITICAL or HIGH defects remain unresolved after the maximum rounds, the
226
+ status is **"needs work"** — do not claim success.
227
+ - Otherwise report the review outcome: scores, items fixed, items deferred, and
228
+ the final test status.
229
+
230
+ ### 8. Final Report and Commit
231
+
232
+ - Commit any review-driven changes from step 7 that are not already committed.
233
+ - Report the completion summary: files modified, review status (scores, items
234
+ fixed, items deferred, final test status), and the overall status (success or
235
+ "needs work").
File without changes
File without changes
File without changes