devspec 0.1.0__py3-none-any.whl

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 (133) hide show
  1. devspec-0.1.0.dist-info/METADATA +522 -0
  2. devspec-0.1.0.dist-info/RECORD +133 -0
  3. devspec-0.1.0.dist-info/WHEEL +4 -0
  4. devspec-0.1.0.dist-info/entry_points.txt +2 -0
  5. devspec-0.1.0.dist-info/licenses/LICENSE +201 -0
  6. devspec_installer/__init__.py +3 -0
  7. devspec_installer/__main__.py +5 -0
  8. devspec_installer/cli.py +615 -0
  9. devspec_installer/payload/.agents/rules/devspec-workflow.md +25 -0
  10. devspec_installer/payload/.agents/skills/devspec-clarify.md +14 -0
  11. devspec_installer/payload/.agents/skills/devspec-codebase-structure.md +14 -0
  12. devspec_installer/payload/.agents/skills/devspec-coding-standards.md +14 -0
  13. devspec_installer/payload/.agents/skills/devspec-diagram.md +14 -0
  14. devspec_installer/payload/.agents/skills/devspec-extract.md +14 -0
  15. devspec_installer/payload/.agents/skills/devspec-finalize.md +14 -0
  16. devspec_installer/payload/.agents/skills/devspec-implement.md +14 -0
  17. devspec_installer/payload/.agents/skills/devspec-projectcontext.md +14 -0
  18. devspec_installer/payload/.agents/skills/devspec-review.md +14 -0
  19. devspec_installer/payload/.agents/skills/devspec-rules.md +14 -0
  20. devspec_installer/payload/.agents/skills/devspec-story.md +14 -0
  21. devspec_installer/payload/.agents/skills/devspec-tasks.md +14 -0
  22. devspec_installer/payload/.agents/skills/devspec-techstack.md +14 -0
  23. devspec_installer/payload/.claude/skills/devspec-clarify/SKILL.md +13 -0
  24. devspec_installer/payload/.claude/skills/devspec-codebase-structure/SKILL.md +13 -0
  25. devspec_installer/payload/.claude/skills/devspec-coding-standards/SKILL.md +13 -0
  26. devspec_installer/payload/.claude/skills/devspec-diagram/SKILL.md +13 -0
  27. devspec_installer/payload/.claude/skills/devspec-extract/SKILL.md +13 -0
  28. devspec_installer/payload/.claude/skills/devspec-finalize/SKILL.md +13 -0
  29. devspec_installer/payload/.claude/skills/devspec-implement/SKILL.md +13 -0
  30. devspec_installer/payload/.claude/skills/devspec-projectcontext/SKILL.md +13 -0
  31. devspec_installer/payload/.claude/skills/devspec-review/SKILL.md +13 -0
  32. devspec_installer/payload/.claude/skills/devspec-rules/SKILL.md +13 -0
  33. devspec_installer/payload/.claude/skills/devspec-story/SKILL.md +13 -0
  34. devspec_installer/payload/.claude/skills/devspec-tasks/SKILL.md +13 -0
  35. devspec_installer/payload/.claude/skills/devspec-techstack/SKILL.md +13 -0
  36. devspec_installer/payload/.cursor/rules/devspec-workflow.mdc +23 -0
  37. devspec_installer/payload/.gemini/commands/devspec/clarify.toml +14 -0
  38. devspec_installer/payload/.gemini/commands/devspec/codebase-structure.toml +14 -0
  39. devspec_installer/payload/.gemini/commands/devspec/coding-standards.toml +14 -0
  40. devspec_installer/payload/.gemini/commands/devspec/diagram.toml +14 -0
  41. devspec_installer/payload/.gemini/commands/devspec/extract.toml +14 -0
  42. devspec_installer/payload/.gemini/commands/devspec/finalize.toml +14 -0
  43. devspec_installer/payload/.gemini/commands/devspec/implement.toml +14 -0
  44. devspec_installer/payload/.gemini/commands/devspec/projectcontext.toml +14 -0
  45. devspec_installer/payload/.gemini/commands/devspec/review.toml +14 -0
  46. devspec_installer/payload/.gemini/commands/devspec/rules.toml +14 -0
  47. devspec_installer/payload/.gemini/commands/devspec/story.toml +14 -0
  48. devspec_installer/payload/.gemini/commands/devspec/tasks.toml +14 -0
  49. devspec_installer/payload/.gemini/commands/devspec/techstack.toml +14 -0
  50. devspec_installer/payload/.github/agents/devspec.clarify.agent.md +39 -0
  51. devspec_installer/payload/.github/agents/devspec.codebase-structure.agent.md +39 -0
  52. devspec_installer/payload/.github/agents/devspec.coding-standards.agent.md +40 -0
  53. devspec_installer/payload/.github/agents/devspec.diagram.agent.md +76 -0
  54. devspec_installer/payload/.github/agents/devspec.extract.agent.md +91 -0
  55. devspec_installer/payload/.github/agents/devspec.finalize.agent.md +51 -0
  56. devspec_installer/payload/.github/agents/devspec.implement-task.agent.md +67 -0
  57. devspec_installer/payload/.github/agents/devspec.projectcontext.agent.md +34 -0
  58. devspec_installer/payload/.github/agents/devspec.review.agent.md +42 -0
  59. devspec_installer/payload/.github/agents/devspec.rules.agent.md +35 -0
  60. devspec_installer/payload/.github/agents/devspec.story.agent.md +54 -0
  61. devspec_installer/payload/.github/agents/devspec.tasks.agent.md +44 -0
  62. devspec_installer/payload/.github/agents/devspec.techstack.agent.md +35 -0
  63. devspec_installer/payload/.github/prompts/PATTERNS.md +278 -0
  64. devspec_installer/payload/.github/prompts/README.md +92 -0
  65. devspec_installer/payload/.github/prompts/devspec.clarify.prompt.md +11 -0
  66. devspec_installer/payload/.github/prompts/devspec.codebase-structure.prompt.md +11 -0
  67. devspec_installer/payload/.github/prompts/devspec.coding-standards.prompt.md +11 -0
  68. devspec_installer/payload/.github/prompts/devspec.diagram.prompt.md +11 -0
  69. devspec_installer/payload/.github/prompts/devspec.extract.prompt.md +11 -0
  70. devspec_installer/payload/.github/prompts/devspec.finalize.prompt.md +11 -0
  71. devspec_installer/payload/.github/prompts/devspec.implement.prompt.md +11 -0
  72. devspec_installer/payload/.github/prompts/devspec.projectcontext.prompt.md +11 -0
  73. devspec_installer/payload/.github/prompts/devspec.review.prompt.md +11 -0
  74. devspec_installer/payload/.github/prompts/devspec.rules.prompt.md +11 -0
  75. devspec_installer/payload/.github/prompts/devspec.story.prompt.md +11 -0
  76. devspec_installer/payload/.github/prompts/devspec.tasks.prompt.md +11 -0
  77. devspec_installer/payload/.github/prompts/devspec.techstack.prompt.md +11 -0
  78. devspec_installer/payload/.github/skills/exploration-recovery/SKILL.md +45 -0
  79. devspec_installer/payload/.github/workflows/python-package-ci.yml +42 -0
  80. devspec_installer/payload/.github/workflows/python-package-publish.yml +69 -0
  81. devspec_installer/payload/.github/workflows/winget-package-publish.yml +110 -0
  82. devspec_installer/payload/AGENTS.md +80 -0
  83. devspec_installer/payload/GEMINI.md +35 -0
  84. devspec_installer/payload/README.md +301 -0
  85. devspec_installer/payload/devspec/adapters/README.md +53 -0
  86. devspec_installer/payload/devspec/adapters/antigravity.md +52 -0
  87. devspec_installer/payload/devspec/adapters/claude-code.md +32 -0
  88. devspec_installer/payload/devspec/adapters/codex-skills/devspec-workflow/SKILL.md +17 -0
  89. devspec_installer/payload/devspec/adapters/codex.md +22 -0
  90. devspec_installer/payload/devspec/adapters/command-registry.md +38 -0
  91. devspec_installer/payload/devspec/adapters/compatibility-matrix.md +21 -0
  92. devspec_installer/payload/devspec/adapters/copilot.md +20 -0
  93. devspec_installer/payload/devspec/adapters/cursor.md +22 -0
  94. devspec_installer/payload/devspec/adapters/enterprise-governance.md +36 -0
  95. devspec_installer/payload/devspec/adapters/gemini-cli.md +54 -0
  96. devspec_installer/payload/devspec/adapters/validation-flows.md +90 -0
  97. devspec_installer/payload/devspec/architecture/_template/artifact-queue.md +27 -0
  98. devspec_installer/payload/devspec/architecture/_template/decision.md +45 -0
  99. devspec_installer/payload/devspec/architecture/_template/diagram.md +62 -0
  100. devspec_installer/payload/devspec/architecture/_template/overview.md +37 -0
  101. devspec_installer/payload/devspec/architecture/artifact-queue.md +27 -0
  102. devspec_installer/payload/devspec/architecture/diagrams/README.md +44 -0
  103. devspec_installer/payload/devspec/architecture/overview.md +37 -0
  104. devspec_installer/payload/devspec/constitution.md +26 -0
  105. devspec_installer/payload/devspec/foundation/_template/codebase-structure.md +64 -0
  106. devspec_installer/payload/devspec/foundation/_template/coding-standards.md +46 -0
  107. devspec_installer/payload/devspec/foundation/_template/discovery-exclusions.md +52 -0
  108. devspec_installer/payload/devspec/foundation/_template/exploration-state.md +15 -0
  109. devspec_installer/payload/devspec/foundation/_template/extraction-state.md +45 -0
  110. devspec_installer/payload/devspec/foundation/_template/project-context.md +37 -0
  111. devspec_installer/payload/devspec/foundation/_template/provider-integrations.md +94 -0
  112. devspec_installer/payload/devspec/foundation/_template/rules.md +54 -0
  113. devspec_installer/payload/devspec/foundation/_template/tech-stack.md +49 -0
  114. devspec_installer/payload/devspec/foundation/codebase-structure.md +64 -0
  115. devspec_installer/payload/devspec/foundation/coding-standards.md +46 -0
  116. devspec_installer/payload/devspec/foundation/discovery-exclusions.md +52 -0
  117. devspec_installer/payload/devspec/foundation/extraction-state.md +45 -0
  118. devspec_installer/payload/devspec/foundation/project-context.md +33 -0
  119. devspec_installer/payload/devspec/foundation/provider-integrations.md +94 -0
  120. devspec_installer/payload/devspec/foundation/rules.md +52 -0
  121. devspec_installer/payload/devspec/foundation/tech-stack.md +49 -0
  122. devspec_installer/payload/devspec/glossary.md +111 -0
  123. devspec_installer/payload/devspec/work-items/_template/clarify.md +28 -0
  124. devspec_installer/payload/devspec/work-items/_template/decisions.md +9 -0
  125. devspec_installer/payload/devspec/work-items/_template/diagrams.md +42 -0
  126. devspec_installer/payload/devspec/work-items/_template/finalize.md +65 -0
  127. devspec_installer/payload/devspec/work-items/_template/implement.md +63 -0
  128. devspec_installer/payload/devspec/work-items/_template/meta.md +63 -0
  129. devspec_installer/payload/devspec/work-items/_template/notes.md +7 -0
  130. devspec_installer/payload/devspec/work-items/_template/review.md +41 -0
  131. devspec_installer/payload/devspec/work-items/_template/story.md +59 -0
  132. devspec_installer/payload/devspec/work-items/_template/tasks.md +38 -0
  133. devspec_installer/payload/packaging/devspec-profiles.json +60 -0
@@ -0,0 +1,615 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import fnmatch
5
+ import hashlib
6
+ import json
7
+ import shutil
8
+ import sys
9
+ from dataclasses import dataclass
10
+ from datetime import datetime, timezone
11
+ from importlib import resources
12
+ from pathlib import Path, PurePosixPath
13
+ from typing import Iterable
14
+
15
+ from . import __version__
16
+
17
+
18
+ MANIFEST_PATH = PurePosixPath("devspec/.install-manifest.json")
19
+ PROFILES_PATH = PurePosixPath("packaging/devspec-profiles.json")
20
+
21
+ FRAMEWORK_OWNED = "framework-owned"
22
+ PROJECT_OWNED = "project-owned"
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class PayloadFile:
27
+ path: PurePosixPath
28
+ source: Path
29
+ ownership: str
30
+ digest: str
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class CopyPlan:
35
+ files: list[PayloadFile]
36
+ conflicts: list[str]
37
+ skipped: list[str]
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class VersionStatus:
42
+ installed: str | None
43
+ package: str
44
+ status: str
45
+ label: str
46
+
47
+
48
+ def main(argv: list[str] | None = None) -> int:
49
+ parser = build_parser()
50
+ args = parser.parse_args(argv)
51
+
52
+ try:
53
+ return args.func(args)
54
+ except DevspecError as exc:
55
+ print(f"error: {exc}", file=sys.stderr)
56
+ return 2
57
+
58
+
59
+ def build_parser() -> argparse.ArgumentParser:
60
+ parser = argparse.ArgumentParser(
61
+ prog="devspec",
62
+ description="Install, diff, sync, and validate devspec framework files.",
63
+ )
64
+ subparsers = parser.add_subparsers(dest="command", required=True)
65
+
66
+ version_parser = subparsers.add_parser("version", help="Print the devspec CLI version.")
67
+ version_parser.set_defaults(func=cmd_version)
68
+
69
+ init_parser = subparsers.add_parser("init", help="Install devspec files into a target repository.")
70
+ add_target(init_parser)
71
+ add_profile(init_parser)
72
+ init_parser.add_argument("--repo-state", choices=["new", "existing"], required=True)
73
+ init_parser.add_argument("--force", action="store_true", help="Overwrite conflicting framework files.")
74
+ init_parser.set_defaults(func=cmd_init)
75
+
76
+ diff_parser = subparsers.add_parser("diff", help="Compare target files with the packaged framework.")
77
+ add_target(diff_parser)
78
+ add_profile(diff_parser, required=False)
79
+ diff_parser.set_defaults(func=cmd_diff)
80
+
81
+ sync_parser = subparsers.add_parser("sync", help="Update installed framework-owned files.")
82
+ add_target(sync_parser)
83
+ add_profile(sync_parser)
84
+ sync_parser.add_argument("--dry-run", action="store_true", help="Show planned changes without writing files.")
85
+ sync_parser.add_argument("--force", action="store_true", help="Overwrite modified framework-owned files.")
86
+ sync_parser.set_defaults(func=cmd_sync)
87
+
88
+ doctor_parser = subparsers.add_parser("doctor", help="Validate a devspec installation.")
89
+ add_target(doctor_parser)
90
+ add_profile(doctor_parser, required=False)
91
+ doctor_parser.set_defaults(func=cmd_doctor)
92
+
93
+ return parser
94
+
95
+
96
+ def add_target(parser: argparse.ArgumentParser) -> None:
97
+ parser.add_argument("--target", default=".", help="Target repository root. Defaults to current directory.")
98
+
99
+
100
+ def add_profile(parser: argparse.ArgumentParser, required: bool = True) -> None:
101
+ parser.add_argument("--profile", default=None if required else "all", required=required, help="Install profile.")
102
+
103
+
104
+ def cmd_version(_args: argparse.Namespace) -> int:
105
+ print(__version__)
106
+ return 0
107
+
108
+
109
+ def cmd_init(args: argparse.Namespace) -> int:
110
+ payload = Payload()
111
+ target = resolve_target(args.target)
112
+ files = payload.resolve_profile_files(args.profile)
113
+ plan = create_copy_plan(files, target, force=args.force, mode="init")
114
+
115
+ if plan.conflicts:
116
+ print_report("Conflicts", plan.conflicts)
117
+ print("No files were written. Re-run with --force only after reviewing the conflicts.")
118
+ return 1
119
+
120
+ written = write_files(plan.files, target, dry_run=False)
121
+ manifest = build_manifest(args.profile, args.repo_state, plan.files)
122
+ write_manifest(target, manifest)
123
+
124
+ print(f"Installed devspec profile '{args.profile}' into {target}")
125
+ print(f"Files written: {written}")
126
+ if plan.skipped:
127
+ print_report("Skipped unchanged files", plan.skipped)
128
+ return 0
129
+
130
+
131
+ def cmd_diff(args: argparse.Namespace) -> int:
132
+ payload = Payload()
133
+ target = resolve_target(args.target)
134
+ profile = args.profile or profile_from_manifest(target) or "all"
135
+ files = payload.resolve_profile_files(profile)
136
+ manifest = read_manifest(target)
137
+ report = diff_files(files, target, manifest, profile)
138
+
139
+ print_version_status(version_status(manifest))
140
+ print_diff_report(report)
141
+ return 1 if report["missing"] or report["modified"] or report["stale"] or report["profile"] else 0
142
+
143
+
144
+ def cmd_sync(args: argparse.Namespace) -> int:
145
+ payload = Payload()
146
+ target = resolve_target(args.target)
147
+ manifest = read_manifest(target)
148
+ files = payload.resolve_profile_files(args.profile)
149
+
150
+ plan = create_sync_plan(files, target, manifest, force=args.force)
151
+ if plan.conflicts:
152
+ print_version_status(version_status(manifest))
153
+ print_report("Conflicts", plan.conflicts)
154
+ print("No files were written. Run with --dry-run first, then use --force only for reviewed framework-owned files.")
155
+ return 1
156
+
157
+ if args.dry_run:
158
+ print_version_status(version_status(manifest))
159
+ print(f"Dry run for devspec profile '{args.profile}' in {target}")
160
+ print(f"Files that would be written: {len(plan.files)}")
161
+ else:
162
+ written = write_files(plan.files, target, dry_run=False)
163
+ repo_state = manifest.get("repo_state", "existing") if manifest else "existing"
164
+ write_manifest(target, build_manifest(args.profile, repo_state, files))
165
+ print_version_status(version_status(manifest))
166
+ print(f"Synchronized devspec profile '{args.profile}' in {target}")
167
+ print(f"Files written: {written}")
168
+ if plan.skipped:
169
+ print_report("Skipped files", plan.skipped)
170
+ return 0
171
+
172
+
173
+ def cmd_doctor(args: argparse.Namespace) -> int:
174
+ payload = Payload()
175
+ target = resolve_target(args.target)
176
+ profile = args.profile or profile_from_manifest(target) or "all"
177
+ files = payload.resolve_profile_files(profile)
178
+ installed_paths = {str(item.path) for item in files}
179
+
180
+ errors: list[str] = []
181
+ warnings: list[str] = []
182
+
183
+ for item in files:
184
+ if not item.source.exists():
185
+ errors.append(f"profile '{profile}' references missing payload file: {item.path}")
186
+
187
+ manifest = read_manifest(target)
188
+ status = version_status(manifest)
189
+ if manifest is None:
190
+ warnings.append(f"install manifest is missing: {MANIFEST_PATH}")
191
+ else:
192
+ manifest_profile = manifest.get("profile")
193
+ if manifest_profile and manifest_profile != profile:
194
+ warnings.append(f"profile mismatch: manifest has '{manifest_profile}', doctor checked '{profile}'")
195
+ if status.status == "unknown":
196
+ warnings.append("manifest devspec_version is missing or invalid")
197
+ elif status.status == "upgrade":
198
+ warnings.append(f"installed devspec version '{status.installed}' is older than package version '{status.package}'")
199
+ elif status.status == "downgrade":
200
+ warnings.append(f"installed devspec version '{status.installed}' is newer than package version '{status.package}'")
201
+
202
+ for item in files:
203
+ if not (target / as_local_path(item.path)).exists():
204
+ warnings.append(f"target is missing installed file: {item.path}")
205
+
206
+ validate_command_registry(payload, installed_paths, errors)
207
+ validate_adapter_wrappers(profile, payload, installed_paths, errors)
208
+
209
+ if errors:
210
+ print_report("Errors", errors)
211
+ if warnings:
212
+ print_report("Warnings", warnings)
213
+ if not errors and not warnings:
214
+ print(f"devspec doctor passed for profile '{profile}' in {target}")
215
+ return 1 if errors else 0
216
+
217
+
218
+ class DevspecError(RuntimeError):
219
+ pass
220
+
221
+
222
+ class Payload:
223
+ def __init__(self) -> None:
224
+ self.root = find_source_root() or materialized_resource_root()
225
+ self.profiles = self._load_profiles()
226
+
227
+ def _load_profiles(self) -> dict:
228
+ path = self.root / as_local_path(PROFILES_PATH)
229
+ if not path.exists():
230
+ raise DevspecError(f"profile manifest not found in payload: {PROFILES_PATH}")
231
+ with path.open("r", encoding="utf-8") as handle:
232
+ data = json.load(handle)
233
+ profiles = data.get("profiles")
234
+ if not isinstance(profiles, dict):
235
+ raise DevspecError("profile manifest must contain a 'profiles' object")
236
+ return profiles
237
+
238
+ def resolve_profile_files(self, profile: str) -> list[PayloadFile]:
239
+ if profile not in self.profiles:
240
+ available = ", ".join(sorted(self.profiles))
241
+ raise DevspecError(f"unknown profile '{profile}'. Available profiles: {available}")
242
+
243
+ patterns = self._resolve_patterns(profile, seen=set())
244
+ paths: dict[PurePosixPath, PayloadFile] = {}
245
+ for pattern in patterns:
246
+ for path in iter_pattern_matches(self.root, pattern):
247
+ rel = to_posix(path.relative_to(self.root))
248
+ if should_exclude_payload(rel):
249
+ continue
250
+ paths[rel] = PayloadFile(
251
+ path=rel,
252
+ source=path,
253
+ ownership=classify_ownership(rel),
254
+ digest=sha256_file(path),
255
+ )
256
+ return [paths[key] for key in sorted(paths)]
257
+
258
+ def _resolve_patterns(self, profile: str, seen: set[str]) -> list[str]:
259
+ if profile in seen:
260
+ raise DevspecError(f"cyclic profile inheritance at '{profile}'")
261
+ branch = {*seen, profile}
262
+ data = self.profiles[profile]
263
+ patterns: list[str] = []
264
+ for parent in data.get("extends", []):
265
+ patterns.extend(self._resolve_patterns(parent, branch))
266
+ patterns.extend(data.get("includes", []))
267
+ return patterns
268
+
269
+
270
+ def find_source_root() -> Path | None:
271
+ current = Path(__file__).resolve()
272
+ for parent in current.parents:
273
+ if (parent / "devspec/adapters/command-registry.md").exists() and (parent / "packaging/devspec-profiles.json").exists():
274
+ return parent
275
+ return None
276
+
277
+
278
+ def materialized_resource_root() -> Path:
279
+ resource = resources.files("devspec_installer").joinpath("payload")
280
+ if not resource.is_dir():
281
+ raise DevspecError("packaged payload is missing")
282
+ return Path(str(resource))
283
+
284
+
285
+ def iter_pattern_matches(root: Path, pattern: str) -> Iterable[Path]:
286
+ normalized = pattern.replace("\\", "/")
287
+ if normalized.endswith("/**"):
288
+ base = root / as_local_path(PurePosixPath(normalized[:-3]))
289
+ if base.exists():
290
+ yield from (path for path in base.rglob("*") if path.is_file())
291
+ return
292
+
293
+ candidate = root / as_local_path(PurePosixPath(normalized))
294
+ if candidate.is_file():
295
+ yield candidate
296
+ return
297
+ if candidate.is_dir():
298
+ yield from (path for path in candidate.rglob("*") if path.is_file())
299
+ return
300
+
301
+ for path in root.rglob("*"):
302
+ if path.is_file() and fnmatch.fnmatch(str(to_posix(path.relative_to(root))), normalized):
303
+ yield path
304
+
305
+
306
+ def should_exclude_payload(path: PurePosixPath) -> bool:
307
+ parts = path.parts
308
+ if any(part in {".git", ".vs", "__pycache__", ".pytest_cache", ".ruff_cache", "dist", "build"} for part in parts):
309
+ return True
310
+ if path.name.endswith((".pyc", ".pyo")):
311
+ return True
312
+ if path == MANIFEST_PATH:
313
+ return True
314
+ return False
315
+
316
+
317
+ def classify_ownership(path: PurePosixPath) -> str:
318
+ parts = path.parts
319
+ if path in {PurePosixPath("devspec/constitution.md"), PurePosixPath("devspec/glossary.md")}:
320
+ return PROJECT_OWNED
321
+ if len(parts) == 3 and parts[0] == "devspec" and parts[1] == "foundation" and path.suffix == ".md":
322
+ return PROJECT_OWNED
323
+ if len(parts) == 3 and parts[0] == "devspec" and parts[1] == "architecture" and path.suffix == ".md":
324
+ return PROJECT_OWNED
325
+ if len(parts) >= 4 and parts[:3] == ("devspec", "architecture", "diagrams") and path.suffix == ".md":
326
+ return PROJECT_OWNED
327
+ if len(parts) >= 3 and parts[:2] == ("devspec", "work-items") and parts[2] != "_template":
328
+ return PROJECT_OWNED
329
+ return FRAMEWORK_OWNED
330
+
331
+
332
+ def create_copy_plan(files: list[PayloadFile], target: Path, force: bool, mode: str) -> CopyPlan:
333
+ conflicts: list[str] = []
334
+ skipped: list[str] = []
335
+ writable: list[PayloadFile] = []
336
+ for item in files:
337
+ destination = target / as_local_path(item.path)
338
+ if not destination.exists():
339
+ writable.append(item)
340
+ continue
341
+ destination_hash = sha256_file(destination)
342
+ if destination_hash == item.digest:
343
+ skipped.append(str(item.path))
344
+ continue
345
+ if item.ownership == PROJECT_OWNED and mode == "sync":
346
+ skipped.append(f"{item.path} (project-owned)")
347
+ continue
348
+ if force and item.ownership == FRAMEWORK_OWNED:
349
+ writable.append(item)
350
+ continue
351
+ conflicts.append(f"{item.path} already exists and differs")
352
+ return CopyPlan(files=writable, conflicts=conflicts, skipped=skipped)
353
+
354
+
355
+ def create_sync_plan(files: list[PayloadFile], target: Path, manifest: dict | None, force: bool) -> CopyPlan:
356
+ conflicts: list[str] = []
357
+ skipped: list[str] = []
358
+ writable: list[PayloadFile] = []
359
+ manifest_files = {entry["path"]: entry for entry in (manifest or {}).get("files", []) if isinstance(entry, dict) and "path" in entry}
360
+
361
+ for item in files:
362
+ destination = target / as_local_path(item.path)
363
+ if item.ownership == PROJECT_OWNED and destination.exists():
364
+ skipped.append(f"{item.path} (project-owned)")
365
+ continue
366
+ if not destination.exists():
367
+ writable.append(item)
368
+ continue
369
+ destination_hash = sha256_file(destination)
370
+ if destination_hash == item.digest:
371
+ skipped.append(str(item.path))
372
+ continue
373
+ previous = manifest_files.get(str(item.path), {}).get("sha256")
374
+ if previous and destination_hash == previous:
375
+ writable.append(item)
376
+ continue
377
+ if force and item.ownership == FRAMEWORK_OWNED:
378
+ writable.append(item)
379
+ continue
380
+ conflicts.append(f"{item.path} has local changes")
381
+ return CopyPlan(files=writable, conflicts=conflicts, skipped=skipped)
382
+
383
+
384
+ def write_files(files: list[PayloadFile], target: Path, dry_run: bool) -> int:
385
+ count = 0
386
+ for item in files:
387
+ destination = target / as_local_path(item.path)
388
+ if dry_run:
389
+ count += 1
390
+ continue
391
+ destination.parent.mkdir(parents=True, exist_ok=True)
392
+ shutil.copyfile(item.source, destination)
393
+ count += 1
394
+ return count
395
+
396
+
397
+ def build_manifest(profile: str, repo_state: str, files: list[PayloadFile]) -> dict:
398
+ return {
399
+ "schema_version": 1,
400
+ "devspec_version": __version__,
401
+ "profile": profile,
402
+ "repo_state": repo_state,
403
+ "installed_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat(),
404
+ "files": [
405
+ {
406
+ "path": str(item.path),
407
+ "sha256": item.digest,
408
+ "ownership": item.ownership,
409
+ }
410
+ for item in sorted(files, key=lambda value: value.path)
411
+ ],
412
+ }
413
+
414
+
415
+ def write_manifest(target: Path, manifest: dict) -> None:
416
+ path = target / as_local_path(MANIFEST_PATH)
417
+ path.parent.mkdir(parents=True, exist_ok=True)
418
+ path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
419
+
420
+
421
+ def read_manifest(target: Path) -> dict | None:
422
+ path = target / as_local_path(MANIFEST_PATH)
423
+ if not path.exists():
424
+ return None
425
+ with path.open("r", encoding="utf-8") as handle:
426
+ return json.load(handle)
427
+
428
+
429
+ def profile_from_manifest(target: Path) -> str | None:
430
+ manifest = read_manifest(target)
431
+ if not manifest:
432
+ return None
433
+ profile = manifest.get("profile")
434
+ return profile if isinstance(profile, str) else None
435
+
436
+
437
+ def version_status(manifest: dict | None) -> VersionStatus:
438
+ if manifest is None:
439
+ return VersionStatus(installed=None, package=__version__, status="not-installed", label="not installed")
440
+
441
+ installed = manifest.get("devspec_version")
442
+ if not isinstance(installed, str):
443
+ return VersionStatus(installed=None, package=__version__, status="unknown", label="unknown")
444
+
445
+ installed_version = parse_semver(installed)
446
+ package_version = parse_semver(__version__)
447
+ if installed_version is None or package_version is None:
448
+ return VersionStatus(installed=installed, package=__version__, status="unknown", label="unknown")
449
+ if installed_version == package_version:
450
+ return VersionStatus(installed=installed, package=__version__, status="same", label="up to date")
451
+ if installed_version < package_version:
452
+ return VersionStatus(installed=installed, package=__version__, status="upgrade", label="upgrade available")
453
+ return VersionStatus(installed=installed, package=__version__, status="downgrade", label="newer than package")
454
+
455
+
456
+ def parse_semver(value: str) -> tuple[int, int, int] | None:
457
+ parts = value.split(".")
458
+ if len(parts) != 3:
459
+ return None
460
+ try:
461
+ parsed = tuple(int(part) for part in parts)
462
+ except ValueError:
463
+ return None
464
+ return parsed if all(part >= 0 for part in parsed) else None
465
+
466
+
467
+ def diff_files(files: list[PayloadFile], target: Path, manifest: dict | None, profile: str) -> dict[str, list[str]]:
468
+ report = {"missing": [], "modified": [], "stale": [], "protected": [], "profile": []}
469
+ manifest_files = {entry["path"]: entry for entry in (manifest or {}).get("files", []) if isinstance(entry, dict) and "path" in entry}
470
+
471
+ if manifest and manifest.get("profile") != profile:
472
+ report["profile"].append(f"manifest profile is '{manifest.get('profile')}', requested profile is '{profile}'")
473
+
474
+ for item in files:
475
+ destination = target / as_local_path(item.path)
476
+ if item.ownership == PROJECT_OWNED:
477
+ report["protected"].append(str(item.path))
478
+ if not destination.exists():
479
+ report["missing"].append(str(item.path))
480
+ continue
481
+ destination_hash = sha256_file(destination)
482
+ if destination_hash == item.digest:
483
+ continue
484
+ previous = manifest_files.get(str(item.path), {}).get("sha256")
485
+ if previous and destination_hash == previous:
486
+ report["stale"].append(str(item.path))
487
+ else:
488
+ report["modified"].append(str(item.path))
489
+ return report
490
+
491
+
492
+ def validate_command_registry(payload: Payload, installed_paths: set[str], errors: list[str]) -> None:
493
+ registry = payload.root / "devspec/adapters/command-registry.md"
494
+ if not registry.exists():
495
+ errors.append("missing command registry in payload")
496
+ return
497
+ for line in registry.read_text(encoding="utf-8").splitlines():
498
+ if not line.startswith("| `/devspec."):
499
+ continue
500
+ columns = [column.strip() for column in line.strip("|").split("|")]
501
+ if len(columns) < 5:
502
+ continue
503
+ prompt = strip_markdown_code(columns[3])
504
+ agent = strip_markdown_code(columns[4])
505
+ for required in (prompt, agent):
506
+ if required and required not in installed_paths:
507
+ errors.append(f"registry references missing profile file: {required}")
508
+
509
+
510
+ def validate_adapter_wrappers(profile: str, payload: Payload, installed_paths: set[str], errors: list[str]) -> None:
511
+ commands = command_names(payload)
512
+ profiles_to_check = expanded_profile_names(payload, profile)
513
+
514
+ if "claude" in profiles_to_check:
515
+ for command in commands:
516
+ name = command.removeprefix("/").replace(".", "-")
517
+ required = f".claude/skills/{name}/SKILL.md"
518
+ if required not in installed_paths:
519
+ errors.append(f"Claude profile missing wrapper: {required}")
520
+ if "gemini" in profiles_to_check:
521
+ for command in commands:
522
+ suffix = command.removeprefix("/devspec.")
523
+ required = f".gemini/commands/devspec/{suffix}.toml"
524
+ if required not in installed_paths:
525
+ errors.append(f"Gemini profile missing wrapper: {required}")
526
+ if "antigravity" in profiles_to_check:
527
+ for command in commands:
528
+ name = command.removeprefix("/").replace(".", "-")
529
+ required = f".agents/skills/{name}.md"
530
+ if required not in installed_paths:
531
+ errors.append(f"Antigravity profile missing wrapper: {required}")
532
+ if "cursor" in profiles_to_check and ".cursor/rules/devspec-workflow.mdc" not in installed_paths:
533
+ errors.append("Cursor profile missing .cursor/rules/devspec-workflow.mdc")
534
+ if "codex" in profiles_to_check and "AGENTS.md" not in installed_paths:
535
+ errors.append("Codex profile missing AGENTS.md")
536
+
537
+
538
+ def command_names(payload: Payload) -> list[str]:
539
+ registry = payload.root / "devspec/adapters/command-registry.md"
540
+ names: list[str] = []
541
+ for line in registry.read_text(encoding="utf-8").splitlines():
542
+ if line.startswith("| `/devspec."):
543
+ columns = [column.strip() for column in line.strip("|").split("|")]
544
+ names.append(strip_markdown_code(columns[0]))
545
+ return names
546
+
547
+
548
+ def expanded_profile_names(payload: Payload, profile: str) -> set[str]:
549
+ names: set[str] = set()
550
+
551
+ def visit(name: str) -> None:
552
+ if name in names:
553
+ return
554
+ names.add(name)
555
+ for parent in payload.profiles[name].get("extends", []):
556
+ visit(parent)
557
+
558
+ visit(profile)
559
+ return names
560
+
561
+
562
+ def strip_markdown_code(value: str) -> str:
563
+ return value.strip().strip("`")
564
+
565
+
566
+ def print_version_status(status: VersionStatus) -> None:
567
+ if status.status == "not-installed":
568
+ installed = "not installed"
569
+ else:
570
+ installed = status.installed or "unknown"
571
+ print(f"Installed version: {installed}")
572
+ print(f"Package version: {status.package}")
573
+ print(f"Version status: {status.label}")
574
+
575
+
576
+ def print_diff_report(report: dict[str, list[str]]) -> None:
577
+ empty = True
578
+ for title, values in (
579
+ ("Profile mismatches", report["profile"]),
580
+ ("Missing files", report["missing"]),
581
+ ("Modified files", report["modified"]),
582
+ ("Stale files", report["stale"]),
583
+ ("Protected project-owned files", report["protected"]),
584
+ ):
585
+ if values:
586
+ empty = False
587
+ print_report(title, values)
588
+ if empty:
589
+ print("No devspec differences found.")
590
+
591
+
592
+ def print_report(title: str, values: list[str]) -> None:
593
+ print(f"{title}:")
594
+ for value in values:
595
+ print(f" - {value}")
596
+
597
+
598
+ def resolve_target(value: str) -> Path:
599
+ return Path(value).expanduser().resolve()
600
+
601
+
602
+ def sha256_file(path: Path) -> str:
603
+ digest = hashlib.sha256()
604
+ with path.open("rb") as handle:
605
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
606
+ digest.update(chunk)
607
+ return digest.hexdigest()
608
+
609
+
610
+ def to_posix(path: Path) -> PurePosixPath:
611
+ return PurePosixPath(path.as_posix())
612
+
613
+
614
+ def as_local_path(path: PurePosixPath) -> Path:
615
+ return Path(*path.parts)
@@ -0,0 +1,25 @@
1
+ ---
2
+ description: Always-on devspec workflow, artifact, and no-intent-drift rules for Google Antigravity.
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Devspec Workflow Rules
7
+
8
+ When the user invokes or references a `/devspec.*` workflow, treat it as command intent from `devspec/adapters/command-registry.md`.
9
+
10
+ Follow these rules:
11
+
12
+ - Read `devspec/adapters/command-registry.md` before acting on a `devspec` command.
13
+ - Preserve the original intent of the canonical Copilot prompt and agent files named in the registry.
14
+ - Use Git-tracked `devspec/` artifacts for recovery before relying on chat history, Antigravity artifacts, memory, or task lists.
15
+ - Preserve required inputs, output artifacts, status values, gates, handoff order, and recovery behavior.
16
+ - Preserve structured question behavior from `.github/prompts/PATTERNS.md#interactive-question-pattern`; if clickable options are unavailable, render the same option labels as text and preserve the recommended option.
17
+ - Use `devspec/glossary.md` for status values.
18
+ - Use `devspec/foundation/codebase-structure.md` for repository access requirements.
19
+ - Use `devspec/adapters/validation-flows.md` for enterprise acceptance checks.
20
+ - Keep provider credentials, tokens, user settings, and secrets outside prompt, rule, skill, and artifact files.
21
+ - Record unsupported Antigravity behavior as a limitation instead of changing workflow semantics.
22
+
23
+ Do not recommend unregistered commands such as `/devspec.plan`, `/devspec.architecture`, `/devspec.provider-integrations`, `/devspec.queue`, or `/devspec.decisions`.
24
+
25
+ For Antigravity execution, prefer strict or review-requesting permission posture for commands, non-workspace file access, browser actions, MCP calls, and artifact application unless the project has explicitly approved broader access.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: devspec-clarify
3
+ description: Run /devspec.clarify using the canonical devspec command registry and Copilot reference contract.
4
+ ---
5
+
6
+ Execute canonical command `/devspec.clarify`.
7
+
8
+ - Read `devspec/adapters/command-registry.md` for the command contract.
9
+ - Read `.github/prompts/devspec.clarify.prompt.md` and `.github/agents/devspec.clarify.agent.md` as the source of intent.
10
+ - Preserve required inputs, output artifacts, status values, gates, handoff order, and recovery behavior.
11
+ - Use Git-tracked `devspec/` artifacts for recovery before relying on chat history or Antigravity artifacts.
12
+ - Treat unsupported Antigravity behavior as an adapter limitation, not a workflow change.
13
+
14
+ Command input comes from the user's current message.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: devspec-codebase-structure
3
+ description: Run /devspec.codebase-structure using the canonical devspec command registry and Copilot reference contract.
4
+ ---
5
+
6
+ Execute canonical command `/devspec.codebase-structure`.
7
+
8
+ - Read `devspec/adapters/command-registry.md` for the command contract.
9
+ - Read `.github/prompts/devspec.codebase-structure.prompt.md` and `.github/agents/devspec.codebase-structure.agent.md` as the source of intent.
10
+ - Preserve required inputs, output artifacts, status values, gates, handoff order, and recovery behavior.
11
+ - Use Git-tracked `devspec/` artifacts for recovery before relying on chat history or Antigravity artifacts.
12
+ - Treat unsupported Antigravity behavior as an adapter limitation, not a workflow change.
13
+
14
+ Command input comes from the user's current message.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: devspec-coding-standards
3
+ description: Run /devspec.coding-standards using the canonical devspec command registry and Copilot reference contract.
4
+ ---
5
+
6
+ Execute canonical command `/devspec.coding-standards`.
7
+
8
+ - Read `devspec/adapters/command-registry.md` for the command contract.
9
+ - Read `.github/prompts/devspec.coding-standards.prompt.md` and `.github/agents/devspec.coding-standards.agent.md` as the source of intent.
10
+ - Preserve required inputs, output artifacts, status values, gates, handoff order, and recovery behavior.
11
+ - Use Git-tracked `devspec/` artifacts for recovery before relying on chat history or Antigravity artifacts.
12
+ - Treat unsupported Antigravity behavior as an adapter limitation, not a workflow change.
13
+
14
+ Command input comes from the user's current message.