@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.
- 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 +82 -50
- package/.agent-src/scripts/update_roadmap_progress.py +17 -5
- 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/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/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 +534 -0
- package/README.md +125 -4
- 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 +2 -2
- package/docs/installation.md +86 -0
- package/docs/showcase.md +204 -0
- package/package.json +1 -1
- package/scripts/agent-config +199 -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/migrate_command_suggestions.py +151 -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,141 @@
|
|
|
1
|
+
"""Read ``hooks.*`` from ``.agent-settings.yml`` into :class:`HookSettings`.
|
|
2
|
+
|
|
3
|
+
Mirror of the chat-history settings pattern (``scripts/command_suggester/
|
|
4
|
+
settings.py``):
|
|
5
|
+
|
|
6
|
+
* Lazy PyYAML import — the engine works without yaml installed when no
|
|
7
|
+
settings file is present (test fixtures, cloud bundles).
|
|
8
|
+
* Default-permissive: a missing file or missing ``hooks:`` block returns
|
|
9
|
+
:class:`HookSettings` with ``enabled=False`` — every hook off, every
|
|
10
|
+
golden replay safe by construction.
|
|
11
|
+
* Malformed YAML / unreadable file → defaults; the engine degrades
|
|
12
|
+
silently rather than crashing the CLI.
|
|
13
|
+
* Chat-history hooks gate on **two** switches: ``hooks.chat_history.
|
|
14
|
+
enabled`` AND the global ``chat_history.enabled``. Either off → no
|
|
15
|
+
chat-history hook registers.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
DEFAULT_SETTINGS_FILE = ".agent-settings.yml"
|
|
24
|
+
DEFAULT_CHAT_HISTORY_SCRIPT = "scripts/chat_history.py"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class HookSettings:
|
|
29
|
+
"""Resolved view of the ``hooks:`` block.
|
|
30
|
+
|
|
31
|
+
``enabled`` is the master switch. When ``False`` the registry stays
|
|
32
|
+
empty regardless of the per-hook fields; this is the default when no
|
|
33
|
+
settings file exists or no ``hooks`` block is declared, and it is
|
|
34
|
+
what keeps golden-replay tests byte-stable.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
enabled: bool = False
|
|
38
|
+
trace: bool = False
|
|
39
|
+
halt_surface_audit: bool = False
|
|
40
|
+
state_shape_validation: bool = False
|
|
41
|
+
directive_set_guard: bool = False
|
|
42
|
+
chat_history_enabled: bool = False
|
|
43
|
+
chat_history_script: str = DEFAULT_CHAT_HISTORY_SCRIPT
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_DEFAULT = HookSettings()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_hook_settings(
|
|
50
|
+
settings_path: Path | str | None = None,
|
|
51
|
+
) -> HookSettings:
|
|
52
|
+
"""Return :class:`HookSettings` hydrated from ``.agent-settings.yml``.
|
|
53
|
+
|
|
54
|
+
``settings_path`` defaults to ``./.agent-settings.yml`` relative to
|
|
55
|
+
the current working directory — same convention as chat-history.
|
|
56
|
+
"""
|
|
57
|
+
path = Path(settings_path) if settings_path else Path(DEFAULT_SETTINGS_FILE)
|
|
58
|
+
raw = _read_yaml(path)
|
|
59
|
+
if raw is None:
|
|
60
|
+
return _DEFAULT
|
|
61
|
+
return _settings_from_raw(raw)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _read_yaml(path: Path) -> dict[str, Any] | None:
|
|
65
|
+
if not path.is_file():
|
|
66
|
+
return None
|
|
67
|
+
try:
|
|
68
|
+
import yaml # type: ignore[import-untyped]
|
|
69
|
+
except ImportError:
|
|
70
|
+
return None
|
|
71
|
+
try:
|
|
72
|
+
with path.open(encoding="utf-8") as fh:
|
|
73
|
+
data = yaml.safe_load(fh) or {}
|
|
74
|
+
except (OSError, yaml.YAMLError):
|
|
75
|
+
return None
|
|
76
|
+
if not isinstance(data, dict):
|
|
77
|
+
return None
|
|
78
|
+
return data
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _settings_from_raw(data: dict[str, Any]) -> HookSettings:
|
|
82
|
+
hooks = data.get("hooks")
|
|
83
|
+
if not isinstance(hooks, dict):
|
|
84
|
+
return _DEFAULT
|
|
85
|
+
enabled = _coerce_bool(hooks.get("enabled"), False)
|
|
86
|
+
if not enabled:
|
|
87
|
+
return HookSettings(enabled=False)
|
|
88
|
+
|
|
89
|
+
chat_section = hooks.get("chat_history")
|
|
90
|
+
if isinstance(chat_section, dict):
|
|
91
|
+
chat_block_enabled = _coerce_bool(chat_section.get("enabled"), True)
|
|
92
|
+
chat_script = str(
|
|
93
|
+
chat_section.get("script") or DEFAULT_CHAT_HISTORY_SCRIPT
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
chat_block_enabled = True
|
|
97
|
+
chat_script = DEFAULT_CHAT_HISTORY_SCRIPT
|
|
98
|
+
|
|
99
|
+
global_chat = data.get("chat_history")
|
|
100
|
+
global_chat_on = (
|
|
101
|
+
isinstance(global_chat, dict)
|
|
102
|
+
and _coerce_bool(global_chat.get("enabled"), False)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return HookSettings(
|
|
106
|
+
enabled=True,
|
|
107
|
+
trace=_coerce_bool(hooks.get("trace"), False),
|
|
108
|
+
halt_surface_audit=_coerce_bool(
|
|
109
|
+
hooks.get("halt_surface_audit"), True
|
|
110
|
+
),
|
|
111
|
+
state_shape_validation=_coerce_bool(
|
|
112
|
+
hooks.get("state_shape_validation"), True
|
|
113
|
+
),
|
|
114
|
+
directive_set_guard=_coerce_bool(
|
|
115
|
+
hooks.get("directive_set_guard"), True
|
|
116
|
+
),
|
|
117
|
+
chat_history_enabled=chat_block_enabled and global_chat_on,
|
|
118
|
+
chat_history_script=chat_script,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _coerce_bool(value: Any, default: bool) -> bool:
|
|
123
|
+
if isinstance(value, bool):
|
|
124
|
+
return value
|
|
125
|
+
if value is None:
|
|
126
|
+
return default
|
|
127
|
+
if isinstance(value, str):
|
|
128
|
+
s = value.strip().lower()
|
|
129
|
+
if s in ("true", "yes", "on", "1"):
|
|
130
|
+
return True
|
|
131
|
+
if s in ("false", "no", "off", "0"):
|
|
132
|
+
return False
|
|
133
|
+
return default
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
__all__ = [
|
|
137
|
+
"DEFAULT_CHAT_HISTORY_SCRIPT",
|
|
138
|
+
"DEFAULT_SETTINGS_FILE",
|
|
139
|
+
"HookSettings",
|
|
140
|
+
"load_hook_settings",
|
|
141
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Intent classification for the universal engine (R3 Phase 1 Step 2).
|
|
2
|
+
|
|
3
|
+
The :mod:`work_engine.intent.classify` module turns a raw user prompt
|
|
4
|
+
(or a ticket's title + body) into one of the five labels the dispatcher
|
|
5
|
+
routes against:
|
|
6
|
+
|
|
7
|
+
- ``ui-build`` — new screen, page, or component.
|
|
8
|
+
- ``ui-improve`` — change to an existing screen / component.
|
|
9
|
+
- ``ui-trivial`` — single-file, single-concern micro-edit (color, copy,
|
|
10
|
+
one class, one prop). Hard preconditions are enforced again at apply
|
|
11
|
+
time; the classifier only labels the *intent*, not the safety floor.
|
|
12
|
+
- ``mixed`` — both UI and backend signals; routes to the mixed track.
|
|
13
|
+
- ``backend-coding`` — default; no UI signal.
|
|
14
|
+
|
|
15
|
+
The classifier is intentionally heuristic-only — it consumes nothing
|
|
16
|
+
beyond the prompt text and optional ticket title. Confidence-band
|
|
17
|
+
gating, AC reconstruction, and assumption surfacing all stay in
|
|
18
|
+
``directives/backend/refine.py`` (R2). This module only owns the
|
|
19
|
+
*label*; the dispatcher owns the routing.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from . import classify
|
|
24
|
+
from .classify import (
|
|
25
|
+
INTENT_BACKEND,
|
|
26
|
+
INTENT_MIXED,
|
|
27
|
+
INTENT_UI_BUILD,
|
|
28
|
+
INTENT_UI_IMPROVE,
|
|
29
|
+
INTENT_UI_TRIVIAL,
|
|
30
|
+
KNOWN_INTENTS,
|
|
31
|
+
classify_intent,
|
|
32
|
+
directive_set_for,
|
|
33
|
+
populate_routing,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"INTENT_BACKEND",
|
|
38
|
+
"INTENT_MIXED",
|
|
39
|
+
"INTENT_UI_BUILD",
|
|
40
|
+
"INTENT_UI_IMPROVE",
|
|
41
|
+
"INTENT_UI_TRIVIAL",
|
|
42
|
+
"KNOWN_INTENTS",
|
|
43
|
+
"classify",
|
|
44
|
+
"classify_intent",
|
|
45
|
+
"directive_set_for",
|
|
46
|
+
"populate_routing",
|
|
47
|
+
]
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Heuristic intent classifier — see :mod:`work_engine.intent` for context.
|
|
2
|
+
|
|
3
|
+
The classifier walks a small priority ladder against the lower-cased
|
|
4
|
+
prompt + optional ticket title. First match wins; ``backend-coding`` is
|
|
5
|
+
the fall-through default so every prompt always lands on a known label.
|
|
6
|
+
|
|
7
|
+
Priority order (deliberately fixed):
|
|
8
|
+
|
|
9
|
+
1. **Trivial-UI** — UI signal AND a trivial-edit verb pattern (``change
|
|
10
|
+
color``, ``make … red``, ``rename label``, ``fix copy``) AND no
|
|
11
|
+
structural verb (``add``, ``build``, ``create``, ``introduce``).
|
|
12
|
+
2. **Mixed** — UI signal AND a backend signal (``endpoint``, ``API``,
|
|
13
|
+
``migration``, ``schema``, ``query``, ``job``, ``queue``).
|
|
14
|
+
3. **UI-Improve** — UI signal AND an improve/redesign/refactor verb,
|
|
15
|
+
OR explicit "existing" surface markers.
|
|
16
|
+
4. **UI-Build** — UI signal AND a build/create/add verb, OR new-screen
|
|
17
|
+
markers (``new page``, ``new screen``, ``new component``).
|
|
18
|
+
5. **Backend-Coding** — default.
|
|
19
|
+
|
|
20
|
+
The label is the dispatcher's *only* input for routing. Confidence
|
|
21
|
+
band, ``ui_intent`` flag from the scorer, and AC reconstruction stay
|
|
22
|
+
the resolution surface — the classifier does not look at them.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from ..state import WorkState
|
|
31
|
+
|
|
32
|
+
INTENT_UI_BUILD = "ui-build"
|
|
33
|
+
INTENT_UI_IMPROVE = "ui-improve"
|
|
34
|
+
INTENT_UI_TRIVIAL = "ui-trivial"
|
|
35
|
+
INTENT_MIXED = "mixed"
|
|
36
|
+
INTENT_BACKEND = "backend-coding"
|
|
37
|
+
|
|
38
|
+
KNOWN_INTENTS: frozenset[str] = frozenset(
|
|
39
|
+
{
|
|
40
|
+
INTENT_UI_BUILD,
|
|
41
|
+
INTENT_UI_IMPROVE,
|
|
42
|
+
INTENT_UI_TRIVIAL,
|
|
43
|
+
INTENT_MIXED,
|
|
44
|
+
INTENT_BACKEND,
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
"""All labels the classifier can return.
|
|
48
|
+
|
|
49
|
+
Locked here so the dispatcher's mapping table and the test suite share
|
|
50
|
+
one source of truth."""
|
|
51
|
+
|
|
52
|
+
_UI_NOUNS: frozenset[str] = frozenset(
|
|
53
|
+
{
|
|
54
|
+
"ui", "screen", "page", "view", "form", "modal", "dialog",
|
|
55
|
+
"button", "card", "tile",
|
|
56
|
+
"header", "footer", "nav", "navigation", "sidebar", "menu",
|
|
57
|
+
"dropdown", "tab", "panel", "layout", "component", "icon",
|
|
58
|
+
"tooltip", "toast", "banner", "badge", "avatar", "label",
|
|
59
|
+
"checkbox", "radio", "toggle", "switch", "stepper", "wizard",
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
"""Strong UI nouns — exclusive UI meaning.
|
|
63
|
+
|
|
64
|
+
Deliberately omits ``table``, ``list``, ``input``, and ``field``:
|
|
65
|
+
``table``/``list`` collide with database tables and Python/PHP lists;
|
|
66
|
+
``input``/``field`` collide with function inputs, command inputs, and
|
|
67
|
+
JSON/DB fields. Genuine UI prompts that mean form inputs always come
|
|
68
|
+
with a strong-UI noun nearby (``form``, ``page``, ``component``)."""
|
|
69
|
+
|
|
70
|
+
_UI_STYLE: frozenset[str] = frozenset(
|
|
71
|
+
{
|
|
72
|
+
"color", "colour", "css", "tailwind", "padding", "margin",
|
|
73
|
+
"spacing", "font", "typography", "responsive", "mobile",
|
|
74
|
+
"dark mode", "light mode", "theme", "shadow", "border",
|
|
75
|
+
"rounded", "radius",
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
_BACKEND_SIGNALS: frozenset[str] = frozenset(
|
|
80
|
+
{
|
|
81
|
+
"endpoint", "api", "route", "controller", "service",
|
|
82
|
+
"migration", "schema", "table", "column", "index", "query",
|
|
83
|
+
"queue", "job", "worker", "webhook", "policy", "gate",
|
|
84
|
+
"command", "cron", "broadcast", "event", "listener",
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
_TRIVIAL_VERBS: frozenset[str] = frozenset(
|
|
89
|
+
{
|
|
90
|
+
"rename", "relabel", "tweak", "adjust", "swap", "change",
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
_IMPROVE_VERBS: frozenset[str] = frozenset(
|
|
95
|
+
{
|
|
96
|
+
"improve", "polish", "redesign", "rework", "refine",
|
|
97
|
+
"refactor", "tighten", "clean", "fix", "update", "tune",
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
_BUILD_VERBS: frozenset[str] = frozenset(
|
|
102
|
+
{
|
|
103
|
+
"add", "build", "create", "introduce", "implement", "ship",
|
|
104
|
+
"draft", "scaffold", "wire",
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
_NEW_SURFACE: re.Pattern[str] = re.compile(
|
|
109
|
+
r"\b(new|fresh|blank)\s+(page|screen|view|component|form|modal|tile|dashboard)\b",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
_EXISTING_SURFACE: re.Pattern[str] = re.compile(
|
|
113
|
+
r"\b(existing|current|the)\s+(page|screen|view|component|form|modal)\b",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
_TRIVIAL_PATTERN: re.Pattern[str] = re.compile(
|
|
117
|
+
r"\b(make|change|update|set|swap)\b[^.]{0,40}\b("
|
|
118
|
+
r"red|blue|green|yellow|black|white|primary|secondary"
|
|
119
|
+
r"|color|colour|copy|text|label|wording|class|prop)\b",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def classify_intent(raw: str, *, title: str | None = None) -> str:
|
|
124
|
+
"""Return one of :data:`KNOWN_INTENTS` for the supplied text.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
raw:
|
|
129
|
+
The user prompt or ticket body. Whitespace is normalised
|
|
130
|
+
internally; ``""`` and ``None`` resolve to ``backend-coding``.
|
|
131
|
+
title:
|
|
132
|
+
Optional ticket title. Concatenated with ``raw`` before
|
|
133
|
+
scanning so single-line ticket headlines (`"Add CSV export"`)
|
|
134
|
+
produce the same label whether they arrive in the body or the
|
|
135
|
+
title slot.
|
|
136
|
+
"""
|
|
137
|
+
text = " ".join(filter(None, (title, raw))).strip().lower()
|
|
138
|
+
if not text:
|
|
139
|
+
return INTENT_BACKEND
|
|
140
|
+
|
|
141
|
+
has_ui = _has_ui_signal(text)
|
|
142
|
+
has_backend = _has_backend_signal(text)
|
|
143
|
+
|
|
144
|
+
if has_ui and _is_trivial(text):
|
|
145
|
+
return INTENT_UI_TRIVIAL
|
|
146
|
+
if has_ui and has_backend:
|
|
147
|
+
return INTENT_MIXED
|
|
148
|
+
if has_ui and _is_improve(text):
|
|
149
|
+
return INTENT_UI_IMPROVE
|
|
150
|
+
if has_ui and _is_build(text):
|
|
151
|
+
return INTENT_UI_BUILD
|
|
152
|
+
if has_ui:
|
|
153
|
+
# UI signal but no clear verb — default to ui-improve so the
|
|
154
|
+
# full audit gate engages. ui-build would skip the existing-
|
|
155
|
+
# surface check, which is the wrong default when the prompt
|
|
156
|
+
# is ambiguous.
|
|
157
|
+
return INTENT_UI_IMPROVE
|
|
158
|
+
return INTENT_BACKEND
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def directive_set_for(intent: str) -> str:
|
|
162
|
+
"""Map an intent label to a directive-set name.
|
|
163
|
+
|
|
164
|
+
Centralised here so the dispatcher and the refine step share one
|
|
165
|
+
routing table; a future intent (``infra``, ``security-review``)
|
|
166
|
+
only needs a single edit. Unknown labels raise ``ValueError`` —
|
|
167
|
+
silently falling back to ``backend`` would mask classifier bugs.
|
|
168
|
+
"""
|
|
169
|
+
if intent not in KNOWN_INTENTS:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
f"unknown intent {intent!r}; "
|
|
172
|
+
f"expected one of {sorted(KNOWN_INTENTS)}",
|
|
173
|
+
)
|
|
174
|
+
if intent in (INTENT_UI_BUILD, INTENT_UI_IMPROVE):
|
|
175
|
+
return "ui"
|
|
176
|
+
if intent == INTENT_UI_TRIVIAL:
|
|
177
|
+
return "ui-trivial"
|
|
178
|
+
if intent == INTENT_MIXED:
|
|
179
|
+
return "mixed"
|
|
180
|
+
return "backend"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# --- helpers ----------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
def _has_ui_signal(text: str) -> bool:
|
|
186
|
+
if any(re.search(rf"\b{re.escape(w)}\b", text) for w in _UI_NOUNS):
|
|
187
|
+
return True
|
|
188
|
+
return any(s in text for s in _UI_STYLE)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _has_backend_signal(text: str) -> bool:
|
|
192
|
+
return any(re.search(rf"\b{re.escape(w)}\b", text) for w in _BACKEND_SIGNALS)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _is_trivial(text: str) -> bool:
|
|
196
|
+
if _TRIVIAL_PATTERN.search(text):
|
|
197
|
+
return True
|
|
198
|
+
return any(re.search(rf"\b{re.escape(v)}\b", text) for v in _TRIVIAL_VERBS) and len(
|
|
199
|
+
text.split()
|
|
200
|
+
) <= 14
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _is_improve(text: str) -> bool:
|
|
204
|
+
if _EXISTING_SURFACE.search(text):
|
|
205
|
+
return True
|
|
206
|
+
return any(re.search(rf"\b{re.escape(v)}\b", text) for v in _IMPROVE_VERBS)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _is_build(text: str) -> bool:
|
|
210
|
+
if _NEW_SURFACE.search(text):
|
|
211
|
+
return True
|
|
212
|
+
return any(re.search(rf"\b{re.escape(v)}\b", text) for v in _BUILD_VERBS)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def populate_routing(state: "WorkState") -> None:
|
|
216
|
+
"""Classify ``state.input`` and write ``intent`` + ``directive_set`` in place.
|
|
217
|
+
|
|
218
|
+
Idempotent and override-safe: if ``state.intent`` is already a
|
|
219
|
+
UI-track or mixed label (``ui-build``, ``ui-improve``, ``ui-trivial``,
|
|
220
|
+
``mixed``), the routing is left untouched. Only freshly-built states
|
|
221
|
+
carrying the construction default ``backend-coding`` are reclassified.
|
|
222
|
+
Loaded state files round-trip without losing a previously-recorded
|
|
223
|
+
intent — including a manual user override in the JSON.
|
|
224
|
+
|
|
225
|
+
The text fed to the classifier depends on the input envelope:
|
|
226
|
+
|
|
227
|
+
- ``prompt`` → ``state.input.data["raw"]``
|
|
228
|
+
- ``ticket`` → ``state.input.data["title"]`` + first non-empty
|
|
229
|
+
acceptance criterion, falling back to ``description`` when AC is
|
|
230
|
+
missing. Title is passed separately so single-line ticket
|
|
231
|
+
headlines (``"Add CSV export"``) classify identically whether
|
|
232
|
+
they arrive in the body or the title slot.
|
|
233
|
+
- ``diff`` / ``file`` → routed directly to ``ui-improve`` without
|
|
234
|
+
running the heuristic. Both envelopes are R3 Phase 1 inputs that
|
|
235
|
+
describe an existing UI surface ("improve this screen"); the
|
|
236
|
+
classifier's prose-oriented signals do not apply, and the audit +
|
|
237
|
+
design directives downstream are the right place to read the
|
|
238
|
+
diff/file contents.
|
|
239
|
+
"""
|
|
240
|
+
if state.intent != INTENT_BACKEND:
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
if state.input.kind in {"diff", "file"}:
|
|
244
|
+
state.intent = INTENT_UI_IMPROVE
|
|
245
|
+
state.directive_set = directive_set_for(INTENT_UI_IMPROVE)
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
text, title = _extract_text(state)
|
|
249
|
+
intent = classify_intent(text, title=title)
|
|
250
|
+
state.intent = intent
|
|
251
|
+
state.directive_set = directive_set_for(intent)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _extract_text(state: "WorkState") -> tuple[str, str | None]:
|
|
255
|
+
data = state.input.data or {}
|
|
256
|
+
if state.input.kind == "prompt":
|
|
257
|
+
return str(data.get("raw") or ""), None
|
|
258
|
+
title = data.get("title")
|
|
259
|
+
title_str = str(title) if isinstance(title, str) and title.strip() else None
|
|
260
|
+
body_parts: list[str] = []
|
|
261
|
+
ac = data.get("acceptance_criteria")
|
|
262
|
+
if isinstance(ac, list):
|
|
263
|
+
body_parts.extend(str(item) for item in ac if isinstance(item, str))
|
|
264
|
+
description = data.get("description")
|
|
265
|
+
if isinstance(description, str) and description.strip():
|
|
266
|
+
body_parts.append(description)
|
|
267
|
+
return " ".join(body_parts), title_str
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
__all__ = [
|
|
271
|
+
"INTENT_BACKEND",
|
|
272
|
+
"INTENT_MIXED",
|
|
273
|
+
"INTENT_UI_BUILD",
|
|
274
|
+
"INTENT_UI_IMPROVE",
|
|
275
|
+
"INTENT_UI_TRIVIAL",
|
|
276
|
+
"KNOWN_INTENTS",
|
|
277
|
+
"classify_intent",
|
|
278
|
+
"directive_set_for",
|
|
279
|
+
"populate_routing",
|
|
280
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""State-file migrations for the universal engine.
|
|
2
|
+
|
|
3
|
+
Each module in this package owns one direction of one schema bump. The
|
|
4
|
+
historical bumps must remain runnable so a freshly-cloned repository
|
|
5
|
+
that finds a v0 state file from a long-running branch can catch up
|
|
6
|
+
without manual intervention.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Migrate a v0 ``DeliveryState`` JSON file to the v1 schema.
|
|
2
|
+
|
|
3
|
+
The v0 era used ``.implement-ticket-state.json`` and stored the ticket
|
|
4
|
+
under a flat ``ticket`` key. v1 wraps the payload under ``input.kind``
|
|
5
|
+
/ ``input.data`` and adds ``intent``, ``directive_set``, and
|
|
6
|
+
``version``. The default destination is ``.work-state.json`` next to
|
|
7
|
+
the v0 file; the v0 file is renamed to ``.implement-ticket-state.json.bak``
|
|
8
|
+
to preserve the rollback surface.
|
|
9
|
+
|
|
10
|
+
The module is both importable and runnable:
|
|
11
|
+
|
|
12
|
+
python3 -m work_engine.migration.v0_to_v1 .implement-ticket-state.json
|
|
13
|
+
|
|
14
|
+
Idempotency: ``migrate_payload`` accepts a payload that already looks
|
|
15
|
+
like v1 and returns it unchanged. ``migrate_file`` refuses to migrate
|
|
16
|
+
twice — if the destination already exists it raises rather than
|
|
17
|
+
silently overwriting work.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import shutil
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Sequence
|
|
27
|
+
|
|
28
|
+
from ..state import (
|
|
29
|
+
DEFAULT_DIRECTIVE_SET,
|
|
30
|
+
DEFAULT_INTENT,
|
|
31
|
+
SCHEMA_VERSION,
|
|
32
|
+
SchemaError,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
DEFAULT_V0_FILENAME = ".implement-ticket-state.json"
|
|
36
|
+
"""Path the dispatcher used while the engine still lived under
|
|
37
|
+
``implement_ticket``. The migration looks here when no source path is
|
|
38
|
+
passed on the CLI."""
|
|
39
|
+
|
|
40
|
+
DEFAULT_V1_FILENAME = ".work-state.json"
|
|
41
|
+
"""Canonical filename for the v1 wire format."""
|
|
42
|
+
|
|
43
|
+
BACKUP_SUFFIX = ".bak"
|
|
44
|
+
"""Appended to the v0 source path when the migration archives it."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def migrate_payload(payload: Any) -> dict[str, Any]:
|
|
48
|
+
"""Return the v1 form of ``payload``.
|
|
49
|
+
|
|
50
|
+
A payload that already declares ``version: 1`` is returned
|
|
51
|
+
unchanged (deep-copied via ``json.loads(json.dumps(...))`` so the
|
|
52
|
+
caller cannot accidentally mutate the input). Anything else is
|
|
53
|
+
treated as v0 and wrapped: ``ticket`` becomes ``input.data``,
|
|
54
|
+
``input.kind`` is set to ``"ticket"``, and the engine defaults are
|
|
55
|
+
filled in.
|
|
56
|
+
|
|
57
|
+
Raises
|
|
58
|
+
------
|
|
59
|
+
SchemaError
|
|
60
|
+
If the payload is not a dict, declares a higher version than
|
|
61
|
+
this migration knows about, or lacks a ``ticket`` key.
|
|
62
|
+
"""
|
|
63
|
+
if not isinstance(payload, dict):
|
|
64
|
+
raise SchemaError(
|
|
65
|
+
f"v0 state must be a JSON object; got {type(payload).__name__}",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
declared_version = payload.get("version")
|
|
69
|
+
if declared_version == SCHEMA_VERSION:
|
|
70
|
+
return json.loads(json.dumps(payload))
|
|
71
|
+
if declared_version is not None:
|
|
72
|
+
raise SchemaError(
|
|
73
|
+
f"cannot migrate from version {declared_version!r} to "
|
|
74
|
+
f"{SCHEMA_VERSION}; this script only handles v0 (no version key)",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if "ticket" not in payload:
|
|
78
|
+
raise SchemaError(
|
|
79
|
+
"v0 state must carry a 'ticket' key; got keys: "
|
|
80
|
+
f"{sorted(payload.keys())}",
|
|
81
|
+
)
|
|
82
|
+
ticket = payload["ticket"]
|
|
83
|
+
if not isinstance(ticket, dict):
|
|
84
|
+
raise SchemaError(
|
|
85
|
+
f"v0 state.ticket must be a JSON object; got {type(ticket).__name__}",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"version": SCHEMA_VERSION,
|
|
90
|
+
"input": {"kind": "ticket", "data": ticket},
|
|
91
|
+
"intent": DEFAULT_INTENT,
|
|
92
|
+
"directive_set": DEFAULT_DIRECTIVE_SET,
|
|
93
|
+
"persona": payload.get("persona", "senior-engineer"),
|
|
94
|
+
"memory": list(payload.get("memory", [])),
|
|
95
|
+
"plan": payload.get("plan"),
|
|
96
|
+
"changes": list(payload.get("changes", [])),
|
|
97
|
+
"tests": payload.get("tests"),
|
|
98
|
+
"verify": payload.get("verify"),
|
|
99
|
+
"outcomes": dict(payload.get("outcomes", {})),
|
|
100
|
+
"questions": list(payload.get("questions", [])),
|
|
101
|
+
"report": payload.get("report", ""),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def migrate_file(
|
|
106
|
+
source: Path,
|
|
107
|
+
*,
|
|
108
|
+
destination: Path | None = None,
|
|
109
|
+
backup: bool = True,
|
|
110
|
+
) -> Path:
|
|
111
|
+
"""Migrate the v0 state file at ``source`` and write the v1 result.
|
|
112
|
+
|
|
113
|
+
``destination`` defaults to :data:`DEFAULT_V1_FILENAME` next to
|
|
114
|
+
``source``. When ``backup`` is true (the default) the original
|
|
115
|
+
file is renamed with :data:`BACKUP_SUFFIX` appended; when false,
|
|
116
|
+
the original is left untouched. The destination must not exist —
|
|
117
|
+
refusing to overwrite is the safety net against accidental
|
|
118
|
+
double-migration on CI.
|
|
119
|
+
|
|
120
|
+
Returns the destination path on success.
|
|
121
|
+
"""
|
|
122
|
+
if not source.is_file():
|
|
123
|
+
raise SchemaError(f"v0 state file not found: {source}")
|
|
124
|
+
|
|
125
|
+
raw = source.read_text(encoding="utf-8")
|
|
126
|
+
try:
|
|
127
|
+
payload = json.loads(raw)
|
|
128
|
+
except json.JSONDecodeError as exc:
|
|
129
|
+
raise SchemaError(f"invalid JSON in {source}: {exc}") from exc
|
|
130
|
+
|
|
131
|
+
target = destination or source.with_name(DEFAULT_V1_FILENAME)
|
|
132
|
+
if target.exists():
|
|
133
|
+
raise SchemaError(
|
|
134
|
+
f"refusing to overwrite existing destination {target}; "
|
|
135
|
+
"delete or rename it first",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
migrated = migrate_payload(payload)
|
|
139
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
target.write_text(
|
|
141
|
+
json.dumps(migrated, indent=2, ensure_ascii=False) + "\n",
|
|
142
|
+
encoding="utf-8",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if backup:
|
|
146
|
+
backup_path = source.with_suffix(source.suffix + BACKUP_SUFFIX)
|
|
147
|
+
shutil.move(str(source), str(backup_path))
|
|
148
|
+
|
|
149
|
+
return target
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
153
|
+
"""CLI entry point — ``python3 -m work_engine.migration.v0_to_v1``.
|
|
154
|
+
|
|
155
|
+
Exits ``0`` on success, ``2`` on any :class:`SchemaError` so the
|
|
156
|
+
invoking shell can branch on the failure category.
|
|
157
|
+
"""
|
|
158
|
+
parser = argparse.ArgumentParser(
|
|
159
|
+
prog="work_engine.migration.v0_to_v1",
|
|
160
|
+
description="Migrate a legacy .implement-ticket-state.json file to "
|
|
161
|
+
"the v1 .work-state.json schema.",
|
|
162
|
+
)
|
|
163
|
+
parser.add_argument(
|
|
164
|
+
"source",
|
|
165
|
+
type=Path,
|
|
166
|
+
nargs="?",
|
|
167
|
+
default=Path(DEFAULT_V0_FILENAME),
|
|
168
|
+
help=f"Path to the v0 state file (default: {DEFAULT_V0_FILENAME}).",
|
|
169
|
+
)
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"--destination",
|
|
172
|
+
type=Path,
|
|
173
|
+
default=None,
|
|
174
|
+
help="Path to write the v1 file to "
|
|
175
|
+
f"(default: {DEFAULT_V1_FILENAME} next to source).",
|
|
176
|
+
)
|
|
177
|
+
parser.add_argument(
|
|
178
|
+
"--no-backup",
|
|
179
|
+
action="store_true",
|
|
180
|
+
help="Do not rename the v0 source to .bak after migration.",
|
|
181
|
+
)
|
|
182
|
+
args = parser.parse_args(argv)
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
target = migrate_file(
|
|
186
|
+
args.source,
|
|
187
|
+
destination=args.destination,
|
|
188
|
+
backup=not args.no_backup,
|
|
189
|
+
)
|
|
190
|
+
except SchemaError as exc:
|
|
191
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
192
|
+
return 2
|
|
193
|
+
|
|
194
|
+
print(f"migrated {args.source} → {target}")
|
|
195
|
+
return 0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
sys.exit(main())
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Input resolvers — turn raw user-supplied payloads into envelopes.
|
|
2
|
+
|
|
3
|
+
A resolver wraps a typed source (a free-form prompt, a diff, a file
|
|
4
|
+
reference) into the canonical :class:`work_engine.state.Input` shape so
|
|
5
|
+
the dispatcher only ever speaks one schema. The R1 ticket flow does not
|
|
6
|
+
need a resolver — ticket payloads arrive pre-structured from
|
|
7
|
+
``/implement-ticket``; R2 introduces :mod:`.prompt`; R3 Phase 1 adds
|
|
8
|
+
:mod:`.diff` and :mod:`.file` for the UI-improve track ("improve this
|
|
9
|
+
screen via diff/PR" and "improve this existing component/page").
|
|
10
|
+
|
|
11
|
+
Resolvers are deliberately thin: they normalize, they do not interpret.
|
|
12
|
+
Reconstruction of acceptance criteria + assumptions + confidence is the
|
|
13
|
+
job of the ``refine-prompt`` skill (R2 Phase 3) called from the
|
|
14
|
+
``refine`` step, not the resolver. Keeping the split sharp means the
|
|
15
|
+
envelope shape stays cheap to round-trip through state and the heavy
|
|
16
|
+
lifting stays with the agent-directive halt where it belongs.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from . import diff, file, prompt
|
|
21
|
+
|
|
22
|
+
__all__ = ["diff", "file", "prompt"]
|