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.
- {specsmith-0.1.4.dev24/src/specsmith.egg-info → specsmith-0.1.4.dev26}/PKG-INFO +1 -1
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/pyproject.toml +1 -1
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/cli.py +48 -1
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/importer.py +196 -91
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/updater.py +13 -8
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26/src/specsmith.egg-info}/PKG-INFO +1 -1
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_auditor.py +0 -1
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_cli.py +0 -1
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_integrations.py +0 -1
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_scaffolder.py +0 -1
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_smoke.py +2 -1
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_vcs.py +0 -1
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/LICENSE +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/README.md +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/setup.cfg +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/__init__.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/__main__.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/auditor.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/commands/__init__.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/compressor.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/config.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/differ.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/doctor.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/exporter.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/__init__.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/aider.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/base.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/claude_code.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/copilot.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/cursor.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/gemini.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/warp.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/integrations/windsurf.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/ledger.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/plugins.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/releaser.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/requirements.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/scaffolder.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/session.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/agents.md.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/docs/workflow.md.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/gitattributes.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/gitignore.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/governance/roles.md.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/governance/rules.md.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/governance/verification.md.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/governance/workflow.md.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/ledger.md.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/pyproject.toml.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/python/cli.py.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/python/init.py.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/readme.md.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/tools.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/upgrader.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/validator.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/vcs/__init__.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/vcs/base.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/vcs/bitbucket.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/vcs/github.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/vcs/gitlab.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/vcs_commands.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith.egg-info/SOURCES.txt +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith.egg-info/dependency_links.txt +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith.egg-info/entry_points.txt +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith.egg-info/requires.txt +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith.egg-info/top_level.txt +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_compressor.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_importer.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_tools.py +0 -0
- {specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/tests/test_validator.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specsmith"
|
|
7
|
-
version = "0.1.4.
|
|
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
|
-
|
|
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
|
|
552
|
-
|
|
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
|
|
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
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
#
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
for
|
|
651
|
-
if
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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"- **
|
|
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"- **
|
|
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(
|
|
62
|
+
def run_self_update(
|
|
63
|
+
*, channel: str = "", target_version: str = "",
|
|
64
|
+
) -> tuple[bool, str]:
|
|
63
65
|
"""Update specsmith via pip.
|
|
64
66
|
|
|
65
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
cmd
|
|
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(
|
|
@@ -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
|
|
@@ -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."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/docs/architecture.md.j2
RENAMED
|
File without changes
|
{specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/docs/requirements.md.j2
RENAMED
|
File without changes
|
{specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/docs/test-spec.md.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/governance/roles.md.j2
RENAMED
|
File without changes
|
{specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/governance/rules.md.j2
RENAMED
|
File without changes
|
|
File without changes
|
{specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/governance/workflow.md.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{specsmith-0.1.4.dev24 → specsmith-0.1.4.dev26}/src/specsmith/templates/scripts/setup.cmd.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|