@event4u/agent-config 1.13.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 (252) 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 +82 -50
  108. package/.agent-src/scripts/update_roadmap_progress.py +17 -5
  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/roadmaps.md +8 -2
  134. package/.agent-src/templates/scripts/implement_ticket/__init__.py +63 -26
  135. package/.agent-src/templates/scripts/implement_ticket/__main__.py +8 -2
  136. package/.agent-src/templates/scripts/telemetry/__init__.py +42 -0
  137. package/.agent-src/templates/scripts/telemetry/aggregator.py +154 -0
  138. package/.agent-src/templates/scripts/telemetry/boundary.py +171 -0
  139. package/.agent-src/templates/scripts/telemetry/engagement.py +238 -0
  140. package/.agent-src/templates/scripts/telemetry/report_renderer.py +170 -0
  141. package/.agent-src/templates/scripts/telemetry/settings.py +112 -0
  142. package/.agent-src/templates/scripts/telemetry_record.py +166 -0
  143. package/.agent-src/templates/scripts/telemetry_report.py +161 -0
  144. package/.agent-src/templates/scripts/telemetry_status.py +142 -0
  145. package/.agent-src/templates/scripts/work_engine/__init__.py +58 -0
  146. package/.agent-src/templates/scripts/work_engine/__main__.py +9 -0
  147. package/.agent-src/templates/scripts/work_engine/cli.py +592 -0
  148. package/.agent-src/templates/scripts/{implement_ticket → work_engine}/delivery_state.py +7 -0
  149. package/.agent-src/templates/scripts/work_engine/directives/__init__.py +33 -0
  150. package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +98 -0
  151. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/analyze.py +1 -1
  152. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/implement.py +2 -2
  153. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/memory.py +1 -1
  154. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/plan.py +1 -1
  155. package/.agent-src/templates/scripts/work_engine/directives/backend/refine.py +396 -0
  156. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/report.py +36 -4
  157. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/test.py +2 -2
  158. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/verify.py +2 -2
  159. package/.agent-src/templates/scripts/work_engine/directives/mixed/__init__.py +116 -0
  160. package/.agent-src/templates/scripts/work_engine/directives/mixed/contract.py +254 -0
  161. package/.agent-src/templates/scripts/work_engine/directives/mixed/stitch.py +229 -0
  162. package/.agent-src/templates/scripts/work_engine/directives/mixed/ui.py +231 -0
  163. package/.agent-src/templates/scripts/work_engine/directives/ui/__init__.py +113 -0
  164. package/.agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +44 -0
  165. package/.agent-src/templates/scripts/work_engine/directives/ui/apply.py +241 -0
  166. package/.agent-src/templates/scripts/work_engine/directives/ui/audit.py +414 -0
  167. package/.agent-src/templates/scripts/work_engine/directives/ui/design.py +335 -0
  168. package/.agent-src/templates/scripts/work_engine/directives/ui/polish.py +510 -0
  169. package/.agent-src/templates/scripts/work_engine/directives/ui/review.py +468 -0
  170. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/__init__.py +119 -0
  171. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.py +37 -0
  172. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.py +165 -0
  173. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.py +66 -0
  174. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/report.py +62 -0
  175. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/test.py +115 -0
  176. package/.agent-src/templates/scripts/work_engine/dispatcher.py +331 -0
  177. package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +54 -0
  178. package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +32 -0
  179. package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +103 -0
  180. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +44 -0
  181. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +42 -0
  182. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +50 -0
  183. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +49 -0
  184. package/.agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.py +53 -0
  185. package/.agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.py +50 -0
  186. package/.agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.py +52 -0
  187. package/.agent-src/templates/scripts/work_engine/hooks/builtin/trace.py +84 -0
  188. package/.agent-src/templates/scripts/work_engine/hooks/context.py +66 -0
  189. package/.agent-src/templates/scripts/work_engine/hooks/events.py +44 -0
  190. package/.agent-src/templates/scripts/work_engine/hooks/exceptions.py +79 -0
  191. package/.agent-src/templates/scripts/work_engine/hooks/registry.py +60 -0
  192. package/.agent-src/templates/scripts/work_engine/hooks/runner.py +73 -0
  193. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +141 -0
  194. package/.agent-src/templates/scripts/work_engine/intent/__init__.py +47 -0
  195. package/.agent-src/templates/scripts/work_engine/intent/classify.py +280 -0
  196. package/.agent-src/templates/scripts/work_engine/migration/__init__.py +8 -0
  197. package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +199 -0
  198. package/.agent-src/templates/scripts/work_engine/resolvers/__init__.py +22 -0
  199. package/.agent-src/templates/scripts/work_engine/resolvers/diff.py +106 -0
  200. package/.agent-src/templates/scripts/work_engine/resolvers/file.py +113 -0
  201. package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +90 -0
  202. package/.agent-src/templates/scripts/work_engine/scoring/__init__.py +14 -0
  203. package/.agent-src/templates/scripts/work_engine/scoring/confidence.py +300 -0
  204. package/.agent-src/templates/scripts/work_engine/stack/__init__.py +31 -0
  205. package/.agent-src/templates/scripts/work_engine/stack/detect.py +187 -0
  206. package/.agent-src/templates/scripts/work_engine/state.py +641 -0
  207. package/.claude-plugin/marketplace.json +105 -2
  208. package/AGENTS.md +36 -8
  209. package/CHANGELOG.md +534 -0
  210. package/README.md +125 -4
  211. package/config/agent-settings.template.yml +45 -0
  212. package/config/gitignore-block.txt +4 -0
  213. package/docs/architecture.md +28 -1
  214. package/docs/development.md +1 -1
  215. package/docs/getting-started.md +2 -2
  216. package/docs/installation.md +86 -0
  217. package/docs/showcase.md +204 -0
  218. package/package.json +1 -1
  219. package/scripts/agent-config +199 -0
  220. package/scripts/audit_cloud_compatibility.py +288 -0
  221. package/scripts/build_cloud_bundle.py +458 -0
  222. package/scripts/build_linear_digest.py +263 -0
  223. package/scripts/chat_history.py +796 -7
  224. package/scripts/check_compression.py +139 -0
  225. package/scripts/check_iron_law_prominence.py +143 -0
  226. package/scripts/check_md_language.py +159 -0
  227. package/scripts/check_portability.py +36 -0
  228. package/scripts/check_reply_consistency.py +140 -0
  229. package/scripts/command_suggester/__init__.py +51 -0
  230. package/scripts/command_suggester/cooldown.py +132 -0
  231. package/scripts/command_suggester/loader.py +70 -0
  232. package/scripts/command_suggester/match.py +180 -0
  233. package/scripts/command_suggester/rank.py +120 -0
  234. package/scripts/command_suggester/render.py +86 -0
  235. package/scripts/command_suggester/sanitize.py +113 -0
  236. package/scripts/command_suggester/settings.py +125 -0
  237. package/scripts/command_suggester/types.py +78 -0
  238. package/scripts/hooks/augment-chat-history.sh +56 -0
  239. package/scripts/install-hooks.sh +67 -0
  240. package/scripts/install.py +150 -33
  241. package/scripts/lint_marketplace.py +27 -0
  242. package/scripts/migrate_command_suggestions.py +151 -0
  243. package/scripts/schemas/command.schema.json +41 -0
  244. package/scripts/skill_linter.py +67 -0
  245. package/scripts/sync_agent_settings.py +42 -12
  246. package/templates/consumer-settings/augment-cli-hooks.json +54 -0
  247. package/templates/consumer-settings/claude-settings.json +55 -1
  248. package/.agent-src/templates/scripts/implement_ticket/cli.py +0 -171
  249. package/.agent-src/templates/scripts/implement_ticket/dispatcher.py +0 -134
  250. package/.agent-src/templates/scripts/implement_ticket/steps/__init__.py +0 -49
  251. package/.agent-src/templates/scripts/implement_ticket/steps/refine.py +0 -140
  252. /package/.agent-src/templates/scripts/{implement_ticket → work_engine}/persona_policy.py +0 -0
@@ -0,0 +1,141 @@
1
+ """Read ``hooks.*`` from ``.agent-settings.yml`` into :class:`HookSettings`.
2
+
3
+ Mirror of the chat-history settings pattern (``scripts/command_suggester/
4
+ settings.py``):
5
+
6
+ * Lazy PyYAML import — the engine works without yaml installed when no
7
+ settings file is present (test fixtures, cloud bundles).
8
+ * Default-permissive: a missing file or missing ``hooks:`` block returns
9
+ :class:`HookSettings` with ``enabled=False`` — every hook off, every
10
+ golden replay safe by construction.
11
+ * Malformed YAML / unreadable file → defaults; the engine degrades
12
+ silently rather than crashing the CLI.
13
+ * Chat-history hooks gate on **two** switches: ``hooks.chat_history.
14
+ enabled`` AND the global ``chat_history.enabled``. Either off → no
15
+ chat-history hook registers.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ DEFAULT_SETTINGS_FILE = ".agent-settings.yml"
24
+ DEFAULT_CHAT_HISTORY_SCRIPT = "scripts/chat_history.py"
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class HookSettings:
29
+ """Resolved view of the ``hooks:`` block.
30
+
31
+ ``enabled`` is the master switch. When ``False`` the registry stays
32
+ empty regardless of the per-hook fields; this is the default when no
33
+ settings file exists or no ``hooks`` block is declared, and it is
34
+ what keeps golden-replay tests byte-stable.
35
+ """
36
+
37
+ enabled: bool = False
38
+ trace: bool = False
39
+ halt_surface_audit: bool = False
40
+ state_shape_validation: bool = False
41
+ directive_set_guard: bool = False
42
+ chat_history_enabled: bool = False
43
+ chat_history_script: str = DEFAULT_CHAT_HISTORY_SCRIPT
44
+
45
+
46
+ _DEFAULT = HookSettings()
47
+
48
+
49
+ def load_hook_settings(
50
+ settings_path: Path | str | None = None,
51
+ ) -> HookSettings:
52
+ """Return :class:`HookSettings` hydrated from ``.agent-settings.yml``.
53
+
54
+ ``settings_path`` defaults to ``./.agent-settings.yml`` relative to
55
+ the current working directory — same convention as chat-history.
56
+ """
57
+ path = Path(settings_path) if settings_path else Path(DEFAULT_SETTINGS_FILE)
58
+ raw = _read_yaml(path)
59
+ if raw is None:
60
+ return _DEFAULT
61
+ return _settings_from_raw(raw)
62
+
63
+
64
+ def _read_yaml(path: Path) -> dict[str, Any] | None:
65
+ if not path.is_file():
66
+ return None
67
+ try:
68
+ import yaml # type: ignore[import-untyped]
69
+ except ImportError:
70
+ return None
71
+ try:
72
+ with path.open(encoding="utf-8") as fh:
73
+ data = yaml.safe_load(fh) or {}
74
+ except (OSError, yaml.YAMLError):
75
+ return None
76
+ if not isinstance(data, dict):
77
+ return None
78
+ return data
79
+
80
+
81
+ def _settings_from_raw(data: dict[str, Any]) -> HookSettings:
82
+ hooks = data.get("hooks")
83
+ if not isinstance(hooks, dict):
84
+ return _DEFAULT
85
+ enabled = _coerce_bool(hooks.get("enabled"), False)
86
+ if not enabled:
87
+ return HookSettings(enabled=False)
88
+
89
+ chat_section = hooks.get("chat_history")
90
+ if isinstance(chat_section, dict):
91
+ chat_block_enabled = _coerce_bool(chat_section.get("enabled"), True)
92
+ chat_script = str(
93
+ chat_section.get("script") or DEFAULT_CHAT_HISTORY_SCRIPT
94
+ )
95
+ else:
96
+ chat_block_enabled = True
97
+ chat_script = DEFAULT_CHAT_HISTORY_SCRIPT
98
+
99
+ global_chat = data.get("chat_history")
100
+ global_chat_on = (
101
+ isinstance(global_chat, dict)
102
+ and _coerce_bool(global_chat.get("enabled"), False)
103
+ )
104
+
105
+ return HookSettings(
106
+ enabled=True,
107
+ trace=_coerce_bool(hooks.get("trace"), False),
108
+ halt_surface_audit=_coerce_bool(
109
+ hooks.get("halt_surface_audit"), True
110
+ ),
111
+ state_shape_validation=_coerce_bool(
112
+ hooks.get("state_shape_validation"), True
113
+ ),
114
+ directive_set_guard=_coerce_bool(
115
+ hooks.get("directive_set_guard"), True
116
+ ),
117
+ chat_history_enabled=chat_block_enabled and global_chat_on,
118
+ chat_history_script=chat_script,
119
+ )
120
+
121
+
122
+ def _coerce_bool(value: Any, default: bool) -> bool:
123
+ if isinstance(value, bool):
124
+ return value
125
+ if value is None:
126
+ return default
127
+ if isinstance(value, str):
128
+ s = value.strip().lower()
129
+ if s in ("true", "yes", "on", "1"):
130
+ return True
131
+ if s in ("false", "no", "off", "0"):
132
+ return False
133
+ return default
134
+
135
+
136
+ __all__ = [
137
+ "DEFAULT_CHAT_HISTORY_SCRIPT",
138
+ "DEFAULT_SETTINGS_FILE",
139
+ "HookSettings",
140
+ "load_hook_settings",
141
+ ]
@@ -0,0 +1,47 @@
1
+ """Intent classification for the universal engine (R3 Phase 1 Step 2).
2
+
3
+ The :mod:`work_engine.intent.classify` module turns a raw user prompt
4
+ (or a ticket's title + body) into one of the five labels the dispatcher
5
+ routes against:
6
+
7
+ - ``ui-build`` — new screen, page, or component.
8
+ - ``ui-improve`` — change to an existing screen / component.
9
+ - ``ui-trivial`` — single-file, single-concern micro-edit (color, copy,
10
+ one class, one prop). Hard preconditions are enforced again at apply
11
+ time; the classifier only labels the *intent*, not the safety floor.
12
+ - ``mixed`` — both UI and backend signals; routes to the mixed track.
13
+ - ``backend-coding`` — default; no UI signal.
14
+
15
+ The classifier is intentionally heuristic-only — it consumes nothing
16
+ beyond the prompt text and optional ticket title. Confidence-band
17
+ gating, AC reconstruction, and assumption surfacing all stay in
18
+ ``directives/backend/refine.py`` (R2). This module only owns the
19
+ *label*; the dispatcher owns the routing.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ from . import classify
24
+ from .classify import (
25
+ INTENT_BACKEND,
26
+ INTENT_MIXED,
27
+ INTENT_UI_BUILD,
28
+ INTENT_UI_IMPROVE,
29
+ INTENT_UI_TRIVIAL,
30
+ KNOWN_INTENTS,
31
+ classify_intent,
32
+ directive_set_for,
33
+ populate_routing,
34
+ )
35
+
36
+ __all__ = [
37
+ "INTENT_BACKEND",
38
+ "INTENT_MIXED",
39
+ "INTENT_UI_BUILD",
40
+ "INTENT_UI_IMPROVE",
41
+ "INTENT_UI_TRIVIAL",
42
+ "KNOWN_INTENTS",
43
+ "classify",
44
+ "classify_intent",
45
+ "directive_set_for",
46
+ "populate_routing",
47
+ ]
@@ -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,199 @@
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
+
47
+ def migrate_payload(payload: Any) -> dict[str, Any]:
48
+ """Return the v1 form of ``payload``.
49
+
50
+ A payload that already declares ``version: 1`` is returned
51
+ unchanged (deep-copied via ``json.loads(json.dumps(...))`` so the
52
+ caller cannot accidentally mutate the input). Anything else is
53
+ treated as v0 and wrapped: ``ticket`` becomes ``input.data``,
54
+ ``input.kind`` is set to ``"ticket"``, and the engine defaults are
55
+ filled in.
56
+
57
+ Raises
58
+ ------
59
+ SchemaError
60
+ If the payload is not a dict, declares a higher version than
61
+ this migration knows about, or lacks a ``ticket`` key.
62
+ """
63
+ if not isinstance(payload, dict):
64
+ raise SchemaError(
65
+ f"v0 state must be a JSON object; got {type(payload).__name__}",
66
+ )
67
+
68
+ declared_version = payload.get("version")
69
+ if declared_version == SCHEMA_VERSION:
70
+ return json.loads(json.dumps(payload))
71
+ if declared_version is not None:
72
+ raise SchemaError(
73
+ f"cannot migrate from version {declared_version!r} to "
74
+ f"{SCHEMA_VERSION}; this script only handles v0 (no version key)",
75
+ )
76
+
77
+ if "ticket" not in payload:
78
+ raise SchemaError(
79
+ "v0 state must carry a 'ticket' key; got keys: "
80
+ f"{sorted(payload.keys())}",
81
+ )
82
+ ticket = payload["ticket"]
83
+ if not isinstance(ticket, dict):
84
+ raise SchemaError(
85
+ f"v0 state.ticket must be a JSON object; got {type(ticket).__name__}",
86
+ )
87
+
88
+ return {
89
+ "version": SCHEMA_VERSION,
90
+ "input": {"kind": "ticket", "data": ticket},
91
+ "intent": DEFAULT_INTENT,
92
+ "directive_set": DEFAULT_DIRECTIVE_SET,
93
+ "persona": payload.get("persona", "senior-engineer"),
94
+ "memory": list(payload.get("memory", [])),
95
+ "plan": payload.get("plan"),
96
+ "changes": list(payload.get("changes", [])),
97
+ "tests": payload.get("tests"),
98
+ "verify": payload.get("verify"),
99
+ "outcomes": dict(payload.get("outcomes", {})),
100
+ "questions": list(payload.get("questions", [])),
101
+ "report": payload.get("report", ""),
102
+ }
103
+
104
+
105
+ def migrate_file(
106
+ source: Path,
107
+ *,
108
+ destination: Path | None = None,
109
+ backup: bool = True,
110
+ ) -> Path:
111
+ """Migrate the v0 state file at ``source`` and write the v1 result.
112
+
113
+ ``destination`` defaults to :data:`DEFAULT_V1_FILENAME` next to
114
+ ``source``. When ``backup`` is true (the default) the original
115
+ file is renamed with :data:`BACKUP_SUFFIX` appended; when false,
116
+ the original is left untouched. The destination must not exist —
117
+ refusing to overwrite is the safety net against accidental
118
+ double-migration on CI.
119
+
120
+ Returns the destination path on success.
121
+ """
122
+ if not source.is_file():
123
+ raise SchemaError(f"v0 state file not found: {source}")
124
+
125
+ raw = source.read_text(encoding="utf-8")
126
+ try:
127
+ payload = json.loads(raw)
128
+ except json.JSONDecodeError as exc:
129
+ raise SchemaError(f"invalid JSON in {source}: {exc}") from exc
130
+
131
+ target = destination or source.with_name(DEFAULT_V1_FILENAME)
132
+ if target.exists():
133
+ raise SchemaError(
134
+ f"refusing to overwrite existing destination {target}; "
135
+ "delete or rename it first",
136
+ )
137
+
138
+ migrated = migrate_payload(payload)
139
+ target.parent.mkdir(parents=True, exist_ok=True)
140
+ target.write_text(
141
+ json.dumps(migrated, indent=2, ensure_ascii=False) + "\n",
142
+ encoding="utf-8",
143
+ )
144
+
145
+ if backup:
146
+ backup_path = source.with_suffix(source.suffix + BACKUP_SUFFIX)
147
+ shutil.move(str(source), str(backup_path))
148
+
149
+ return target
150
+
151
+
152
+ def main(argv: Sequence[str] | None = None) -> int:
153
+ """CLI entry point — ``python3 -m work_engine.migration.v0_to_v1``.
154
+
155
+ Exits ``0`` on success, ``2`` on any :class:`SchemaError` so the
156
+ invoking shell can branch on the failure category.
157
+ """
158
+ parser = argparse.ArgumentParser(
159
+ prog="work_engine.migration.v0_to_v1",
160
+ description="Migrate a legacy .implement-ticket-state.json file to "
161
+ "the v1 .work-state.json schema.",
162
+ )
163
+ parser.add_argument(
164
+ "source",
165
+ type=Path,
166
+ nargs="?",
167
+ default=Path(DEFAULT_V0_FILENAME),
168
+ help=f"Path to the v0 state file (default: {DEFAULT_V0_FILENAME}).",
169
+ )
170
+ parser.add_argument(
171
+ "--destination",
172
+ type=Path,
173
+ default=None,
174
+ help="Path to write the v1 file to "
175
+ f"(default: {DEFAULT_V1_FILENAME} next to source).",
176
+ )
177
+ parser.add_argument(
178
+ "--no-backup",
179
+ action="store_true",
180
+ help="Do not rename the v0 source to .bak after migration.",
181
+ )
182
+ args = parser.parse_args(argv)
183
+
184
+ try:
185
+ target = migrate_file(
186
+ args.source,
187
+ destination=args.destination,
188
+ backup=not args.no_backup,
189
+ )
190
+ except SchemaError as exc:
191
+ print(f"error: {exc}", file=sys.stderr)
192
+ return 2
193
+
194
+ print(f"migrated {args.source} → {target}")
195
+ return 0
196
+
197
+
198
+ if __name__ == "__main__":
199
+ sys.exit(main())
@@ -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"]