@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,49 @@
1
+ """``ChatHistoryTurnCheckHook`` — guards engine-driven turns.
2
+
3
+ Fires on ``before_dispatch``; classifies the chat-history file via
4
+ ``scripts/chat_history.py turn-check``:
5
+
6
+ - exit 0 (``ok``) → continue
7
+ - exit 10 (``missing``) → continue (auto-init handled by chat_history.py)
8
+ - exit 11 (``foreign``) → raise :class:`HookHalt` so CLI exits 2
9
+ - exit 12 (``returning``)→ raise :class:`HookHalt` so CLI exits 2
10
+ - any other exit → raise :class:`HookError` (warn, continue)
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from ..context import HookContext
15
+ from ..events import HookEvent
16
+ from ..exceptions import HookError, HookHalt
17
+ from ..registry import HookRegistry
18
+ from ._chat_history_base import (
19
+ EXIT_FOREIGN,
20
+ EXIT_MISSING,
21
+ EXIT_OK,
22
+ EXIT_RETURNING,
23
+ _ChatHistoryHookBase,
24
+ )
25
+
26
+
27
+ class ChatHistoryTurnCheckHook(_ChatHistoryHookBase):
28
+ """Run ``turn-check`` at the start of dispatch; halt on drift."""
29
+
30
+ def register(self, registry: HookRegistry) -> None:
31
+ registry.register(HookEvent.BEFORE_DISPATCH, self._on_before_dispatch)
32
+
33
+ def _on_before_dispatch(self, ctx: HookContext) -> None:
34
+ msg = self._resolve_msg(ctx)
35
+ result = self._invoke("turn-check", "--first-user-msg", msg)
36
+ code = result.returncode
37
+ if code in (EXIT_OK, EXIT_MISSING):
38
+ return
39
+ if code in (EXIT_FOREIGN, EXIT_RETURNING):
40
+ text = (result.stderr or result.stdout or "").strip()
41
+ reason = "foreign" if code == EXIT_FOREIGN else "returning"
42
+ surface = [line for line in text.splitlines() if line] or [
43
+ f"chat-history turn-check: {reason}",
44
+ ]
45
+ raise HookHalt(f"chat_history_turn_check_{reason}", surface=surface)
46
+ raise HookError(f"chat-history turn-check failed (exit {code})")
47
+
48
+
49
+ __all__ = ["ChatHistoryTurnCheckHook"]
@@ -0,0 +1,53 @@
1
+ """``DirectiveSetGuardHook`` — catch CLI / state directive-set drift.
2
+
3
+ Fires on :data:`HookEvent.BEFORE_DISPATCH`. Compares the resolved
4
+ ``set_name`` (the directive bundle the CLI just loaded) against the
5
+ ``directive_set`` field on the persisted ``WorkState``. Mismatch →
6
+ :class:`HookError` (non-fatal: the runner warns), so a flow that
7
+ silently re-dispatches under a different set surfaces the drift before
8
+ any step runs.
9
+
10
+ The guard is read-only. It does not rewrite ``state.directive_set``;
11
+ fixing the drift is the user's call (typically a ``/mode`` switch or a
12
+ fresh state file).
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from ..context import HookContext
17
+ from ..events import HookEvent
18
+ from ..exceptions import HookError
19
+ from ..registry import HookRegistry
20
+
21
+
22
+ class DirectiveSetGuardHook:
23
+ """Asserts ``set_name`` matches ``state.directive_set`` on dispatch."""
24
+
25
+ def register(self, registry: HookRegistry) -> None:
26
+ """Register on :data:`HookEvent.BEFORE_DISPATCH`."""
27
+ registry.register(HookEvent.BEFORE_DISPATCH, self._guard)
28
+
29
+ def _guard(self, ctx: HookContext) -> None:
30
+ set_name = ctx.set_name
31
+ work = ctx.work
32
+ if set_name is None or work is None:
33
+ # ``before_dispatch`` always carries both refs per the
34
+ # context surface; missing means a hook-bug, not drift.
35
+ raise HookError(
36
+ "directive-set guard: missing set_name or work on "
37
+ f"before_dispatch (set_name={set_name!r}, work={work!r})",
38
+ )
39
+
40
+ persisted = getattr(work, "directive_set", None)
41
+ if persisted is None:
42
+ # Legacy v0 envelopes have no ``directive_set`` field;
43
+ # the guard is a no-op for those — nothing to compare.
44
+ return
45
+
46
+ if persisted != set_name:
47
+ raise HookError(
48
+ "directive-set drift: CLI resolved "
49
+ f"{set_name!r} but state carries {persisted!r}",
50
+ )
51
+
52
+
53
+ __all__ = ["DirectiveSetGuardHook"]
@@ -0,0 +1,50 @@
1
+ """``HaltSurfaceAuditHook`` — defense-in-depth around halt surfaces.
2
+
3
+ The dispatcher already calls ``_validate_step_result`` to reject a
4
+ ``BLOCKED`` / ``PARTIAL`` outcome with no questions. This hook fires on
5
+ ``on_halt`` and re-asserts the same invariant from the hook side, so a
6
+ hand-crafted handler that bypasses the validator (e.g. a future direct
7
+ ``state.questions`` mutation) still surfaces a clear failure.
8
+
9
+ Pure observability: emits :class:`HookError` (non-fatal) when the
10
+ surface is empty. The runner converts it to a ``warnings.warn`` so the
11
+ violation is visible in test logs and CI.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from ..context import HookContext
16
+ from ..events import HookEvent
17
+ from ..exceptions import HookError
18
+ from ..registry import HookRegistry
19
+
20
+
21
+ class HaltSurfaceAuditHook:
22
+ """Asserts that every halt carries a non-empty user-facing surface."""
23
+
24
+ def register(self, registry: HookRegistry) -> None:
25
+ """Register on :data:`HookEvent.ON_HALT` only."""
26
+ registry.register(HookEvent.ON_HALT, self._audit)
27
+
28
+ def _audit(self, ctx: HookContext) -> None:
29
+ result = ctx.result
30
+ if result is None:
31
+ # Hook-driven halts go through ``_hook_halt_blocked`` and
32
+ # may not carry a ``StepResult`` — the surface lives on
33
+ # ``state.questions`` instead. Audit that fallback too.
34
+ questions = getattr(ctx.delivery, "questions", None)
35
+ if not questions:
36
+ raise HookError(
37
+ f"halt at step {ctx.step_name!r} surfaced no questions "
38
+ "(hook-driven halt with empty state.questions)",
39
+ )
40
+ return
41
+
42
+ questions = getattr(result, "questions", None)
43
+ if not questions:
44
+ raise HookError(
45
+ f"halt at step {ctx.step_name!r} surfaced no questions "
46
+ "(StepResult.questions empty); the user has nothing to act on",
47
+ )
48
+
49
+
50
+ __all__ = ["HaltSurfaceAuditHook"]
@@ -0,0 +1,52 @@
1
+ """``StateShapeValidationHook`` — round-trip the v1 envelope on load and save.
2
+
3
+ Fires on :data:`HookEvent.AFTER_LOAD` and :data:`HookEvent.BEFORE_SAVE`.
4
+ For each event, serialises the live :class:`work_engine.state.WorkState`
5
+ through ``state.to_dict`` and re-validates via ``state.from_dict``. A
6
+ :class:`work_engine.state.SchemaError` from either side is reported as
7
+ a :class:`HookError` so the runner warns and continues — observability,
8
+ not a gate.
9
+
10
+ The hook only sees the post-migration v1 shape. ``_load_or_build`` owns
11
+ v0 → v1 migration; this hook is the safety net catching any drift the
12
+ migration or a hand-edited state file might have produced.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from ..context import HookContext
17
+ from ..events import HookEvent
18
+ from ..exceptions import HookError
19
+ from ..registry import HookRegistry
20
+
21
+
22
+ class StateShapeValidationHook:
23
+ """Round-trips the loaded ``WorkState`` against the v1 schema."""
24
+
25
+ def register(self, registry: HookRegistry) -> None:
26
+ """Register on AFTER_LOAD and BEFORE_SAVE."""
27
+ registry.register(HookEvent.AFTER_LOAD, self._validate)
28
+ registry.register(HookEvent.BEFORE_SAVE, self._validate)
29
+
30
+ def _validate(self, ctx: HookContext) -> None:
31
+ work = ctx.work
32
+ if work is None:
33
+ # Should not happen on AFTER_LOAD/BEFORE_SAVE; treat as
34
+ # a hook-side bug rather than swallow silently.
35
+ raise HookError(
36
+ "state-shape validation: HookContext.work is None at "
37
+ f"event for state_file={ctx.state_file}",
38
+ )
39
+
40
+ # Local imports keep the hook module import-light and avoid a
41
+ # cycle with ``work_engine.state`` at package import time.
42
+ from ...state import SchemaError, from_dict, to_dict # noqa: PLC0415
43
+
44
+ try:
45
+ from_dict(to_dict(work))
46
+ except SchemaError as exc:
47
+ raise HookError(
48
+ f"state-shape validation failed: {exc}",
49
+ ) from exc
50
+
51
+
52
+ __all__ = ["StateShapeValidationHook"]
@@ -0,0 +1,84 @@
1
+ """``TraceHook`` — emit one stderr line per hook event.
2
+
3
+ Useful for debugging dispatch flow and Phase 5 chat-history wiring.
4
+ Registers on every :class:`HookEvent`; output goes to a configurable
5
+ stream (default ``sys.stderr``) so tests can capture it.
6
+
7
+ Pure observability — never mutates context, never halts. A misbehaving
8
+ sink (e.g. closed stream) raises :class:`HookError`, which the runner
9
+ swallows with a warning per the three-tier contract.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+ from typing import IO
15
+
16
+ from ..context import HookContext
17
+ from ..events import HookEvent
18
+ from ..exceptions import HookError
19
+ from ..registry import HookRegistry
20
+
21
+
22
+ class TraceHook:
23
+ """Stderr-trace hook for every lifecycle event.
24
+
25
+ Parameters
26
+ ----------
27
+ stream:
28
+ Output stream. Defaults to ``sys.stderr``. Tests pass an
29
+ ``io.StringIO`` to capture the trace without touching stderr.
30
+ prefix:
31
+ Line prefix. Defaults to ``"[hook]"`` for visual separation
32
+ from regular CLI output.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ stream: IO[str] | None = None,
38
+ prefix: str = "[hook]",
39
+ ) -> None:
40
+ self._stream = stream if stream is not None else sys.stderr
41
+ self._prefix = prefix
42
+
43
+ def register(self, registry: HookRegistry) -> None:
44
+ """Register the trace callback for every :class:`HookEvent`."""
45
+ for event in HookEvent:
46
+ registry.register(event, self._make_callback(event))
47
+
48
+ def _make_callback(self, event: HookEvent):
49
+ def _cb(ctx: HookContext) -> None:
50
+ try:
51
+ line = self._format(event, ctx)
52
+ self._stream.write(line + "\n")
53
+ self._stream.flush()
54
+ except (OSError, ValueError) as exc:
55
+ raise HookError(f"trace stream unavailable: {exc}") from exc
56
+
57
+ return _cb
58
+
59
+ def _format(self, event: HookEvent, ctx: HookContext) -> str:
60
+ """Build a one-line trace record.
61
+
62
+ Format: ``[hook] event=<name> step=<step> set=<set> outcome=<o>``.
63
+ Missing fields are skipped so the line stays short on events that
64
+ only carry a subset of the context.
65
+ """
66
+ parts: list[str] = [self._prefix, f"event={event.value}"]
67
+ if ctx.step_name:
68
+ parts.append(f"step={ctx.step_name}")
69
+ if ctx.set_name:
70
+ parts.append(f"set={ctx.set_name}")
71
+ if ctx.result is not None:
72
+ outcome = getattr(ctx.result, "outcome", None)
73
+ if outcome is not None:
74
+ parts.append(f"outcome={getattr(outcome, 'value', outcome)}")
75
+ if ctx.final is not None:
76
+ parts.append(f"final={getattr(ctx.final, 'value', ctx.final)}")
77
+ if ctx.halting:
78
+ parts.append(f"halting={ctx.halting}")
79
+ if ctx.exception is not None:
80
+ parts.append(f"exception={type(ctx.exception).__name__}")
81
+ return " ".join(parts)
82
+
83
+
84
+ __all__ = ["TraceHook"]
@@ -0,0 +1,66 @@
1
+ """``HookContext`` — payload carried into every hook callback.
2
+
3
+ One dataclass for both layers. Most fields are ``None`` for any given
4
+ event; the per-event subset is documented below and locked by the
5
+ roadmap's hook event surface table. Hooks must tolerate missing fields
6
+ gracefully — accessing a field that is ``None`` for the current event
7
+ is a hook bug, not an engine bug.
8
+
9
+ Per-event subset (mirrors the roadmap):
10
+
11
+ Dispatcher layer (``delivery`` is set; ``work`` is ``None``):
12
+ - ``before_step`` → ``step_name``, ``delivery``
13
+ - ``after_step`` → ``step_name``, ``delivery``, ``result``
14
+ - ``on_halt`` → ``step_name``, ``delivery``, ``result``
15
+ - ``on_error`` → ``step_name``, ``delivery``, ``exception``
16
+
17
+ CLI layer (``work`` is set; ``delivery`` may be set after load):
18
+ - ``before_load`` → ``state_file``, ``args``
19
+ - ``after_load`` → ``state_file``, ``work``, ``fmt``
20
+ - ``before_dispatch`` → ``work``, ``delivery``, ``set_name``
21
+ - ``after_dispatch`` → ``work``, ``delivery``, ``final``,
22
+ ``halting``
23
+ - ``before_save`` → ``work``, ``delivery``, ``fmt``
24
+ - ``after_save`` → ``work``, ``state_file``, ``fmt``
25
+ """
26
+ from __future__ import annotations
27
+
28
+ from dataclasses import dataclass, field
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+
33
+ @dataclass
34
+ class HookContext:
35
+ """Per-event payload passed to every hook callback.
36
+
37
+ Fields are intentionally optional — the runner does not validate
38
+ which ones are populated for a given event. The contract is
39
+ enforced by the call sites in ``dispatcher.py`` and ``cli.py``,
40
+ not by the dataclass.
41
+
42
+ ``extra`` exists as an escape hatch for hook-specific state that
43
+ does not warrant a dedicated field. Use sparingly; if a piece of
44
+ state is read by more than one hook, promote it to a real field.
45
+ """
46
+
47
+ # Dispatcher-layer refs.
48
+ step_name: str | None = None
49
+ delivery: Any = None # DeliveryState — typed Any to avoid an import cycle.
50
+ result: Any = None # StepResult
51
+ exception: BaseException | None = None
52
+
53
+ # CLI-layer refs.
54
+ work: Any = None # WorkState — typed Any to avoid an import cycle.
55
+ state_file: Path | None = None
56
+ fmt: str | None = None
57
+ set_name: str | None = None
58
+ final: Any = None # Outcome
59
+ halting: str | None = None
60
+ args: Any = None # argparse.Namespace
61
+
62
+ # Escape hatch for hook-specific state.
63
+ extra: dict[str, Any] = field(default_factory=dict)
64
+
65
+
66
+ __all__ = ["HookContext"]
@@ -0,0 +1,44 @@
1
+ """Hook event surface for ``work_engine``.
2
+
3
+ Ten events split across two layers per
4
+ ``agents/roadmaps/road-to-work-engine-hooks.md`` (locked).
5
+
6
+ Dispatcher-layer events fire from inside ``dispatcher.dispatch()`` and
7
+ operate on ``DeliveryState`` (legacy, internal). CLI-layer events fire
8
+ from ``cli.main()`` and operate on ``WorkState`` (v1 envelope) plus
9
+ auxiliary refs (``state_file``, ``fmt``, ``args``). The split is
10
+ deliberate — see the ``Hook event surface (locked)`` section of the
11
+ roadmap for the per-event context payloads.
12
+
13
+ Adding events is a roadmap-level decision: hook consumers depend on
14
+ the surface staying stable, and an enum makes accidental string typos
15
+ fail at import time.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from enum import Enum
20
+
21
+
22
+ class HookEvent(str, Enum):
23
+ """Lifecycle events emitted by the work engine.
24
+
25
+ Subclassing ``str`` keeps round-trips trivial for telemetry and
26
+ JSON tracing — the value is the event name verbatim.
27
+ """
28
+
29
+ # Dispatcher layer (DeliveryState).
30
+ BEFORE_STEP = "before_step"
31
+ AFTER_STEP = "after_step"
32
+ ON_HALT = "on_halt"
33
+ ON_ERROR = "on_error"
34
+
35
+ # CLI layer (WorkState).
36
+ BEFORE_LOAD = "before_load"
37
+ AFTER_LOAD = "after_load"
38
+ BEFORE_DISPATCH = "before_dispatch"
39
+ AFTER_DISPATCH = "after_dispatch"
40
+ BEFORE_SAVE = "before_save"
41
+ AFTER_SAVE = "after_save"
42
+
43
+
44
+ __all__ = ["HookEvent"]
@@ -0,0 +1,79 @@
1
+ """Hook control-flow signals.
2
+
3
+ Three-tier error contract (locked by roadmap P1):
4
+
5
+ - ``HookError`` — non-fatal. Hook implementation failed; the runner
6
+ catches it, warns via ``warnings.warn``, and continues with the next
7
+ callback for the same event. Work proceeds.
8
+ - ``HookHalt`` — fatal-controlled. Hook demands a clean stop (canonical
9
+ example: chat-history ``turn-check`` foreign session). The runner
10
+ catches it and **returns** it to the caller, who decides how to
11
+ surface it (engine halt, CLI exit code 2 + readable surface). Not
12
+ re-raised through the dispatch loop.
13
+ - any other ``Exception`` — fatal-uncontrolled. Treated as a bug in the
14
+ hook. The runner lets it propagate verbatim; dispatch unwinds.
15
+
16
+ Both signals share a private ``_HookSignal`` base so the runner can
17
+ distinguish hook-originated control flow from genuine bugs without
18
+ catching ``BaseException``.
19
+ """
20
+ from __future__ import annotations
21
+
22
+
23
+ class _HookSignal(Exception):
24
+ """Internal marker for hook-originated control flow.
25
+
26
+ Not part of the public API. The runner uses ``isinstance`` checks
27
+ against the concrete subclasses below; the base exists only so a
28
+ single ``except _HookSignal`` would cover both signals if a future
29
+ refactor needs it.
30
+ """
31
+
32
+
33
+ class HookError(_HookSignal):
34
+ """Non-fatal hook failure.
35
+
36
+ Raised (or ``warn``-equivalent — both forms work) when a hook
37
+ callback fails in a way the *engine* should ignore. The runner
38
+ catches it, emits a ``warnings.warn`` with the message, and moves
39
+ on to the next callback registered for the event.
40
+
41
+ Example:
42
+ ``raise HookError("trace sink unavailable: connection refused")``
43
+
44
+ Use this for transient or non-critical hook failures (telemetry
45
+ sinks, optional reporters). Do **not** use it to signal "stop the
46
+ engine" — that is what :class:`HookHalt` is for.
47
+ """
48
+
49
+
50
+ class HookHalt(_HookSignal):
51
+ """Fatal-controlled stop requested by a hook.
52
+
53
+ Hooks raise this when execution must not continue (e.g. chat-history
54
+ ``turn-check`` returns ``foreign``: a different session owns the
55
+ log, work cannot safely proceed). The runner catches it and returns
56
+ it to the caller; the caller turns it into the appropriate halt
57
+ surface:
58
+
59
+ - Dispatcher layer → ``Outcome.BLOCKED`` with ``state.questions``
60
+ populated from ``surface``.
61
+ - CLI layer → exit code 2, ``surface`` printed to stderr, no state
62
+ saved unless the halt fires after ``_save()``.
63
+
64
+ ``surface`` is a list of pre-formatted numbered options per the
65
+ ``user-interaction`` rule (one entry per line). Callers must not
66
+ reformat — surface is rendered verbatim.
67
+
68
+ ``reason`` is a short machine-readable code (e.g. ``"foreign"``,
69
+ ``"missing"``, ``"validation_failed"``) for logging and tests; it
70
+ is not shown to the user.
71
+ """
72
+
73
+ def __init__(self, reason: str, surface: list[str] | None = None) -> None:
74
+ super().__init__(reason)
75
+ self.reason = reason
76
+ self.surface: list[str] = list(surface or [])
77
+
78
+
79
+ __all__ = ["HookError", "HookHalt"]
@@ -0,0 +1,60 @@
1
+ """``HookRegistry`` — insertion-ordered map from event to callbacks.
2
+
3
+ Phase 1 ships insertion-ordered registration only. If a real ordering
4
+ need surfaces later (e.g. trace must fire before mutation hooks), add
5
+ a priority field as a follow-up — do not pre-build it (per Notes
6
+ section of the roadmap).
7
+
8
+ The registry is a plain container. It does not invoke callbacks, does
9
+ not catch exceptions, and does not know about the error contract;
10
+ that responsibility lives in :class:`HookRunner`.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Callable
15
+ from typing import Iterable
16
+
17
+ from .context import HookContext
18
+ from .events import HookEvent
19
+
20
+ HookCallback = Callable[[HookContext], None]
21
+ """A hook callback. Returns ``None`` on success, raises ``HookError``
22
+ or ``HookHalt`` to signal control flow per ``exceptions.py``."""
23
+
24
+
25
+ class HookRegistry:
26
+ """Insertion-ordered registry of hook callbacks per event.
27
+
28
+ Single instance per CLI invocation. Built once in ``cli.main()``
29
+ and shared with ``dispatch()`` so dispatcher events and CLI events
30
+ are routed through the same callback set.
31
+ """
32
+
33
+ def __init__(self) -> None:
34
+ self._hooks: dict[HookEvent, list[HookCallback]] = {}
35
+
36
+ def register(self, event: HookEvent, callback: HookCallback) -> None:
37
+ """Register ``callback`` for ``event``.
38
+
39
+ Multiple callbacks for the same event are allowed; they fire
40
+ in registration order.
41
+ """
42
+ self._hooks.setdefault(event, []).append(callback)
43
+
44
+ def for_event(self, event: HookEvent) -> tuple[HookCallback, ...]:
45
+ """Return callbacks registered for ``event`` in insertion order.
46
+
47
+ Returns an empty tuple when no callbacks are registered — the
48
+ runner uses this to short-circuit a no-op fast path.
49
+ """
50
+ return tuple(self._hooks.get(event, ()))
51
+
52
+ def events(self) -> Iterable[HookEvent]:
53
+ """Iterate over events that have at least one callback.
54
+
55
+ Diagnostics-only; not used on the hot path.
56
+ """
57
+ return self._hooks.keys()
58
+
59
+
60
+ __all__ = ["HookCallback", "HookRegistry"]
@@ -0,0 +1,73 @@
1
+ """``HookRunner`` — single emit point for hook callbacks.
2
+
3
+ Implements the three-tier error contract documented in
4
+ ``exceptions.py``:
5
+
6
+ - ``HookError`` from a callback → caught, ``warnings.warn`` is emitted,
7
+ the runner continues with the next callback for the same event.
8
+ Returns ``None`` once the event is fully drained.
9
+ - ``HookHalt`` from a callback → caught, **returned** to the caller
10
+ with no further callbacks invoked for this event. The caller
11
+ decides how to surface the halt (engine halt, CLI exit 2). Never
12
+ re-raised through the dispatch loop.
13
+ - any other ``Exception`` → propagates unchanged. Treated as a hook
14
+ bug; dispatch unwinds.
15
+
16
+ The runner is intentionally tiny. Behavior changes belong here so
17
+ ``dispatcher.py`` and ``cli.py`` stay free of hook bookkeeping.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import warnings
22
+
23
+ from .context import HookContext
24
+ from .events import HookEvent
25
+ from .exceptions import HookError, HookHalt
26
+ from .registry import HookRegistry
27
+
28
+
29
+ class HookRunner:
30
+ """Emit hook events through a :class:`HookRegistry`.
31
+
32
+ Construct once per CLI invocation, share between the CLI and the
33
+ dispatcher. ``emit`` is the only public method on the hot path.
34
+ """
35
+
36
+ def __init__(self, registry: HookRegistry | None = None) -> None:
37
+ self._registry = registry if registry is not None else HookRegistry()
38
+
39
+ @property
40
+ def registry(self) -> HookRegistry:
41
+ """Return the underlying registry.
42
+
43
+ Exposed so callers can register additional hooks after
44
+ construction (e.g. in tests). Not used on the hot path.
45
+ """
46
+ return self._registry
47
+
48
+ def emit(self, event: HookEvent, ctx: HookContext) -> HookHalt | None:
49
+ """Fire all callbacks registered for ``event``.
50
+
51
+ Returns ``None`` when every callback completed (with or without
52
+ a swallowed :class:`HookError`). Returns the first
53
+ :class:`HookHalt` raised, after which no further callbacks are
54
+ invoked for this event. Any other exception propagates.
55
+ """
56
+ callbacks = self._registry.for_event(event)
57
+ if not callbacks:
58
+ return None
59
+ for callback in callbacks:
60
+ try:
61
+ callback(ctx)
62
+ except HookHalt as halt:
63
+ return halt
64
+ except HookError as err:
65
+ warnings.warn(
66
+ f"hook {event.value} raised HookError: {err}",
67
+ stacklevel=2,
68
+ )
69
+ continue
70
+ return None
71
+
72
+
73
+ __all__ = ["HookRunner"]