@event4u/agent-config 1.13.0 → 1.15.0

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 (291) hide show
  1. package/.agent-src/commands/agent-handoff.md +4 -1
  2. package/.agent-src/commands/agent-status.md +3 -0
  3. package/.agent-src/commands/agents-audit.md +4 -0
  4. package/.agent-src/commands/agents-cleanup.md +6 -1
  5. package/.agent-src/commands/agents-prepare.md +3 -0
  6. package/.agent-src/commands/analyze-reference-repo.md +4 -0
  7. package/.agent-src/commands/bug-fix.md +7 -3
  8. package/.agent-src/commands/bug-investigate.md +4 -0
  9. package/.agent-src/commands/chat-history-checkpoint.md +126 -0
  10. package/.agent-src/commands/chat-history-clear.md +6 -1
  11. package/.agent-src/commands/chat-history-resume.md +7 -2
  12. package/.agent-src/commands/chat-history.md +7 -2
  13. package/.agent-src/commands/check-current-md.md +137 -0
  14. package/.agent-src/commands/commit-in-chunks.md +118 -0
  15. package/.agent-src/commands/commit.md +4 -0
  16. package/.agent-src/commands/compress.md +37 -2
  17. package/.agent-src/commands/context-create.md +4 -0
  18. package/.agent-src/commands/context-refactor.md +4 -0
  19. package/.agent-src/commands/copilot-agents-init.md +3 -0
  20. package/.agent-src/commands/copilot-agents-optimize.md +3 -0
  21. package/.agent-src/commands/create-pr-description.md +4 -0
  22. package/.agent-src/commands/create-pr.md +4 -0
  23. package/.agent-src/commands/do-and-judge.md +4 -1
  24. package/.agent-src/commands/do-in-steps.md +3 -0
  25. package/.agent-src/commands/e2e-heal.md +4 -0
  26. package/.agent-src/commands/e2e-plan.md +4 -0
  27. package/.agent-src/commands/estimate-ticket.md +4 -1
  28. package/.agent-src/commands/feature-dev.md +4 -0
  29. package/.agent-src/commands/feature-explore.md +4 -0
  30. package/.agent-src/commands/feature-plan.md +4 -0
  31. package/.agent-src/commands/feature-refactor.md +4 -0
  32. package/.agent-src/commands/feature-roadmap.md +6 -0
  33. package/.agent-src/commands/fix-ci.md +4 -0
  34. package/.agent-src/commands/fix-portability.md +5 -2
  35. package/.agent-src/commands/fix-pr-bot-comments.md +4 -0
  36. package/.agent-src/commands/fix-pr-comments.md +4 -0
  37. package/.agent-src/commands/fix-pr-developer-comments.md +4 -0
  38. package/.agent-src/commands/fix-references.md +3 -0
  39. package/.agent-src/commands/fix-seeder.md +4 -0
  40. package/.agent-src/commands/implement-ticket.md +39 -13
  41. package/.agent-src/commands/jira-ticket.md +4 -0
  42. package/.agent-src/commands/judge.md +3 -0
  43. package/.agent-src/commands/memory-add.md +5 -3
  44. package/.agent-src/commands/memory-full.md +5 -2
  45. package/.agent-src/commands/memory-promote.md +7 -6
  46. package/.agent-src/commands/mode.md +3 -0
  47. package/.agent-src/commands/module-create.md +4 -0
  48. package/.agent-src/commands/module-explore.md +4 -0
  49. package/.agent-src/commands/onboard.md +33 -0
  50. package/.agent-src/commands/optimize-agents.md +4 -0
  51. package/.agent-src/commands/optimize-augmentignore.md +12 -0
  52. package/.agent-src/commands/optimize-rtk-filters.md +3 -0
  53. package/.agent-src/commands/optimize-skills.md +4 -0
  54. package/.agent-src/commands/override-create.md +4 -0
  55. package/.agent-src/commands/override-manage.md +4 -0
  56. package/.agent-src/commands/package-reset.md +3 -0
  57. package/.agent-src/commands/package-test.md +3 -0
  58. package/.agent-src/commands/prepare-for-review.md +4 -0
  59. package/.agent-src/commands/project-analyze.md +4 -0
  60. package/.agent-src/commands/project-health.md +4 -0
  61. package/.agent-src/commands/propose-memory.md +6 -8
  62. package/.agent-src/commands/quality-fix.md +4 -0
  63. package/.agent-src/commands/refine-ticket.md +12 -7
  64. package/.agent-src/commands/review-changes.md +39 -8
  65. package/.agent-src/commands/review-routing.md +4 -0
  66. package/.agent-src/commands/roadmap-create.md +18 -0
  67. package/.agent-src/commands/roadmap-execute.md +14 -1
  68. package/.agent-src/commands/rule-compliance-audit.md +4 -0
  69. package/.agent-src/commands/set-cost-profile.md +11 -0
  70. package/.agent-src/commands/sync-agent-settings.md +12 -0
  71. package/.agent-src/commands/sync-gitignore.md +3 -0
  72. package/.agent-src/commands/tests-create.md +4 -0
  73. package/.agent-src/commands/tests-execute.md +6 -3
  74. package/.agent-src/commands/threat-model.md +4 -0
  75. package/.agent-src/commands/update-form-request-messages.md +4 -0
  76. package/.agent-src/commands/upstream-contribute.md +4 -0
  77. package/.agent-src/commands/work.md +161 -0
  78. package/.agent-src/guidelines/agent-infra/engineering-memory-data-format.md +2 -6
  79. package/.agent-src/guidelines/agent-infra/layered-settings.md +0 -1
  80. package/.agent-src/guidelines/agent-infra/memory-access.md +0 -7
  81. package/.agent-src/guidelines/agent-infra/role-contracts.md +2 -4
  82. package/.agent-src/guidelines/agent-infra/self-improvement-pipeline.md +0 -1
  83. package/.agent-src/guidelines/php/patterns/strategy.md +180 -2
  84. package/.agent-src/personas/README.md +0 -1
  85. package/.agent-src/rules/artifact-drafting-protocol.md +7 -2
  86. package/.agent-src/rules/artifact-engagement-recording.md +133 -0
  87. package/.agent-src/rules/ask-when-uncertain.md +18 -13
  88. package/.agent-src/rules/augment-portability.md +64 -37
  89. package/.agent-src/rules/autonomous-execution.md +158 -0
  90. package/.agent-src/rules/chat-history-cadence.md +109 -0
  91. package/.agent-src/rules/chat-history-ownership.md +123 -0
  92. package/.agent-src/rules/chat-history-visibility.md +96 -0
  93. package/.agent-src/rules/cli-output-handling.md +27 -4
  94. package/.agent-src/rules/command-suggestion.md +134 -0
  95. package/.agent-src/rules/commit-policy.md +109 -0
  96. package/.agent-src/rules/direct-answers.md +114 -0
  97. package/.agent-src/rules/docs-sync.md +36 -0
  98. package/.agent-src/rules/downstream-changes.md +10 -9
  99. package/.agent-src/rules/improve-before-implement.md +9 -6
  100. package/.agent-src/rules/language-and-tone.md +85 -6
  101. package/.agent-src/rules/non-destructive-by-default.md +117 -0
  102. package/.agent-src/rules/package-ci-checks.md +4 -0
  103. package/.agent-src/rules/preservation-guard.md +20 -0
  104. package/.agent-src/rules/roadmap-progress-sync.md +159 -27
  105. package/.agent-src/rules/role-mode-adherence.md +1 -1
  106. package/.agent-src/rules/scope-control.md +42 -1
  107. package/.agent-src/rules/size-enforcement.md +2 -3
  108. package/.agent-src/rules/skill-quality.md +3 -8
  109. package/.agent-src/rules/ui-audit-before-build.md +106 -0
  110. package/.agent-src/rules/user-interaction.md +107 -51
  111. package/.agent-src/scripts/update_roadmap_progress.py +73 -9
  112. package/.agent-src/skills/blade-ui/SKILL.md +47 -3
  113. package/.agent-src/skills/command-routing/SKILL.md +32 -0
  114. package/.agent-src/skills/command-writing/SKILL.md +52 -2
  115. package/.agent-src/skills/description-assist/SKILL.md +21 -0
  116. package/.agent-src/skills/estimate-ticket/SKILL.md +0 -1
  117. package/.agent-src/skills/existing-ui-audit/SKILL.md +202 -0
  118. package/.agent-src/skills/fe-design/SKILL.md +78 -61
  119. package/.agent-src/skills/file-editor/SKILL.md +9 -0
  120. package/.agent-src/skills/finishing-a-development-branch/SKILL.md +4 -0
  121. package/.agent-src/skills/flux/SKILL.md +31 -4
  122. package/.agent-src/skills/guideline-writing/SKILL.md +24 -2
  123. package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +51 -9
  124. package/.agent-src/skills/livewire/SKILL.md +49 -4
  125. package/.agent-src/skills/md-language-check/SKILL.md +103 -0
  126. package/.agent-src/skills/php-coder/SKILL.md +24 -0
  127. package/.agent-src/skills/react-shadcn-ui/SKILL.md +121 -0
  128. package/.agent-src/skills/refine-prompt/SKILL.md +220 -0
  129. package/.agent-src/skills/refine-ticket/SKILL.md +32 -28
  130. package/.agent-src/skills/roadmap-management/SKILL.md +24 -11
  131. package/.agent-src/skills/rule-writing/SKILL.md +23 -1
  132. package/.agent-src/skills/skill-writing/SKILL.md +3 -5
  133. package/.agent-src/skills/upstream-contribute/SKILL.md +3 -3
  134. package/.agent-src/skills/using-git-worktrees/SKILL.md +3 -1
  135. package/.agent-src/templates/AGENTS.md +24 -6
  136. package/.agent-src/templates/agent-settings.md +149 -0
  137. package/.agent-src/templates/roadmaps.md +11 -4
  138. package/.agent-src/templates/scripts/implement_ticket/__init__.py +63 -26
  139. package/.agent-src/templates/scripts/implement_ticket/__main__.py +8 -2
  140. package/.agent-src/templates/scripts/memory_lookup.py +1 -1
  141. package/.agent-src/templates/scripts/telemetry/__init__.py +42 -0
  142. package/.agent-src/templates/scripts/telemetry/aggregator.py +154 -0
  143. package/.agent-src/templates/scripts/telemetry/boundary.py +171 -0
  144. package/.agent-src/templates/scripts/telemetry/engagement.py +238 -0
  145. package/.agent-src/templates/scripts/telemetry/report_renderer.py +170 -0
  146. package/.agent-src/templates/scripts/telemetry/settings.py +112 -0
  147. package/.agent-src/templates/scripts/telemetry_record.py +166 -0
  148. package/.agent-src/templates/scripts/telemetry_report.py +161 -0
  149. package/.agent-src/templates/scripts/telemetry_status.py +142 -0
  150. package/.agent-src/templates/scripts/work_engine/__init__.py +58 -0
  151. package/.agent-src/templates/scripts/work_engine/__main__.py +9 -0
  152. package/.agent-src/templates/scripts/work_engine/cli.py +195 -0
  153. package/.agent-src/templates/scripts/work_engine/cli_args.py +116 -0
  154. package/.agent-src/templates/scripts/{implement_ticket → work_engine}/delivery_state.py +10 -3
  155. package/.agent-src/templates/scripts/work_engine/directives/__init__.py +33 -0
  156. package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +98 -0
  157. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/analyze.py +1 -1
  158. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/implement.py +3 -3
  159. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/memory.py +2 -2
  160. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/plan.py +2 -2
  161. package/.agent-src/templates/scripts/work_engine/directives/backend/refine.py +396 -0
  162. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/report.py +37 -5
  163. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/test.py +2 -2
  164. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/verify.py +2 -2
  165. package/.agent-src/templates/scripts/work_engine/directives/mixed/__init__.py +116 -0
  166. package/.agent-src/templates/scripts/work_engine/directives/mixed/contract.py +254 -0
  167. package/.agent-src/templates/scripts/work_engine/directives/mixed/stitch.py +229 -0
  168. package/.agent-src/templates/scripts/work_engine/directives/mixed/ui.py +231 -0
  169. package/.agent-src/templates/scripts/work_engine/directives/ui/__init__.py +113 -0
  170. package/.agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +44 -0
  171. package/.agent-src/templates/scripts/work_engine/directives/ui/apply.py +241 -0
  172. package/.agent-src/templates/scripts/work_engine/directives/ui/audit.py +414 -0
  173. package/.agent-src/templates/scripts/work_engine/directives/ui/design.py +335 -0
  174. package/.agent-src/templates/scripts/work_engine/directives/ui/polish.py +510 -0
  175. package/.agent-src/templates/scripts/work_engine/directives/ui/review.py +468 -0
  176. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/__init__.py +119 -0
  177. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.py +37 -0
  178. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.py +165 -0
  179. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.py +66 -0
  180. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/report.py +62 -0
  181. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/test.py +115 -0
  182. package/.agent-src/templates/scripts/work_engine/dispatcher.py +331 -0
  183. package/.agent-src/templates/scripts/work_engine/emitters.py +43 -0
  184. package/.agent-src/templates/scripts/work_engine/errors.py +19 -0
  185. package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +76 -0
  186. package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +54 -0
  187. package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +32 -0
  188. package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +103 -0
  189. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +44 -0
  190. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +42 -0
  191. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +50 -0
  192. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +49 -0
  193. package/.agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.py +53 -0
  194. package/.agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.py +50 -0
  195. package/.agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.py +52 -0
  196. package/.agent-src/templates/scripts/work_engine/hooks/builtin/trace.py +84 -0
  197. package/.agent-src/templates/scripts/work_engine/hooks/context.py +66 -0
  198. package/.agent-src/templates/scripts/work_engine/hooks/events.py +44 -0
  199. package/.agent-src/templates/scripts/work_engine/hooks/exceptions.py +79 -0
  200. package/.agent-src/templates/scripts/work_engine/hooks/registry.py +60 -0
  201. package/.agent-src/templates/scripts/work_engine/hooks/runner.py +73 -0
  202. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +141 -0
  203. package/.agent-src/templates/scripts/work_engine/input_builders.py +163 -0
  204. package/.agent-src/templates/scripts/work_engine/intent/__init__.py +47 -0
  205. package/.agent-src/templates/scripts/work_engine/intent/classify.py +280 -0
  206. package/.agent-src/templates/scripts/work_engine/migration/__init__.py +8 -0
  207. package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +231 -0
  208. package/.agent-src/templates/scripts/{implement_ticket → work_engine}/persona_policy.py +1 -1
  209. package/.agent-src/templates/scripts/work_engine/resolvers/__init__.py +22 -0
  210. package/.agent-src/templates/scripts/work_engine/resolvers/diff.py +106 -0
  211. package/.agent-src/templates/scripts/work_engine/resolvers/file.py +113 -0
  212. package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +90 -0
  213. package/.agent-src/templates/scripts/work_engine/scoring/__init__.py +14 -0
  214. package/.agent-src/templates/scripts/work_engine/scoring/confidence.py +300 -0
  215. package/.agent-src/templates/scripts/work_engine/stack/__init__.py +31 -0
  216. package/.agent-src/templates/scripts/work_engine/stack/detect.py +187 -0
  217. package/.agent-src/templates/scripts/work_engine/state.py +641 -0
  218. package/.agent-src/templates/scripts/work_engine/state_io.py +202 -0
  219. package/.claude-plugin/marketplace.json +105 -2
  220. package/AGENTS.md +38 -8
  221. package/CHANGELOG.md +609 -0
  222. package/README.md +136 -14
  223. package/config/agent-settings.template.yml +45 -0
  224. package/config/gitignore-block.txt +4 -0
  225. package/docs/MIGRATION.md +122 -0
  226. package/docs/architecture.md +111 -35
  227. package/docs/contracts/STABILITY.md +95 -0
  228. package/docs/contracts/adr-chat-history-split.md +132 -0
  229. package/docs/contracts/adr-command-suggestion.md +146 -0
  230. package/docs/contracts/adr-implement-ticket-runtime.md +122 -0
  231. package/docs/contracts/adr-product-ui-track.md +384 -0
  232. package/docs/contracts/adr-prompt-driven-execution.md +187 -0
  233. package/docs/contracts/agent-memory-contract.md +149 -0
  234. package/docs/contracts/artifact-engagement-flow.md +262 -0
  235. package/docs/contracts/command-clusters.md +126 -0
  236. package/docs/contracts/command-suggestion-flow.md +148 -0
  237. package/docs/contracts/implement-ticket-flow.md +628 -0
  238. package/docs/contracts/linear-ai-rules-inclusion.md +143 -0
  239. package/docs/contracts/linear-ai-three-layers.md +131 -0
  240. package/docs/contracts/rule-interactions.md +107 -0
  241. package/docs/contracts/rule-interactions.yml +142 -0
  242. package/docs/contracts/ui-stack-extension.md +236 -0
  243. package/docs/contracts/ui-track-flow.md +338 -0
  244. package/docs/development.md +1 -1
  245. package/docs/getting-started.md +3 -3
  246. package/docs/installation.md +124 -2
  247. package/docs/migrations/commands-1.15.0.md +112 -0
  248. package/docs/showcase.md +204 -0
  249. package/docs/ui-track-mental-model.md +121 -0
  250. package/package.json +1 -1
  251. package/scripts/agent-config +199 -0
  252. package/scripts/audit_cloud_compatibility.py +288 -0
  253. package/scripts/build_cloud_bundle.py +458 -0
  254. package/scripts/build_linear_digest.py +263 -0
  255. package/scripts/chat_history.py +796 -7
  256. package/scripts/check_compression.py +139 -0
  257. package/scripts/check_iron_law_prominence.py +143 -0
  258. package/scripts/check_md_language.py +159 -0
  259. package/scripts/check_portability.py +38 -0
  260. package/scripts/check_public_links.py +185 -0
  261. package/scripts/check_references.py +1 -0
  262. package/scripts/check_reply_consistency.py +140 -0
  263. package/scripts/command_suggester/__init__.py +51 -0
  264. package/scripts/command_suggester/cooldown.py +132 -0
  265. package/scripts/command_suggester/loader.py +70 -0
  266. package/scripts/command_suggester/match.py +180 -0
  267. package/scripts/command_suggester/rank.py +120 -0
  268. package/scripts/command_suggester/render.py +86 -0
  269. package/scripts/command_suggester/sanitize.py +113 -0
  270. package/scripts/command_suggester/settings.py +125 -0
  271. package/scripts/command_suggester/types.py +78 -0
  272. package/scripts/hooks/augment-chat-history.sh +56 -0
  273. package/scripts/install-hooks.sh +67 -0
  274. package/scripts/install.py +150 -33
  275. package/scripts/lint_marketplace.py +27 -0
  276. package/scripts/lint_no_new_atomic_commands.py +179 -0
  277. package/scripts/lint_rule_interactions.py +149 -0
  278. package/scripts/memory_lookup.py +1 -1
  279. package/scripts/migrate_command_suggestions.py +151 -0
  280. package/scripts/release.py +297 -64
  281. package/scripts/schemas/command.schema.json +41 -0
  282. package/scripts/skill_linter.py +81 -0
  283. package/scripts/sync_agent_settings.py +42 -12
  284. package/scripts/update_counts.py +10 -0
  285. package/templates/consumer-settings/augment-cli-hooks.json +54 -0
  286. package/templates/consumer-settings/claude-settings.json +55 -1
  287. package/.agent-src/rules/chat-history.md +0 -171
  288. package/.agent-src/templates/scripts/implement_ticket/cli.py +0 -171
  289. package/.agent-src/templates/scripts/implement_ticket/dispatcher.py +0 -134
  290. package/.agent-src/templates/scripts/implement_ticket/steps/__init__.py +0 -49
  291. package/.agent-src/templates/scripts/implement_ticket/steps/refine.py +0 -140
@@ -0,0 +1,641 @@
1
+ """Schema v1 of the universal-engine work state.
2
+
3
+ The wire format adds five envelope fields on top of the legacy
4
+ ``DeliveryState`` shape from ``implement_ticket.delivery_state``:
5
+
6
+ - ``version`` — integer schema version, currently ``1``.
7
+ - ``input.kind`` — typed input variant (only ``"ticket"`` for R1).
8
+ - ``input.data`` — the original payload (was ``state.ticket`` in v0).
9
+ - ``intent`` — coarse intent label (``"backend-coding"`` for R1).
10
+ - ``directive_set`` — name of the directive bundle the dispatcher
11
+ loads. The enum is forward-compatible: ``ui``, ``ui-trivial``, and
12
+ ``mixed`` are accepted by the schema even though only ``backend``
13
+ has working directives in R1 (Phase 4 Step 4 — pre-listed to avoid
14
+ a schema bump when R3 V2 lands).
15
+ - ``stack`` — optional ``{frontend, mtime}`` cache populated by
16
+ :mod:`work_engine.stack.detect` (R3 Phase 1). ``None`` while the
17
+ detector has not yet run; the dispatcher fills it on the first UI
18
+ dispatch and re-runs detection when ``mtime`` no longer matches the
19
+ filesystem (manifest edited).
20
+ - ``ui_audit`` — optional inventory written by the
21
+ ``existing-ui-audit`` skill (R3 Phase 2). ``None`` while the audit
22
+ has not run; populated dict once the skill returns. ``greenfield``
23
+ flag plus ``greenfield_decision`` carry the user's scaffolding
24
+ pick. The audit gate (:mod:`work_engine.directives.ui.audit`)
25
+ refuses to advance to design/apply while the slot is empty or
26
+ while ``greenfield`` is set without a recorded decision.
27
+ - ``ui_design`` — optional design brief produced by
28
+ :mod:`work_engine.directives.ui.design` (R3 Phase 3 Step 1). Locks
29
+ layout / components / states / microcopy / a11y; ``design_confirmed``
30
+ carries the user's sign-off.
31
+ - ``ui_review`` — optional review-pass output written by
32
+ :mod:`work_engine.directives.ui.review` (R3 Phase 3 Step 4). Carries
33
+ the design-review findings list and a ``review_clean`` flag set when
34
+ no findings remain.
35
+ - ``ui_polish`` — optional polish-pass log written by
36
+ :mod:`work_engine.directives.ui.polish` (R3 Phase 3 Step 5). Tracks
37
+ the round counter (``rounds`` ≤ 2 ceiling) and the per-round
38
+ applied-fix list so a re-entry knows whether the loop has been
39
+ exhausted.
40
+ - ``contract`` — optional backend-contract envelope written by
41
+ :mod:`work_engine.directives.mixed.contract` (R3 Phase 4 Step 1).
42
+ Locks ``data_model`` and ``api_surface`` before any UI work starts;
43
+ ``contract_confirmed`` carries the user's sign-off. The mixed UI
44
+ step refuses to advance without a confirmed contract — this is the
45
+ sentinel that prevents UI work from racing ahead of the backend.
46
+ - ``stitch`` — optional integration-verification envelope written by
47
+ :mod:`work_engine.directives.mixed.stitch` (R3 Phase 4 Step 3).
48
+ Carries the end-to-end smoke ``scenarios`` list, an aggregate
49
+ ``verdict`` (success / blocked / partial), and the
50
+ ``integration_confirmed`` flag the user sets after reviewing the
51
+ integration evidence.
52
+
53
+ All other fields keep their v0 names so the dispatcher can read the
54
+ legacy slice unchanged once Phase 3 wires the steps over.
55
+
56
+ The module exposes:
57
+
58
+ - :data:`SCHEMA_VERSION` — the integer carried in ``version``.
59
+ - :data:`KNOWN_INPUT_KINDS` / :data:`KNOWN_DIRECTIVE_SETS` — the
60
+ whitelists used by validation.
61
+ - :class:`Input`, :class:`WorkState` — typed dataclasses.
62
+ - :func:`from_dict` / :func:`to_dict` — JSON round-trip helpers.
63
+ - :class:`SchemaError` — raised on every validation failure.
64
+ """
65
+ from __future__ import annotations
66
+
67
+ import json
68
+ from dataclasses import dataclass, field
69
+ from pathlib import Path
70
+ from typing import Any
71
+
72
+ SCHEMA_VERSION = 1
73
+ """Integer version stored under the ``version`` key on disk."""
74
+
75
+ DEFAULT_INTENT = "backend-coding"
76
+ """Intent applied when migrating a v0 file or building a fresh state."""
77
+
78
+ DEFAULT_DIRECTIVE_SET = "backend"
79
+ """Directive set applied when migrating a v0 file or building a fresh state."""
80
+
81
+ KNOWN_INPUT_KINDS: frozenset[str] = frozenset({"ticket", "prompt", "diff", "file"})
82
+ """Input kinds accepted by the schema.
83
+
84
+ ``ticket`` is the R1 kind: pre-structured ``{id, title, acceptance_criteria, …}``
85
+ fed by the ``/implement-ticket`` flow. ``prompt`` is the R2 kind: a free-form
86
+ user prompt wrapped via :mod:`work_engine.resolvers.prompt` into
87
+ ``{raw, reconstructed_ac, assumptions}``; the engine refines the raw text
88
+ into actionable AC + a confidence band before plan/apply/test/review run.
89
+
90
+ ``diff`` and ``file`` are the R3 Phase 1 UI-improve kinds. ``diff`` carries a
91
+ unified-diff / patch payload (``{raw, reconstructed_ac, assumptions}``) so the
92
+ ``directives/ui`` set can take an "improve this screen" PR-style input; ``file``
93
+ carries a path reference to an existing component/page (``{path,
94
+ reconstructed_ac, assumptions}``) for the same surface. Both default-route to
95
+ ``ui-improve`` via :func:`work_engine.intent.populate_routing`.
96
+
97
+ Per the schema/capability split documented on
98
+ :data:`work_engine.directives.backend.SUPPORTED_KINDS`, presence here only
99
+ means the *envelope* is accepted on disk — a directive set still has to
100
+ list the kind in its ``SUPPORTED_KINDS`` tuple before the dispatcher will
101
+ route it. R2 widens the envelope; R2 Phase 3 widens backend's capability
102
+ tuple in lockstep with the ``refine-prompt`` skill landing. R3 Phase 1 widens
103
+ the envelope further; ``ui`` capability is wired in Phase 3 of the UI track.
104
+
105
+ Other kinds are rejected so unknown values surface as errors instead of
106
+ silently falling through to a default branch."""
107
+
108
+ KNOWN_DIRECTIVE_SETS: frozenset[str] = frozenset(
109
+ {"backend", "ui", "ui-trivial", "mixed"},
110
+ )
111
+ """Directive sets recognised by the schema.
112
+
113
+ Per the roadmap (Phase 4 Step 4), ``ui``, ``ui-trivial``, and ``mixed``
114
+ are intentionally pre-listed so a future R3 V2 release does not need a
115
+ schema bump. Only ``backend`` has working directives in R1; the others
116
+ raise ``NotImplementedError`` at dispatch time."""
117
+
118
+
119
+ class SchemaError(ValueError):
120
+ """Raised when a state payload violates the v1 contract."""
121
+
122
+
123
+ @dataclass
124
+ class Input:
125
+ """Typed envelope for the user-supplied work item.
126
+
127
+ ``kind`` is one of :data:`KNOWN_INPUT_KINDS`; ``data`` is the raw
128
+ payload. The legacy ``state.ticket`` dict lands here as
129
+ ``Input(kind="ticket", data=<dict>)`` after migration.
130
+ """
131
+
132
+ kind: str
133
+ data: dict[str, Any] = field(default_factory=dict)
134
+
135
+
136
+ @dataclass
137
+ class WorkState:
138
+ """Schema v1 of the persisted work state.
139
+
140
+ Field order mirrors the on-disk JSON: envelope (``version``,
141
+ ``input``, ``intent``, ``directive_set``, ``stack``) first, then
142
+ the legacy ``DeliveryState`` slice (``persona`` … ``report``) so a
143
+ diff between a v1 file and its v0 ancestor stays readable.
144
+ """
145
+
146
+ input: Input
147
+ intent: str = DEFAULT_INTENT
148
+ directive_set: str = DEFAULT_DIRECTIVE_SET
149
+ stack: dict[str, Any] | None = None
150
+ ui_audit: dict[str, Any] | None = None
151
+ ui_design: dict[str, Any] | None = None
152
+ ui_review: dict[str, Any] | None = None
153
+ ui_polish: dict[str, Any] | None = None
154
+ contract: dict[str, Any] | None = None
155
+ stitch: dict[str, Any] | None = None
156
+ version: int = SCHEMA_VERSION
157
+ persona: str = "senior-engineer"
158
+ memory: list[dict[str, Any]] = field(default_factory=list)
159
+ plan: Any = None
160
+ changes: list[dict[str, Any]] = field(default_factory=list)
161
+ tests: Any = None
162
+ verify: Any = None
163
+ outcomes: dict[str, str] = field(default_factory=dict)
164
+ questions: list[str] = field(default_factory=list)
165
+ report: str = ""
166
+
167
+
168
+ def to_dict(state: WorkState) -> dict[str, Any]:
169
+ """Serialise ``state`` to the canonical v1 JSON shape.
170
+
171
+ Field order is fixed: ``version`` → ``input`` → ``intent`` →
172
+ ``directive_set`` → legacy slice. Stable order keeps state
173
+ snapshots diff-friendly across re-runs and across the freeze-guard
174
+ replay. Validation runs before serialisation so an in-memory
175
+ object that was mutated past the schema cannot reach disk.
176
+ """
177
+ _validate_kind(state.input.kind)
178
+ _validate_directive_set(state.directive_set)
179
+ if state.version != SCHEMA_VERSION:
180
+ raise SchemaError(
181
+ f"version must be {SCHEMA_VERSION}; got {state.version!r}",
182
+ )
183
+ _validate_stack(state.stack)
184
+ _validate_ui_audit(state.ui_audit)
185
+ _validate_ui_design(state.ui_design)
186
+ _validate_ui_review(state.ui_review)
187
+ _validate_ui_polish(state.ui_polish)
188
+ _validate_contract(state.contract)
189
+ _validate_stitch(state.stitch)
190
+ return {
191
+ "version": state.version,
192
+ "input": {"kind": state.input.kind, "data": state.input.data},
193
+ "intent": state.intent,
194
+ "directive_set": state.directive_set,
195
+ "stack": state.stack,
196
+ "ui_audit": state.ui_audit,
197
+ "ui_design": state.ui_design,
198
+ "ui_review": state.ui_review,
199
+ "ui_polish": state.ui_polish,
200
+ "contract": state.contract,
201
+ "stitch": state.stitch,
202
+ "persona": state.persona,
203
+ "memory": state.memory,
204
+ "plan": state.plan,
205
+ "changes": state.changes,
206
+ "tests": state.tests,
207
+ "verify": state.verify,
208
+ "outcomes": state.outcomes,
209
+ "questions": state.questions,
210
+ "report": state.report,
211
+ }
212
+
213
+
214
+ def from_dict(payload: Any) -> WorkState:
215
+ """Build a :class:`WorkState` from a parsed JSON payload.
216
+
217
+ Validates the envelope (``version``, ``input.kind``,
218
+ ``directive_set``) before instantiating the dataclass. Unknown
219
+ top-level keys are tolerated and dropped — the schema is additive,
220
+ not strict-rejecting, so a future field rolled out by a newer
221
+ engine version does not crash an older reader.
222
+ """
223
+ if not isinstance(payload, dict):
224
+ raise SchemaError(
225
+ f"state payload must be a JSON object; got {type(payload).__name__}",
226
+ )
227
+
228
+ version = payload.get("version")
229
+ if version != SCHEMA_VERSION:
230
+ raise SchemaError(
231
+ f"version must be {SCHEMA_VERSION}; got {version!r}. "
232
+ "Run the v0→v1 migration before loading legacy files.",
233
+ )
234
+
235
+ raw_input = payload.get("input")
236
+ if not isinstance(raw_input, dict):
237
+ raise SchemaError(
238
+ "state.input must be a JSON object with 'kind' and 'data' keys",
239
+ )
240
+ kind = raw_input.get("kind")
241
+ _validate_kind(kind)
242
+ data = raw_input.get("data", {})
243
+ if not isinstance(data, dict):
244
+ raise SchemaError(
245
+ f"state.input.data must be a JSON object; got {type(data).__name__}",
246
+ )
247
+
248
+ directive_set = payload.get("directive_set", DEFAULT_DIRECTIVE_SET)
249
+ _validate_directive_set(directive_set)
250
+
251
+ stack = payload.get("stack")
252
+ _validate_stack(stack)
253
+
254
+ ui_audit = payload.get("ui_audit")
255
+ _validate_ui_audit(ui_audit)
256
+
257
+ ui_design = payload.get("ui_design")
258
+ _validate_ui_design(ui_design)
259
+
260
+ ui_review = payload.get("ui_review")
261
+ _validate_ui_review(ui_review)
262
+
263
+ ui_polish = payload.get("ui_polish")
264
+ _validate_ui_polish(ui_polish)
265
+
266
+ contract = payload.get("contract")
267
+ _validate_contract(contract)
268
+
269
+ stitch = payload.get("stitch")
270
+ _validate_stitch(stitch)
271
+
272
+ return WorkState(
273
+ input=Input(kind=kind, data=data),
274
+ intent=payload.get("intent", DEFAULT_INTENT),
275
+ directive_set=directive_set,
276
+ stack=dict(stack) if isinstance(stack, dict) else None,
277
+ ui_audit=dict(ui_audit) if isinstance(ui_audit, dict) else None,
278
+ ui_design=dict(ui_design) if isinstance(ui_design, dict) else None,
279
+ ui_review=dict(ui_review) if isinstance(ui_review, dict) else None,
280
+ ui_polish=dict(ui_polish) if isinstance(ui_polish, dict) else None,
281
+ contract=dict(contract) if isinstance(contract, dict) else None,
282
+ stitch=dict(stitch) if isinstance(stitch, dict) else None,
283
+ version=version,
284
+ persona=payload.get("persona", "senior-engineer"),
285
+ memory=list(payload.get("memory", [])),
286
+ plan=payload.get("plan"),
287
+ changes=list(payload.get("changes", [])),
288
+ tests=payload.get("tests"),
289
+ verify=payload.get("verify"),
290
+ outcomes=dict(payload.get("outcomes", {})),
291
+ questions=list(payload.get("questions", [])),
292
+ report=payload.get("report", ""),
293
+ )
294
+
295
+
296
+ def load(path: Path) -> WorkState:
297
+ """Read a v1 state file from disk and return a :class:`WorkState`."""
298
+ raw = path.read_text(encoding="utf-8")
299
+ try:
300
+ payload = json.loads(raw)
301
+ except json.JSONDecodeError as exc:
302
+ raise SchemaError(f"invalid JSON in {path}: {exc}") from exc
303
+ return from_dict(payload)
304
+
305
+
306
+ def dump(state: WorkState, path: Path) -> None:
307
+ """Write ``state`` to ``path`` as pretty JSON, terminating newline included."""
308
+ path.parent.mkdir(parents=True, exist_ok=True)
309
+ path.write_text(
310
+ json.dumps(to_dict(state), indent=2, ensure_ascii=False) + "\n",
311
+ encoding="utf-8",
312
+ )
313
+
314
+
315
+ def _validate_kind(kind: Any) -> None:
316
+ if not isinstance(kind, str):
317
+ raise SchemaError(
318
+ f"state.input.kind must be a string; got {type(kind).__name__}",
319
+ )
320
+ if kind not in KNOWN_INPUT_KINDS:
321
+ raise SchemaError(
322
+ f"unknown input.kind {kind!r}; "
323
+ f"expected one of {sorted(KNOWN_INPUT_KINDS)}",
324
+ )
325
+
326
+
327
+ def _validate_directive_set(name: Any) -> None:
328
+ if not isinstance(name, str):
329
+ raise SchemaError(
330
+ f"state.directive_set must be a string; got {type(name).__name__}",
331
+ )
332
+ if name not in KNOWN_DIRECTIVE_SETS:
333
+ raise SchemaError(
334
+ f"unknown directive_set {name!r}; "
335
+ f"expected one of {sorted(KNOWN_DIRECTIVE_SETS)}",
336
+ )
337
+
338
+
339
+ def _validate_stack(stack: Any) -> None:
340
+ """Reject malformed stack envelopes; tolerate ``None`` (not yet detected).
341
+
342
+ The detector populates ``state.stack`` lazily — the first dispatch
343
+ of a new state file may run without it set, then the dispatcher
344
+ fills it in before any UI handler reads it. We only validate the
345
+ shape when present so the absence-of-detection case stays a normal
346
+ code path, not an error.
347
+ """
348
+ if stack is None:
349
+ return
350
+ if not isinstance(stack, dict):
351
+ raise SchemaError(
352
+ f"state.stack must be a JSON object or null; "
353
+ f"got {type(stack).__name__}",
354
+ )
355
+ frontend = stack.get("frontend")
356
+ if not isinstance(frontend, str) or not frontend:
357
+ raise SchemaError(
358
+ "state.stack.frontend must be a non-empty string",
359
+ )
360
+ mtime = stack.get("mtime", 0.0)
361
+ if not isinstance(mtime, (int, float)):
362
+ raise SchemaError(
363
+ f"state.stack.mtime must be a number; got {type(mtime).__name__}",
364
+ )
365
+
366
+
367
+ def _validate_ui_audit(ui_audit: Any) -> None:
368
+ """Reject malformed ``ui_audit`` envelopes; tolerate ``None`` and ``{}``.
369
+
370
+ ``None`` means the audit has not run yet — the dispatcher's audit
371
+ gate (``directives.ui.audit``) will emit the agent-directive that
372
+ populates it. An empty dict is the in-progress shape after the
373
+ skill returns but before findings land; the gate treats it the
374
+ same as ``None``. Once populated, ``greenfield`` (when present)
375
+ must be a bool, and ``greenfield_decision`` (when present) must
376
+ be one of the three documented choices. Other keys (``components``,
377
+ ``patterns``, …) are validated by the audit handler against the
378
+ skill contract — the schema only enforces shape, not content.
379
+ """
380
+ if ui_audit is None:
381
+ return
382
+ if not isinstance(ui_audit, dict):
383
+ raise SchemaError(
384
+ f"state.ui_audit must be a JSON object or null; "
385
+ f"got {type(ui_audit).__name__}",
386
+ )
387
+ if "greenfield" in ui_audit and not isinstance(
388
+ ui_audit["greenfield"], bool,
389
+ ):
390
+ raise SchemaError(
391
+ "state.ui_audit.greenfield must be a boolean when present",
392
+ )
393
+ decision = ui_audit.get("greenfield_decision")
394
+ if decision is not None and decision not in {
395
+ "scaffold",
396
+ "bare",
397
+ "external_reference",
398
+ }:
399
+ raise SchemaError(
400
+ f"state.ui_audit.greenfield_decision must be one of "
401
+ f"'scaffold', 'bare', 'external_reference', or null; "
402
+ f"got {decision!r}",
403
+ )
404
+ if "a11y_baseline" in ui_audit and not isinstance(
405
+ ui_audit["a11y_baseline"], list,
406
+ ):
407
+ raise SchemaError(
408
+ "state.ui_audit.a11y_baseline must be a list when present",
409
+ )
410
+
411
+
412
+ def _validate_ui_design(ui_design: Any) -> None:
413
+ """Reject malformed ``ui_design`` envelopes; tolerate ``None`` and ``{}``.
414
+
415
+ ``None`` means the design step has not produced a brief yet — the
416
+ dispatcher's design gate (``directives.ui.design``) emits the
417
+ agent-directive that populates it. An empty dict is the in-progress
418
+ shape after the skill returns but before the brief lands; the gate
419
+ treats it the same as ``None``. Once populated, ``design_confirmed``
420
+ (when present) must be a bool. Other keys (``layout``, ``components``,
421
+ ``states``, ``microcopy``, ``a11y``, ``reused_from_audit``) are
422
+ validated by the design handler against the skill contract — the
423
+ schema only enforces shape, not content.
424
+ """
425
+ if ui_design is None:
426
+ return
427
+ if not isinstance(ui_design, dict):
428
+ raise SchemaError(
429
+ f"state.ui_design must be a JSON object or null; "
430
+ f"got {type(ui_design).__name__}",
431
+ )
432
+ if "design_confirmed" in ui_design and not isinstance(
433
+ ui_design["design_confirmed"], bool,
434
+ ):
435
+ raise SchemaError(
436
+ "state.ui_design.design_confirmed must be a boolean when present",
437
+ )
438
+
439
+
440
+ def _validate_ui_review(ui_review: Any) -> None:
441
+ """Reject malformed ``ui_review`` envelopes; tolerate ``None`` and ``{}``.
442
+
443
+ ``None`` means the review pass has not run yet — the dispatcher's
444
+ review gate (``directives.ui.review``) emits the agent-directive
445
+ that populates it. An empty dict is the in-progress shape after
446
+ the skill returns but before findings land. Once populated,
447
+ ``findings`` (when present) must be a list and ``review_clean``
448
+ (when present) must be a bool. Field content (severity labels,
449
+ fix suggestions) is validated by the review handler; the schema
450
+ enforces only shape.
451
+ """
452
+ if ui_review is None:
453
+ return
454
+ if not isinstance(ui_review, dict):
455
+ raise SchemaError(
456
+ f"state.ui_review must be a JSON object or null; "
457
+ f"got {type(ui_review).__name__}",
458
+ )
459
+ if "findings" in ui_review and not isinstance(ui_review["findings"], list):
460
+ raise SchemaError(
461
+ "state.ui_review.findings must be a list when present",
462
+ )
463
+ if "review_clean" in ui_review and not isinstance(
464
+ ui_review["review_clean"], bool,
465
+ ):
466
+ raise SchemaError(
467
+ "state.ui_review.review_clean must be a boolean when present",
468
+ )
469
+ a11y = ui_review.get("a11y")
470
+ if a11y is not None:
471
+ if not isinstance(a11y, dict):
472
+ raise SchemaError(
473
+ "state.ui_review.a11y must be a JSON object or null when present",
474
+ )
475
+ if "violations" in a11y and not isinstance(a11y["violations"], list):
476
+ raise SchemaError(
477
+ "state.ui_review.a11y.violations must be a list when present",
478
+ )
479
+ floor = a11y.get("severity_floor")
480
+ if floor is not None and floor not in {
481
+ "minor",
482
+ "moderate",
483
+ "serious",
484
+ "critical",
485
+ }:
486
+ raise SchemaError(
487
+ f"state.ui_review.a11y.severity_floor must be one of "
488
+ f"'minor', 'moderate', 'serious', 'critical', or null; "
489
+ f"got {floor!r}",
490
+ )
491
+ if "accepted_violations" in a11y and not isinstance(
492
+ a11y["accepted_violations"], list,
493
+ ):
494
+ raise SchemaError(
495
+ "state.ui_review.a11y.accepted_violations must be a list when present",
496
+ )
497
+ preview = ui_review.get("preview")
498
+ if preview is not None:
499
+ if not isinstance(preview, dict):
500
+ raise SchemaError(
501
+ "state.ui_review.preview must be a JSON object or null when present",
502
+ )
503
+ if "render_ok" in preview and not isinstance(preview["render_ok"], bool):
504
+ raise SchemaError(
505
+ "state.ui_review.preview.render_ok must be a boolean when present",
506
+ )
507
+
508
+
509
+ def _validate_ui_polish(ui_polish: Any) -> None:
510
+ """Reject malformed ``ui_polish`` envelopes; tolerate ``None`` and ``{}``.
511
+
512
+ ``None`` means the polish loop has not entered yet. Once
513
+ populated, ``rounds`` (when present) must be an int in ``[0, 2]``
514
+ by default — the polish-loop ceiling defined in
515
+ ``agents/roadmaps/road-to-product-ui-track.md`` Phase 3 Step 5.
516
+ R4 Phase 2 widens the upper bound to ``3`` when
517
+ ``extension_used`` is ``True`` (one-shot a11y extension halt).
518
+ ``applied`` (when present) must be a list. The polish handler
519
+ enforces ceiling semantics; the schema enforces only shape.
520
+ """
521
+ if ui_polish is None:
522
+ return
523
+ if not isinstance(ui_polish, dict):
524
+ raise SchemaError(
525
+ f"state.ui_polish must be a JSON object or null; "
526
+ f"got {type(ui_polish).__name__}",
527
+ )
528
+ if "extension_used" in ui_polish and not isinstance(
529
+ ui_polish["extension_used"], bool,
530
+ ):
531
+ raise SchemaError(
532
+ "state.ui_polish.extension_used must be a boolean when present",
533
+ )
534
+ extension_used = bool(ui_polish.get("extension_used", False))
535
+ if "rounds" in ui_polish:
536
+ rounds = ui_polish["rounds"]
537
+ if not isinstance(rounds, int) or isinstance(rounds, bool):
538
+ raise SchemaError(
539
+ f"state.ui_polish.rounds must be an integer; got {type(rounds).__name__}",
540
+ )
541
+ max_rounds = 3 if extension_used else 2
542
+ if rounds < 0 or rounds > max_rounds:
543
+ raise SchemaError(
544
+ f"state.ui_polish.rounds must be in [0, {max_rounds}]; "
545
+ f"got {rounds} (extension_used={extension_used})",
546
+ )
547
+ if "applied" in ui_polish and not isinstance(ui_polish["applied"], list):
548
+ raise SchemaError(
549
+ "state.ui_polish.applied must be a list when present",
550
+ )
551
+
552
+
553
+ def _validate_contract(contract: Any) -> None:
554
+ """Reject malformed ``contract`` envelopes; tolerate ``None`` and ``{}``.
555
+
556
+ ``None`` means the contract step has not run yet — the mixed
557
+ ``contract`` directive (R3 Phase 4 Step 1) emits the
558
+ agent-directive that populates it. Once populated,
559
+ ``data_model`` and ``api_surface`` (when present) must be lists,
560
+ and ``contract_confirmed`` (when present) must be a bool. Field
561
+ content (entity names, endpoint shapes) is validated by the
562
+ contract handler; the schema enforces only shape so the mixed UI
563
+ step's sentinel check (``contract_confirmed is True``) stays a
564
+ simple equality test.
565
+ """
566
+ if contract is None:
567
+ return
568
+ if not isinstance(contract, dict):
569
+ raise SchemaError(
570
+ f"state.contract must be a JSON object or null; "
571
+ f"got {type(contract).__name__}",
572
+ )
573
+ if "data_model" in contract and not isinstance(contract["data_model"], list):
574
+ raise SchemaError(
575
+ "state.contract.data_model must be a list when present",
576
+ )
577
+ if "api_surface" in contract and not isinstance(contract["api_surface"], list):
578
+ raise SchemaError(
579
+ "state.contract.api_surface must be a list when present",
580
+ )
581
+ if "contract_confirmed" in contract and not isinstance(
582
+ contract["contract_confirmed"], bool
583
+ ):
584
+ raise SchemaError(
585
+ "state.contract.contract_confirmed must be a bool when present",
586
+ )
587
+
588
+
589
+ def _validate_stitch(stitch: Any) -> None:
590
+ """Reject malformed ``stitch`` envelopes; tolerate ``None`` and ``{}``.
591
+
592
+ ``None`` means the integration-verification step has not run yet
593
+ — the mixed ``stitch`` directive (R3 Phase 4 Step 3) emits the
594
+ agent-directive that populates it. Once populated, ``scenarios``
595
+ (when present) must be a list of integration smoke cases,
596
+ ``verdict`` (when present) must be one of
597
+ ``{"success", "blocked", "partial"}``, and
598
+ ``integration_confirmed`` (when present) must be a bool. The
599
+ stitch handler enforces verdict semantics; the schema enforces
600
+ only shape.
601
+ """
602
+ if stitch is None:
603
+ return
604
+ if not isinstance(stitch, dict):
605
+ raise SchemaError(
606
+ f"state.stitch must be a JSON object or null; "
607
+ f"got {type(stitch).__name__}",
608
+ )
609
+ if "scenarios" in stitch and not isinstance(stitch["scenarios"], list):
610
+ raise SchemaError(
611
+ "state.stitch.scenarios must be a list when present",
612
+ )
613
+ if "verdict" in stitch:
614
+ verdict = stitch["verdict"]
615
+ if verdict not in {"success", "blocked", "partial"}:
616
+ raise SchemaError(
617
+ f"state.stitch.verdict must be one of success/blocked/partial; "
618
+ f"got {verdict!r}",
619
+ )
620
+ if "integration_confirmed" in stitch and not isinstance(
621
+ stitch["integration_confirmed"], bool
622
+ ):
623
+ raise SchemaError(
624
+ "state.stitch.integration_confirmed must be a bool when present",
625
+ )
626
+
627
+
628
+ __all__ = [
629
+ "DEFAULT_DIRECTIVE_SET",
630
+ "DEFAULT_INTENT",
631
+ "Input",
632
+ "KNOWN_DIRECTIVE_SETS",
633
+ "KNOWN_INPUT_KINDS",
634
+ "SCHEMA_VERSION",
635
+ "SchemaError",
636
+ "WorkState",
637
+ "dump",
638
+ "from_dict",
639
+ "load",
640
+ "to_dict",
641
+ ]