@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,592 @@
1
+ """Command-line entry point for ``/implement-ticket``.
2
+
3
+ Minimal Option-A transport: the script loads a persisted state file,
4
+ runs the dispatcher once, writes the updated state back, and prints
5
+ either the delivery report (on SUCCESS) or the halt surface —
6
+ directive plus numbered questions — on BLOCKED/PARTIAL.
7
+
8
+ The script never edits code, runs tests, or opens pull requests.
9
+ All of that is delegated to the agent via ``@agent-directive:``
10
+ markers per
11
+ ``agents/contexts/implement-ticket-flow.md#agent-directives``. The
12
+ agent executes the directive, writes the resulting slice back to
13
+ the state file, and re-invokes this script to resume.
14
+
15
+ Wire format (R1 P4 S1, Option A2): the CLI accepts both the legacy
16
+ v0 wire format (``{"ticket": …, "persona": …}``) and the v1 schema
17
+ (``{"version": 1, "input": {"kind": "ticket", "data": …}}``). Loaded
18
+ state is wrapped in :class:`work_engine.state.WorkState` for the
19
+ boundary; before dispatch it is projected into a ``DeliveryState``
20
+ the step handlers understand. After dispatch the mutations are
21
+ mirrored back, and the file is rewritten in the **same** wire format
22
+ it was loaded with — Goldens captured against v0 stay v0 byte-for-
23
+ byte, while flows that already store v1 round-trip as v1. The
24
+ dispatcher selects the directive set via
25
+ :func:`work_engine.dispatcher.select_directive_set`, defaulting to
26
+ ``"backend"`` so v0 callers behave exactly as they did before R1
27
+ Phase 4.
28
+
29
+ Exit codes:
30
+
31
+ - ``0`` — flow reached SUCCESS; ``state.report`` printed.
32
+ - ``1`` — flow halted BLOCKED; halt surface printed on stdout, the
33
+ state file carries the updated ``outcomes`` and ``questions`` so
34
+ the agent can resume.
35
+ - ``2`` — argument or I/O error (ticket file missing, JSON parse
36
+ failure, etc.). The state file is *not* written in this case.
37
+ """
38
+ from __future__ import annotations
39
+
40
+ import argparse
41
+ import json
42
+ import sys
43
+ from pathlib import Path
44
+ from typing import Any, Sequence
45
+
46
+ from . import state as _state_module
47
+ from .delivery_state import DeliveryState, Outcome
48
+ from .dispatcher import (
49
+ assert_kind_supported,
50
+ dispatch,
51
+ load_directive_set,
52
+ select_directive_set,
53
+ )
54
+ from .hooks import HookContext, HookEvent, HookHalt, HookRegistry, HookRunner
55
+ from .hooks.builtin import (
56
+ ChatHistoryAppendHook,
57
+ ChatHistoryHaltAppendHook,
58
+ ChatHistoryHeartbeatHook,
59
+ ChatHistoryTurnCheckHook,
60
+ DirectiveSetGuardHook,
61
+ HaltSurfaceAuditHook,
62
+ StateShapeValidationHook,
63
+ TraceHook,
64
+ )
65
+ from .hooks.settings import HookSettings, load_hook_settings
66
+ from .intent import populate_routing
67
+ from .migration.v0_to_v1 import migrate_payload
68
+ from .resolvers.diff import DiffResolverError, build_envelope as _build_diff_envelope
69
+ from .resolvers.file import FileResolverError, build_envelope as _build_file_envelope
70
+ from .resolvers.prompt import PromptResolverError, build_envelope as _build_prompt_envelope
71
+ from .state import Input, SchemaError, WorkState
72
+
73
+ DEFAULT_STATE_FILE = Path(".implement-ticket-state.json")
74
+ """State file used when ``--state-file`` is not passed."""
75
+
76
+ _FMT_V0 = "v0"
77
+ _FMT_V1 = "v1"
78
+ """Wire-format markers carried alongside the loaded :class:`WorkState`.
79
+
80
+ Format-preserving roundtrip: ``_load`` records which shape it parsed,
81
+ ``_save`` rewrites in that same shape. v0 in → v0 out (Goldens stay
82
+ byte-equal); v1 in → v1 out (future flows produced by the migration
83
+ tool or a fresh v1 init keep their envelope fields)."""
84
+
85
+
86
+ def main(argv: Sequence[str] | None = None) -> int:
87
+ """Run one dispatch cycle against the persisted state.
88
+
89
+ ``argv`` is taken as-is; pass ``None`` to fall back to
90
+ ``sys.argv[1:]`` (the usual entry-point contract).
91
+ """
92
+ parser = _build_parser()
93
+ args = parser.parse_args(argv)
94
+ state_file: Path = args.state_file
95
+
96
+ runner = HookRunner(_build_hook_registry(args))
97
+
98
+ halt = runner.emit(
99
+ HookEvent.BEFORE_LOAD,
100
+ HookContext(state_file=state_file, args=args),
101
+ )
102
+ if halt is not None:
103
+ return _emit_halt(halt)
104
+
105
+ try:
106
+ work, fmt = _load_or_build(state_file, args)
107
+ except _CLIError as exc:
108
+ print(f"error: {exc}", file=sys.stderr)
109
+ return 2
110
+
111
+ halt = runner.emit(
112
+ HookEvent.AFTER_LOAD,
113
+ HookContext(state_file=state_file, work=work, fmt=fmt, args=args),
114
+ )
115
+ if halt is not None:
116
+ return _emit_halt(halt)
117
+
118
+ try:
119
+ set_name = select_directive_set(work)
120
+ assert_kind_supported(work.input.kind, set_name)
121
+ steps = load_directive_set(set_name)
122
+ except (ValueError, NotImplementedError) as exc:
123
+ print(f"error: {exc}", file=sys.stderr)
124
+ return 2
125
+
126
+ delivery = _to_delivery(work)
127
+
128
+ halt = runner.emit(
129
+ HookEvent.BEFORE_DISPATCH,
130
+ HookContext(work=work, delivery=delivery, set_name=set_name, args=args),
131
+ )
132
+ if halt is not None:
133
+ return _emit_halt(halt)
134
+
135
+ final, halting = dispatch(delivery, steps, hooks=runner)
136
+
137
+ halt = runner.emit(
138
+ HookEvent.AFTER_DISPATCH,
139
+ HookContext(
140
+ work=work,
141
+ delivery=delivery,
142
+ final=final,
143
+ halting=halting,
144
+ args=args,
145
+ ),
146
+ )
147
+ if halt is not None:
148
+ return _emit_halt(halt)
149
+
150
+ _sync_back(work, delivery)
151
+
152
+ halt = runner.emit(
153
+ HookEvent.BEFORE_SAVE,
154
+ HookContext(work=work, delivery=delivery, fmt=fmt, args=args),
155
+ )
156
+ if halt is not None:
157
+ return _emit_halt(halt)
158
+
159
+ _save(state_file, work, fmt)
160
+
161
+ halt = runner.emit(
162
+ HookEvent.AFTER_SAVE,
163
+ HookContext(work=work, state_file=state_file, fmt=fmt, args=args),
164
+ )
165
+ if halt is not None:
166
+ # State is already on disk; exit 2 still per the P3 branch table.
167
+ return _emit_halt(halt)
168
+
169
+ _emit(work, final, halting)
170
+ return 0 if final is Outcome.SUCCESS else 1
171
+
172
+
173
+ def _build_hook_registry(args: argparse.Namespace) -> HookRegistry:
174
+ """Build the CLI-side :class:`HookRegistry` for one ``main()`` run.
175
+
176
+ Reads ``hooks.*`` from ``.agent-settings.yml`` and registers the
177
+ enabled hooks. The master switch ``hooks.enabled`` defaults to
178
+ ``False`` when the block (or the file) is missing — the registry
179
+ stays empty and golden replay flows are byte-stable.
180
+
181
+ ``--no-hooks`` on the CLI forces an empty registry regardless of
182
+ settings, which is the explicit escape hatch golden-replay test
183
+ harnesses can use.
184
+ """
185
+ registry = HookRegistry()
186
+ if getattr(args, "no_hooks", False):
187
+ return registry
188
+
189
+ settings_path = getattr(args, "hooks_config", None)
190
+ settings = load_hook_settings(settings_path)
191
+ if not settings.enabled:
192
+ return registry
193
+
194
+ if settings.trace:
195
+ TraceHook().register(registry)
196
+ if settings.halt_surface_audit:
197
+ HaltSurfaceAuditHook().register(registry)
198
+ if settings.state_shape_validation:
199
+ StateShapeValidationHook().register(registry)
200
+ if settings.directive_set_guard:
201
+ DirectiveSetGuardHook().register(registry)
202
+ if settings.chat_history_enabled:
203
+ _register_chat_history_hooks(registry, settings)
204
+
205
+ return registry
206
+
207
+
208
+ def _register_chat_history_hooks(
209
+ registry: HookRegistry, settings: HookSettings,
210
+ ) -> None:
211
+ """Register the four chat-history hooks bound to the configured script."""
212
+ script = Path(settings.chat_history_script)
213
+ ChatHistoryTurnCheckHook(script).register(registry)
214
+ ChatHistoryAppendHook(script).register(registry)
215
+ ChatHistoryHaltAppendHook(script).register(registry)
216
+ ChatHistoryHeartbeatHook(script).register(registry)
217
+
218
+
219
+ def _emit_halt(halt: HookHalt) -> int:
220
+ """Render a :class:`HookHalt` surface to stderr and return exit 2.
221
+
222
+ Per the P3 halt branch table, every CLI-layer halt yields exit code
223
+ ``2`` regardless of which event fired it. State persistence is
224
+ governed by *where* in ``main`` the halt is detected: the call site
225
+ decides whether ``_save`` already ran. This helper is the single
226
+ place that formats the surface so the wire output stays consistent.
227
+ """
228
+ if halt.surface:
229
+ for line in halt.surface:
230
+ print(line, file=sys.stderr)
231
+ else:
232
+ print(f"halt: {halt.reason}", file=sys.stderr)
233
+ return 2
234
+
235
+
236
+ def _build_parser() -> argparse.ArgumentParser:
237
+ parser = argparse.ArgumentParser(
238
+ prog="implement-ticket",
239
+ description="Run one dispatch cycle of the /implement-ticket flow.",
240
+ )
241
+ parser.add_argument(
242
+ "--state-file",
243
+ type=Path,
244
+ default=DEFAULT_STATE_FILE,
245
+ help=f"Path to persisted state JSON (default: {DEFAULT_STATE_FILE}).",
246
+ )
247
+ parser.add_argument(
248
+ "--ticket-file",
249
+ type=Path,
250
+ default=None,
251
+ help="JSON file carrying the ticket payload; used only when the "
252
+ "state file does not exist yet.",
253
+ )
254
+ parser.add_argument(
255
+ "--prompt-file",
256
+ type=Path,
257
+ default=None,
258
+ help="Plain-text file carrying the raw user prompt; builds an "
259
+ "input.kind='prompt' envelope. Mutually exclusive with "
260
+ "--ticket-file. Used only when the state file does not exist yet.",
261
+ )
262
+ parser.add_argument(
263
+ "--diff-file",
264
+ type=Path,
265
+ default=None,
266
+ help="Plain-text file carrying a unified diff payload; builds an "
267
+ "input.kind='diff' envelope routed through the UI-improve "
268
+ "directive set. Mutually exclusive with --ticket-file / "
269
+ "--prompt-file / --file-file. Used only when the state file does "
270
+ "not exist yet.",
271
+ )
272
+ parser.add_argument(
273
+ "--file-file",
274
+ type=Path,
275
+ default=None,
276
+ help="Plain-text file carrying a single path reference (one line); "
277
+ "builds an input.kind='file' envelope routed through the UI-improve "
278
+ "directive set. Mutually exclusive with --ticket-file / "
279
+ "--prompt-file / --diff-file. Used only when the state file does "
280
+ "not exist yet.",
281
+ )
282
+ parser.add_argument(
283
+ "--persona",
284
+ type=str,
285
+ default=None,
286
+ help="Persona name (senior-engineer | qa | advisory). Only honoured "
287
+ "when the state file does not exist yet; ignored on resume so a "
288
+ "mid-flight persona switch cannot silently change behaviour.",
289
+ )
290
+ parser.add_argument(
291
+ "--no-hooks",
292
+ action="store_true",
293
+ default=False,
294
+ help="Disable every lifecycle hook for this run. Use in golden-"
295
+ "replay test harnesses so a future settings change cannot "
296
+ "silently invalidate captured outputs.",
297
+ )
298
+ parser.add_argument(
299
+ "--hooks-config",
300
+ type=Path,
301
+ default=None,
302
+ help="Override the path to the agent-settings file used to resolve "
303
+ "the hooks.* block. Defaults to ./.agent-settings.yml.",
304
+ )
305
+ return parser
306
+
307
+
308
+ def _load_or_build(
309
+ state_file: Path,
310
+ args: argparse.Namespace,
311
+ ) -> tuple[WorkState, str]:
312
+ """Return the WorkState to dispatch against plus its wire format.
313
+
314
+ Either loaded from ``state_file`` (format-preserving) or freshly
315
+ built from ``--ticket-file`` (R1), ``--prompt-file`` (R2),
316
+ ``--diff-file`` (R3) or ``--file-file`` (R3). Fresh ticket files
317
+ default to v0 wire format so that newly captured Goldens stay
318
+ byte-equal with the pre-Phase-4 baseline; the prompt / diff / file
319
+ paths emit v1 directly (v0 has no envelope concept for these
320
+ kinds). v1 round-trips for state files already on disk in v1 shape.
321
+ """
322
+ if state_file.exists():
323
+ return _load(state_file)
324
+ inputs = [
325
+ ("--ticket-file", args.ticket_file),
326
+ ("--prompt-file", args.prompt_file),
327
+ ("--diff-file", args.diff_file),
328
+ ("--file-file", args.file_file),
329
+ ]
330
+ supplied = [name for name, value in inputs if value is not None]
331
+ if len(supplied) > 1:
332
+ raise _CLIError(
333
+ f"{', '.join(supplied)} are mutually exclusive; pass exactly "
334
+ "one when building an initial state.",
335
+ )
336
+ if not supplied:
337
+ raise _CLIError(
338
+ f"No state file at {state_file} and no --ticket-file, "
339
+ "--prompt-file, --diff-file, or --file-file given; cannot "
340
+ "build an initial state.",
341
+ )
342
+ if args.prompt_file is not None:
343
+ return _build_from_prompt_file(args), _FMT_V1
344
+ if args.diff_file is not None:
345
+ return _build_from_diff_file(args), _FMT_V1
346
+ if args.file_file is not None:
347
+ return _build_from_file_file(args), _FMT_V1
348
+ ticket = _read_json(args.ticket_file)
349
+ if not isinstance(ticket, dict):
350
+ raise _CLIError(
351
+ f"--ticket-file must carry a JSON object; got {type(ticket).__name__}.",
352
+ )
353
+ work = WorkState(input=Input(kind="ticket", data=ticket))
354
+ if args.persona:
355
+ work.persona = args.persona
356
+ populate_routing(work)
357
+ return work, _FMT_V0
358
+
359
+
360
+ def _build_from_prompt_file(args: argparse.Namespace) -> WorkState:
361
+ """Read ``--prompt-file`` as raw text and wrap it in a prompt envelope.
362
+
363
+ The file is read verbatim (UTF-8) and handed to the prompt resolver,
364
+ which validates non-emptiness and returns the canonical
365
+ ``Input(kind="prompt", data={raw, reconstructed_ac, assumptions})``
366
+ envelope. Persona is honoured the same way as the ticket path.
367
+ """
368
+ try:
369
+ raw = args.prompt_file.read_text(encoding="utf-8")
370
+ except OSError as exc:
371
+ raise _CLIError(f"Cannot read {args.prompt_file}: {exc}") from exc
372
+ try:
373
+ envelope = _build_prompt_envelope(raw)
374
+ except PromptResolverError as exc:
375
+ raise _CLIError(f"--prompt-file is not a valid prompt: {exc}") from exc
376
+ work = WorkState(input=envelope)
377
+ if args.persona:
378
+ work.persona = args.persona
379
+ populate_routing(work)
380
+ return work
381
+
382
+
383
+ def _build_from_diff_file(args: argparse.Namespace) -> WorkState:
384
+ """Read ``--diff-file`` as raw text and wrap it in a diff envelope.
385
+
386
+ The file is read verbatim (UTF-8) and handed to the diff resolver,
387
+ which validates the unified-diff header heuristic and returns the
388
+ canonical
389
+ ``Input(kind="diff", data={raw, reconstructed_ac, assumptions})``
390
+ envelope. ``populate_routing`` then routes the envelope to the
391
+ UI-improve directive set without running the prose classifier — see
392
+ :mod:`work_engine.intent.classify` for the routing contract.
393
+ """
394
+ try:
395
+ raw = args.diff_file.read_text(encoding="utf-8")
396
+ except OSError as exc:
397
+ raise _CLIError(f"Cannot read {args.diff_file}: {exc}") from exc
398
+ try:
399
+ envelope = _build_diff_envelope(raw)
400
+ except DiffResolverError as exc:
401
+ raise _CLIError(f"--diff-file is not a valid diff: {exc}") from exc
402
+ work = WorkState(input=envelope)
403
+ if args.persona:
404
+ work.persona = args.persona
405
+ populate_routing(work)
406
+ return work
407
+
408
+
409
+ def _build_from_file_file(args: argparse.Namespace) -> WorkState:
410
+ """Read ``--file-file`` as a single-line path and wrap it in a file envelope.
411
+
412
+ The file is read verbatim (UTF-8); the first non-empty line is taken
413
+ as the path reference and handed to the file resolver, which
414
+ validates path shape (non-empty, NUL-free, not a URL) and returns
415
+ the canonical
416
+ ``Input(kind="file", data={path, reconstructed_ac, assumptions})``
417
+ envelope. Trailing whitespace and additional lines are ignored —
418
+ the resolver treats the file's content as the path itself, not as
419
+ structured payload.
420
+ """
421
+ try:
422
+ raw = args.file_file.read_text(encoding="utf-8")
423
+ except OSError as exc:
424
+ raise _CLIError(f"Cannot read {args.file_file}: {exc}") from exc
425
+ path = raw.strip().splitlines()[0] if raw.strip() else ""
426
+ try:
427
+ envelope = _build_file_envelope(path)
428
+ except FileResolverError as exc:
429
+ raise _CLIError(
430
+ f"--file-file does not carry a valid path: {exc}",
431
+ ) from exc
432
+ work = WorkState(input=envelope)
433
+ if args.persona:
434
+ work.persona = args.persona
435
+ populate_routing(work)
436
+ return work
437
+
438
+
439
+ def _load(state_file: Path) -> tuple[WorkState, str]:
440
+ """Load ``state_file`` and tag it with the wire format detected."""
441
+ data = _read_json(state_file)
442
+ if not isinstance(data, dict):
443
+ raise _CLIError(
444
+ f"State file {state_file} must carry a JSON object; "
445
+ f"got {type(data).__name__}.",
446
+ )
447
+
448
+ # v1 declares ``version``; v0 has none. Anything else is invalid.
449
+ if data.get("version") == _state_module.SCHEMA_VERSION:
450
+ try:
451
+ return _state_module.from_dict(data), _FMT_V1
452
+ except SchemaError as exc:
453
+ raise _CLIError(f"State file shape is invalid: {exc}") from exc
454
+ if "version" in data:
455
+ raise _CLIError(
456
+ f"State file shape is invalid: unsupported version "
457
+ f"{data.get('version')!r}; expected {_state_module.SCHEMA_VERSION}",
458
+ )
459
+ if "ticket" not in data:
460
+ raise _CLIError(
461
+ "State file shape is invalid: missing 'ticket' (v0) or "
462
+ "'version' (v1) — file is neither shape.",
463
+ )
464
+ try:
465
+ migrated = migrate_payload(data)
466
+ return _state_module.from_dict(migrated), _FMT_V0
467
+ except SchemaError as exc:
468
+ raise _CLIError(f"State file shape is invalid: {exc}") from exc
469
+
470
+
471
+ def _to_delivery(work: WorkState) -> DeliveryState:
472
+ """Project ``work`` into a ``DeliveryState`` for handler dispatch.
473
+
474
+ R1 P4 S1 (Option A2): handlers continue to consume ``DeliveryState``
475
+ with ``state.ticket``; the ``WorkState`` wrapper exists at the CLI
476
+ boundary so the dispatcher's directive-set selection has a v1
477
+ state object to read ``directive_set`` from. Mutable containers
478
+ (``memory``, ``changes``, ``outcomes``, ``questions``) are passed
479
+ by reference — in-place mutations land on both objects without an
480
+ explicit sync. Reassignments (``state.plan = …``, ``state.report
481
+ = …``) are mirrored back by :func:`_sync_back`.
482
+ """
483
+ return DeliveryState(
484
+ ticket=work.input.data,
485
+ persona=work.persona,
486
+ memory=work.memory,
487
+ plan=work.plan,
488
+ changes=work.changes,
489
+ tests=work.tests,
490
+ verify=work.verify,
491
+ outcomes=work.outcomes,
492
+ questions=work.questions,
493
+ report=work.report,
494
+ ui_audit=work.ui_audit,
495
+ ui_design=work.ui_design,
496
+ ui_review=work.ui_review,
497
+ ui_polish=work.ui_polish,
498
+ contract=work.contract,
499
+ stitch=work.stitch,
500
+ stack=work.stack,
501
+ )
502
+
503
+
504
+ def _sync_back(work: WorkState, delivery: DeliveryState) -> None:
505
+ """Mirror handler mutations from ``delivery`` back into ``work``.
506
+
507
+ Container fields are shared by reference (see :func:`_to_delivery`)
508
+ so the assignment is a no-op for those — we still mirror them
509
+ defensively to cover the case where a handler reassigned the
510
+ attribute (``state.memory = [new_list]``) instead of mutating in
511
+ place.
512
+ """
513
+ work.input.data = delivery.ticket
514
+ work.persona = delivery.persona
515
+ work.memory = delivery.memory
516
+ work.plan = delivery.plan
517
+ work.changes = delivery.changes
518
+ work.tests = delivery.tests
519
+ work.verify = delivery.verify
520
+ work.outcomes = delivery.outcomes
521
+ work.questions = delivery.questions
522
+ work.report = delivery.report
523
+ work.ui_audit = delivery.ui_audit
524
+ work.ui_design = delivery.ui_design
525
+ work.ui_review = delivery.ui_review
526
+ work.ui_polish = delivery.ui_polish
527
+ work.contract = delivery.contract
528
+ work.stitch = delivery.stitch
529
+ work.stack = delivery.stack
530
+
531
+
532
+ def _save(state_file: Path, work: WorkState, fmt: str) -> None:
533
+ """Persist ``work`` in the wire format it was loaded with.
534
+
535
+ v1 emits the canonical envelope via :func:`work_engine.state.to_dict`;
536
+ v0 emits the legacy flat shape that ``DeliveryState.asdict`` used
537
+ to produce, byte-identical to the pre-Phase-4 output so the
538
+ Golden Transcript replay stays green.
539
+ """
540
+ state_file.parent.mkdir(parents=True, exist_ok=True)
541
+ payload = _state_module.to_dict(work) if fmt == _FMT_V1 else _to_v0_dict(work)
542
+ state_file.write_text(
543
+ json.dumps(payload, indent=2, ensure_ascii=False) + "\n",
544
+ encoding="utf-8",
545
+ )
546
+
547
+
548
+ def _to_v0_dict(work: WorkState) -> dict[str, Any]:
549
+ """Serialise ``work`` in the legacy v0 wire format.
550
+
551
+ Field order matches ``DeliveryState`` declaration order so
552
+ pre-Phase-4 state files round-trip byte-equal.
553
+ """
554
+ return {
555
+ "ticket": work.input.data,
556
+ "persona": work.persona,
557
+ "memory": work.memory,
558
+ "plan": work.plan,
559
+ "changes": work.changes,
560
+ "tests": work.tests,
561
+ "verify": work.verify,
562
+ "outcomes": work.outcomes,
563
+ "questions": work.questions,
564
+ "report": work.report,
565
+ }
566
+
567
+
568
+ def _read_json(path: Path):
569
+ try:
570
+ raw = path.read_text(encoding="utf-8")
571
+ except OSError as exc:
572
+ raise _CLIError(f"Cannot read {path}: {exc}") from exc
573
+ try:
574
+ return json.loads(raw)
575
+ except json.JSONDecodeError as exc:
576
+ raise _CLIError(f"Invalid JSON in {path}: {exc}") from exc
577
+
578
+
579
+ def _emit(work: WorkState, final: Outcome, halting: str | None) -> None:
580
+ if final is Outcome.SUCCESS:
581
+ print(work.report)
582
+ return
583
+ print(f"[halt] outcome={final.value} step={halting or '(none)'}")
584
+ for line in work.questions:
585
+ print(line)
586
+
587
+
588
+ class _CLIError(Exception):
589
+ """Raised on configuration or I/O problems. Converted to exit code 2."""
590
+
591
+
592
+ __all__ = ["DEFAULT_STATE_FILE", "main"]
@@ -75,6 +75,13 @@ class DeliveryState:
75
75
  outcomes: dict[str, str] = field(default_factory=dict)
76
76
  questions: list[str] = field(default_factory=list)
77
77
  report: str = ""
78
+ ui_audit: dict[str, Any] | None = None
79
+ ui_design: dict[str, Any] | None = None
80
+ ui_review: dict[str, Any] | None = None
81
+ ui_polish: dict[str, Any] | None = None
82
+ contract: dict[str, Any] | None = None
83
+ stitch: dict[str, Any] | None = None
84
+ stack: dict[str, Any] | None = None
78
85
 
79
86
 
80
87
  Step = Callable[[DeliveryState], StepResult]
@@ -0,0 +1,33 @@
1
+ """Directive-set bundles consumed by the dispatcher.
2
+
3
+ A *directive set* is a coherent group of step handlers (refine,
4
+ memory, analyze, plan, implement, test, verify, report) tuned for a
5
+ particular kind of work — backend coding, UI work, mixed front+back
6
+ work, and so on. The dispatcher selects exactly one set per cycle
7
+ (see ``dispatcher.select_directive_set``) and walks its eight steps
8
+ in the canonical order.
9
+
10
+ Each set is a Python sub-package exposing a single function::
11
+
12
+ def get_steps() -> Mapping[str, Step]:
13
+ '''Return the {step_name: handler} mapping the dispatcher walks.'''
14
+
15
+ The mapping must cover every entry in :data:`dispatcher.STEP_ORDER`;
16
+ incomplete bundles raise ``KeyError`` at dispatch time.
17
+
18
+ Roadmap status (R1 Phase 4):
19
+
20
+ - ``backend`` — fully implemented; landing in Step 3 of this phase.
21
+ - ``ui`` — stub; lands in Roadmap 3 (``road-to-product-ui-track.md``).
22
+ - ``ui_trivial`` — stub; lands in Roadmap 3 V2.
23
+ - ``mixed`` — stub; lands in Roadmap 3.
24
+
25
+ The schema (``state.KNOWN_DIRECTIVE_SETS``) carries the *external*
26
+ names ``ui``, ``ui-trivial``, ``mixed``; the directory layout uses
27
+ underscores (``ui_trivial``) because Python packages cannot contain
28
+ hyphens. The dispatcher's loader is the single place that translates
29
+ between the two.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ __all__: list[str] = []