@event4u/agent-config 1.13.0 → 1.15.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 +4 -1
- 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 +7 -3
- 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 +6 -1
- package/.agent-src/commands/chat-history-resume.md +7 -2
- package/.agent-src/commands/chat-history.md +7 -2
- package/.agent-src/commands/check-current-md.md +137 -0
- package/.agent-src/commands/commit-in-chunks.md +118 -0
- package/.agent-src/commands/commit.md +4 -0
- package/.agent-src/commands/compress.md +37 -2
- 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 +5 -2
- 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 +33 -0
- package/.agent-src/commands/optimize-agents.md +4 -0
- package/.agent-src/commands/optimize-augmentignore.md +12 -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 +12 -7
- package/.agent-src/commands/review-changes.md +39 -8
- package/.agent-src/commands/review-routing.md +4 -0
- package/.agent-src/commands/roadmap-create.md +18 -0
- package/.agent-src/commands/roadmap-execute.md +14 -1
- package/.agent-src/commands/rule-compliance-audit.md +4 -0
- package/.agent-src/commands/set-cost-profile.md +11 -0
- package/.agent-src/commands/sync-agent-settings.md +12 -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 +6 -3
- 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 +64 -37
- package/.agent-src/rules/autonomous-execution.md +158 -0
- package/.agent-src/rules/chat-history-cadence.md +109 -0
- package/.agent-src/rules/chat-history-ownership.md +123 -0
- package/.agent-src/rules/chat-history-visibility.md +96 -0
- package/.agent-src/rules/cli-output-handling.md +27 -4
- package/.agent-src/rules/command-suggestion.md +134 -0
- package/.agent-src/rules/commit-policy.md +109 -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 +85 -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 +159 -27
- package/.agent-src/rules/role-mode-adherence.md +1 -1
- package/.agent-src/rules/scope-control.md +42 -1
- package/.agent-src/rules/size-enforcement.md +2 -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 +107 -51
- package/.agent-src/scripts/update_roadmap_progress.py +73 -9
- package/.agent-src/skills/blade-ui/SKILL.md +47 -3
- package/.agent-src/skills/command-routing/SKILL.md +32 -0
- package/.agent-src/skills/command-writing/SKILL.md +52 -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 +202 -0
- package/.agent-src/skills/fe-design/SKILL.md +78 -61
- package/.agent-src/skills/file-editor/SKILL.md +9 -0
- 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 +49 -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 +32 -28
- package/.agent-src/skills/roadmap-management/SKILL.md +24 -11
- package/.agent-src/skills/rule-writing/SKILL.md +23 -1
- package/.agent-src/skills/skill-writing/SKILL.md +3 -5
- package/.agent-src/skills/upstream-contribute/SKILL.md +3 -3
- 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 +11 -4
- 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 +1 -1
- 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 +195 -0
- package/.agent-src/templates/scripts/work_engine/cli_args.py +116 -0
- package/.agent-src/templates/scripts/{implement_ticket → work_engine}/delivery_state.py +10 -3
- 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 +3 -3
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/memory.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/plan.py +2 -2
- 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 +37 -5
- 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/emitters.py +43 -0
- package/.agent-src/templates/scripts/work_engine/errors.py +19 -0
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +76 -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/input_builders.py +163 -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 +231 -0
- package/.agent-src/templates/scripts/{implement_ticket → work_engine}/persona_policy.py +1 -1
- 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/.agent-src/templates/scripts/work_engine/state_io.py +202 -0
- package/.claude-plugin/marketplace.json +105 -2
- package/AGENTS.md +38 -8
- package/CHANGELOG.md +609 -0
- package/README.md +136 -14
- package/config/agent-settings.template.yml +45 -0
- package/config/gitignore-block.txt +4 -0
- package/docs/MIGRATION.md +122 -0
- package/docs/architecture.md +111 -35
- package/docs/contracts/STABILITY.md +95 -0
- package/docs/contracts/adr-chat-history-split.md +132 -0
- package/docs/contracts/adr-command-suggestion.md +146 -0
- package/docs/contracts/adr-implement-ticket-runtime.md +122 -0
- package/docs/contracts/adr-product-ui-track.md +384 -0
- package/docs/contracts/adr-prompt-driven-execution.md +187 -0
- package/docs/contracts/agent-memory-contract.md +149 -0
- package/docs/contracts/artifact-engagement-flow.md +262 -0
- package/docs/contracts/command-clusters.md +126 -0
- package/docs/contracts/command-suggestion-flow.md +148 -0
- package/docs/contracts/implement-ticket-flow.md +628 -0
- package/docs/contracts/linear-ai-rules-inclusion.md +143 -0
- package/docs/contracts/linear-ai-three-layers.md +131 -0
- package/docs/contracts/rule-interactions.md +107 -0
- package/docs/contracts/rule-interactions.yml +142 -0
- package/docs/contracts/ui-stack-extension.md +236 -0
- package/docs/contracts/ui-track-flow.md +338 -0
- package/docs/development.md +1 -1
- package/docs/getting-started.md +3 -3
- package/docs/installation.md +124 -2
- package/docs/migrations/commands-1.15.0.md +112 -0
- package/docs/showcase.md +204 -0
- package/docs/ui-track-mental-model.md +121 -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 +38 -0
- package/scripts/check_public_links.py +185 -0
- package/scripts/check_references.py +1 -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/lint_no_new_atomic_commands.py +179 -0
- package/scripts/lint_rule_interactions.py +149 -0
- package/scripts/memory_lookup.py +1 -1
- package/scripts/migrate_command_suggestions.py +151 -0
- package/scripts/release.py +297 -64
- package/scripts/schemas/command.schema.json +41 -0
- package/scripts/skill_linter.py +81 -0
- package/scripts/sync_agent_settings.py +42 -12
- package/scripts/update_counts.py +10 -0
- package/templates/consumer-settings/augment-cli-hooks.json +54 -0
- package/templates/consumer-settings/claude-settings.json +55 -1
- package/.agent-src/rules/chat-history.md +0 -171
- 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
|
@@ -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,231 @@
|
|
|
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
|
+
If the ``.bak`` slot is already taken (re-running the migration after
|
|
47
|
+
an aborted run, manual rollback, etc.) the rotator falls back to
|
|
48
|
+
``.bak.1``, ``.bak.2``, ... — see :func:`_rotate_backup_path`. The
|
|
49
|
+
migration never silently overwrites an existing backup."""
|
|
50
|
+
|
|
51
|
+
_MAX_BACKUP_ROTATIONS = 999
|
|
52
|
+
"""Hard ceiling on rotated backup filenames; surfaces an explicit
|
|
53
|
+
:class:`SchemaError` instead of looping forever if a checkout has
|
|
54
|
+
hundreds of stale backups."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _rotate_backup_path(source: Path) -> Path:
|
|
58
|
+
"""Return the next free ``.bak`` slot for ``source``.
|
|
59
|
+
|
|
60
|
+
Tries ``source.bak`` first, then ``source.bak.1``,
|
|
61
|
+
``source.bak.2``, ... up to :data:`_MAX_BACKUP_ROTATIONS`. The
|
|
62
|
+
rotator only inspects existence — collision-safe by construction —
|
|
63
|
+
and never deletes or overwrites prior backups.
|
|
64
|
+
"""
|
|
65
|
+
primary = source.with_suffix(source.suffix + BACKUP_SUFFIX)
|
|
66
|
+
if not primary.exists():
|
|
67
|
+
return primary
|
|
68
|
+
for index in range(1, _MAX_BACKUP_ROTATIONS + 1):
|
|
69
|
+
candidate = primary.with_suffix(primary.suffix + f".{index}")
|
|
70
|
+
if not candidate.exists():
|
|
71
|
+
return candidate
|
|
72
|
+
raise SchemaError(
|
|
73
|
+
f"refusing to rotate backup for {source}: more than "
|
|
74
|
+
f"{_MAX_BACKUP_ROTATIONS} stale .bak files already exist; "
|
|
75
|
+
"clean them up before re-running the migration",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def migrate_payload(payload: Any) -> dict[str, Any]:
|
|
80
|
+
"""Return the v1 form of ``payload``.
|
|
81
|
+
|
|
82
|
+
A payload that already declares ``version: 1`` is returned
|
|
83
|
+
unchanged (deep-copied via ``json.loads(json.dumps(...))`` so the
|
|
84
|
+
caller cannot accidentally mutate the input). Anything else is
|
|
85
|
+
treated as v0 and wrapped: ``ticket`` becomes ``input.data``,
|
|
86
|
+
``input.kind`` is set to ``"ticket"``, and the engine defaults are
|
|
87
|
+
filled in.
|
|
88
|
+
|
|
89
|
+
Raises
|
|
90
|
+
------
|
|
91
|
+
SchemaError
|
|
92
|
+
If the payload is not a dict, declares a higher version than
|
|
93
|
+
this migration knows about, or lacks a ``ticket`` key.
|
|
94
|
+
"""
|
|
95
|
+
if not isinstance(payload, dict):
|
|
96
|
+
raise SchemaError(
|
|
97
|
+
f"v0 state must be a JSON object; got {type(payload).__name__}",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
declared_version = payload.get("version")
|
|
101
|
+
if declared_version == SCHEMA_VERSION:
|
|
102
|
+
return json.loads(json.dumps(payload))
|
|
103
|
+
if declared_version is not None:
|
|
104
|
+
raise SchemaError(
|
|
105
|
+
f"cannot migrate from version {declared_version!r} to "
|
|
106
|
+
f"{SCHEMA_VERSION}; this script only handles v0 (no version key)",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if "ticket" not in payload:
|
|
110
|
+
raise SchemaError(
|
|
111
|
+
"v0 state must carry a 'ticket' key; got keys: "
|
|
112
|
+
f"{sorted(payload.keys())}",
|
|
113
|
+
)
|
|
114
|
+
ticket = payload["ticket"]
|
|
115
|
+
if not isinstance(ticket, dict):
|
|
116
|
+
raise SchemaError(
|
|
117
|
+
f"v0 state.ticket must be a JSON object; got {type(ticket).__name__}",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
"version": SCHEMA_VERSION,
|
|
122
|
+
"input": {"kind": "ticket", "data": ticket},
|
|
123
|
+
"intent": DEFAULT_INTENT,
|
|
124
|
+
"directive_set": DEFAULT_DIRECTIVE_SET,
|
|
125
|
+
"persona": payload.get("persona", "senior-engineer"),
|
|
126
|
+
"memory": list(payload.get("memory", [])),
|
|
127
|
+
"plan": payload.get("plan"),
|
|
128
|
+
"changes": list(payload.get("changes", [])),
|
|
129
|
+
"tests": payload.get("tests"),
|
|
130
|
+
"verify": payload.get("verify"),
|
|
131
|
+
"outcomes": dict(payload.get("outcomes", {})),
|
|
132
|
+
"questions": list(payload.get("questions", [])),
|
|
133
|
+
"report": payload.get("report", ""),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def migrate_file(
|
|
138
|
+
source: Path,
|
|
139
|
+
*,
|
|
140
|
+
destination: Path | None = None,
|
|
141
|
+
backup: bool = True,
|
|
142
|
+
) -> Path:
|
|
143
|
+
"""Migrate the v0 state file at ``source`` and write the v1 result.
|
|
144
|
+
|
|
145
|
+
``destination`` defaults to :data:`DEFAULT_V1_FILENAME` next to
|
|
146
|
+
``source``. When ``backup`` is true (the default) the original
|
|
147
|
+
file is renamed with :data:`BACKUP_SUFFIX` appended; when false,
|
|
148
|
+
the original is left untouched. The destination must not exist —
|
|
149
|
+
refusing to overwrite is the safety net against accidental
|
|
150
|
+
double-migration on CI.
|
|
151
|
+
|
|
152
|
+
Returns the destination path on success.
|
|
153
|
+
"""
|
|
154
|
+
if not source.is_file():
|
|
155
|
+
raise SchemaError(f"v0 state file not found: {source}")
|
|
156
|
+
|
|
157
|
+
raw = source.read_text(encoding="utf-8")
|
|
158
|
+
try:
|
|
159
|
+
payload = json.loads(raw)
|
|
160
|
+
except json.JSONDecodeError as exc:
|
|
161
|
+
raise SchemaError(f"invalid JSON in {source}: {exc}") from exc
|
|
162
|
+
|
|
163
|
+
target = destination or source.with_name(DEFAULT_V1_FILENAME)
|
|
164
|
+
if target.exists():
|
|
165
|
+
raise SchemaError(
|
|
166
|
+
f"refusing to overwrite existing destination {target}; "
|
|
167
|
+
"delete or rename it first",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
migrated = migrate_payload(payload)
|
|
171
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
target.write_text(
|
|
173
|
+
json.dumps(migrated, indent=2, ensure_ascii=False) + "\n",
|
|
174
|
+
encoding="utf-8",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if backup:
|
|
178
|
+
backup_path = _rotate_backup_path(source)
|
|
179
|
+
shutil.move(str(source), str(backup_path))
|
|
180
|
+
|
|
181
|
+
return target
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
185
|
+
"""CLI entry point — ``python3 -m work_engine.migration.v0_to_v1``.
|
|
186
|
+
|
|
187
|
+
Exits ``0`` on success, ``2`` on any :class:`SchemaError` so the
|
|
188
|
+
invoking shell can branch on the failure category.
|
|
189
|
+
"""
|
|
190
|
+
parser = argparse.ArgumentParser(
|
|
191
|
+
prog="work_engine.migration.v0_to_v1",
|
|
192
|
+
description="Migrate a legacy .implement-ticket-state.json file to "
|
|
193
|
+
"the v1 .work-state.json schema.",
|
|
194
|
+
)
|
|
195
|
+
parser.add_argument(
|
|
196
|
+
"source",
|
|
197
|
+
type=Path,
|
|
198
|
+
nargs="?",
|
|
199
|
+
default=Path(DEFAULT_V0_FILENAME),
|
|
200
|
+
help=f"Path to the v0 state file (default: {DEFAULT_V0_FILENAME}).",
|
|
201
|
+
)
|
|
202
|
+
parser.add_argument(
|
|
203
|
+
"--destination",
|
|
204
|
+
type=Path,
|
|
205
|
+
default=None,
|
|
206
|
+
help="Path to write the v1 file to "
|
|
207
|
+
f"(default: {DEFAULT_V1_FILENAME} next to source).",
|
|
208
|
+
)
|
|
209
|
+
parser.add_argument(
|
|
210
|
+
"--no-backup",
|
|
211
|
+
action="store_true",
|
|
212
|
+
help="Do not rename the v0 source to .bak after migration.",
|
|
213
|
+
)
|
|
214
|
+
args = parser.parse_args(argv)
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
target = migrate_file(
|
|
218
|
+
args.source,
|
|
219
|
+
destination=args.destination,
|
|
220
|
+
backup=not args.no_backup,
|
|
221
|
+
)
|
|
222
|
+
except SchemaError as exc:
|
|
223
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
224
|
+
return 2
|
|
225
|
+
|
|
226
|
+
print(f"migrated {args.source} → {target}")
|
|
227
|
+
return 0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__":
|
|
231
|
+
sys.exit(main())
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Three personas ship today, each keyed by the string already carried
|
|
4
4
|
on ``DeliveryState.persona`` (see
|
|
5
|
-
``
|
|
5
|
+
``docs/contracts/implement-ticket-flow.md#personas``):
|
|
6
6
|
|
|
7
7
|
``senior-engineer``
|
|
8
8
|
Default. Runs every step. No test widening.
|
|
@@ -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"]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Diff resolver — wrap a unified-diff payload as an :class:`Input` envelope.
|
|
2
|
+
|
|
3
|
+
The resolver is the R3 Phase 1 entry point for the "improve this screen via
|
|
4
|
+
diff/PR" surface: a user (or `/work` adapter) hands the engine a patch text,
|
|
5
|
+
the resolver normalises it, and the dispatcher routes it through the UI-track
|
|
6
|
+
``ui-improve`` directive set.
|
|
7
|
+
|
|
8
|
+
Like :mod:`work_engine.resolvers.prompt`, this module is intentionally thin:
|
|
9
|
+
it normalises and rejects garbage payloads, nothing more. Reconstruction of
|
|
10
|
+
acceptance criteria + assumptions + confidence is the job of the
|
|
11
|
+
``refine-prompt`` skill (R2 Phase 3) running against the diff once the engine
|
|
12
|
+
hits the ``refine`` step. Keeping the split sharp means the envelope shape
|
|
13
|
+
stays cheap to round-trip through state and the heavy lifting stays with the
|
|
14
|
+
agent-directive halt where it belongs.
|
|
15
|
+
|
|
16
|
+
The envelope mirrors the prompt resolver's shape so a single refiner code
|
|
17
|
+
path can read both — ``{raw, reconstructed_ac, assumptions}``. The only
|
|
18
|
+
material difference is the heuristic header check that rejects payloads
|
|
19
|
+
that obviously are not unified-diff text.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
|
|
25
|
+
from ..state import Input
|
|
26
|
+
|
|
27
|
+
KIND = "diff"
|
|
28
|
+
"""Wire value carried in :attr:`work_engine.state.Input.kind`."""
|
|
29
|
+
|
|
30
|
+
_MIN_DIFF_LEN = 1
|
|
31
|
+
"""Minimum non-whitespace character count before the heuristic runs.
|
|
32
|
+
|
|
33
|
+
The resolver is not a *quality* gate; it only rejects literally empty payloads
|
|
34
|
+
and obvious non-diffs. A semantically empty diff (e.g., headers but no hunks)
|
|
35
|
+
is still accepted so the refiner can score its tractability and surface a
|
|
36
|
+
``low``-band halt where appropriate."""
|
|
37
|
+
|
|
38
|
+
_DIFF_MARKERS = (
|
|
39
|
+
re.compile(r"^diff --git ", re.MULTILINE),
|
|
40
|
+
re.compile(r"^--- ", re.MULTILINE),
|
|
41
|
+
re.compile(r"^\+\+\+ ", re.MULTILINE),
|
|
42
|
+
re.compile(r"^@@ ", re.MULTILINE),
|
|
43
|
+
re.compile(r"^Index: ", re.MULTILINE),
|
|
44
|
+
)
|
|
45
|
+
"""Heuristic markers that flag a payload as a unified or git-style diff.
|
|
46
|
+
|
|
47
|
+
A payload qualifies if **any** marker matches — the resolver accepts unified
|
|
48
|
+
diffs (``--- ``/``+++ ``/``@@ ``), ``git diff`` output (``diff --git``), and
|
|
49
|
+
the legacy ``Index: `` SVN/CVS header. The match is anchored at line start so
|
|
50
|
+
quoted snippets inside prose ("the function `--- foo` failed") do not pass
|
|
51
|
+
the gate."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DiffResolverError(ValueError):
|
|
55
|
+
"""Raised when a payload cannot be resolved into a diff envelope."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_envelope(raw: str) -> Input:
|
|
59
|
+
"""Return an :class:`Input` carrying the raw diff + empty refinement slots.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
raw:
|
|
64
|
+
The user-supplied diff text. Whitespace is preserved verbatim — the
|
|
65
|
+
refiner reads original spacing/casing when scoring goal clarity, and
|
|
66
|
+
a unified-diff round-trip cannot tolerate normalised whitespace.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
Input
|
|
71
|
+
Envelope of shape
|
|
72
|
+
``{"kind": "diff", "data": {"raw": <raw>, "reconstructed_ac": [],
|
|
73
|
+
"assumptions": []}}``. The two empty lists are placeholders the
|
|
74
|
+
``refine-prompt`` skill writes into on the rebound from ``refine``.
|
|
75
|
+
|
|
76
|
+
Raises
|
|
77
|
+
------
|
|
78
|
+
DiffResolverError
|
|
79
|
+
If ``raw`` is not a string, contains no non-whitespace characters,
|
|
80
|
+
or does not match any :data:`_DIFF_MARKERS`. The marker check guards
|
|
81
|
+
against accidentally routing free-form prose through the diff path.
|
|
82
|
+
"""
|
|
83
|
+
if not isinstance(raw, str):
|
|
84
|
+
raise DiffResolverError(
|
|
85
|
+
f"diff must be a string; got {type(raw).__name__}",
|
|
86
|
+
)
|
|
87
|
+
if len(raw.strip()) < _MIN_DIFF_LEN:
|
|
88
|
+
raise DiffResolverError(
|
|
89
|
+
"diff is empty or whitespace-only — nothing to resolve",
|
|
90
|
+
)
|
|
91
|
+
if not any(marker.search(raw) for marker in _DIFF_MARKERS):
|
|
92
|
+
raise DiffResolverError(
|
|
93
|
+
"payload does not look like a unified diff — expected one of "
|
|
94
|
+
"'diff --git', '--- ', '+++ ', '@@ ', or 'Index: ' headers",
|
|
95
|
+
)
|
|
96
|
+
return Input(
|
|
97
|
+
kind=KIND,
|
|
98
|
+
data={
|
|
99
|
+
"raw": raw,
|
|
100
|
+
"reconstructed_ac": [],
|
|
101
|
+
"assumptions": [],
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
__all__ = ["KIND", "DiffResolverError", "build_envelope"]
|