@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,280 @@
1
+ """Heuristic intent classifier — see :mod:`work_engine.intent` for context.
2
+
3
+ The classifier walks a small priority ladder against the lower-cased
4
+ prompt + optional ticket title. First match wins; ``backend-coding`` is
5
+ the fall-through default so every prompt always lands on a known label.
6
+
7
+ Priority order (deliberately fixed):
8
+
9
+ 1. **Trivial-UI** — UI signal AND a trivial-edit verb pattern (``change
10
+ color``, ``make … red``, ``rename label``, ``fix copy``) AND no
11
+ structural verb (``add``, ``build``, ``create``, ``introduce``).
12
+ 2. **Mixed** — UI signal AND a backend signal (``endpoint``, ``API``,
13
+ ``migration``, ``schema``, ``query``, ``job``, ``queue``).
14
+ 3. **UI-Improve** — UI signal AND an improve/redesign/refactor verb,
15
+ OR explicit "existing" surface markers.
16
+ 4. **UI-Build** — UI signal AND a build/create/add verb, OR new-screen
17
+ markers (``new page``, ``new screen``, ``new component``).
18
+ 5. **Backend-Coding** — default.
19
+
20
+ The label is the dispatcher's *only* input for routing. Confidence
21
+ band, ``ui_intent`` flag from the scorer, and AC reconstruction stay
22
+ the resolution surface — the classifier does not look at them.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import re
27
+ from typing import TYPE_CHECKING
28
+
29
+ if TYPE_CHECKING:
30
+ from ..state import WorkState
31
+
32
+ INTENT_UI_BUILD = "ui-build"
33
+ INTENT_UI_IMPROVE = "ui-improve"
34
+ INTENT_UI_TRIVIAL = "ui-trivial"
35
+ INTENT_MIXED = "mixed"
36
+ INTENT_BACKEND = "backend-coding"
37
+
38
+ KNOWN_INTENTS: frozenset[str] = frozenset(
39
+ {
40
+ INTENT_UI_BUILD,
41
+ INTENT_UI_IMPROVE,
42
+ INTENT_UI_TRIVIAL,
43
+ INTENT_MIXED,
44
+ INTENT_BACKEND,
45
+ },
46
+ )
47
+ """All labels the classifier can return.
48
+
49
+ Locked here so the dispatcher's mapping table and the test suite share
50
+ one source of truth."""
51
+
52
+ _UI_NOUNS: frozenset[str] = frozenset(
53
+ {
54
+ "ui", "screen", "page", "view", "form", "modal", "dialog",
55
+ "button", "card", "tile",
56
+ "header", "footer", "nav", "navigation", "sidebar", "menu",
57
+ "dropdown", "tab", "panel", "layout", "component", "icon",
58
+ "tooltip", "toast", "banner", "badge", "avatar", "label",
59
+ "checkbox", "radio", "toggle", "switch", "stepper", "wizard",
60
+ },
61
+ )
62
+ """Strong UI nouns — exclusive UI meaning.
63
+
64
+ Deliberately omits ``table``, ``list``, ``input``, and ``field``:
65
+ ``table``/``list`` collide with database tables and Python/PHP lists;
66
+ ``input``/``field`` collide with function inputs, command inputs, and
67
+ JSON/DB fields. Genuine UI prompts that mean form inputs always come
68
+ with a strong-UI noun nearby (``form``, ``page``, ``component``)."""
69
+
70
+ _UI_STYLE: frozenset[str] = frozenset(
71
+ {
72
+ "color", "colour", "css", "tailwind", "padding", "margin",
73
+ "spacing", "font", "typography", "responsive", "mobile",
74
+ "dark mode", "light mode", "theme", "shadow", "border",
75
+ "rounded", "radius",
76
+ },
77
+ )
78
+
79
+ _BACKEND_SIGNALS: frozenset[str] = frozenset(
80
+ {
81
+ "endpoint", "api", "route", "controller", "service",
82
+ "migration", "schema", "table", "column", "index", "query",
83
+ "queue", "job", "worker", "webhook", "policy", "gate",
84
+ "command", "cron", "broadcast", "event", "listener",
85
+ },
86
+ )
87
+
88
+ _TRIVIAL_VERBS: frozenset[str] = frozenset(
89
+ {
90
+ "rename", "relabel", "tweak", "adjust", "swap", "change",
91
+ },
92
+ )
93
+
94
+ _IMPROVE_VERBS: frozenset[str] = frozenset(
95
+ {
96
+ "improve", "polish", "redesign", "rework", "refine",
97
+ "refactor", "tighten", "clean", "fix", "update", "tune",
98
+ },
99
+ )
100
+
101
+ _BUILD_VERBS: frozenset[str] = frozenset(
102
+ {
103
+ "add", "build", "create", "introduce", "implement", "ship",
104
+ "draft", "scaffold", "wire",
105
+ },
106
+ )
107
+
108
+ _NEW_SURFACE: re.Pattern[str] = re.compile(
109
+ r"\b(new|fresh|blank)\s+(page|screen|view|component|form|modal|tile|dashboard)\b",
110
+ )
111
+
112
+ _EXISTING_SURFACE: re.Pattern[str] = re.compile(
113
+ r"\b(existing|current|the)\s+(page|screen|view|component|form|modal)\b",
114
+ )
115
+
116
+ _TRIVIAL_PATTERN: re.Pattern[str] = re.compile(
117
+ r"\b(make|change|update|set|swap)\b[^.]{0,40}\b("
118
+ r"red|blue|green|yellow|black|white|primary|secondary"
119
+ r"|color|colour|copy|text|label|wording|class|prop)\b",
120
+ )
121
+
122
+
123
+ def classify_intent(raw: str, *, title: str | None = None) -> str:
124
+ """Return one of :data:`KNOWN_INTENTS` for the supplied text.
125
+
126
+ Parameters
127
+ ----------
128
+ raw:
129
+ The user prompt or ticket body. Whitespace is normalised
130
+ internally; ``""`` and ``None`` resolve to ``backend-coding``.
131
+ title:
132
+ Optional ticket title. Concatenated with ``raw`` before
133
+ scanning so single-line ticket headlines (`"Add CSV export"`)
134
+ produce the same label whether they arrive in the body or the
135
+ title slot.
136
+ """
137
+ text = " ".join(filter(None, (title, raw))).strip().lower()
138
+ if not text:
139
+ return INTENT_BACKEND
140
+
141
+ has_ui = _has_ui_signal(text)
142
+ has_backend = _has_backend_signal(text)
143
+
144
+ if has_ui and _is_trivial(text):
145
+ return INTENT_UI_TRIVIAL
146
+ if has_ui and has_backend:
147
+ return INTENT_MIXED
148
+ if has_ui and _is_improve(text):
149
+ return INTENT_UI_IMPROVE
150
+ if has_ui and _is_build(text):
151
+ return INTENT_UI_BUILD
152
+ if has_ui:
153
+ # UI signal but no clear verb — default to ui-improve so the
154
+ # full audit gate engages. ui-build would skip the existing-
155
+ # surface check, which is the wrong default when the prompt
156
+ # is ambiguous.
157
+ return INTENT_UI_IMPROVE
158
+ return INTENT_BACKEND
159
+
160
+
161
+ def directive_set_for(intent: str) -> str:
162
+ """Map an intent label to a directive-set name.
163
+
164
+ Centralised here so the dispatcher and the refine step share one
165
+ routing table; a future intent (``infra``, ``security-review``)
166
+ only needs a single edit. Unknown labels raise ``ValueError`` —
167
+ silently falling back to ``backend`` would mask classifier bugs.
168
+ """
169
+ if intent not in KNOWN_INTENTS:
170
+ raise ValueError(
171
+ f"unknown intent {intent!r}; "
172
+ f"expected one of {sorted(KNOWN_INTENTS)}",
173
+ )
174
+ if intent in (INTENT_UI_BUILD, INTENT_UI_IMPROVE):
175
+ return "ui"
176
+ if intent == INTENT_UI_TRIVIAL:
177
+ return "ui-trivial"
178
+ if intent == INTENT_MIXED:
179
+ return "mixed"
180
+ return "backend"
181
+
182
+
183
+ # --- helpers ----------------------------------------------------------
184
+
185
+ def _has_ui_signal(text: str) -> bool:
186
+ if any(re.search(rf"\b{re.escape(w)}\b", text) for w in _UI_NOUNS):
187
+ return True
188
+ return any(s in text for s in _UI_STYLE)
189
+
190
+
191
+ def _has_backend_signal(text: str) -> bool:
192
+ return any(re.search(rf"\b{re.escape(w)}\b", text) for w in _BACKEND_SIGNALS)
193
+
194
+
195
+ def _is_trivial(text: str) -> bool:
196
+ if _TRIVIAL_PATTERN.search(text):
197
+ return True
198
+ return any(re.search(rf"\b{re.escape(v)}\b", text) for v in _TRIVIAL_VERBS) and len(
199
+ text.split()
200
+ ) <= 14
201
+
202
+
203
+ def _is_improve(text: str) -> bool:
204
+ if _EXISTING_SURFACE.search(text):
205
+ return True
206
+ return any(re.search(rf"\b{re.escape(v)}\b", text) for v in _IMPROVE_VERBS)
207
+
208
+
209
+ def _is_build(text: str) -> bool:
210
+ if _NEW_SURFACE.search(text):
211
+ return True
212
+ return any(re.search(rf"\b{re.escape(v)}\b", text) for v in _BUILD_VERBS)
213
+
214
+
215
+ def populate_routing(state: "WorkState") -> None:
216
+ """Classify ``state.input`` and write ``intent`` + ``directive_set`` in place.
217
+
218
+ Idempotent and override-safe: if ``state.intent`` is already a
219
+ UI-track or mixed label (``ui-build``, ``ui-improve``, ``ui-trivial``,
220
+ ``mixed``), the routing is left untouched. Only freshly-built states
221
+ carrying the construction default ``backend-coding`` are reclassified.
222
+ Loaded state files round-trip without losing a previously-recorded
223
+ intent — including a manual user override in the JSON.
224
+
225
+ The text fed to the classifier depends on the input envelope:
226
+
227
+ - ``prompt`` → ``state.input.data["raw"]``
228
+ - ``ticket`` → ``state.input.data["title"]`` + first non-empty
229
+ acceptance criterion, falling back to ``description`` when AC is
230
+ missing. Title is passed separately so single-line ticket
231
+ headlines (``"Add CSV export"``) classify identically whether
232
+ they arrive in the body or the title slot.
233
+ - ``diff`` / ``file`` → routed directly to ``ui-improve`` without
234
+ running the heuristic. Both envelopes are R3 Phase 1 inputs that
235
+ describe an existing UI surface ("improve this screen"); the
236
+ classifier's prose-oriented signals do not apply, and the audit +
237
+ design directives downstream are the right place to read the
238
+ diff/file contents.
239
+ """
240
+ if state.intent != INTENT_BACKEND:
241
+ return
242
+
243
+ if state.input.kind in {"diff", "file"}:
244
+ state.intent = INTENT_UI_IMPROVE
245
+ state.directive_set = directive_set_for(INTENT_UI_IMPROVE)
246
+ return
247
+
248
+ text, title = _extract_text(state)
249
+ intent = classify_intent(text, title=title)
250
+ state.intent = intent
251
+ state.directive_set = directive_set_for(intent)
252
+
253
+
254
+ def _extract_text(state: "WorkState") -> tuple[str, str | None]:
255
+ data = state.input.data or {}
256
+ if state.input.kind == "prompt":
257
+ return str(data.get("raw") or ""), None
258
+ title = data.get("title")
259
+ title_str = str(title) if isinstance(title, str) and title.strip() else None
260
+ body_parts: list[str] = []
261
+ ac = data.get("acceptance_criteria")
262
+ if isinstance(ac, list):
263
+ body_parts.extend(str(item) for item in ac if isinstance(item, str))
264
+ description = data.get("description")
265
+ if isinstance(description, str) and description.strip():
266
+ body_parts.append(description)
267
+ return " ".join(body_parts), title_str
268
+
269
+
270
+ __all__ = [
271
+ "INTENT_BACKEND",
272
+ "INTENT_MIXED",
273
+ "INTENT_UI_BUILD",
274
+ "INTENT_UI_IMPROVE",
275
+ "INTENT_UI_TRIVIAL",
276
+ "KNOWN_INTENTS",
277
+ "classify_intent",
278
+ "directive_set_for",
279
+ "populate_routing",
280
+ ]
@@ -0,0 +1,8 @@
1
+ """State-file migrations for the universal engine.
2
+
3
+ Each module in this package owns one direction of one schema bump. The
4
+ historical bumps must remain runnable so a freshly-cloned repository
5
+ that finds a v0 state file from a long-running branch can catch up
6
+ without manual intervention.
7
+ """
8
+ from __future__ import annotations
@@ -0,0 +1,231 @@
1
+ """Migrate a v0 ``DeliveryState`` JSON file to the v1 schema.
2
+
3
+ The v0 era used ``.implement-ticket-state.json`` and stored the ticket
4
+ under a flat ``ticket`` key. v1 wraps the payload under ``input.kind``
5
+ / ``input.data`` and adds ``intent``, ``directive_set``, and
6
+ ``version``. The default destination is ``.work-state.json`` next to
7
+ the v0 file; the v0 file is renamed to ``.implement-ticket-state.json.bak``
8
+ to preserve the rollback surface.
9
+
10
+ The module is both importable and runnable:
11
+
12
+ python3 -m work_engine.migration.v0_to_v1 .implement-ticket-state.json
13
+
14
+ Idempotency: ``migrate_payload`` accepts a payload that already looks
15
+ like v1 and returns it unchanged. ``migrate_file`` refuses to migrate
16
+ twice — if the destination already exists it raises rather than
17
+ silently overwriting work.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import shutil
24
+ import sys
25
+ from pathlib import Path
26
+ from typing import Any, Sequence
27
+
28
+ from ..state import (
29
+ DEFAULT_DIRECTIVE_SET,
30
+ DEFAULT_INTENT,
31
+ SCHEMA_VERSION,
32
+ SchemaError,
33
+ )
34
+
35
+ DEFAULT_V0_FILENAME = ".implement-ticket-state.json"
36
+ """Path the dispatcher used while the engine still lived under
37
+ ``implement_ticket``. The migration looks here when no source path is
38
+ passed on the CLI."""
39
+
40
+ DEFAULT_V1_FILENAME = ".work-state.json"
41
+ """Canonical filename for the v1 wire format."""
42
+
43
+ BACKUP_SUFFIX = ".bak"
44
+ """Appended to the v0 source path when the migration archives it.
45
+
46
+ If the ``.bak`` slot is already taken (re-running the migration after
47
+ an aborted run, manual rollback, etc.) the rotator falls back to
48
+ ``.bak.1``, ``.bak.2``, ... — see :func:`_rotate_backup_path`. The
49
+ migration never silently overwrites an existing backup."""
50
+
51
+ _MAX_BACKUP_ROTATIONS = 999
52
+ """Hard ceiling on rotated backup filenames; surfaces an explicit
53
+ :class:`SchemaError` instead of looping forever if a checkout has
54
+ hundreds of stale backups."""
55
+
56
+
57
+ def _rotate_backup_path(source: Path) -> Path:
58
+ """Return the next free ``.bak`` slot for ``source``.
59
+
60
+ Tries ``source.bak`` first, then ``source.bak.1``,
61
+ ``source.bak.2``, ... up to :data:`_MAX_BACKUP_ROTATIONS`. The
62
+ rotator only inspects existence — collision-safe by construction —
63
+ and never deletes or overwrites prior backups.
64
+ """
65
+ primary = source.with_suffix(source.suffix + BACKUP_SUFFIX)
66
+ if not primary.exists():
67
+ return primary
68
+ for index in range(1, _MAX_BACKUP_ROTATIONS + 1):
69
+ candidate = primary.with_suffix(primary.suffix + f".{index}")
70
+ if not candidate.exists():
71
+ return candidate
72
+ raise SchemaError(
73
+ f"refusing to rotate backup for {source}: more than "
74
+ f"{_MAX_BACKUP_ROTATIONS} stale .bak files already exist; "
75
+ "clean them up before re-running the migration",
76
+ )
77
+
78
+
79
+ def migrate_payload(payload: Any) -> dict[str, Any]:
80
+ """Return the v1 form of ``payload``.
81
+
82
+ A payload that already declares ``version: 1`` is returned
83
+ unchanged (deep-copied via ``json.loads(json.dumps(...))`` so the
84
+ caller cannot accidentally mutate the input). Anything else is
85
+ treated as v0 and wrapped: ``ticket`` becomes ``input.data``,
86
+ ``input.kind`` is set to ``"ticket"``, and the engine defaults are
87
+ filled in.
88
+
89
+ Raises
90
+ ------
91
+ SchemaError
92
+ If the payload is not a dict, declares a higher version than
93
+ this migration knows about, or lacks a ``ticket`` key.
94
+ """
95
+ if not isinstance(payload, dict):
96
+ raise SchemaError(
97
+ f"v0 state must be a JSON object; got {type(payload).__name__}",
98
+ )
99
+
100
+ declared_version = payload.get("version")
101
+ if declared_version == SCHEMA_VERSION:
102
+ return json.loads(json.dumps(payload))
103
+ if declared_version is not None:
104
+ raise SchemaError(
105
+ f"cannot migrate from version {declared_version!r} to "
106
+ f"{SCHEMA_VERSION}; this script only handles v0 (no version key)",
107
+ )
108
+
109
+ if "ticket" not in payload:
110
+ raise SchemaError(
111
+ "v0 state must carry a 'ticket' key; got keys: "
112
+ f"{sorted(payload.keys())}",
113
+ )
114
+ ticket = payload["ticket"]
115
+ if not isinstance(ticket, dict):
116
+ raise SchemaError(
117
+ f"v0 state.ticket must be a JSON object; got {type(ticket).__name__}",
118
+ )
119
+
120
+ return {
121
+ "version": SCHEMA_VERSION,
122
+ "input": {"kind": "ticket", "data": ticket},
123
+ "intent": DEFAULT_INTENT,
124
+ "directive_set": DEFAULT_DIRECTIVE_SET,
125
+ "persona": payload.get("persona", "senior-engineer"),
126
+ "memory": list(payload.get("memory", [])),
127
+ "plan": payload.get("plan"),
128
+ "changes": list(payload.get("changes", [])),
129
+ "tests": payload.get("tests"),
130
+ "verify": payload.get("verify"),
131
+ "outcomes": dict(payload.get("outcomes", {})),
132
+ "questions": list(payload.get("questions", [])),
133
+ "report": payload.get("report", ""),
134
+ }
135
+
136
+
137
+ def migrate_file(
138
+ source: Path,
139
+ *,
140
+ destination: Path | None = None,
141
+ backup: bool = True,
142
+ ) -> Path:
143
+ """Migrate the v0 state file at ``source`` and write the v1 result.
144
+
145
+ ``destination`` defaults to :data:`DEFAULT_V1_FILENAME` next to
146
+ ``source``. When ``backup`` is true (the default) the original
147
+ file is renamed with :data:`BACKUP_SUFFIX` appended; when false,
148
+ the original is left untouched. The destination must not exist —
149
+ refusing to overwrite is the safety net against accidental
150
+ double-migration on CI.
151
+
152
+ Returns the destination path on success.
153
+ """
154
+ if not source.is_file():
155
+ raise SchemaError(f"v0 state file not found: {source}")
156
+
157
+ raw = source.read_text(encoding="utf-8")
158
+ try:
159
+ payload = json.loads(raw)
160
+ except json.JSONDecodeError as exc:
161
+ raise SchemaError(f"invalid JSON in {source}: {exc}") from exc
162
+
163
+ target = destination or source.with_name(DEFAULT_V1_FILENAME)
164
+ if target.exists():
165
+ raise SchemaError(
166
+ f"refusing to overwrite existing destination {target}; "
167
+ "delete or rename it first",
168
+ )
169
+
170
+ migrated = migrate_payload(payload)
171
+ target.parent.mkdir(parents=True, exist_ok=True)
172
+ target.write_text(
173
+ json.dumps(migrated, indent=2, ensure_ascii=False) + "\n",
174
+ encoding="utf-8",
175
+ )
176
+
177
+ if backup:
178
+ backup_path = _rotate_backup_path(source)
179
+ shutil.move(str(source), str(backup_path))
180
+
181
+ return target
182
+
183
+
184
+ def main(argv: Sequence[str] | None = None) -> int:
185
+ """CLI entry point — ``python3 -m work_engine.migration.v0_to_v1``.
186
+
187
+ Exits ``0`` on success, ``2`` on any :class:`SchemaError` so the
188
+ invoking shell can branch on the failure category.
189
+ """
190
+ parser = argparse.ArgumentParser(
191
+ prog="work_engine.migration.v0_to_v1",
192
+ description="Migrate a legacy .implement-ticket-state.json file to "
193
+ "the v1 .work-state.json schema.",
194
+ )
195
+ parser.add_argument(
196
+ "source",
197
+ type=Path,
198
+ nargs="?",
199
+ default=Path(DEFAULT_V0_FILENAME),
200
+ help=f"Path to the v0 state file (default: {DEFAULT_V0_FILENAME}).",
201
+ )
202
+ parser.add_argument(
203
+ "--destination",
204
+ type=Path,
205
+ default=None,
206
+ help="Path to write the v1 file to "
207
+ f"(default: {DEFAULT_V1_FILENAME} next to source).",
208
+ )
209
+ parser.add_argument(
210
+ "--no-backup",
211
+ action="store_true",
212
+ help="Do not rename the v0 source to .bak after migration.",
213
+ )
214
+ args = parser.parse_args(argv)
215
+
216
+ try:
217
+ target = migrate_file(
218
+ args.source,
219
+ destination=args.destination,
220
+ backup=not args.no_backup,
221
+ )
222
+ except SchemaError as exc:
223
+ print(f"error: {exc}", file=sys.stderr)
224
+ return 2
225
+
226
+ print(f"migrated {args.source} → {target}")
227
+ return 0
228
+
229
+
230
+ if __name__ == "__main__":
231
+ sys.exit(main())
@@ -2,7 +2,7 @@
2
2
 
3
3
  Three personas ship today, each keyed by the string already carried
4
4
  on ``DeliveryState.persona`` (see
5
- ``agents/contexts/implement-ticket-flow.md#personas``):
5
+ ``docs/contracts/implement-ticket-flow.md#personas``):
6
6
 
7
7
  ``senior-engineer``
8
8
  Default. Runs every step. No test widening.
@@ -0,0 +1,22 @@
1
+ """Input resolvers — turn raw user-supplied payloads into envelopes.
2
+
3
+ A resolver wraps a typed source (a free-form prompt, a diff, a file
4
+ reference) into the canonical :class:`work_engine.state.Input` shape so
5
+ the dispatcher only ever speaks one schema. The R1 ticket flow does not
6
+ need a resolver — ticket payloads arrive pre-structured from
7
+ ``/implement-ticket``; R2 introduces :mod:`.prompt`; R3 Phase 1 adds
8
+ :mod:`.diff` and :mod:`.file` for the UI-improve track ("improve this
9
+ screen via diff/PR" and "improve this existing component/page").
10
+
11
+ Resolvers are deliberately thin: they normalize, they do not interpret.
12
+ Reconstruction of acceptance criteria + assumptions + confidence is the
13
+ job of the ``refine-prompt`` skill (R2 Phase 3) called from the
14
+ ``refine`` step, not the resolver. Keeping the split sharp means the
15
+ envelope shape stays cheap to round-trip through state and the heavy
16
+ lifting stays with the agent-directive halt where it belongs.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from . import diff, file, prompt
21
+
22
+ __all__ = ["diff", "file", "prompt"]
@@ -0,0 +1,106 @@
1
+ """Diff resolver — wrap a unified-diff payload as an :class:`Input` envelope.
2
+
3
+ The resolver is the R3 Phase 1 entry point for the "improve this screen via
4
+ diff/PR" surface: a user (or `/work` adapter) hands the engine a patch text,
5
+ the resolver normalises it, and the dispatcher routes it through the UI-track
6
+ ``ui-improve`` directive set.
7
+
8
+ Like :mod:`work_engine.resolvers.prompt`, this module is intentionally thin:
9
+ it normalises and rejects garbage payloads, nothing more. Reconstruction of
10
+ acceptance criteria + assumptions + confidence is the job of the
11
+ ``refine-prompt`` skill (R2 Phase 3) running against the diff once the engine
12
+ hits the ``refine`` step. Keeping the split sharp means the envelope shape
13
+ stays cheap to round-trip through state and the heavy lifting stays with the
14
+ agent-directive halt where it belongs.
15
+
16
+ The envelope mirrors the prompt resolver's shape so a single refiner code
17
+ path can read both — ``{raw, reconstructed_ac, assumptions}``. The only
18
+ material difference is the heuristic header check that rejects payloads
19
+ that obviously are not unified-diff text.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import re
24
+
25
+ from ..state import Input
26
+
27
+ KIND = "diff"
28
+ """Wire value carried in :attr:`work_engine.state.Input.kind`."""
29
+
30
+ _MIN_DIFF_LEN = 1
31
+ """Minimum non-whitespace character count before the heuristic runs.
32
+
33
+ The resolver is not a *quality* gate; it only rejects literally empty payloads
34
+ and obvious non-diffs. A semantically empty diff (e.g., headers but no hunks)
35
+ is still accepted so the refiner can score its tractability and surface a
36
+ ``low``-band halt where appropriate."""
37
+
38
+ _DIFF_MARKERS = (
39
+ re.compile(r"^diff --git ", re.MULTILINE),
40
+ re.compile(r"^--- ", re.MULTILINE),
41
+ re.compile(r"^\+\+\+ ", re.MULTILINE),
42
+ re.compile(r"^@@ ", re.MULTILINE),
43
+ re.compile(r"^Index: ", re.MULTILINE),
44
+ )
45
+ """Heuristic markers that flag a payload as a unified or git-style diff.
46
+
47
+ A payload qualifies if **any** marker matches — the resolver accepts unified
48
+ diffs (``--- ``/``+++ ``/``@@ ``), ``git diff`` output (``diff --git``), and
49
+ the legacy ``Index: `` SVN/CVS header. The match is anchored at line start so
50
+ quoted snippets inside prose ("the function `--- foo` failed") do not pass
51
+ the gate."""
52
+
53
+
54
+ class DiffResolverError(ValueError):
55
+ """Raised when a payload cannot be resolved into a diff envelope."""
56
+
57
+
58
+ def build_envelope(raw: str) -> Input:
59
+ """Return an :class:`Input` carrying the raw diff + empty refinement slots.
60
+
61
+ Parameters
62
+ ----------
63
+ raw:
64
+ The user-supplied diff text. Whitespace is preserved verbatim — the
65
+ refiner reads original spacing/casing when scoring goal clarity, and
66
+ a unified-diff round-trip cannot tolerate normalised whitespace.
67
+
68
+ Returns
69
+ -------
70
+ Input
71
+ Envelope of shape
72
+ ``{"kind": "diff", "data": {"raw": <raw>, "reconstructed_ac": [],
73
+ "assumptions": []}}``. The two empty lists are placeholders the
74
+ ``refine-prompt`` skill writes into on the rebound from ``refine``.
75
+
76
+ Raises
77
+ ------
78
+ DiffResolverError
79
+ If ``raw`` is not a string, contains no non-whitespace characters,
80
+ or does not match any :data:`_DIFF_MARKERS`. The marker check guards
81
+ against accidentally routing free-form prose through the diff path.
82
+ """
83
+ if not isinstance(raw, str):
84
+ raise DiffResolverError(
85
+ f"diff must be a string; got {type(raw).__name__}",
86
+ )
87
+ if len(raw.strip()) < _MIN_DIFF_LEN:
88
+ raise DiffResolverError(
89
+ "diff is empty or whitespace-only — nothing to resolve",
90
+ )
91
+ if not any(marker.search(raw) for marker in _DIFF_MARKERS):
92
+ raise DiffResolverError(
93
+ "payload does not look like a unified diff — expected one of "
94
+ "'diff --git', '--- ', '+++ ', '@@ ', or 'Index: ' headers",
95
+ )
96
+ return Input(
97
+ kind=KIND,
98
+ data={
99
+ "raw": raw,
100
+ "reconstructed_ac": [],
101
+ "assumptions": [],
102
+ },
103
+ )
104
+
105
+
106
+ __all__ = ["KIND", "DiffResolverError", "build_envelope"]