agentflow-proof 1.0.0__py3-none-any.whl
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.
- agentflow/__init__.py +5 -0
- agentflow/__main__.py +5 -0
- agentflow/aggregate.py +1015 -0
- agentflow/artifacts.py +331 -0
- agentflow/capabilities.py +124 -0
- agentflow/cli.py +2108 -0
- agentflow/cli_contract.py +238 -0
- agentflow/contracts.py +263 -0
- agentflow/coverage.py +438 -0
- agentflow/draft_plan.py +402 -0
- agentflow/events.py +201 -0
- agentflow/execution.py +1027 -0
- agentflow/execution_coverage.py +646 -0
- agentflow/git.py +89 -0
- agentflow/handoff.py +112 -0
- agentflow/hunks.py +245 -0
- agentflow/locks.py +71 -0
- agentflow/mcp_server.py +710 -0
- agentflow/packs.py +398 -0
- agentflow/porcelain.py +1014 -0
- agentflow/proof.py +1062 -0
- agentflow/receipts.py +604 -0
- agentflow/recommend.py +299 -0
- agentflow/review.py +924 -0
- agentflow/review_runner.py +516 -0
- agentflow/risk.py +328 -0
- agentflow/runtime.py +863 -0
- agentflow/stuck.py +344 -0
- agentflow/validation.py +696 -0
- agentflow/versioning.py +120 -0
- agentflow/viewer.py +391 -0
- agentflow/workflow_contract.py +164 -0
- agentflow_proof-1.0.0.dist-info/METADATA +524 -0
- agentflow_proof-1.0.0.dist-info/RECORD +38 -0
- agentflow_proof-1.0.0.dist-info/WHEEL +5 -0
- agentflow_proof-1.0.0.dist-info/entry_points.txt +3 -0
- agentflow_proof-1.0.0.dist-info/licenses/LICENSE +21 -0
- agentflow_proof-1.0.0.dist-info/top_level.txt +1 -0
agentflow/cli.py
ADDED
|
@@ -0,0 +1,2108 @@
|
|
|
1
|
+
"""Command line interface for Agentflow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .aggregate import analyze, parse_sources, write_canonical
|
|
14
|
+
from .artifacts import (
|
|
15
|
+
SCHEMA_VERSION,
|
|
16
|
+
append_jsonl,
|
|
17
|
+
create_initial_artifacts,
|
|
18
|
+
read_json,
|
|
19
|
+
read_jsonl,
|
|
20
|
+
utc_now,
|
|
21
|
+
write_json,
|
|
22
|
+
)
|
|
23
|
+
from .contracts import (
|
|
24
|
+
AMENDMENTS_SCHEMA_VERSION,
|
|
25
|
+
CONTEXT_RECEIPTS_SCHEMA_VERSION,
|
|
26
|
+
EVIDENCE_KINDS,
|
|
27
|
+
EVIDENCE_SCHEMA_VERSION,
|
|
28
|
+
FAILURES_SCHEMA_VERSION,
|
|
29
|
+
WORKFLOW_REVIEW_DEPTHS,
|
|
30
|
+
strict_mode,
|
|
31
|
+
)
|
|
32
|
+
from .execution import (
|
|
33
|
+
AMENDMENT_REASON_CODES,
|
|
34
|
+
amend_step,
|
|
35
|
+
block_step,
|
|
36
|
+
claim_step,
|
|
37
|
+
complete_step,
|
|
38
|
+
doctor,
|
|
39
|
+
fail_step,
|
|
40
|
+
init_execution_artifacts,
|
|
41
|
+
mark_step_verified,
|
|
42
|
+
next_step,
|
|
43
|
+
reclaim_step,
|
|
44
|
+
renew_lease,
|
|
45
|
+
require_lifecycle_owner,
|
|
46
|
+
)
|
|
47
|
+
from .execution_coverage import verify_run, verify_step
|
|
48
|
+
from .events import filter_events_since, project_events, valid_since
|
|
49
|
+
from .capabilities import append_capability_receipt, build_capability_receipt
|
|
50
|
+
from .handoff import export_handoff, lint_handoff_text
|
|
51
|
+
from . import porcelain
|
|
52
|
+
from .packs import (
|
|
53
|
+
PackError,
|
|
54
|
+
find_profile,
|
|
55
|
+
inspect_summary,
|
|
56
|
+
load_pack,
|
|
57
|
+
profile_to_contract,
|
|
58
|
+
render_inspect_summary,
|
|
59
|
+
template_to_plan,
|
|
60
|
+
)
|
|
61
|
+
from .draft_plan import (
|
|
62
|
+
DRAFT_PLAN_SCHEMA_VERSION,
|
|
63
|
+
DraftPlanError,
|
|
64
|
+
compile_draft_plan,
|
|
65
|
+
selection_reason,
|
|
66
|
+
)
|
|
67
|
+
from .proof import (
|
|
68
|
+
InvalidWorkingState,
|
|
69
|
+
build_proof,
|
|
70
|
+
render_markdown,
|
|
71
|
+
verify_proof,
|
|
72
|
+
write_proof_metadata,
|
|
73
|
+
)
|
|
74
|
+
from .viewer import collect_view_model, render_html
|
|
75
|
+
from .stuck import Thresholds, detect_stuck
|
|
76
|
+
from .recommend import (
|
|
77
|
+
RecommendError,
|
|
78
|
+
recommend as recommend_workflow,
|
|
79
|
+
render_text as render_recommendation,
|
|
80
|
+
validate_brief,
|
|
81
|
+
)
|
|
82
|
+
from .receipts import record_command, record_file_change, replay_gates, run_command
|
|
83
|
+
from .review import parse_finding_ref, record_review_run, review_evidence_entries
|
|
84
|
+
from .review_runner import MANIFEST_FILENAME, exit_code_for, produce_manifest
|
|
85
|
+
from .runtime import build_runtime_status
|
|
86
|
+
from .validation import (
|
|
87
|
+
audit_drift,
|
|
88
|
+
validate_design_decision_traceability,
|
|
89
|
+
validate_plan,
|
|
90
|
+
validate_requirement_traceability,
|
|
91
|
+
)
|
|
92
|
+
from .workflow_contract import (
|
|
93
|
+
WORKFLOW_CONTRACT_PATH,
|
|
94
|
+
validate_workflow_contract,
|
|
95
|
+
write_workflow_contract,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def print_lines(lines: List[str]) -> None:
|
|
100
|
+
for line in lines:
|
|
101
|
+
print(line)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _positive_int(value: str) -> int:
|
|
105
|
+
try:
|
|
106
|
+
parsed = int(value)
|
|
107
|
+
except ValueError as exc:
|
|
108
|
+
raise argparse.ArgumentTypeError("must be a positive integer") from exc
|
|
109
|
+
if parsed < 1:
|
|
110
|
+
raise argparse.ArgumentTypeError("must be a positive integer")
|
|
111
|
+
return parsed
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def command_init(args: argparse.Namespace) -> int:
|
|
115
|
+
root = Path(args.root).resolve()
|
|
116
|
+
pack_path = getattr(args, "pack", None)
|
|
117
|
+
profile_id = getattr(args, "profile", None)
|
|
118
|
+
if profile_id and not pack_path:
|
|
119
|
+
print("error: --profile requires --pack", file=sys.stderr)
|
|
120
|
+
return 2
|
|
121
|
+
if pack_path:
|
|
122
|
+
return _command_init_with_pack(root, pack_path, profile_id, args)
|
|
123
|
+
created, skipped = create_initial_artifacts(root, force=args.force)
|
|
124
|
+
for path in created:
|
|
125
|
+
print(f"created {path}")
|
|
126
|
+
for path in skipped:
|
|
127
|
+
print(f"skipped {path}")
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _command_init_with_pack(
|
|
132
|
+
root: Path, pack_path: str, profile_id: Optional[str], args: argparse.Namespace
|
|
133
|
+
) -> int:
|
|
134
|
+
if not profile_id:
|
|
135
|
+
print("error: --pack requires --profile", file=sys.stderr)
|
|
136
|
+
return 2
|
|
137
|
+
try:
|
|
138
|
+
pack = load_pack(Path(pack_path))
|
|
139
|
+
profile = profile_to_contract(
|
|
140
|
+
pack.manifest,
|
|
141
|
+
profile_id,
|
|
142
|
+
"init --pack",
|
|
143
|
+
args.reason
|
|
144
|
+
or f"Selected pack {pack.manifest['id']} profile {profile_id} via init --pack.",
|
|
145
|
+
)
|
|
146
|
+
template_id = find_profile(pack.manifest, profile_id)["plan_template"]
|
|
147
|
+
plan = template_to_plan(pack.manifest, template_id)
|
|
148
|
+
except PackError as exc:
|
|
149
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
150
|
+
return 1
|
|
151
|
+
|
|
152
|
+
plan_path = root / ".agent" / "plan.lock.json"
|
|
153
|
+
contract_path = root / WORKFLOW_CONTRACT_PATH
|
|
154
|
+
if (plan_path.exists() or contract_path.exists()) and not args.force:
|
|
155
|
+
print(
|
|
156
|
+
"error: refusing to overwrite existing .agent/plan.lock.json or "
|
|
157
|
+
".agent/workflow.contract.json (use --force)",
|
|
158
|
+
file=sys.stderr,
|
|
159
|
+
)
|
|
160
|
+
return 1
|
|
161
|
+
|
|
162
|
+
# Scaffold base artifacts without force: never wipe pre-existing ledgers
|
|
163
|
+
# when --force was passed only to override the plan/contract conflict above.
|
|
164
|
+
# The plan and contract below are written explicitly regardless.
|
|
165
|
+
created, _skipped = create_initial_artifacts(root, force=False)
|
|
166
|
+
for path in created:
|
|
167
|
+
print(f"created {path}")
|
|
168
|
+
write_json(plan_path, plan)
|
|
169
|
+
write_workflow_contract(root, profile)
|
|
170
|
+
print(f"seeded .agent/plan.lock.json from template {template_id}")
|
|
171
|
+
print(f"wrote {WORKFLOW_CONTRACT_PATH}")
|
|
172
|
+
return 0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def command_pack_inspect(args: argparse.Namespace) -> int:
|
|
176
|
+
try:
|
|
177
|
+
pack = load_pack(Path(args.path))
|
|
178
|
+
except PackError as exc:
|
|
179
|
+
if args.json:
|
|
180
|
+
print(
|
|
181
|
+
json.dumps(
|
|
182
|
+
{
|
|
183
|
+
"status": "invalid",
|
|
184
|
+
"errors": [_plan_diagnostic("validation_error", str(exc))],
|
|
185
|
+
},
|
|
186
|
+
indent=2,
|
|
187
|
+
sort_keys=True,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
else:
|
|
191
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
192
|
+
return 1
|
|
193
|
+
summary = inspect_summary(pack)
|
|
194
|
+
if args.json:
|
|
195
|
+
print(json.dumps(summary, indent=2, sort_keys=True))
|
|
196
|
+
else:
|
|
197
|
+
print_lines(render_inspect_summary(summary))
|
|
198
|
+
return 0
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def command_init_execution(args: argparse.Namespace) -> int:
|
|
202
|
+
root = Path(args.root).resolve()
|
|
203
|
+
created, skipped = init_execution_artifacts(root, force=args.force)
|
|
204
|
+
for path in created:
|
|
205
|
+
print(f"created {path}")
|
|
206
|
+
for path in skipped:
|
|
207
|
+
print(f"skipped {path}")
|
|
208
|
+
return 0
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def command_doctor(args: argparse.Namespace) -> int:
|
|
212
|
+
root = Path(args.root).resolve()
|
|
213
|
+
result = doctor(root)
|
|
214
|
+
if args.json:
|
|
215
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
216
|
+
else:
|
|
217
|
+
print(f"doctor {result['status']}")
|
|
218
|
+
for finding in result["findings"]:
|
|
219
|
+
print(f"{finding['severity']}: {finding['message']}")
|
|
220
|
+
return 1 if result["status"] == "failed" else 0
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def command_intake(args: argparse.Namespace) -> int:
|
|
224
|
+
root = Path(args.root).resolve()
|
|
225
|
+
source = Path(args.from_file)
|
|
226
|
+
text = source.read_text(encoding="utf-8")
|
|
227
|
+
objective = next((line.strip() for line in text.splitlines() if line.strip()), "")
|
|
228
|
+
intake = {
|
|
229
|
+
# Intake is a transient draft artifact and intentionally tracks the plan schema.
|
|
230
|
+
"schema_version": SCHEMA_VERSION,
|
|
231
|
+
"objective": objective,
|
|
232
|
+
"non_goals": [],
|
|
233
|
+
"success_criteria": [],
|
|
234
|
+
"risk_level": "low",
|
|
235
|
+
"requires_user_approval": False,
|
|
236
|
+
"initial_unknowns": [],
|
|
237
|
+
"source": str(source),
|
|
238
|
+
"created_at": utc_now(),
|
|
239
|
+
}
|
|
240
|
+
output = root / ".agent/intake.json"
|
|
241
|
+
write_json(output, intake)
|
|
242
|
+
print(f"created {output.relative_to(root)}")
|
|
243
|
+
return 0
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _load_plan(path: Path) -> Dict[str, Any]:
|
|
247
|
+
if not path.exists():
|
|
248
|
+
raise FileNotFoundError(f"plan file not found: {path}")
|
|
249
|
+
return read_json(path)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _plan_diagnostic(code: str, message: str) -> Dict[str, str]:
|
|
253
|
+
return {"code": code, "message": message}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _print_plan_result(
|
|
257
|
+
status: str,
|
|
258
|
+
*,
|
|
259
|
+
path: Optional[Path] = None,
|
|
260
|
+
errors: Optional[List[Dict[str, str]]] = None,
|
|
261
|
+
) -> None:
|
|
262
|
+
payload: Dict[str, Any] = {
|
|
263
|
+
"status": status,
|
|
264
|
+
"errors": errors or [],
|
|
265
|
+
}
|
|
266
|
+
if path is not None:
|
|
267
|
+
payload["path"] = str(path)
|
|
268
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _json_decode_message(source: str, exc: json.JSONDecodeError) -> str:
|
|
272
|
+
return f"{source}: invalid JSON at line {exc.lineno} column {exc.colno}: {exc.msg}"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _coerce_plan_object(data: Any, source: str) -> Dict[str, Any]:
|
|
276
|
+
if not isinstance(data, dict):
|
|
277
|
+
raise ValueError(f"{source}: top-level value must be a JSON object")
|
|
278
|
+
return data
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _load_plan_from_path(path: Path) -> Dict[str, Any]:
|
|
282
|
+
if not path.exists():
|
|
283
|
+
raise FileNotFoundError(f"plan file not found: {path}")
|
|
284
|
+
try:
|
|
285
|
+
return _coerce_plan_object(read_json(path), str(path))
|
|
286
|
+
except json.JSONDecodeError as exc:
|
|
287
|
+
raise ValueError(_json_decode_message(str(path), exc)) from exc
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _load_plan_from_stdin() -> Dict[str, Any]:
|
|
291
|
+
try:
|
|
292
|
+
data = json.loads(sys.stdin.read())
|
|
293
|
+
except json.JSONDecodeError as exc:
|
|
294
|
+
raise ValueError(_json_decode_message("stdin", exc)) from exc
|
|
295
|
+
return _coerce_plan_object(data, "stdin")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _load_root_plan(root: Path) -> Dict[str, Any]:
|
|
299
|
+
return read_json(root / ".agent/plan.lock.json")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def command_validate_plan(args: argparse.Namespace) -> int:
|
|
303
|
+
plan_path = Path(args.plan).resolve()
|
|
304
|
+
try:
|
|
305
|
+
plan = _load_plan(plan_path)
|
|
306
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
307
|
+
# One presentation for every unreadable plan (missing, malformed JSON,
|
|
308
|
+
# or schema_version policy rejection): stderr + exit 1.
|
|
309
|
+
print(f"invalid plan: {exc}", file=sys.stderr)
|
|
310
|
+
return 1
|
|
311
|
+
|
|
312
|
+
errors = validate_plan(plan)
|
|
313
|
+
if errors:
|
|
314
|
+
print("plan invalid")
|
|
315
|
+
for error in errors:
|
|
316
|
+
print(f"- {error}")
|
|
317
|
+
return 1
|
|
318
|
+
|
|
319
|
+
print("plan valid")
|
|
320
|
+
return 0
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def command_lock_plan(args: argparse.Namespace) -> int:
|
|
324
|
+
if args.stdin and args.from_json:
|
|
325
|
+
message = "--stdin and --from-json are mutually exclusive"
|
|
326
|
+
if args.json:
|
|
327
|
+
_print_plan_result(
|
|
328
|
+
"invalid",
|
|
329
|
+
errors=[_plan_diagnostic("invalid_arguments", message)],
|
|
330
|
+
)
|
|
331
|
+
else:
|
|
332
|
+
print(message, file=sys.stderr)
|
|
333
|
+
return 2
|
|
334
|
+
|
|
335
|
+
output_path = Path(args.plan).resolve()
|
|
336
|
+
try:
|
|
337
|
+
if args.stdin:
|
|
338
|
+
plan = _load_plan_from_stdin()
|
|
339
|
+
elif args.from_json:
|
|
340
|
+
plan = _load_plan_from_path(Path(args.from_json).resolve())
|
|
341
|
+
else:
|
|
342
|
+
plan = _load_plan_from_path(output_path)
|
|
343
|
+
except FileNotFoundError as exc:
|
|
344
|
+
if args.json:
|
|
345
|
+
_print_plan_result(
|
|
346
|
+
"invalid",
|
|
347
|
+
errors=[_plan_diagnostic("input_not_found", str(exc))],
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
print(f"invalid plan: {exc}", file=sys.stderr)
|
|
351
|
+
return 1
|
|
352
|
+
except ValueError as exc:
|
|
353
|
+
code = (
|
|
354
|
+
"invalid_json"
|
|
355
|
+
if isinstance(exc.__cause__, json.JSONDecodeError)
|
|
356
|
+
else "invalid_plan_json"
|
|
357
|
+
)
|
|
358
|
+
if args.json:
|
|
359
|
+
_print_plan_result("invalid", errors=[_plan_diagnostic(code, str(exc))])
|
|
360
|
+
else:
|
|
361
|
+
print(f"invalid plan: {exc}", file=sys.stderr)
|
|
362
|
+
return 1
|
|
363
|
+
|
|
364
|
+
errors = validate_plan(plan)
|
|
365
|
+
if errors:
|
|
366
|
+
if args.json:
|
|
367
|
+
_print_plan_result(
|
|
368
|
+
"invalid",
|
|
369
|
+
errors=[_plan_diagnostic("validation_error", error) for error in errors],
|
|
370
|
+
)
|
|
371
|
+
else:
|
|
372
|
+
print("plan invalid")
|
|
373
|
+
for error in errors:
|
|
374
|
+
print(f"- {error}")
|
|
375
|
+
return 1
|
|
376
|
+
|
|
377
|
+
plan["locked"] = True
|
|
378
|
+
plan["locked_at"] = utc_now()
|
|
379
|
+
write_json(output_path, plan)
|
|
380
|
+
if args.json:
|
|
381
|
+
_print_plan_result("locked", path=output_path)
|
|
382
|
+
else:
|
|
383
|
+
print(f"locked {output_path}")
|
|
384
|
+
return 0
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _load_json_object(path: Path, kind: str) -> Dict[str, Any]:
|
|
388
|
+
if not path.exists():
|
|
389
|
+
raise FileNotFoundError(f"{kind} file not found: {path}")
|
|
390
|
+
try:
|
|
391
|
+
data = read_json(path)
|
|
392
|
+
except json.JSONDecodeError as exc:
|
|
393
|
+
raise ValueError(_json_decode_message(str(path), exc)) from exc
|
|
394
|
+
if not isinstance(data, dict):
|
|
395
|
+
raise ValueError(f"{path}: top-level value must be a JSON object")
|
|
396
|
+
return data
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _print_workflow_contract_errors(errors: List[str]) -> None:
|
|
400
|
+
print("workflow contract invalid")
|
|
401
|
+
for error in errors:
|
|
402
|
+
print(f"- {error}")
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def command_workflow_contract(args: argparse.Namespace) -> int:
|
|
406
|
+
root = Path(args.root).resolve()
|
|
407
|
+
if args.from_json:
|
|
408
|
+
if args.path != WORKFLOW_CONTRACT_PATH:
|
|
409
|
+
print(
|
|
410
|
+
f"PATH cannot be used with --from-json; writes go to {WORKFLOW_CONTRACT_PATH}",
|
|
411
|
+
file=sys.stderr,
|
|
412
|
+
)
|
|
413
|
+
return 2
|
|
414
|
+
source = Path(args.from_json)
|
|
415
|
+
if not source.is_absolute():
|
|
416
|
+
source = root / source
|
|
417
|
+
try:
|
|
418
|
+
contract = _load_json_object(source, "workflow contract")
|
|
419
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
420
|
+
print(f"workflow contract invalid: {exc}", file=sys.stderr)
|
|
421
|
+
return 1
|
|
422
|
+
errors = validate_workflow_contract(contract)
|
|
423
|
+
if errors:
|
|
424
|
+
_print_workflow_contract_errors(errors)
|
|
425
|
+
return 1
|
|
426
|
+
path = write_workflow_contract(root, contract)
|
|
427
|
+
print(f"wrote {path.relative_to(root)}")
|
|
428
|
+
return 0
|
|
429
|
+
|
|
430
|
+
target = Path(args.path or WORKFLOW_CONTRACT_PATH)
|
|
431
|
+
if not target.is_absolute():
|
|
432
|
+
target = root / target
|
|
433
|
+
try:
|
|
434
|
+
contract = _load_json_object(target, "workflow contract")
|
|
435
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
436
|
+
print(f"workflow contract invalid: {exc}", file=sys.stderr)
|
|
437
|
+
return 1
|
|
438
|
+
errors = validate_workflow_contract(contract)
|
|
439
|
+
if errors:
|
|
440
|
+
_print_workflow_contract_errors(errors)
|
|
441
|
+
return 1
|
|
442
|
+
print("workflow contract valid")
|
|
443
|
+
return 0
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def command_amend_plan(args: argparse.Namespace) -> int:
|
|
447
|
+
root = Path(args.root).resolve()
|
|
448
|
+
amendment = {
|
|
449
|
+
"schema_version": AMENDMENTS_SCHEMA_VERSION,
|
|
450
|
+
"id": args.id,
|
|
451
|
+
"reason": args.reason,
|
|
452
|
+
"new_evidence": args.evidence or [],
|
|
453
|
+
"changed_scope": args.changed_scope or [],
|
|
454
|
+
"changed_steps": args.changed_steps or [],
|
|
455
|
+
"changed_risk": args.changed_risk,
|
|
456
|
+
"requires_approval": args.requires_approval,
|
|
457
|
+
"created_at": utc_now(),
|
|
458
|
+
}
|
|
459
|
+
append_jsonl(root / ".agent/amendments.jsonl", amendment)
|
|
460
|
+
print(f"recorded amendment {args.id}")
|
|
461
|
+
return 0
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def command_record_evidence(args: argparse.Namespace) -> int:
|
|
465
|
+
root = Path(args.root).resolve()
|
|
466
|
+
evidence = {
|
|
467
|
+
"schema_version": EVIDENCE_SCHEMA_VERSION,
|
|
468
|
+
"id": args.id,
|
|
469
|
+
"claim": args.claim,
|
|
470
|
+
"source": args.source,
|
|
471
|
+
"confidence": args.confidence,
|
|
472
|
+
"last_verified": utc_now(),
|
|
473
|
+
}
|
|
474
|
+
if args.kind:
|
|
475
|
+
evidence["kind"] = args.kind
|
|
476
|
+
if args.supports:
|
|
477
|
+
evidence["supports"] = args.supports
|
|
478
|
+
append_jsonl(root / ".agent/evidence.jsonl", evidence)
|
|
479
|
+
print(f"recorded evidence {args.id}")
|
|
480
|
+
return 0
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def command_record_review(args: argparse.Namespace) -> int:
|
|
484
|
+
root = Path(args.root).resolve()
|
|
485
|
+
manifest_path = Path(args.manifest)
|
|
486
|
+
if not manifest_path.is_absolute():
|
|
487
|
+
manifest_path = root / manifest_path
|
|
488
|
+
try:
|
|
489
|
+
record = record_review_run(root, manifest_path)
|
|
490
|
+
except ValueError as exc:
|
|
491
|
+
print(f"invalid review manifest: {exc}", file=sys.stderr)
|
|
492
|
+
return 1
|
|
493
|
+
if args.emit_evidence:
|
|
494
|
+
for entry in review_evidence_entries(record):
|
|
495
|
+
append_jsonl(root / ".agent/evidence.jsonl", entry)
|
|
496
|
+
if args.json:
|
|
497
|
+
print(json.dumps(record, indent=2, sort_keys=True))
|
|
498
|
+
else:
|
|
499
|
+
print(f"recorded review run {record['review_run_id']}")
|
|
500
|
+
return 0
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def command_record_capability(args: argparse.Namespace) -> int:
|
|
504
|
+
root = Path(args.root).resolve()
|
|
505
|
+
try:
|
|
506
|
+
row = build_capability_receipt(
|
|
507
|
+
args.id,
|
|
508
|
+
args.capability,
|
|
509
|
+
"used",
|
|
510
|
+
args.reason,
|
|
511
|
+
provider=args.provider,
|
|
512
|
+
evidence=args.evidence,
|
|
513
|
+
)
|
|
514
|
+
except ValueError as exc:
|
|
515
|
+
print(f"invalid capability receipt: {exc}", file=sys.stderr)
|
|
516
|
+
return 1
|
|
517
|
+
append_capability_receipt(root, row)
|
|
518
|
+
print(f"recorded capability {row['id']}")
|
|
519
|
+
return 0
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def command_waive_capability(args: argparse.Namespace) -> int:
|
|
523
|
+
root = Path(args.root).resolve()
|
|
524
|
+
try:
|
|
525
|
+
row = build_capability_receipt(
|
|
526
|
+
args.id,
|
|
527
|
+
args.capability,
|
|
528
|
+
"waived",
|
|
529
|
+
args.reason,
|
|
530
|
+
provider=None,
|
|
531
|
+
evidence=args.evidence,
|
|
532
|
+
)
|
|
533
|
+
except ValueError as exc:
|
|
534
|
+
print(f"invalid capability receipt: {exc}", file=sys.stderr)
|
|
535
|
+
return 1
|
|
536
|
+
append_capability_receipt(root, row)
|
|
537
|
+
print(f"waived capability {row['id']}")
|
|
538
|
+
return 0
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def command_review_manifest(args: argparse.Namespace) -> int:
|
|
542
|
+
root = Path(args.root).resolve()
|
|
543
|
+
config_path = Path(args.config)
|
|
544
|
+
if not config_path.is_absolute():
|
|
545
|
+
config_path = root / config_path
|
|
546
|
+
try:
|
|
547
|
+
manifest = produce_manifest(
|
|
548
|
+
root=root,
|
|
549
|
+
state_dir=args.state_dir,
|
|
550
|
+
branch=args.branch,
|
|
551
|
+
findings_json=args.findings_json,
|
|
552
|
+
config_path=config_path,
|
|
553
|
+
depth_profile=args.depth_profile,
|
|
554
|
+
)
|
|
555
|
+
except ValueError as exc:
|
|
556
|
+
print(f"review-manifest failed: {exc}", file=sys.stderr)
|
|
557
|
+
return 1
|
|
558
|
+
if args.write:
|
|
559
|
+
out_dir = (root / args.state_dir).resolve()
|
|
560
|
+
write_json(out_dir / MANIFEST_FILENAME, manifest)
|
|
561
|
+
if args.json:
|
|
562
|
+
print(json.dumps(manifest, indent=2, sort_keys=True))
|
|
563
|
+
else:
|
|
564
|
+
print(
|
|
565
|
+
f"review-manifest {manifest['review_run_id']} "
|
|
566
|
+
f"gate_status={manifest['gate_status']} "
|
|
567
|
+
f"active_blocking={len(manifest['active_blocking'])}"
|
|
568
|
+
)
|
|
569
|
+
return exit_code_for(manifest, args.fail_on_block, args.strict_exit)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def command_record_context(args: argparse.Namespace) -> int:
|
|
573
|
+
root = Path(args.root).resolve()
|
|
574
|
+
if args.bytes is not None and args.bytes < 0:
|
|
575
|
+
print("context bytes must be non-negative", file=sys.stderr)
|
|
576
|
+
return 1
|
|
577
|
+
receipt = {
|
|
578
|
+
"schema_version": CONTEXT_RECEIPTS_SCHEMA_VERSION,
|
|
579
|
+
"id": args.id,
|
|
580
|
+
"source": args.source,
|
|
581
|
+
"reason": args.reason,
|
|
582
|
+
"used_for": args.used_for or [],
|
|
583
|
+
"created_at": utc_now(),
|
|
584
|
+
}
|
|
585
|
+
if args.bytes is not None:
|
|
586
|
+
receipt["bytes"] = args.bytes
|
|
587
|
+
append_jsonl(root / ".agent/context-receipts.jsonl", receipt)
|
|
588
|
+
print(f"recorded context receipt {args.id}")
|
|
589
|
+
return 0
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def command_record_failure(args: argparse.Namespace) -> int:
|
|
593
|
+
root = Path(args.root).resolve()
|
|
594
|
+
failure = {
|
|
595
|
+
"schema_version": FAILURES_SCHEMA_VERSION,
|
|
596
|
+
"command": args.command,
|
|
597
|
+
"failure_type": args.failure_type,
|
|
598
|
+
"relevant_lines": args.relevant_lines or [],
|
|
599
|
+
"suspected_cause": args.suspected_cause,
|
|
600
|
+
"next_action": args.next_action,
|
|
601
|
+
"created_at": utc_now(),
|
|
602
|
+
}
|
|
603
|
+
append_jsonl(root / ".agent/failures.jsonl", failure)
|
|
604
|
+
print(f"recorded failure signature for {args.command}")
|
|
605
|
+
return 0
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def command_audit_drift(args: argparse.Namespace) -> int:
|
|
609
|
+
root = Path(args.root).resolve()
|
|
610
|
+
plan_path = Path(args.plan)
|
|
611
|
+
if not plan_path.is_absolute():
|
|
612
|
+
plan_path = root / plan_path
|
|
613
|
+
|
|
614
|
+
try:
|
|
615
|
+
plan = _load_plan(plan_path)
|
|
616
|
+
except (FileNotFoundError, json.JSONDecodeError) as exc:
|
|
617
|
+
print(f"invalid plan: {exc}", file=sys.stderr)
|
|
618
|
+
return 1
|
|
619
|
+
|
|
620
|
+
report = audit_drift(root, plan)
|
|
621
|
+
output = root / ".agent/drift-report.json"
|
|
622
|
+
write_json(output, report)
|
|
623
|
+
print(f"drift audit {report['status']}")
|
|
624
|
+
if report["out_of_scope_files"]:
|
|
625
|
+
print("out of scope:")
|
|
626
|
+
print_lines([f"- {path}" for path in report["out_of_scope_files"]])
|
|
627
|
+
if report["blocked_files_changed"]:
|
|
628
|
+
print("blocked files changed:")
|
|
629
|
+
print_lines([f"- {path}" for path in report["blocked_files_changed"]])
|
|
630
|
+
if report.get("unmapped_hunks"):
|
|
631
|
+
print("unmapped hunks:")
|
|
632
|
+
hunk_lines = []
|
|
633
|
+
for entry in report["unmapped_hunks"]:
|
|
634
|
+
if isinstance(entry, dict):
|
|
635
|
+
hunk_lines.append(
|
|
636
|
+
f"- {entry['path']} @@ -{entry['old_start']},{entry['old_count']} "
|
|
637
|
+
f"+{entry['new_start']},{entry['new_count']} @@ "
|
|
638
|
+
f"{entry['hash'][:12]} ({entry['reason']})"
|
|
639
|
+
)
|
|
640
|
+
else:
|
|
641
|
+
hunk_lines.append(f"- {entry}")
|
|
642
|
+
print_lines(hunk_lines)
|
|
643
|
+
for note in report["notes"]:
|
|
644
|
+
print(f"note: {note}")
|
|
645
|
+
return 1 if report["status"] == "fail" else 0
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def command_next_step(args: argparse.Namespace) -> int:
|
|
649
|
+
root = Path(args.root).resolve()
|
|
650
|
+
plan = _load_root_plan(root)
|
|
651
|
+
step = next_step(root, plan)
|
|
652
|
+
if args.json:
|
|
653
|
+
print(json.dumps(step, indent=2, sort_keys=True))
|
|
654
|
+
elif step:
|
|
655
|
+
print(step["id"])
|
|
656
|
+
else:
|
|
657
|
+
print("no eligible step")
|
|
658
|
+
return 0
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def command_claim_step(args: argparse.Namespace) -> int:
|
|
662
|
+
root = Path(args.root).resolve()
|
|
663
|
+
plan = _load_root_plan(root)
|
|
664
|
+
try:
|
|
665
|
+
event = claim_step(root, plan, args.step, args.agent, args.lease_minutes)
|
|
666
|
+
except ValueError as exc:
|
|
667
|
+
print(str(exc), file=sys.stderr)
|
|
668
|
+
return 2
|
|
669
|
+
if args.json:
|
|
670
|
+
print(json.dumps(event, indent=2, sort_keys=True))
|
|
671
|
+
else:
|
|
672
|
+
print(f"claimed {event['step_id']} as {event['attempt_id']}")
|
|
673
|
+
return 0
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def command_amend_step(args: argparse.Namespace) -> int:
|
|
677
|
+
root = Path(args.root).resolve()
|
|
678
|
+
plan = _load_root_plan(root)
|
|
679
|
+
finding_refs = []
|
|
680
|
+
for raw in args.finding or []:
|
|
681
|
+
try:
|
|
682
|
+
finding_refs.append(parse_finding_ref(raw))
|
|
683
|
+
except ValueError as exc:
|
|
684
|
+
print(str(exc), file=sys.stderr)
|
|
685
|
+
return 2
|
|
686
|
+
try:
|
|
687
|
+
event = amend_step(
|
|
688
|
+
root,
|
|
689
|
+
plan,
|
|
690
|
+
args.step,
|
|
691
|
+
args.agent,
|
|
692
|
+
args.reason,
|
|
693
|
+
args.reason_code,
|
|
694
|
+
finding_refs=finding_refs or None,
|
|
695
|
+
lease_minutes=args.lease_minutes,
|
|
696
|
+
)
|
|
697
|
+
except ValueError as exc:
|
|
698
|
+
print(str(exc), file=sys.stderr)
|
|
699
|
+
return 2
|
|
700
|
+
if args.json:
|
|
701
|
+
print(json.dumps(event, indent=2, sort_keys=True))
|
|
702
|
+
else:
|
|
703
|
+
print(
|
|
704
|
+
f"amended {event['step_id']} as {event['attempt_id']} "
|
|
705
|
+
f"(amends {event['amends_attempt']})"
|
|
706
|
+
)
|
|
707
|
+
return 0
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def command_complete_step(args: argparse.Namespace) -> int:
|
|
711
|
+
root = Path(args.root).resolve()
|
|
712
|
+
try:
|
|
713
|
+
event = complete_step(root, args.step, args.attempt, agent_id=args.agent)
|
|
714
|
+
except ValueError as exc:
|
|
715
|
+
print(str(exc), file=sys.stderr)
|
|
716
|
+
return 2
|
|
717
|
+
except RuntimeError as exc:
|
|
718
|
+
print(str(exc), file=sys.stderr)
|
|
719
|
+
return 1
|
|
720
|
+
if args.json:
|
|
721
|
+
print(json.dumps(event, indent=2, sort_keys=True))
|
|
722
|
+
else:
|
|
723
|
+
print(f"completed {event['step_id']} {event['attempt_id']}")
|
|
724
|
+
return 0
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def command_block_step(args: argparse.Namespace) -> int:
|
|
728
|
+
root = Path(args.root).resolve()
|
|
729
|
+
try:
|
|
730
|
+
event = block_step(root, args.step, args.attempt, args.reason, agent_id=args.agent)
|
|
731
|
+
except ValueError as exc:
|
|
732
|
+
print(str(exc), file=sys.stderr)
|
|
733
|
+
return 2
|
|
734
|
+
if args.json:
|
|
735
|
+
print(json.dumps(event, indent=2, sort_keys=True))
|
|
736
|
+
else:
|
|
737
|
+
print(f"blocked {event['step_id']} {event['attempt_id']}")
|
|
738
|
+
return 0
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def command_fail_step(args: argparse.Namespace) -> int:
|
|
742
|
+
root = Path(args.root).resolve()
|
|
743
|
+
try:
|
|
744
|
+
event = fail_step(root, args.step, args.attempt, args.reason, agent_id=args.agent)
|
|
745
|
+
except ValueError as exc:
|
|
746
|
+
print(str(exc), file=sys.stderr)
|
|
747
|
+
return 2
|
|
748
|
+
if args.json:
|
|
749
|
+
print(json.dumps(event, indent=2, sort_keys=True))
|
|
750
|
+
else:
|
|
751
|
+
print(f"failed {event['step_id']} {event['attempt_id']}")
|
|
752
|
+
return 0
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def command_reclaim_step(args: argparse.Namespace) -> int:
|
|
756
|
+
root = Path(args.root).resolve()
|
|
757
|
+
plan = _load_root_plan(root)
|
|
758
|
+
try:
|
|
759
|
+
event = reclaim_step(
|
|
760
|
+
root, plan, args.step, args.agent, args.reason, args.lease_minutes
|
|
761
|
+
)
|
|
762
|
+
except ValueError as exc:
|
|
763
|
+
print(str(exc), file=sys.stderr)
|
|
764
|
+
return 2
|
|
765
|
+
if args.json:
|
|
766
|
+
print(json.dumps(event, indent=2, sort_keys=True))
|
|
767
|
+
else:
|
|
768
|
+
print(f"reclaimed {event['step_id']} as {event['attempt_id']}")
|
|
769
|
+
return 0
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def command_renew_lease(args: argparse.Namespace) -> int:
|
|
773
|
+
root = Path(args.root).resolve()
|
|
774
|
+
try:
|
|
775
|
+
event = renew_lease(root, args.step, args.attempt, args.agent, args.minutes)
|
|
776
|
+
except ValueError as exc:
|
|
777
|
+
print(str(exc), file=sys.stderr)
|
|
778
|
+
return 2
|
|
779
|
+
if args.json:
|
|
780
|
+
print(json.dumps(event, indent=2, sort_keys=True))
|
|
781
|
+
else:
|
|
782
|
+
print(f"renewed {event['step_id']} {event['attempt_id']} "
|
|
783
|
+
f"until {event['lease_expires_at']}")
|
|
784
|
+
return 0
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _normalize_remainder(command: List[str]) -> List[str]:
|
|
788
|
+
if command and command[0] == "--":
|
|
789
|
+
return command[1:]
|
|
790
|
+
return command
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def command_run(args: argparse.Namespace) -> int:
|
|
794
|
+
root = Path(args.root).resolve()
|
|
795
|
+
plan = _load_root_plan(root)
|
|
796
|
+
command = _normalize_remainder(args.command)
|
|
797
|
+
if not command:
|
|
798
|
+
print("run requires a command after --", file=sys.stderr)
|
|
799
|
+
return 2
|
|
800
|
+
confirmed = bool(getattr(args, "confirm_risk", False)) or (
|
|
801
|
+
os.environ.get("AGENTFLOW_CONFIRM_RISK") == "1"
|
|
802
|
+
)
|
|
803
|
+
source = "cli" if getattr(args, "confirm_risk", False) else ("env" if confirmed else None)
|
|
804
|
+
try:
|
|
805
|
+
receipt = run_command(
|
|
806
|
+
root, plan, args.step, args.attempt, command,
|
|
807
|
+
gate=args.gate, confirmed=confirmed, confirmation_source=source,
|
|
808
|
+
agent_id=args.agent,
|
|
809
|
+
)
|
|
810
|
+
except ValueError as exc:
|
|
811
|
+
print(str(exc), file=sys.stderr)
|
|
812
|
+
return 2
|
|
813
|
+
if receipt.get("decision") == "blocked":
|
|
814
|
+
categories = sorted({f["category"] for f in receipt["risk"]["findings"]})
|
|
815
|
+
risk_policy = receipt.get("risk_policy")
|
|
816
|
+
message = (
|
|
817
|
+
"blocked high-risk command ("
|
|
818
|
+
+ ", ".join(categories)
|
|
819
|
+
+ f"); risk_policy={risk_policy} refused execution."
|
|
820
|
+
)
|
|
821
|
+
if risk_policy == "require-confirmation":
|
|
822
|
+
message += " Re-run with --confirm-risk to override."
|
|
823
|
+
print(message, file=sys.stderr)
|
|
824
|
+
if args.json:
|
|
825
|
+
print(json.dumps(receipt, indent=2, sort_keys=True))
|
|
826
|
+
return 2
|
|
827
|
+
if receipt.get("decision") == "timeout":
|
|
828
|
+
timeout_seconds = receipt.get("timeout_seconds")
|
|
829
|
+
print(f"command timed out after {timeout_seconds} seconds", file=sys.stderr)
|
|
830
|
+
if args.json:
|
|
831
|
+
print(json.dumps(receipt, indent=2, sort_keys=True))
|
|
832
|
+
return 124
|
|
833
|
+
if args.json:
|
|
834
|
+
print(json.dumps(receipt, indent=2, sort_keys=True))
|
|
835
|
+
else:
|
|
836
|
+
print(f"recorded {receipt['id']} exit {receipt['exit_code']}")
|
|
837
|
+
return int(receipt["exit_code"])
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def command_record_command(args: argparse.Namespace) -> int:
|
|
841
|
+
root = Path(args.root).resolve()
|
|
842
|
+
plan = _load_root_plan(root)
|
|
843
|
+
command = _normalize_remainder(args.command)
|
|
844
|
+
if not command:
|
|
845
|
+
print("record-command requires a command after --", file=sys.stderr)
|
|
846
|
+
return 2
|
|
847
|
+
try:
|
|
848
|
+
receipt = record_command(
|
|
849
|
+
root,
|
|
850
|
+
args.step,
|
|
851
|
+
args.attempt,
|
|
852
|
+
command,
|
|
853
|
+
args.exit_code,
|
|
854
|
+
gate=args.gate,
|
|
855
|
+
plan=plan,
|
|
856
|
+
agent_id=args.agent,
|
|
857
|
+
)
|
|
858
|
+
except ValueError as exc:
|
|
859
|
+
print(str(exc), file=sys.stderr)
|
|
860
|
+
return 2
|
|
861
|
+
if args.json:
|
|
862
|
+
print(json.dumps(receipt, indent=2, sort_keys=True))
|
|
863
|
+
else:
|
|
864
|
+
print(f"recorded {receipt['id']} attested exit {receipt['exit_code']}")
|
|
865
|
+
return 0
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def command_record_file_change(args: argparse.Namespace) -> int:
|
|
869
|
+
root = Path(args.root).resolve()
|
|
870
|
+
plan = _load_root_plan(root)
|
|
871
|
+
receipts = []
|
|
872
|
+
try:
|
|
873
|
+
for path in args.path:
|
|
874
|
+
receipts.append(
|
|
875
|
+
record_file_change(root, plan, args.step, args.attempt, path, agent_id=args.agent)
|
|
876
|
+
)
|
|
877
|
+
except ValueError as exc:
|
|
878
|
+
print(str(exc), file=sys.stderr)
|
|
879
|
+
return 1
|
|
880
|
+
if args.json:
|
|
881
|
+
print(json.dumps(receipts, indent=2, sort_keys=True))
|
|
882
|
+
else:
|
|
883
|
+
for receipt in receipts:
|
|
884
|
+
print(f"recorded {receipt['id']} {receipt['path']}")
|
|
885
|
+
return 0
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def command_verify_step(args: argparse.Namespace) -> int:
|
|
889
|
+
root = Path(args.root).resolve()
|
|
890
|
+
plan = _load_root_plan(root)
|
|
891
|
+
try:
|
|
892
|
+
resolved_attempt = require_lifecycle_owner(
|
|
893
|
+
root, args.step, args.attempt, args.agent, action="verify"
|
|
894
|
+
)
|
|
895
|
+
result = verify_step(
|
|
896
|
+
root,
|
|
897
|
+
plan,
|
|
898
|
+
args.step,
|
|
899
|
+
resolved_attempt,
|
|
900
|
+
strict_mode(args.strict),
|
|
901
|
+
args.replay,
|
|
902
|
+
)
|
|
903
|
+
if result["status"] != "failed":
|
|
904
|
+
mark_step_verified(
|
|
905
|
+
root,
|
|
906
|
+
args.step,
|
|
907
|
+
resolved_attempt,
|
|
908
|
+
[*result["errors"], *result["warnings"]],
|
|
909
|
+
agent_id=args.agent,
|
|
910
|
+
)
|
|
911
|
+
except ValueError as exc:
|
|
912
|
+
print(str(exc), file=sys.stderr)
|
|
913
|
+
return 2
|
|
914
|
+
if args.json:
|
|
915
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
916
|
+
else:
|
|
917
|
+
print(f"verify-step {result['status']}")
|
|
918
|
+
for finding in [*result["errors"], *result["warnings"]]:
|
|
919
|
+
print(f"{finding['severity']}: {finding['message']}")
|
|
920
|
+
return 1 if result["status"] == "failed" else 0
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def command_verify_run(args: argparse.Namespace) -> int:
|
|
924
|
+
root = Path(args.root).resolve()
|
|
925
|
+
plan_path = Path(args.plan)
|
|
926
|
+
if not plan_path.is_absolute():
|
|
927
|
+
plan_path = root / plan_path
|
|
928
|
+
try:
|
|
929
|
+
plan = _load_plan(plan_path)
|
|
930
|
+
except (FileNotFoundError, json.JSONDecodeError) as exc:
|
|
931
|
+
print(f"invalid plan: {exc}", file=sys.stderr)
|
|
932
|
+
return 1
|
|
933
|
+
result = verify_run(root, plan, strict_mode(args.strict), record=args.record)
|
|
934
|
+
if args.json:
|
|
935
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
936
|
+
else:
|
|
937
|
+
print(f"verify-run {result['status']}")
|
|
938
|
+
for finding in [*result["errors"], *result["warnings"]]:
|
|
939
|
+
print(f"{finding['severity']}: {finding['message']}")
|
|
940
|
+
return 1 if result["status"] == "failed" else 0
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def command_aggregate_ledgers(args: argparse.Namespace) -> int:
|
|
944
|
+
inputs = args.input or []
|
|
945
|
+
source_ids = args.source_id or []
|
|
946
|
+
if not inputs:
|
|
947
|
+
print("aggregate-ledgers: at least one --input with --source-id is required", file=sys.stderr)
|
|
948
|
+
return 2
|
|
949
|
+
sources, errors = parse_sources(inputs, source_ids, args.label)
|
|
950
|
+
if errors:
|
|
951
|
+
for err in errors:
|
|
952
|
+
print(f"aggregate-ledgers: {err}", file=sys.stderr)
|
|
953
|
+
return 2
|
|
954
|
+
output_root = Path(args.output).resolve()
|
|
955
|
+
if not output_root.is_dir():
|
|
956
|
+
print(
|
|
957
|
+
f"aggregate-ledgers: --output {output_root} is not a directory "
|
|
958
|
+
"(it must already contain the merged source tree)",
|
|
959
|
+
file=sys.stderr,
|
|
960
|
+
)
|
|
961
|
+
return 2
|
|
962
|
+
if args.dry_run:
|
|
963
|
+
report = analyze(sources, output_root, base_ref=args.base)
|
|
964
|
+
if args.json:
|
|
965
|
+
print(json.dumps(report, indent=2, sort_keys=True))
|
|
966
|
+
else:
|
|
967
|
+
print(
|
|
968
|
+
f"aggregate-ledgers dry-run: {report['status']} "
|
|
969
|
+
f"({len(sources)} sources, {len(report['collisions'])} collisions)"
|
|
970
|
+
)
|
|
971
|
+
for col in report["collisions"]:
|
|
972
|
+
detail = {k: v for k, v in col.items() if k != "kind"}
|
|
973
|
+
print(f"collision: {col['kind']} {json.dumps(detail, sort_keys=True)}")
|
|
974
|
+
return 1 if report["status"] == "collision" else 0
|
|
975
|
+
|
|
976
|
+
result = write_canonical(sources, output_root, base_ref=args.base)
|
|
977
|
+
if args.json:
|
|
978
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
979
|
+
elif result["status"] == "collision":
|
|
980
|
+
print(
|
|
981
|
+
f"aggregate-ledgers: {result['status']} "
|
|
982
|
+
f"({len(sources)} sources, {len(result['collisions'])} collisions); nothing written"
|
|
983
|
+
)
|
|
984
|
+
for col in result["collisions"]:
|
|
985
|
+
detail = {k: v for k, v in col.items() if k != "kind"}
|
|
986
|
+
print(f"collision: {col['kind']} {json.dumps(detail, sort_keys=True)}")
|
|
987
|
+
else:
|
|
988
|
+
print(f"aggregate-ledgers: wrote canonical .agent/ into {output_root} ({len(sources)} sources)")
|
|
989
|
+
return 1 if result["status"] == "collision" else 0
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def command_detect_stuck(args: argparse.Namespace) -> int:
|
|
993
|
+
root = Path(args.root).resolve()
|
|
994
|
+
plan_path = Path(args.plan)
|
|
995
|
+
if not plan_path.is_absolute():
|
|
996
|
+
plan_path = root / plan_path
|
|
997
|
+
try:
|
|
998
|
+
plan = _load_plan(plan_path)
|
|
999
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
1000
|
+
plan = None
|
|
1001
|
+
thresholds = Thresholds(
|
|
1002
|
+
min_command_failures=args.min_command_failures,
|
|
1003
|
+
min_verify_failures=args.min_verify_failures,
|
|
1004
|
+
min_cycle_repeats=args.min_cycle_repeats,
|
|
1005
|
+
)
|
|
1006
|
+
report = detect_stuck(root, plan, thresholds)
|
|
1007
|
+
if args.json:
|
|
1008
|
+
print(json.dumps(report, indent=2, sort_keys=True))
|
|
1009
|
+
else:
|
|
1010
|
+
print(f"detect-stuck {report['status']}")
|
|
1011
|
+
for finding in report["findings"]:
|
|
1012
|
+
location = f"{finding['step_id']}/{finding['attempt_id']}"
|
|
1013
|
+
print(f"warning [{location}]: {finding['message']}")
|
|
1014
|
+
print(f" -> {finding['suggested_action']}")
|
|
1015
|
+
return 1 if (args.strict and report["findings"]) else 0
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def command_export_handoff(args: argparse.Namespace) -> int:
|
|
1019
|
+
root = Path(args.root).resolve()
|
|
1020
|
+
plan = _load_root_plan(root)
|
|
1021
|
+
try:
|
|
1022
|
+
rendered = export_handoff(plan, args.step, args.format)
|
|
1023
|
+
except ValueError as exc:
|
|
1024
|
+
print(str(exc), file=sys.stderr)
|
|
1025
|
+
return 1
|
|
1026
|
+
if args.output:
|
|
1027
|
+
output = Path(args.output)
|
|
1028
|
+
if not output.is_absolute():
|
|
1029
|
+
output = root / output
|
|
1030
|
+
else:
|
|
1031
|
+
suffix = "json" if args.format == "json" else "md"
|
|
1032
|
+
output = root / ".agent/handoffs" / f"{args.step}.{suffix}"
|
|
1033
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
1034
|
+
if args.format == "json":
|
|
1035
|
+
output.write_text(json.dumps(rendered, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
1036
|
+
else:
|
|
1037
|
+
output.write_text(str(rendered), encoding="utf-8")
|
|
1038
|
+
print(f"created {output.relative_to(root)}")
|
|
1039
|
+
return 0
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
def command_lint_handoff(args: argparse.Namespace) -> int:
|
|
1043
|
+
root = Path(args.root).resolve()
|
|
1044
|
+
path = Path(args.input)
|
|
1045
|
+
if not path.is_absolute():
|
|
1046
|
+
path = root / path
|
|
1047
|
+
findings = lint_handoff_text(path.read_text(encoding="utf-8"))
|
|
1048
|
+
result = {"findings": findings}
|
|
1049
|
+
if args.json:
|
|
1050
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
1051
|
+
else:
|
|
1052
|
+
for finding in findings:
|
|
1053
|
+
print(f"{finding['severity']}: {finding['message']}")
|
|
1054
|
+
if not findings:
|
|
1055
|
+
print("handoff valid")
|
|
1056
|
+
return 1 if findings else 0
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def command_replay_gates(args: argparse.Namespace) -> int:
|
|
1060
|
+
root = Path(args.root).resolve()
|
|
1061
|
+
plan = _load_root_plan(root)
|
|
1062
|
+
result = replay_gates(root, plan, step_id=args.step, record=args.record)
|
|
1063
|
+
if args.json:
|
|
1064
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
1065
|
+
else:
|
|
1066
|
+
print(f"replay-gates {result['status']}")
|
|
1067
|
+
for finding in [*result["errors"], *result["warnings"]]:
|
|
1068
|
+
print(f"{finding['severity']}: {finding['message']}")
|
|
1069
|
+
return 1 if result["status"] == "failed" else 0
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
def command_runtime_status(args: argparse.Namespace) -> int:
|
|
1073
|
+
root = Path(args.root).resolve()
|
|
1074
|
+
config_path = Path(args.config)
|
|
1075
|
+
if not config_path.is_absolute():
|
|
1076
|
+
config_path = root / config_path
|
|
1077
|
+
if not config_path.exists():
|
|
1078
|
+
message = f"runtime config missing: {config_path.relative_to(root)}"
|
|
1079
|
+
if args.json:
|
|
1080
|
+
print(
|
|
1081
|
+
json.dumps(
|
|
1082
|
+
{
|
|
1083
|
+
"status": "missing",
|
|
1084
|
+
"findings": [{"severity": "warning", "message": message}],
|
|
1085
|
+
},
|
|
1086
|
+
indent=2,
|
|
1087
|
+
sort_keys=True,
|
|
1088
|
+
)
|
|
1089
|
+
)
|
|
1090
|
+
else:
|
|
1091
|
+
print(message)
|
|
1092
|
+
return 1 if strict_mode(args.strict) else 0
|
|
1093
|
+
|
|
1094
|
+
snapshot = build_runtime_status(
|
|
1095
|
+
config_path,
|
|
1096
|
+
record_id=f"R{len(read_jsonl(root / '.agent/runtime-snapshots.jsonl')) + 1}",
|
|
1097
|
+
allow_probe=args.probe,
|
|
1098
|
+
)
|
|
1099
|
+
if args.record:
|
|
1100
|
+
append_jsonl(root / ".agent/runtime-snapshots.jsonl", snapshot)
|
|
1101
|
+
if args.json:
|
|
1102
|
+
print(json.dumps(snapshot, indent=2, sort_keys=True))
|
|
1103
|
+
else:
|
|
1104
|
+
for runtime in snapshot["runtimes"]:
|
|
1105
|
+
print(f"{runtime['id']} {runtime['status']}")
|
|
1106
|
+
for server in snapshot.get("mcp_servers", []):
|
|
1107
|
+
print(f"mcp {server['id']} {server['status']}")
|
|
1108
|
+
for finding in snapshot.get("findings", []):
|
|
1109
|
+
print(f"{finding['severity']}: {finding['message']}")
|
|
1110
|
+
has_error = any(item.get("severity") == "error" for item in snapshot.get("findings", []))
|
|
1111
|
+
has_warning = any(item.get("severity") == "warning" for item in snapshot.get("findings", []))
|
|
1112
|
+
return 1 if has_error or (strict_mode(args.strict) and has_warning) else 0
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def _markdown_list(items: List[Any], empty: str = "None.") -> str:
|
|
1116
|
+
if not items:
|
|
1117
|
+
return empty
|
|
1118
|
+
return "\n".join(f"- {item}" for item in items)
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def command_build_proof(args: argparse.Namespace) -> int:
|
|
1122
|
+
root = Path(args.root).resolve()
|
|
1123
|
+
plan_path = Path(args.plan)
|
|
1124
|
+
if not plan_path.is_absolute():
|
|
1125
|
+
plan_path = root / plan_path
|
|
1126
|
+
output = Path(args.output)
|
|
1127
|
+
if not output.is_absolute():
|
|
1128
|
+
output = root / output
|
|
1129
|
+
|
|
1130
|
+
try:
|
|
1131
|
+
plan = _load_plan(plan_path)
|
|
1132
|
+
except (FileNotFoundError, json.JSONDecodeError) as exc:
|
|
1133
|
+
print(f"invalid plan: {exc}", file=sys.stderr)
|
|
1134
|
+
return 1
|
|
1135
|
+
traceability_errors = validate_requirement_traceability(plan)
|
|
1136
|
+
if traceability_errors:
|
|
1137
|
+
print("invalid requirement traceability", file=sys.stderr)
|
|
1138
|
+
for error in traceability_errors:
|
|
1139
|
+
print(f"- {error}", file=sys.stderr)
|
|
1140
|
+
return 1
|
|
1141
|
+
design_errors = validate_design_decision_traceability(plan)
|
|
1142
|
+
if design_errors:
|
|
1143
|
+
print("invalid design decision traceability", file=sys.stderr)
|
|
1144
|
+
for error in design_errors:
|
|
1145
|
+
print(f"- {error}", file=sys.stderr)
|
|
1146
|
+
return 1
|
|
1147
|
+
|
|
1148
|
+
try:
|
|
1149
|
+
proof = build_proof(root, plan_path, strict_mode(args.strict))
|
|
1150
|
+
except InvalidWorkingState as exc:
|
|
1151
|
+
# #28: nothing is written, so a rejected build leaves any existing proof
|
|
1152
|
+
# in place rather than replacing it with one built from invalid input.
|
|
1153
|
+
print(f"invalid working state: {exc.subject}:", file=sys.stderr)
|
|
1154
|
+
for error in exc.errors:
|
|
1155
|
+
print(f"- {error}", file=sys.stderr)
|
|
1156
|
+
return 1
|
|
1157
|
+
except ValueError as exc:
|
|
1158
|
+
print(f"invalid ledger: {exc}", file=sys.stderr)
|
|
1159
|
+
return 1
|
|
1160
|
+
write_proof_metadata(root, proof)
|
|
1161
|
+
drift_path = root / ".agent/drift-report.json"
|
|
1162
|
+
drift = read_json(drift_path) if drift_path.exists() else {"status": "missing", "notes": []}
|
|
1163
|
+
try:
|
|
1164
|
+
evidence = read_jsonl(root / ".agent/evidence.jsonl")
|
|
1165
|
+
except ValueError as exc:
|
|
1166
|
+
print(f"invalid ledger: {exc}", file=sys.stderr)
|
|
1167
|
+
return 1
|
|
1168
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
1169
|
+
output.write_text(render_markdown(plan, proof, evidence, drift), encoding="utf-8")
|
|
1170
|
+
print(f"created {output.relative_to(root)}")
|
|
1171
|
+
print("created .agent/proof-pack.json")
|
|
1172
|
+
|
|
1173
|
+
has_failed = any(check.get("status") == "failed" for check in proof["checks"])
|
|
1174
|
+
has_warning = any(check.get("status") == "warning" for check in proof["checks"])
|
|
1175
|
+
return 1 if has_failed or (strict_mode(args.strict) and has_warning) else 0
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
def command_verify_proof(args: argparse.Namespace) -> int:
|
|
1179
|
+
root = Path(args.root).resolve()
|
|
1180
|
+
proof_path = Path(args.proof)
|
|
1181
|
+
if not proof_path.is_absolute():
|
|
1182
|
+
proof_path = root / proof_path
|
|
1183
|
+
if not proof_path.exists():
|
|
1184
|
+
print(f"proof metadata missing: {proof_path.relative_to(root)}")
|
|
1185
|
+
return 1
|
|
1186
|
+
findings = verify_proof(root, proof_path, replay=args.replay, strict=strict_mode(args.strict))
|
|
1187
|
+
for finding in findings:
|
|
1188
|
+
print(f"{finding['severity']}: {finding['message']}")
|
|
1189
|
+
if any(finding.get("severity") == "error" for finding in findings):
|
|
1190
|
+
return 1
|
|
1191
|
+
print("proof verified")
|
|
1192
|
+
return 0
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
def command_view_proof(args: argparse.Namespace) -> int:
|
|
1196
|
+
if not args.html:
|
|
1197
|
+
print("only --html output is supported; pass --html", file=sys.stderr)
|
|
1198
|
+
return 2
|
|
1199
|
+
root = Path(args.root).resolve()
|
|
1200
|
+
proof_path = Path(args.proof)
|
|
1201
|
+
if not proof_path.is_absolute():
|
|
1202
|
+
proof_path = root / proof_path
|
|
1203
|
+
output = Path(args.output)
|
|
1204
|
+
if not output.is_absolute():
|
|
1205
|
+
output = root / output
|
|
1206
|
+
try:
|
|
1207
|
+
model = collect_view_model(root, proof_path, output)
|
|
1208
|
+
except ValueError as exc:
|
|
1209
|
+
print(str(exc), file=sys.stderr)
|
|
1210
|
+
return 1
|
|
1211
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
1212
|
+
output.write_text(render_html(model), encoding="utf-8")
|
|
1213
|
+
print(f"created {output.relative_to(root) if output.is_relative_to(root) else output}")
|
|
1214
|
+
return 0
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
def _event_detail(event: Dict[str, Any]) -> str:
|
|
1218
|
+
data = event["data"]
|
|
1219
|
+
event_type = event["type"]
|
|
1220
|
+
if event_type == "command.recorded":
|
|
1221
|
+
command = data.get("command")
|
|
1222
|
+
rendered = " ".join(command) if isinstance(command, list) else ""
|
|
1223
|
+
return f"exit={data.get('exit_code')} {rendered}".strip()
|
|
1224
|
+
if event_type == "file.changed":
|
|
1225
|
+
return f"{data.get('change_kind')} {data.get('path')}".strip()
|
|
1226
|
+
if event_type == "verification.run":
|
|
1227
|
+
return f"{data.get('scope')} {data.get('status')}".strip()
|
|
1228
|
+
return str(data.get("reason") or data.get("agent_id") or "")
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def command_events(args: argparse.Namespace) -> int:
|
|
1232
|
+
root = Path(args.root).resolve()
|
|
1233
|
+
events = project_events(root)
|
|
1234
|
+
if args.since is not None:
|
|
1235
|
+
if not valid_since(args.since):
|
|
1236
|
+
print(f"invalid --since timestamp: {args.since}", file=sys.stderr)
|
|
1237
|
+
return 2
|
|
1238
|
+
events = filter_events_since(events, args.since)
|
|
1239
|
+
if args.jsonl:
|
|
1240
|
+
for event in events:
|
|
1241
|
+
print(json.dumps(event, sort_keys=True))
|
|
1242
|
+
elif args.json:
|
|
1243
|
+
print(json.dumps(events, indent=2, sort_keys=True))
|
|
1244
|
+
else:
|
|
1245
|
+
for event in events:
|
|
1246
|
+
step = event.get("step_id") or "-"
|
|
1247
|
+
attempt = event.get("attempt_id") or "-"
|
|
1248
|
+
line = f"{event['timestamp']} {event['type']} {step}/{attempt} {_event_detail(event)}"
|
|
1249
|
+
print(line.rstrip())
|
|
1250
|
+
return 0
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
def command_next_action(args: argparse.Namespace) -> int:
|
|
1254
|
+
root = Path(args.root).resolve()
|
|
1255
|
+
action = porcelain.next_action(
|
|
1256
|
+
root,
|
|
1257
|
+
strict_mode(args.strict),
|
|
1258
|
+
agent_id=args.agent,
|
|
1259
|
+
)
|
|
1260
|
+
if args.json:
|
|
1261
|
+
print(json.dumps(action.to_dict(), indent=2, sort_keys=True))
|
|
1262
|
+
else:
|
|
1263
|
+
print(f"next-action {action.state}: {action.reason}")
|
|
1264
|
+
if action.command:
|
|
1265
|
+
print(action.command)
|
|
1266
|
+
for line in action.diagnostics:
|
|
1267
|
+
print(f" {line}")
|
|
1268
|
+
return 0
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
def command_finish_step(args: argparse.Namespace) -> int:
|
|
1272
|
+
root = Path(args.root).resolve()
|
|
1273
|
+
plan = _load_root_plan(root)
|
|
1274
|
+
try:
|
|
1275
|
+
result = porcelain.finish_step(
|
|
1276
|
+
root, plan, args.step, args.attempt,
|
|
1277
|
+
strict_mode(args.strict), args.replay,
|
|
1278
|
+
agent_id=args.agent,
|
|
1279
|
+
)
|
|
1280
|
+
except ValueError as exc:
|
|
1281
|
+
print(str(exc), file=sys.stderr)
|
|
1282
|
+
return 2
|
|
1283
|
+
if args.json:
|
|
1284
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
1285
|
+
else:
|
|
1286
|
+
print(f"finish-step {result['verification_status']} "
|
|
1287
|
+
f"verified={result['verified']} completed={result['completed']}")
|
|
1288
|
+
for line in result["diagnostics"]:
|
|
1289
|
+
print(f" {line}")
|
|
1290
|
+
return 0 if result["completed"] else 1
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
def command_finish_run(args: argparse.Namespace) -> int:
|
|
1294
|
+
root = Path(args.root).resolve()
|
|
1295
|
+
plan_path = Path(args.plan)
|
|
1296
|
+
if not plan_path.is_absolute():
|
|
1297
|
+
plan_path = root / plan_path
|
|
1298
|
+
# finish_run captures each delegated gate's output internally, so stdout
|
|
1299
|
+
# stays clean for --json and the failing gate's detail is surfaced in
|
|
1300
|
+
# result["diagnostics"] rather than swallowed.
|
|
1301
|
+
result = porcelain.finish_run(root, plan_path, strict_mode(args.strict))
|
|
1302
|
+
if args.json:
|
|
1303
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
1304
|
+
else:
|
|
1305
|
+
for gate in result["gates"]:
|
|
1306
|
+
print(f"{gate['name']} {gate['status']}")
|
|
1307
|
+
if not result["ok"]:
|
|
1308
|
+
print(f"stopped at {result['stopped_at']}")
|
|
1309
|
+
for line in result["diagnostics"]:
|
|
1310
|
+
print(f" {line}")
|
|
1311
|
+
return 0 if result["ok"] else 1
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
def command_status(args: argparse.Namespace) -> int:
|
|
1315
|
+
root = Path(args.root).resolve()
|
|
1316
|
+
plan_path = root / ".agent/plan.lock.json"
|
|
1317
|
+
drift_path = root / ".agent/drift-report.json"
|
|
1318
|
+
evidence_path = root / ".agent/evidence.jsonl"
|
|
1319
|
+
failures_path = root / ".agent/failures.jsonl"
|
|
1320
|
+
runtime_config_path = root / ".agent/runtime.config.json"
|
|
1321
|
+
runtime_snapshots_path = root / ".agent/runtime-snapshots.jsonl"
|
|
1322
|
+
execution_contract_path = root / ".agent/execution.contract.json"
|
|
1323
|
+
step_runs_path = root / ".agent/step-runs.jsonl"
|
|
1324
|
+
command_receipts_path = root / ".agent/command-receipts.jsonl"
|
|
1325
|
+
file_receipts_path = root / ".agent/file-receipts.jsonl"
|
|
1326
|
+
|
|
1327
|
+
print(f"agentflow {__version__}")
|
|
1328
|
+
print(f"root {root}")
|
|
1329
|
+
if not plan_path.exists():
|
|
1330
|
+
print("status uninitialized")
|
|
1331
|
+
print("missing .agent/plan.lock.json")
|
|
1332
|
+
return 1
|
|
1333
|
+
|
|
1334
|
+
try:
|
|
1335
|
+
plan = read_json(plan_path)
|
|
1336
|
+
except ValueError as exc:
|
|
1337
|
+
# Covers malformed JSON (JSONDecodeError is a ValueError) and
|
|
1338
|
+
# schema_version policy rejections: status must report, never traceback.
|
|
1339
|
+
print(f"status invalid: {exc}")
|
|
1340
|
+
return 1
|
|
1341
|
+
|
|
1342
|
+
problems = False
|
|
1343
|
+
|
|
1344
|
+
def count(path: Path) -> str:
|
|
1345
|
+
nonlocal problems
|
|
1346
|
+
if not path.exists():
|
|
1347
|
+
return "0"
|
|
1348
|
+
try:
|
|
1349
|
+
return str(len(read_jsonl(path)))
|
|
1350
|
+
except ValueError as exc:
|
|
1351
|
+
problems = True
|
|
1352
|
+
return f"unreadable: {exc}"
|
|
1353
|
+
|
|
1354
|
+
print(f"objective {plan.get('objective') or 'TBD'}")
|
|
1355
|
+
print(f"locked {bool(plan.get('locked'))}")
|
|
1356
|
+
print(f"steps {len(plan.get('steps', []))}")
|
|
1357
|
+
print(f"evidence {count(evidence_path)}")
|
|
1358
|
+
print(f"failures {count(failures_path)}")
|
|
1359
|
+
if drift_path.exists():
|
|
1360
|
+
try:
|
|
1361
|
+
drift = read_json(drift_path)
|
|
1362
|
+
print(f"drift {drift.get('status', 'unknown')}")
|
|
1363
|
+
except ValueError as exc:
|
|
1364
|
+
problems = True
|
|
1365
|
+
print(f"drift unreadable: {exc}")
|
|
1366
|
+
else:
|
|
1367
|
+
print("drift missing")
|
|
1368
|
+
print(f"runtime config {'present' if runtime_config_path.exists() else 'missing'}")
|
|
1369
|
+
print(f"runtime snapshots {count(runtime_snapshots_path)}")
|
|
1370
|
+
print(f"execution contract {'present' if execution_contract_path.exists() else 'missing'}")
|
|
1371
|
+
print(f"step runs {count(step_runs_path)}")
|
|
1372
|
+
print(f"command receipts {count(command_receipts_path)}")
|
|
1373
|
+
print(f"file receipts {count(file_receipts_path)}")
|
|
1374
|
+
return 1 if problems else 0
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
def _load_brief_from_path(path: Path) -> Any:
|
|
1378
|
+
# Return the parsed JSON as-is; validate_brief is the single source of truth
|
|
1379
|
+
# for the "must be an object" rule, so a non-dict body surfaces as a
|
|
1380
|
+
# validation_error rather than being mislabeled a JSON-parse failure.
|
|
1381
|
+
if not path.exists():
|
|
1382
|
+
raise FileNotFoundError(f"brief file not found: {path}")
|
|
1383
|
+
try:
|
|
1384
|
+
return read_json(path)
|
|
1385
|
+
except json.JSONDecodeError as exc:
|
|
1386
|
+
raise ValueError(_json_decode_message(str(path), exc)) from exc
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def _load_brief_from_stdin() -> Any:
|
|
1390
|
+
try:
|
|
1391
|
+
return json.loads(sys.stdin.read())
|
|
1392
|
+
except json.JSONDecodeError as exc:
|
|
1393
|
+
raise ValueError(_json_decode_message("stdin", exc)) from exc
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
def _recommend_fail(code: str, message: str, *, json_mode: bool, exit_code: int) -> int:
|
|
1397
|
+
if json_mode:
|
|
1398
|
+
_print_plan_result("invalid", errors=[_plan_diagnostic(code, message)])
|
|
1399
|
+
else:
|
|
1400
|
+
print(message if exit_code == 2 else f"invalid brief: {message}", file=sys.stderr)
|
|
1401
|
+
return exit_code
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
def command_recommend_workflow(args: argparse.Namespace) -> int:
|
|
1405
|
+
if args.brief and args.stdin:
|
|
1406
|
+
return _recommend_fail(
|
|
1407
|
+
"invalid_arguments", "--brief and --stdin are mutually exclusive",
|
|
1408
|
+
json_mode=args.json, exit_code=2,
|
|
1409
|
+
)
|
|
1410
|
+
if not args.brief and not args.stdin:
|
|
1411
|
+
return _recommend_fail(
|
|
1412
|
+
"invalid_arguments", "one of --brief or --stdin is required",
|
|
1413
|
+
json_mode=args.json, exit_code=2,
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
try:
|
|
1417
|
+
brief = (
|
|
1418
|
+
_load_brief_from_stdin()
|
|
1419
|
+
if args.stdin
|
|
1420
|
+
else _load_brief_from_path(Path(args.brief).resolve())
|
|
1421
|
+
)
|
|
1422
|
+
except FileNotFoundError as exc:
|
|
1423
|
+
return _recommend_fail("input_not_found", str(exc), json_mode=args.json, exit_code=1)
|
|
1424
|
+
except ValueError as exc:
|
|
1425
|
+
code = "invalid_json" if isinstance(exc.__cause__, json.JSONDecodeError) else "invalid_brief_json"
|
|
1426
|
+
return _recommend_fail(code, str(exc), json_mode=args.json, exit_code=1)
|
|
1427
|
+
|
|
1428
|
+
errors = validate_brief(brief)
|
|
1429
|
+
if errors:
|
|
1430
|
+
if args.json:
|
|
1431
|
+
_print_plan_result(
|
|
1432
|
+
"invalid",
|
|
1433
|
+
errors=[_plan_diagnostic("validation_error", error) for error in errors],
|
|
1434
|
+
)
|
|
1435
|
+
else:
|
|
1436
|
+
print("brief invalid")
|
|
1437
|
+
for error in errors:
|
|
1438
|
+
print(f"- {error}")
|
|
1439
|
+
return 1
|
|
1440
|
+
|
|
1441
|
+
try:
|
|
1442
|
+
report = recommend_workflow(
|
|
1443
|
+
brief, selected_profile=args.selected_profile, reason=args.reason
|
|
1444
|
+
)
|
|
1445
|
+
except RecommendError as exc:
|
|
1446
|
+
return _recommend_fail(exc.code, str(exc), json_mode=args.json, exit_code=2)
|
|
1447
|
+
|
|
1448
|
+
if args.json:
|
|
1449
|
+
print(json.dumps(report, indent=2, sort_keys=True))
|
|
1450
|
+
else:
|
|
1451
|
+
print_lines(render_recommendation(report))
|
|
1452
|
+
return 0
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
def _draft_fail(code: str, message: str, *, json_mode: bool, exit_code: int) -> int:
|
|
1456
|
+
if json_mode:
|
|
1457
|
+
_print_plan_result("invalid", errors=[_plan_diagnostic(code, message)])
|
|
1458
|
+
else:
|
|
1459
|
+
print(message, file=sys.stderr)
|
|
1460
|
+
return exit_code
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
def _existing_plan_is_locked(path: Path) -> bool:
|
|
1464
|
+
# A malformed/unreadable existing plan reports "not locked" and falls through
|
|
1465
|
+
# to the normal overwrite guard rather than masking the real problem.
|
|
1466
|
+
try:
|
|
1467
|
+
plan = read_json(path)
|
|
1468
|
+
return isinstance(plan, dict) and plan.get("locked") is True
|
|
1469
|
+
except (ValueError, OSError):
|
|
1470
|
+
return False
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def _print_draft_warnings(warnings: List[Dict[str, str]]) -> None:
|
|
1474
|
+
# Non-fatal diagnostics (e.g. #89 greenfield candidate_file_missing) go to
|
|
1475
|
+
# stderr so machine consumers reading stdout JSON stay unaffected.
|
|
1476
|
+
for warning in warnings:
|
|
1477
|
+
print(f"warning [{warning['code']}]: {warning['message']}", file=sys.stderr)
|
|
1478
|
+
|
|
1479
|
+
|
|
1480
|
+
def _render_draft_summary(payload: Dict[str, Any]) -> List[str]:
|
|
1481
|
+
recommended = payload["recommended"]
|
|
1482
|
+
selected = payload["selected"]
|
|
1483
|
+
plan = payload["plan_candidate"]
|
|
1484
|
+
return [
|
|
1485
|
+
f"drafted plan candidate from {selected['pack']}/{selected['profile']}",
|
|
1486
|
+
f"recommended {recommended['pack']}/{recommended['profile']}; "
|
|
1487
|
+
f"selected {selected['pack']}/{selected['profile']} ({payload['selection_mode']})",
|
|
1488
|
+
f"steps: {len(plan['steps'])}",
|
|
1489
|
+
f"validation gates: {', '.join(plan['validation_gates'])}",
|
|
1490
|
+
"(not written; pass --write to materialize "
|
|
1491
|
+
".agent/plan.lock.json and .agent/workflow.contract.json)",
|
|
1492
|
+
]
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
def command_draft_plan(args: argparse.Namespace) -> int:
|
|
1496
|
+
if args.brief and args.stdin:
|
|
1497
|
+
return _draft_fail(
|
|
1498
|
+
"invalid_arguments", "--brief and --stdin are mutually exclusive",
|
|
1499
|
+
json_mode=args.json, exit_code=2,
|
|
1500
|
+
)
|
|
1501
|
+
if not args.brief and not args.stdin:
|
|
1502
|
+
return _draft_fail(
|
|
1503
|
+
"invalid_arguments", "one of --brief or --stdin is required",
|
|
1504
|
+
json_mode=args.json, exit_code=2,
|
|
1505
|
+
)
|
|
1506
|
+
|
|
1507
|
+
root = Path(args.root).resolve()
|
|
1508
|
+
|
|
1509
|
+
try:
|
|
1510
|
+
brief = (
|
|
1511
|
+
_load_brief_from_stdin()
|
|
1512
|
+
if args.stdin
|
|
1513
|
+
else _load_brief_from_path(Path(args.brief).resolve())
|
|
1514
|
+
)
|
|
1515
|
+
except FileNotFoundError as exc:
|
|
1516
|
+
return _draft_fail("input_not_found", str(exc), json_mode=args.json, exit_code=1)
|
|
1517
|
+
except ValueError as exc:
|
|
1518
|
+
code = "invalid_json" if isinstance(exc.__cause__, json.JSONDecodeError) else "invalid_brief_json"
|
|
1519
|
+
return _draft_fail(code, str(exc), json_mode=args.json, exit_code=1)
|
|
1520
|
+
|
|
1521
|
+
try:
|
|
1522
|
+
pack = load_pack(Path(args.workflow))
|
|
1523
|
+
except PackError as exc:
|
|
1524
|
+
return _draft_fail("invalid_pack", str(exc), json_mode=args.json, exit_code=1)
|
|
1525
|
+
|
|
1526
|
+
try:
|
|
1527
|
+
result = compile_draft_plan(
|
|
1528
|
+
brief,
|
|
1529
|
+
pack.manifest,
|
|
1530
|
+
objective=args.objective or "",
|
|
1531
|
+
root=root,
|
|
1532
|
+
profile_id=args.profile,
|
|
1533
|
+
reason=args.reason,
|
|
1534
|
+
allow_missing_candidates=args.allow_missing_candidates,
|
|
1535
|
+
)
|
|
1536
|
+
except DraftPlanError as exc:
|
|
1537
|
+
return _draft_fail(exc.code, str(exc), json_mode=args.json, exit_code=1)
|
|
1538
|
+
|
|
1539
|
+
plan = result["plan"]
|
|
1540
|
+
profile = result["profile"]
|
|
1541
|
+
report = result["report"]
|
|
1542
|
+
mode = result["selection_mode"]
|
|
1543
|
+
warnings = result["warnings"]
|
|
1544
|
+
reason_text = selection_reason(report, profile, mode, args.reason)
|
|
1545
|
+
contract = profile_to_contract(pack.manifest, profile["id"], "draft-plan", reason_text)
|
|
1546
|
+
contract["validation_policy"]["required_gates"] = list(plan["validation_gates"])
|
|
1547
|
+
# Validate the contract before any write so a malformed custom pack fails
|
|
1548
|
+
# closed with a diagnostic instead of half-materializing .agent/.
|
|
1549
|
+
contract_errors = validate_workflow_contract(contract)
|
|
1550
|
+
if contract_errors:
|
|
1551
|
+
return _draft_fail(
|
|
1552
|
+
"contract_invalid", "; ".join(contract_errors), json_mode=args.json, exit_code=1
|
|
1553
|
+
)
|
|
1554
|
+
recommended = report["recommended"]
|
|
1555
|
+
selected = {"pack": pack.manifest["id"], "profile": profile["id"]}
|
|
1556
|
+
|
|
1557
|
+
if args.write:
|
|
1558
|
+
plan_path = root / ".agent/plan.lock.json"
|
|
1559
|
+
contract_path = root / WORKFLOW_CONTRACT_PATH
|
|
1560
|
+
# A locked plan is sacred: lock-plan is the only authority for it, so
|
|
1561
|
+
# --force overrides an unlocked draft but never a locked plan.
|
|
1562
|
+
if plan_path.exists() and _existing_plan_is_locked(plan_path):
|
|
1563
|
+
return _draft_fail(
|
|
1564
|
+
"locked_plan_exists",
|
|
1565
|
+
"refusing to overwrite a locked .agent/plan.lock.json; "
|
|
1566
|
+
"unlock or remove it first",
|
|
1567
|
+
json_mode=args.json, exit_code=1,
|
|
1568
|
+
)
|
|
1569
|
+
if (plan_path.exists() or contract_path.exists()) and not args.force:
|
|
1570
|
+
return _draft_fail(
|
|
1571
|
+
"artifact_exists",
|
|
1572
|
+
"refusing to overwrite existing .agent/plan.lock.json or "
|
|
1573
|
+
".agent/workflow.contract.json (use --force)",
|
|
1574
|
+
json_mode=args.json, exit_code=1,
|
|
1575
|
+
)
|
|
1576
|
+
write_json(plan_path, plan)
|
|
1577
|
+
write_workflow_contract(root, contract)
|
|
1578
|
+
if args.json:
|
|
1579
|
+
print(json.dumps({
|
|
1580
|
+
"schema_version": DRAFT_PLAN_SCHEMA_VERSION,
|
|
1581
|
+
"status": "drafted",
|
|
1582
|
+
"path": str(plan_path),
|
|
1583
|
+
"contract_path": str(contract_path),
|
|
1584
|
+
"recommended": recommended,
|
|
1585
|
+
"selected": selected,
|
|
1586
|
+
"selection_mode": mode,
|
|
1587
|
+
"warnings": warnings,
|
|
1588
|
+
"errors": [],
|
|
1589
|
+
}, indent=2, sort_keys=True))
|
|
1590
|
+
else:
|
|
1591
|
+
print(f"drafted .agent/plan.lock.json from {selected['pack']}/{selected['profile']}")
|
|
1592
|
+
print(f"wrote {WORKFLOW_CONTRACT_PATH}")
|
|
1593
|
+
_print_draft_warnings(warnings)
|
|
1594
|
+
return 0
|
|
1595
|
+
|
|
1596
|
+
payload = {
|
|
1597
|
+
"schema_version": DRAFT_PLAN_SCHEMA_VERSION,
|
|
1598
|
+
"status": "draft",
|
|
1599
|
+
"plan_candidate": plan,
|
|
1600
|
+
"workflow_contract": contract,
|
|
1601
|
+
"recommended": recommended,
|
|
1602
|
+
"selected": selected,
|
|
1603
|
+
"selection_mode": mode,
|
|
1604
|
+
"warnings": warnings,
|
|
1605
|
+
}
|
|
1606
|
+
if args.json:
|
|
1607
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
1608
|
+
else:
|
|
1609
|
+
print_lines(_render_draft_summary(payload))
|
|
1610
|
+
_print_draft_warnings(warnings)
|
|
1611
|
+
return 0
|
|
1612
|
+
|
|
1613
|
+
|
|
1614
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
1615
|
+
parser = argparse.ArgumentParser(prog="agentflow")
|
|
1616
|
+
parser.add_argument("--version", action="version", version=f"agentflow {__version__}")
|
|
1617
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
1618
|
+
|
|
1619
|
+
init = subparsers.add_parser("init", help="create .agent scaffold")
|
|
1620
|
+
init.add_argument("--root", default=".")
|
|
1621
|
+
init.add_argument("--force", action="store_true")
|
|
1622
|
+
init.add_argument("--pack", help="initialize from a workflow pack manifest path")
|
|
1623
|
+
init.add_argument("--profile", help="pack profile id (required with --pack)")
|
|
1624
|
+
init.add_argument(
|
|
1625
|
+
"--reason", help="selection reason recorded in the workflow contract"
|
|
1626
|
+
)
|
|
1627
|
+
init.set_defaults(func=command_init)
|
|
1628
|
+
|
|
1629
|
+
init_execution = subparsers.add_parser("init-execution", help="create execution contract and ledgers")
|
|
1630
|
+
init_execution.add_argument("--root", default=".")
|
|
1631
|
+
init_execution.add_argument("--force", action="store_true")
|
|
1632
|
+
init_execution.set_defaults(func=command_init_execution)
|
|
1633
|
+
|
|
1634
|
+
pack = subparsers.add_parser("pack", help="inspect workflow packs")
|
|
1635
|
+
pack_sub = pack.add_subparsers(dest="pack_command", required=True)
|
|
1636
|
+
pack_inspect = pack_sub.add_parser(
|
|
1637
|
+
"inspect", help="validate and summarize a workflow pack"
|
|
1638
|
+
)
|
|
1639
|
+
pack_inspect.add_argument("path")
|
|
1640
|
+
pack_inspect.add_argument(
|
|
1641
|
+
"--json", action="store_true", help="emit a JSON summary including manifest_sha256"
|
|
1642
|
+
)
|
|
1643
|
+
pack_inspect.set_defaults(func=command_pack_inspect)
|
|
1644
|
+
|
|
1645
|
+
doctor_parser = subparsers.add_parser("doctor", help="check shell runtime readiness")
|
|
1646
|
+
doctor_parser.add_argument("--root", default=".")
|
|
1647
|
+
doctor_parser.add_argument("--json", action="store_true")
|
|
1648
|
+
doctor_parser.set_defaults(func=command_doctor)
|
|
1649
|
+
|
|
1650
|
+
intake = subparsers.add_parser("intake", help="create task intake from a prompt file")
|
|
1651
|
+
intake.add_argument("--root", default=".")
|
|
1652
|
+
intake.add_argument("--from", dest="from_file", required=True)
|
|
1653
|
+
intake.set_defaults(func=command_intake)
|
|
1654
|
+
|
|
1655
|
+
validate = subparsers.add_parser("validate-plan", help="validate a plan lock")
|
|
1656
|
+
validate.add_argument("plan", nargs="?", default=".agent/plan.lock.json")
|
|
1657
|
+
validate.set_defaults(func=command_validate_plan)
|
|
1658
|
+
|
|
1659
|
+
lock = subparsers.add_parser("lock-plan", help="validate and lock a plan")
|
|
1660
|
+
lock.add_argument("plan", nargs="?", default=".agent/plan.lock.json")
|
|
1661
|
+
lock.add_argument("--stdin", action="store_true", help="read plan JSON from standard input")
|
|
1662
|
+
lock.add_argument("--from-json", dest="from_json", help="read plan JSON from this file")
|
|
1663
|
+
lock.add_argument("--json", action="store_true", help="emit machine-readable diagnostics")
|
|
1664
|
+
lock.set_defaults(func=command_lock_plan)
|
|
1665
|
+
|
|
1666
|
+
recommend_parser = subparsers.add_parser(
|
|
1667
|
+
"recommend-workflow", help="recommend a workflow posture from a task brief"
|
|
1668
|
+
)
|
|
1669
|
+
recommend_parser.add_argument("--brief", help="read the task brief JSON from this file")
|
|
1670
|
+
recommend_parser.add_argument(
|
|
1671
|
+
"--stdin", action="store_true", help="read the task brief JSON from standard input"
|
|
1672
|
+
)
|
|
1673
|
+
recommend_parser.add_argument(
|
|
1674
|
+
"--json", action="store_true", help="emit the recommendation as JSON"
|
|
1675
|
+
)
|
|
1676
|
+
recommend_parser.add_argument(
|
|
1677
|
+
"--selected-profile", dest="selected_profile",
|
|
1678
|
+
help="operator-selected profile id (override)",
|
|
1679
|
+
)
|
|
1680
|
+
recommend_parser.add_argument(
|
|
1681
|
+
"--reason",
|
|
1682
|
+
help="rationale required when --selected-profile differs from the recommendation",
|
|
1683
|
+
)
|
|
1684
|
+
recommend_parser.set_defaults(func=command_recommend_workflow)
|
|
1685
|
+
|
|
1686
|
+
draft = subparsers.add_parser(
|
|
1687
|
+
"draft-plan",
|
|
1688
|
+
help="compile a task brief and workflow pack into an unlocked draft plan",
|
|
1689
|
+
)
|
|
1690
|
+
draft.add_argument("--root", default=".")
|
|
1691
|
+
draft.add_argument("--brief", help="read the task brief JSON from this file")
|
|
1692
|
+
draft.add_argument(
|
|
1693
|
+
"--stdin", action="store_true", help="read the task brief JSON from standard input"
|
|
1694
|
+
)
|
|
1695
|
+
draft.add_argument(
|
|
1696
|
+
"--workflow", required=True,
|
|
1697
|
+
help="path to the workflow pack (a .agentflow-pack dir, its parent, or pack.json)",
|
|
1698
|
+
)
|
|
1699
|
+
draft.add_argument(
|
|
1700
|
+
"--objective", help="the plan objective: the human intent the brief does not carry",
|
|
1701
|
+
)
|
|
1702
|
+
draft.add_argument("--profile", help="explicitly select a pack profile (override)")
|
|
1703
|
+
draft.add_argument(
|
|
1704
|
+
"--reason", help="rationale required when --profile is weaker than the recommendation",
|
|
1705
|
+
)
|
|
1706
|
+
draft.add_argument(
|
|
1707
|
+
"--allow-missing-candidates", "--greenfield",
|
|
1708
|
+
dest="allow_missing_candidates", action="store_true",
|
|
1709
|
+
help="downgrade candidate_file_missing to a warning so a plan can be "
|
|
1710
|
+
"drafted for files that do not exist yet (greenfield planning)",
|
|
1711
|
+
)
|
|
1712
|
+
draft.add_argument(
|
|
1713
|
+
"--write", action="store_true",
|
|
1714
|
+
help="write .agent/plan.lock.json (unlocked) and .agent/workflow.contract.json",
|
|
1715
|
+
)
|
|
1716
|
+
draft.add_argument(
|
|
1717
|
+
"--force", action="store_true", help="overwrite existing plan/contract when writing",
|
|
1718
|
+
)
|
|
1719
|
+
draft.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
1720
|
+
draft.set_defaults(func=command_draft_plan)
|
|
1721
|
+
|
|
1722
|
+
workflow_contract = subparsers.add_parser(
|
|
1723
|
+
"workflow-contract",
|
|
1724
|
+
help="write or validate workflow contract artifact",
|
|
1725
|
+
)
|
|
1726
|
+
workflow_contract.add_argument("path", nargs="?", default=WORKFLOW_CONTRACT_PATH)
|
|
1727
|
+
workflow_contract.add_argument("--root", default=".")
|
|
1728
|
+
workflow_contract_mode = workflow_contract.add_mutually_exclusive_group()
|
|
1729
|
+
workflow_contract_mode.add_argument(
|
|
1730
|
+
"--from-json",
|
|
1731
|
+
dest="from_json",
|
|
1732
|
+
help=f"read a contract JSON object and write {WORKFLOW_CONTRACT_PATH}",
|
|
1733
|
+
)
|
|
1734
|
+
workflow_contract_mode.add_argument(
|
|
1735
|
+
"--validate",
|
|
1736
|
+
action="store_true",
|
|
1737
|
+
help="validate an existing workflow contract artifact (default)",
|
|
1738
|
+
)
|
|
1739
|
+
workflow_contract.set_defaults(func=command_workflow_contract)
|
|
1740
|
+
|
|
1741
|
+
amend = subparsers.add_parser("amend-plan", help="record a plan amendment")
|
|
1742
|
+
amend.add_argument("--root", default=".")
|
|
1743
|
+
amend.add_argument("--id", required=True)
|
|
1744
|
+
amend.add_argument("--reason", required=True)
|
|
1745
|
+
amend.add_argument("--evidence", action="append")
|
|
1746
|
+
amend.add_argument("--changed-scope", action="append")
|
|
1747
|
+
amend.add_argument("--changed-steps", action="append")
|
|
1748
|
+
amend.add_argument("--changed-risk")
|
|
1749
|
+
amend.add_argument("--requires-approval", action="store_true")
|
|
1750
|
+
amend.set_defaults(func=command_amend_plan)
|
|
1751
|
+
|
|
1752
|
+
evidence = subparsers.add_parser("record-evidence", help="append an evidence ledger entry")
|
|
1753
|
+
evidence.add_argument("--root", default=".")
|
|
1754
|
+
evidence.add_argument("--id", required=True)
|
|
1755
|
+
evidence.add_argument("--claim", required=True)
|
|
1756
|
+
evidence.add_argument("--source", required=True)
|
|
1757
|
+
evidence.add_argument("--confidence", choices=["low", "medium", "high"], default="medium")
|
|
1758
|
+
evidence.add_argument("--kind", choices=list(EVIDENCE_KINDS))
|
|
1759
|
+
evidence.add_argument("--supports", action="append")
|
|
1760
|
+
evidence.set_defaults(func=command_record_evidence)
|
|
1761
|
+
|
|
1762
|
+
review = subparsers.add_parser("record-review", help="record a review run from its manifest")
|
|
1763
|
+
review.add_argument("--root", default=".")
|
|
1764
|
+
review.add_argument("--manifest", dest="manifest", required=True)
|
|
1765
|
+
review.add_argument("--emit-evidence", action="store_true")
|
|
1766
|
+
review.add_argument("--json", action="store_true")
|
|
1767
|
+
review.set_defaults(func=command_record_review)
|
|
1768
|
+
|
|
1769
|
+
record_capability = subparsers.add_parser(
|
|
1770
|
+
"record-capability", help="record a used specialized capability"
|
|
1771
|
+
)
|
|
1772
|
+
record_capability.add_argument("--root", default=".")
|
|
1773
|
+
record_capability.add_argument("--id", required=True)
|
|
1774
|
+
record_capability.add_argument("--capability", required=True)
|
|
1775
|
+
record_capability.add_argument("--provider", required=True)
|
|
1776
|
+
record_capability.add_argument("--reason", required=True)
|
|
1777
|
+
record_capability.add_argument("--evidence", action="append")
|
|
1778
|
+
record_capability.set_defaults(func=command_record_capability)
|
|
1779
|
+
|
|
1780
|
+
waive_capability = subparsers.add_parser(
|
|
1781
|
+
"waive-capability", help="record a knowingly waived specialized capability"
|
|
1782
|
+
)
|
|
1783
|
+
waive_capability.add_argument("--root", default=".")
|
|
1784
|
+
waive_capability.add_argument("--id", required=True)
|
|
1785
|
+
waive_capability.add_argument("--capability", required=True)
|
|
1786
|
+
waive_capability.add_argument("--reason", required=True)
|
|
1787
|
+
waive_capability.add_argument("--evidence", action="append")
|
|
1788
|
+
waive_capability.set_defaults(func=command_waive_capability)
|
|
1789
|
+
|
|
1790
|
+
review_manifest = subparsers.add_parser(
|
|
1791
|
+
"review-manifest",
|
|
1792
|
+
help="produce review-manifest.json from a findings-final.json sidecar",
|
|
1793
|
+
)
|
|
1794
|
+
review_manifest.add_argument("--root", default=".")
|
|
1795
|
+
review_manifest.add_argument("--state-dir", dest="state_dir", required=True)
|
|
1796
|
+
review_manifest.add_argument("--branch", default=None)
|
|
1797
|
+
review_manifest.add_argument("--config", default="docs/ai/config.json")
|
|
1798
|
+
review_manifest.add_argument(
|
|
1799
|
+
"--findings-json",
|
|
1800
|
+
dest="findings_json",
|
|
1801
|
+
default=None,
|
|
1802
|
+
help="path to findings sidecar; relative to --state-dir unless absolute "
|
|
1803
|
+
"(default: findings-final.json)",
|
|
1804
|
+
)
|
|
1805
|
+
review_manifest.add_argument(
|
|
1806
|
+
"--depth-profile",
|
|
1807
|
+
dest="depth_profile",
|
|
1808
|
+
default="deep",
|
|
1809
|
+
choices=WORKFLOW_REVIEW_DEPTHS,
|
|
1810
|
+
help="review depth this recorded run satisfies; lighter depths require "
|
|
1811
|
+
"fewer artifacts (default: deep = full four-pass)",
|
|
1812
|
+
)
|
|
1813
|
+
review_manifest.add_argument("--write", action="store_true")
|
|
1814
|
+
review_manifest.add_argument("--json", action="store_true")
|
|
1815
|
+
review_manifest.add_argument("--fail-on-block", dest="fail_on_block", action="store_true")
|
|
1816
|
+
review_manifest.add_argument("--strict-exit", dest="strict_exit", action="store_true")
|
|
1817
|
+
review_manifest.set_defaults(func=command_review_manifest)
|
|
1818
|
+
|
|
1819
|
+
context = subparsers.add_parser("record-context", help="append a context receipt")
|
|
1820
|
+
context.add_argument("--root", default=".")
|
|
1821
|
+
context.add_argument("--id", required=True)
|
|
1822
|
+
context.add_argument("--source", required=True)
|
|
1823
|
+
context.add_argument("--reason", required=True)
|
|
1824
|
+
context.add_argument("--used-for", action="append", dest="used_for")
|
|
1825
|
+
context.add_argument("--bytes", type=int)
|
|
1826
|
+
context.set_defaults(func=command_record_context)
|
|
1827
|
+
|
|
1828
|
+
failure = subparsers.add_parser("record-failure", help="append a failure signature")
|
|
1829
|
+
failure.add_argument("--root", default=".")
|
|
1830
|
+
failure.add_argument("--command", required=True)
|
|
1831
|
+
failure.add_argument("--failure-type", required=True)
|
|
1832
|
+
failure.add_argument("--relevant-lines", action="append")
|
|
1833
|
+
failure.add_argument("--suspected-cause", required=True)
|
|
1834
|
+
failure.add_argument("--next-action", required=True)
|
|
1835
|
+
failure.set_defaults(func=command_record_failure)
|
|
1836
|
+
|
|
1837
|
+
audit = subparsers.add_parser("audit-drift", help="compare git changes to plan scope")
|
|
1838
|
+
audit.add_argument("--root", default=".")
|
|
1839
|
+
audit.add_argument("--plan", default=".agent/plan.lock.json")
|
|
1840
|
+
audit.set_defaults(func=command_audit_drift)
|
|
1841
|
+
|
|
1842
|
+
next_step_parser = subparsers.add_parser("next-step", help="show the next eligible plan step")
|
|
1843
|
+
next_step_parser.add_argument("--root", default=".")
|
|
1844
|
+
next_step_parser.add_argument("--json", action="store_true")
|
|
1845
|
+
next_step_parser.set_defaults(func=command_next_step)
|
|
1846
|
+
|
|
1847
|
+
claim = subparsers.add_parser("claim-step", help="claim a plan step")
|
|
1848
|
+
claim.add_argument("step")
|
|
1849
|
+
claim.add_argument("--root", default=".")
|
|
1850
|
+
claim.add_argument("--agent", required=True)
|
|
1851
|
+
claim.add_argument("--lease-minutes", type=int)
|
|
1852
|
+
claim.add_argument("--json", action="store_true")
|
|
1853
|
+
claim.set_defaults(func=command_claim_step)
|
|
1854
|
+
|
|
1855
|
+
amend = subparsers.add_parser(
|
|
1856
|
+
"amend-step", help="open an auditable amendment attempt on a completed step"
|
|
1857
|
+
)
|
|
1858
|
+
amend.add_argument("step")
|
|
1859
|
+
amend.add_argument("--root", default=".")
|
|
1860
|
+
amend.add_argument("--agent", required=True)
|
|
1861
|
+
amend.add_argument("--reason", required=True)
|
|
1862
|
+
amend.add_argument("--reason-code", choices=sorted(AMENDMENT_REASON_CODES))
|
|
1863
|
+
amend.add_argument("--finding", action="append", help="review-run-scoped finding ref RR-...#ID (repeatable)")
|
|
1864
|
+
amend.add_argument("--lease-minutes", type=int, help="lease TTL for the amendment attempt under enforce")
|
|
1865
|
+
amend.add_argument("--json", action="store_true")
|
|
1866
|
+
amend.set_defaults(func=command_amend_step)
|
|
1867
|
+
|
|
1868
|
+
complete = subparsers.add_parser("complete-step", help="complete a verified step")
|
|
1869
|
+
complete.add_argument("step")
|
|
1870
|
+
complete.add_argument("--root", default=".")
|
|
1871
|
+
complete.add_argument("--attempt")
|
|
1872
|
+
complete.add_argument("--agent", default=os.environ.get("AGENTFLOW_AGENT_ID"))
|
|
1873
|
+
complete.add_argument("--json", action="store_true")
|
|
1874
|
+
complete.set_defaults(func=command_complete_step)
|
|
1875
|
+
|
|
1876
|
+
block = subparsers.add_parser("block-step", help="mark an attempt blocked")
|
|
1877
|
+
block.add_argument("step")
|
|
1878
|
+
block.add_argument("--root", default=".")
|
|
1879
|
+
block.add_argument("--attempt")
|
|
1880
|
+
block.add_argument("--reason", required=True)
|
|
1881
|
+
block.add_argument("--agent", default=os.environ.get("AGENTFLOW_AGENT_ID"))
|
|
1882
|
+
block.add_argument("--json", action="store_true")
|
|
1883
|
+
block.set_defaults(func=command_block_step)
|
|
1884
|
+
|
|
1885
|
+
fail = subparsers.add_parser("fail-step", help="mark an attempt failed")
|
|
1886
|
+
fail.add_argument("step")
|
|
1887
|
+
fail.add_argument("--root", default=".")
|
|
1888
|
+
fail.add_argument("--attempt")
|
|
1889
|
+
fail.add_argument("--reason", required=True)
|
|
1890
|
+
fail.add_argument("--agent", default=os.environ.get("AGENTFLOW_AGENT_ID"),
|
|
1891
|
+
help="record who forced the failure (break-glass; no owner check)")
|
|
1892
|
+
fail.add_argument("--json", action="store_true")
|
|
1893
|
+
fail.set_defaults(func=command_fail_step)
|
|
1894
|
+
|
|
1895
|
+
reclaim = subparsers.add_parser(
|
|
1896
|
+
"reclaim-step", help="abandon an expired attempt and open a fresh claim"
|
|
1897
|
+
)
|
|
1898
|
+
reclaim.add_argument("step")
|
|
1899
|
+
reclaim.add_argument("--root", default=".")
|
|
1900
|
+
reclaim.add_argument("--agent", default=os.environ.get("AGENTFLOW_AGENT_ID"), required=False)
|
|
1901
|
+
reclaim.add_argument("--reason", required=True)
|
|
1902
|
+
reclaim.add_argument("--lease-minutes", type=int)
|
|
1903
|
+
reclaim.add_argument("--json", action="store_true")
|
|
1904
|
+
reclaim.set_defaults(func=command_reclaim_step)
|
|
1905
|
+
|
|
1906
|
+
renew = subparsers.add_parser(
|
|
1907
|
+
"renew-lease", help="extend an attempt's lease (owner self-recovery)"
|
|
1908
|
+
)
|
|
1909
|
+
renew.add_argument("step")
|
|
1910
|
+
renew.add_argument("--root", default=".")
|
|
1911
|
+
renew.add_argument("--attempt")
|
|
1912
|
+
renew.add_argument("--agent", default=os.environ.get("AGENTFLOW_AGENT_ID"))
|
|
1913
|
+
renew.add_argument("--minutes", type=int)
|
|
1914
|
+
renew.add_argument("--json", action="store_true")
|
|
1915
|
+
renew.set_defaults(func=command_renew_lease)
|
|
1916
|
+
|
|
1917
|
+
run = subparsers.add_parser("run", help="run a command and record an observed receipt")
|
|
1918
|
+
run.add_argument("--root", default=".")
|
|
1919
|
+
run.add_argument("--step", required=True)
|
|
1920
|
+
run.add_argument("--attempt")
|
|
1921
|
+
run.add_argument("--gate")
|
|
1922
|
+
run.add_argument("--agent", default=os.environ.get("AGENTFLOW_AGENT_ID"))
|
|
1923
|
+
run.add_argument("--confirm-risk", action="store_true")
|
|
1924
|
+
run.add_argument("--json", action="store_true")
|
|
1925
|
+
run.add_argument("command", nargs=argparse.REMAINDER)
|
|
1926
|
+
run.set_defaults(func=command_run)
|
|
1927
|
+
|
|
1928
|
+
record_cmd = subparsers.add_parser("record-command", help="record an externally run command")
|
|
1929
|
+
record_cmd.add_argument("--root", default=".")
|
|
1930
|
+
record_cmd.add_argument("--step", required=True)
|
|
1931
|
+
record_cmd.add_argument("--attempt")
|
|
1932
|
+
record_cmd.add_argument("--exit-code", type=int, required=True)
|
|
1933
|
+
record_cmd.add_argument("--gate")
|
|
1934
|
+
record_cmd.add_argument("--agent", default=os.environ.get("AGENTFLOW_AGENT_ID"))
|
|
1935
|
+
record_cmd.add_argument("--json", action="store_true")
|
|
1936
|
+
record_cmd.add_argument("command", nargs=argparse.REMAINDER)
|
|
1937
|
+
record_cmd.set_defaults(func=command_record_command)
|
|
1938
|
+
|
|
1939
|
+
record_file = subparsers.add_parser("record-file-change", help="record changed files for a step")
|
|
1940
|
+
record_file.add_argument("--root", default=".")
|
|
1941
|
+
record_file.add_argument("--step", required=True)
|
|
1942
|
+
record_file.add_argument("--attempt")
|
|
1943
|
+
record_file.add_argument("--path", action="append", required=True)
|
|
1944
|
+
record_file.add_argument("--agent", default=os.environ.get("AGENTFLOW_AGENT_ID"))
|
|
1945
|
+
record_file.add_argument("--json", action="store_true")
|
|
1946
|
+
record_file.set_defaults(func=command_record_file_change)
|
|
1947
|
+
|
|
1948
|
+
verify_step_parser = subparsers.add_parser("verify-step", help="verify one step attempt")
|
|
1949
|
+
verify_step_parser.add_argument("step")
|
|
1950
|
+
verify_step_parser.add_argument("--root", default=".")
|
|
1951
|
+
verify_step_parser.add_argument("--attempt")
|
|
1952
|
+
verify_step_parser.add_argument("--agent", default=os.environ.get("AGENTFLOW_AGENT_ID"))
|
|
1953
|
+
verify_step_parser.add_argument("--strict", action="store_true")
|
|
1954
|
+
verify_step_parser.add_argument("--replay", action="store_true")
|
|
1955
|
+
verify_step_parser.add_argument("--json", action="store_true")
|
|
1956
|
+
verify_step_parser.set_defaults(func=command_verify_step)
|
|
1957
|
+
|
|
1958
|
+
verify_run_parser = subparsers.add_parser("verify-run", help="verify execution run coverage")
|
|
1959
|
+
verify_run_parser.add_argument("--root", default=".")
|
|
1960
|
+
verify_run_parser.add_argument("--plan", default=".agent/plan.lock.json")
|
|
1961
|
+
verify_run_parser.add_argument("--strict", action="store_true")
|
|
1962
|
+
verify_run_parser.add_argument(
|
|
1963
|
+
"--no-record",
|
|
1964
|
+
action="store_false",
|
|
1965
|
+
dest="record",
|
|
1966
|
+
help="verify without appending to .agent/verification-runs.jsonl",
|
|
1967
|
+
)
|
|
1968
|
+
verify_run_parser.add_argument("--json", action="store_true")
|
|
1969
|
+
verify_run_parser.set_defaults(record=True)
|
|
1970
|
+
verify_run_parser.set_defaults(func=command_verify_run)
|
|
1971
|
+
|
|
1972
|
+
aggregate_parser = subparsers.add_parser(
|
|
1973
|
+
"aggregate-ledgers",
|
|
1974
|
+
help="merge N worktree ledgers into one canonical .agent/ (pass --dry-run to preview collisions without writing)",
|
|
1975
|
+
)
|
|
1976
|
+
aggregate_parser.add_argument("--input", action="append")
|
|
1977
|
+
aggregate_parser.add_argument("--source-id", action="append", dest="source_id")
|
|
1978
|
+
aggregate_parser.add_argument("--label", action="append")
|
|
1979
|
+
aggregate_parser.add_argument("--output", default=".")
|
|
1980
|
+
aggregate_parser.add_argument("--base")
|
|
1981
|
+
aggregate_parser.add_argument("--dry-run", action="store_true", dest="dry_run")
|
|
1982
|
+
aggregate_parser.add_argument("--json", action="store_true")
|
|
1983
|
+
aggregate_parser.set_defaults(func=command_aggregate_ledgers)
|
|
1984
|
+
|
|
1985
|
+
detect_stuck_parser = subparsers.add_parser(
|
|
1986
|
+
"detect-stuck",
|
|
1987
|
+
help="detect stuck agent loops from receipts and verification history",
|
|
1988
|
+
)
|
|
1989
|
+
detect_stuck_parser.add_argument("--root", default=".")
|
|
1990
|
+
detect_stuck_parser.add_argument("--plan", default=".agent/plan.lock.json")
|
|
1991
|
+
detect_stuck_parser.add_argument("--strict", action="store_true")
|
|
1992
|
+
detect_stuck_parser.add_argument("--json", action="store_true")
|
|
1993
|
+
detect_stuck_parser.add_argument("--min-command-failures", type=_positive_int, default=3)
|
|
1994
|
+
detect_stuck_parser.add_argument("--min-verify-failures", type=_positive_int, default=2)
|
|
1995
|
+
detect_stuck_parser.add_argument("--min-cycle-repeats", type=_positive_int, default=3)
|
|
1996
|
+
detect_stuck_parser.set_defaults(func=command_detect_stuck)
|
|
1997
|
+
|
|
1998
|
+
export_handoff_parser = subparsers.add_parser("export-handoff", help="write a provider-neutral work packet")
|
|
1999
|
+
export_handoff_parser.add_argument("--root", default=".")
|
|
2000
|
+
export_handoff_parser.add_argument("--step", required=True)
|
|
2001
|
+
export_handoff_parser.add_argument("--format", choices=["json", "markdown"], default="json")
|
|
2002
|
+
export_handoff_parser.add_argument("--output")
|
|
2003
|
+
export_handoff_parser.set_defaults(func=command_export_handoff)
|
|
2004
|
+
|
|
2005
|
+
lint_handoff_parser = subparsers.add_parser("lint-handoff", help="lint a handoff for provider-specific language")
|
|
2006
|
+
lint_handoff_parser.add_argument("--root", default=".")
|
|
2007
|
+
lint_handoff_parser.add_argument("--input", required=True)
|
|
2008
|
+
lint_handoff_parser.add_argument("--json", action="store_true")
|
|
2009
|
+
lint_handoff_parser.set_defaults(func=command_lint_handoff)
|
|
2010
|
+
|
|
2011
|
+
replay = subparsers.add_parser("replay-gates", help="replay attested validation gates")
|
|
2012
|
+
replay.add_argument("--root", default=".")
|
|
2013
|
+
replay.add_argument("--step")
|
|
2014
|
+
replay.add_argument("--record", action="store_true")
|
|
2015
|
+
replay.add_argument("--json", action="store_true")
|
|
2016
|
+
replay.set_defaults(func=command_replay_gates)
|
|
2017
|
+
|
|
2018
|
+
runtime = subparsers.add_parser("runtime-status", help="summarize configured runtime readiness")
|
|
2019
|
+
runtime.add_argument("--root", default=".")
|
|
2020
|
+
runtime.add_argument("--config", default=".agent/runtime.config.json")
|
|
2021
|
+
runtime.add_argument("--record", action="store_true")
|
|
2022
|
+
runtime.add_argument("--json", action="store_true")
|
|
2023
|
+
runtime.add_argument("--probe", action="store_true")
|
|
2024
|
+
runtime.add_argument("--strict", action="store_true")
|
|
2025
|
+
runtime.set_defaults(func=command_runtime_status)
|
|
2026
|
+
|
|
2027
|
+
proof = subparsers.add_parser("build-proof", help="generate proof pack markdown")
|
|
2028
|
+
proof.add_argument("--root", default=".")
|
|
2029
|
+
proof.add_argument("--plan", default=".agent/plan.lock.json")
|
|
2030
|
+
proof.add_argument("--output", default=".agent/proof-pack.md")
|
|
2031
|
+
proof.add_argument("--strict", action="store_true")
|
|
2032
|
+
proof.set_defaults(func=command_build_proof)
|
|
2033
|
+
|
|
2034
|
+
verify = subparsers.add_parser("verify-proof", help="verify proof-pack source hashes")
|
|
2035
|
+
verify.add_argument("--root", default=".")
|
|
2036
|
+
verify.add_argument("--proof", default=".agent/proof-pack.json")
|
|
2037
|
+
verify.add_argument("--replay", action="store_true")
|
|
2038
|
+
verify.add_argument("--strict", action="store_true")
|
|
2039
|
+
verify.set_defaults(func=command_verify_proof)
|
|
2040
|
+
|
|
2041
|
+
view = subparsers.add_parser(
|
|
2042
|
+
"view-proof",
|
|
2043
|
+
help="render a static HTML proof report (review aid; verify-proof is authoritative)",
|
|
2044
|
+
)
|
|
2045
|
+
view.add_argument("--html", action="store_true")
|
|
2046
|
+
view.add_argument("--root", default=".")
|
|
2047
|
+
view.add_argument("--proof", default=".agent/proof-pack.json")
|
|
2048
|
+
view.add_argument("--output", default=".agent/proof-report.html")
|
|
2049
|
+
view.set_defaults(func=command_view_proof)
|
|
2050
|
+
|
|
2051
|
+
events_parser = subparsers.add_parser(
|
|
2052
|
+
"events", help="project a chronological event stream over .agent ledgers"
|
|
2053
|
+
)
|
|
2054
|
+
events_parser.add_argument("--root", default=".")
|
|
2055
|
+
events_parser.add_argument("--since")
|
|
2056
|
+
events_format = events_parser.add_mutually_exclusive_group()
|
|
2057
|
+
events_format.add_argument("--jsonl", action="store_true")
|
|
2058
|
+
events_format.add_argument("--json", action="store_true")
|
|
2059
|
+
events_parser.set_defaults(func=command_events)
|
|
2060
|
+
|
|
2061
|
+
next_action_parser = subparsers.add_parser(
|
|
2062
|
+
"next-action", help="report the next required Agentflow action")
|
|
2063
|
+
next_action_parser.add_argument("--root", default=".")
|
|
2064
|
+
next_action_parser.add_argument(
|
|
2065
|
+
"--agent",
|
|
2066
|
+
default=os.environ.get("AGENTFLOW_AGENT_ID"),
|
|
2067
|
+
)
|
|
2068
|
+
next_action_parser.add_argument("--json", action="store_true")
|
|
2069
|
+
next_action_parser.add_argument("--strict", action="store_true")
|
|
2070
|
+
next_action_parser.set_defaults(func=command_next_action)
|
|
2071
|
+
|
|
2072
|
+
finish_step_parser = subparsers.add_parser(
|
|
2073
|
+
"finish-step", help="verify then complete a step")
|
|
2074
|
+
finish_step_parser.add_argument("step")
|
|
2075
|
+
finish_step_parser.add_argument("--attempt", default=None)
|
|
2076
|
+
finish_step_parser.add_argument("--root", default=".")
|
|
2077
|
+
finish_step_parser.add_argument("--agent", default=os.environ.get("AGENTFLOW_AGENT_ID"))
|
|
2078
|
+
finish_step_parser.add_argument("--json", action="store_true")
|
|
2079
|
+
finish_step_parser.add_argument("--strict", action="store_true")
|
|
2080
|
+
finish_step_parser.add_argument("--replay", action="store_true")
|
|
2081
|
+
finish_step_parser.set_defaults(func=command_finish_step)
|
|
2082
|
+
|
|
2083
|
+
finish_run_parser = subparsers.add_parser(
|
|
2084
|
+
"finish-run", help="run audit/verify/proof gates in order")
|
|
2085
|
+
finish_run_parser.add_argument("--root", default=".")
|
|
2086
|
+
finish_run_parser.add_argument("--plan", default=".agent/plan.lock.json")
|
|
2087
|
+
finish_run_parser.add_argument("--json", action="store_true")
|
|
2088
|
+
finish_run_parser.add_argument("--strict", action="store_true")
|
|
2089
|
+
finish_run_parser.set_defaults(func=command_finish_run)
|
|
2090
|
+
|
|
2091
|
+
status = subparsers.add_parser("status", help="summarize current workflow state")
|
|
2092
|
+
status.add_argument("--root", default=".")
|
|
2093
|
+
status.set_defaults(func=command_status)
|
|
2094
|
+
|
|
2095
|
+
return parser
|
|
2096
|
+
|
|
2097
|
+
|
|
2098
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
2099
|
+
parser = build_parser()
|
|
2100
|
+
args = parser.parse_args(argv)
|
|
2101
|
+
try:
|
|
2102
|
+
return args.func(args)
|
|
2103
|
+
except ValueError as exc:
|
|
2104
|
+
# Safety net for reader-gate errors (artifact schema_version policy
|
|
2105
|
+
# rejections raise ValueError): cross-version .agent state must surface
|
|
2106
|
+
# as a clean diagnostic, never a traceback.
|
|
2107
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
2108
|
+
return 1
|