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.
@@ -0,0 +1,924 @@
1
+ import json
2
+
3
+ from packwright.core.emotion_engine_contract import (
4
+ EMOTION_ENGINE_AVAILABLE_RUNTIME,
5
+ EMOTION_ENGINE_CLAUDE_RUNTIME,
6
+ EMOTION_ENGINE_CODEX_HELPER_PATH,
7
+ EMOTION_ENGINE_CODEX_MCP_PATH,
8
+ EMOTION_ENGINE_CODEX_MCP_REGISTRATION_PATH,
9
+ EMOTION_ENGINE_CODEX_SCRIPT_PATH,
10
+ EMOTION_ENGINE_CODEX_SKILL_DIR,
11
+ EMOTION_ENGINE_CODEX_SKILL_PATH,
12
+ EMOTION_ENGINE_CODEX_STATE_PATH,
13
+ EMOTION_ENGINE_CODEX_WRAPPER_PATH,
14
+ EMOTION_ENGINE_MODES,
15
+ EMOTION_ENGINE_RUNTIME,
16
+ emotion_engine_codex_expected,
17
+ emotion_engine_codex_manifest_diagnostics,
18
+ )
19
+ from packwright.core.errors import PackwrightValidationError
20
+ from packwright.core.handoff import (
21
+ DEFAULT_HANDOFF_DIR,
22
+ DEFAULT_SESSION_BRIEF_DIR,
23
+ HANDOFF_HELPER_PATH,
24
+ HANDOFF_SCHEMA,
25
+ HANDOFF_WRAPPER_PATH,
26
+ )
27
+ from packwright.core.knowledge_contract import (
28
+ KNOWLEDGE_INDEX,
29
+ KNOWLEDGE_MANIFEST,
30
+ SOURCE_MANIFESTS,
31
+ knowledge_artifacts,
32
+ )
33
+ from packwright.core.naming import (
34
+ character_name,
35
+ character_slug,
36
+ durable_memory_source,
37
+ reference_prefix,
38
+ save_context_skill_path,
39
+ )
40
+ from packwright.core.validation import file_exists, path_exists, validate_mechanism
41
+ from packwright.core.workspace_contract import (
42
+ WORKSPACE_INDEX_OWNER,
43
+ WORKSPACE_LAYOUT,
44
+ WORKSPACE_ROOT,
45
+ workspace_artifacts,
46
+ workspace_readme_required_markers,
47
+ )
48
+
49
+
50
+ def score_mechanism(mechanism, adapter_pack, adapter="codex", threshold=None):
51
+ """Score a resolved mechanism and adapter pack."""
52
+ checks = []
53
+ try:
54
+ validate_mechanism(mechanism)
55
+ _add(checks, "mechanism_valid", True, 15, "mechanism spec passes validation")
56
+ except PackwrightValidationError as exc:
57
+ _add(checks, "mechanism_valid", False, 15, "; ".join(exc.issues))
58
+ return _result(checks, 85 if threshold is None else threshold)
59
+
60
+ configured_threshold = mechanism.get("checker", {}).get("threshold", 85)
61
+ threshold = configured_threshold if threshold is None else threshold
62
+ adapter_pack = adapter_pack or {}
63
+ adapter = _adapter_from_manifest(adapter_pack, adapter)
64
+ entry_path = _entry_path(mechanism, adapter)
65
+ skill_path = save_context_skill_path(mechanism, adapter)
66
+ entry = adapter_pack.get(entry_path, "")
67
+ skill = adapter_pack.get(skill_path, "")
68
+ settings = adapter_pack.get(".claude/settings.local.json.example", "")
69
+ manifest = _parse_json(adapter_pack.get("manifest.json", "{}"))
70
+
71
+ implemented_by = mechanism["coverage"]["implemented_by"]
72
+ identity = mechanism["identity"]
73
+ name = character_name(mechanism)
74
+
75
+ _add(
76
+ checks,
77
+ "source_files_exist",
78
+ _source_files_exist(mechanism),
79
+ 10,
80
+ "all referenced identity, operating, mechanism, emotion, memory, and skill source files exist",
81
+ )
82
+ _add(
83
+ checks,
84
+ "coverage_paths",
85
+ all(path_exists(mechanism, path) for paths in implemented_by.values() for path in paths),
86
+ 10,
87
+ "mechanism coverage paths all resolve",
88
+ )
89
+ _add(
90
+ checks,
91
+ "adapter_pack_manifest",
92
+ _manifest_matches_pack(manifest, adapter_pack, adapter),
93
+ 10,
94
+ "adapter pack manifest describes emitted artifacts",
95
+ )
96
+ _add(
97
+ checks,
98
+ "projection_contracts_present",
99
+ _projection_contracts_present(mechanism, adapter_pack, adapter),
100
+ 10,
101
+ "adapter pack includes platform capabilities and ownership contract",
102
+ )
103
+ _add(
104
+ checks,
105
+ "ownership_contract_valid",
106
+ _ownership_contract_valid(mechanism, manifest, adapter),
107
+ 10,
108
+ "adapter manifest declares runtime ownership without taking durable memory ownership",
109
+ )
110
+ _add(
111
+ checks,
112
+ "entry_has_identity",
113
+ identity["name"] in entry and identity["role"] in entry and f"You are {name}." in entry,
114
+ 10,
115
+ "entry file keeps character person-like stable identity hot",
116
+ )
117
+ _add(
118
+ checks,
119
+ "entry_has_voice",
120
+ _entry_has_voice(entry),
121
+ 10,
122
+ "entry file carries stable voice guidance",
123
+ )
124
+ _add(
125
+ checks,
126
+ "entry_excludes_implementation_scope",
127
+ _entry_excludes_implementation_scope(mechanism, entry),
128
+ 10,
129
+ "entry file does not embed Packwright implementation-scope details or run state",
130
+ )
131
+ _add(
132
+ checks,
133
+ "entry_points_to_save_context_skill",
134
+ skill_path in entry,
135
+ 10,
136
+ "entry file points to the character save-context skill without turning foundation mechanisms into skills",
137
+ )
138
+ _add(
139
+ checks,
140
+ "entry_uses_runtime_appropriate_links",
141
+ _entry_uses_runtime_appropriate_links(entry, adapter, skill_path),
142
+ 10,
143
+ "entry file uses Codex plain paths or Claude Code @path syntax as appropriate",
144
+ )
145
+ _add(
146
+ checks,
147
+ "on_demand_references_have_purpose",
148
+ _on_demand_references_have_purpose(entry, adapter),
149
+ 10,
150
+ "on-demand entry references describe when or why to read each file",
151
+ )
152
+ _add(
153
+ checks,
154
+ "foundation_mechanisms_not_projected_as_skills",
155
+ _foundation_mechanisms_not_projected_as_skills(adapter_pack),
156
+ 10,
157
+ "recent activity and fact check remain foundation mechanisms, not projected skills",
158
+ )
159
+ _add(
160
+ checks,
161
+ "save_context_skill_valid",
162
+ "## Procedure" in skill
163
+ and "memory/session-index.md" in skill
164
+ and "canonical owner file" in skill
165
+ and "## Memory Tracks" in skill,
166
+ 10,
167
+ "save-context skill carries the heavy memory handoff procedure",
168
+ )
169
+ _add(
170
+ checks,
171
+ "save_context_skill_projection_neutral",
172
+ _save_context_skill_projection_neutral(skill),
173
+ 10,
174
+ "save-context skill avoids Codex, Claude Code, and adapter-specific projection wording",
175
+ )
176
+ if adapter == "claude-code":
177
+ _add(
178
+ checks,
179
+ "hook_injects_facts_only",
180
+ _hook_injects_facts_only(settings),
181
+ 10,
182
+ "SessionStart example injects date, memory, relationship, and emotion facts, not long instructions",
183
+ )
184
+ else:
185
+ _add(
186
+ checks,
187
+ "no_fake_claude_hook",
188
+ ".claude/settings.local.json.example" not in adapter_pack and "SessionStart" not in entry,
189
+ 10,
190
+ "non-Claude Code projection does not fake Claude Code SessionStart hook semantics",
191
+ )
192
+ _add(
193
+ checks,
194
+ "memory_skeleton_present",
195
+ all(item["path"] in adapter_pack for item in mechanism["memory"]["local_files"]),
196
+ 10,
197
+ "adapter pack includes local memory skeleton files",
198
+ )
199
+ _add(
200
+ checks,
201
+ "memory_capacity_policy_present",
202
+ _memory_capacity_policy_present(mechanism, adapter_pack),
203
+ 10,
204
+ "memory skeleton carries owner-based routing, profile, workstream router, project, and compatibility limits",
205
+ )
206
+ _add(
207
+ checks,
208
+ "workspace_structure_present",
209
+ _workspace_structure_present(mechanism, adapter_pack),
210
+ 10,
211
+ "adapter pack includes workspace directories and keeps output indexing in source-map",
212
+ )
213
+ _add(
214
+ checks,
215
+ "knowledge_skeleton_present",
216
+ _knowledge_skeleton_present(adapter_pack, manifest, entry, adapter),
217
+ 10,
218
+ "adapter pack includes a reviewed-knowledge scaffold, source manifests, and explicit loading guidance",
219
+ )
220
+ if adapter == "cursor":
221
+ _add(
222
+ checks,
223
+ "cursor_handoff_tool_present",
224
+ _cursor_handoff_tool_present(adapter_pack, manifest, entry),
225
+ 10,
226
+ "Cursor pack includes target-local handoff export helper and handoff/session-brief path guidance",
227
+ )
228
+ _add(
229
+ checks,
230
+ "empty_memory_skeleton_is_user_ready",
231
+ _empty_memory_skeleton_is_user_ready(adapter_pack),
232
+ 10,
233
+ "empty memory skeleton files avoid template placeholders and read as usable empty state",
234
+ )
235
+ if adapter == "codex":
236
+ _add(
237
+ checks,
238
+ "reserved_emotion_not_in_daily_codex_entry",
239
+ "memory/emotion-state.json.example" not in entry and "live Emotion Engine state" not in entry,
240
+ 10,
241
+ "Codex daily entry keeps reserved Emotion Engine state out of normal operating prompts",
242
+ )
243
+ _add(
244
+ checks,
245
+ "emotion_specs_present",
246
+ _emotion_specs_present(mechanism, adapter_pack, adapter),
247
+ 10,
248
+ "adapter pack includes structured Emotion Engine spec references",
249
+ )
250
+ _add(
251
+ checks,
252
+ "emotion_engine_default_light",
253
+ _emotion_engine_default_light(mechanism, manifest, adapter),
254
+ 10,
255
+ "Emotion Engine defaults to light mode with user-visible token overhead estimates",
256
+ )
257
+ _add(
258
+ checks,
259
+ "emotion_reserved_not_runtime",
260
+ _emotion_reserved_not_runtime(mechanism, manifest),
261
+ 10,
262
+ "Emotion Engine is placed as structured reserved state modulation, not runtime execution",
263
+ )
264
+ _add(
265
+ checks,
266
+ "reserved_runtimes_not_implemented",
267
+ _reserved_runtimes_not_implemented(mechanism, manifest),
268
+ 10,
269
+ "reserved runtimes and specs remain explicitly non-implemented",
270
+ )
271
+ if adapter == "codex" and _emotion_engine_codex_enabled(adapter_pack, entry, manifest):
272
+ _add(
273
+ checks,
274
+ "emotion_engine_codex_skill_present",
275
+ EMOTION_ENGINE_CODEX_SKILL_PATH in adapter_pack,
276
+ 10,
277
+ "Emotion Engine Codex sidecar skill is installed when enabled",
278
+ )
279
+ _add(
280
+ checks,
281
+ "emotion_engine_codex_state_present",
282
+ _emotion_engine_codex_state_valid(adapter_pack.get(EMOTION_ENGINE_CODEX_STATE_PATH, "")),
283
+ 10,
284
+ "Emotion Engine Codex state exists as project-local runtime state when enabled",
285
+ )
286
+ _add(
287
+ checks,
288
+ "emotion_engine_codex_settle_trust_present",
289
+ _emotion_engine_codex_settle_trust_present(adapter_pack, entry),
290
+ 10,
291
+ "Emotion Engine Codex sidecar supports conservative trust settlement",
292
+ )
293
+ _add(
294
+ checks,
295
+ "emotion_engine_codex_record_policy_present",
296
+ _emotion_engine_codex_record_policy_present(adapter_pack, entry),
297
+ 10,
298
+ "Emotion Engine Codex sidecar supports deterministic light/always record policy",
299
+ )
300
+ _add(
301
+ checks,
302
+ "emotion_engine_codex_mcp_present",
303
+ _emotion_engine_codex_mcp_present(adapter_pack, manifest),
304
+ 10,
305
+ "Emotion Engine Codex sidecar exposes MCP state tools without owning Packwright repair",
306
+ )
307
+ _add(
308
+ checks,
309
+ "emotion_engine_codex_project_wrapper_present",
310
+ _emotion_engine_codex_project_wrapper_present(adapter_pack, manifest),
311
+ 10,
312
+ "project-root Emotion Engine wrapper forwards to the installed sidecar",
313
+ )
314
+ _add(
315
+ checks,
316
+ "emotion_engine_codex_entry_internal",
317
+ _emotion_engine_codex_entry_internal(entry),
318
+ 10,
319
+ "AGENTS.md keeps Emotion Engine internals hidden from normal replies",
320
+ )
321
+ _add(
322
+ checks,
323
+ "relationship_state_not_runtime_state",
324
+ _relationship_state_not_runtime_state(
325
+ adapter_pack.get("memory/relationship-state.md", "")
326
+ + "\n"
327
+ + adapter_pack.get("memory/collaboration.md", "")
328
+ ),
329
+ 10,
330
+ "collaboration memory stays human-readable and does not store PAD/trust runtime JSON",
331
+ )
332
+ manifest_diagnostics = emotion_engine_codex_manifest_diagnostics(manifest)
333
+ _add(
334
+ checks,
335
+ "emotion_engine_codex_manifest_consistent",
336
+ not manifest_diagnostics,
337
+ 10,
338
+ _emotion_engine_codex_diagnostic_message(
339
+ manifest_diagnostics,
340
+ "installed Emotion Engine Codex sidecar is reflected in manifest features and sidecars",
341
+ ),
342
+ )
343
+
344
+ return _result(checks, threshold)
345
+ def _source_files_exist(mechanism):
346
+ refs = [
347
+ mechanism["identity"]["persona_path"],
348
+ mechanism["identity"]["voice_path"],
349
+ mechanism["identity"]["relationship_path"],
350
+ mechanism["operating"]["principles_path"],
351
+ mechanism["operating"]["boundaries_path"],
352
+ mechanism["mechanism"]["context_loading_path"],
353
+ mechanism["mechanism"]["session_guards_path"],
354
+ mechanism["mechanism"]["memory_policy_path"],
355
+ mechanism["projection"]["platform_capabilities_path"],
356
+ mechanism["projection"]["ownership_contract_path"],
357
+ mechanism["emotion"]["model_path"],
358
+ mechanism["emotion"]["state_schema_path"],
359
+ mechanism["emotion"]["update_policy_path"],
360
+ mechanism["emotion"]["voice_modulation_path"],
361
+ mechanism["emotion"]["memory_events_path"],
362
+ ]
363
+ refs.extend(item["path"] for item in mechanism["memory"]["local_files"])
364
+ refs.extend(skill["path"] for skill in mechanism["skills"])
365
+ return _source_files_exist_for_refs(mechanism, refs)
366
+
367
+
368
+ def _source_files_exist_for_refs(mechanism, refs):
369
+ return all(file_exists(mechanism, ref) for ref in refs)
370
+
371
+
372
+ def _adapter_from_manifest(adapter_pack, fallback):
373
+ manifest = _parse_json(adapter_pack.get("manifest.json", "{}"))
374
+ adapter = manifest.get("adapter")
375
+ return adapter if adapter in {"codex", "claude-code", "cursor"} else fallback
376
+
377
+
378
+ def _entry_path(mechanism, adapter):
379
+ if adapter == "codex":
380
+ return "AGENTS.md"
381
+ if adapter == "claude-code":
382
+ return "CLAUDE.md"
383
+ if adapter == "cursor":
384
+ return f".cursor/rules/{character_slug(mechanism)}.mdc"
385
+ return "AGENTS.md"
386
+
387
+
388
+ def _manifest_matches_pack(manifest, adapter_pack, adapter):
389
+ if not isinstance(manifest, dict):
390
+ return False
391
+ all_paths = set(manifest.get("artifacts", [])) | set(adapter_pack.keys())
392
+ external = _manifest_external_artifacts(all_paths)
393
+ artifacts = set(manifest.get("artifacts", [])) - external
394
+ emitted = set(adapter_pack.keys()) - external
395
+ expected_kinds = {
396
+ "codex": "CodexAdapterPack",
397
+ "claude-code": "ClaudeCodeAdapterPack",
398
+ "cursor": "CursorAdapterPack",
399
+ }
400
+ expected_kind = expected_kinds.get(adapter)
401
+ return manifest.get("kind") == expected_kind and manifest.get("adapter") == adapter and artifacts == emitted
402
+
403
+
404
+ def _manifest_external_artifacts(paths):
405
+ allowed_prefixes = (
406
+ f"{EMOTION_ENGINE_CODEX_SKILL_DIR}/",
407
+ f"{EMOTION_ENGINE_CODEX_STATE_PATH.rsplit('/', 1)[0]}/",
408
+ )
409
+ return {path for path in paths if path.startswith(allowed_prefixes)}
410
+
411
+
412
+ def _projection_contracts_present(mechanism, adapter_pack, adapter):
413
+ prefix = reference_prefix(mechanism, adapter)
414
+ expected = [
415
+ f"{prefix}/projection/platform-capabilities.yaml",
416
+ f"{prefix}/projection/ownership-contract.yaml",
417
+ ]
418
+ return all(path in adapter_pack for path in expected) and _source_files_exist_for_refs(
419
+ mechanism,
420
+ [
421
+ mechanism["projection"]["platform_capabilities_path"],
422
+ mechanism["projection"]["ownership_contract_path"],
423
+ ],
424
+ )
425
+
426
+
427
+ def _ownership_contract_valid(mechanism, manifest, adapter):
428
+ ownership = manifest.get("ownership", {}) if isinstance(manifest, dict) else {}
429
+ return (
430
+ ownership.get("model_loop") == adapter
431
+ and ownership.get("thread_state") == adapter
432
+ and ownership.get("tools") == adapter
433
+ and ownership.get("durable_memory_source_of_truth") == durable_memory_source(mechanism)
434
+ )
435
+
436
+
437
+ def _entry_has_voice(entry):
438
+ return (
439
+ "## Voice" in entry
440
+ and "Say what matters first." in entry
441
+ and "When uncertain, name the uncertainty" in entry
442
+ and "When corrected, restate the corrected model" in entry
443
+ )
444
+
445
+
446
+ def _entry_excludes_implementation_scope(mechanism, entry):
447
+ forbidden = ["Packwright", "MVP", "adapter pack", "cloud service", "Do not build UI"]
448
+ run = mechanism.get("run", {})
449
+ run_values = [run.get("objective"), run.get("scope")]
450
+ return not any(item and item in entry for item in forbidden + run_values)
451
+
452
+
453
+ def _hook_injects_facts_only(settings_text):
454
+ settings = _parse_json(settings_text)
455
+ if not isinstance(settings, dict):
456
+ return False
457
+ hooks = settings.get("hooks", {}).get("SessionStart", [])
458
+ command = json.dumps(hooks)
459
+ required = (
460
+ "date",
461
+ "memory/index.md",
462
+ "memory/profile.md",
463
+ "memory/workstreams.md",
464
+ "memory/session-index.md",
465
+ "memory/source-map.md",
466
+ "memory/todos.md",
467
+ "memory/collaboration.md",
468
+ "memory/emotion-state.json.example",
469
+ )
470
+ forbidden = ("You are", "Always", "Never", "Workflow", "implement")
471
+ return all(item in command for item in required) and not any(item in command for item in forbidden)
472
+
473
+
474
+ def _emotion_specs_present(mechanism, adapter_pack, adapter):
475
+ prefix = reference_prefix(mechanism, adapter)
476
+ expected = [
477
+ f"{prefix}/emotion/model.yaml",
478
+ f"{prefix}/emotion/state-schema.yaml",
479
+ f"{prefix}/emotion/update-policy.yaml",
480
+ f"{prefix}/emotion/voice-modulation.yaml",
481
+ f"{prefix}/emotion/memory-events.yaml",
482
+ "memory/collaboration.md",
483
+ "memory/emotion-state.json.example",
484
+ ]
485
+ return all(path in adapter_pack for path in expected) and mechanism["emotion"]["status"] == "structured_reserved"
486
+
487
+
488
+ def _foundation_mechanisms_not_projected_as_skills(adapter_pack):
489
+ forbidden = (
490
+ ".agents/skills/atlas-recent-activity/SKILL.md",
491
+ ".agents/skills/atlas-fact-check/SKILL.md",
492
+ ".agents/skills/atlas-work/SKILL.md",
493
+ ".agents/skills/atlas-work/references/source-skills/recent-activity/SKILL.md",
494
+ ".agents/skills/atlas-work/references/source-skills/fact-check/SKILL.md",
495
+ ".claude/skills/atlas-recent-activity/SKILL.md",
496
+ ".claude/skills/atlas-fact-check/SKILL.md",
497
+ ".claude/skills/atlas-work/SKILL.md",
498
+ ".claude/skills/atlas-work/references/source-skills/recent-activity/SKILL.md",
499
+ ".claude/skills/atlas-work/references/source-skills/fact-check/SKILL.md",
500
+ )
501
+ if any(path in adapter_pack for path in forbidden):
502
+ return False
503
+ projected_foundation_suffixes = (
504
+ "recent-activity/SKILL.md",
505
+ "fact-check/SKILL.md",
506
+ )
507
+ return not any(path.endswith(projected_foundation_suffixes) for path in adapter_pack)
508
+
509
+
510
+ def _entry_uses_runtime_appropriate_links(entry, adapter, skill_path):
511
+ daily_memory_paths = (
512
+ "memory/index.md",
513
+ "memory/profile.md",
514
+ "memory/workstreams.md",
515
+ "memory/session-index.md",
516
+ "memory/source-map.md",
517
+ "memory/todos.md",
518
+ "memory/collaboration.md",
519
+ )
520
+ if adapter == "codex":
521
+ return (
522
+ "## Use When Needed" in entry
523
+ and f"`{skill_path}`" in entry
524
+ and "@" not in entry
525
+ and all(f"`{path}`" in entry for path in daily_memory_paths)
526
+ and "`memory/emotion-state.json.example`" not in entry
527
+ )
528
+ if adapter == "cursor":
529
+ return (
530
+ "## Use When Needed" in entry
531
+ and f"`{skill_path}`" in entry
532
+ and all(f"`{path}`" in entry for path in daily_memory_paths)
533
+ and "`memory/emotion-state.json.example`" not in entry
534
+ )
535
+ memory_paths = (*daily_memory_paths, "memory/emotion-state.json.example")
536
+ return (
537
+ "## Load When Needed" in entry
538
+ and f"@{skill_path}" in entry
539
+ and all(f"@{path}" in entry for path in memory_paths)
540
+ )
541
+
542
+
543
+ def _on_demand_references_have_purpose(entry, adapter):
544
+ heading = "## Load When Needed" if adapter == "claude-code" else "## Use When Needed"
545
+ lines = _section_bullets(entry, heading)
546
+ minimum = 11 if adapter == "claude-code" else 10
547
+ if len(lines) < minimum:
548
+ return False
549
+ if adapter in {"codex", "cursor"}:
550
+ return all((" for " in line or " when " in line or " only as " in line) for line in lines)
551
+ return all(": " in line and line.split(": ", 1)[1].strip() for line in lines)
552
+
553
+
554
+ def _section_bullets(text, heading):
555
+ in_section = False
556
+ bullets = []
557
+ for line in text.splitlines():
558
+ if line.strip() == heading:
559
+ in_section = True
560
+ continue
561
+ if in_section and line.startswith("## "):
562
+ break
563
+ if in_section and line.startswith("- "):
564
+ bullets.append(line)
565
+ return bullets
566
+
567
+
568
+ def _save_context_skill_projection_neutral(skill):
569
+ forbidden = ("Codex", "Claude", "Cursor", "Projection Notes", "adapter pack", ".codex", ".claude", ".cursor")
570
+ return not any(item in skill for item in forbidden)
571
+
572
+
573
+ def _empty_memory_skeleton_is_user_ready(adapter_pack):
574
+ index = adapter_pack.get("memory/index.md", "")
575
+ profile = adapter_pack.get("memory/profile.md", "")
576
+ session_index = adapter_pack.get("memory/session-index.md", "")
577
+ source_map = adapter_pack.get("memory/source-map.md", "")
578
+ collaboration = adapter_pack.get("memory/collaboration.md", "")
579
+ pinned = adapter_pack.get("memory/pinned.md", "")
580
+ workstreams = adapter_pack.get("memory/workstreams.md", "")
581
+ workstream_template = adapter_pack.get("memory/workstreams/_template.md", "")
582
+ recent = adapter_pack.get("memory/recent-activity.md", "")
583
+ todos = adapter_pack.get("memory/todos.md", "")
584
+ knowledge_map = adapter_pack.get("memory/knowledge_map.md", "")
585
+ project_template = adapter_pack.get("memory/projects/_template.md", "")
586
+ relationship_state = adapter_pack.get("memory/relationship-state.md", "")
587
+ workspace_readme = adapter_pack.get("workspace/README.md", "")
588
+ memory_text = "\n".join(
589
+ (
590
+ index,
591
+ profile,
592
+ session_index,
593
+ source_map,
594
+ collaboration,
595
+ pinned,
596
+ workstreams,
597
+ workstream_template,
598
+ recent,
599
+ todos,
600
+ knowledge_map,
601
+ project_template,
602
+ relationship_state,
603
+ workspace_readme,
604
+ )
605
+ )
606
+ forbidden = ("template skeleton", "Add newest pickup entries below this line")
607
+ required = (
608
+ "This is the default memory router.",
609
+ "This file stores stable profile facts",
610
+ "No pinned memory has been recorded yet.",
611
+ "domain router",
612
+ "Agent Promotion",
613
+ "No pickup entries have been recorded yet.",
614
+ "newest 20",
615
+ "No current todos have been recorded yet.",
616
+ "not a knowledge base by itself",
617
+ "No project state has been recorded yet.",
618
+ "No relationship continuity notes have been recorded yet.",
619
+ "Use this directory for generated work products",
620
+ )
621
+ usable_state = (
622
+ ("No active projects have been recorded yet." in index or "## Active Projects" in index)
623
+ and ("No session index entries have been recorded yet." in session_index or "<!-- entries -->" in session_index)
624
+ and (
625
+ "No source mappings have been recorded yet." in source_map
626
+ or "## Sources" in source_map
627
+ or "## Packwright Sources" in source_map
628
+ or "## Project Memory" in source_map
629
+ )
630
+ and ("No collaboration calibrations have been recorded yet." in collaboration or "## Current Calibrations" in collaboration)
631
+ )
632
+ return (
633
+ all(item in memory_text for item in required)
634
+ and usable_state
635
+ and not any(item in memory_text for item in forbidden)
636
+ )
637
+
638
+
639
+ def _memory_capacity_policy_present(mechanism, adapter_pack):
640
+ limits = mechanism.get("memory", {}).get("limits", {})
641
+ expected = {
642
+ "pinned_items": 20,
643
+ "recent_activity_hot_entries": 20,
644
+ "session_index_entries": 20,
645
+ "workstream_summary_bullets": 7,
646
+ "project_summary_lines": 12,
647
+ "workspace_artifact_index_entries": 50,
648
+ }
649
+ if any(limits.get(key) != value for key, value in expected.items()):
650
+ return False
651
+ combined = "\n".join(
652
+ adapter_pack.get(path, "")
653
+ for path in (
654
+ "memory/index.md",
655
+ "memory/profile.md",
656
+ "memory/session-index.md",
657
+ "memory/source-map.md",
658
+ "memory/collaboration.md",
659
+ "memory/pinned.md",
660
+ "memory/workstreams.md",
661
+ "memory/workstreams/_template.md",
662
+ "memory/recent-activity.md",
663
+ "memory/projects/_template.md",
664
+ "workspace/README.md",
665
+ )
666
+ )
667
+ required = (
668
+ "default memory router",
669
+ "stable profile facts",
670
+ "domain router",
671
+ "newest 20",
672
+ "source registry",
673
+ "stable collaboration calibrations",
674
+ "compatibility",
675
+ "12 lines or fewer",
676
+ "generated work products",
677
+ )
678
+ return all(item in combined for item in required)
679
+
680
+
681
+ def _workspace_structure_present(mechanism, adapter_pack):
682
+ workspace = mechanism.get("workspace", {})
683
+ required = set(workspace_artifacts())
684
+ readme = adapter_pack.get("workspace/README.md", "")
685
+ return (
686
+ workspace.get("root") == WORKSPACE_ROOT
687
+ and workspace.get("layout") == WORKSPACE_LAYOUT
688
+ and workspace.get("index_owner") == WORKSPACE_INDEX_OWNER
689
+ and required.issubset(adapter_pack.keys())
690
+ and all(marker in readme for marker in workspace_readme_required_markers())
691
+ )
692
+
693
+
694
+ def _knowledge_skeleton_present(adapter_pack, manifest, entry, adapter):
695
+ artifacts = set(manifest.get("artifacts", [])) if isinstance(manifest, dict) else set()
696
+ feature = manifest.get("features", {}).get("knowledge", {}) if isinstance(manifest, dict) else {}
697
+ required = set(knowledge_artifacts())
698
+ source_json_ok = all(isinstance(_parse_json(adapter_pack.get(path, "")), dict) for path in SOURCE_MANIFESTS)
699
+ manifest_json = _parse_json(adapter_pack.get(KNOWLEDGE_MANIFEST, ""))
700
+ if adapter == "claude-code":
701
+ entry_mentions = f"@{KNOWLEDGE_INDEX}" in entry and "@sources/local/manifest.json" in entry
702
+ else:
703
+ entry_mentions = f"`{KNOWLEDGE_INDEX}`" in entry and "`sources/*/manifest.json`" in entry
704
+ return (
705
+ required.issubset(adapter_pack.keys())
706
+ and required.issubset(artifacts)
707
+ and feature.get("root") == "knowledge"
708
+ and feature.get("recall_index") == KNOWLEDGE_INDEX
709
+ and feature.get("manifest") == KNOWLEDGE_MANIFEST
710
+ and feature.get("sources_root") == "sources"
711
+ and adapter_pack.get(KNOWLEDGE_INDEX, "").startswith("# Knowledge Recall Index")
712
+ and isinstance(manifest_json, dict)
713
+ and manifest_json.get("schema") == "packwright-knowledge-manifest/v1"
714
+ and source_json_ok
715
+ and entry_mentions
716
+ )
717
+
718
+
719
+ def _cursor_handoff_tool_present(adapter_pack, manifest, entry):
720
+ helper = adapter_pack.get(HANDOFF_HELPER_PATH, "")
721
+ wrapper = adapter_pack.get(HANDOFF_WRAPPER_PATH, "")
722
+ artifacts = set(manifest.get("artifacts", [])) if isinstance(manifest, dict) else set()
723
+ feature = manifest.get("features", {}).get("handoff", {}) if isinstance(manifest, dict) else {}
724
+ local_tool = manifest.get("local_tools", {}).get("handoff_export", {}) if isinstance(manifest, dict) else {}
725
+ return (
726
+ HANDOFF_HELPER_PATH in adapter_pack
727
+ and HANDOFF_WRAPPER_PATH in adapter_pack
728
+ and HANDOFF_HELPER_PATH in artifacts
729
+ and HANDOFF_WRAPPER_PATH in artifacts
730
+ and HANDOFF_SCHEMA in helper
731
+ and DEFAULT_HANDOFF_DIR in helper
732
+ and DEFAULT_SESSION_BRIEF_DIR in helper
733
+ and HANDOFF_HELPER_PATH.rsplit("/", 1)[-1] in wrapper
734
+ and feature.get("schema") == HANDOFF_SCHEMA
735
+ and feature.get("command") == HANDOFF_WRAPPER_PATH
736
+ and feature.get("default_handoff_dir") == DEFAULT_HANDOFF_DIR
737
+ and feature.get("session_brief_dir") == DEFAULT_SESSION_BRIEF_DIR
738
+ and local_tool.get("schema") == HANDOFF_SCHEMA
739
+ and local_tool.get("command") == HANDOFF_WRAPPER_PATH
740
+ and DEFAULT_HANDOFF_DIR in entry
741
+ and DEFAULT_SESSION_BRIEF_DIR in entry
742
+ )
743
+
744
+
745
+ def _emotion_reserved_not_runtime(mechanism, manifest):
746
+ boundaries = manifest.get("boundaries", {}) if isinstance(manifest, dict) else {}
747
+ runtime = boundaries.get("emotion_engine_runtime")
748
+ return (
749
+ mechanism["emotion"].get("status") == "structured_reserved"
750
+ and mechanism["emotion"].get("runtime") == "not_implemented"
751
+ and runtime in {False, EMOTION_ENGINE_AVAILABLE_RUNTIME, EMOTION_ENGINE_RUNTIME, EMOTION_ENGINE_CLAUDE_RUNTIME}
752
+ )
753
+
754
+
755
+ def _emotion_engine_default_light(mechanism, manifest, adapter):
756
+ emotion = mechanism.get("emotion", {})
757
+ feature = manifest.get("features", {}).get("emotion_engine", {}) if isinstance(manifest, dict) else {}
758
+ overhead = feature.get("estimated_overhead", {})
759
+ return (
760
+ emotion.get("default_mode") == "light"
761
+ and feature.get("default_mode") == "light"
762
+ and feature.get("mode") in EMOTION_ENGINE_MODES
763
+ and feature.get("adapter") == adapter
764
+ and "light" in overhead
765
+ and "always" in overhead
766
+ and "<1%" in overhead.get("light", "")
767
+ and "<=5%" in overhead.get("always", "")
768
+ )
769
+
770
+
771
+ def _reserved_runtimes_not_implemented(mechanism, manifest):
772
+ reserved_targets = mechanism.get("targets", {}).get("reserved", {})
773
+ reserved_specs = mechanism.get("reserved_specs", {})
774
+ manifest_boundaries = manifest.get("boundaries", {}) if isinstance(manifest, dict) else {}
775
+ specs_ok = all(
776
+ spec.get("status") in {"reserved", "structured_reserved"} and spec.get("runtime") == "not_implemented"
777
+ for spec in reserved_specs.values()
778
+ )
779
+ targets_ok = all(spec.get("status") == "reserved" for spec in reserved_targets.values())
780
+ manifest_ok = manifest_boundaries.get("is_runtime_executor") is False and manifest_boundaries.get("implements_cloud") is False
781
+ return specs_ok and targets_ok and manifest_ok
782
+
783
+
784
+ def _emotion_engine_codex_enabled(adapter_pack, entry, manifest):
785
+ return (
786
+ emotion_engine_codex_expected(manifest, adapter_pack)
787
+ or "## Emotion Engine" in entry
788
+ or "## Optional Emotion Engine" in entry
789
+ )
790
+
791
+
792
+ def _emotion_engine_codex_state_valid(text):
793
+ state = _parse_json(text)
794
+ return (
795
+ isinstance(state, dict)
796
+ and state.get("_schema") == "emotion-engine-state/v2"
797
+ and isinstance(state.get("character_profile"), dict)
798
+ )
799
+
800
+
801
+ def _emotion_engine_codex_entry_internal(entry):
802
+ forbidden = (
803
+ '"pleasure"',
804
+ '"arousal"',
805
+ '"dominance"',
806
+ '"trust"',
807
+ "pleasure:",
808
+ "arousal:",
809
+ "dominance:",
810
+ "trust_history",
811
+ "emotion_trajectory",
812
+ )
813
+ return (
814
+ ("## Emotion Engine" in entry or "## Optional Emotion Engine" in entry)
815
+ and EMOTION_ENGINE_CODEX_SKILL_PATH in entry
816
+ and EMOTION_ENGINE_CODEX_STATE_PATH in entry
817
+ and "settle_trust" in entry
818
+ and "record_policy" in entry
819
+ and not any(item in entry for item in forbidden)
820
+ )
821
+
822
+
823
+ def _emotion_engine_codex_settle_trust_present(adapter_pack, entry):
824
+ skill = adapter_pack.get(EMOTION_ENGINE_CODEX_SKILL_PATH, "")
825
+ helper = adapter_pack.get(EMOTION_ENGINE_CODEX_HELPER_PATH, "")
826
+ wrapper = adapter_pack.get(EMOTION_ENGINE_CODEX_SCRIPT_PATH, "")
827
+ return (
828
+ "settle_trust" in entry
829
+ and "settle_trust" in skill
830
+ and "settle_trust" in helper
831
+ and 'exec "$PYTHON" "$ENGINE" "$COMMAND" "$STATE_FILE"' in wrapper
832
+ )
833
+
834
+
835
+ def _emotion_engine_codex_record_policy_present(adapter_pack, entry):
836
+ skill = adapter_pack.get(EMOTION_ENGINE_CODEX_SKILL_PATH, "")
837
+ helper = adapter_pack.get(EMOTION_ENGINE_CODEX_HELPER_PATH, "")
838
+ return (
839
+ "record_policy" in entry
840
+ and "record_policy" in skill
841
+ and "Runtime Modes And Record Policy" in skill
842
+ and "record_policy" in helper
843
+ and "parse_record_policy_args" in helper
844
+ and "reply_bias" in helper
845
+ and '"decision"' in helper
846
+ and "generic_praise_habituated" in helper
847
+ )
848
+
849
+
850
+ def _emotion_engine_codex_mcp_present(adapter_pack, manifest):
851
+ mcp = adapter_pack.get(EMOTION_ENGINE_CODEX_MCP_PATH, "")
852
+ registration = adapter_pack.get(EMOTION_ENGINE_CODEX_MCP_REGISTRATION_PATH, "")
853
+ artifacts = set(manifest.get("artifacts", [])) if isinstance(manifest, dict) else set()
854
+ return (
855
+ EMOTION_ENGINE_CODEX_MCP_PATH in adapter_pack
856
+ and EMOTION_ENGINE_CODEX_MCP_REGISTRATION_PATH in adapter_pack
857
+ and EMOTION_ENGINE_CODEX_MCP_PATH in artifacts
858
+ and EMOTION_ENGINE_CODEX_MCP_REGISTRATION_PATH in artifacts
859
+ and "tools/list" in mcp
860
+ and "emotion_engine_record_policy" in mcp
861
+ and "codex" in registration
862
+ and "state" in registration
863
+ and "emotion_engine_repair" not in mcp
864
+ and "doctor_target" not in mcp
865
+ )
866
+
867
+
868
+ def _emotion_engine_codex_project_wrapper_present(adapter_pack, manifest):
869
+ wrapper = adapter_pack.get(EMOTION_ENGINE_CODEX_WRAPPER_PATH, "")
870
+ artifacts = set(manifest.get("artifacts", [])) if isinstance(manifest, dict) else set()
871
+ return (
872
+ EMOTION_ENGINE_CODEX_WRAPPER_PATH in adapter_pack
873
+ and EMOTION_ENGINE_CODEX_WRAPPER_PATH in artifacts
874
+ and EMOTION_ENGINE_CODEX_SCRIPT_PATH in wrapper
875
+ )
876
+
877
+
878
+ def _relationship_state_not_runtime_state(text):
879
+ forbidden = (
880
+ '"pleasure"',
881
+ '"arousal"',
882
+ '"dominance"',
883
+ '"trust"',
884
+ "trust_history",
885
+ "emotion_trajectory",
886
+ "emotion-engine-state/v2",
887
+ )
888
+ return not any(item in text for item in forbidden)
889
+
890
+
891
+ def _emotion_engine_codex_diagnostic_message(diagnostics, fallback):
892
+ if not diagnostics:
893
+ return fallback
894
+ return "; ".join(issue["message"] for issue in diagnostics)
895
+
896
+
897
+ def _parse_json(text):
898
+ try:
899
+ return json.loads(text)
900
+ except (TypeError, json.JSONDecodeError):
901
+ return {}
902
+
903
+
904
+ def _add(checks, check_id, passed, weight, message):
905
+ checks.append(
906
+ {
907
+ "id": check_id,
908
+ "passed": bool(passed),
909
+ "weight": weight,
910
+ "message": message,
911
+ }
912
+ )
913
+
914
+
915
+ def _result(checks, threshold):
916
+ total = sum(check["weight"] for check in checks)
917
+ earned = sum(check["weight"] for check in checks if check["passed"])
918
+ score = round((earned / total) * 100, 2) if total else 0.0
919
+ return {
920
+ "score": score,
921
+ "threshold": threshold,
922
+ "passed": score >= threshold and all(check["passed"] for check in checks),
923
+ "checks": checks,
924
+ }