@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,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"]
|