@event4u/agent-config 1.14.0 → 1.16.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 +1 -1
- package/.agent-src/commands/bug-fix.md +3 -3
- package/.agent-src/commands/bug-investigate.md +2 -2
- package/.agent-src/commands/chat-history-checkpoint.md +3 -3
- package/.agent-src/commands/chat-history-clear.md +2 -2
- package/.agent-src/commands/chat-history-resume.md +2 -2
- package/.agent-src/commands/chat-history.md +3 -3
- package/.agent-src/commands/check-current-md.md +44 -33
- package/.agent-src/commands/commit-in-chunks.md +43 -23
- package/.agent-src/commands/compress.md +34 -2
- package/.agent-src/commands/council-design.md +96 -0
- package/.agent-src/commands/council-optimize.md +115 -0
- package/.agent-src/commands/council-pr.md +123 -0
- package/.agent-src/commands/council.md +219 -0
- package/.agent-src/commands/create-pr.md +23 -0
- package/.agent-src/commands/do-and-judge.md +3 -3
- package/.agent-src/commands/do-in-steps.md +4 -4
- package/.agent-src/commands/e2e-heal.md +1 -1
- package/.agent-src/commands/e2e-plan.md +1 -1
- package/.agent-src/commands/feature-dev.md +8 -0
- package/.agent-src/commands/feature-explore.md +6 -1
- package/.agent-src/commands/feature-plan.md +33 -2
- package/.agent-src/commands/feature-refactor.md +5 -0
- package/.agent-src/commands/feature-roadmap.md +8 -3
- package/.agent-src/commands/feature.md +58 -0
- package/.agent-src/commands/fix-ci.md +5 -0
- package/.agent-src/commands/fix-portability.md +7 -2
- package/.agent-src/commands/fix-pr-bot-comments.md +5 -0
- package/.agent-src/commands/fix-pr-comments.md +5 -0
- package/.agent-src/commands/fix-pr-developer-comments.md +5 -0
- package/.agent-src/commands/fix-references.md +5 -0
- package/.agent-src/commands/fix-seeder.md +5 -0
- package/.agent-src/commands/fix.md +60 -0
- package/.agent-src/commands/jira-ticket.md +1 -1
- package/.agent-src/commands/judge.md +1 -1
- package/.agent-src/commands/memory-add.md +3 -3
- package/.agent-src/commands/memory-full.md +2 -2
- package/.agent-src/commands/memory-promote.md +2 -2
- package/.agent-src/commands/mode.md +5 -5
- package/.agent-src/commands/onboard.md +17 -8
- package/.agent-src/commands/optimize-agents.md +6 -1
- package/.agent-src/commands/optimize-augmentignore.md +14 -0
- package/.agent-src/commands/optimize-rtk-filters.md +5 -0
- package/.agent-src/commands/optimize-skills.md +6 -1
- package/.agent-src/commands/optimize.md +54 -0
- package/.agent-src/commands/propose-memory.md +2 -2
- package/.agent-src/commands/refine-ticket.md +9 -7
- package/.agent-src/commands/review-changes.md +61 -9
- package/.agent-src/commands/review-routing.md +1 -1
- package/.agent-src/commands/roadmap-create.md +42 -4
- package/.agent-src/commands/roadmap-execute.md +9 -7
- package/.agent-src/commands/set-cost-profile.md +11 -3
- package/.agent-src/commands/sync-agent-settings.md +11 -2
- package/.agent-src/commands/tests-create.md +1 -1
- package/.agent-src/commands/tests-execute.md +2 -3
- package/.agent-src/commands/upstream-contribute.md +1 -1
- package/.agent-src/contexts/authority/commit-mechanics.md +57 -0
- package/.agent-src/contexts/authority/destructive-mechanics.md +66 -0
- package/.agent-src/contexts/authority/scope-mechanics.md +87 -0
- package/.agent-src/contexts/execution/autonomy-detection.md +54 -0
- package/.agent-src/contexts/execution/autonomy-examples.md +90 -0
- package/.agent-src/contexts/execution/autonomy-mechanics.md +29 -0
- package/.agent-src/contexts/execution/verification-mechanics.md +80 -0
- package/.agent-src/personas/README.md +1 -1
- package/.agent-src/rules/agent-authority.md +24 -0
- package/.agent-src/rules/architecture.md +1 -1
- package/.agent-src/rules/artifact-drafting-protocol.md +1 -1
- package/.agent-src/rules/artifact-engagement-recording.md +2 -2
- package/.agent-src/rules/ask-when-uncertain.md +1 -1
- package/.agent-src/rules/augment-portability.md +56 -37
- package/.agent-src/rules/autonomous-execution.md +78 -114
- package/.agent-src/rules/capture-learnings.md +1 -1
- 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 +1 -1
- package/.agent-src/rules/{command-suggestion.md → command-suggestion-policy.md} +10 -9
- package/.agent-src/rules/commit-conventions.md +1 -1
- package/.agent-src/rules/commit-policy.md +43 -61
- package/.agent-src/rules/context-hygiene.md +3 -3
- package/.agent-src/rules/direct-answers.md +2 -2
- package/.agent-src/rules/docs-sync.md +1 -1
- package/.agent-src/rules/e2e-testing.md +1 -1
- package/.agent-src/rules/guidelines.md +4 -4
- package/.agent-src/rules/improve-before-implement.md +2 -2
- package/.agent-src/rules/language-and-tone.md +41 -96
- package/.agent-src/rules/minimal-safe-diff.md +3 -3
- package/.agent-src/rules/model-recommendation.md +4 -4
- package/.agent-src/rules/no-cheap-questions.md +89 -0
- package/.agent-src/rules/non-destructive-by-default.md +25 -59
- package/.agent-src/rules/onboarding-gate.md +5 -5
- package/.agent-src/rules/review-routing-awareness.md +9 -9
- package/.agent-src/rules/roadmap-progress-sync.md +132 -80
- package/.agent-src/rules/role-mode-adherence.md +3 -3
- package/.agent-src/rules/scope-control.md +65 -46
- package/.agent-src/rules/security-sensitive-stop.md +2 -2
- package/.agent-src/rules/size-enforcement.md +3 -2
- package/.agent-src/rules/think-before-action.md +5 -5
- package/.agent-src/rules/token-efficiency.md +4 -4
- package/.agent-src/rules/{ui-audit-before-build.md → ui-audit-gate.md} +3 -3
- package/.agent-src/rules/user-interaction.md +31 -7
- package/.agent-src/rules/verify-before-complete.md +12 -67
- package/.agent-src/scripts/update_roadmap_progress.py +65 -8
- package/.agent-src/skills/ai-council/SKILL.md +333 -0
- package/.agent-src/skills/api-endpoint/SKILL.md +2 -2
- package/.agent-src/skills/blade-ui/SKILL.md +30 -11
- package/.agent-src/skills/blast-radius-analyzer/SKILL.md +1 -1
- package/.agent-src/skills/bug-analyzer/SKILL.md +1 -1
- package/.agent-src/skills/command-routing/SKILL.md +1 -1
- package/.agent-src/skills/command-writing/SKILL.md +16 -5
- package/.agent-src/skills/conventional-commits-writing/SKILL.md +1 -1
- package/.agent-src/skills/copilot-agents-optimization/SKILL.md +2 -2
- package/.agent-src/skills/developer-like-execution/SKILL.md +2 -2
- package/.agent-src/skills/existing-ui-audit/SKILL.md +24 -9
- package/.agent-src/skills/fe-design/SKILL.md +20 -15
- package/.agent-src/skills/file-editor/SKILL.md +9 -0
- package/.agent-src/skills/flux/SKILL.md +1 -1
- package/.agent-src/skills/git-workflow/SKILL.md +1 -1
- package/.agent-src/skills/guideline-writing/SKILL.md +11 -11
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +4 -4
- package/.agent-src/skills/livewire/SKILL.md +27 -8
- package/.agent-src/skills/override-management/SKILL.md +2 -2
- package/.agent-src/skills/php-coder/SKILL.md +1 -1
- package/.agent-src/skills/playwright-testing/SKILL.md +2 -2
- package/.agent-src/skills/readme-reviewer/SKILL.md +1 -1
- package/.agent-src/skills/readme-writing/SKILL.md +1 -1
- package/.agent-src/skills/readme-writing-package/SKILL.md +1 -1
- package/.agent-src/skills/receiving-code-review/SKILL.md +1 -1
- package/.agent-src/skills/refine-ticket/SKILL.md +30 -24
- package/.agent-src/skills/review-routing/SKILL.md +2 -2
- package/.agent-src/skills/roadmap-management/SKILL.md +22 -16
- package/.agent-src/skills/rule-writing/SKILL.md +1 -1
- package/.agent-src/skills/skill-reviewer/SKILL.md +1 -1
- package/.agent-src/skills/skill-writing/SKILL.md +6 -6
- package/.agent-src/skills/subagent-orchestration/SKILL.md +1 -0
- package/.agent-src/skills/systematic-debugging/SKILL.md +1 -1
- package/.agent-src/skills/upstream-contribute/SKILL.md +3 -3
- package/.agent-src/skills/validate-feature-fit/SKILL.md +2 -2
- package/.agent-src/skills/{verify-before-complete → verify-completion-evidence}/SKILL.md +2 -2
- package/.agent-src/templates/agent-settings.md +9 -9
- package/.agent-src/templates/contexts/auth-model.md +1 -1
- package/.agent-src/templates/roadmaps.md +9 -8
- package/.agent-src/templates/scripts/README.md +2 -2
- package/.agent-src/templates/scripts/memory_lookup.py +1 -1
- package/.agent-src/templates/scripts/telemetry/aggregator.py +16 -1
- package/.agent-src/templates/scripts/telemetry/engagement.py +59 -0
- package/.agent-src/templates/scripts/telemetry/report_renderer.py +28 -1
- package/.agent-src/templates/scripts/telemetry_record.py +14 -1
- package/.agent-src/templates/scripts/work_engine/__init__.py +2 -2
- package/.agent-src/templates/scripts/work_engine/cli.py +64 -461
- package/.agent-src/templates/scripts/work_engine/cli_args.py +116 -0
- package/.agent-src/templates/scripts/work_engine/delivery_state.py +3 -3
- package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/implement.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/memory.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/plan.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/report.py +1 -1
- package/.agent-src/templates/scripts/work_engine/dispatcher.py +1 -1
- 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/input_builders.py +163 -0
- package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +34 -2
- package/.agent-src/templates/scripts/work_engine/persona_policy.py +1 -1
- package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +1 -1
- package/.agent-src/templates/scripts/work_engine/state_io.py +202 -0
- package/.claude-plugin/marketplace.json +10 -2
- package/AGENTS.md +16 -12
- package/CHANGELOG.md +206 -9
- package/README.md +51 -52
- package/config/agent-settings.template.yml +58 -1
- package/config/gitignore-block.txt +3 -0
- package/docs/MIGRATION.md +122 -0
- package/docs/architecture.md +83 -34
- package/docs/catalog.md +331 -0
- package/docs/contracts/STABILITY.md +134 -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/load-context-schema.md +186 -0
- package/docs/contracts/rule-interactions.md +107 -0
- package/docs/contracts/rule-interactions.yml +238 -0
- package/docs/contracts/rule-priority-hierarchy.md +87 -0
- package/docs/contracts/ui-stack-extension.md +236 -0
- package/docs/contracts/ui-track-flow.md +338 -0
- package/docs/customization.md +14 -0
- package/docs/end-to-end-walkthroughs.md +165 -0
- package/docs/getting-started.md +27 -9
- package/docs/github-topics.md +12 -3
- package/docs/guidelines/agent-infra/language-and-tone-examples.md +79 -0
- package/{.agent-src → docs}/guidelines/docs/readme-size-and-splitting.md +26 -25
- package/docs/guidelines/php/git.md +164 -0
- package/docs/installation.md +42 -6
- package/docs/migrations/commands-1.15.0.md +112 -0
- package/docs/showcase.md +9 -4
- package/docs/skills-catalog.md +14 -8
- package/docs/ui-track-mental-model.md +121 -0
- package/llms.txt +13 -7
- package/package.json +1 -1
- package/scripts/agent-config +23 -0
- package/scripts/ai_council/__init__.py +39 -0
- package/scripts/ai_council/_default_prices.py +41 -0
- package/scripts/ai_council/_one_off_rebalancing_audit.py +149 -0
- package/scripts/ai_council/_one_off_roundtrip.py +106 -0
- package/scripts/ai_council/budget_guard.py +172 -0
- package/scripts/ai_council/bundler.py +261 -0
- package/scripts/ai_council/clients.py +381 -0
- package/scripts/ai_council/modes.py +127 -0
- package/scripts/ai_council/orchestrator.py +350 -0
- package/scripts/ai_council/pricing.py +213 -0
- package/scripts/ai_council/project_context.py +159 -0
- package/scripts/ai_council/prompts.py +232 -0
- package/scripts/ai_council/session.py +144 -0
- package/scripts/build_linear_digest.py +4 -4
- package/scripts/check_always_budget.py +126 -0
- package/scripts/check_augmentignore.py +69 -0
- package/scripts/check_command_count_messaging.py +120 -0
- package/scripts/check_portability.py +57 -0
- package/scripts/check_public_catalog_links.py +122 -0
- package/scripts/check_public_links.py +185 -0
- package/scripts/check_references.py +5 -1
- package/scripts/check_roadmap_trackable.py +111 -0
- package/scripts/command_suggester/cooldown.py +1 -1
- package/scripts/generate_index.py +266 -0
- package/scripts/install_anthropic_key.sh +5 -0
- package/scripts/install_openai_key.sh +106 -0
- package/scripts/lint_load_context.py +163 -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/release.py +297 -64
- package/scripts/schemas/command.schema.json +20 -0
- package/scripts/schemas/rule.schema.json +10 -0
- package/scripts/skill_linter.py +26 -4
- package/scripts/sync_agent_settings.py +1 -1
- package/scripts/update_counts.py +19 -4
- package/scripts/update_prices.py +124 -0
- package/.agent-src/guidelines/php/git.md +0 -96
- package/.agent-src/rules/chat-history.md +0 -200
- /package/.agent-src/rules/{slash-commands.md → slash-command-routing-policy.md} +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/agent-interaction-and-decision-quality.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/break-glass-usage.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/developer-judgment.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/engineering-memory-data-format.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/layered-settings.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/memory-access.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/naming.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/output-patterns.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/review-routing-data-format.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/role-contracts.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/role-mode-router.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/runtime-layer.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/self-improvement-pipeline.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/size-and-scope.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/tool-integration.md +0 -0
- /package/{.agent-src → docs}/guidelines/e2e/playwright.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/api-design.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/artisan-commands.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/blade-ui.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/controllers.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/database.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/eloquent.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/flux.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/general.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/jobs.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/livewire.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/logging.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/naming.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/dependency-injection.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/dtos.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/events.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/factory.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/pipelines.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/policies.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/repositories.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/service-layer.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/strategy.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/performance.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/resources.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/security.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/sql.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/validations.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/websocket.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""``DeliveryState`` — the only object shared between orchestrator steps.
|
|
2
2
|
|
|
3
|
-
The shape mirrors ``
|
|
3
|
+
The shape mirrors ``docs/contracts/implement-ticket-flow.md``. No step
|
|
4
4
|
may invent fields not declared here; extensions require a roadmap
|
|
5
5
|
amendment plus a flow-contract update.
|
|
6
6
|
|
|
@@ -59,7 +59,7 @@ class DeliveryState:
|
|
|
59
59
|
"""Canonical state passed between orchestrator steps.
|
|
60
60
|
|
|
61
61
|
Field order matches the table in
|
|
62
|
-
``
|
|
62
|
+
``docs/contracts/implement-ticket-flow.md``. Mutable defaults use
|
|
63
63
|
``field(default_factory=...)`` so every instance owns its own
|
|
64
64
|
containers — a single shared list across runs would be a
|
|
65
65
|
cross-run contamination hazard for the metrics pipeline.
|
|
@@ -103,7 +103,7 @@ skill; the user-facing numbered options follow on subsequent lines.
|
|
|
103
103
|
|
|
104
104
|
The prefix is public contract: changing it breaks every agent that
|
|
105
105
|
has learned to recognise it. See
|
|
106
|
-
``
|
|
106
|
+
``docs/contracts/implement-ticket-flow.md#agent-directives``.
|
|
107
107
|
"""
|
|
108
108
|
|
|
109
109
|
|
|
@@ -22,7 +22,7 @@ validate upstream state; the delegation gates (``plan``,
|
|
|
22
22
|
matching skill and resume. ``report`` renders the delivery Markdown
|
|
23
23
|
once everything else has succeeded. See
|
|
24
24
|
``agents/roadmaps/road-to-implement-ticket.md`` for the shipping
|
|
25
|
-
order and ``
|
|
25
|
+
order and ``docs/contracts/implement-ticket-flow.md`` for the
|
|
26
26
|
slice contracts each handler writes to.
|
|
27
27
|
"""
|
|
28
28
|
from __future__ import annotations
|
|
@@ -17,7 +17,7 @@ Flow:
|
|
|
17
17
|
- Otherwise → SUCCESS.
|
|
18
18
|
|
|
19
19
|
``changes`` entries use the loose shape described in
|
|
20
|
-
``
|
|
20
|
+
``docs/contracts/implement-ticket-flow.md#deliverystate-the-only-shared-object``
|
|
21
21
|
\u2014 each entry is a dict with at least a ``path``; optional
|
|
22
22
|
``lines`` / ``purpose`` feed the delivery report.
|
|
23
23
|
"""
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""``memory`` step — bounded retrieval over the four allowed types.
|
|
2
2
|
|
|
3
3
|
Contract (see
|
|
4
|
-
``
|
|
4
|
+
``docs/contracts/implement-ticket-flow.md#memory-retrieval-contract``):
|
|
5
5
|
|
|
6
6
|
- Four allowed types: ``domain-invariants``, ``architecture-decisions``,
|
|
7
7
|
``incident-learnings``, ``historical-patterns``.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The dispatcher cannot synthesise a plan from pure Python: the real
|
|
4
4
|
work needs code reading and judgement that only the agent has. The
|
|
5
5
|
step therefore follows the Option-A delegation pattern described in
|
|
6
|
-
``
|
|
6
|
+
``docs/contracts/implement-ticket-flow.md#agent-directives``:
|
|
7
7
|
|
|
8
8
|
- ``state.plan`` empty → halt with ``BLOCKED`` and emit
|
|
9
9
|
``@agent-directive: create-plan``. The orchestrator runs the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""``report`` step — delivery report renderer.
|
|
2
2
|
|
|
3
3
|
Produces the markdown block described in
|
|
4
|
-
``
|
|
4
|
+
``docs/contracts/implement-ticket-flow.md#delivery-report-schema``.
|
|
5
5
|
All nine headings are present on every run — the schema is stable
|
|
6
6
|
for consumers — but section bodies are omitted when the matching
|
|
7
7
|
slice of ``DeliveryState`` is empty. The single exception is the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Linear step dispatcher for ``/implement-ticket``.
|
|
2
2
|
|
|
3
3
|
The dispatcher holds no business logic. It walks the fixed eight-step
|
|
4
|
-
order declared in ``
|
|
4
|
+
order declared in ``docs/contracts/implement-ticket-flow.md``, hands
|
|
5
5
|
each step a live ``DeliveryState``, and honours the three terminal
|
|
6
6
|
outcomes:
|
|
7
7
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Stdout / stderr emitters for the CLI entry point.
|
|
2
|
+
|
|
3
|
+
Extracted from ``cli.py`` in P2.3 of
|
|
4
|
+
``road-to-post-pr29-optimize.md``. Holds the two output helpers that
|
|
5
|
+
shape the wire surface of ``main()``: the SUCCESS/halt branch printed
|
|
6
|
+
on stdout, and the lifecycle-hook halt surface printed on stderr.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from .delivery_state import Outcome
|
|
13
|
+
from .hooks import HookHalt
|
|
14
|
+
from .state import WorkState
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _emit(work: WorkState, final: Outcome, halting: str | None) -> None:
|
|
18
|
+
if final is Outcome.SUCCESS:
|
|
19
|
+
print(work.report)
|
|
20
|
+
return
|
|
21
|
+
print(f"[halt] outcome={final.value} step={halting or '(none)'}")
|
|
22
|
+
for line in work.questions:
|
|
23
|
+
print(line)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _emit_halt(halt: HookHalt) -> int:
|
|
27
|
+
"""Render a :class:`HookHalt` surface to stderr and return exit 2.
|
|
28
|
+
|
|
29
|
+
Per the P3 halt branch table, every CLI-layer halt yields exit code
|
|
30
|
+
``2`` regardless of which event fired it. State persistence is
|
|
31
|
+
governed by *where* in ``main`` the halt is detected: the call site
|
|
32
|
+
decides whether ``_save`` already ran. This helper is the single
|
|
33
|
+
place that formats the surface so the wire output stays consistent.
|
|
34
|
+
"""
|
|
35
|
+
if halt.surface:
|
|
36
|
+
for line in halt.surface:
|
|
37
|
+
print(line, file=sys.stderr)
|
|
38
|
+
else:
|
|
39
|
+
print(f"halt: {halt.reason}", file=sys.stderr)
|
|
40
|
+
return 2
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = ["_emit", "_emit_halt"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""CLI-layer error type used by the dispatcher entry point.
|
|
2
|
+
|
|
3
|
+
Lives in its own module so the helper modules (``state_io``,
|
|
4
|
+
``input_builders``, etc.) can raise it without depending on
|
|
5
|
+
``cli.py``, which would create an import cycle.
|
|
6
|
+
|
|
7
|
+
Behaviour is identical to the original ``cli._CLIError`` it replaced
|
|
8
|
+
in P2.3 of ``road-to-post-pr29-optimize.md`` — same name (private,
|
|
9
|
+
underscore-prefixed) and same role: convert to exit code ``2`` at the
|
|
10
|
+
``main()`` boundary.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _CLIError(Exception):
|
|
16
|
+
"""Raised on configuration or I/O problems. Converted to exit code 2."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ["_CLIError"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Lifecycle-hook registry assembly for the CLI entry point.
|
|
2
|
+
|
|
3
|
+
Extracted from ``cli.py`` in P2.3 of
|
|
4
|
+
``road-to-post-pr29-optimize.md``. Owns nothing but
|
|
5
|
+
``_build_hook_registry`` and its chat-history helper. The function
|
|
6
|
+
remains re-exported from ``work_engine.cli`` so the existing test
|
|
7
|
+
import (``from work_engine.cli import _build_hook_registry``) and
|
|
8
|
+
monkeypatch target (``work_engine.cli._build_hook_registry``) keep
|
|
9
|
+
working without a breaking change.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .hooks import HookRegistry
|
|
17
|
+
from .hooks.builtin import (
|
|
18
|
+
ChatHistoryAppendHook,
|
|
19
|
+
ChatHistoryHaltAppendHook,
|
|
20
|
+
ChatHistoryHeartbeatHook,
|
|
21
|
+
ChatHistoryTurnCheckHook,
|
|
22
|
+
DirectiveSetGuardHook,
|
|
23
|
+
HaltSurfaceAuditHook,
|
|
24
|
+
StateShapeValidationHook,
|
|
25
|
+
TraceHook,
|
|
26
|
+
)
|
|
27
|
+
from .hooks.settings import HookSettings, load_hook_settings
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _build_hook_registry(args: argparse.Namespace) -> HookRegistry:
|
|
31
|
+
"""Build the CLI-side :class:`HookRegistry` for one ``main()`` run.
|
|
32
|
+
|
|
33
|
+
Reads ``hooks.*`` from ``.agent-settings.yml`` and registers the
|
|
34
|
+
enabled hooks. The master switch ``hooks.enabled`` defaults to
|
|
35
|
+
``False`` when the block (or the file) is missing — the registry
|
|
36
|
+
stays empty and golden replay flows are byte-stable.
|
|
37
|
+
|
|
38
|
+
``--no-hooks`` on the CLI forces an empty registry regardless of
|
|
39
|
+
settings, which is the explicit escape hatch golden-replay test
|
|
40
|
+
harnesses can use.
|
|
41
|
+
"""
|
|
42
|
+
registry = HookRegistry()
|
|
43
|
+
if getattr(args, "no_hooks", False):
|
|
44
|
+
return registry
|
|
45
|
+
|
|
46
|
+
settings_path = getattr(args, "hooks_config", None)
|
|
47
|
+
settings = load_hook_settings(settings_path)
|
|
48
|
+
if not settings.enabled:
|
|
49
|
+
return registry
|
|
50
|
+
|
|
51
|
+
if settings.trace:
|
|
52
|
+
TraceHook().register(registry)
|
|
53
|
+
if settings.halt_surface_audit:
|
|
54
|
+
HaltSurfaceAuditHook().register(registry)
|
|
55
|
+
if settings.state_shape_validation:
|
|
56
|
+
StateShapeValidationHook().register(registry)
|
|
57
|
+
if settings.directive_set_guard:
|
|
58
|
+
DirectiveSetGuardHook().register(registry)
|
|
59
|
+
if settings.chat_history_enabled:
|
|
60
|
+
_register_chat_history_hooks(registry, settings)
|
|
61
|
+
|
|
62
|
+
return registry
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _register_chat_history_hooks(
|
|
66
|
+
registry: HookRegistry, settings: HookSettings,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Register the four chat-history hooks bound to the configured script."""
|
|
69
|
+
script = Path(settings.chat_history_script)
|
|
70
|
+
ChatHistoryTurnCheckHook(script).register(registry)
|
|
71
|
+
ChatHistoryAppendHook(script).register(registry)
|
|
72
|
+
ChatHistoryHaltAppendHook(script).register(registry)
|
|
73
|
+
ChatHistoryHeartbeatHook(script).register(registry)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
__all__ = ["_build_hook_registry", "_register_chat_history_hooks"]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""File-based input builders and the load-or-build dispatch helper.
|
|
2
|
+
|
|
3
|
+
Extracted from ``cli.py`` in P2.3 of
|
|
4
|
+
``road-to-post-pr29-optimize.md``. Owns the CLI's "first run" path:
|
|
5
|
+
when no state file exists, build a fresh :class:`WorkState` from
|
|
6
|
+
``--ticket-file``, ``--prompt-file``, ``--diff-file`` or
|
|
7
|
+
``--file-file``. Every builder is byte-identical in behaviour to the
|
|
8
|
+
pre-split version — the resolvers it calls and the persona / routing
|
|
9
|
+
post-processing did not move.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .cli_args import _FMT_V0, _FMT_V1
|
|
17
|
+
from .errors import _CLIError
|
|
18
|
+
from .intent import populate_routing
|
|
19
|
+
from .resolvers.diff import DiffResolverError, build_envelope as _build_diff_envelope
|
|
20
|
+
from .resolvers.file import FileResolverError, build_envelope as _build_file_envelope
|
|
21
|
+
from .resolvers.prompt import PromptResolverError, build_envelope as _build_prompt_envelope
|
|
22
|
+
from .state import Input, WorkState
|
|
23
|
+
from .state_io import _load, _maybe_raise_legacy_hint, _read_json
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_or_build(
|
|
27
|
+
state_file: Path,
|
|
28
|
+
args: argparse.Namespace,
|
|
29
|
+
) -> tuple[WorkState, str]:
|
|
30
|
+
"""Return the WorkState to dispatch against plus its wire format.
|
|
31
|
+
|
|
32
|
+
Either loaded from ``state_file`` (format-preserving) or freshly
|
|
33
|
+
built from ``--ticket-file`` (R1), ``--prompt-file`` (R2),
|
|
34
|
+
``--diff-file`` (R3) or ``--file-file`` (R3). Fresh ticket files
|
|
35
|
+
default to v0 wire format so that newly captured Goldens stay
|
|
36
|
+
byte-equal with the pre-Phase-4 baseline; the prompt / diff / file
|
|
37
|
+
paths emit v1 directly (v0 has no envelope concept for these
|
|
38
|
+
kinds). v1 round-trips for state files already on disk in v1 shape.
|
|
39
|
+
"""
|
|
40
|
+
if state_file.exists():
|
|
41
|
+
return _load(state_file)
|
|
42
|
+
_maybe_raise_legacy_hint(state_file)
|
|
43
|
+
inputs = [
|
|
44
|
+
("--ticket-file", args.ticket_file),
|
|
45
|
+
("--prompt-file", args.prompt_file),
|
|
46
|
+
("--diff-file", args.diff_file),
|
|
47
|
+
("--file-file", args.file_file),
|
|
48
|
+
]
|
|
49
|
+
supplied = [name for name, value in inputs if value is not None]
|
|
50
|
+
if len(supplied) > 1:
|
|
51
|
+
raise _CLIError(
|
|
52
|
+
f"{', '.join(supplied)} are mutually exclusive; pass exactly "
|
|
53
|
+
"one when building an initial state.",
|
|
54
|
+
)
|
|
55
|
+
if not supplied:
|
|
56
|
+
raise _CLIError(
|
|
57
|
+
f"No state file at {state_file} and no --ticket-file, "
|
|
58
|
+
"--prompt-file, --diff-file, or --file-file given; cannot "
|
|
59
|
+
"build an initial state.",
|
|
60
|
+
)
|
|
61
|
+
if args.prompt_file is not None:
|
|
62
|
+
return _build_from_prompt_file(args), _FMT_V1
|
|
63
|
+
if args.diff_file is not None:
|
|
64
|
+
return _build_from_diff_file(args), _FMT_V1
|
|
65
|
+
if args.file_file is not None:
|
|
66
|
+
return _build_from_file_file(args), _FMT_V1
|
|
67
|
+
ticket = _read_json(args.ticket_file)
|
|
68
|
+
if not isinstance(ticket, dict):
|
|
69
|
+
raise _CLIError(
|
|
70
|
+
f"--ticket-file must carry a JSON object; got {type(ticket).__name__}.",
|
|
71
|
+
)
|
|
72
|
+
work = WorkState(input=Input(kind="ticket", data=ticket))
|
|
73
|
+
if args.persona:
|
|
74
|
+
work.persona = args.persona
|
|
75
|
+
populate_routing(work)
|
|
76
|
+
return work, _FMT_V0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _build_from_prompt_file(args: argparse.Namespace) -> WorkState:
|
|
80
|
+
"""Read ``--prompt-file`` as raw text and wrap it in a prompt envelope.
|
|
81
|
+
|
|
82
|
+
The file is read verbatim (UTF-8) and handed to the prompt resolver,
|
|
83
|
+
which validates non-emptiness and returns the canonical
|
|
84
|
+
``Input(kind="prompt", data={raw, reconstructed_ac, assumptions})``
|
|
85
|
+
envelope. Persona is honoured the same way as the ticket path.
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
raw = args.prompt_file.read_text(encoding="utf-8")
|
|
89
|
+
except OSError as exc:
|
|
90
|
+
raise _CLIError(f"Cannot read {args.prompt_file}: {exc}") from exc
|
|
91
|
+
try:
|
|
92
|
+
envelope = _build_prompt_envelope(raw)
|
|
93
|
+
except PromptResolverError as exc:
|
|
94
|
+
raise _CLIError(f"--prompt-file is not a valid prompt: {exc}") from exc
|
|
95
|
+
work = WorkState(input=envelope)
|
|
96
|
+
if args.persona:
|
|
97
|
+
work.persona = args.persona
|
|
98
|
+
populate_routing(work)
|
|
99
|
+
return work
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _build_from_diff_file(args: argparse.Namespace) -> WorkState:
|
|
103
|
+
"""Read ``--diff-file`` as raw text and wrap it in a diff envelope.
|
|
104
|
+
|
|
105
|
+
The file is read verbatim (UTF-8) and handed to the diff resolver,
|
|
106
|
+
which validates the unified-diff header heuristic and returns the
|
|
107
|
+
canonical
|
|
108
|
+
``Input(kind="diff", data={raw, reconstructed_ac, assumptions})``
|
|
109
|
+
envelope. ``populate_routing`` then routes the envelope to the
|
|
110
|
+
UI-improve directive set without running the prose classifier — see
|
|
111
|
+
:mod:`work_engine.intent.classify` for the routing contract.
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
raw = args.diff_file.read_text(encoding="utf-8")
|
|
115
|
+
except OSError as exc:
|
|
116
|
+
raise _CLIError(f"Cannot read {args.diff_file}: {exc}") from exc
|
|
117
|
+
try:
|
|
118
|
+
envelope = _build_diff_envelope(raw)
|
|
119
|
+
except DiffResolverError as exc:
|
|
120
|
+
raise _CLIError(f"--diff-file is not a valid diff: {exc}") from exc
|
|
121
|
+
work = WorkState(input=envelope)
|
|
122
|
+
if args.persona:
|
|
123
|
+
work.persona = args.persona
|
|
124
|
+
populate_routing(work)
|
|
125
|
+
return work
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _build_from_file_file(args: argparse.Namespace) -> WorkState:
|
|
129
|
+
"""Read ``--file-file`` as a single-line path and wrap it in a file envelope.
|
|
130
|
+
|
|
131
|
+
The file is read verbatim (UTF-8); the first non-empty line is taken
|
|
132
|
+
as the path reference and handed to the file resolver, which
|
|
133
|
+
validates path shape (non-empty, NUL-free, not a URL) and returns
|
|
134
|
+
the canonical
|
|
135
|
+
``Input(kind="file", data={path, reconstructed_ac, assumptions})``
|
|
136
|
+
envelope. Trailing whitespace and additional lines are ignored —
|
|
137
|
+
the resolver treats the file's content as the path itself, not as
|
|
138
|
+
structured payload.
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
raw = args.file_file.read_text(encoding="utf-8")
|
|
142
|
+
except OSError as exc:
|
|
143
|
+
raise _CLIError(f"Cannot read {args.file_file}: {exc}") from exc
|
|
144
|
+
path = raw.strip().splitlines()[0] if raw.strip() else ""
|
|
145
|
+
try:
|
|
146
|
+
envelope = _build_file_envelope(path)
|
|
147
|
+
except FileResolverError as exc:
|
|
148
|
+
raise _CLIError(
|
|
149
|
+
f"--file-file does not carry a valid path: {exc}",
|
|
150
|
+
) from exc
|
|
151
|
+
work = WorkState(input=envelope)
|
|
152
|
+
if args.persona:
|
|
153
|
+
work.persona = args.persona
|
|
154
|
+
populate_routing(work)
|
|
155
|
+
return work
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
__all__ = [
|
|
159
|
+
"_build_from_diff_file",
|
|
160
|
+
"_build_from_file_file",
|
|
161
|
+
"_build_from_prompt_file",
|
|
162
|
+
"_load_or_build",
|
|
163
|
+
]
|
|
@@ -41,7 +41,39 @@ DEFAULT_V1_FILENAME = ".work-state.json"
|
|
|
41
41
|
"""Canonical filename for the v1 wire format."""
|
|
42
42
|
|
|
43
43
|
BACKUP_SUFFIX = ".bak"
|
|
44
|
-
"""Appended to the v0 source path when the migration archives it.
|
|
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
|
+
)
|
|
45
77
|
|
|
46
78
|
|
|
47
79
|
def migrate_payload(payload: Any) -> dict[str, Any]:
|
|
@@ -143,7 +175,7 @@ def migrate_file(
|
|
|
143
175
|
)
|
|
144
176
|
|
|
145
177
|
if backup:
|
|
146
|
-
backup_path =
|
|
178
|
+
backup_path = _rotate_backup_path(source)
|
|
147
179
|
shutil.move(str(source), str(backup_path))
|
|
148
180
|
|
|
149
181
|
return target
|
|
@@ -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.
|
|
@@ -15,7 +15,7 @@ Why split the resolver from the refiner:
|
|
|
15
15
|
it without touching the LLM-facing skill harness.
|
|
16
16
|
- The refiner runs inside the dispatcher loop and is allowed to halt
|
|
17
17
|
(medium-confidence assumptions report, low-confidence one-question
|
|
18
|
-
block) per :doc:`
|
|
18
|
+
block) per :doc:`docs/contracts/implement-ticket-flow.md`. That
|
|
19
19
|
control-flow surface does not belong in a resolver.
|
|
20
20
|
|
|
21
21
|
Future R3 resolvers (``diff``, ``file``) follow the same pattern: thin
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""State-file I/O helpers for the CLI entry point.
|
|
2
|
+
|
|
3
|
+
Extracted from ``cli.py`` in P2.3 of
|
|
4
|
+
``road-to-post-pr29-optimize.md``. Holds the format-preserving
|
|
5
|
+
load/save pair, the v0 legacy serialiser, the JSON reader, the
|
|
6
|
+
``DeliveryState`` projection helpers, and the legacy-file migration
|
|
7
|
+
hint. Behaviour is byte-identical to the pre-split version — Goldens
|
|
8
|
+
stay green.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from . import state as _state_module
|
|
17
|
+
from .cli_args import DEFAULT_STATE_FILE, LEGACY_STATE_FILE, _FMT_V0, _FMT_V1
|
|
18
|
+
from .delivery_state import DeliveryState
|
|
19
|
+
from .errors import _CLIError
|
|
20
|
+
from .migration.v0_to_v1 import migrate_payload
|
|
21
|
+
from .state import SchemaError, WorkState
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _maybe_raise_legacy_hint(state_file: Path) -> None:
|
|
25
|
+
"""Surface a migration hint when only the pre-1.15.0 file is present.
|
|
26
|
+
|
|
27
|
+
The dispatcher renamed the default state file from
|
|
28
|
+
``.implement-ticket-state.json`` to ``.work-state.json`` in 1.15.0
|
|
29
|
+
(alongside the ``implement_ticket → work_engine`` package move).
|
|
30
|
+
Existing checkouts that still carry the legacy file would otherwise
|
|
31
|
+
fail with a generic "no state file" message. This helper detects
|
|
32
|
+
the legacy file in the same directory and points the user at the
|
|
33
|
+
one-shot migration command instead.
|
|
34
|
+
|
|
35
|
+
Only fires when ``state_file`` has the canonical default name and
|
|
36
|
+
sits next to a legacy file — explicit ``--state-file`` overrides
|
|
37
|
+
bypass the hint so power users can carry their own naming scheme.
|
|
38
|
+
"""
|
|
39
|
+
if state_file.name != DEFAULT_STATE_FILE.name:
|
|
40
|
+
return
|
|
41
|
+
legacy_candidate = state_file.with_name(LEGACY_STATE_FILE.name)
|
|
42
|
+
if not legacy_candidate.is_file():
|
|
43
|
+
return
|
|
44
|
+
raise _CLIError(
|
|
45
|
+
f"Found legacy state file {legacy_candidate} but no "
|
|
46
|
+
f"{state_file}. The default state file was renamed in 1.15.0. "
|
|
47
|
+
f"Run `python3 -m work_engine.migration.v0_to_v1 "
|
|
48
|
+
f"{legacy_candidate}` to migrate, or pass `--state-file "
|
|
49
|
+
f"{legacy_candidate}` to keep using the old name. See "
|
|
50
|
+
"docs/MIGRATION.md.",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _load(state_file: Path) -> tuple[WorkState, str]:
|
|
55
|
+
"""Load ``state_file`` and tag it with the wire format detected."""
|
|
56
|
+
data = _read_json(state_file)
|
|
57
|
+
if not isinstance(data, dict):
|
|
58
|
+
raise _CLIError(
|
|
59
|
+
f"State file {state_file} must carry a JSON object; "
|
|
60
|
+
f"got {type(data).__name__}.",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# v1 declares ``version``; v0 has none. Anything else is invalid.
|
|
64
|
+
if data.get("version") == _state_module.SCHEMA_VERSION:
|
|
65
|
+
try:
|
|
66
|
+
return _state_module.from_dict(data), _FMT_V1
|
|
67
|
+
except SchemaError as exc:
|
|
68
|
+
raise _CLIError(f"State file shape is invalid: {exc}") from exc
|
|
69
|
+
if "version" in data:
|
|
70
|
+
raise _CLIError(
|
|
71
|
+
f"State file shape is invalid: unsupported version "
|
|
72
|
+
f"{data.get('version')!r}; expected {_state_module.SCHEMA_VERSION}",
|
|
73
|
+
)
|
|
74
|
+
if "ticket" not in data:
|
|
75
|
+
raise _CLIError(
|
|
76
|
+
"State file shape is invalid: missing 'ticket' (v0) or "
|
|
77
|
+
"'version' (v1) — file is neither shape.",
|
|
78
|
+
)
|
|
79
|
+
try:
|
|
80
|
+
migrated = migrate_payload(data)
|
|
81
|
+
return _state_module.from_dict(migrated), _FMT_V0
|
|
82
|
+
except SchemaError as exc:
|
|
83
|
+
raise _CLIError(f"State file shape is invalid: {exc}") from exc
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _to_delivery(work: WorkState) -> DeliveryState:
|
|
87
|
+
"""Project ``work`` into a ``DeliveryState`` for handler dispatch.
|
|
88
|
+
|
|
89
|
+
R1 P4 S1 (Option A2): handlers continue to consume ``DeliveryState``
|
|
90
|
+
with ``state.ticket``; the ``WorkState`` wrapper exists at the CLI
|
|
91
|
+
boundary so the dispatcher's directive-set selection has a v1
|
|
92
|
+
state object to read ``directive_set`` from. Mutable containers
|
|
93
|
+
(``memory``, ``changes``, ``outcomes``, ``questions``) are passed
|
|
94
|
+
by reference — in-place mutations land on both objects without an
|
|
95
|
+
explicit sync. Reassignments (``state.plan = …``, ``state.report
|
|
96
|
+
= …``) are mirrored back by :func:`_sync_back`.
|
|
97
|
+
"""
|
|
98
|
+
return DeliveryState(
|
|
99
|
+
ticket=work.input.data,
|
|
100
|
+
persona=work.persona,
|
|
101
|
+
memory=work.memory,
|
|
102
|
+
plan=work.plan,
|
|
103
|
+
changes=work.changes,
|
|
104
|
+
tests=work.tests,
|
|
105
|
+
verify=work.verify,
|
|
106
|
+
outcomes=work.outcomes,
|
|
107
|
+
questions=work.questions,
|
|
108
|
+
report=work.report,
|
|
109
|
+
ui_audit=work.ui_audit,
|
|
110
|
+
ui_design=work.ui_design,
|
|
111
|
+
ui_review=work.ui_review,
|
|
112
|
+
ui_polish=work.ui_polish,
|
|
113
|
+
contract=work.contract,
|
|
114
|
+
stitch=work.stitch,
|
|
115
|
+
stack=work.stack,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _sync_back(work: WorkState, delivery: DeliveryState) -> None:
|
|
120
|
+
"""Mirror handler mutations from ``delivery`` back into ``work``.
|
|
121
|
+
|
|
122
|
+
Container fields are shared by reference (see :func:`_to_delivery`)
|
|
123
|
+
so the assignment is a no-op for those — we still mirror them
|
|
124
|
+
defensively to cover the case where a handler reassigned the
|
|
125
|
+
attribute (``state.memory = [new_list]``) instead of mutating in
|
|
126
|
+
place.
|
|
127
|
+
"""
|
|
128
|
+
work.input.data = delivery.ticket
|
|
129
|
+
work.persona = delivery.persona
|
|
130
|
+
work.memory = delivery.memory
|
|
131
|
+
work.plan = delivery.plan
|
|
132
|
+
work.changes = delivery.changes
|
|
133
|
+
work.tests = delivery.tests
|
|
134
|
+
work.verify = delivery.verify
|
|
135
|
+
work.outcomes = delivery.outcomes
|
|
136
|
+
work.questions = delivery.questions
|
|
137
|
+
work.report = delivery.report
|
|
138
|
+
work.ui_audit = delivery.ui_audit
|
|
139
|
+
work.ui_design = delivery.ui_design
|
|
140
|
+
work.ui_review = delivery.ui_review
|
|
141
|
+
work.ui_polish = delivery.ui_polish
|
|
142
|
+
work.contract = delivery.contract
|
|
143
|
+
work.stitch = delivery.stitch
|
|
144
|
+
work.stack = delivery.stack
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _save(state_file: Path, work: WorkState, fmt: str) -> None:
|
|
148
|
+
"""Persist ``work`` in the wire format it was loaded with.
|
|
149
|
+
|
|
150
|
+
v1 emits the canonical envelope via :func:`work_engine.state.to_dict`;
|
|
151
|
+
v0 emits the legacy flat shape that ``DeliveryState.asdict`` used
|
|
152
|
+
to produce, byte-identical to the pre-Phase-4 output so the
|
|
153
|
+
Golden Transcript replay stays green.
|
|
154
|
+
"""
|
|
155
|
+
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
payload = _state_module.to_dict(work) if fmt == _FMT_V1 else _to_v0_dict(work)
|
|
157
|
+
state_file.write_text(
|
|
158
|
+
json.dumps(payload, indent=2, ensure_ascii=False) + "\n",
|
|
159
|
+
encoding="utf-8",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _to_v0_dict(work: WorkState) -> dict[str, Any]:
|
|
164
|
+
"""Serialise ``work`` in the legacy v0 wire format.
|
|
165
|
+
|
|
166
|
+
Field order matches ``DeliveryState`` declaration order so
|
|
167
|
+
pre-Phase-4 state files round-trip byte-equal.
|
|
168
|
+
"""
|
|
169
|
+
return {
|
|
170
|
+
"ticket": work.input.data,
|
|
171
|
+
"persona": work.persona,
|
|
172
|
+
"memory": work.memory,
|
|
173
|
+
"plan": work.plan,
|
|
174
|
+
"changes": work.changes,
|
|
175
|
+
"tests": work.tests,
|
|
176
|
+
"verify": work.verify,
|
|
177
|
+
"outcomes": work.outcomes,
|
|
178
|
+
"questions": work.questions,
|
|
179
|
+
"report": work.report,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _read_json(path: Path):
|
|
184
|
+
try:
|
|
185
|
+
raw = path.read_text(encoding="utf-8")
|
|
186
|
+
except OSError as exc:
|
|
187
|
+
raise _CLIError(f"Cannot read {path}: {exc}") from exc
|
|
188
|
+
try:
|
|
189
|
+
return json.loads(raw)
|
|
190
|
+
except json.JSONDecodeError as exc:
|
|
191
|
+
raise _CLIError(f"Invalid JSON in {path}: {exc}") from exc
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
__all__ = [
|
|
195
|
+
"_load",
|
|
196
|
+
"_maybe_raise_legacy_hint",
|
|
197
|
+
"_read_json",
|
|
198
|
+
"_save",
|
|
199
|
+
"_sync_back",
|
|
200
|
+
"_to_delivery",
|
|
201
|
+
"_to_v0_dict",
|
|
202
|
+
]
|