codexspec 0.6.3__tar.gz → 0.6.5__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 (57) hide show
  1. {codexspec-0.6.3 → codexspec-0.6.5}/PKG-INFO +1 -1
  2. {codexspec-0.6.3 → codexspec-0.6.5}/pyproject.toml +1 -1
  3. {codexspec-0.6.3 → codexspec-0.6.5}/src/codexspec/__init__.py +63 -11
  4. {codexspec-0.6.3 → codexspec-0.6.5}/src/codexspec/commands/installer.py +49 -1
  5. {codexspec-0.6.3 → codexspec-0.6.5}/src/codexspec/i18n.py +18 -0
  6. {codexspec-0.6.3 → codexspec-0.6.5}/src/codexspec/translator.py +34 -72
  7. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/implement-tasks.md +98 -5
  8. {codexspec-0.6.3 → codexspec-0.6.5}/.gitignore +0 -0
  9. {codexspec-0.6.3 → codexspec-0.6.5}/LICENSE +0 -0
  10. {codexspec-0.6.3 → codexspec-0.6.5}/README.md +0 -0
  11. {codexspec-0.6.3 → codexspec-0.6.5}/codexspec-icon.svg +0 -0
  12. {codexspec-0.6.3 → codexspec-0.6.5}/codexspec-logo-dark.svg +0 -0
  13. {codexspec-0.6.3 → codexspec-0.6.5}/codexspec-logo-light.svg +0 -0
  14. {codexspec-0.6.3 → codexspec-0.6.5}/scripts/bash/check-i18n-completeness.sh +0 -0
  15. {codexspec-0.6.3 → codexspec-0.6.5}/scripts/bash/check-i18n-structure.sh +0 -0
  16. {codexspec-0.6.3 → codexspec-0.6.5}/scripts/bash/check-prerequisites.sh +0 -0
  17. {codexspec-0.6.3 → codexspec-0.6.5}/scripts/bash/common.sh +0 -0
  18. {codexspec-0.6.3 → codexspec-0.6.5}/scripts/bash/create-new-feature.sh +0 -0
  19. {codexspec-0.6.3 → codexspec-0.6.5}/scripts/powershell/check-prerequisites.ps1 +0 -0
  20. {codexspec-0.6.3 → codexspec-0.6.5}/scripts/powershell/common.ps1 +0 -0
  21. {codexspec-0.6.3 → codexspec-0.6.5}/scripts/powershell/create-new-feature.ps1 +0 -0
  22. {codexspec-0.6.3 → codexspec-0.6.5}/src/codexspec/commands/__init__.py +0 -0
  23. {codexspec-0.6.3 → codexspec-0.6.5}/src/codexspec/idea.md +0 -0
  24. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/analyze.md +0 -0
  25. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/checklist.md +0 -0
  26. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/clarify.md +0 -0
  27. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/commit-staged.md +0 -0
  28. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/config.md +0 -0
  29. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/constitution.md +0 -0
  30. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/generate-spec.md +0 -0
  31. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/plan-to-tasks.md +0 -0
  32. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/pr.md +0 -0
  33. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/quick.md +0 -0
  34. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/review-code.md +0 -0
  35. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/review-plan.md +0 -0
  36. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/review-spec.md +0 -0
  37. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/review-tasks.md +0 -0
  38. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/spec-to-plan.md +0 -0
  39. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/specify.md +0 -0
  40. {codexspec-0.6.3 → codexspec-0.6.5}/templates/commands/tasks-to-issues.md +0 -0
  41. {codexspec-0.6.3 → codexspec-0.6.5}/templates/docs/checklist-template.md +0 -0
  42. {codexspec-0.6.3 → codexspec-0.6.5}/templates/docs/constitution-template.md +0 -0
  43. {codexspec-0.6.3 → codexspec-0.6.5}/templates/docs/plan-template-detailed.md +0 -0
  44. {codexspec-0.6.3 → codexspec-0.6.5}/templates/docs/plan-template-simple.md +0 -0
  45. {codexspec-0.6.3 → codexspec-0.6.5}/templates/docs/requirements-template.md +0 -0
  46. {codexspec-0.6.3 → codexspec-0.6.5}/templates/docs/spec-template-detailed.md +0 -0
  47. {codexspec-0.6.3 → codexspec-0.6.5}/templates/docs/spec-template-simple.md +0 -0
  48. {codexspec-0.6.3 → codexspec-0.6.5}/templates/docs/tasks-template-detailed.md +0 -0
  49. {codexspec-0.6.3 → codexspec-0.6.5}/templates/docs/tasks-template-simple.md +0 -0
  50. {codexspec-0.6.3 → codexspec-0.6.5}/templates/translations/de.json +0 -0
  51. {codexspec-0.6.3 → codexspec-0.6.5}/templates/translations/en.json +0 -0
  52. {codexspec-0.6.3 → codexspec-0.6.5}/templates/translations/es.json +0 -0
  53. {codexspec-0.6.3 → codexspec-0.6.5}/templates/translations/fr.json +0 -0
  54. {codexspec-0.6.3 → codexspec-0.6.5}/templates/translations/ja.json +0 -0
  55. {codexspec-0.6.3 → codexspec-0.6.5}/templates/translations/ko.json +0 -0
  56. {codexspec-0.6.3 → codexspec-0.6.5}/templates/translations/pt-BR.json +0 -0
  57. {codexspec-0.6.3 → codexspec-0.6.5}/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.3
3
+ Version: 0.6.5
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codexspec"
3
- version = "0.6.3"
3
+ version = "0.6.5"
4
4
  description = "CodexSpec - A Requirements-First SDD toolkit for Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -26,9 +26,11 @@ from .commands.installer import (
26
26
  install_commands_to_subdir,
27
27
  migrate_old_commands,
28
28
  should_update_commands,
29
+ update_installed_command_frontmatter,
29
30
  )
30
31
  from .i18n import (
31
32
  generate_config_content,
33
+ get_explicit_language_key,
32
34
  get_interaction_language,
33
35
  get_language_name,
34
36
  get_supported_languages,
@@ -36,7 +38,7 @@ from .i18n import (
36
38
  normalize_locale,
37
39
  update_language_field,
38
40
  )
39
- from .translator import translate
41
+ from .translator import SUPPORTED_LANGUAGES, translate
40
42
 
41
43
  # Version info
42
44
  __version__ = "0.6.0"
@@ -270,6 +272,7 @@ def config(
270
272
  else:
271
273
  console.print("[red]Failed to update language setting[/red]")
272
274
  raise typer.Exit(1)
275
+ _rerender_command_frontmatter(config_file)
273
276
  return
274
277
 
275
278
  # Handle set commit language
@@ -348,6 +351,7 @@ def config(
348
351
  else:
349
352
  console.print("[red]Failed to update interaction language setting[/red]")
350
353
  raise typer.Exit(1)
354
+ _rerender_command_frontmatter(config_file)
351
355
  return
352
356
 
353
357
  # Handle set document language
@@ -467,9 +471,11 @@ def init(
467
471
  "--force",
468
472
  "-f",
469
473
  help=(
470
- "Overwrite existing files and auto-confirm prompts (skips the language "
471
- "prompt and command-update/migration confirms). Language config is updated "
472
- "surgically, never fully regenerated."
474
+ "Overwrite regenerable files (commands/scripts) and auto-confirm prompts "
475
+ "(skips the language prompt and command-update/migration confirms). "
476
+ "User-authored content is preserved: language config is updated surgically "
477
+ "and an existing CLAUDE.md body is never overwritten (only the constitution "
478
+ "@import is prepended if missing)."
473
479
  ),
474
480
  ),
475
481
  no_git: bool = typer.Option(
@@ -553,21 +559,40 @@ def init(
553
559
  )
554
560
  prompted_base = False
555
561
  if base_determinable:
556
- normalized_lang = normalize_locale(output_value) if output_value else "en"
562
+ # Output base already resolved (or None when only dimension flags were
563
+ # given); no prompt needed to resolve it.
564
+ pass
557
565
  elif config_exists:
558
- normalized_lang = get_interaction_language(config_file)
566
+ # Existing project: preserve every unspecified key (REQ-007); no prompt.
567
+ pass
559
568
  elif sys.stdin.isatty():
560
569
  try:
561
570
  output_value = normalize_locale(prompt_language_selection())
562
571
  prompted_base = True
563
- normalized_lang = output_value
564
572
  except KeyboardInterrupt:
565
573
  console.print()
566
574
  console.print("[yellow]Selection cancelled, using default language (en)[/yellow]")
567
575
  output_value = "en"
568
- normalized_lang = "en"
569
576
  else:
570
577
  output_value = "en"
578
+
579
+ # Render/CLI-message language = the effective interaction language for THIS
580
+ # run (frontmatter descriptions are an interaction-language artifact). This is
581
+ # deliberately decoupled from output_value/prompted_base above, which only
582
+ # drive config writing and the first-time prompt. Precedence: explicit
583
+ # --interaction-lang -> explicitly-configured interaction -> --lang (output
584
+ # base) -> explicitly-configured output -> "en". Existing interaction is
585
+ # deliberately stronger than --lang; existing output is only a fallback when
586
+ # this run did not provide a new base.
587
+ if "interaction" in lang_overrides:
588
+ normalized_lang = lang_overrides["interaction"]
589
+ elif config_exists and (explicit_interaction := get_explicit_language_key(config_file, "interaction")) is not None:
590
+ normalized_lang = explicit_interaction
591
+ elif output_value is not None:
592
+ normalized_lang = output_value
593
+ elif config_exists and (explicit_output := get_explicit_language_key(config_file, "output")) is not None:
594
+ normalized_lang = explicit_output
595
+ else:
571
596
  normalized_lang = "en"
572
597
 
573
598
  if debug:
@@ -749,15 +774,19 @@ def init(
749
774
  console.print(f"[dim]{translate('cli.init.language_dimensions_hint', normalized_lang)}[/dim]")
750
775
 
751
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.
752
781
  claude_md = target_dir / "CLAUDE.md"
753
- if not claude_md.exists() or force:
782
+ if not claude_md.exists():
754
783
  project_name = target_dir.name
755
784
  claude_md.write_text(_get_claude_md_content(project_name), encoding="utf-8")
756
785
  console.print(f"[green]{translate('cli.init.created_file', normalized_lang, file='CLAUDE.md')}[/green]")
757
786
  else:
758
- # Check if existing CLAUDE.md has compliance section
787
+ # Existing CLAUDE.md: ensure the compliance @import, never clobber the body.
759
788
  if not has_compliance_section(claude_md):
760
- if confirm_add_compliance(normalized_lang):
789
+ if force or confirm_add_compliance(normalized_lang):
761
790
  prepend_compliance_section(claude_md)
762
791
  console.print(f"[green]{translate('cli.init.compliance_added', normalized_lang)}[/green]")
763
792
 
@@ -802,6 +831,29 @@ def init(
802
831
  console.print(f"[yellow]{translate('cli.init.important_action', normalized_lang)}[/yellow]")
803
832
 
804
833
 
834
+ def _rerender_command_frontmatter(config_file: Path) -> None:
835
+ """Re-render installed command frontmatter in the current interaction language.
836
+
837
+ Called after a ``config`` language change so ``.claude/commands/codexspec/*.md``
838
+ descriptions follow the new interaction language immediately, mirroring what
839
+ ``init`` does. No-op when commands are not installed. Languages without a
840
+ pre-translated cache are not re-rendered here (that path would invoke the
841
+ ``claude`` CLI once per command); the user is advised to run ``codexspec init``.
842
+ """
843
+ commands_subdir = Path.cwd() / ".claude" / "commands" / COMMANDS_SUBDIR
844
+ if not commands_subdir.exists():
845
+ return
846
+ language = get_interaction_language(config_file) # re-read AFTER the config write
847
+ if language != "en" and language not in SUPPORTED_LANGUAGES:
848
+ console.print(f"[dim]Run [cyan]codexspec init[/cyan] to update command descriptions for {language}.[/dim]")
849
+ return
850
+ templates_dir = get_templates_dir() / "commands"
851
+ if not templates_dir.exists():
852
+ return
853
+ update_installed_command_frontmatter(commands_subdir, templates_dir, language=language)
854
+ console.print(f"[dim]Updated command descriptions to: {get_language_name(language)}[/dim]")
855
+
856
+
805
857
  def _print_command_summary(language: str = "en") -> None:
806
858
  """Print a summary of installed commands grouped by category."""
807
859
  metadata = get_commands_metadata()
@@ -8,7 +8,12 @@ import shutil
8
8
  from pathlib import Path
9
9
  from typing import Optional, TypedDict
10
10
 
11
- from codexspec.translator import extract_frontmatter_fields, load_translation_cache, translate_template_frontmatter
11
+ from codexspec.translator import (
12
+ apply_translations_to_template,
13
+ extract_frontmatter_fields,
14
+ load_translation_cache,
15
+ translate_template_frontmatter,
16
+ )
12
17
 
13
18
  # Constants
14
19
  COMMANDS_SUBDIR = "codexspec" # Subdirectory name for commands
@@ -348,6 +353,49 @@ def install_commands_to_subdir(
348
353
  return installed_count
349
354
 
350
355
 
356
+ def update_installed_command_frontmatter(
357
+ target_dir: Path,
358
+ templates_dir: Path,
359
+ language: str = "en",
360
+ translations_dir: Optional[Path] = None,
361
+ ) -> int:
362
+ """Update frontmatter translations for already-installed command files.
363
+
364
+ This preserves command bodies and any non-translated frontmatter while
365
+ refreshing the language-specific ``description`` and ``argument-hint`` fields.
366
+ Missing installed commands are intentionally skipped.
367
+ """
368
+ if not target_dir.exists() or not templates_dir.exists():
369
+ return 0
370
+
371
+ translation_cache = None
372
+ if language != "en":
373
+ translation_cache = load_translation_cache(language, translations_dir)
374
+
375
+ updated_count = 0
376
+ for template_file in templates_dir.glob("*.md"):
377
+ target_path = target_dir / template_file.name
378
+ if not target_path.exists():
379
+ continue
380
+
381
+ template_content = template_file.read_text(encoding="utf-8")
382
+ template_name = template_file.stem
383
+ rendered_template = translate_template_frontmatter(template_content, template_name, language, translation_cache)
384
+ fields = {
385
+ key: value for key, value in extract_frontmatter_fields(rendered_template).items() if value is not None
386
+ }
387
+ if not fields:
388
+ continue
389
+
390
+ current_content = target_path.read_text(encoding="utf-8")
391
+ updated_content = apply_translations_to_template(current_content, fields)
392
+ if updated_content != current_content:
393
+ target_path.write_text(updated_content, encoding="utf-8")
394
+ updated_count += 1
395
+
396
+ return updated_count
397
+
398
+
351
399
  def should_update_commands(codexspec_dir: Path) -> bool:
352
400
  """Check if commands need updating (subdirectory already exists).
353
401
 
@@ -290,6 +290,24 @@ def _resolve_language(config_file: Path, primary_key: str) -> str:
290
290
  return "en"
291
291
 
292
292
 
293
+ def get_explicit_language_key(config_file: Optional[Path], key: str) -> Optional[str]:
294
+ """Return one explicitly configured ``language.<key>`` value, if present.
295
+
296
+ This differs from the public language-resolution helpers because it never
297
+ falls back to ``language.output`` or ``"en"``.
298
+ """
299
+ path = config_file or _default_config_path()
300
+ if not path.exists():
301
+ return None
302
+ try:
303
+ value = _read_language_key(path.read_text(encoding="utf-8"), key)
304
+ if value:
305
+ return normalize_locale(value) or None
306
+ except (OSError, re.error):
307
+ pass
308
+ return None
309
+
310
+
293
311
  def _default_config_path() -> Path:
294
312
  return Path.cwd() / ".codexspec" / "config.yml"
295
313
 
@@ -7,8 +7,6 @@ frontmatter fields (description, argument-hint).
7
7
  import json
8
8
  import logging
9
9
  import re
10
- import shutil
11
- import subprocess
12
10
  from pathlib import Path
13
11
  from typing import Optional
14
12
 
@@ -336,87 +334,56 @@ def apply_translations_to_template(content: str, translations: dict) -> str:
336
334
  Returns:
337
335
  Template content with translated frontmatter
338
336
  """
337
+ frontmatter_match = re.match(r"^(---\n)(.*?)(\n---(?:\n|$))", content, re.DOTALL)
338
+ if not frontmatter_match:
339
+ return content
340
+
341
+ prefix, frontmatter, suffix = frontmatter_match.groups()
342
+
339
343
  # Replace description
340
344
  if "description" in translations:
341
- content = re.sub(r"^(description:\s*).+$", rf"\g<1>{translations['description']}", content, flags=re.MULTILINE)
345
+ frontmatter = re.sub(
346
+ r"^(description:\s*).+$",
347
+ lambda match: f"{match.group(1)}{translations['description']}",
348
+ frontmatter,
349
+ flags=re.MULTILINE,
350
+ )
342
351
 
343
352
  # Replace argument-hint (handle both single-line and multiline)
344
353
  if "argument-hint" in translations:
345
354
  new_hint = translations["argument-hint"]
346
355
  # Check if original hint is multiline (uses |)
347
- if re.search(r"^argument-hint:\s*\|", content, re.MULTILINE):
356
+ if re.search(r"^argument-hint:\s*\|", frontmatter, re.MULTILINE):
348
357
  # Build new multiline content with proper indentation
349
358
  # Only add indentation to non-empty lines
350
359
  hint_lines = new_hint.split("\n")
351
360
  indented_lines = [(" " + line) if line else "" for line in hint_lines]
352
361
  indented_hint = "\n".join(indented_lines)
353
362
  # Replace multiline hint - match the entire multiline block (supports empty lines)
354
- content = re.sub(
363
+ frontmatter = re.sub(
355
364
  r"(argument-hint:\s*\|)\s*\n((?: .+\n?|\s*\n)+)",
356
- rf"\g<1>\n{indented_hint}\n",
357
- content,
365
+ lambda match: f"{match.group(1)}\n{indented_hint}\n",
366
+ frontmatter,
358
367
  flags=re.MULTILINE,
359
368
  )
360
369
  else:
361
370
  # Replace single-line hint (with or without quotes)
362
- if re.search(r'^argument-hint:\s*"', content, re.MULTILINE):
363
- content = re.sub(r'^(argument-hint:\s*)"[^"]+"$', rf'\g<1>"{new_hint}"', content, flags=re.MULTILINE)
371
+ if re.search(r'^argument-hint:\s*"', frontmatter, re.MULTILINE):
372
+ frontmatter = re.sub(
373
+ r'^(argument-hint:\s*)"[^"]+"$',
374
+ lambda match: f'{match.group(1)}"{new_hint}"',
375
+ frontmatter,
376
+ flags=re.MULTILINE,
377
+ )
364
378
  else:
365
- content = re.sub(r"^(argument-hint:\s*).+$", rf"\g<1>{new_hint}", content, flags=re.MULTILINE)
366
-
367
- return content
368
-
369
-
370
- def check_claude_cli_available() -> bool:
371
- """Check if Claude CLI is available.
372
-
373
- Returns:
374
- True if claude command is available
375
- """
376
- return shutil.which("claude") is not None
379
+ frontmatter = re.sub(
380
+ r"^(argument-hint:\s*).+$",
381
+ lambda match: f"{match.group(1)}{new_hint}",
382
+ frontmatter,
383
+ flags=re.MULTILINE,
384
+ )
377
385
 
378
-
379
- def translate_with_claude_cli(
380
- texts: dict[str, dict[str, str]], target_lang: str, timeout: int = 60
381
- ) -> Optional[dict[str, dict[str, str]]]:
382
- """Translate texts using Claude CLI.
383
-
384
- Args:
385
- texts: Dictionary of {command_name: {field: text}}
386
- target_lang: Target language code
387
- timeout: Timeout in seconds
388
-
389
- Returns:
390
- Translated dictionary or None on failure
391
- """
392
- if not check_claude_cli_available():
393
- return None
394
-
395
- # Build prompt
396
- input_json = json.dumps(texts, ensure_ascii=False, indent=2)
397
- prompt = f"""Translate the following JSON values to {target_lang}.
398
- Keep all keys unchanged. Return only valid JSON with the same structure.
399
- Do not translate technical terms like "spec.md", "plan.md", "TDD", etc.
400
-
401
- {input_json}"""
402
-
403
- try:
404
- result = subprocess.run(["claude", "--print", prompt], capture_output=True, text=True, timeout=timeout)
405
-
406
- if result.returncode != 0:
407
- return None
408
-
409
- # Parse JSON from output
410
- output = result.stdout.strip()
411
- # Remove potential markdown code blocks
412
- if output.startswith("```"):
413
- output = re.sub(r"^```(?:json)?\n?", "", output)
414
- output = re.sub(r"\n?```$", "", output)
415
-
416
- return json.loads(output)
417
-
418
- except (subprocess.TimeoutExpired, json.JSONDecodeError, OSError):
419
- return None
386
+ return f"{prefix}{frontmatter}{suffix}{content[frontmatter_match.end() :]}"
420
387
 
421
388
 
422
389
  def translate_template_frontmatter(
@@ -445,13 +412,8 @@ def translate_template_frontmatter(
445
412
  if cache and template_name in cache:
446
413
  return apply_translations_to_template(template_content, cache[template_name])
447
414
 
448
- # For non-cached languages, try dynamic translation
449
- if target_lang not in SUPPORTED_LANGUAGES:
450
- fields = extract_frontmatter_fields(template_content)
451
- if fields.get("description") or fields.get("argument-hint"):
452
- translations = translate_with_claude_cli({template_name: fields}, target_lang)
453
- if translations and template_name in translations:
454
- return apply_translations_to_template(template_content, translations[template_name])
455
-
456
- # Return original if all translation attempts fail
415
+ # No cache for this language: keep the original (English) frontmatter.
416
+ # Command templates are translated at runtime by Claude reading config.yml
417
+ # (see the template "Language Preference" section), never via a subprocess
418
+ # at install time.
457
419
  return template_content
@@ -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
File without changes