@event4u/agent-config 1.14.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 +1 -1
- package/.agent-src/commands/bug-fix.md +2 -2
- package/.agent-src/commands/chat-history-checkpoint.md +2 -2
- package/.agent-src/commands/chat-history-clear.md +1 -1
- package/.agent-src/commands/chat-history-resume.md +2 -2
- package/.agent-src/commands/chat-history.md +2 -2
- package/.agent-src/commands/check-current-md.md +43 -32
- package/.agent-src/commands/commit-in-chunks.md +43 -23
- package/.agent-src/commands/compress.md +34 -2
- package/.agent-src/commands/feature-roadmap.md +2 -2
- package/.agent-src/commands/fix-portability.md +2 -2
- package/.agent-src/commands/onboard.md +14 -5
- package/.agent-src/commands/optimize-augmentignore.md +9 -0
- package/.agent-src/commands/refine-ticket.md +9 -7
- package/.agent-src/commands/review-changes.md +35 -8
- package/.agent-src/commands/roadmap-create.md +13 -2
- package/.agent-src/commands/roadmap-execute.md +9 -7
- package/.agent-src/commands/set-cost-profile.md +8 -0
- package/.agent-src/commands/sync-agent-settings.md +9 -0
- package/.agent-src/commands/tests-execute.md +2 -3
- package/.agent-src/rules/artifact-engagement-recording.md +1 -1
- package/.agent-src/rules/augment-portability.md +56 -37
- 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 +3 -2
- package/.agent-src/rules/commit-policy.md +44 -34
- package/.agent-src/rules/direct-answers.md +1 -1
- package/.agent-src/rules/language-and-tone.md +19 -15
- package/.agent-src/rules/non-destructive-by-default.md +18 -18
- package/.agent-src/rules/roadmap-progress-sync.md +133 -74
- package/.agent-src/rules/role-mode-adherence.md +1 -1
- package/.agent-src/rules/size-enforcement.md +2 -1
- package/.agent-src/rules/user-interaction.md +28 -4
- package/.agent-src/scripts/update_roadmap_progress.py +56 -4
- package/.agent-src/skills/blade-ui/SKILL.md +29 -10
- package/.agent-src/skills/command-writing/SKILL.md +15 -4
- 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/livewire/SKILL.md +26 -7
- package/.agent-src/skills/refine-ticket/SKILL.md +30 -24
- package/.agent-src/skills/roadmap-management/SKILL.md +22 -16
- package/.agent-src/skills/skill-writing/SKILL.md +3 -3
- package/.agent-src/skills/upstream-contribute/SKILL.md +2 -2
- package/.agent-src/templates/agent-settings.md +1 -1
- package/.agent-src/templates/roadmaps.md +9 -8
- package/.agent-src/templates/scripts/memory_lookup.py +1 -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 +1 -1
- package/AGENTS.md +6 -4
- package/CHANGELOG.md +83 -8
- package/README.md +24 -23
- package/docs/MIGRATION.md +122 -0
- package/docs/architecture.md +83 -34
- 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/getting-started.md +2 -2
- package/docs/installation.md +42 -6
- package/docs/migrations/commands-1.15.0.md +112 -0
- package/docs/ui-track-mental-model.md +121 -0
- package/package.json +1 -1
- package/scripts/build_linear_digest.py +4 -4
- package/scripts/check_portability.py +2 -0
- package/scripts/check_public_links.py +185 -0
- package/scripts/check_references.py +1 -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/skill_linter.py +14 -0
- package/scripts/update_counts.py +10 -0
- package/.agent-src/rules/chat-history.md +0 -200
|
@@ -8,7 +8,7 @@ directive plus numbered questions — on BLOCKED/PARTIAL.
|
|
|
8
8
|
The script never edits code, runs tests, or opens pull requests.
|
|
9
9
|
All of that is delegated to the agent via ``@agent-directive:``
|
|
10
10
|
markers per
|
|
11
|
-
``
|
|
11
|
+
``docs/contracts/implement-ticket-flow.md#agent-directives``. The
|
|
12
12
|
agent executes the directive, writes the resulting slice back to
|
|
13
13
|
the state file, and re-invokes this script to resume.
|
|
14
14
|
|
|
@@ -20,11 +20,17 @@ boundary; before dispatch it is projected into a ``DeliveryState``
|
|
|
20
20
|
the step handlers understand. After dispatch the mutations are
|
|
21
21
|
mirrored back, and the file is rewritten in the **same** wire format
|
|
22
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.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
byte, while flows that already store v1 round-trip as v1.
|
|
24
|
+
|
|
25
|
+
Layout (post P2.3 of ``road-to-post-pr29-optimize.md``): this file
|
|
26
|
+
is a thin orchestrator. The argument parser, state I/O, file-input
|
|
27
|
+
builders, hook bootstrap, and stdout/stderr emitters live in their
|
|
28
|
+
own leaf modules under ``work_engine`` — see ``cli_args``,
|
|
29
|
+
``state_io``, ``input_builders``, ``hook_bootstrap``, ``emitters``,
|
|
30
|
+
``errors``. Public names (``main``, ``DEFAULT_STATE_FILE``) and the
|
|
31
|
+
private monkeypatch surface (``_build_hook_registry``,
|
|
32
|
+
``_CLIError``, ``_load_or_build``, …) are re-exported here so
|
|
33
|
+
existing imports and patch targets continue to resolve.
|
|
28
34
|
|
|
29
35
|
Exit codes:
|
|
30
36
|
|
|
@@ -37,50 +43,43 @@ Exit codes:
|
|
|
37
43
|
"""
|
|
38
44
|
from __future__ import annotations
|
|
39
45
|
|
|
40
|
-
import argparse
|
|
41
|
-
import json
|
|
42
46
|
import sys
|
|
43
47
|
from pathlib import Path
|
|
44
|
-
from typing import
|
|
45
|
-
|
|
46
|
-
from . import
|
|
47
|
-
|
|
48
|
+
from typing import Sequence
|
|
49
|
+
|
|
50
|
+
from .cli_args import (
|
|
51
|
+
DEFAULT_STATE_FILE,
|
|
52
|
+
LEGACY_STATE_FILE,
|
|
53
|
+
_build_parser,
|
|
54
|
+
_FMT_V0,
|
|
55
|
+
_FMT_V1,
|
|
56
|
+
)
|
|
57
|
+
from .delivery_state import Outcome
|
|
48
58
|
from .dispatcher import (
|
|
49
59
|
assert_kind_supported,
|
|
50
60
|
dispatch,
|
|
51
61
|
load_directive_set,
|
|
52
62
|
select_directive_set,
|
|
53
63
|
)
|
|
54
|
-
from .
|
|
55
|
-
from .
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
from .emitters import _emit, _emit_halt
|
|
65
|
+
from .errors import _CLIError
|
|
66
|
+
from .hook_bootstrap import _build_hook_registry, _register_chat_history_hooks
|
|
67
|
+
from .hooks import HookContext, HookEvent, HookRunner
|
|
68
|
+
from .input_builders import (
|
|
69
|
+
_build_from_diff_file,
|
|
70
|
+
_build_from_file_file,
|
|
71
|
+
_build_from_prompt_file,
|
|
72
|
+
_load_or_build,
|
|
73
|
+
)
|
|
74
|
+
from .state_io import (
|
|
75
|
+
_load,
|
|
76
|
+
_maybe_raise_legacy_hint,
|
|
77
|
+
_read_json,
|
|
78
|
+
_save,
|
|
79
|
+
_sync_back,
|
|
80
|
+
_to_delivery,
|
|
81
|
+
_to_v0_dict,
|
|
64
82
|
)
|
|
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
83
|
|
|
85
84
|
|
|
86
85
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
@@ -170,423 +169,27 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
170
169
|
return 0 if final is Outcome.SUCCESS else 1
|
|
171
170
|
|
|
172
171
|
|
|
173
|
-
|
|
174
|
-
""
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
""
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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"]
|
|
172
|
+
__all__ = [
|
|
173
|
+
"DEFAULT_STATE_FILE",
|
|
174
|
+
"LEGACY_STATE_FILE",
|
|
175
|
+
"_CLIError",
|
|
176
|
+
"_FMT_V0",
|
|
177
|
+
"_FMT_V1",
|
|
178
|
+
"_build_from_diff_file",
|
|
179
|
+
"_build_from_file_file",
|
|
180
|
+
"_build_from_prompt_file",
|
|
181
|
+
"_build_hook_registry",
|
|
182
|
+
"_build_parser",
|
|
183
|
+
"_emit",
|
|
184
|
+
"_emit_halt",
|
|
185
|
+
"_load",
|
|
186
|
+
"_load_or_build",
|
|
187
|
+
"_maybe_raise_legacy_hint",
|
|
188
|
+
"_read_json",
|
|
189
|
+
"_register_chat_history_hooks",
|
|
190
|
+
"_save",
|
|
191
|
+
"_sync_back",
|
|
192
|
+
"_to_delivery",
|
|
193
|
+
"_to_v0_dict",
|
|
194
|
+
"main",
|
|
195
|
+
]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Argument parser and state-file constants for the CLI entry point.
|
|
2
|
+
|
|
3
|
+
Extracted from ``cli.py`` in P2.3 of
|
|
4
|
+
``road-to-post-pr29-optimize.md``. Behaviour-preserving: the parser
|
|
5
|
+
shape, default values, help strings and exit-code semantics are
|
|
6
|
+
byte-identical to the pre-split version. The constants moved here
|
|
7
|
+
so the parser default and the legacy-file detector both reference
|
|
8
|
+
a single source of truth.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
DEFAULT_STATE_FILE = Path(".work-state.json")
|
|
16
|
+
"""State file used when ``--state-file`` is not passed.
|
|
17
|
+
|
|
18
|
+
Renamed from ``.implement-ticket-state.json`` in 1.15.0 alongside the
|
|
19
|
+
``implement_ticket → work_engine`` package move. The legacy filename is
|
|
20
|
+
still recognised on load (see :data:`LEGACY_STATE_FILE` below) so that
|
|
21
|
+
existing checkouts surface a clear migration message instead of a
|
|
22
|
+
silent "no state file" error."""
|
|
23
|
+
|
|
24
|
+
LEGACY_STATE_FILE = Path(".implement-ticket-state.json")
|
|
25
|
+
"""Pre-1.15.0 default state file. Detected only as a migration hint;
|
|
26
|
+
never written to. See ``docs/MIGRATION.md``."""
|
|
27
|
+
|
|
28
|
+
_FMT_V0 = "v0"
|
|
29
|
+
_FMT_V1 = "v1"
|
|
30
|
+
"""Wire-format markers carried alongside the loaded :class:`WorkState`.
|
|
31
|
+
|
|
32
|
+
Format-preserving roundtrip: ``_load`` records which shape it parsed,
|
|
33
|
+
``_save`` rewrites in that same shape. v0 in → v0 out (Goldens stay
|
|
34
|
+
byte-equal); v1 in → v1 out (future flows produced by the migration
|
|
35
|
+
tool or a fresh v1 init keep their envelope fields)."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
39
|
+
parser = argparse.ArgumentParser(
|
|
40
|
+
prog="implement-ticket",
|
|
41
|
+
description="Run one dispatch cycle of the /implement-ticket flow.",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--state-file",
|
|
45
|
+
type=Path,
|
|
46
|
+
default=DEFAULT_STATE_FILE,
|
|
47
|
+
help=f"Path to persisted state JSON (default: {DEFAULT_STATE_FILE}).",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--ticket-file",
|
|
51
|
+
type=Path,
|
|
52
|
+
default=None,
|
|
53
|
+
help="JSON file carrying the ticket payload; used only when the "
|
|
54
|
+
"state file does not exist yet.",
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--prompt-file",
|
|
58
|
+
type=Path,
|
|
59
|
+
default=None,
|
|
60
|
+
help="Plain-text file carrying the raw user prompt; builds an "
|
|
61
|
+
"input.kind='prompt' envelope. Mutually exclusive with "
|
|
62
|
+
"--ticket-file. Used only when the state file does not exist yet.",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--diff-file",
|
|
66
|
+
type=Path,
|
|
67
|
+
default=None,
|
|
68
|
+
help="Plain-text file carrying a unified diff payload; builds an "
|
|
69
|
+
"input.kind='diff' envelope routed through the UI-improve "
|
|
70
|
+
"directive set. Mutually exclusive with --ticket-file / "
|
|
71
|
+
"--prompt-file / --file-file. Used only when the state file does "
|
|
72
|
+
"not exist yet.",
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--file-file",
|
|
76
|
+
type=Path,
|
|
77
|
+
default=None,
|
|
78
|
+
help="Plain-text file carrying a single path reference (one line); "
|
|
79
|
+
"builds an input.kind='file' envelope routed through the UI-improve "
|
|
80
|
+
"directive set. Mutually exclusive with --ticket-file / "
|
|
81
|
+
"--prompt-file / --diff-file. Used only when the state file does "
|
|
82
|
+
"not exist yet.",
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--persona",
|
|
86
|
+
type=str,
|
|
87
|
+
default=None,
|
|
88
|
+
help="Persona name (senior-engineer | qa | advisory). Only honoured "
|
|
89
|
+
"when the state file does not exist yet; ignored on resume so a "
|
|
90
|
+
"mid-flight persona switch cannot silently change behaviour.",
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--no-hooks",
|
|
94
|
+
action="store_true",
|
|
95
|
+
default=False,
|
|
96
|
+
help="Disable every lifecycle hook for this run. Use in golden-"
|
|
97
|
+
"replay test harnesses so a future settings change cannot "
|
|
98
|
+
"silently invalidate captured outputs.",
|
|
99
|
+
)
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--hooks-config",
|
|
102
|
+
type=Path,
|
|
103
|
+
default=None,
|
|
104
|
+
help="Override the path to the agent-settings file used to resolve "
|
|
105
|
+
"the hooks.* block. Defaults to ./.agent-settings.yml.",
|
|
106
|
+
)
|
|
107
|
+
return parser
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = [
|
|
111
|
+
"DEFAULT_STATE_FILE",
|
|
112
|
+
"LEGACY_STATE_FILE",
|
|
113
|
+
"_FMT_V0",
|
|
114
|
+
"_FMT_V1",
|
|
115
|
+
"_build_parser",
|
|
116
|
+
]
|