@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.
- package/.agent-src/commands/agent-handoff.md +3 -0
- package/.agent-src/commands/agent-status.md +3 -0
- package/.agent-src/commands/agents-audit.md +4 -0
- package/.agent-src/commands/agents-cleanup.md +6 -1
- package/.agent-src/commands/agents-prepare.md +3 -0
- package/.agent-src/commands/analyze-reference-repo.md +4 -0
- package/.agent-src/commands/bug-fix.md +5 -1
- package/.agent-src/commands/bug-investigate.md +4 -0
- package/.agent-src/commands/chat-history-checkpoint.md +126 -0
- package/.agent-src/commands/chat-history-clear.md +5 -0
- package/.agent-src/commands/chat-history-resume.md +5 -0
- package/.agent-src/commands/chat-history.md +5 -0
- package/.agent-src/commands/check-current-md.md +126 -0
- package/.agent-src/commands/commit-in-chunks.md +98 -0
- package/.agent-src/commands/commit.md +4 -0
- package/.agent-src/commands/compress.md +3 -0
- package/.agent-src/commands/context-create.md +4 -0
- package/.agent-src/commands/context-refactor.md +4 -0
- package/.agent-src/commands/copilot-agents-init.md +3 -0
- package/.agent-src/commands/copilot-agents-optimize.md +3 -0
- package/.agent-src/commands/create-pr-description.md +4 -0
- package/.agent-src/commands/create-pr.md +4 -0
- package/.agent-src/commands/do-and-judge.md +4 -1
- package/.agent-src/commands/do-in-steps.md +3 -0
- package/.agent-src/commands/e2e-heal.md +4 -0
- package/.agent-src/commands/e2e-plan.md +4 -0
- package/.agent-src/commands/estimate-ticket.md +4 -1
- package/.agent-src/commands/feature-dev.md +4 -0
- package/.agent-src/commands/feature-explore.md +4 -0
- package/.agent-src/commands/feature-plan.md +4 -0
- package/.agent-src/commands/feature-refactor.md +4 -0
- package/.agent-src/commands/feature-roadmap.md +6 -0
- package/.agent-src/commands/fix-ci.md +4 -0
- package/.agent-src/commands/fix-portability.md +3 -0
- package/.agent-src/commands/fix-pr-bot-comments.md +4 -0
- package/.agent-src/commands/fix-pr-comments.md +4 -0
- package/.agent-src/commands/fix-pr-developer-comments.md +4 -0
- package/.agent-src/commands/fix-references.md +3 -0
- package/.agent-src/commands/fix-seeder.md +4 -0
- package/.agent-src/commands/implement-ticket.md +39 -13
- package/.agent-src/commands/jira-ticket.md +4 -0
- package/.agent-src/commands/judge.md +3 -0
- package/.agent-src/commands/memory-add.md +5 -3
- package/.agent-src/commands/memory-full.md +5 -2
- package/.agent-src/commands/memory-promote.md +7 -6
- package/.agent-src/commands/mode.md +3 -0
- package/.agent-src/commands/module-create.md +4 -0
- package/.agent-src/commands/module-explore.md +4 -0
- package/.agent-src/commands/onboard.md +24 -0
- package/.agent-src/commands/optimize-agents.md +4 -0
- package/.agent-src/commands/optimize-augmentignore.md +3 -0
- package/.agent-src/commands/optimize-rtk-filters.md +3 -0
- package/.agent-src/commands/optimize-skills.md +4 -0
- package/.agent-src/commands/override-create.md +4 -0
- package/.agent-src/commands/override-manage.md +4 -0
- package/.agent-src/commands/package-reset.md +3 -0
- package/.agent-src/commands/package-test.md +3 -0
- package/.agent-src/commands/prepare-for-review.md +4 -0
- package/.agent-src/commands/project-analyze.md +4 -0
- package/.agent-src/commands/project-health.md +4 -0
- package/.agent-src/commands/propose-memory.md +6 -8
- package/.agent-src/commands/quality-fix.md +4 -0
- package/.agent-src/commands/refine-ticket.md +4 -1
- package/.agent-src/commands/review-changes.md +4 -0
- package/.agent-src/commands/review-routing.md +4 -0
- package/.agent-src/commands/roadmap-create.md +7 -0
- package/.agent-src/commands/roadmap-execute.md +12 -1
- package/.agent-src/commands/rule-compliance-audit.md +4 -0
- package/.agent-src/commands/set-cost-profile.md +3 -0
- package/.agent-src/commands/sync-agent-settings.md +3 -0
- package/.agent-src/commands/sync-gitignore.md +3 -0
- package/.agent-src/commands/tests-create.md +4 -0
- package/.agent-src/commands/tests-execute.md +4 -0
- package/.agent-src/commands/threat-model.md +4 -0
- package/.agent-src/commands/update-form-request-messages.md +4 -0
- package/.agent-src/commands/upstream-contribute.md +4 -0
- package/.agent-src/commands/work.md +161 -0
- package/.agent-src/guidelines/agent-infra/engineering-memory-data-format.md +2 -6
- package/.agent-src/guidelines/agent-infra/layered-settings.md +0 -1
- package/.agent-src/guidelines/agent-infra/memory-access.md +0 -7
- package/.agent-src/guidelines/agent-infra/role-contracts.md +2 -4
- package/.agent-src/guidelines/agent-infra/self-improvement-pipeline.md +0 -1
- package/.agent-src/guidelines/php/patterns/strategy.md +180 -2
- package/.agent-src/personas/README.md +0 -1
- package/.agent-src/rules/artifact-drafting-protocol.md +7 -2
- package/.agent-src/rules/artifact-engagement-recording.md +133 -0
- package/.agent-src/rules/ask-when-uncertain.md +18 -13
- package/.agent-src/rules/augment-portability.md +8 -0
- package/.agent-src/rules/autonomous-execution.md +158 -0
- package/.agent-src/rules/chat-history.md +147 -118
- package/.agent-src/rules/cli-output-handling.md +26 -3
- package/.agent-src/rules/command-suggestion.md +133 -0
- package/.agent-src/rules/commit-policy.md +99 -0
- package/.agent-src/rules/direct-answers.md +114 -0
- package/.agent-src/rules/docs-sync.md +36 -0
- package/.agent-src/rules/downstream-changes.md +10 -9
- package/.agent-src/rules/improve-before-implement.md +9 -6
- package/.agent-src/rules/language-and-tone.md +81 -6
- package/.agent-src/rules/non-destructive-by-default.md +117 -0
- package/.agent-src/rules/package-ci-checks.md +4 -0
- package/.agent-src/rules/preservation-guard.md +20 -0
- package/.agent-src/rules/roadmap-progress-sync.md +103 -30
- package/.agent-src/rules/scope-control.md +42 -1
- package/.agent-src/rules/size-enforcement.md +1 -3
- package/.agent-src/rules/skill-quality.md +3 -8
- package/.agent-src/rules/ui-audit-before-build.md +106 -0
- package/.agent-src/rules/user-interaction.md +81 -3
- package/.agent-src/scripts/update_roadmap_progress.py +48 -6
- package/.agent-src/skills/blade-ui/SKILL.md +30 -5
- package/.agent-src/skills/command-routing/SKILL.md +32 -0
- package/.agent-src/skills/command-writing/SKILL.md +41 -2
- package/.agent-src/skills/description-assist/SKILL.md +21 -0
- package/.agent-src/skills/estimate-ticket/SKILL.md +0 -1
- package/.agent-src/skills/existing-ui-audit/SKILL.md +187 -0
- package/.agent-src/skills/fe-design/SKILL.md +72 -60
- package/.agent-src/skills/finishing-a-development-branch/SKILL.md +4 -0
- package/.agent-src/skills/flux/SKILL.md +31 -4
- package/.agent-src/skills/guideline-writing/SKILL.md +24 -2
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +51 -9
- package/.agent-src/skills/livewire/SKILL.md +30 -4
- package/.agent-src/skills/md-language-check/SKILL.md +103 -0
- package/.agent-src/skills/php-coder/SKILL.md +24 -0
- package/.agent-src/skills/react-shadcn-ui/SKILL.md +121 -0
- package/.agent-src/skills/refine-prompt/SKILL.md +220 -0
- package/.agent-src/skills/refine-ticket/SKILL.md +2 -4
- package/.agent-src/skills/roadmap-management/SKILL.md +10 -3
- package/.agent-src/skills/rule-writing/SKILL.md +23 -1
- package/.agent-src/skills/skill-writing/SKILL.md +1 -3
- package/.agent-src/skills/upstream-contribute/SKILL.md +1 -1
- package/.agent-src/skills/using-git-worktrees/SKILL.md +3 -1
- package/.agent-src/templates/AGENTS.md +24 -6
- package/.agent-src/templates/agent-settings.md +149 -0
- package/.agent-src/templates/github-workflows/roadmap-progress-check.yml +63 -0
- package/.agent-src/templates/hooks/pre-commit-roadmap-progress +60 -0
- package/.agent-src/templates/roadmaps.md +8 -2
- package/.agent-src/templates/scripts/implement_ticket/__init__.py +63 -26
- package/.agent-src/templates/scripts/implement_ticket/__main__.py +8 -2
- package/.agent-src/templates/scripts/memory_lookup.py +382 -21
- package/.agent-src/templates/scripts/memory_status.py +110 -9
- package/.agent-src/templates/scripts/telemetry/__init__.py +42 -0
- package/.agent-src/templates/scripts/telemetry/aggregator.py +154 -0
- package/.agent-src/templates/scripts/telemetry/boundary.py +171 -0
- package/.agent-src/templates/scripts/telemetry/engagement.py +238 -0
- package/.agent-src/templates/scripts/telemetry/report_renderer.py +170 -0
- package/.agent-src/templates/scripts/telemetry/settings.py +112 -0
- package/.agent-src/templates/scripts/telemetry_record.py +166 -0
- package/.agent-src/templates/scripts/telemetry_report.py +161 -0
- package/.agent-src/templates/scripts/telemetry_status.py +142 -0
- package/.agent-src/templates/scripts/work_engine/__init__.py +58 -0
- package/.agent-src/templates/scripts/work_engine/__main__.py +9 -0
- package/.agent-src/templates/scripts/work_engine/cli.py +592 -0
- package/.agent-src/templates/scripts/{implement_ticket → work_engine}/delivery_state.py +7 -0
- package/.agent-src/templates/scripts/work_engine/directives/__init__.py +33 -0
- package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +98 -0
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/analyze.py +1 -1
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/implement.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/memory.py +1 -1
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/plan.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/refine.py +396 -0
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/report.py +36 -4
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/test.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/verify.py +2 -2
- package/.agent-src/templates/scripts/work_engine/directives/mixed/__init__.py +116 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/contract.py +254 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/stitch.py +229 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/ui.py +231 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/__init__.py +113 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +44 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/apply.py +241 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/audit.py +414 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/design.py +335 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/polish.py +510 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/review.py +468 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/__init__.py +119 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.py +37 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.py +165 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.py +66 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/report.py +62 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/test.py +115 -0
- package/.agent-src/templates/scripts/work_engine/dispatcher.py +331 -0
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +54 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +32 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +103 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +44 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +42 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +50 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +49 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.py +53 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.py +50 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.py +52 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/trace.py +84 -0
- package/.agent-src/templates/scripts/work_engine/hooks/context.py +66 -0
- package/.agent-src/templates/scripts/work_engine/hooks/events.py +44 -0
- package/.agent-src/templates/scripts/work_engine/hooks/exceptions.py +79 -0
- package/.agent-src/templates/scripts/work_engine/hooks/registry.py +60 -0
- package/.agent-src/templates/scripts/work_engine/hooks/runner.py +73 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +141 -0
- package/.agent-src/templates/scripts/work_engine/intent/__init__.py +47 -0
- package/.agent-src/templates/scripts/work_engine/intent/classify.py +280 -0
- package/.agent-src/templates/scripts/work_engine/migration/__init__.py +8 -0
- package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +199 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/__init__.py +22 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/diff.py +106 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/file.py +113 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +90 -0
- package/.agent-src/templates/scripts/work_engine/scoring/__init__.py +14 -0
- package/.agent-src/templates/scripts/work_engine/scoring/confidence.py +300 -0
- package/.agent-src/templates/scripts/work_engine/stack/__init__.py +31 -0
- package/.agent-src/templates/scripts/work_engine/stack/detect.py +187 -0
- package/.agent-src/templates/scripts/work_engine/state.py +641 -0
- package/.claude-plugin/marketplace.json +105 -2
- package/AGENTS.md +36 -8
- package/CHANGELOG.md +558 -0
- package/README.md +146 -4
- package/composer.json +3 -0
- package/config/agent-settings.template.yml +45 -0
- package/config/gitignore-block.txt +4 -0
- package/docs/architecture.md +28 -1
- package/docs/development.md +1 -1
- package/docs/getting-started.md +3 -2
- package/docs/installation.md +86 -0
- package/docs/showcase.md +204 -0
- package/package.json +9 -1
- package/scripts/agent-config +274 -0
- package/scripts/audit_cloud_compatibility.py +288 -0
- package/scripts/build_cloud_bundle.py +458 -0
- package/scripts/build_linear_digest.py +263 -0
- package/scripts/chat_history.py +796 -7
- package/scripts/check_compression.py +139 -0
- package/scripts/check_iron_law_prominence.py +143 -0
- package/scripts/check_md_language.py +159 -0
- package/scripts/check_portability.py +36 -0
- package/scripts/check_reply_consistency.py +140 -0
- package/scripts/command_suggester/__init__.py +51 -0
- package/scripts/command_suggester/cooldown.py +132 -0
- package/scripts/command_suggester/loader.py +70 -0
- package/scripts/command_suggester/match.py +180 -0
- package/scripts/command_suggester/rank.py +120 -0
- package/scripts/command_suggester/render.py +86 -0
- package/scripts/command_suggester/sanitize.py +113 -0
- package/scripts/command_suggester/settings.py +125 -0
- package/scripts/command_suggester/types.py +78 -0
- package/scripts/hooks/augment-chat-history.sh +56 -0
- package/scripts/install-hooks.sh +67 -0
- package/scripts/install.py +150 -33
- package/scripts/lint_marketplace.py +27 -0
- package/scripts/memory_lookup.py +143 -7
- package/scripts/memory_status.py +76 -14
- package/scripts/migrate_command_suggestions.py +151 -0
- package/scripts/postinstall.sh +16 -0
- package/scripts/schemas/command.schema.json +41 -0
- package/scripts/skill_linter.py +67 -0
- package/scripts/sync_agent_settings.py +42 -12
- package/templates/consumer-settings/augment-cli-hooks.json +54 -0
- package/templates/consumer-settings/claude-settings.json +55 -1
- package/.agent-src/templates/scripts/implement_ticket/cli.py +0 -171
- package/.agent-src/templates/scripts/implement_ticket/dispatcher.py +0 -134
- package/.agent-src/templates/scripts/implement_ticket/steps/__init__.py +0 -49
- package/.agent-src/templates/scripts/implement_ticket/steps/refine.py +0 -140
- /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"]
|