@event4u/agent-config 1.12.0 → 1.14.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 (260) hide show
  1. package/.agent-src/commands/agent-handoff.md +3 -0
  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 +5 -1
  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 +5 -0
  11. package/.agent-src/commands/chat-history-resume.md +5 -0
  12. package/.agent-src/commands/chat-history.md +5 -0
  13. package/.agent-src/commands/check-current-md.md +126 -0
  14. package/.agent-src/commands/commit-in-chunks.md +98 -0
  15. package/.agent-src/commands/commit.md +4 -0
  16. package/.agent-src/commands/compress.md +3 -0
  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 +3 -0
  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 +24 -0
  50. package/.agent-src/commands/optimize-agents.md +4 -0
  51. package/.agent-src/commands/optimize-augmentignore.md +3 -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 +4 -1
  64. package/.agent-src/commands/review-changes.md +4 -0
  65. package/.agent-src/commands/review-routing.md +4 -0
  66. package/.agent-src/commands/roadmap-create.md +7 -0
  67. package/.agent-src/commands/roadmap-execute.md +12 -1
  68. package/.agent-src/commands/rule-compliance-audit.md +4 -0
  69. package/.agent-src/commands/set-cost-profile.md +3 -0
  70. package/.agent-src/commands/sync-agent-settings.md +3 -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 +4 -0
  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 +8 -0
  89. package/.agent-src/rules/autonomous-execution.md +158 -0
  90. package/.agent-src/rules/chat-history.md +147 -118
  91. package/.agent-src/rules/cli-output-handling.md +26 -3
  92. package/.agent-src/rules/command-suggestion.md +133 -0
  93. package/.agent-src/rules/commit-policy.md +99 -0
  94. package/.agent-src/rules/direct-answers.md +114 -0
  95. package/.agent-src/rules/docs-sync.md +36 -0
  96. package/.agent-src/rules/downstream-changes.md +10 -9
  97. package/.agent-src/rules/improve-before-implement.md +9 -6
  98. package/.agent-src/rules/language-and-tone.md +81 -6
  99. package/.agent-src/rules/non-destructive-by-default.md +117 -0
  100. package/.agent-src/rules/package-ci-checks.md +4 -0
  101. package/.agent-src/rules/preservation-guard.md +20 -0
  102. package/.agent-src/rules/roadmap-progress-sync.md +103 -30
  103. package/.agent-src/rules/scope-control.md +42 -1
  104. package/.agent-src/rules/size-enforcement.md +1 -3
  105. package/.agent-src/rules/skill-quality.md +3 -8
  106. package/.agent-src/rules/ui-audit-before-build.md +106 -0
  107. package/.agent-src/rules/user-interaction.md +81 -3
  108. package/.agent-src/scripts/update_roadmap_progress.py +48 -6
  109. package/.agent-src/skills/blade-ui/SKILL.md +30 -5
  110. package/.agent-src/skills/command-routing/SKILL.md +32 -0
  111. package/.agent-src/skills/command-writing/SKILL.md +41 -2
  112. package/.agent-src/skills/description-assist/SKILL.md +21 -0
  113. package/.agent-src/skills/estimate-ticket/SKILL.md +0 -1
  114. package/.agent-src/skills/existing-ui-audit/SKILL.md +187 -0
  115. package/.agent-src/skills/fe-design/SKILL.md +72 -60
  116. package/.agent-src/skills/finishing-a-development-branch/SKILL.md +4 -0
  117. package/.agent-src/skills/flux/SKILL.md +31 -4
  118. package/.agent-src/skills/guideline-writing/SKILL.md +24 -2
  119. package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +51 -9
  120. package/.agent-src/skills/livewire/SKILL.md +30 -4
  121. package/.agent-src/skills/md-language-check/SKILL.md +103 -0
  122. package/.agent-src/skills/php-coder/SKILL.md +24 -0
  123. package/.agent-src/skills/react-shadcn-ui/SKILL.md +121 -0
  124. package/.agent-src/skills/refine-prompt/SKILL.md +220 -0
  125. package/.agent-src/skills/refine-ticket/SKILL.md +2 -4
  126. package/.agent-src/skills/roadmap-management/SKILL.md +10 -3
  127. package/.agent-src/skills/rule-writing/SKILL.md +23 -1
  128. package/.agent-src/skills/skill-writing/SKILL.md +1 -3
  129. package/.agent-src/skills/upstream-contribute/SKILL.md +1 -1
  130. package/.agent-src/skills/using-git-worktrees/SKILL.md +3 -1
  131. package/.agent-src/templates/AGENTS.md +24 -6
  132. package/.agent-src/templates/agent-settings.md +149 -0
  133. package/.agent-src/templates/github-workflows/roadmap-progress-check.yml +63 -0
  134. package/.agent-src/templates/hooks/pre-commit-roadmap-progress +60 -0
  135. package/.agent-src/templates/roadmaps.md +8 -2
  136. package/.agent-src/templates/scripts/implement_ticket/__init__.py +63 -26
  137. package/.agent-src/templates/scripts/implement_ticket/__main__.py +8 -2
  138. package/.agent-src/templates/scripts/memory_lookup.py +382 -21
  139. package/.agent-src/templates/scripts/memory_status.py +110 -9
  140. package/.agent-src/templates/scripts/telemetry/__init__.py +42 -0
  141. package/.agent-src/templates/scripts/telemetry/aggregator.py +154 -0
  142. package/.agent-src/templates/scripts/telemetry/boundary.py +171 -0
  143. package/.agent-src/templates/scripts/telemetry/engagement.py +238 -0
  144. package/.agent-src/templates/scripts/telemetry/report_renderer.py +170 -0
  145. package/.agent-src/templates/scripts/telemetry/settings.py +112 -0
  146. package/.agent-src/templates/scripts/telemetry_record.py +166 -0
  147. package/.agent-src/templates/scripts/telemetry_report.py +161 -0
  148. package/.agent-src/templates/scripts/telemetry_status.py +142 -0
  149. package/.agent-src/templates/scripts/work_engine/__init__.py +58 -0
  150. package/.agent-src/templates/scripts/work_engine/__main__.py +9 -0
  151. package/.agent-src/templates/scripts/work_engine/cli.py +592 -0
  152. package/.agent-src/templates/scripts/{implement_ticket → work_engine}/delivery_state.py +7 -0
  153. package/.agent-src/templates/scripts/work_engine/directives/__init__.py +33 -0
  154. package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +98 -0
  155. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/analyze.py +1 -1
  156. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/implement.py +2 -2
  157. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/memory.py +1 -1
  158. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/plan.py +1 -1
  159. package/.agent-src/templates/scripts/work_engine/directives/backend/refine.py +396 -0
  160. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/report.py +36 -4
  161. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/test.py +2 -2
  162. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/verify.py +2 -2
  163. package/.agent-src/templates/scripts/work_engine/directives/mixed/__init__.py +116 -0
  164. package/.agent-src/templates/scripts/work_engine/directives/mixed/contract.py +254 -0
  165. package/.agent-src/templates/scripts/work_engine/directives/mixed/stitch.py +229 -0
  166. package/.agent-src/templates/scripts/work_engine/directives/mixed/ui.py +231 -0
  167. package/.agent-src/templates/scripts/work_engine/directives/ui/__init__.py +113 -0
  168. package/.agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +44 -0
  169. package/.agent-src/templates/scripts/work_engine/directives/ui/apply.py +241 -0
  170. package/.agent-src/templates/scripts/work_engine/directives/ui/audit.py +414 -0
  171. package/.agent-src/templates/scripts/work_engine/directives/ui/design.py +335 -0
  172. package/.agent-src/templates/scripts/work_engine/directives/ui/polish.py +510 -0
  173. package/.agent-src/templates/scripts/work_engine/directives/ui/review.py +468 -0
  174. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/__init__.py +119 -0
  175. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.py +37 -0
  176. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.py +165 -0
  177. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.py +66 -0
  178. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/report.py +62 -0
  179. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/test.py +115 -0
  180. package/.agent-src/templates/scripts/work_engine/dispatcher.py +331 -0
  181. package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +54 -0
  182. package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +32 -0
  183. package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +103 -0
  184. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +44 -0
  185. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +42 -0
  186. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +50 -0
  187. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +49 -0
  188. package/.agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.py +53 -0
  189. package/.agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.py +50 -0
  190. package/.agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.py +52 -0
  191. package/.agent-src/templates/scripts/work_engine/hooks/builtin/trace.py +84 -0
  192. package/.agent-src/templates/scripts/work_engine/hooks/context.py +66 -0
  193. package/.agent-src/templates/scripts/work_engine/hooks/events.py +44 -0
  194. package/.agent-src/templates/scripts/work_engine/hooks/exceptions.py +79 -0
  195. package/.agent-src/templates/scripts/work_engine/hooks/registry.py +60 -0
  196. package/.agent-src/templates/scripts/work_engine/hooks/runner.py +73 -0
  197. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +141 -0
  198. package/.agent-src/templates/scripts/work_engine/intent/__init__.py +47 -0
  199. package/.agent-src/templates/scripts/work_engine/intent/classify.py +280 -0
  200. package/.agent-src/templates/scripts/work_engine/migration/__init__.py +8 -0
  201. package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +199 -0
  202. package/.agent-src/templates/scripts/work_engine/resolvers/__init__.py +22 -0
  203. package/.agent-src/templates/scripts/work_engine/resolvers/diff.py +106 -0
  204. package/.agent-src/templates/scripts/work_engine/resolvers/file.py +113 -0
  205. package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +90 -0
  206. package/.agent-src/templates/scripts/work_engine/scoring/__init__.py +14 -0
  207. package/.agent-src/templates/scripts/work_engine/scoring/confidence.py +300 -0
  208. package/.agent-src/templates/scripts/work_engine/stack/__init__.py +31 -0
  209. package/.agent-src/templates/scripts/work_engine/stack/detect.py +187 -0
  210. package/.agent-src/templates/scripts/work_engine/state.py +641 -0
  211. package/.claude-plugin/marketplace.json +105 -2
  212. package/AGENTS.md +36 -8
  213. package/CHANGELOG.md +558 -0
  214. package/README.md +146 -4
  215. package/composer.json +3 -0
  216. package/config/agent-settings.template.yml +45 -0
  217. package/config/gitignore-block.txt +4 -0
  218. package/docs/architecture.md +28 -1
  219. package/docs/development.md +1 -1
  220. package/docs/getting-started.md +3 -2
  221. package/docs/installation.md +86 -0
  222. package/docs/showcase.md +204 -0
  223. package/package.json +9 -1
  224. package/scripts/agent-config +274 -0
  225. package/scripts/audit_cloud_compatibility.py +288 -0
  226. package/scripts/build_cloud_bundle.py +458 -0
  227. package/scripts/build_linear_digest.py +263 -0
  228. package/scripts/chat_history.py +796 -7
  229. package/scripts/check_compression.py +139 -0
  230. package/scripts/check_iron_law_prominence.py +143 -0
  231. package/scripts/check_md_language.py +159 -0
  232. package/scripts/check_portability.py +36 -0
  233. package/scripts/check_reply_consistency.py +140 -0
  234. package/scripts/command_suggester/__init__.py +51 -0
  235. package/scripts/command_suggester/cooldown.py +132 -0
  236. package/scripts/command_suggester/loader.py +70 -0
  237. package/scripts/command_suggester/match.py +180 -0
  238. package/scripts/command_suggester/rank.py +120 -0
  239. package/scripts/command_suggester/render.py +86 -0
  240. package/scripts/command_suggester/sanitize.py +113 -0
  241. package/scripts/command_suggester/settings.py +125 -0
  242. package/scripts/command_suggester/types.py +78 -0
  243. package/scripts/hooks/augment-chat-history.sh +56 -0
  244. package/scripts/install-hooks.sh +67 -0
  245. package/scripts/install.py +150 -33
  246. package/scripts/lint_marketplace.py +27 -0
  247. package/scripts/memory_lookup.py +143 -7
  248. package/scripts/memory_status.py +76 -14
  249. package/scripts/migrate_command_suggestions.py +151 -0
  250. package/scripts/postinstall.sh +16 -0
  251. package/scripts/schemas/command.schema.json +41 -0
  252. package/scripts/skill_linter.py +67 -0
  253. package/scripts/sync_agent_settings.py +42 -12
  254. package/templates/consumer-settings/augment-cli-hooks.json +54 -0
  255. package/templates/consumer-settings/claude-settings.json +55 -1
  256. package/.agent-src/templates/scripts/implement_ticket/cli.py +0 -171
  257. package/.agent-src/templates/scripts/implement_ticket/dispatcher.py +0 -134
  258. package/.agent-src/templates/scripts/implement_ticket/steps/__init__.py +0 -49
  259. package/.agent-src/templates/scripts/implement_ticket/steps/refine.py +0 -140
  260. /package/.agent-src/templates/scripts/{implement_ticket → work_engine}/persona_policy.py +0 -0
@@ -0,0 +1,510 @@
1
+ """``polish`` step — bounded fix loop for review findings.
2
+
3
+ Phase 3 Step 5 of ``agents/roadmaps/road-to-product-ui-track.md``: the
4
+ polish step drives the fix loop after ``review`` produces findings.
5
+ It is the **only** step that re-enters review during a single
6
+ ``/work`` run, and the loop is hard-capped at two rounds — anything
7
+ the agent cannot fix in two passes goes back to the user as a
8
+ ship-as-is / abort decision rather than burning rounds silently.
9
+
10
+ Routes on ``state.ui_review`` and ``state.ui_polish``:
11
+
12
+ - **review_clean is True (or findings empty)** — nothing to polish;
13
+ return ``SUCCESS`` so the dispatcher advances to ``report``.
14
+ - **review_clean is False, rounds < 2** — emit
15
+ ``@agent-directive: ui-polish-<stack>``. The agent applies the
16
+ fixes from ``state.ui_review.findings``, re-runs the review skill,
17
+ and increments ``state.ui_polish.rounds``. On rebound, the
18
+ dispatcher walks back here; if the new review is clean we succeed,
19
+ otherwise the next round fires.
20
+ - **review_clean is False, rounds == 2** — ceiling reached. Halt
21
+ with three numbered options (ship as-is / abort / hand off) so the
22
+ user breaks the deadlock instead of the engine looping forever.
23
+
24
+ Idempotent: when the review is clean the step round-trips through
25
+ ``SUCCESS`` regardless of how many rounds ran. Schema-level validation
26
+ in :mod:`work_engine.state` already rejects ``rounds > 2`` on disk,
27
+ so the ceiling check here is the runtime mirror of that contract.
28
+ """
29
+ from __future__ import annotations
30
+
31
+ from typing import Any
32
+
33
+ from ...delivery_state import (
34
+ DeliveryState,
35
+ Outcome,
36
+ StepResult,
37
+ agent_directive,
38
+ )
39
+
40
+ POLISH_CEILING = 2
41
+ """Maximum number of polish rounds per ``/work`` run.
42
+
43
+ Mirrored by :func:`work_engine.state._validate_ui_polish` (rejects
44
+ ``rounds > 2`` at schema load when ``extension_used`` is ``False``)
45
+ so the contract holds across in-memory state, on-disk state, and the
46
+ dispatcher. R4 Phase 2 adds a one-shot extension that lifts the
47
+ runtime cap to ``POLISH_CEILING + 1`` when
48
+ ``state.ui_polish.extension_used`` is ``True``.
49
+ """
50
+
51
+ A11Y_VIOLATION_KIND = "a11y_violation"
52
+ """Marker on a review finding that flags an axe-core (or equivalent)
53
+ accessibility issue. Synthesised by
54
+ :func:`work_engine.directives.ui.review._synthesize_a11y_findings`
55
+ when actionable violations remain after baseline / accepted /
56
+ severity-floor filtering. Polish treats them as ordinary findings
57
+ during rounds 1..N but isolates them at the ceiling so the user
58
+ gets the dedicated ``polish_a11y_blocking`` halt instead of the
59
+ subjective ``polish_ceiling_reached`` halt.
60
+ """
61
+
62
+ TOKEN_VIOLATION_KIND = "token_violation"
63
+ """Marker on a review finding that flags a hardcoded design value.
64
+
65
+ Findings with ``kind == TOKEN_VIOLATION_KIND`` carry ``category``
66
+ (``"colors"`` / ``"spacing"`` / ``"typography"`` / …) and ``value``
67
+ (the literal hardcoded string). Polish classifies them into matched
68
+ (value already present in ``state.ui_audit.design_tokens``) and
69
+ unmatched, then reacts per :data:`TOKEN_REPEAT_THRESHOLD`.
70
+ """
71
+
72
+ TOKEN_REPEAT_THRESHOLD = 2
73
+ """Number of repeats above which an unmatched value triggers the
74
+ extraction halt — mirrors the roadmap's ">2 times" wording in
75
+ ``agents/roadmaps/road-to-product-ui-track.md`` Phase 3 Step 5.
76
+ """
77
+
78
+ STACK_DIRECTIVES: dict[str, str] = {
79
+ "blade-livewire-flux": "ui-polish-blade-livewire-flux",
80
+ "react-shadcn": "ui-polish-react-shadcn",
81
+ "vue": "ui-polish-vue",
82
+ "plain": "ui-polish-plain",
83
+ }
84
+ """Map ``state.stack.frontend`` → agent-directive skill name.
85
+
86
+ Mirrors :data:`work_engine.directives.ui.review.STACK_DIRECTIVES`.
87
+ An unknown stack falls through to ``ui-polish-plain``.
88
+ """
89
+
90
+ DEFAULT_DIRECTIVE = "ui-polish-plain"
91
+ """Fallback directive when ``state.stack`` is missing or malformed."""
92
+
93
+ AMBIGUITIES: tuple[dict[str, str], ...] = (
94
+ {
95
+ "code": "polish_round_pending",
96
+ "trigger": "state.ui_review.review_clean is False and "
97
+ "state.ui_polish.rounds < 2 — fixes have not yet been applied "
98
+ "for the current findings",
99
+ "resolution": "agent directive `ui-polish-<stack>` → skill "
100
+ "applies fixes, re-runs the review, increments "
101
+ "state.ui_polish.rounds",
102
+ },
103
+ {
104
+ "code": "polish_ceiling_reached",
105
+ "trigger": "state.ui_polish.rounds == ceiling and remaining "
106
+ "findings are non-a11y (subjective polish that did not "
107
+ "converge) — a11y blocks take precedence via "
108
+ "polish_a11y_blocking",
109
+ "resolution": "user picks: ship as-is, abort, or hand off to "
110
+ "manual fix; engine refuses to start another round",
111
+ },
112
+ {
113
+ "code": "polish_a11y_blocking",
114
+ "trigger": "state.ui_polish.rounds == ceiling and "
115
+ "state.ui_review.findings still contains a11y_violation "
116
+ "entries — objective gate that takes precedence over the "
117
+ "subjective polish_ceiling_reached halt",
118
+ "resolution": "user picks: extend by one round (engine sets "
119
+ "state.ui_polish.extension_used=True so the next round can "
120
+ "fire), accept-with-known-violations (engine appends the "
121
+ "leftover violations to state.ui_review.a11y.accepted_violations "
122
+ "so the review gate stops blocking on them), or abort the "
123
+ "UI request",
124
+ },
125
+ {
126
+ "code": "polish_token_extraction_pending",
127
+ "trigger": "state.ui_review.findings has token_violation entries "
128
+ "whose value repeats >2 times and has no match in "
129
+ "state.ui_audit.design_tokens",
130
+ "resolution": "user picks: extract as a new token (agent adds "
131
+ "it to state.ui_audit.design_tokens.<category>), inline (agent "
132
+ "drops the token_violation findings before re-entering polish), "
133
+ "or abort the UI request",
134
+ },
135
+ )
136
+ """Declared ambiguity surfaces for this step."""
137
+
138
+
139
+ def run(state: DeliveryState) -> StepResult:
140
+ """Apply the polish-loop gate."""
141
+ review = state.ui_review or {}
142
+ findings = review.get("findings", [])
143
+ if not isinstance(findings, list):
144
+ findings = []
145
+ review_clean = bool(review.get("review_clean", False))
146
+
147
+ if review_clean or not findings:
148
+ return StepResult(outcome=Outcome.SUCCESS)
149
+
150
+ polish = state.ui_polish or {}
151
+ rounds = polish.get("rounds", 0)
152
+ if not isinstance(rounds, int) or isinstance(rounds, bool):
153
+ rounds = 0
154
+ extension_used = bool(polish.get("extension_used", False))
155
+ effective_ceiling = POLISH_CEILING + (1 if extension_used else 0)
156
+
157
+ if rounds >= effective_ceiling:
158
+ a11y_findings = _a11y_findings(findings)
159
+ if a11y_findings:
160
+ return _halt_a11y_blocking(
161
+ state,
162
+ a11y_findings=a11y_findings,
163
+ rounds=rounds,
164
+ extension_available=not extension_used,
165
+ )
166
+ return _halt_ceiling(
167
+ state,
168
+ findings_count=len(findings),
169
+ rounds=rounds,
170
+ ceiling=effective_ceiling,
171
+ )
172
+
173
+ tokens = _design_tokens(state)
174
+ matched, unmatched_repeats = _classify_token_violations(findings, tokens)
175
+ if unmatched_repeats:
176
+ return _halt_token_extraction(state, repeats=unmatched_repeats)
177
+
178
+ return _delegate_to_polish_skill(
179
+ state,
180
+ findings_count=len(findings),
181
+ matched_token_count=len(matched),
182
+ rounds=rounds,
183
+ ceiling=effective_ceiling,
184
+ )
185
+
186
+
187
+ def _resolve_directive(state: DeliveryState) -> str:
188
+ """Pick the agent directive for the project's frontend stack."""
189
+ stack = getattr(state, "stack", None) or {}
190
+ if isinstance(stack, dict):
191
+ frontend = stack.get("frontend")
192
+ if isinstance(frontend, str) and frontend in STACK_DIRECTIVES:
193
+ return STACK_DIRECTIVES[frontend]
194
+ return DEFAULT_DIRECTIVE
195
+
196
+
197
+ def _stack_label(state: DeliveryState) -> str:
198
+ """Return the frontend stack label, defaulting to ``plain``."""
199
+ stack = getattr(state, "stack", None) or {}
200
+ if isinstance(stack, dict):
201
+ frontend = stack.get("frontend")
202
+ if isinstance(frontend, str) and frontend:
203
+ return frontend
204
+ return "plain"
205
+
206
+
207
+ def _delegate_to_polish_skill(
208
+ state: DeliveryState,
209
+ *,
210
+ findings_count: int,
211
+ matched_token_count: int,
212
+ rounds: int,
213
+ ceiling: int,
214
+ ) -> StepResult:
215
+ """BLOCKED halt — emit the stack-specific polish directive.
216
+
217
+ The skill applies fixes from ``state.ui_review.findings``,
218
+ re-runs the review, and writes:
219
+
220
+ - new ``state.ui_review`` (refreshed findings + ``review_clean``)
221
+ - ``state.ui_polish.rounds = rounds + 1``
222
+ - ``state.ui_polish.applied`` extended with this round's fix log
223
+
224
+ ``matched_token_count`` reports how many findings are
225
+ ``token_violation`` entries whose value already lives in
226
+ ``state.ui_audit.design_tokens`` — those auto-convert in this
227
+ round without further user input. ``ceiling`` is the
228
+ extension-aware upper bound (``POLISH_CEILING`` or
229
+ ``POLISH_CEILING + 1`` after an a11y extension).
230
+ """
231
+ directive = _resolve_directive(state)
232
+ stack_label = _stack_label(state)
233
+ next_round = rounds + 1
234
+ findings_line = (
235
+ f"> {findings_count} finding(s) from `state.ui_review`. "
236
+ "Apply each fix, re-run the review, and write the refreshed "
237
+ "envelope back."
238
+ )
239
+ if matched_token_count:
240
+ findings_line = (
241
+ f"> {findings_count} finding(s) from `state.ui_review` "
242
+ f"({matched_token_count} token-violation match(es) "
243
+ "auto-convert against `state.ui_audit.design_tokens`). "
244
+ "Apply each fix, re-run the review, and write the refreshed "
245
+ "envelope back."
246
+ )
247
+ return StepResult(
248
+ outcome=Outcome.BLOCKED,
249
+ questions=[
250
+ agent_directive(directive),
251
+ f"> Stack: `{stack_label}`. Polish round "
252
+ f"{next_round} of {ceiling}.",
253
+ findings_line,
254
+ "> 1. Continue \u2014 apply fixes, re-review, and increment "
255
+ "`state.ui_polish.rounds`",
256
+ "> 2. Abort \u2014 drop this UI request",
257
+ ],
258
+ message=(
259
+ f"UI polish round {next_round}/{ceiling}; delegating "
260
+ f"to `{directive}` for stack `{stack_label}`."
261
+ ),
262
+ )
263
+
264
+
265
+ def _halt_ceiling(
266
+ state: DeliveryState,
267
+ *,
268
+ findings_count: int,
269
+ rounds: int,
270
+ ceiling: int,
271
+ ) -> StepResult:
272
+ """BLOCKED halt — ceiling reached on subjective (non-a11y) findings.
273
+
274
+ R4 Phase 2 narrows this halt: a11y findings are routed to
275
+ :func:`_halt_a11y_blocking` first, so this halt only fires when
276
+ every remaining finding is subjective polish that did not
277
+ converge.
278
+ """
279
+ stack_label = _stack_label(state)
280
+ return StepResult(
281
+ outcome=Outcome.BLOCKED,
282
+ questions=[
283
+ f"> Stack: `{stack_label}`. Polish ceiling reached "
284
+ f"({rounds}/{ceiling} rounds).",
285
+ f"> {findings_count} finding(s) still open in "
286
+ "`state.ui_review`. The engine refuses a third round.",
287
+ "> 1. Ship as-is \u2014 mark `state.ui_review.review_clean "
288
+ "= True` and continue to `report` (the open findings stay "
289
+ "in the delivery report)",
290
+ "> 2. Abort \u2014 drop this UI request",
291
+ "> 3. Hand off \u2014 a human picks up the remaining "
292
+ "findings outside the engine; re-invoke `/work` only after "
293
+ "they are resolved",
294
+ "",
295
+ "**Recommendation: 3 \u2014 Hand off** \u2014 two automated "
296
+ "rounds failed to converge. Caveat: pick 1 only when the "
297
+ "remaining findings are explicitly acceptable (low-priority "
298
+ "polish, deferred to a follow-up).",
299
+ ],
300
+ message=(
301
+ f"UI polish ceiling reached ({rounds}/{ceiling}); "
302
+ f"{findings_count} finding(s) still open."
303
+ ),
304
+ )
305
+
306
+
307
+ def _a11y_findings(findings: list[Any]) -> list[dict[str, Any]]:
308
+ """Return the subset of ``findings`` synthesised by the a11y gate."""
309
+ return [
310
+ f for f in findings
311
+ if isinstance(f, dict) and f.get("kind") == A11Y_VIOLATION_KIND
312
+ ]
313
+
314
+
315
+ def _halt_a11y_blocking(
316
+ state: DeliveryState,
317
+ *,
318
+ a11y_findings: list[dict[str, Any]],
319
+ rounds: int,
320
+ extension_available: bool,
321
+ ) -> StepResult:
322
+ """BLOCKED halt — ceiling reached with actionable a11y findings.
323
+
324
+ Takes precedence over :func:`_halt_ceiling`: the user gets three
325
+ options tailored to a11y (extend / accept / abort) instead of
326
+ the subjective ship-or-handoff trio. ``extension_available`` is
327
+ ``False`` once the one-shot extension was already used; in that
328
+ case option 1 disappears and only accept / abort remain.
329
+ """
330
+ stack_label = _stack_label(state)
331
+ count = len(a11y_findings)
332
+ questions: list[str] = [
333
+ f"> Stack: `{stack_label}`. Polish ceiling reached "
334
+ f"({rounds}/{POLISH_CEILING} rounds) with {count} a11y "
335
+ "violation(s) still open.",
336
+ ]
337
+ for finding in a11y_findings[:5]:
338
+ rule = finding.get("rule") or "?"
339
+ selector = finding.get("selector") or "?"
340
+ severity = finding.get("severity") or "?"
341
+ questions.append(
342
+ f"> - `{rule}` on `{selector}` (severity: {severity})"
343
+ )
344
+ if count > 5:
345
+ questions.append(f"> ... and {count - 5} more")
346
+ if extension_available:
347
+ questions.extend([
348
+ "> 1. Extend \u2014 grant one extra polish round; the "
349
+ "engine sets `state.ui_polish.extension_used = True` so "
350
+ "the next delegation can fire",
351
+ "> 2. Accept \u2014 append the open violations to "
352
+ "`state.ui_review.a11y.accepted_violations` so the review "
353
+ "gate stops blocking on them, then continue to `report`",
354
+ "> 3. Abort \u2014 drop this UI request",
355
+ "",
356
+ "**Recommendation: 1 \u2014 Extend** \u2014 a11y "
357
+ "violations are objective; one more round usually closes "
358
+ "the gap. Pick 2 only when the violations are explicitly "
359
+ "out of scope for this run.",
360
+ ])
361
+ else:
362
+ questions.extend([
363
+ "> 1. Accept \u2014 append the open violations to "
364
+ "`state.ui_review.a11y.accepted_violations` so the review "
365
+ "gate stops blocking on them, then continue to `report`",
366
+ "> 2. Abort \u2014 drop this UI request",
367
+ "",
368
+ "**Recommendation: 1 \u2014 Accept** \u2014 the one-shot "
369
+ "extension is already spent; either accept the residual "
370
+ "violations or abort.",
371
+ ])
372
+ return StepResult(
373
+ outcome=Outcome.BLOCKED,
374
+ questions=questions,
375
+ message=(
376
+ f"UI polish ceiling reached ({rounds}/{POLISH_CEILING}); "
377
+ f"{count} a11y violation(s) still open."
378
+ ),
379
+ )
380
+
381
+
382
+ def _design_tokens(state: DeliveryState) -> dict[str, Any]:
383
+ """Return ``state.ui_audit.design_tokens`` as a dict, or ``{}``.
384
+
385
+ Defensive: tolerates missing audit, non-dict audit, missing
386
+ ``design_tokens`` key, and non-dict ``design_tokens`` payload.
387
+ All four shapes degrade to "no tokens known" so the unmatched
388
+ classifier treats every value as an extraction candidate.
389
+ """
390
+ audit = getattr(state, "ui_audit", None) or {}
391
+ if not isinstance(audit, dict):
392
+ return {}
393
+ tokens = audit.get("design_tokens") or {}
394
+ if not isinstance(tokens, dict):
395
+ return {}
396
+ return tokens
397
+
398
+
399
+ def _classify_token_violations(
400
+ findings: list[Any],
401
+ tokens: dict[str, Any],
402
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
403
+ """Split ``token_violation`` findings into matched / unmatched-repeats.
404
+
405
+ A finding qualifies when ``kind == TOKEN_VIOLATION_KIND`` and it
406
+ carries string ``category`` + ``value``. Matched: ``value`` is
407
+ among the values of ``tokens[category]`` (the token bucket is a
408
+ name → literal mapping per ``state.ui_audit.design_tokens``
409
+ schema). Unmatched values are bucketed by ``(category, value)``;
410
+ only buckets with ``count > TOKEN_REPEAT_THRESHOLD`` are returned
411
+ so single-use hardcoded values do not trigger the halt.
412
+ """
413
+ matched: list[dict[str, Any]] = []
414
+ unmatched_counts: dict[tuple[str, str], int] = {}
415
+ for finding in findings:
416
+ if not isinstance(finding, dict):
417
+ continue
418
+ if finding.get("kind") != TOKEN_VIOLATION_KIND:
419
+ continue
420
+ category = finding.get("category")
421
+ value = finding.get("value")
422
+ if not isinstance(category, str) or not isinstance(value, str):
423
+ continue
424
+ bucket = tokens.get(category)
425
+ if isinstance(bucket, dict) and value in bucket.values():
426
+ matched.append(finding)
427
+ continue
428
+ key = (category, value)
429
+ unmatched_counts[key] = unmatched_counts.get(key, 0) + 1
430
+ repeats = [
431
+ {"category": cat, "value": val, "count": count}
432
+ for (cat, val), count in unmatched_counts.items()
433
+ if count > TOKEN_REPEAT_THRESHOLD
434
+ ]
435
+ return matched, repeats
436
+
437
+
438
+ def _suggest_token_name(category: str, value: str) -> str:
439
+ """Build a suggested CSS-custom-property name for an extraction halt.
440
+
441
+ Heuristic only — the agent picks the final name when applying
442
+ the extraction. Strips non-alphanumerics from ``value``, prefixes
443
+ with the singular of ``category`` (``colors`` → ``color``), and
444
+ caps the length so the halt body stays readable.
445
+ """
446
+ safe = "".join(c if c.isalnum() else "-" for c in value).strip("-").lower()
447
+ if not safe:
448
+ safe = "value"
449
+ base = category.rstrip("s") or category
450
+ return f"{base}-{safe}"[:40]
451
+
452
+
453
+ def _halt_token_extraction(
454
+ state: DeliveryState,
455
+ *,
456
+ repeats: list[dict[str, Any]],
457
+ ) -> StepResult:
458
+ """BLOCKED halt — repeated hardcoded value(s) without a matching token.
459
+
460
+ Polish refuses to silently inline the same hardcoded value across
461
+ multiple call sites; the user picks whether the value graduates
462
+ to a design token or stays inline for this run.
463
+ """
464
+ stack_label = _stack_label(state)
465
+ questions: list[str] = [
466
+ f"> Stack: `{stack_label}`. {len(repeats)} hardcoded value(s) "
467
+ f"appear >{TOKEN_REPEAT_THRESHOLD} times without a matching "
468
+ "entry in `state.ui_audit.design_tokens`.",
469
+ ]
470
+ for repeat in repeats:
471
+ suggested = _suggest_token_name(repeat["category"], repeat["value"])
472
+ questions.append(
473
+ f"> - `{repeat['value']}` "
474
+ f"({repeat['category']}, {repeat['count']}\u00d7) "
475
+ f"\u2014 suggested name: `--{suggested}`"
476
+ )
477
+ questions.extend([
478
+ "> 1. Extract as design token(s) \u2014 add to "
479
+ "`state.ui_audit.design_tokens.<category>` and re-enter polish; "
480
+ "matching findings auto-convert next round",
481
+ "> 2. Inline \u2014 keep the hardcoded value(s) for this run; "
482
+ "drop the token_violation findings from "
483
+ "`state.ui_review.findings` before re-entering polish",
484
+ "> 3. Abort \u2014 drop this UI request",
485
+ "",
486
+ "**Recommendation: 1 \u2014 Extract** \u2014 a value used "
487
+ f">{TOKEN_REPEAT_THRESHOLD} times is a de-facto token; "
488
+ "promoting it now keeps the design system honest. Pick 2 only "
489
+ "when the value is intentionally one-off.",
490
+ ])
491
+ return StepResult(
492
+ outcome=Outcome.BLOCKED,
493
+ questions=questions,
494
+ message=(
495
+ f"UI polish paused; {len(repeats)} hardcoded value(s) "
496
+ "repeat without a matching design token."
497
+ ),
498
+ )
499
+
500
+
501
+ __all__ = [
502
+ "A11Y_VIOLATION_KIND",
503
+ "AMBIGUITIES",
504
+ "DEFAULT_DIRECTIVE",
505
+ "POLISH_CEILING",
506
+ "STACK_DIRECTIVES",
507
+ "TOKEN_REPEAT_THRESHOLD",
508
+ "TOKEN_VIOLATION_KIND",
509
+ "run",
510
+ ]