specsmith 0.1.4.dev24__tar.gz → 0.1.4.dev26__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 (81) hide show
  1. {specsmith-0.1.4.dev24/src/specsmith.egg-info → specsmith-0.1.4.dev26}/PKG-INFO +1 -1
  2. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/pyproject.toml +1 -1
  3. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/cli.py +48 -1
  4. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/importer.py +196 -91
  5. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/updater.py +13 -8
  6. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26/src/specsmith.egg-info}/PKG-INFO +1 -1
  7. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_auditor.py +0 -1
  8. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_cli.py +0 -1
  9. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_integrations.py +0 -1
  10. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_scaffolder.py +0 -1
  11. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_smoke.py +2 -1
  12. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_vcs.py +0 -1
  13. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/LICENSE +0 -0
  14. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/README.md +0 -0
  15. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/setup.cfg +0 -0
  16. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/__init__.py +0 -0
  17. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/__main__.py +0 -0
  18. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/auditor.py +0 -0
  19. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/commands/__init__.py +0 -0
  20. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/compressor.py +0 -0
  21. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/config.py +0 -0
  22. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/differ.py +0 -0
  23. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/doctor.py +0 -0
  24. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/exporter.py +0 -0
  25. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/__init__.py +0 -0
  26. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/aider.py +0 -0
  27. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/base.py +0 -0
  28. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/claude_code.py +0 -0
  29. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/copilot.py +0 -0
  30. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/cursor.py +0 -0
  31. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/gemini.py +0 -0
  32. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/warp.py +0 -0
  33. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/windsurf.py +0 -0
  34. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/ledger.py +0 -0
  35. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/plugins.py +0 -0
  36. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/releaser.py +0 -0
  37. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/requirements.py +0 -0
  38. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/scaffolder.py +0 -0
  39. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/session.py +0 -0
  40. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/agents.md.j2 +0 -0
  41. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
  42. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
  43. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
  44. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/docs/workflow.md.j2 +0 -0
  45. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/gitattributes.j2 +0 -0
  46. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/gitignore.j2 +0 -0
  47. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
  48. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
  49. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/governance/roles.md.j2 +0 -0
  50. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/governance/rules.md.j2 +0 -0
  51. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/governance/verification.md.j2 +0 -0
  52. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/governance/workflow.md.j2 +0 -0
  53. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/ledger.md.j2 +0 -0
  54. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/pyproject.toml.j2 +0 -0
  55. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/python/cli.py.j2 +0 -0
  56. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/python/init.py.j2 +0 -0
  57. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/readme.md.j2 +0 -0
  58. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
  59. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
  60. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
  61. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
  62. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
  63. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
  64. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/tools.py +0 -0
  65. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/upgrader.py +0 -0
  66. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/validator.py +0 -0
  67. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/vcs/__init__.py +0 -0
  68. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/vcs/base.py +0 -0
  69. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/vcs/bitbucket.py +0 -0
  70. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/vcs/github.py +0 -0
  71. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/vcs/gitlab.py +0 -0
  72. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/vcs_commands.py +0 -0
  73. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith.egg-info/SOURCES.txt +0 -0
  74. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith.egg-info/dependency_links.txt +0 -0
  75. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith.egg-info/entry_points.txt +0 -0
  76. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith.egg-info/requires.txt +0 -0
  77. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith.egg-info/top_level.txt +0 -0
  78. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_compressor.py +0 -0
  79. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_importer.py +0 -0
  80. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_tools.py +0 -0
  81. {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specsmith
3
- Version: 0.1.4.dev24
3
+ Version: 0.1.4.dev26
4
4
  Summary: Forge governed project scaffolds from the Agentic AI Development Workflow Specification.
5
5
  Author: BitConcepts
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "specsmith"
7
- version = "0.1.4.dev24"
7
+ version = "0.1.4.dev26"
8
8
  description = "Forge governed project scaffolds from the Agentic AI Development Workflow Specification."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -478,7 +478,10 @@ def import_project(project_dir: str, force: bool, guided: bool, dry_run: bool) -
478
478
  result = detect_project(root)
479
479
 
480
480
  console.print(f" Files: {result.file_count}")
481
- console.print(f" Language: [cyan]{result.primary_language or 'unknown'}[/cyan]")
481
+ lang_display = result.primary_language or "unknown"
482
+ if result.secondary_languages:
483
+ lang_display += f" + {', '.join(result.secondary_languages)}"
484
+ console.print(f" Languages: [cyan]{lang_display}[/cyan]")
482
485
  console.print(f" Build system: {result.build_system or 'not detected'}")
483
486
  console.print(f" Test framework: {result.test_framework or 'not detected'}")
484
487
  console.print(f" CI: {result.existing_ci or 'not detected'}")
@@ -1205,6 +1208,50 @@ def update_cmd(check_only: bool, auto_yes: bool, project_dir: str) -> None:
1205
1208
  console.print(f" [green]\u2713[/green] {a}")
1206
1209
 
1207
1210
 
1211
+ @main.command(name="self-update")
1212
+ @click.option(
1213
+ "--channel",
1214
+ type=click.Choice(["stable", "dev"]),
1215
+ default=None,
1216
+ help="Force channel (default: auto-detect from installed version).",
1217
+ )
1218
+ @click.option("--version", "target_version", default="", help="Install a specific version.")
1219
+ def self_update_cmd(channel: str | None, target_version: str) -> None:
1220
+ """Update specsmith to the latest version.
1221
+
1222
+ Auto-detects channel: stable builds upgrade to latest stable,
1223
+ dev builds upgrade to latest dev. Use --channel to override.
1224
+ Use --version to pin a specific version.
1225
+ """
1226
+ from specsmith.updater import check_latest_version, get_update_channel, run_self_update
1227
+
1228
+ current_channel = get_update_channel()
1229
+ effective_channel = channel or current_channel
1230
+
1231
+ if target_version:
1232
+ console.print(f"[bold]Installing specsmith {target_version}...[/bold]")
1233
+ success, msg = run_self_update(target_version=target_version)
1234
+ else:
1235
+ current, latest, effective_channel = check_latest_version(channel=effective_channel)
1236
+ if not latest:
1237
+ console.print("[yellow]Could not reach PyPI.[/yellow]")
1238
+ return
1239
+ if current == latest:
1240
+ console.print(
1241
+ f"[green]\u2713[/green] specsmith {current} is up to date ({effective_channel})."
1242
+ )
1243
+ return
1244
+ console.print(f" Current: {current} ({current_channel})")
1245
+ console.print(f" Latest: {latest} ({effective_channel})")
1246
+ success, msg = run_self_update(channel=effective_channel)
1247
+
1248
+ if success:
1249
+ console.print("[green]\u2713[/green] Updated successfully.")
1250
+ console.print(" Restart your shell to use the new version.")
1251
+ else:
1252
+ console.print(f"[red]\u2717[/red] Update failed: {msg}")
1253
+
1254
+
1208
1255
  @main.command(name="migrate-project")
1209
1256
  @click.option("--project-dir", type=click.Path(exists=True), default=".")
1210
1257
  @click.option("--dry-run", is_flag=True, default=False, help="Show changes without writing.")
@@ -101,6 +101,7 @@ class DetectionResult:
101
101
  root: Path
102
102
  languages: dict[str, int] = field(default_factory=dict)
103
103
  primary_language: str = ""
104
+ secondary_languages: list[str] = field(default_factory=list)
104
105
  build_system: str = ""
105
106
  test_framework: str = ""
106
107
  vcs_platform: str = ""
@@ -183,6 +184,14 @@ def detect_project(root: Path) -> DetectionResult:
183
184
  result.languages = dict(lang_counter.most_common())
184
185
  if lang_counter:
185
186
  result.primary_language = lang_counter.most_common(1)[0][0]
187
+ # Secondary languages: all others above a minimum threshold (5 files or 5%)
188
+ total = sum(lang_counter.values())
189
+ threshold = max(5, int(total * 0.05))
190
+ result.secondary_languages = [
191
+ lang
192
+ for lang, count in lang_counter.most_common()
193
+ if lang != result.primary_language and count >= threshold
194
+ ]
186
195
 
187
196
  # Build system — check root AND first-level subdirectories
188
197
  for indicator, system in _BUILD_SYSTEMS.items():
@@ -545,11 +554,86 @@ def _extract_git_contributors(root: Path) -> list[str]:
545
554
  return []
546
555
 
547
556
 
557
+ # Section heading keywords → governance file mapping.
558
+ # Each list is checked with substring matching against lowercased ## headings.
559
+ _RULES_KW: list[str] = [
560
+ "hard rule",
561
+ "stop condition",
562
+ "acceptance",
563
+ "forbidden",
564
+ "mandatory",
565
+ "pre-flight",
566
+ "preflight",
567
+ "critical rule",
568
+ "tool invocation",
569
+ "prohibition",
570
+ "enforcement",
571
+ "final rule",
572
+ "phase enforcement",
573
+ "phase 1",
574
+ "phase 2",
575
+ "repository structure",
576
+ ".work/",
577
+ ".work",
578
+ "path-length",
579
+ "directory structure",
580
+ ]
581
+ _WORKFLOW_KW: list[str] = [
582
+ "session",
583
+ "lifecycle",
584
+ "quick command",
585
+ "ledger",
586
+ "new session",
587
+ "resume session",
588
+ "save session",
589
+ "git commit",
590
+ "git update",
591
+ "startup checklist",
592
+ "stop / save",
593
+ "stop/save",
594
+ "push-to-git",
595
+ "push to git",
596
+ "command handling",
597
+ "roadmap",
598
+ ]
599
+ _ROLES_KW: list[str] = [
600
+ "agent role",
601
+ "drafting",
602
+ "agents are",
603
+ "drafting assistance",
604
+ ]
605
+ _CTX_KW: list[str] = [
606
+ "context",
607
+ "window",
608
+ "budget",
609
+ "context window",
610
+ "token",
611
+ ]
612
+ _VERIFY_KW: list[str] = [
613
+ "verification",
614
+ "conflict",
615
+ "consistency",
616
+ "solver",
617
+ "engine mode",
618
+ "benchmark",
619
+ "deployment",
620
+ "connectivity",
621
+ "target",
622
+ ]
623
+ _DRIFT_KW: list[str] = [
624
+ "environment",
625
+ "platform",
626
+ "shell wrapper",
627
+ "bootstrap",
628
+ "scripts",
629
+ ]
630
+
631
+
548
632
  def _extract_governance_sections(root: Path) -> dict[str, str]:
549
633
  """Extract modular governance content from existing AGENTS.md.
550
634
 
551
- If AGENTS.md exists and has relevant sections, extract them.
552
- Otherwise return generic stubs.
635
+ If AGENTS.md exists and is large, extract sections into modular files.
636
+ Unmatched sections are collected into rules.md so nothing is lost.
553
637
  """
554
638
  defaults = {
555
639
  "rules": (
@@ -591,98 +675,71 @@ def _extract_governance_sections(root: Path) -> dict[str, str]:
591
675
  for line in content.splitlines():
592
676
  if line.startswith("## "):
593
677
  if current_heading and current_lines:
594
- sections[current_heading.lower()] = "\n".join(current_lines).strip()
678
+ sections[current_heading] = "\n".join(current_lines).strip()
595
679
  current_heading = line[3:].strip()
596
680
  current_lines = []
597
681
  else:
598
682
  current_lines.append(line)
599
683
  if current_heading and current_lines:
600
- sections[current_heading.lower()] = "\n".join(current_lines).strip()
601
-
602
- # Map AGENTS.md sections to modular governance files
603
- result = dict(defaults) # Start with defaults
604
-
605
- # Rules: look for HARD RULES, STOP CONDITIONS, ACCEPTANCE STANDARD
606
- rules_parts: list[str] = ["# Rules\n"]
607
- for key in sections:
608
- if any(kw in key for kw in ("hard rule", "stop condition", "acceptance", "forbidden")):
609
- rules_parts.append(f"## {key.title()}\n")
610
- rules_parts.append(sections[key])
611
- rules_parts.append("")
612
- if len(rules_parts) > 1:
613
- result["rules"] = "\n".join(rules_parts) + "\n"
614
-
615
- # Workflow: SESSION LIFECYCLE, QUICK COMMANDS, LEDGER format
616
- wf_parts: list[str] = ["# Workflow\n"]
617
- for key in sections:
618
- if any(
619
- kw in key
620
- for kw in (
621
- "session",
622
- "lifecycle",
623
- "quick command",
624
- "ledger",
625
- "new session",
626
- "resume session",
627
- "save session",
628
- "git commit",
629
- "git update",
630
- )
631
- ):
632
- wf_parts.append(f"## {key.title()}\n")
633
- wf_parts.append(sections[key])
634
- wf_parts.append("")
635
- if len(wf_parts) > 1:
636
- result["workflow"] = "\n".join(wf_parts) + "\n"
637
-
638
- # Roles: AGENT ROLE, DRAFTING ASSISTANCE
639
- roles_parts: list[str] = ["# Roles\n"]
640
- for key in sections:
641
- if any(kw in key for kw in ("agent role", "drafting", "agents are")):
642
- roles_parts.append(f"## {key.title()}\n")
643
- roles_parts.append(sections[key])
644
- roles_parts.append("")
645
- if len(roles_parts) > 1:
646
- result["roles"] = "\n".join(roles_parts) + "\n"
647
-
648
- # Context budget: CONTEXT WINDOW MANAGEMENT
649
- ctx_parts: list[str] = ["# Context Budget\n"]
650
- for key in sections:
651
- if any(kw in key for kw in ("context", "window", "budget")):
652
- ctx_parts.append(f"## {key.title()}\n")
653
- ctx_parts.append(sections[key])
654
- ctx_parts.append("")
655
- if len(ctx_parts) > 1:
656
- result["context-budget"] = "\n".join(ctx_parts) + "\n"
657
-
658
- # Verification: VERIFICATION MINIMUM, CONFLICT AND CONSISTENCY
659
- ver_parts: list[str] = ["# Verification\n"]
660
- for key in sections:
661
- if any(kw in key for kw in ("verification", "conflict", "consistency")):
662
- ver_parts.append(f"## {key.title()}\n")
663
- ver_parts.append(sections[key])
664
- ver_parts.append("")
665
- if len(ver_parts) > 1:
666
- result["verification"] = "\n".join(ver_parts) + "\n"
667
-
668
- # Drift metrics: ENVIRONMENT, PLATFORM EXPECTATIONS, SHELL WRAPPER
669
- drift_parts: list[str] = ["# Environment & Platform\n"]
670
- for key in sections:
671
- if any(
672
- kw in key
673
- for kw in (
674
- "environment",
675
- "platform",
676
- "shell wrapper",
677
- "bootstrap",
678
- "scripts",
679
- )
680
- ):
681
- drift_parts.append(f"## {key.title()}\n")
682
- drift_parts.append(sections[key])
683
- drift_parts.append("")
684
- if len(drift_parts) > 1:
685
- result["drift-metrics"] = "\n".join(drift_parts) + "\n"
684
+ sections[current_heading] = "\n".join(current_lines).strip()
685
+
686
+ if not sections:
687
+ return defaults
688
+
689
+ # Classify each section into a governance category
690
+ category_map: list[tuple[str, list[str]]] = [
691
+ ("rules", _RULES_KW),
692
+ ("workflow", _WORKFLOW_KW),
693
+ ("roles", _ROLES_KW),
694
+ ("context-budget", _CTX_KW),
695
+ ("verification", _VERIFY_KW),
696
+ ("drift-metrics", _DRIFT_KW),
697
+ ]
698
+
699
+ buckets: dict[str, list[tuple[str, str]]] = {
700
+ "rules": [],
701
+ "workflow": [],
702
+ "roles": [],
703
+ "context-budget": [],
704
+ "verification": [],
705
+ "drift-metrics": [],
706
+ }
707
+ unmatched: list[tuple[str, str]] = []
708
+
709
+ for heading, body in sections.items():
710
+ key_lower = heading.lower()
711
+ matched = False
712
+ for category, keywords in category_map:
713
+ if any(kw in key_lower for kw in keywords):
714
+ buckets[category].append((heading, body))
715
+ matched = True
716
+ break # First match wins
717
+ if not matched:
718
+ unmatched.append((heading, body))
719
+
720
+ # Unmatched sections go to rules.md as project-specific rules
721
+ if unmatched:
722
+ buckets["rules"].extend(unmatched)
723
+
724
+ # Build output
725
+ titles = {
726
+ "rules": "# Rules",
727
+ "workflow": "# Workflow",
728
+ "roles": "# Roles",
729
+ "context-budget": "# Context Budget",
730
+ "verification": "# Verification",
731
+ "drift-metrics": "# Environment & Platform",
732
+ }
733
+ result = dict(defaults)
734
+ for category, items in buckets.items():
735
+ if not items:
736
+ continue
737
+ parts: list[str] = [titles[category] + "\n"]
738
+ for heading, body in items:
739
+ parts.append(f"## {heading}\n")
740
+ parts.append(body)
741
+ parts.append("")
742
+ result[category] = "\n".join(parts) + "\n"
686
743
 
687
744
  return result
688
745
 
@@ -799,6 +856,8 @@ def generate_overlay(
799
856
 
800
857
  name = result.root.name
801
858
  lang = result.primary_language or "unknown"
859
+ all_langs = [lang] + (result.secondary_languages or [])
860
+ lang_display = ", ".join(all_langs) if len(all_langs) > 1 else lang
802
861
  today = date.today().isoformat()
803
862
  ptype = result.inferred_type.value if result.inferred_type else "unknown"
804
863
 
@@ -809,7 +868,7 @@ def generate_overlay(
809
868
  "This project was imported by specsmith. The governance files contain "
810
869
  "detected structure. Review and enrich with your agent.\n\n"
811
870
  "## Project Summary\n"
812
- f"- **Language**: {lang}\n"
871
+ f"- **Languages**: {lang_display}\n"
813
872
  f"- **Build system**: {result.build_system or 'not detected'}\n"
814
873
  f"- **Test framework**: {result.test_framework or 'not detected'}\n"
815
874
  f"- **Files detected**: {result.file_count}\n"
@@ -867,7 +926,7 @@ def generate_overlay(
867
926
  f"# Architecture — {name}\n\n"
868
927
  "Architecture auto-generated from project detection.\n\n"
869
928
  "## Overview\n"
870
- f"- **Language**: {lang}\n"
929
+ f"- **Languages**: {lang_display}\n"
871
930
  f"- **Build system**: {result.build_system or 'not detected'}\n"
872
931
  f"- **Test framework**: {result.test_framework or 'not detected'}\n\n"
873
932
  )
@@ -898,6 +957,52 @@ def generate_overlay(
898
957
  _write("docs/governance/verification.md", gov["verification"])
899
958
  _write("docs/governance/drift-metrics.md", gov["drift-metrics"])
900
959
 
960
+ # If existing AGENTS.md is oversized, back it up and replace with a hub.
961
+ agents_path = target / "AGENTS.md"
962
+ if agents_path.exists():
963
+ agents_lines = len(agents_path.read_text(encoding="utf-8").splitlines())
964
+ if agents_lines > 200:
965
+ backup_path = target / "AGENTS.md.bak"
966
+ if not backup_path.exists():
967
+ import shutil
968
+
969
+ shutil.copy2(agents_path, backup_path)
970
+
971
+ hub = (
972
+ f"# {name} \u2014 Agent Governance\n\n"
973
+ f"**Type:** {ptype} \n"
974
+ f"**Language:** {lang} \n\n"
975
+ "---\n\n"
976
+ "## Governance File Registry\n\n"
977
+ "| File | Content | Load timing |\n"
978
+ "| ---- | ------- | ----------- |\n"
979
+ "| `docs/governance/rules.md` | Hard rules, stop conditions, "
980
+ "project-specific rules | Every session start |\n"
981
+ "| `docs/governance/workflow.md` | Session lifecycle, "
982
+ "save/push protocol | Every session start |\n"
983
+ "| `docs/governance/roles.md` | Agent role boundaries | "
984
+ "Every session start |\n"
985
+ "| `docs/governance/context-budget.md` | Context management | "
986
+ "Every session start |\n"
987
+ "| `docs/governance/verification.md` | Verification, "
988
+ "consistency | When verifying |\n"
989
+ "| `docs/governance/drift-metrics.md` | Environment, "
990
+ "platform | On audit |\n\n"
991
+ "Other project documents:\n\n"
992
+ "| File | Content |\n"
993
+ "| ---- | ------- |\n"
994
+ "| `LEDGER.md` | Append-only work record |\n"
995
+ "| `docs/REQUIREMENTS.md` | Formal requirements |\n"
996
+ "| `docs/TEST_SPEC.md` | Test cases |\n"
997
+ "| `docs/architecture.md` | Architecture |\n\n"
998
+ "---\n\n"
999
+ f"*Original AGENTS.md ({agents_lines} lines) backed up "
1000
+ "to AGENTS.md.bak. Content extracted into modular "
1001
+ "governance files above.*\n"
1002
+ )
1003
+ agents_path.write_text(hub, encoding="utf-8")
1004
+ created.append(agents_path)
1005
+
901
1006
  # --- CI config (merge: only create if no CI detected) ---
902
1007
  if not result.existing_ci and result.vcs_platform:
903
1008
  try:
@@ -59,17 +59,22 @@ def is_outdated() -> bool:
59
59
  return current != latest
60
60
 
61
61
 
62
- def run_self_update(*, channel: str = "") -> tuple[bool, str]:
62
+ def run_self_update(
63
+ *, channel: str = "", target_version: str = "",
64
+ ) -> tuple[bool, str]:
63
65
  """Update specsmith via pip.
64
66
 
65
- Uses --pre flag for dev channel.
67
+ If target_version is set, installs that exact version.
68
+ Otherwise uses --pre flag for dev channel, plain upgrade for stable.
66
69
  """
67
- if not channel:
68
- channel = get_update_channel()
69
-
70
- cmd = ["pip", "install", "--upgrade", "specsmith"]
71
- if channel == "dev":
72
- cmd.insert(2, "--pre")
70
+ if target_version:
71
+ cmd = ["pip", "install", f"specsmith=={target_version}"]
72
+ else:
73
+ if not channel:
74
+ channel = get_update_channel()
75
+ cmd = ["pip", "install", "--upgrade", "specsmith"]
76
+ if channel == "dev":
77
+ cmd.insert(2, "--pre")
73
78
 
74
79
  try:
75
80
  result = subprocess.run(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specsmith
3
- Version: 0.1.4.dev24
3
+ Version: 0.1.4.dev26
4
4
  Summary: Forge governed project scaffolds from the Agentic AI Development Workflow Specification.
5
5
  Author: BitConcepts
6
6
  License: MIT
@@ -7,7 +7,6 @@ from __future__ import annotations
7
7
  from pathlib import Path
8
8
 
9
9
  import pytest
10
-
11
10
  from specsmith.auditor import run_audit
12
11
 
13
12
 
@@ -8,7 +8,6 @@ from pathlib import Path
8
8
 
9
9
  import yaml
10
10
  from click.testing import CliRunner
11
-
12
11
  from specsmith.cli import main
13
12
  from specsmith.config import ProjectConfig, ProjectType
14
13
  from specsmith.scaffolder import scaffold_project
@@ -7,7 +7,6 @@ from __future__ import annotations
7
7
  from pathlib import Path
8
8
 
9
9
  import pytest
10
-
11
10
  from specsmith.config import ProjectConfig, ProjectType
12
11
  from specsmith.integrations import get_adapter, list_adapters
13
12
  from specsmith.integrations.claude_code import ClaudeCodeAdapter
@@ -7,7 +7,6 @@ from __future__ import annotations
7
7
  from pathlib import Path
8
8
 
9
9
  import pytest
10
-
11
10
  from specsmith.config import Platform, ProjectConfig, ProjectType
12
11
  from specsmith.scaffolder import scaffold_project
13
12
 
@@ -4,9 +4,10 @@
4
4
 
5
5
  from importlib.metadata import version as _pkg_version
6
6
 
7
- from specsmith import __version__
8
7
  from specsmith.config import Platform, ProjectConfig, ProjectType
9
8
 
9
+ from specsmith import __version__
10
+
10
11
 
11
12
  def test_version():
12
13
  """Version string matches installed package metadata."""
@@ -7,7 +7,6 @@ from __future__ import annotations
7
7
  from pathlib import Path
8
8
 
9
9
  import pytest
10
-
11
10
  from specsmith.config import Platform, ProjectConfig, ProjectType
12
11
  from specsmith.vcs import get_platform, list_platforms
13
12
  from specsmith.vcs.base import CommandResult
File without changes