@event4u/agent-config 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent-src/commands/agent-handoff.md +3 -0
- package/.agent-src/commands/agent-status.md +3 -0
- package/.agent-src/commands/agents-audit.md +4 -0
- package/.agent-src/commands/agents-cleanup.md +6 -1
- package/.agent-src/commands/agents-prepare.md +3 -0
- package/.agent-src/commands/analyze-reference-repo.md +4 -0
- package/.agent-src/commands/bug-fix.md +5 -1
- package/.agent-src/commands/bug-investigate.md +4 -0
- package/.agent-src/commands/chat-history-checkpoint.md +126 -0
- package/.agent-src/commands/chat-history-clear.md +5 -0
- package/.agent-src/commands/chat-history-resume.md +5 -0
- package/.agent-src/commands/chat-history.md +5 -0
- package/.agent-src/commands/check-current-md.md +126 -0
- package/.agent-src/commands/commit-in-chunks.md +98 -0
- package/.agent-src/commands/commit.md +4 -0
- package/.agent-src/commands/compress.md +3 -0
- package/.agent-src/commands/context-create.md +4 -0
- package/.agent-src/commands/context-refactor.md +4 -0
- package/.agent-src/commands/copilot-agents-init.md +3 -0
- package/.agent-src/commands/copilot-agents-optimize.md +3 -0
- package/.agent-src/commands/create-pr-description.md +4 -0
- package/.agent-src/commands/create-pr.md +4 -0
- package/.agent-src/commands/do-and-judge.md +4 -1
- package/.agent-src/commands/do-in-steps.md +3 -0
- package/.agent-src/commands/e2e-heal.md +4 -0
- package/.agent-src/commands/e2e-plan.md +4 -0
- package/.agent-src/commands/estimate-ticket.md +4 -1
- package/.agent-src/commands/feature-dev.md +4 -0
- package/.agent-src/commands/feature-explore.md +4 -0
- package/.agent-src/commands/feature-plan.md +4 -0
- package/.agent-src/commands/feature-refactor.md +4 -0
- package/.agent-src/commands/feature-roadmap.md +6 -0
- package/.agent-src/commands/fix-ci.md +4 -0
- package/.agent-src/commands/fix-portability.md +3 -0
- package/.agent-src/commands/fix-pr-bot-comments.md +4 -0
- package/.agent-src/commands/fix-pr-comments.md +4 -0
- package/.agent-src/commands/fix-pr-developer-comments.md +4 -0
- package/.agent-src/commands/fix-references.md +3 -0
- package/.agent-src/commands/fix-seeder.md +4 -0
- package/.agent-src/commands/implement-ticket.md +39 -13
- package/.agent-src/commands/jira-ticket.md +4 -0
- package/.agent-src/commands/judge.md +3 -0
- package/.agent-src/commands/memory-add.md +5 -3
- package/.agent-src/commands/memory-full.md +5 -2
- package/.agent-src/commands/memory-promote.md +7 -6
- package/.agent-src/commands/mode.md +3 -0
- package/.agent-src/commands/module-create.md +4 -0
- package/.agent-src/commands/module-explore.md +4 -0
- package/.agent-src/commands/onboard.md +24 -0
- package/.agent-src/commands/optimize-agents.md +4 -0
- package/.agent-src/commands/optimize-augmentignore.md +3 -0
- package/.agent-src/commands/optimize-rtk-filters.md +3 -0
- package/.agent-src/commands/optimize-skills.md +4 -0
- package/.agent-src/commands/override-create.md +4 -0
- package/.agent-src/commands/override-manage.md +4 -0
- package/.agent-src/commands/package-reset.md +3 -0
- package/.agent-src/commands/package-test.md +3 -0
- package/.agent-src/commands/prepare-for-review.md +4 -0
- package/.agent-src/commands/project-analyze.md +4 -0
- package/.agent-src/commands/project-health.md +4 -0
- package/.agent-src/commands/propose-memory.md +6 -8
- package/.agent-src/commands/quality-fix.md +4 -0
- package/.agent-src/commands/refine-ticket.md +4 -1
- package/.agent-src/commands/review-changes.md +4 -0
- package/.agent-src/commands/review-routing.md +4 -0
- package/.agent-src/commands/roadmap-create.md +7 -0
- package/.agent-src/commands/roadmap-execute.md +12 -1
- package/.agent-src/commands/rule-compliance-audit.md +4 -0
- package/.agent-src/commands/set-cost-profile.md +3 -0
- package/.agent-src/commands/sync-agent-settings.md +3 -0
- package/.agent-src/commands/sync-gitignore.md +3 -0
- package/.agent-src/commands/tests-create.md +4 -0
- package/.agent-src/commands/tests-execute.md +4 -0
- package/.agent-src/commands/threat-model.md +4 -0
- package/.agent-src/commands/update-form-request-messages.md +4 -0
- package/.agent-src/commands/upstream-contribute.md +4 -0
- package/.agent-src/commands/work.md +161 -0
- package/.agent-src/guidelines/agent-infra/engineering-memory-data-format.md +2 -6
- package/.agent-src/guidelines/agent-infra/layered-settings.md +0 -1
- package/.agent-src/guidelines/agent-infra/memory-access.md +0 -7
- package/.agent-src/guidelines/agent-infra/role-contracts.md +2 -4
- package/.agent-src/guidelines/agent-infra/self-improvement-pipeline.md +0 -1
- package/.agent-src/guidelines/php/patterns/strategy.md +180 -2
- package/.agent-src/personas/README.md +0 -1
- package/.agent-src/rules/artifact-drafting-protocol.md +7 -2
- package/.agent-src/rules/artifact-engagement-recording.md +133 -0
- package/.agent-src/rules/ask-when-uncertain.md +18 -13
- package/.agent-src/rules/augment-portability.md +8 -0
- package/.agent-src/rules/autonomous-execution.md +158 -0
- package/.agent-src/rules/chat-history.md +147 -118
- package/.agent-src/rules/cli-output-handling.md +26 -3
- package/.agent-src/rules/command-suggestion.md +133 -0
- package/.agent-src/rules/commit-policy.md +99 -0
- package/.agent-src/rules/direct-answers.md +114 -0
- package/.agent-src/rules/docs-sync.md +36 -0
- package/.agent-src/rules/downstream-changes.md +10 -9
- package/.agent-src/rules/improve-before-implement.md +9 -6
- package/.agent-src/rules/language-and-tone.md +81 -6
- package/.agent-src/rules/non-destructive-by-default.md +117 -0
- package/.agent-src/rules/package-ci-checks.md +4 -0
- package/.agent-src/rules/preservation-guard.md +20 -0
- package/.agent-src/rules/roadmap-progress-sync.md +103 -30
- package/.agent-src/rules/scope-control.md +42 -1
- package/.agent-src/rules/size-enforcement.md +1 -3
- package/.agent-src/rules/skill-quality.md +3 -8
- package/.agent-src/rules/ui-audit-before-build.md +106 -0
- package/.agent-src/rules/user-interaction.md +82 -50
- package/.agent-src/scripts/update_roadmap_progress.py +17 -5
- package/.agent-src/skills/blade-ui/SKILL.md +30 -5
- package/.agent-src/skills/command-routing/SKILL.md +32 -0
- package/.agent-src/skills/command-writing/SKILL.md +41 -2
- package/.agent-src/skills/description-assist/SKILL.md +21 -0
- package/.agent-src/skills/estimate-ticket/SKILL.md +0 -1
- package/.agent-src/skills/existing-ui-audit/SKILL.md +187 -0
- package/.agent-src/skills/fe-design/SKILL.md +72 -60
- package/.agent-src/skills/finishing-a-development-branch/SKILL.md +4 -0
- package/.agent-src/skills/flux/SKILL.md +31 -4
- package/.agent-src/skills/guideline-writing/SKILL.md +24 -2
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +51 -9
- package/.agent-src/skills/livewire/SKILL.md +30 -4
- package/.agent-src/skills/md-language-check/SKILL.md +103 -0
- package/.agent-src/skills/php-coder/SKILL.md +24 -0
- package/.agent-src/skills/react-shadcn-ui/SKILL.md +121 -0
- package/.agent-src/skills/refine-prompt/SKILL.md +220 -0
- package/.agent-src/skills/refine-ticket/SKILL.md +2 -4
- package/.agent-src/skills/roadmap-management/SKILL.md +10 -3
- package/.agent-src/skills/rule-writing/SKILL.md +23 -1
- package/.agent-src/skills/skill-writing/SKILL.md +1 -3
- package/.agent-src/skills/upstream-contribute/SKILL.md +1 -1
- package/.agent-src/skills/using-git-worktrees/SKILL.md +3 -1
- package/.agent-src/templates/AGENTS.md +24 -6
- package/.agent-src/templates/agent-settings.md +149 -0
- package/.agent-src/templates/roadmaps.md +8 -2
- package/.agent-src/templates/scripts/implement_ticket/__init__.py +63 -26
- package/.agent-src/templates/scripts/implement_ticket/__main__.py +8 -2
- package/.agent-src/templates/scripts/telemetry/__init__.py +42 -0
- package/.agent-src/templates/scripts/telemetry/aggregator.py +154 -0
- package/.agent-src/templates/scripts/telemetry/boundary.py +171 -0
- package/.agent-src/templates/scripts/telemetry/engagement.py +238 -0
- package/.agent-src/templates/scripts/telemetry/report_renderer.py +170 -0
- package/.agent-src/templates/scripts/telemetry/settings.py +112 -0
- package/.agent-src/templates/scripts/telemetry_record.py +166 -0
- package/.agent-src/templates/scripts/telemetry_report.py +161 -0
- package/.agent-src/templates/scripts/telemetry_status.py +142 -0
- package/.agent-src/templates/scripts/work_engine/__init__.py +58 -0
- package/.agent-src/templates/scripts/work_engine/__main__.py +9 -0
- package/.agent-src/templates/scripts/work_engine/cli.py +592 -0
- package/.agent-src/templates/scripts/{implement_ticket → work_engine}/delivery_state.py +7 -0
- package/.agent-src/templates/scripts/work_engine/directives/__init__.py +33 -0
- package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +98 -0
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/analyze.py +1 -1
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/implement.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/memory.py +1 -1
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/plan.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/refine.py +396 -0
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/report.py +36 -4
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/test.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/verify.py +2 -2
- package/.agent-src/templates/scripts/work_engine/directives/mixed/__init__.py +116 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/contract.py +254 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/stitch.py +229 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/ui.py +231 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/__init__.py +113 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +44 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/apply.py +241 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/audit.py +414 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/design.py +335 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/polish.py +510 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/review.py +468 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/__init__.py +119 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.py +37 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.py +165 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.py +66 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/report.py +62 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/test.py +115 -0
- package/.agent-src/templates/scripts/work_engine/dispatcher.py +331 -0
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +54 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +32 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +103 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +44 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +42 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +50 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +49 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.py +53 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.py +50 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.py +52 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/trace.py +84 -0
- package/.agent-src/templates/scripts/work_engine/hooks/context.py +66 -0
- package/.agent-src/templates/scripts/work_engine/hooks/events.py +44 -0
- package/.agent-src/templates/scripts/work_engine/hooks/exceptions.py +79 -0
- package/.agent-src/templates/scripts/work_engine/hooks/registry.py +60 -0
- package/.agent-src/templates/scripts/work_engine/hooks/runner.py +73 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +141 -0
- package/.agent-src/templates/scripts/work_engine/intent/__init__.py +47 -0
- package/.agent-src/templates/scripts/work_engine/intent/classify.py +280 -0
- package/.agent-src/templates/scripts/work_engine/migration/__init__.py +8 -0
- package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +199 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/__init__.py +22 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/diff.py +106 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/file.py +113 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +90 -0
- package/.agent-src/templates/scripts/work_engine/scoring/__init__.py +14 -0
- package/.agent-src/templates/scripts/work_engine/scoring/confidence.py +300 -0
- package/.agent-src/templates/scripts/work_engine/stack/__init__.py +31 -0
- package/.agent-src/templates/scripts/work_engine/stack/detect.py +187 -0
- package/.agent-src/templates/scripts/work_engine/state.py +641 -0
- package/.claude-plugin/marketplace.json +105 -2
- package/AGENTS.md +36 -8
- package/CHANGELOG.md +534 -0
- package/README.md +125 -4
- package/config/agent-settings.template.yml +45 -0
- package/config/gitignore-block.txt +4 -0
- package/docs/architecture.md +28 -1
- package/docs/development.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/installation.md +86 -0
- package/docs/showcase.md +204 -0
- package/package.json +1 -1
- package/scripts/agent-config +199 -0
- package/scripts/audit_cloud_compatibility.py +288 -0
- package/scripts/build_cloud_bundle.py +458 -0
- package/scripts/build_linear_digest.py +263 -0
- package/scripts/chat_history.py +796 -7
- package/scripts/check_compression.py +139 -0
- package/scripts/check_iron_law_prominence.py +143 -0
- package/scripts/check_md_language.py +159 -0
- package/scripts/check_portability.py +36 -0
- package/scripts/check_reply_consistency.py +140 -0
- package/scripts/command_suggester/__init__.py +51 -0
- package/scripts/command_suggester/cooldown.py +132 -0
- package/scripts/command_suggester/loader.py +70 -0
- package/scripts/command_suggester/match.py +180 -0
- package/scripts/command_suggester/rank.py +120 -0
- package/scripts/command_suggester/render.py +86 -0
- package/scripts/command_suggester/sanitize.py +113 -0
- package/scripts/command_suggester/settings.py +125 -0
- package/scripts/command_suggester/types.py +78 -0
- package/scripts/hooks/augment-chat-history.sh +56 -0
- package/scripts/install-hooks.sh +67 -0
- package/scripts/install.py +150 -33
- package/scripts/lint_marketplace.py +27 -0
- package/scripts/migrate_command_suggestions.py +151 -0
- package/scripts/schemas/command.schema.json +41 -0
- package/scripts/skill_linter.py +67 -0
- package/scripts/sync_agent_settings.py +42 -12
- package/templates/consumer-settings/augment-cli-hooks.json +54 -0
- package/templates/consumer-settings/claude-settings.json +55 -1
- package/.agent-src/templates/scripts/implement_ticket/cli.py +0 -171
- package/.agent-src/templates/scripts/implement_ticket/dispatcher.py +0 -134
- package/.agent-src/templates/scripts/implement_ticket/steps/__init__.py +0 -49
- package/.agent-src/templates/scripts/implement_ticket/steps/refine.py +0 -140
- /package/.agent-src/templates/scripts/{implement_ticket → work_engine}/persona_policy.py +0 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
"""Command-line entry point for ``/implement-ticket``.
|
|
2
|
+
|
|
3
|
+
Minimal Option-A transport: the script loads a persisted state file,
|
|
4
|
+
runs the dispatcher once, writes the updated state back, and prints
|
|
5
|
+
either the delivery report (on SUCCESS) or the halt surface —
|
|
6
|
+
directive plus numbered questions — on BLOCKED/PARTIAL.
|
|
7
|
+
|
|
8
|
+
The script never edits code, runs tests, or opens pull requests.
|
|
9
|
+
All of that is delegated to the agent via ``@agent-directive:``
|
|
10
|
+
markers per
|
|
11
|
+
``agents/contexts/implement-ticket-flow.md#agent-directives``. The
|
|
12
|
+
agent executes the directive, writes the resulting slice back to
|
|
13
|
+
the state file, and re-invokes this script to resume.
|
|
14
|
+
|
|
15
|
+
Wire format (R1 P4 S1, Option A2): the CLI accepts both the legacy
|
|
16
|
+
v0 wire format (``{"ticket": …, "persona": …}``) and the v1 schema
|
|
17
|
+
(``{"version": 1, "input": {"kind": "ticket", "data": …}}``). Loaded
|
|
18
|
+
state is wrapped in :class:`work_engine.state.WorkState` for the
|
|
19
|
+
boundary; before dispatch it is projected into a ``DeliveryState``
|
|
20
|
+
the step handlers understand. After dispatch the mutations are
|
|
21
|
+
mirrored back, and the file is rewritten in the **same** wire format
|
|
22
|
+
it was loaded with — Goldens captured against v0 stay v0 byte-for-
|
|
23
|
+
byte, while flows that already store v1 round-trip as v1. The
|
|
24
|
+
dispatcher selects the directive set via
|
|
25
|
+
:func:`work_engine.dispatcher.select_directive_set`, defaulting to
|
|
26
|
+
``"backend"`` so v0 callers behave exactly as they did before R1
|
|
27
|
+
Phase 4.
|
|
28
|
+
|
|
29
|
+
Exit codes:
|
|
30
|
+
|
|
31
|
+
- ``0`` — flow reached SUCCESS; ``state.report`` printed.
|
|
32
|
+
- ``1`` — flow halted BLOCKED; halt surface printed on stdout, the
|
|
33
|
+
state file carries the updated ``outcomes`` and ``questions`` so
|
|
34
|
+
the agent can resume.
|
|
35
|
+
- ``2`` — argument or I/O error (ticket file missing, JSON parse
|
|
36
|
+
failure, etc.). The state file is *not* written in this case.
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import argparse
|
|
41
|
+
import json
|
|
42
|
+
import sys
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
from typing import Any, Sequence
|
|
45
|
+
|
|
46
|
+
from . import state as _state_module
|
|
47
|
+
from .delivery_state import DeliveryState, Outcome
|
|
48
|
+
from .dispatcher import (
|
|
49
|
+
assert_kind_supported,
|
|
50
|
+
dispatch,
|
|
51
|
+
load_directive_set,
|
|
52
|
+
select_directive_set,
|
|
53
|
+
)
|
|
54
|
+
from .hooks import HookContext, HookEvent, HookHalt, HookRegistry, HookRunner
|
|
55
|
+
from .hooks.builtin import (
|
|
56
|
+
ChatHistoryAppendHook,
|
|
57
|
+
ChatHistoryHaltAppendHook,
|
|
58
|
+
ChatHistoryHeartbeatHook,
|
|
59
|
+
ChatHistoryTurnCheckHook,
|
|
60
|
+
DirectiveSetGuardHook,
|
|
61
|
+
HaltSurfaceAuditHook,
|
|
62
|
+
StateShapeValidationHook,
|
|
63
|
+
TraceHook,
|
|
64
|
+
)
|
|
65
|
+
from .hooks.settings import HookSettings, load_hook_settings
|
|
66
|
+
from .intent import populate_routing
|
|
67
|
+
from .migration.v0_to_v1 import migrate_payload
|
|
68
|
+
from .resolvers.diff import DiffResolverError, build_envelope as _build_diff_envelope
|
|
69
|
+
from .resolvers.file import FileResolverError, build_envelope as _build_file_envelope
|
|
70
|
+
from .resolvers.prompt import PromptResolverError, build_envelope as _build_prompt_envelope
|
|
71
|
+
from .state import Input, SchemaError, WorkState
|
|
72
|
+
|
|
73
|
+
DEFAULT_STATE_FILE = Path(".implement-ticket-state.json")
|
|
74
|
+
"""State file used when ``--state-file`` is not passed."""
|
|
75
|
+
|
|
76
|
+
_FMT_V0 = "v0"
|
|
77
|
+
_FMT_V1 = "v1"
|
|
78
|
+
"""Wire-format markers carried alongside the loaded :class:`WorkState`.
|
|
79
|
+
|
|
80
|
+
Format-preserving roundtrip: ``_load`` records which shape it parsed,
|
|
81
|
+
``_save`` rewrites in that same shape. v0 in → v0 out (Goldens stay
|
|
82
|
+
byte-equal); v1 in → v1 out (future flows produced by the migration
|
|
83
|
+
tool or a fresh v1 init keep their envelope fields)."""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
87
|
+
"""Run one dispatch cycle against the persisted state.
|
|
88
|
+
|
|
89
|
+
``argv`` is taken as-is; pass ``None`` to fall back to
|
|
90
|
+
``sys.argv[1:]`` (the usual entry-point contract).
|
|
91
|
+
"""
|
|
92
|
+
parser = _build_parser()
|
|
93
|
+
args = parser.parse_args(argv)
|
|
94
|
+
state_file: Path = args.state_file
|
|
95
|
+
|
|
96
|
+
runner = HookRunner(_build_hook_registry(args))
|
|
97
|
+
|
|
98
|
+
halt = runner.emit(
|
|
99
|
+
HookEvent.BEFORE_LOAD,
|
|
100
|
+
HookContext(state_file=state_file, args=args),
|
|
101
|
+
)
|
|
102
|
+
if halt is not None:
|
|
103
|
+
return _emit_halt(halt)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
work, fmt = _load_or_build(state_file, args)
|
|
107
|
+
except _CLIError as exc:
|
|
108
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
109
|
+
return 2
|
|
110
|
+
|
|
111
|
+
halt = runner.emit(
|
|
112
|
+
HookEvent.AFTER_LOAD,
|
|
113
|
+
HookContext(state_file=state_file, work=work, fmt=fmt, args=args),
|
|
114
|
+
)
|
|
115
|
+
if halt is not None:
|
|
116
|
+
return _emit_halt(halt)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
set_name = select_directive_set(work)
|
|
120
|
+
assert_kind_supported(work.input.kind, set_name)
|
|
121
|
+
steps = load_directive_set(set_name)
|
|
122
|
+
except (ValueError, NotImplementedError) as exc:
|
|
123
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
124
|
+
return 2
|
|
125
|
+
|
|
126
|
+
delivery = _to_delivery(work)
|
|
127
|
+
|
|
128
|
+
halt = runner.emit(
|
|
129
|
+
HookEvent.BEFORE_DISPATCH,
|
|
130
|
+
HookContext(work=work, delivery=delivery, set_name=set_name, args=args),
|
|
131
|
+
)
|
|
132
|
+
if halt is not None:
|
|
133
|
+
return _emit_halt(halt)
|
|
134
|
+
|
|
135
|
+
final, halting = dispatch(delivery, steps, hooks=runner)
|
|
136
|
+
|
|
137
|
+
halt = runner.emit(
|
|
138
|
+
HookEvent.AFTER_DISPATCH,
|
|
139
|
+
HookContext(
|
|
140
|
+
work=work,
|
|
141
|
+
delivery=delivery,
|
|
142
|
+
final=final,
|
|
143
|
+
halting=halting,
|
|
144
|
+
args=args,
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
if halt is not None:
|
|
148
|
+
return _emit_halt(halt)
|
|
149
|
+
|
|
150
|
+
_sync_back(work, delivery)
|
|
151
|
+
|
|
152
|
+
halt = runner.emit(
|
|
153
|
+
HookEvent.BEFORE_SAVE,
|
|
154
|
+
HookContext(work=work, delivery=delivery, fmt=fmt, args=args),
|
|
155
|
+
)
|
|
156
|
+
if halt is not None:
|
|
157
|
+
return _emit_halt(halt)
|
|
158
|
+
|
|
159
|
+
_save(state_file, work, fmt)
|
|
160
|
+
|
|
161
|
+
halt = runner.emit(
|
|
162
|
+
HookEvent.AFTER_SAVE,
|
|
163
|
+
HookContext(work=work, state_file=state_file, fmt=fmt, args=args),
|
|
164
|
+
)
|
|
165
|
+
if halt is not None:
|
|
166
|
+
# State is already on disk; exit 2 still per the P3 branch table.
|
|
167
|
+
return _emit_halt(halt)
|
|
168
|
+
|
|
169
|
+
_emit(work, final, halting)
|
|
170
|
+
return 0 if final is Outcome.SUCCESS else 1
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _build_hook_registry(args: argparse.Namespace) -> HookRegistry:
|
|
174
|
+
"""Build the CLI-side :class:`HookRegistry` for one ``main()`` run.
|
|
175
|
+
|
|
176
|
+
Reads ``hooks.*`` from ``.agent-settings.yml`` and registers the
|
|
177
|
+
enabled hooks. The master switch ``hooks.enabled`` defaults to
|
|
178
|
+
``False`` when the block (or the file) is missing — the registry
|
|
179
|
+
stays empty and golden replay flows are byte-stable.
|
|
180
|
+
|
|
181
|
+
``--no-hooks`` on the CLI forces an empty registry regardless of
|
|
182
|
+
settings, which is the explicit escape hatch golden-replay test
|
|
183
|
+
harnesses can use.
|
|
184
|
+
"""
|
|
185
|
+
registry = HookRegistry()
|
|
186
|
+
if getattr(args, "no_hooks", False):
|
|
187
|
+
return registry
|
|
188
|
+
|
|
189
|
+
settings_path = getattr(args, "hooks_config", None)
|
|
190
|
+
settings = load_hook_settings(settings_path)
|
|
191
|
+
if not settings.enabled:
|
|
192
|
+
return registry
|
|
193
|
+
|
|
194
|
+
if settings.trace:
|
|
195
|
+
TraceHook().register(registry)
|
|
196
|
+
if settings.halt_surface_audit:
|
|
197
|
+
HaltSurfaceAuditHook().register(registry)
|
|
198
|
+
if settings.state_shape_validation:
|
|
199
|
+
StateShapeValidationHook().register(registry)
|
|
200
|
+
if settings.directive_set_guard:
|
|
201
|
+
DirectiveSetGuardHook().register(registry)
|
|
202
|
+
if settings.chat_history_enabled:
|
|
203
|
+
_register_chat_history_hooks(registry, settings)
|
|
204
|
+
|
|
205
|
+
return registry
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _register_chat_history_hooks(
|
|
209
|
+
registry: HookRegistry, settings: HookSettings,
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Register the four chat-history hooks bound to the configured script."""
|
|
212
|
+
script = Path(settings.chat_history_script)
|
|
213
|
+
ChatHistoryTurnCheckHook(script).register(registry)
|
|
214
|
+
ChatHistoryAppendHook(script).register(registry)
|
|
215
|
+
ChatHistoryHaltAppendHook(script).register(registry)
|
|
216
|
+
ChatHistoryHeartbeatHook(script).register(registry)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _emit_halt(halt: HookHalt) -> int:
|
|
220
|
+
"""Render a :class:`HookHalt` surface to stderr and return exit 2.
|
|
221
|
+
|
|
222
|
+
Per the P3 halt branch table, every CLI-layer halt yields exit code
|
|
223
|
+
``2`` regardless of which event fired it. State persistence is
|
|
224
|
+
governed by *where* in ``main`` the halt is detected: the call site
|
|
225
|
+
decides whether ``_save`` already ran. This helper is the single
|
|
226
|
+
place that formats the surface so the wire output stays consistent.
|
|
227
|
+
"""
|
|
228
|
+
if halt.surface:
|
|
229
|
+
for line in halt.surface:
|
|
230
|
+
print(line, file=sys.stderr)
|
|
231
|
+
else:
|
|
232
|
+
print(f"halt: {halt.reason}", file=sys.stderr)
|
|
233
|
+
return 2
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
237
|
+
parser = argparse.ArgumentParser(
|
|
238
|
+
prog="implement-ticket",
|
|
239
|
+
description="Run one dispatch cycle of the /implement-ticket flow.",
|
|
240
|
+
)
|
|
241
|
+
parser.add_argument(
|
|
242
|
+
"--state-file",
|
|
243
|
+
type=Path,
|
|
244
|
+
default=DEFAULT_STATE_FILE,
|
|
245
|
+
help=f"Path to persisted state JSON (default: {DEFAULT_STATE_FILE}).",
|
|
246
|
+
)
|
|
247
|
+
parser.add_argument(
|
|
248
|
+
"--ticket-file",
|
|
249
|
+
type=Path,
|
|
250
|
+
default=None,
|
|
251
|
+
help="JSON file carrying the ticket payload; used only when the "
|
|
252
|
+
"state file does not exist yet.",
|
|
253
|
+
)
|
|
254
|
+
parser.add_argument(
|
|
255
|
+
"--prompt-file",
|
|
256
|
+
type=Path,
|
|
257
|
+
default=None,
|
|
258
|
+
help="Plain-text file carrying the raw user prompt; builds an "
|
|
259
|
+
"input.kind='prompt' envelope. Mutually exclusive with "
|
|
260
|
+
"--ticket-file. Used only when the state file does not exist yet.",
|
|
261
|
+
)
|
|
262
|
+
parser.add_argument(
|
|
263
|
+
"--diff-file",
|
|
264
|
+
type=Path,
|
|
265
|
+
default=None,
|
|
266
|
+
help="Plain-text file carrying a unified diff payload; builds an "
|
|
267
|
+
"input.kind='diff' envelope routed through the UI-improve "
|
|
268
|
+
"directive set. Mutually exclusive with --ticket-file / "
|
|
269
|
+
"--prompt-file / --file-file. Used only when the state file does "
|
|
270
|
+
"not exist yet.",
|
|
271
|
+
)
|
|
272
|
+
parser.add_argument(
|
|
273
|
+
"--file-file",
|
|
274
|
+
type=Path,
|
|
275
|
+
default=None,
|
|
276
|
+
help="Plain-text file carrying a single path reference (one line); "
|
|
277
|
+
"builds an input.kind='file' envelope routed through the UI-improve "
|
|
278
|
+
"directive set. Mutually exclusive with --ticket-file / "
|
|
279
|
+
"--prompt-file / --diff-file. Used only when the state file does "
|
|
280
|
+
"not exist yet.",
|
|
281
|
+
)
|
|
282
|
+
parser.add_argument(
|
|
283
|
+
"--persona",
|
|
284
|
+
type=str,
|
|
285
|
+
default=None,
|
|
286
|
+
help="Persona name (senior-engineer | qa | advisory). Only honoured "
|
|
287
|
+
"when the state file does not exist yet; ignored on resume so a "
|
|
288
|
+
"mid-flight persona switch cannot silently change behaviour.",
|
|
289
|
+
)
|
|
290
|
+
parser.add_argument(
|
|
291
|
+
"--no-hooks",
|
|
292
|
+
action="store_true",
|
|
293
|
+
default=False,
|
|
294
|
+
help="Disable every lifecycle hook for this run. Use in golden-"
|
|
295
|
+
"replay test harnesses so a future settings change cannot "
|
|
296
|
+
"silently invalidate captured outputs.",
|
|
297
|
+
)
|
|
298
|
+
parser.add_argument(
|
|
299
|
+
"--hooks-config",
|
|
300
|
+
type=Path,
|
|
301
|
+
default=None,
|
|
302
|
+
help="Override the path to the agent-settings file used to resolve "
|
|
303
|
+
"the hooks.* block. Defaults to ./.agent-settings.yml.",
|
|
304
|
+
)
|
|
305
|
+
return parser
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _load_or_build(
|
|
309
|
+
state_file: Path,
|
|
310
|
+
args: argparse.Namespace,
|
|
311
|
+
) -> tuple[WorkState, str]:
|
|
312
|
+
"""Return the WorkState to dispatch against plus its wire format.
|
|
313
|
+
|
|
314
|
+
Either loaded from ``state_file`` (format-preserving) or freshly
|
|
315
|
+
built from ``--ticket-file`` (R1), ``--prompt-file`` (R2),
|
|
316
|
+
``--diff-file`` (R3) or ``--file-file`` (R3). Fresh ticket files
|
|
317
|
+
default to v0 wire format so that newly captured Goldens stay
|
|
318
|
+
byte-equal with the pre-Phase-4 baseline; the prompt / diff / file
|
|
319
|
+
paths emit v1 directly (v0 has no envelope concept for these
|
|
320
|
+
kinds). v1 round-trips for state files already on disk in v1 shape.
|
|
321
|
+
"""
|
|
322
|
+
if state_file.exists():
|
|
323
|
+
return _load(state_file)
|
|
324
|
+
inputs = [
|
|
325
|
+
("--ticket-file", args.ticket_file),
|
|
326
|
+
("--prompt-file", args.prompt_file),
|
|
327
|
+
("--diff-file", args.diff_file),
|
|
328
|
+
("--file-file", args.file_file),
|
|
329
|
+
]
|
|
330
|
+
supplied = [name for name, value in inputs if value is not None]
|
|
331
|
+
if len(supplied) > 1:
|
|
332
|
+
raise _CLIError(
|
|
333
|
+
f"{', '.join(supplied)} are mutually exclusive; pass exactly "
|
|
334
|
+
"one when building an initial state.",
|
|
335
|
+
)
|
|
336
|
+
if not supplied:
|
|
337
|
+
raise _CLIError(
|
|
338
|
+
f"No state file at {state_file} and no --ticket-file, "
|
|
339
|
+
"--prompt-file, --diff-file, or --file-file given; cannot "
|
|
340
|
+
"build an initial state.",
|
|
341
|
+
)
|
|
342
|
+
if args.prompt_file is not None:
|
|
343
|
+
return _build_from_prompt_file(args), _FMT_V1
|
|
344
|
+
if args.diff_file is not None:
|
|
345
|
+
return _build_from_diff_file(args), _FMT_V1
|
|
346
|
+
if args.file_file is not None:
|
|
347
|
+
return _build_from_file_file(args), _FMT_V1
|
|
348
|
+
ticket = _read_json(args.ticket_file)
|
|
349
|
+
if not isinstance(ticket, dict):
|
|
350
|
+
raise _CLIError(
|
|
351
|
+
f"--ticket-file must carry a JSON object; got {type(ticket).__name__}.",
|
|
352
|
+
)
|
|
353
|
+
work = WorkState(input=Input(kind="ticket", data=ticket))
|
|
354
|
+
if args.persona:
|
|
355
|
+
work.persona = args.persona
|
|
356
|
+
populate_routing(work)
|
|
357
|
+
return work, _FMT_V0
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _build_from_prompt_file(args: argparse.Namespace) -> WorkState:
|
|
361
|
+
"""Read ``--prompt-file`` as raw text and wrap it in a prompt envelope.
|
|
362
|
+
|
|
363
|
+
The file is read verbatim (UTF-8) and handed to the prompt resolver,
|
|
364
|
+
which validates non-emptiness and returns the canonical
|
|
365
|
+
``Input(kind="prompt", data={raw, reconstructed_ac, assumptions})``
|
|
366
|
+
envelope. Persona is honoured the same way as the ticket path.
|
|
367
|
+
"""
|
|
368
|
+
try:
|
|
369
|
+
raw = args.prompt_file.read_text(encoding="utf-8")
|
|
370
|
+
except OSError as exc:
|
|
371
|
+
raise _CLIError(f"Cannot read {args.prompt_file}: {exc}") from exc
|
|
372
|
+
try:
|
|
373
|
+
envelope = _build_prompt_envelope(raw)
|
|
374
|
+
except PromptResolverError as exc:
|
|
375
|
+
raise _CLIError(f"--prompt-file is not a valid prompt: {exc}") from exc
|
|
376
|
+
work = WorkState(input=envelope)
|
|
377
|
+
if args.persona:
|
|
378
|
+
work.persona = args.persona
|
|
379
|
+
populate_routing(work)
|
|
380
|
+
return work
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _build_from_diff_file(args: argparse.Namespace) -> WorkState:
|
|
384
|
+
"""Read ``--diff-file`` as raw text and wrap it in a diff envelope.
|
|
385
|
+
|
|
386
|
+
The file is read verbatim (UTF-8) and handed to the diff resolver,
|
|
387
|
+
which validates the unified-diff header heuristic and returns the
|
|
388
|
+
canonical
|
|
389
|
+
``Input(kind="diff", data={raw, reconstructed_ac, assumptions})``
|
|
390
|
+
envelope. ``populate_routing`` then routes the envelope to the
|
|
391
|
+
UI-improve directive set without running the prose classifier — see
|
|
392
|
+
:mod:`work_engine.intent.classify` for the routing contract.
|
|
393
|
+
"""
|
|
394
|
+
try:
|
|
395
|
+
raw = args.diff_file.read_text(encoding="utf-8")
|
|
396
|
+
except OSError as exc:
|
|
397
|
+
raise _CLIError(f"Cannot read {args.diff_file}: {exc}") from exc
|
|
398
|
+
try:
|
|
399
|
+
envelope = _build_diff_envelope(raw)
|
|
400
|
+
except DiffResolverError as exc:
|
|
401
|
+
raise _CLIError(f"--diff-file is not a valid diff: {exc}") from exc
|
|
402
|
+
work = WorkState(input=envelope)
|
|
403
|
+
if args.persona:
|
|
404
|
+
work.persona = args.persona
|
|
405
|
+
populate_routing(work)
|
|
406
|
+
return work
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _build_from_file_file(args: argparse.Namespace) -> WorkState:
|
|
410
|
+
"""Read ``--file-file`` as a single-line path and wrap it in a file envelope.
|
|
411
|
+
|
|
412
|
+
The file is read verbatim (UTF-8); the first non-empty line is taken
|
|
413
|
+
as the path reference and handed to the file resolver, which
|
|
414
|
+
validates path shape (non-empty, NUL-free, not a URL) and returns
|
|
415
|
+
the canonical
|
|
416
|
+
``Input(kind="file", data={path, reconstructed_ac, assumptions})``
|
|
417
|
+
envelope. Trailing whitespace and additional lines are ignored —
|
|
418
|
+
the resolver treats the file's content as the path itself, not as
|
|
419
|
+
structured payload.
|
|
420
|
+
"""
|
|
421
|
+
try:
|
|
422
|
+
raw = args.file_file.read_text(encoding="utf-8")
|
|
423
|
+
except OSError as exc:
|
|
424
|
+
raise _CLIError(f"Cannot read {args.file_file}: {exc}") from exc
|
|
425
|
+
path = raw.strip().splitlines()[0] if raw.strip() else ""
|
|
426
|
+
try:
|
|
427
|
+
envelope = _build_file_envelope(path)
|
|
428
|
+
except FileResolverError as exc:
|
|
429
|
+
raise _CLIError(
|
|
430
|
+
f"--file-file does not carry a valid path: {exc}",
|
|
431
|
+
) from exc
|
|
432
|
+
work = WorkState(input=envelope)
|
|
433
|
+
if args.persona:
|
|
434
|
+
work.persona = args.persona
|
|
435
|
+
populate_routing(work)
|
|
436
|
+
return work
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _load(state_file: Path) -> tuple[WorkState, str]:
|
|
440
|
+
"""Load ``state_file`` and tag it with the wire format detected."""
|
|
441
|
+
data = _read_json(state_file)
|
|
442
|
+
if not isinstance(data, dict):
|
|
443
|
+
raise _CLIError(
|
|
444
|
+
f"State file {state_file} must carry a JSON object; "
|
|
445
|
+
f"got {type(data).__name__}.",
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# v1 declares ``version``; v0 has none. Anything else is invalid.
|
|
449
|
+
if data.get("version") == _state_module.SCHEMA_VERSION:
|
|
450
|
+
try:
|
|
451
|
+
return _state_module.from_dict(data), _FMT_V1
|
|
452
|
+
except SchemaError as exc:
|
|
453
|
+
raise _CLIError(f"State file shape is invalid: {exc}") from exc
|
|
454
|
+
if "version" in data:
|
|
455
|
+
raise _CLIError(
|
|
456
|
+
f"State file shape is invalid: unsupported version "
|
|
457
|
+
f"{data.get('version')!r}; expected {_state_module.SCHEMA_VERSION}",
|
|
458
|
+
)
|
|
459
|
+
if "ticket" not in data:
|
|
460
|
+
raise _CLIError(
|
|
461
|
+
"State file shape is invalid: missing 'ticket' (v0) or "
|
|
462
|
+
"'version' (v1) — file is neither shape.",
|
|
463
|
+
)
|
|
464
|
+
try:
|
|
465
|
+
migrated = migrate_payload(data)
|
|
466
|
+
return _state_module.from_dict(migrated), _FMT_V0
|
|
467
|
+
except SchemaError as exc:
|
|
468
|
+
raise _CLIError(f"State file shape is invalid: {exc}") from exc
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _to_delivery(work: WorkState) -> DeliveryState:
|
|
472
|
+
"""Project ``work`` into a ``DeliveryState`` for handler dispatch.
|
|
473
|
+
|
|
474
|
+
R1 P4 S1 (Option A2): handlers continue to consume ``DeliveryState``
|
|
475
|
+
with ``state.ticket``; the ``WorkState`` wrapper exists at the CLI
|
|
476
|
+
boundary so the dispatcher's directive-set selection has a v1
|
|
477
|
+
state object to read ``directive_set`` from. Mutable containers
|
|
478
|
+
(``memory``, ``changes``, ``outcomes``, ``questions``) are passed
|
|
479
|
+
by reference — in-place mutations land on both objects without an
|
|
480
|
+
explicit sync. Reassignments (``state.plan = …``, ``state.report
|
|
481
|
+
= …``) are mirrored back by :func:`_sync_back`.
|
|
482
|
+
"""
|
|
483
|
+
return DeliveryState(
|
|
484
|
+
ticket=work.input.data,
|
|
485
|
+
persona=work.persona,
|
|
486
|
+
memory=work.memory,
|
|
487
|
+
plan=work.plan,
|
|
488
|
+
changes=work.changes,
|
|
489
|
+
tests=work.tests,
|
|
490
|
+
verify=work.verify,
|
|
491
|
+
outcomes=work.outcomes,
|
|
492
|
+
questions=work.questions,
|
|
493
|
+
report=work.report,
|
|
494
|
+
ui_audit=work.ui_audit,
|
|
495
|
+
ui_design=work.ui_design,
|
|
496
|
+
ui_review=work.ui_review,
|
|
497
|
+
ui_polish=work.ui_polish,
|
|
498
|
+
contract=work.contract,
|
|
499
|
+
stitch=work.stitch,
|
|
500
|
+
stack=work.stack,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _sync_back(work: WorkState, delivery: DeliveryState) -> None:
|
|
505
|
+
"""Mirror handler mutations from ``delivery`` back into ``work``.
|
|
506
|
+
|
|
507
|
+
Container fields are shared by reference (see :func:`_to_delivery`)
|
|
508
|
+
so the assignment is a no-op for those — we still mirror them
|
|
509
|
+
defensively to cover the case where a handler reassigned the
|
|
510
|
+
attribute (``state.memory = [new_list]``) instead of mutating in
|
|
511
|
+
place.
|
|
512
|
+
"""
|
|
513
|
+
work.input.data = delivery.ticket
|
|
514
|
+
work.persona = delivery.persona
|
|
515
|
+
work.memory = delivery.memory
|
|
516
|
+
work.plan = delivery.plan
|
|
517
|
+
work.changes = delivery.changes
|
|
518
|
+
work.tests = delivery.tests
|
|
519
|
+
work.verify = delivery.verify
|
|
520
|
+
work.outcomes = delivery.outcomes
|
|
521
|
+
work.questions = delivery.questions
|
|
522
|
+
work.report = delivery.report
|
|
523
|
+
work.ui_audit = delivery.ui_audit
|
|
524
|
+
work.ui_design = delivery.ui_design
|
|
525
|
+
work.ui_review = delivery.ui_review
|
|
526
|
+
work.ui_polish = delivery.ui_polish
|
|
527
|
+
work.contract = delivery.contract
|
|
528
|
+
work.stitch = delivery.stitch
|
|
529
|
+
work.stack = delivery.stack
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _save(state_file: Path, work: WorkState, fmt: str) -> None:
|
|
533
|
+
"""Persist ``work`` in the wire format it was loaded with.
|
|
534
|
+
|
|
535
|
+
v1 emits the canonical envelope via :func:`work_engine.state.to_dict`;
|
|
536
|
+
v0 emits the legacy flat shape that ``DeliveryState.asdict`` used
|
|
537
|
+
to produce, byte-identical to the pre-Phase-4 output so the
|
|
538
|
+
Golden Transcript replay stays green.
|
|
539
|
+
"""
|
|
540
|
+
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
541
|
+
payload = _state_module.to_dict(work) if fmt == _FMT_V1 else _to_v0_dict(work)
|
|
542
|
+
state_file.write_text(
|
|
543
|
+
json.dumps(payload, indent=2, ensure_ascii=False) + "\n",
|
|
544
|
+
encoding="utf-8",
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _to_v0_dict(work: WorkState) -> dict[str, Any]:
|
|
549
|
+
"""Serialise ``work`` in the legacy v0 wire format.
|
|
550
|
+
|
|
551
|
+
Field order matches ``DeliveryState`` declaration order so
|
|
552
|
+
pre-Phase-4 state files round-trip byte-equal.
|
|
553
|
+
"""
|
|
554
|
+
return {
|
|
555
|
+
"ticket": work.input.data,
|
|
556
|
+
"persona": work.persona,
|
|
557
|
+
"memory": work.memory,
|
|
558
|
+
"plan": work.plan,
|
|
559
|
+
"changes": work.changes,
|
|
560
|
+
"tests": work.tests,
|
|
561
|
+
"verify": work.verify,
|
|
562
|
+
"outcomes": work.outcomes,
|
|
563
|
+
"questions": work.questions,
|
|
564
|
+
"report": work.report,
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _read_json(path: Path):
|
|
569
|
+
try:
|
|
570
|
+
raw = path.read_text(encoding="utf-8")
|
|
571
|
+
except OSError as exc:
|
|
572
|
+
raise _CLIError(f"Cannot read {path}: {exc}") from exc
|
|
573
|
+
try:
|
|
574
|
+
return json.loads(raw)
|
|
575
|
+
except json.JSONDecodeError as exc:
|
|
576
|
+
raise _CLIError(f"Invalid JSON in {path}: {exc}") from exc
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _emit(work: WorkState, final: Outcome, halting: str | None) -> None:
|
|
580
|
+
if final is Outcome.SUCCESS:
|
|
581
|
+
print(work.report)
|
|
582
|
+
return
|
|
583
|
+
print(f"[halt] outcome={final.value} step={halting or '(none)'}")
|
|
584
|
+
for line in work.questions:
|
|
585
|
+
print(line)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
class _CLIError(Exception):
|
|
589
|
+
"""Raised on configuration or I/O problems. Converted to exit code 2."""
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
__all__ = ["DEFAULT_STATE_FILE", "main"]
|
|
@@ -75,6 +75,13 @@ class DeliveryState:
|
|
|
75
75
|
outcomes: dict[str, str] = field(default_factory=dict)
|
|
76
76
|
questions: list[str] = field(default_factory=list)
|
|
77
77
|
report: str = ""
|
|
78
|
+
ui_audit: dict[str, Any] | None = None
|
|
79
|
+
ui_design: dict[str, Any] | None = None
|
|
80
|
+
ui_review: dict[str, Any] | None = None
|
|
81
|
+
ui_polish: dict[str, Any] | None = None
|
|
82
|
+
contract: dict[str, Any] | None = None
|
|
83
|
+
stitch: dict[str, Any] | None = None
|
|
84
|
+
stack: dict[str, Any] | None = None
|
|
78
85
|
|
|
79
86
|
|
|
80
87
|
Step = Callable[[DeliveryState], StepResult]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Directive-set bundles consumed by the dispatcher.
|
|
2
|
+
|
|
3
|
+
A *directive set* is a coherent group of step handlers (refine,
|
|
4
|
+
memory, analyze, plan, implement, test, verify, report) tuned for a
|
|
5
|
+
particular kind of work — backend coding, UI work, mixed front+back
|
|
6
|
+
work, and so on. The dispatcher selects exactly one set per cycle
|
|
7
|
+
(see ``dispatcher.select_directive_set``) and walks its eight steps
|
|
8
|
+
in the canonical order.
|
|
9
|
+
|
|
10
|
+
Each set is a Python sub-package exposing a single function::
|
|
11
|
+
|
|
12
|
+
def get_steps() -> Mapping[str, Step]:
|
|
13
|
+
'''Return the {step_name: handler} mapping the dispatcher walks.'''
|
|
14
|
+
|
|
15
|
+
The mapping must cover every entry in :data:`dispatcher.STEP_ORDER`;
|
|
16
|
+
incomplete bundles raise ``KeyError`` at dispatch time.
|
|
17
|
+
|
|
18
|
+
Roadmap status (R1 Phase 4):
|
|
19
|
+
|
|
20
|
+
- ``backend`` — fully implemented; landing in Step 3 of this phase.
|
|
21
|
+
- ``ui`` — stub; lands in Roadmap 3 (``road-to-product-ui-track.md``).
|
|
22
|
+
- ``ui_trivial`` — stub; lands in Roadmap 3 V2.
|
|
23
|
+
- ``mixed`` — stub; lands in Roadmap 3.
|
|
24
|
+
|
|
25
|
+
The schema (``state.KNOWN_DIRECTIVE_SETS``) carries the *external*
|
|
26
|
+
names ``ui``, ``ui-trivial``, ``mixed``; the directory layout uses
|
|
27
|
+
underscores (``ui_trivial``) because Python packages cannot contain
|
|
28
|
+
hyphens. The dispatcher's loader is the single place that translates
|
|
29
|
+
between the two.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
__all__: list[str] = []
|