@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,331 @@
1
+ """Linear step dispatcher for ``/implement-ticket``.
2
+
3
+ The dispatcher holds no business logic. It walks the fixed eight-step
4
+ order declared in ``agents/contexts/implement-ticket-flow.md``, hands
5
+ each step a live ``DeliveryState``, and honours the three terminal
6
+ outcomes:
7
+
8
+ - ``SUCCESS`` — record and advance.
9
+ - ``BLOCKED`` — record, copy questions onto the state, halt.
10
+ - ``PARTIAL`` — record, copy questions onto the state, halt.
11
+
12
+ Resumption semantics (Option A, flow contract §agent-directives):
13
+ steps whose name is already marked ``success`` in
14
+ ``state.outcomes`` are **skipped**. This lets a caller re-invoke the
15
+ dispatcher after executing an agent-directive (the ``implement``,
16
+ ``test``, ``verify`` steps cannot run from pure Python), update the
17
+ relevant slice of ``DeliveryState``, record ``success`` on the
18
+ resumed step, and continue without replaying earlier work.
19
+
20
+ Step handlers are injected by the caller rather than discovered at
21
+ import time. Phase 1 shipped the dispatcher with mock handlers;
22
+ Phase 2 wires the real ones under ``steps/``. Keeping injection
23
+ explicit means the dispatcher is trivially testable and never
24
+ depends on handler import order.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ from collections.abc import Mapping
29
+ from importlib import import_module
30
+ from typing import Any
31
+
32
+ from .delivery_state import DeliveryState, Outcome, Step, StepResult
33
+ from .hooks import HookContext, HookEvent, HookHalt, HookRunner
34
+ from .state import KNOWN_DIRECTIVE_SETS
35
+
36
+ _NOOP_RUNNER: HookRunner = HookRunner()
37
+ """Shared empty-registry runner reused when ``dispatch`` is called
38
+ without an explicit ``hooks`` argument. ``HookRunner.emit`` short-circuits
39
+ when no callbacks are registered, so the hot path stays branch-light
40
+ while the call sites stay uniform."""
41
+
42
+ STEP_ORDER: tuple[str, ...] = (
43
+ "refine",
44
+ "memory",
45
+ "analyze",
46
+ "plan",
47
+ "implement",
48
+ "test",
49
+ "verify",
50
+ "report",
51
+ )
52
+ """Canonical execution order. Eight steps, fixed, no branching.
53
+
54
+ Changing this order is a roadmap-level decision — not a PR rider — per
55
+ the surface-growth guardrails in
56
+ ``agents/roadmaps/road-to-implement-ticket.md``.
57
+ """
58
+
59
+ DEFAULT_DIRECTIVE_SET: str = "backend"
60
+ """Directive set chosen when ``state`` does not carry one explicitly.
61
+
62
+ Backwards compatibility for v0 ``DeliveryState`` callers: the legacy
63
+ shape has no ``directive_set`` field, so ``select_directive_set``
64
+ falls back to ``"backend"`` and the engine behaves exactly as it did
65
+ before R1 Phase 4.
66
+ """
67
+
68
+ # Schema enum names use hyphens (``ui-trivial``) but Python packages
69
+ # cannot. The loader is the single place that bridges between the two
70
+ # forms; everywhere else uses the wire form.
71
+ _PACKAGE_NAME_OVERRIDES: Mapping[str, str] = {"ui-trivial": "ui_trivial"}
72
+
73
+
74
+ def dispatch(
75
+ state: DeliveryState,
76
+ steps: Mapping[str, Step],
77
+ hooks: HookRunner | None = None,
78
+ ) -> tuple[Outcome, str | None]:
79
+ """Run the eight steps linearly against ``state``.
80
+
81
+ Returns a ``(final_outcome, halting_step)`` tuple. ``halting_step``
82
+ is ``None`` when every step succeeded; otherwise it carries the
83
+ name of the step whose result halted the flow.
84
+
85
+ Parameters
86
+ ----------
87
+ state:
88
+ Live ``DeliveryState``. Mutated in place: each step's outcome
89
+ is recorded in ``state.outcomes`` under the step name, and
90
+ any surfaced questions land on ``state.questions``.
91
+ steps:
92
+ Mapping from step name to handler. Every entry in
93
+ :data:`STEP_ORDER` must be present; missing entries raise
94
+ ``KeyError`` at dispatch time rather than silently skipping,
95
+ so incomplete wiring surfaces as a hard failure.
96
+ hooks:
97
+ Optional :class:`HookRunner` carrying a registry of dispatcher-
98
+ layer hooks (``before_step``, ``after_step``, ``on_halt``,
99
+ ``on_error``). Default ``None`` preserves every existing call
100
+ site verbatim — internally ``dispatch`` falls back to a shared
101
+ empty-registry runner so hook bookkeeping stays uniform without
102
+ a per-emit ``if hooks is None`` branch.
103
+
104
+ Raises
105
+ ------
106
+ KeyError
107
+ If ``steps`` does not cover every entry in
108
+ :data:`STEP_ORDER`.
109
+ """
110
+ _assert_all_steps_present(steps)
111
+
112
+ # Clear stale questions from a previous halt before we resume so
113
+ # the caller never mistakes old options for fresh ones.
114
+ state.questions = []
115
+
116
+ runner = hooks if hooks is not None else _NOOP_RUNNER
117
+
118
+ for name in STEP_ORDER:
119
+ if state.outcomes.get(name) == Outcome.SUCCESS.value:
120
+ # Already completed on an earlier invocation — skip per the
121
+ # resume contract. The caller is responsible for keeping
122
+ # ``state.outcomes`` and the matching slice in sync.
123
+ continue
124
+
125
+ before_halt = runner.emit(
126
+ HookEvent.BEFORE_STEP,
127
+ HookContext(step_name=name, delivery=state),
128
+ )
129
+ if before_halt is not None:
130
+ return _hook_halt_blocked(state, runner, name, before_halt, result=None)
131
+
132
+ handler = steps[name]
133
+ try:
134
+ result = handler(state)
135
+ except Exception as exc:
136
+ # Let dispatcher-layer observers see the failure before the
137
+ # exception unwinds the engine. ``on_error`` is observe-only;
138
+ # the original exception is always re-raised.
139
+ runner.emit(
140
+ HookEvent.ON_ERROR,
141
+ HookContext(step_name=name, delivery=state, exception=exc),
142
+ )
143
+ raise
144
+ _validate_step_result(name, result)
145
+
146
+ state.outcomes[name] = result.outcome.value
147
+
148
+ after_halt = runner.emit(
149
+ HookEvent.AFTER_STEP,
150
+ HookContext(step_name=name, delivery=state, result=result),
151
+ )
152
+ if after_halt is not None:
153
+ return _hook_halt_blocked(state, runner, name, after_halt, result=result)
154
+
155
+ if result.outcome is Outcome.BLOCKED:
156
+ state.questions = list(result.questions)
157
+ _emit_on_halt(runner, name, state, result)
158
+ return Outcome.BLOCKED, name
159
+
160
+ if result.outcome is Outcome.PARTIAL:
161
+ state.questions = list(result.questions)
162
+ _emit_on_halt(runner, name, state, result)
163
+ return Outcome.PARTIAL, name
164
+
165
+ return Outcome.SUCCESS, None
166
+
167
+
168
+ def _hook_halt_blocked(
169
+ state: DeliveryState,
170
+ runner: HookRunner,
171
+ name: str,
172
+ halt: HookHalt,
173
+ result: StepResult | None,
174
+ ) -> tuple[Outcome, str | None]:
175
+ """Translate a hook-driven :class:`HookHalt` into a clean engine halt.
176
+
177
+ Hook-driven halts are treated as first-class engine halts per the
178
+ P2 contract: the dispatcher returns ``(BLOCKED, step_name)`` with
179
+ ``state.questions`` rendered verbatim from the halt's ``surface``.
180
+ The step's outcome marker is set to ``"blocked"`` only when the
181
+ halt fires before the handler ran (so resume re-enters the gate);
182
+ when it fires after the handler, the marker the handler produced
183
+ is preserved so resume reflects what actually happened.
184
+ """
185
+ if result is None:
186
+ state.outcomes[name] = Outcome.BLOCKED.value
187
+ state.questions = list(halt.surface)
188
+ _emit_on_halt(runner, name, state, result)
189
+ return Outcome.BLOCKED, name
190
+
191
+
192
+ def _emit_on_halt(
193
+ runner: HookRunner,
194
+ name: str,
195
+ state: DeliveryState,
196
+ result: StepResult | None,
197
+ ) -> None:
198
+ """Fire ``on_halt`` as an observe-only event.
199
+
200
+ A :class:`HookHalt` raised from inside ``on_halt`` would create a
201
+ halt-of-a-halt loop; the runner returns it but the dispatcher
202
+ deliberately ignores it — the halt surface is already populated.
203
+ """
204
+ runner.emit(
205
+ HookEvent.ON_HALT,
206
+ HookContext(step_name=name, delivery=state, result=result),
207
+ )
208
+
209
+
210
+ def _assert_all_steps_present(steps: Mapping[str, Step]) -> None:
211
+ """Reject an incomplete step mapping up front.
212
+
213
+ We deliberately fail loudly here: a missing step would otherwise
214
+ raise deep inside the dispatch loop after partial state mutation,
215
+ which makes debugging the wiring harder than it needs to be.
216
+ """
217
+ missing = [name for name in STEP_ORDER if name not in steps]
218
+ if missing:
219
+ raise KeyError(
220
+ "Step mapping is missing handlers for: " + ", ".join(missing),
221
+ )
222
+
223
+
224
+ def _validate_step_result(name: str, result: StepResult) -> None:
225
+ """Enforce the blocked/partial invariant: questions must be set.
226
+
227
+ A step that blocks without surfacing a question is a bug — there
228
+ is nothing for the user to answer. We raise ``ValueError`` instead
229
+ of silently recording the outcome so the defect is visible at the
230
+ earliest possible point.
231
+ """
232
+ if result.outcome in (Outcome.BLOCKED, Outcome.PARTIAL) and not result.questions:
233
+ raise ValueError(
234
+ f"Step {name!r} returned {result.outcome.value} with no questions; "
235
+ "blocked and partial outcomes must surface at least one numbered option.",
236
+ )
237
+
238
+
239
+ def select_directive_set(state: Any) -> str:
240
+ """Return the directive set name to dispatch ``state`` against.
241
+
242
+ Looks for ``state.directive_set`` (the v1 :class:`work_engine.state.WorkState`
243
+ field) and falls back to :data:`DEFAULT_DIRECTIVE_SET` when the
244
+ attribute is missing — the legacy v0 :class:`DeliveryState` has no
245
+ such field, and existing callers must keep working unchanged
246
+ until R1 Phase 4 Step 1 lands the runtime switch.
247
+
248
+ The returned name is validated against :data:`KNOWN_DIRECTIVE_SETS`;
249
+ an unknown value raises ``ValueError`` rather than silently
250
+ falling back, so a typo in a hand-written state file fails loudly
251
+ instead of producing surprising behavior.
252
+ """
253
+ name = getattr(state, "directive_set", DEFAULT_DIRECTIVE_SET)
254
+ if not isinstance(name, str) or not name:
255
+ raise ValueError(
256
+ f"directive_set must be a non-empty string; got {name!r}",
257
+ )
258
+ if name not in KNOWN_DIRECTIVE_SETS:
259
+ raise ValueError(
260
+ f"unknown directive_set {name!r}; "
261
+ f"known sets: {sorted(KNOWN_DIRECTIVE_SETS)}",
262
+ )
263
+ return name
264
+
265
+
266
+ def load_directive_set(name: str) -> Mapping[str, Step]:
267
+ """Import the ``directives.<name>`` package and return its step mapping.
268
+
269
+ The selected set's ``__init__`` exposes a ``get_steps()`` factory
270
+ (see :class:`work_engine.directives.backend`) that returns the
271
+ ``{step_name: handler}`` mapping the dispatcher walks. Unimplemented
272
+ sets (``ui``, ``ui-trivial``, ``mixed``) raise
273
+ ``NotImplementedError`` from their ``get_steps()`` so the failure
274
+ point is the loader, not a half-walked dispatch loop.
275
+
276
+ The schema enum carries hyphenated wire names (``ui-trivial``) but
277
+ Python packages must use underscores; :data:`_PACKAGE_NAME_OVERRIDES`
278
+ is the single translation point.
279
+ """
280
+ module = _import_directive_set(name)
281
+ get_steps = getattr(module, "get_steps", None)
282
+ if not callable(get_steps):
283
+ raise AttributeError(
284
+ f"work_engine.directives.{module.__name__.rsplit('.', 1)[-1]} "
285
+ "does not expose a callable get_steps()",
286
+ )
287
+ steps = get_steps()
288
+ if not isinstance(steps, Mapping):
289
+ raise TypeError(
290
+ f"work_engine.directives.{module.__name__.rsplit('.', 1)[-1]}"
291
+ f".get_steps() must return a Mapping; "
292
+ f"got {type(steps).__name__}",
293
+ )
294
+ return steps
295
+
296
+
297
+ def assert_kind_supported(kind: str, set_name: str) -> None:
298
+ """Raise ``NotImplementedError`` if ``set_name`` cannot handle ``kind``.
299
+
300
+ Reads the per-set ``SUPPORTED_KINDS`` tuple (see
301
+ :data:`work_engine.directives.backend.SUPPORTED_KINDS`) and checks
302
+ membership. Distinct from :func:`select_directive_set`, which only
303
+ validates the directive-set *name*: this gate validates the
304
+ name/kind *pair*, so a future schema widening that adds new
305
+ ``input.kind`` values (R2 ``prompt``) halts loudly at the boundary
306
+ instead of crashing inside the first deterministic step.
307
+
308
+ Sets that have no ``SUPPORTED_KINDS`` attribute are treated as
309
+ "supports nothing" — the unimplemented stubs (``ui``,
310
+ ``ui-trivial``, ``mixed``) already raise from ``get_steps()``, so
311
+ this branch only matters during the brief window between adding a
312
+ new directive set and wiring its capability tuple.
313
+ """
314
+ module = _import_directive_set(set_name)
315
+ supported = getattr(module, "SUPPORTED_KINDS", ())
316
+ if kind not in supported:
317
+ raise NotImplementedError(
318
+ f"directive_set {set_name!r} does not handle "
319
+ f"input.kind={kind!r}; supported kinds: {sorted(set(supported))}",
320
+ )
321
+
322
+
323
+ def _import_directive_set(name: str):
324
+ """Validate ``name`` and import the matching package module."""
325
+ if name not in KNOWN_DIRECTIVE_SETS:
326
+ raise ValueError(
327
+ f"unknown directive_set {name!r}; "
328
+ f"known sets: {sorted(KNOWN_DIRECTIVE_SETS)}",
329
+ )
330
+ package_name = _PACKAGE_NAME_OVERRIDES.get(name, name)
331
+ return import_module(f"work_engine.directives.{package_name}")
@@ -0,0 +1,54 @@
1
+ """``work_engine.hooks`` — cross-cutting lifecycle hooks for the engine.
2
+
3
+ Phase 1 of ``agents/roadmaps/road-to-work-engine-hooks.md`` ships the
4
+ primitives only. The dispatcher and CLI are not yet instrumented;
5
+ golden tests must remain byte-identical until Phase 2 / Phase 3 land.
6
+
7
+ Public surface:
8
+
9
+ - :class:`HookEvent` — ten lifecycle events, two layers.
10
+ - :class:`HookContext` — per-event payload.
11
+ - :class:`HookError` / :class:`HookHalt` — three-tier error contract.
12
+ - :class:`HookRegistry` — insertion-ordered event \u2192 callbacks map.
13
+ - :class:`HookRunner` — single emit point, owns the error contract.
14
+
15
+ The principle is documented in
16
+ ``agents/roadmaps/road-to-work-engine-hooks.md`` § Underlying
17
+ principle: agent hooks are emulated by moving lifecycle ownership
18
+ from the agent into the work engine. The engine owns boundaries.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ from .builtin import (
23
+ ChatHistoryAppendHook,
24
+ ChatHistoryHaltAppendHook,
25
+ ChatHistoryHeartbeatHook,
26
+ ChatHistoryTurnCheckHook,
27
+ DirectiveSetGuardHook,
28
+ HaltSurfaceAuditHook,
29
+ StateShapeValidationHook,
30
+ TraceHook,
31
+ )
32
+ from .context import HookContext
33
+ from .events import HookEvent
34
+ from .exceptions import HookError, HookHalt
35
+ from .registry import HookCallback, HookRegistry
36
+ from .runner import HookRunner
37
+
38
+ __all__ = [
39
+ "ChatHistoryAppendHook",
40
+ "ChatHistoryHaltAppendHook",
41
+ "ChatHistoryHeartbeatHook",
42
+ "ChatHistoryTurnCheckHook",
43
+ "DirectiveSetGuardHook",
44
+ "HaltSurfaceAuditHook",
45
+ "HookCallback",
46
+ "HookContext",
47
+ "HookError",
48
+ "HookEvent",
49
+ "HookHalt",
50
+ "HookRegistry",
51
+ "HookRunner",
52
+ "StateShapeValidationHook",
53
+ "TraceHook",
54
+ ]
@@ -0,0 +1,32 @@
1
+ """Concrete observability hooks shipped with the engine.
2
+
3
+ Phase 4 hooks: low-risk, default-off, observe-only. They are registered
4
+ by ``cli._build_hook_registry`` only when explicitly enabled in
5
+ ``.agent-settings.yml`` (Phase 6 wires the settings → registry path).
6
+
7
+ Each hook is a small class exposing a ``register(registry)`` method so
8
+ the registry stays the single source of truth for event → callback
9
+ wiring. None of these hooks mutate engine state; failures surface as
10
+ :class:`HookError` (non-fatal, the runner warns and continues).
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from .chat_history_append import ChatHistoryAppendHook
15
+ from .chat_history_halt_append import ChatHistoryHaltAppendHook
16
+ from .chat_history_heartbeat import ChatHistoryHeartbeatHook
17
+ from .chat_history_turn_check import ChatHistoryTurnCheckHook
18
+ from .directive_set_guard import DirectiveSetGuardHook
19
+ from .halt_surface_audit import HaltSurfaceAuditHook
20
+ from .state_shape_validation import StateShapeValidationHook
21
+ from .trace import TraceHook
22
+
23
+ __all__ = [
24
+ "ChatHistoryAppendHook",
25
+ "ChatHistoryHaltAppendHook",
26
+ "ChatHistoryHeartbeatHook",
27
+ "ChatHistoryTurnCheckHook",
28
+ "DirectiveSetGuardHook",
29
+ "HaltSurfaceAuditHook",
30
+ "StateShapeValidationHook",
31
+ "TraceHook",
32
+ ]
@@ -0,0 +1,103 @@
1
+ """Shared plumbing for chat-history hooks.
2
+
3
+ Subprocess-driven so the work-engine package stays decoupled from
4
+ ``scripts/chat_history.py``'s internals. The ``runner`` injection
5
+ point is the test seam — production passes ``subprocess.run``,
6
+ tests pass a fake.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Callable, Sequence
14
+
15
+ from ..context import HookContext
16
+ from ..exceptions import HookError
17
+
18
+ ProcessRunner = Callable[[Sequence[str]], "subprocess.CompletedProcess[str]"]
19
+ """Callable that runs a subprocess. Production default: ``_default_runner``."""
20
+
21
+ EXIT_OK = 0
22
+ EXIT_MISSING = 10
23
+ EXIT_FOREIGN = 11
24
+ EXIT_RETURNING = 12
25
+
26
+
27
+ def _default_runner(cmd: Sequence[str]) -> "subprocess.CompletedProcess[str]":
28
+ return subprocess.run(list(cmd), capture_output=True, text=True, check=False)
29
+
30
+
31
+ def _derive_first_user_msg(ctx: HookContext) -> str | None:
32
+ """Pull a stable first-user-msg out of the available context.
33
+
34
+ CLI-layer events carry ``ctx.work`` (the v1 envelope); dispatcher-layer
35
+ events (``before_step`` / ``after_step`` / ``on_halt``) carry only
36
+ ``ctx.delivery`` (the legacy :class:`DeliveryState`). Both shapes feed
37
+ the same ``id: title`` / ``raw`` derivation so chat-history entries
38
+ stay stable across the lifecycle. Returns ``None`` when the shape is
39
+ unknown — callers raise ``HookError`` so the runner converts it to
40
+ a warning.
41
+ """
42
+ work = ctx.work
43
+ if work is not None and getattr(work, "input", None) is not None:
44
+ inp = work.input
45
+ data = getattr(inp, "data", None) or {}
46
+ kind = getattr(inp, "kind", None)
47
+ if kind == "prompt":
48
+ raw = data.get("raw")
49
+ if raw:
50
+ return str(raw)
51
+ elif kind == "ticket":
52
+ joined = _ticket_msg(data)
53
+ if joined:
54
+ return joined
55
+
56
+ delivery = ctx.delivery
57
+ if delivery is not None:
58
+ ticket = getattr(delivery, "ticket", None) or {}
59
+ joined = _ticket_msg(ticket)
60
+ if joined:
61
+ return joined
62
+ return None
63
+
64
+
65
+ def _ticket_msg(ticket: dict) -> str:
66
+ ticket_id = ticket.get("id") or ""
67
+ title = ticket.get("title") or ""
68
+ return f"{ticket_id}: {title}".strip(": ").strip()
69
+
70
+
71
+ class _ChatHistoryHookBase:
72
+ """Shared plumbing — script path, runner, and first-msg derivation."""
73
+
74
+ def __init__(
75
+ self,
76
+ script_path: Path,
77
+ *,
78
+ runner: ProcessRunner | None = None,
79
+ first_user_msg: str | None = None,
80
+ ) -> None:
81
+ self.script_path = Path(script_path)
82
+ self._runner = runner or _default_runner
83
+ self._fixed_msg = first_user_msg
84
+
85
+ def _resolve_msg(self, ctx: HookContext) -> str:
86
+ msg = self._fixed_msg or _derive_first_user_msg(ctx)
87
+ if not msg:
88
+ raise HookError("chat-history hook: cannot derive first-user-msg")
89
+ return msg
90
+
91
+ def _invoke(self, *args: str) -> "subprocess.CompletedProcess[str]":
92
+ cmd = [sys.executable, str(self.script_path), *args]
93
+ return self._runner(cmd)
94
+
95
+
96
+ __all__ = [
97
+ "EXIT_FOREIGN",
98
+ "EXIT_MISSING",
99
+ "EXIT_OK",
100
+ "EXIT_RETURNING",
101
+ "ProcessRunner",
102
+ "_ChatHistoryHookBase",
103
+ ]
@@ -0,0 +1,44 @@
1
+ """``ChatHistoryAppendHook`` — phase-boundary persistence.
2
+
3
+ Fires on ``after_step``. Appends a ``--type phase`` entry whenever a
4
+ step closed with ``Outcome.SUCCESS``. Failures bubble up as
5
+ :class:`HookError` so the runner converts them to warnings — append
6
+ errors must not break the main flow.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any
12
+
13
+ from ..context import HookContext
14
+ from ..events import HookEvent
15
+ from ..exceptions import HookError
16
+ from ..registry import HookRegistry
17
+ from ._chat_history_base import EXIT_OK, _ChatHistoryHookBase
18
+
19
+
20
+ class ChatHistoryAppendHook(_ChatHistoryHookBase):
21
+ """Append a phase-boundary entry after every successful step."""
22
+
23
+ def register(self, registry: HookRegistry) -> None:
24
+ registry.register(HookEvent.AFTER_STEP, self._on_after_step)
25
+
26
+ def _on_after_step(self, ctx: HookContext) -> None:
27
+ from ...delivery_state import Outcome # local: avoid import cycle.
28
+
29
+ result = ctx.result
30
+ if result is None or getattr(result, "outcome", None) != Outcome.SUCCESS:
31
+ return
32
+ msg = self._resolve_msg(ctx)
33
+ payload: dict[str, Any] = {"step": ctx.step_name or "<unknown>"}
34
+ proc = self._invoke(
35
+ "append", "--first-user-msg", msg,
36
+ "--type", "phase", "--json", json.dumps(payload),
37
+ )
38
+ if proc.returncode != EXIT_OK:
39
+ raise HookError(
40
+ f"chat-history append failed (exit {proc.returncode})"
41
+ )
42
+
43
+
44
+ __all__ = ["ChatHistoryAppendHook"]
@@ -0,0 +1,42 @@
1
+ """``ChatHistoryHaltAppendHook`` — capture halt surfaces in the log.
2
+
3
+ Fires on ``on_halt``. Records a ``--type decision`` entry with the
4
+ step name and any pending questions so a fresh chat can resume from
5
+ the persisted log alone.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+
11
+ from ..context import HookContext
12
+ from ..events import HookEvent
13
+ from ..exceptions import HookError
14
+ from ..registry import HookRegistry
15
+ from ._chat_history_base import EXIT_OK, _ChatHistoryHookBase
16
+
17
+
18
+ class ChatHistoryHaltAppendHook(_ChatHistoryHookBase):
19
+ """Append a decision entry whenever a step halts."""
20
+
21
+ def register(self, registry: HookRegistry) -> None:
22
+ registry.register(HookEvent.ON_HALT, self._on_halt)
23
+
24
+ def _on_halt(self, ctx: HookContext) -> None:
25
+ msg = self._resolve_msg(ctx)
26
+ questions: list[str] = []
27
+ if ctx.result is not None:
28
+ questions = list(getattr(ctx.result, "questions", []) or [])
29
+ if not questions and ctx.delivery is not None:
30
+ questions = list(getattr(ctx.delivery, "questions", []) or [])
31
+ payload = {"step": ctx.step_name or "<unknown>", "questions": questions}
32
+ proc = self._invoke(
33
+ "append", "--first-user-msg", msg,
34
+ "--type", "decision", "--json", json.dumps(payload),
35
+ )
36
+ if proc.returncode != EXIT_OK:
37
+ raise HookError(
38
+ f"chat-history halt-append failed (exit {proc.returncode})"
39
+ )
40
+
41
+
42
+ __all__ = ["ChatHistoryHaltAppendHook"]
@@ -0,0 +1,50 @@
1
+ """``ChatHistoryHeartbeatHook`` — visibility marker before save.
2
+
3
+ Fires on ``before_save``. Runs ``chat_history.py heartbeat`` and,
4
+ if the script emits a marker line, threads it onto ``state.report``
5
+ so the agent's reply naturally carries the heartbeat without manual
6
+ copy/paste.
7
+
8
+ Why ``before_save`` and not ``after_dispatch``: the marker must land
9
+ in the report that gets persisted. ``cli._sync_back`` runs between
10
+ ``after_dispatch`` and ``before_save`` and reassigns
11
+ ``work.report = delivery.report`` — a marker written on
12
+ ``after_dispatch`` would be overwritten before ``_save``. Firing on
13
+ ``before_save`` runs after the sync, so the marker survives.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from ..context import HookContext
18
+ from ..events import HookEvent
19
+ from ..exceptions import HookError
20
+ from ..registry import HookRegistry
21
+ from ._chat_history_base import EXIT_OK, _ChatHistoryHookBase
22
+
23
+
24
+ class ChatHistoryHeartbeatHook(_ChatHistoryHookBase):
25
+ """Run heartbeat before save; thread marker into ``state.report``."""
26
+
27
+ def register(self, registry: HookRegistry) -> None:
28
+ registry.register(HookEvent.BEFORE_SAVE, self._on_before_save)
29
+
30
+ def _on_before_save(self, ctx: HookContext) -> None:
31
+ msg = self._resolve_msg(ctx)
32
+ proc = self._invoke("heartbeat", "--first-user-msg", msg)
33
+ if proc.returncode != EXIT_OK:
34
+ raise HookError(
35
+ f"chat-history heartbeat failed (exit {proc.returncode})"
36
+ )
37
+ marker = (proc.stdout or "").strip()
38
+ if not marker or ctx.work is None:
39
+ return
40
+ existing = getattr(ctx.work, "report", "") or ""
41
+ if marker in existing:
42
+ return
43
+ sep = "\n\n" if existing else ""
44
+ try:
45
+ ctx.work.report = f"{existing}{sep}{marker}"
46
+ except AttributeError:
47
+ raise HookError("chat-history heartbeat: state.report not writable")
48
+
49
+
50
+ __all__ = ["ChatHistoryHeartbeatHook"]