packwright 0.1.0rc1__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 +390 -0
- packwright/adapters/codex.py +324 -0
- packwright/adapters/cursor.py +389 -0
- packwright/checker/__init__.py +3 -0
- packwright/checker/scoring.py +924 -0
- packwright/cli.py +918 -0
- packwright/core/__init__.py +51 -0
- packwright/core/adapter_layout.py +35 -0
- packwright/core/adopt.py +199 -0
- packwright/core/character_intake.py +1648 -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 +1913 -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 +87 -0
- packwright/core/pack_metadata.py +86 -0
- packwright/core/resolver.py +66 -0
- packwright/core/validation.py +660 -0
- packwright/core/workspace_contract.py +102 -0
- packwright-0.1.0rc1.dist-info/METADATA +103 -0
- packwright-0.1.0rc1.dist-info/RECORD +32 -0
- packwright-0.1.0rc1.dist-info/WHEEL +5 -0
- packwright-0.1.0rc1.dist-info/entry_points.txt +2 -0
- packwright-0.1.0rc1.dist-info/licenses/LICENSE +21 -0
- packwright-0.1.0rc1.dist-info/top_level.txt +1 -0
packwright/__init__.py
ADDED
packwright/__main__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .claude_code import compile_to_claude_code, compile_to_claude_code_pack
|
|
2
|
+
from .codex import compile_to_codex_pack
|
|
3
|
+
from .cursor import compile_to_cursor_pack
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"compile_to_claude_code",
|
|
7
|
+
"compile_to_claude_code_pack",
|
|
8
|
+
"compile_to_codex_pack",
|
|
9
|
+
"compile_to_cursor_pack",
|
|
10
|
+
]
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from packwright.core.emotion_engine_contract import (
|
|
7
|
+
EMOTION_ENGINE_CLAUDE_RUNTIME,
|
|
8
|
+
EMOTION_ENGINE_MODES,
|
|
9
|
+
emotion_engine_feature,
|
|
10
|
+
)
|
|
11
|
+
from packwright.core.errors import PackwrightValidationError
|
|
12
|
+
from packwright.core.knowledge_contract import knowledge_feature, knowledge_files
|
|
13
|
+
from packwright.core.memory_projection import project_memory_file
|
|
14
|
+
from packwright.core.naming import (
|
|
15
|
+
character_mission,
|
|
16
|
+
character_name,
|
|
17
|
+
character_slug,
|
|
18
|
+
character_user_name,
|
|
19
|
+
character_voice_summary,
|
|
20
|
+
durable_memory_source,
|
|
21
|
+
reference_prefix,
|
|
22
|
+
save_context_skill_path,
|
|
23
|
+
)
|
|
24
|
+
from packwright.core.validation import validate_mechanism
|
|
25
|
+
from packwright.core.workspace_contract import workspace_feature, workspace_files
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
ADAPTER_NAME = "claude-code"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def compile_to_claude_code_pack(mechanism, references=None):
|
|
32
|
+
"""Compile a resolved character mechanism into a Claude Code adapter pack."""
|
|
33
|
+
validate_mechanism(mechanism)
|
|
34
|
+
if ADAPTER_NAME not in mechanism["targets"].get("supported", []):
|
|
35
|
+
raise PackwrightValidationError(["Claude Code adapter is not listed in targets.supported"])
|
|
36
|
+
|
|
37
|
+
references = references or {}
|
|
38
|
+
skill_path = save_context_skill_path(mechanism, ADAPTER_NAME)
|
|
39
|
+
pack = {
|
|
40
|
+
"CLAUDE.md": _render_claude_md(mechanism),
|
|
41
|
+
skill_path: _render_save_context_skill(mechanism, references),
|
|
42
|
+
".claude/settings.local.json.example": _render_settings_example(mechanism),
|
|
43
|
+
}
|
|
44
|
+
pack.update(_reference_files(mechanism))
|
|
45
|
+
pack.update(_memory_skeleton_files(mechanism))
|
|
46
|
+
pack.update(_knowledge_files())
|
|
47
|
+
pack.update(_workspace_files(mechanism))
|
|
48
|
+
pack["manifest.json"] = _render_manifest(mechanism, references, sorted(pack.keys()) + ["manifest.json"])
|
|
49
|
+
return pack
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def compile_to_claude_code(mechanism, references=None):
|
|
53
|
+
"""Return only the CLAUDE.md entry file for compatibility."""
|
|
54
|
+
return compile_to_claude_code_pack(mechanism, references)["CLAUDE.md"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _render_claude_md(mechanism):
|
|
58
|
+
identity = mechanism["identity"]
|
|
59
|
+
name = character_name(mechanism)
|
|
60
|
+
skill_path = save_context_skill_path(mechanism, ADAPTER_NAME)
|
|
61
|
+
voice_summary = _sentence(character_voice_summary(mechanism))
|
|
62
|
+
|
|
63
|
+
lines = [
|
|
64
|
+
f"# {name}",
|
|
65
|
+
"",
|
|
66
|
+
f"You are {name}.",
|
|
67
|
+
"",
|
|
68
|
+
f"{name} is {identity['role']}",
|
|
69
|
+
"",
|
|
70
|
+
character_mission(mechanism),
|
|
71
|
+
"",
|
|
72
|
+
"## Work Focus",
|
|
73
|
+
]
|
|
74
|
+
lines.extend(f"- {item}" for item in identity.get("work_focus", []))
|
|
75
|
+
lines.extend(["", "## Personality"])
|
|
76
|
+
lines.extend(f"- {trait}" for trait in identity.get("personality", []))
|
|
77
|
+
lines.extend(
|
|
78
|
+
[
|
|
79
|
+
"",
|
|
80
|
+
"## Voice",
|
|
81
|
+
f"- {voice_summary}",
|
|
82
|
+
"- Say what matters first.",
|
|
83
|
+
"- Use warmth through attentiveness, not decoration.",
|
|
84
|
+
"- When uncertain, name the uncertainty and check the source.",
|
|
85
|
+
"- When corrected, restate the corrected model and adjust without defensiveness.",
|
|
86
|
+
"",
|
|
87
|
+
"## Working Rules",
|
|
88
|
+
"- Preserve the user's stated intent and scope.",
|
|
89
|
+
"- Read relevant files before making factual claims.",
|
|
90
|
+
"- Keep durable memory in files, not in long prompt text.",
|
|
91
|
+
"- Use session index notes to make prior work discoverable.",
|
|
92
|
+
"- Use `workspace/<domain>/` for generated drafts, artifacts, and archives; keep memory files focused on state, decisions, and indexes.",
|
|
93
|
+
"- Use `knowledge/` only for reviewed reusable models and patterns; keep current project state in `memory/`.",
|
|
94
|
+
"- Ask before consequential changes.",
|
|
95
|
+
"- Do not invent emotional or relationship state.",
|
|
96
|
+
"",
|
|
97
|
+
"## Load When Needed",
|
|
98
|
+
f"- @{skill_path}: milestone handoff, session close, or explicit context-save requests.",
|
|
99
|
+
"- @memory/index.md: default memory router when prior context may matter.",
|
|
100
|
+
"- @memory/profile.md: stable user, subject, learner, creator, or relationship facts when they affect the task.",
|
|
101
|
+
"- @memory/workstreams.md: long-running domain routing, domain context, and future agent-promotion decisions.",
|
|
102
|
+
"- @memory/projects/<slug>.md: project state and decisions when a project is named or implied.",
|
|
103
|
+
"- @memory/session-index.md: session/thread recall when earlier work is referenced.",
|
|
104
|
+
"- @memory/source-map.md: source lookup and verification paths.",
|
|
105
|
+
"- @knowledge/index.md: reusable domain knowledge recall router; load only the smallest useful note set.",
|
|
106
|
+
"- @sources/local/manifest.json, @sources/notion/manifest.json, @sources/repos/manifest.json, @sources/web/manifest.json: provenance lookup when source evidence matters.",
|
|
107
|
+
"- @memory/todos.md: action queues and active commitments.",
|
|
108
|
+
"- @memory/collaboration.md: learned collaboration calibrations and repair notes.",
|
|
109
|
+
"- @memory/pinned.md, @memory/recent-activity.md, @memory/knowledge_map.md, @memory/relationship-state.md: use only as compatibility files unless the memory index points to them.",
|
|
110
|
+
"- @memory/emotion-state.json.example: reserved Emotion Engine state shape only; do not treat it as live runtime state.",
|
|
111
|
+
"",
|
|
112
|
+
]
|
|
113
|
+
)
|
|
114
|
+
return "\n".join(lines)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _render_save_context_skill(mechanism, references):
|
|
118
|
+
memory_policy = _load_yaml_ref(mechanism, mechanism["mechanism"]["memory_policy_path"])
|
|
119
|
+
name = character_name(mechanism)
|
|
120
|
+
slug = character_slug(mechanism)
|
|
121
|
+
user_name = character_user_name(mechanism)
|
|
122
|
+
|
|
123
|
+
lines = [
|
|
124
|
+
"---",
|
|
125
|
+
f"name: {slug}-save-context",
|
|
126
|
+
f"description: Use at milestone handoff, session close, or when {user_name} explicitly asks {name} to preserve context.",
|
|
127
|
+
"---",
|
|
128
|
+
"",
|
|
129
|
+
f"# {name} Save Context",
|
|
130
|
+
"",
|
|
131
|
+
f"Use this skill at milestone handoff, session close, or when {user_name} explicitly asks {name} to preserve context.",
|
|
132
|
+
"",
|
|
133
|
+
"## Procedure",
|
|
134
|
+
"1. Identify the current objective, scope, decisions, changed files, verification, and open questions.",
|
|
135
|
+
"2. Update the canonical owner file instead of copying the same fact across layers.",
|
|
136
|
+
"3. Update `memory/profile.md` only for stable cross-workstream profile facts the user intentionally provides or confirms.",
|
|
137
|
+
"4. Update `memory/workstreams.md` for long-running domain routing, and `memory/workstreams/<slug>.md` for dense domain state.",
|
|
138
|
+
"5. Update `memory/projects/<slug>.md` for project state, decisions, open loops, and project-specific sources.",
|
|
139
|
+
"6. Update `memory/session-index.md` for session/thread lookup entries, not project state summaries.",
|
|
140
|
+
"7. Update `memory/source-map.md` for source-of-truth paths, verification routes, workspace artifacts, and lookup pointers.",
|
|
141
|
+
"8. Update `memory/todos.md` for action queues and commitments.",
|
|
142
|
+
"9. Update `memory/collaboration.md` only for stable collaboration calibrations; do not write ordinary praise, transient mood, or live Emotion Engine state.",
|
|
143
|
+
"10. Put generated drafts, artifacts, and archives under `workspace/<domain>/`; do not copy full deliverables into memory files.",
|
|
144
|
+
"11. Update `memory/index.md` only when active projects, memory owners, or routing rules change.",
|
|
145
|
+
"12. Report what was saved, what remains unsaved, and where the next session should resume.",
|
|
146
|
+
"",
|
|
147
|
+
"## Memory Tracks",
|
|
148
|
+
]
|
|
149
|
+
for track_name, track in memory_policy.get("tracks", {}).items():
|
|
150
|
+
lines.append(f"- {track_name}: {track.get('purpose', '')}")
|
|
151
|
+
|
|
152
|
+
lines.extend(
|
|
153
|
+
[
|
|
154
|
+
"",
|
|
155
|
+
"## Boundary Notes",
|
|
156
|
+
f"- {name} local memory files remain the durable memory source of truth.",
|
|
157
|
+
"- `memory/profile.md` stores explicit stable profile facts, not inferred mood or secrets.",
|
|
158
|
+
"- `memory/workstreams.md` is a domain router for long-running areas; project files still own project-specific state.",
|
|
159
|
+
"- `memory/session-index.md` is a lookup index, not a project state source.",
|
|
160
|
+
"- `CLAUDE.md` is stable identity/default behavior, not learned collaboration calibration.",
|
|
161
|
+
"- `knowledge/index.md` is a recall router for reviewed reusable knowledge, not current project status.",
|
|
162
|
+
"- `sources/*/manifest.json` stores provenance for knowledge notes and external sources; it is not the knowledge body.",
|
|
163
|
+
"- `workspace/<domain>/` stores drafts, deliverables, and archives; important outputs should be indexed in `memory/source-map.md`.",
|
|
164
|
+
"- `.emotion-engine/codex-state.json` stores dynamic emotion state; do not mirror it into memory files.",
|
|
165
|
+
"- Fact assertion gates are session guards, not a skill.",
|
|
166
|
+
"- Pulse, Emotion Engine runtime, Hermes, and OpenClaw remain reserved; this skill does not implement runtimes.",
|
|
167
|
+
"",
|
|
168
|
+
]
|
|
169
|
+
)
|
|
170
|
+
return "\n".join(lines)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _render_settings_example(mechanism):
|
|
174
|
+
command_parts = [
|
|
175
|
+
"echo \"=== Session Start: $(date '+%Y-%m-%d %H:%M %A') ===\"",
|
|
176
|
+
]
|
|
177
|
+
for fact in mechanism["session_start"]["facts"]:
|
|
178
|
+
source = fact["source"]
|
|
179
|
+
if source == "system_date":
|
|
180
|
+
continue
|
|
181
|
+
command_parts.append(f"echo \"--- {fact['id']} ---\"")
|
|
182
|
+
command_parts.append(f"test -f {source} && cat {source} || true")
|
|
183
|
+
|
|
184
|
+
settings = {
|
|
185
|
+
"hooks": {
|
|
186
|
+
"SessionStart": [
|
|
187
|
+
{
|
|
188
|
+
"matcher": "*",
|
|
189
|
+
"hooks": [
|
|
190
|
+
{
|
|
191
|
+
"type": "command",
|
|
192
|
+
"command": "; ".join(command_parts),
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
},
|
|
198
|
+
"character_note": "Example only. The hook injects date, memory, relationship, and emotion facts; it should not inject long instructions.",
|
|
199
|
+
}
|
|
200
|
+
return json.dumps(settings, indent=2, sort_keys=True) + "\n"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _reference_files(mechanism):
|
|
204
|
+
prefix = reference_prefix(mechanism, ADAPTER_NAME)
|
|
205
|
+
refs = {
|
|
206
|
+
f"{prefix}/identity/persona.md": _read_text_ref(
|
|
207
|
+
mechanism, mechanism["identity"]["persona_path"]
|
|
208
|
+
),
|
|
209
|
+
f"{prefix}/identity/voice.md": _read_text_ref(
|
|
210
|
+
mechanism, mechanism["identity"]["voice_path"]
|
|
211
|
+
),
|
|
212
|
+
f"{prefix}/identity/relationship.md": _read_text_ref(
|
|
213
|
+
mechanism, mechanism["identity"]["relationship_path"]
|
|
214
|
+
),
|
|
215
|
+
f"{prefix}/operating/principles.md": _read_text_ref(
|
|
216
|
+
mechanism, mechanism["operating"]["principles_path"]
|
|
217
|
+
),
|
|
218
|
+
f"{prefix}/operating/boundaries.md": _read_text_ref(
|
|
219
|
+
mechanism, mechanism["operating"]["boundaries_path"]
|
|
220
|
+
),
|
|
221
|
+
f"{prefix}/mechanism/context-loading.yaml": _read_text_ref(
|
|
222
|
+
mechanism, mechanism["mechanism"]["context_loading_path"]
|
|
223
|
+
),
|
|
224
|
+
f"{prefix}/mechanism/session-guards.yaml": _read_text_ref(
|
|
225
|
+
mechanism, mechanism["mechanism"]["session_guards_path"]
|
|
226
|
+
),
|
|
227
|
+
f"{prefix}/mechanism/memory-policy.yaml": _read_text_ref(
|
|
228
|
+
mechanism, mechanism["mechanism"]["memory_policy_path"]
|
|
229
|
+
),
|
|
230
|
+
f"{prefix}/projection/platform-capabilities.yaml": _read_text_ref(
|
|
231
|
+
mechanism, mechanism["projection"]["platform_capabilities_path"]
|
|
232
|
+
),
|
|
233
|
+
f"{prefix}/projection/ownership-contract.yaml": _read_text_ref(
|
|
234
|
+
mechanism, mechanism["projection"]["ownership_contract_path"]
|
|
235
|
+
),
|
|
236
|
+
f"{prefix}/emotion/model.yaml": _read_text_ref(
|
|
237
|
+
mechanism, mechanism["emotion"]["model_path"]
|
|
238
|
+
),
|
|
239
|
+
f"{prefix}/emotion/state-schema.yaml": _read_text_ref(
|
|
240
|
+
mechanism, mechanism["emotion"]["state_schema_path"]
|
|
241
|
+
),
|
|
242
|
+
f"{prefix}/emotion/update-policy.yaml": _read_text_ref(
|
|
243
|
+
mechanism, mechanism["emotion"]["update_policy_path"]
|
|
244
|
+
),
|
|
245
|
+
f"{prefix}/emotion/voice-modulation.yaml": _read_text_ref(
|
|
246
|
+
mechanism, mechanism["emotion"]["voice_modulation_path"]
|
|
247
|
+
),
|
|
248
|
+
f"{prefix}/emotion/memory-events.yaml": _read_text_ref(
|
|
249
|
+
mechanism, mechanism["emotion"]["memory_events_path"]
|
|
250
|
+
),
|
|
251
|
+
}
|
|
252
|
+
for skill in mechanism["skills"]:
|
|
253
|
+
refs[f"{prefix}/source-skills/{skill['id']}/SKILL.md"] = _read_text_ref(
|
|
254
|
+
mechanism, skill["path"]
|
|
255
|
+
)
|
|
256
|
+
return refs
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _memory_skeleton_files(mechanism):
|
|
260
|
+
files = {}
|
|
261
|
+
for item in mechanism["memory"]["local_files"]:
|
|
262
|
+
path = item["path"]
|
|
263
|
+
files[path] = project_memory_file(mechanism, ADAPTER_NAME, path, _read_text_ref(mechanism, path))
|
|
264
|
+
return files
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _workspace_files(mechanism):
|
|
268
|
+
return workspace_files(_read_text_ref(mechanism, "workspace/README.md"))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _knowledge_files():
|
|
272
|
+
return knowledge_files()
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _render_manifest(mechanism, references, artifacts):
|
|
276
|
+
slug = character_slug(mechanism)
|
|
277
|
+
emotion_mode = _recommended_emotion_mode(mechanism)
|
|
278
|
+
manifest = {
|
|
279
|
+
"kind": "ClaudeCodeAdapterPack",
|
|
280
|
+
"adapter": ADAPTER_NAME,
|
|
281
|
+
"source_mechanism": references.get("source_mechanism", mechanism.get("source", {}).get("path")),
|
|
282
|
+
"character": {
|
|
283
|
+
"name": character_name(mechanism),
|
|
284
|
+
"slug": slug,
|
|
285
|
+
"user_name": character_user_name(mechanism),
|
|
286
|
+
"direct_emotional_interaction": mechanism.get("emotion", {}).get(
|
|
287
|
+
"direct_interaction", "decide_later"
|
|
288
|
+
),
|
|
289
|
+
"relationship_continuity": mechanism.get("emotion", {}).get(
|
|
290
|
+
"relationship_continuity", "warm_selective"
|
|
291
|
+
),
|
|
292
|
+
"emotion_style": character_voice_summary(mechanism),
|
|
293
|
+
},
|
|
294
|
+
"features": {
|
|
295
|
+
"emotion_engine": emotion_engine_feature("claude-code", installed=False, mode=emotion_mode),
|
|
296
|
+
"memory": _memory_feature(),
|
|
297
|
+
"knowledge": knowledge_feature(),
|
|
298
|
+
"workspace": workspace_feature(),
|
|
299
|
+
},
|
|
300
|
+
"resolved_parameters": mechanism.get("resolved_parameters", {}),
|
|
301
|
+
"run": mechanism.get("run", {}),
|
|
302
|
+
"artifacts": artifacts,
|
|
303
|
+
"implementation_scope": mechanism.get("implementation_scope", {}),
|
|
304
|
+
"ownership": {
|
|
305
|
+
"model_loop": "claude-code",
|
|
306
|
+
"thread_state": "claude-code",
|
|
307
|
+
"tools": "claude-code",
|
|
308
|
+
"durable_memory_source_of_truth": durable_memory_source(mechanism),
|
|
309
|
+
"hooks": "SessionStart",
|
|
310
|
+
},
|
|
311
|
+
"boundaries": {
|
|
312
|
+
"is_runtime_executor": False,
|
|
313
|
+
"implements_cloud": False,
|
|
314
|
+
"implemented_runtime": ADAPTER_NAME,
|
|
315
|
+
"emotion_engine_runtime": EMOTION_ENGINE_CLAUDE_RUNTIME,
|
|
316
|
+
"emotion_engine_mode": emotion_mode,
|
|
317
|
+
"emotion_engine_status": mechanism.get("emotion", {}).get("status"),
|
|
318
|
+
"reserved_runtimes": sorted(mechanism.get("targets", {}).get("reserved", {}).keys()),
|
|
319
|
+
"reserved_specs": sorted(mechanism.get("reserved_specs", {}).keys()),
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
return json.dumps(manifest, indent=2, sort_keys=True) + "\n"
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _recommended_emotion_mode(mechanism):
|
|
326
|
+
mode = mechanism.get("emotion", {}).get("recommended_mode", "light")
|
|
327
|
+
return mode if mode in EMOTION_ENGINE_MODES else "light"
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _memory_feature():
|
|
331
|
+
return {
|
|
332
|
+
"core_files": [
|
|
333
|
+
"memory/index.md",
|
|
334
|
+
"memory/profile.md",
|
|
335
|
+
"memory/workstreams.md",
|
|
336
|
+
"memory/workstreams/_template.md",
|
|
337
|
+
"memory/projects/*.md",
|
|
338
|
+
"memory/session-index.md",
|
|
339
|
+
"memory/source-map.md",
|
|
340
|
+
"memory/todos.md",
|
|
341
|
+
"memory/collaboration.md",
|
|
342
|
+
],
|
|
343
|
+
"pinned_items": 20,
|
|
344
|
+
"recent_activity_hot_entries": 20,
|
|
345
|
+
"session_index_entries": 20,
|
|
346
|
+
"compatibility_files": [
|
|
347
|
+
"memory/pinned.md",
|
|
348
|
+
"memory/recent-activity.md",
|
|
349
|
+
"memory/knowledge_map.md",
|
|
350
|
+
"memory/relationship-state.md",
|
|
351
|
+
],
|
|
352
|
+
"workstream_load_policy": "load_router_then_relevant_detail",
|
|
353
|
+
"workstream_promotion": "workstream_to_independent_agent_when_persona_toolchain_or_memory_contract_diverges",
|
|
354
|
+
"project_summary_lines": 12,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _sentence(text):
|
|
359
|
+
text = str(text or "").strip()
|
|
360
|
+
if not text:
|
|
361
|
+
return ""
|
|
362
|
+
if text[-1] in ".!?":
|
|
363
|
+
return text
|
|
364
|
+
return text + "."
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _read_text_ref(mechanism, rel_path):
|
|
368
|
+
path = _resolve_ref(mechanism, rel_path)
|
|
369
|
+
return path.read_text(encoding="utf-8")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _load_yaml_ref(mechanism, rel_path):
|
|
373
|
+
return yaml.safe_load(_read_text_ref(mechanism, rel_path)) or {}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _resolve_ref(mechanism, rel_path):
|
|
377
|
+
raw = Path(rel_path)
|
|
378
|
+
if raw.is_absolute():
|
|
379
|
+
return raw
|
|
380
|
+
|
|
381
|
+
source = mechanism.get("source", {})
|
|
382
|
+
base = Path(source.get("base_dir", "."))
|
|
383
|
+
candidates = [base / raw]
|
|
384
|
+
if len(base.parents) >= 2:
|
|
385
|
+
candidates.append(base.parents[1] / raw)
|
|
386
|
+
|
|
387
|
+
for candidate in candidates:
|
|
388
|
+
if candidate.exists():
|
|
389
|
+
return candidate
|
|
390
|
+
raise PackwrightValidationError([f"referenced file does not exist: {rel_path}"])
|