packwright 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.
- packwright/__init__.py +3 -0
- packwright/__main__.py +5 -0
- packwright/adapters/__init__.py +10 -0
- packwright/adapters/claude_code.py +377 -0
- packwright/adapters/codex.py +311 -0
- packwright/adapters/cursor.py +375 -0
- packwright/checker/__init__.py +3 -0
- packwright/checker/scoring.py +924 -0
- packwright/cli.py +971 -0
- packwright/core/__init__.py +57 -0
- packwright/core/adapter_layout.py +35 -0
- packwright/core/adopt.py +199 -0
- packwright/core/character_intake.py +1649 -0
- packwright/core/emotion_engine_contract.py +147 -0
- packwright/core/errors.py +13 -0
- packwright/core/handoff.py +531 -0
- packwright/core/install.py +2114 -0
- packwright/core/intake_contract.py +105 -0
- packwright/core/knowledge_contract.py +212 -0
- packwright/core/loader.py +35 -0
- packwright/core/memory_projection.py +126 -0
- packwright/core/naming.py +98 -0
- packwright/core/pack_metadata.py +100 -0
- packwright/core/path_safety.py +70 -0
- packwright/core/resolver.py +66 -0
- packwright/core/validation.py +645 -0
- packwright/core/workspace_contract.py +102 -0
- packwright-0.1.0.dist-info/METADATA +213 -0
- packwright-0.1.0.dist-info/RECORD +33 -0
- packwright-0.1.0.dist-info/WHEEL +5 -0
- packwright-0.1.0.dist-info/entry_points.txt +2 -0
- packwright-0.1.0.dist-info/licenses/LICENSE +21 -0
- packwright-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from .errors import PackwrightError, PackwrightValidationError
|
|
2
|
+
from .adopt import adopt_existing
|
|
3
|
+
from .character_intake import (
|
|
4
|
+
generate_character_source,
|
|
5
|
+
generate_character_source_from_data,
|
|
6
|
+
generate_character_template,
|
|
7
|
+
generate_character_template_from_data,
|
|
8
|
+
load_character_intake,
|
|
9
|
+
starter_character_intake,
|
|
10
|
+
starter_character_preset_names,
|
|
11
|
+
starter_character_template_names,
|
|
12
|
+
validate_character_intake,
|
|
13
|
+
)
|
|
14
|
+
from .handoff import create_handoff
|
|
15
|
+
from .install import (
|
|
16
|
+
MigrationPlan,
|
|
17
|
+
apply_migration,
|
|
18
|
+
doctor_target,
|
|
19
|
+
install_pack,
|
|
20
|
+
migrate_target,
|
|
21
|
+
plan_migration,
|
|
22
|
+
refresh_emotion_engine_codex,
|
|
23
|
+
)
|
|
24
|
+
from .intake_contract import render_interviewer_prompt, write_interviewer_prompt
|
|
25
|
+
from .loader import load_mechanism
|
|
26
|
+
from .resolver import resolve_mechanism
|
|
27
|
+
from .validation import file_exists, path_exists, validate_mechanism
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"PackwrightError",
|
|
31
|
+
"PackwrightValidationError",
|
|
32
|
+
"MigrationPlan",
|
|
33
|
+
"adopt_existing",
|
|
34
|
+
"apply_migration",
|
|
35
|
+
"create_handoff",
|
|
36
|
+
"doctor_target",
|
|
37
|
+
"file_exists",
|
|
38
|
+
"generate_character_source",
|
|
39
|
+
"generate_character_source_from_data",
|
|
40
|
+
"generate_character_template",
|
|
41
|
+
"generate_character_template_from_data",
|
|
42
|
+
"install_pack",
|
|
43
|
+
"load_character_intake",
|
|
44
|
+
"load_mechanism",
|
|
45
|
+
"path_exists",
|
|
46
|
+
"render_interviewer_prompt",
|
|
47
|
+
"migrate_target",
|
|
48
|
+
"plan_migration",
|
|
49
|
+
"refresh_emotion_engine_codex",
|
|
50
|
+
"resolve_mechanism",
|
|
51
|
+
"starter_character_intake",
|
|
52
|
+
"starter_character_preset_names",
|
|
53
|
+
"starter_character_template_names",
|
|
54
|
+
"validate_character_intake",
|
|
55
|
+
"validate_mechanism",
|
|
56
|
+
"write_interviewer_prompt",
|
|
57
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
ADAPTER_LAYOUTS = {
|
|
2
|
+
"codex": {
|
|
3
|
+
"entry": "AGENTS.md",
|
|
4
|
+
"skill_root": ".agents/skills",
|
|
5
|
+
"legacy_skill_roots": (".codex/skills",),
|
|
6
|
+
},
|
|
7
|
+
"claude-code": {
|
|
8
|
+
"entry": "CLAUDE.md",
|
|
9
|
+
"skill_root": ".claude/skills",
|
|
10
|
+
"legacy_skill_roots": (),
|
|
11
|
+
},
|
|
12
|
+
"cursor": {
|
|
13
|
+
"entry": ".cursor/rules/<slug>.mdc",
|
|
14
|
+
"skill_root": ".cursor/rules",
|
|
15
|
+
"legacy_skill_roots": (),
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def adapter_entry(adapter, slug="<slug>"):
|
|
21
|
+
return ADAPTER_LAYOUTS[adapter]["entry"].replace("<slug>", slug)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def adapter_skill_root(adapter):
|
|
25
|
+
return ADAPTER_LAYOUTS[adapter]["skill_root"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def legacy_skill_roots(adapter):
|
|
29
|
+
return ADAPTER_LAYOUTS[adapter]["legacy_skill_roots"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def save_context_artifact(adapter, slug):
|
|
33
|
+
root = adapter_skill_root(adapter)
|
|
34
|
+
suffix = ".mdc" if adapter == "cursor" else "/SKILL.md"
|
|
35
|
+
return f"{root}/{slug}-save-context{suffix}"
|
packwright/core/adopt.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
from datetime import date
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .errors import PackwrightValidationError
|
|
7
|
+
from .knowledge_contract import knowledge_files
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
MIGRATION_DIR = "workspace/shared/artifacts/migrations"
|
|
11
|
+
RUNTIME_PATTERNS = (
|
|
12
|
+
"AGENTS.md",
|
|
13
|
+
"CLAUDE.md",
|
|
14
|
+
".cursor/rules/",
|
|
15
|
+
".codex/",
|
|
16
|
+
".claude/",
|
|
17
|
+
)
|
|
18
|
+
MEMORY_PATTERNS = (
|
|
19
|
+
"memory/",
|
|
20
|
+
"todo",
|
|
21
|
+
"session",
|
|
22
|
+
"project",
|
|
23
|
+
"workstream",
|
|
24
|
+
"collaboration",
|
|
25
|
+
)
|
|
26
|
+
KNOWLEDGE_PATTERNS = (
|
|
27
|
+
"knowledge/",
|
|
28
|
+
"principle",
|
|
29
|
+
"model",
|
|
30
|
+
"playbook",
|
|
31
|
+
"framework",
|
|
32
|
+
"method",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def adopt_existing(source_dir, target_dir=None, dry_run=True, force=False):
|
|
37
|
+
"""Inventory an existing local agent/workspace for reviewable Packwright adoption.
|
|
38
|
+
|
|
39
|
+
Adoption treats the existing instance as source material. It writes a
|
|
40
|
+
migration report and inventory only when dry_run is false; it does not
|
|
41
|
+
merge old memory or promote knowledge notes.
|
|
42
|
+
"""
|
|
43
|
+
source_dir = Path(source_dir)
|
|
44
|
+
if not source_dir.is_dir():
|
|
45
|
+
raise PackwrightValidationError([f"adopt source directory does not exist: {source_dir}"])
|
|
46
|
+
|
|
47
|
+
inventory = _inventory(source_dir)
|
|
48
|
+
categories = _category_counts(inventory)
|
|
49
|
+
result = {
|
|
50
|
+
"source_dir": str(source_dir),
|
|
51
|
+
"dry_run": bool(dry_run),
|
|
52
|
+
"files": len(inventory),
|
|
53
|
+
"categories": categories,
|
|
54
|
+
"adoption_policy": {
|
|
55
|
+
"existing_instance_role": "source_material",
|
|
56
|
+
"memory_merge": "review_required",
|
|
57
|
+
"knowledge_promotion": "review_required",
|
|
58
|
+
"in_place_modification": False,
|
|
59
|
+
},
|
|
60
|
+
"inventory": inventory,
|
|
61
|
+
}
|
|
62
|
+
if dry_run:
|
|
63
|
+
return result
|
|
64
|
+
if target_dir is None:
|
|
65
|
+
raise PackwrightValidationError(["adopt target_dir is required unless dry_run is true"])
|
|
66
|
+
target_dir = Path(target_dir)
|
|
67
|
+
report_path = target_dir / MIGRATION_DIR / f"adopt-report-{date.today().isoformat()}.md"
|
|
68
|
+
inventory_path = target_dir / MIGRATION_DIR / "inventory.json"
|
|
69
|
+
if not force and (report_path.exists() or inventory_path.exists()):
|
|
70
|
+
raise PackwrightValidationError([
|
|
71
|
+
"adopt migration report already exists; rerun with --force after reviewing it",
|
|
72
|
+
f"existing target artifact: {report_path.relative_to(target_dir)}",
|
|
73
|
+
f"existing target artifact: {inventory_path.relative_to(target_dir)}",
|
|
74
|
+
])
|
|
75
|
+
_write_knowledge_scaffold(target_dir, force=force)
|
|
76
|
+
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
report_path.write_text(_render_report(source_dir, inventory, categories), encoding="utf-8")
|
|
78
|
+
inventory_path.write_text(json.dumps({"files": inventory}, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
79
|
+
result.update({
|
|
80
|
+
"target_dir": str(target_dir),
|
|
81
|
+
"report": str(report_path),
|
|
82
|
+
"inventory_json": str(inventory_path),
|
|
83
|
+
"written": [
|
|
84
|
+
str(report_path.relative_to(target_dir)),
|
|
85
|
+
str(inventory_path.relative_to(target_dir)),
|
|
86
|
+
],
|
|
87
|
+
})
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _inventory(source_dir):
|
|
92
|
+
files = []
|
|
93
|
+
for path in sorted(source_dir.rglob("*")):
|
|
94
|
+
if not path.is_file():
|
|
95
|
+
continue
|
|
96
|
+
rel_path = path.relative_to(source_dir).as_posix()
|
|
97
|
+
if _skip_path(rel_path):
|
|
98
|
+
continue
|
|
99
|
+
files.append({
|
|
100
|
+
"path": rel_path,
|
|
101
|
+
"size": path.stat().st_size,
|
|
102
|
+
"sha256": _sha256(path),
|
|
103
|
+
"category": _classify(rel_path),
|
|
104
|
+
})
|
|
105
|
+
return files
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _skip_path(rel_path):
|
|
109
|
+
parts = set(Path(rel_path).parts)
|
|
110
|
+
return bool(parts & {".git", "__pycache__", "node_modules", ".venv", "venv"})
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _classify(rel_path):
|
|
114
|
+
lowered = rel_path.lower()
|
|
115
|
+
if any(lowered == pattern.lower() or lowered.startswith(pattern.lower()) for pattern in RUNTIME_PATTERNS):
|
|
116
|
+
return "runtime_instruction"
|
|
117
|
+
if lowered.startswith(("workspace/", "artifacts/", "drafts/", "archive/")):
|
|
118
|
+
return "workspace_artifact"
|
|
119
|
+
if any(pattern in lowered for pattern in MEMORY_PATTERNS):
|
|
120
|
+
return "memory_candidate"
|
|
121
|
+
if any(pattern in lowered for pattern in KNOWLEDGE_PATTERNS):
|
|
122
|
+
return "knowledge_candidate"
|
|
123
|
+
if lowered.endswith((".md", ".txt", ".yaml", ".yml", ".json", ".pdf", ".docx")):
|
|
124
|
+
return "source_candidate"
|
|
125
|
+
return "unclassified"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _category_counts(inventory):
|
|
129
|
+
counts = {}
|
|
130
|
+
for item in inventory:
|
|
131
|
+
counts[item["category"]] = counts.get(item["category"], 0) + 1
|
|
132
|
+
return counts
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _sha256(path):
|
|
136
|
+
digest = hashlib.sha256()
|
|
137
|
+
with path.open("rb") as handle:
|
|
138
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
139
|
+
digest.update(chunk)
|
|
140
|
+
return digest.hexdigest()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _write_knowledge_scaffold(target_dir, force=False):
|
|
144
|
+
for rel_path, text in knowledge_files().items():
|
|
145
|
+
path = target_dir / rel_path
|
|
146
|
+
if path.exists() and not force:
|
|
147
|
+
continue
|
|
148
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
path.write_text(text, encoding="utf-8")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _render_report(source_dir, inventory, categories):
|
|
153
|
+
lines = [
|
|
154
|
+
"# Packwright Adopt Report",
|
|
155
|
+
"",
|
|
156
|
+
f"Source: `{source_dir}`",
|
|
157
|
+
"",
|
|
158
|
+
"## Policy",
|
|
159
|
+
"",
|
|
160
|
+
"- Existing instances are source material, not automatic memory or knowledge.",
|
|
161
|
+
"- Runtime instructions need review before becoming Packwright mechanism or adapter projections.",
|
|
162
|
+
"- Memory candidates need review before writing to `memory/*` owner files.",
|
|
163
|
+
"- Knowledge candidates need review before promotion to `knowledge/**/*.md`.",
|
|
164
|
+
"- Source candidates may be registered in `sources/*/manifest.json` for provenance.",
|
|
165
|
+
"",
|
|
166
|
+
"## Inventory Summary",
|
|
167
|
+
"",
|
|
168
|
+
]
|
|
169
|
+
for category, count in sorted(categories.items()):
|
|
170
|
+
lines.append(f"- {category}: {count}")
|
|
171
|
+
lines.extend(["", "## Review Queues", ""])
|
|
172
|
+
for category in (
|
|
173
|
+
"runtime_instruction",
|
|
174
|
+
"memory_candidate",
|
|
175
|
+
"knowledge_candidate",
|
|
176
|
+
"source_candidate",
|
|
177
|
+
"workspace_artifact",
|
|
178
|
+
"unclassified",
|
|
179
|
+
):
|
|
180
|
+
items = [item for item in inventory if item["category"] == category]
|
|
181
|
+
if not items:
|
|
182
|
+
continue
|
|
183
|
+
lines.extend([f"### {category}", ""])
|
|
184
|
+
for item in items[:50]:
|
|
185
|
+
lines.append(f"- `{item['path']}` ({item['size']} bytes)")
|
|
186
|
+
if len(items) > 50:
|
|
187
|
+
lines.append(f"- ... {len(items) - 50} more")
|
|
188
|
+
lines.append("")
|
|
189
|
+
lines.extend(
|
|
190
|
+
[
|
|
191
|
+
"## Next Steps",
|
|
192
|
+
"",
|
|
193
|
+
"1. Review runtime instructions and decide which intent belongs in a Packwright mechanism.",
|
|
194
|
+
"2. Promote only current, confirmed state into `memory/*` owner files.",
|
|
195
|
+
"3. Promote only stable reusable models into `knowledge/**/*.md` with source refs.",
|
|
196
|
+
"4. Keep original files registered as sources when provenance matters.",
|
|
197
|
+
]
|
|
198
|
+
)
|
|
199
|
+
return "\n".join(lines) + "\n"
|