@hallucination-studio/harness-engine 1.0.0 → 1.0.1

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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -27
  3. package/bin/install.js +29 -17
  4. package/package.json +10 -4
  5. package/skills/harness-engine/SKILL.md +97 -0
  6. package/skills/harness-engine/agents/openai.yaml +4 -0
  7. package/skills/harness-engine/evals/cases.json +94 -0
  8. package/skills/harness-engine/evals/harness_engine_evals/__init__.py +1 -0
  9. package/skills/harness-engine/evals/harness_engine_evals/cases_frontend.py +211 -0
  10. package/skills/harness-engine/evals/harness_engine_evals/cases_lifecycle.py +1616 -0
  11. package/skills/harness-engine/evals/harness_engine_evals/helpers.py +155 -0
  12. package/skills/harness-engine/evals/harness_engine_evals/registry.py +55 -0
  13. package/skills/harness-engine/evals/harness_engine_evals/report.py +36 -0
  14. package/skills/harness-engine/evals/harness_engine_evals/runner.py +53 -0
  15. package/skills/harness-engine/evals/run_evals.py +14 -0
  16. package/skills/{harness-repo-bootstrap → harness-engine}/references/evaluation-loop.md +8 -2
  17. package/skills/harness-engine/references/evidence-first-evals.md +187 -0
  18. package/skills/harness-engine/references/exec-plans.md +59 -0
  19. package/skills/{harness-repo-bootstrap → harness-engine}/references/file-map.md +3 -3
  20. package/skills/{harness-repo-bootstrap → harness-engine}/references/knowledge-capture.md +2 -2
  21. package/skills/{harness-repo-bootstrap → harness-engine}/references/sop-index.md +3 -0
  22. package/skills/harness-engine/references/template-policy.md +17 -0
  23. package/skills/harness-engine/references/workflow.md +62 -0
  24. package/skills/harness-engine/scripts/harness_engine/__init__.py +1 -0
  25. package/skills/harness-engine/scripts/harness_engine/analysis.py +240 -0
  26. package/skills/harness-engine/scripts/harness_engine/checks.py +287 -0
  27. package/skills/harness-engine/scripts/harness_engine/cli.py +656 -0
  28. package/skills/harness-engine/scripts/harness_engine/common.py +977 -0
  29. package/skills/harness-engine/scripts/harness_engine/continuation.py +520 -0
  30. package/skills/harness-engine/scripts/harness_engine/git_ops.py +88 -0
  31. package/skills/harness-engine/scripts/harness_engine/knowledge.py +329 -0
  32. package/skills/harness-engine/scripts/harness_engine/plans.py +630 -0
  33. package/skills/harness-engine/scripts/harness_engine/templates.py +124 -0
  34. package/skills/harness-engine/scripts/manage_harness.py +14 -0
  35. package/skills/harness-repo-bootstrap/SKILL.md +0 -68
  36. package/skills/harness-repo-bootstrap/agents/openai.yaml +0 -4
  37. package/skills/harness-repo-bootstrap/evals/cases.json +0 -18
  38. package/skills/harness-repo-bootstrap/evals/run_evals.py +0 -337
  39. package/skills/harness-repo-bootstrap/references/exec-plans.md +0 -39
  40. package/skills/harness-repo-bootstrap/references/template-policy.md +0 -12
  41. package/skills/harness-repo-bootstrap/references/workflow.md +0 -47
  42. package/skills/harness-repo-bootstrap/scripts/manage_harness.py +0 -1181
  43. /package/skills/{harness-repo-bootstrap → harness-engine}/assets/repo-template/.keep +0 -0
  44. /package/skills/{harness-repo-bootstrap → harness-engine}/assets/sops/.keep +0 -0
  45. /package/skills/{harness-repo-bootstrap → harness-engine}/references/question-catalog.md +0 -0
@@ -0,0 +1,656 @@
1
+ import argparse
2
+ import json
3
+ from pathlib import Path
4
+
5
+ from .analysis import analyze_repo
6
+ from .templates import make_default_answers, ensure_parent, write_scaffold
7
+ from .plans import (PlanCloseError, sidecar_path_for_plan, create_plan, plan_path_from_arg, set_acceptance_contract, close_plan, ensure_acceptance_ready, missing_quality_notes, weak_quality_notes, update_quality_gate)
8
+ from .knowledge import append_knowledge_item, append_defect_item, mark_defect_resolved, mark_single_knowledge_item_written
9
+ from .continuation import map_legacy_phase_mode, default_workstream_id_from_plan, continuation_command_issues, update_phase_continuity, append_workstream_entry, plan_goal_for_workstream, update_continuation_decision
10
+ from .checks import check_harness, evidence_prune_candidates
11
+ from .git_ops import CLEAN_INIT_DIRS, ensure_gitignore, clean_init_state, git_tracked_harness_runtime_files, git_untrack_files, git_add_paths
12
+
13
+ def load_json(path):
14
+ return json.loads(Path(path).read_text())
15
+
16
+
17
+ def write_json(path, payload):
18
+ output = json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
19
+ if path:
20
+ target = Path(path)
21
+ ensure_parent(target)
22
+ target.write_text(output)
23
+ else:
24
+ print(output, end="")
25
+
26
+
27
+ def read_text_arg(value=None, file_path=None, label="value"):
28
+ if value and file_path:
29
+ raise ValueError(f"Use either --{label} or --{label}-file, not both")
30
+ if file_path:
31
+ return Path(file_path).read_text().strip()
32
+ return value
33
+
34
+
35
+ def command_analyze(args):
36
+ repo = Path(args.repo).resolve()
37
+ analysis = analyze_repo(repo)
38
+ write_json(args.output, analysis)
39
+
40
+
41
+ def command_sample_answers(args):
42
+ analysis = load_json(args.analysis)
43
+ payload = make_default_answers(analysis)
44
+ write_json(args.output, payload)
45
+
46
+
47
+ def command_init(args):
48
+ repo = Path(args.repo).resolve()
49
+ analysis = analyze_repo(repo)
50
+ answers = load_json(args.answers)
51
+ has_harness = bool(analysis["existing_harness_files"] or analysis["existing_managed_files"])
52
+ effective_refresh = has_harness or args.force
53
+ written, skipped, created, refreshed = write_scaffold(
54
+ repo,
55
+ analysis,
56
+ answers,
57
+ refresh_managed=effective_refresh,
58
+ force=args.force,
59
+ )
60
+ result = {
61
+ "repo": str(repo),
62
+ "written": written,
63
+ "created": created,
64
+ "refreshed": refreshed,
65
+ "skipped": skipped,
66
+ "mode": "init",
67
+ "operation": "reconciled" if has_harness else "created",
68
+ "refresh_managed": effective_refresh,
69
+ "force": args.force,
70
+ }
71
+ write_json(args.output, result)
72
+
73
+
74
+ def command_plan_start(args):
75
+ repo = Path(args.repo).resolve()
76
+ plan_path = create_plan(repo, args.slug, args.goal)
77
+ result = {
78
+ "repo": str(repo),
79
+ "plan": str(plan_path),
80
+ "metadata": str(sidecar_path_for_plan(plan_path)),
81
+ "acceptance_contract": "draft",
82
+ "status": "created",
83
+ }
84
+ write_json(args.output, result)
85
+
86
+
87
+ def command_acceptance_set(args):
88
+ repo = Path(args.repo).resolve()
89
+ plan_path, _ = plan_path_from_arg(repo, args.plan)
90
+ criteria = {
91
+ "product_correctness": args.product,
92
+ "ux_operator_clarity": args.ux,
93
+ "architecture_maintainability": args.architecture,
94
+ "reliability_observability": args.reliability,
95
+ "security_data_handling": args.security,
96
+ }
97
+ result = set_acceptance_contract(plan_path, criteria)
98
+ result.update({"repo": str(repo), "plan": str(plan_path), "metadata": str(sidecar_path_for_plan(plan_path))})
99
+ write_json(args.output, result)
100
+ if result["status"] != "ready":
101
+ raise SystemExit(1)
102
+
103
+
104
+ def command_knowledge_log(args):
105
+ repo = Path(args.repo).resolve()
106
+ plan_path, _ = plan_path_from_arg(repo, args.plan)
107
+ fact = read_text_arg(args.fact, args.fact_file, "fact")
108
+ if not fact:
109
+ raise ValueError("Provide --fact or --fact-file")
110
+ item, item_id = append_knowledge_item(plan_path, fact, args.destination)
111
+ result = {"repo": str(repo), "plan": str(plan_path), "id": item_id, "logged": item}
112
+ write_json(args.output, result)
113
+
114
+
115
+ def command_defect_log(args):
116
+ repo = Path(args.repo).resolve()
117
+ plan_path, _ = plan_path_from_arg(repo, args.plan)
118
+ summary = read_text_arg(args.summary, args.summary_file, "summary")
119
+ evidence = read_text_arg(args.evidence, args.evidence_file, "evidence")
120
+ if not summary:
121
+ raise ValueError("Provide --summary or --summary-file")
122
+ item, item_id = append_defect_item(plan_path, args.severity, summary, evidence=evidence)
123
+ result = {"repo": str(repo), "plan": str(plan_path), "id": item_id, "logged": item, "status": "fail"}
124
+ write_json(args.output, result)
125
+ raise SystemExit(1)
126
+
127
+
128
+ def command_defect_resolve(args):
129
+ repo = Path(args.repo).resolve()
130
+ plan_path, _ = plan_path_from_arg(repo, args.plan)
131
+ fix_evidence = read_text_arg(args.fix_evidence, args.fix_evidence_file, "fix-evidence")
132
+ mark_defect_resolved(plan_path, args.id, fix_evidence)
133
+ result = {
134
+ "repo": str(repo),
135
+ "plan": str(plan_path),
136
+ "id": args.id,
137
+ "status": "resolved",
138
+ "fix_evidence": fix_evidence,
139
+ }
140
+ write_json(args.output, result)
141
+
142
+
143
+ def command_plan_close(args):
144
+ repo = Path(args.repo).resolve()
145
+ try:
146
+ destination, unresolved = close_plan(repo, args.plan, args.summary, args.force)
147
+ except PlanCloseError as error:
148
+ plan = None
149
+ try:
150
+ plan_path, _ = plan_path_from_arg(repo, args.plan)
151
+ plan = str(plan_path)
152
+ except Exception:
153
+ plan = args.plan
154
+ result = {
155
+ "repo": str(repo),
156
+ "plan": plan,
157
+ "status": "blocked",
158
+ "reason": error.reason,
159
+ "message": str(error),
160
+ "details": error.details,
161
+ }
162
+ write_json(args.output, result)
163
+ raise SystemExit(1)
164
+ result = {
165
+ "repo": str(repo),
166
+ "closed_plan": str(destination),
167
+ "unresolved_items_forced": unresolved,
168
+ "status": "closed",
169
+ }
170
+ write_json(args.output, result)
171
+
172
+
173
+ def score_arg(args, name):
174
+ value = getattr(args, name)
175
+ if value < 0 or value > 10:
176
+ raise ValueError(f"{name.replace('_', '-')} must be between 0 and 10")
177
+ return float(value)
178
+
179
+
180
+ def command_quality_score(args):
181
+ repo = Path(args.repo).resolve()
182
+ plan_path, _ = plan_path_from_arg(repo, args.plan)
183
+ try:
184
+ ensure_acceptance_ready(plan_path)
185
+ except RuntimeError as error:
186
+ result = {
187
+ "status": "fail",
188
+ "repo": str(repo),
189
+ "plan": str(plan_path),
190
+ "reason": "acceptance-contract-not-ready",
191
+ "message": str(error),
192
+ }
193
+ write_json(args.output, result)
194
+ raise SystemExit(1)
195
+ scores = {
196
+ "product_correctness": score_arg(args, "product_correctness"),
197
+ "ux_operator_clarity": score_arg(args, "ux_operator_clarity"),
198
+ "architecture_maintainability": score_arg(args, "architecture_maintainability"),
199
+ "reliability_observability": score_arg(args, "reliability_observability"),
200
+ "security_data_handling": score_arg(args, "security_data_handling"),
201
+ }
202
+ notes = {
203
+ "product_correctness": args.product_note,
204
+ "ux_operator_clarity": args.ux_note,
205
+ "architecture_maintainability": args.architecture_note,
206
+ "reliability_observability": args.reliability_note,
207
+ "security_data_handling": args.security_note,
208
+ }
209
+ missing_notes = missing_quality_notes(notes)
210
+ if missing_notes and not args.allow_empty_notes:
211
+ result = {
212
+ "status": "fail",
213
+ "repo": str(repo),
214
+ "plan": str(plan_path),
215
+ "reason": "missing-quality-notes",
216
+ "message": "quality-score requires evidence notes for every dimension.",
217
+ "missing_notes": missing_notes,
218
+ }
219
+ write_json(args.output, result)
220
+ raise SystemExit(1)
221
+ weak_notes = weak_quality_notes(notes)
222
+ if weak_notes and not args.allow_empty_notes:
223
+ result = {
224
+ "status": "fail",
225
+ "repo": str(repo),
226
+ "plan": str(plan_path),
227
+ "reason": "weak-quality-notes",
228
+ "message": "quality-score requires concrete verification evidence notes.",
229
+ "weak_notes": weak_notes,
230
+ }
231
+ write_json(args.output, result)
232
+ raise SystemExit(1)
233
+ result = update_quality_gate(plan_path, scores, notes, args.minimum)
234
+ result.update({"repo": str(repo), "plan": str(plan_path)})
235
+ write_json(args.output, result)
236
+ if result["status"] != "pass":
237
+ raise SystemExit(1)
238
+
239
+
240
+ def command_phase_set(args):
241
+ repo = Path(args.repo).resolve()
242
+ plan_path, relative_plan = plan_path_from_arg(repo, args.plan)
243
+ decision = map_legacy_phase_mode(args.mode)
244
+ resolved_workstream = args.workstream or (
245
+ default_workstream_id_from_plan(plan_path, plan_path.read_text()) if decision in {"continue", "pause"} else "none"
246
+ )
247
+ issues = continuation_command_issues(
248
+ repo,
249
+ relative_plan,
250
+ decision,
251
+ resolved_workstream,
252
+ args.continuation,
253
+ args.next_action,
254
+ args.closure_reason,
255
+ args.resume_notes,
256
+ )
257
+ if issues:
258
+ result = {
259
+ "status": "blocked",
260
+ "repo": str(repo),
261
+ "plan": str(plan_path),
262
+ "reason": "continuation-decision-incomplete",
263
+ "message": "Cannot update continuation decision until required fields are provided.",
264
+ "issues": issues,
265
+ "warning": "phase-set is deprecated; use continuation-set.",
266
+ }
267
+ write_json(args.output, result)
268
+ raise SystemExit(1)
269
+ result = update_phase_continuity(
270
+ plan_path,
271
+ args.mode,
272
+ resolved_workstream,
273
+ args.current_phase,
274
+ args.next_phase,
275
+ args.continuation,
276
+ args.next_action,
277
+ args.closure_reason,
278
+ args.resume_notes,
279
+ )
280
+ if result["decision"] in {"continue", "pause"}:
281
+ append_workstream_entry(
282
+ repo,
283
+ result["workstream"],
284
+ "active" if result["decision"] == "continue" else "paused",
285
+ relative_plan,
286
+ "none",
287
+ args.next_action,
288
+ plan_goal_for_workstream(plan_path, args.closure_reason),
289
+ args.resume_notes,
290
+ )
291
+ result.update(
292
+ {
293
+ "repo": str(repo),
294
+ "plan": str(plan_path),
295
+ "warning": "phase-set is deprecated; use continuation-set.",
296
+ }
297
+ )
298
+ write_json(args.output, result)
299
+
300
+
301
+ def command_continuation_set(args):
302
+ repo = Path(args.repo).resolve()
303
+ plan_path, relative_plan = plan_path_from_arg(repo, args.plan)
304
+ decision = args.decision.lower()
305
+ resolved_workstream = args.workstream or (
306
+ default_workstream_id_from_plan(plan_path, plan_path.read_text()) if decision in {"continue", "pause"} else "none"
307
+ )
308
+ issues = continuation_command_issues(
309
+ repo,
310
+ relative_plan,
311
+ decision,
312
+ resolved_workstream,
313
+ args.next_target,
314
+ args.next_action,
315
+ args.closure_reason,
316
+ args.resume_notes,
317
+ )
318
+ if issues:
319
+ result = {
320
+ "status": "blocked",
321
+ "repo": str(repo),
322
+ "plan": str(plan_path),
323
+ "reason": "continuation-decision-incomplete",
324
+ "message": "Cannot update continuation decision until required fields are provided.",
325
+ "issues": issues,
326
+ }
327
+ write_json(args.output, result)
328
+ raise SystemExit(1)
329
+ result = update_continuation_decision(
330
+ plan_path,
331
+ decision,
332
+ resolved_workstream,
333
+ args.next_target,
334
+ args.next_action,
335
+ args.closure_reason,
336
+ args.resume_notes,
337
+ )
338
+ if result["decision"] in {"continue", "pause"}:
339
+ append_workstream_entry(
340
+ repo,
341
+ result["workstream"],
342
+ "active" if result["decision"] == "continue" else "paused",
343
+ relative_plan,
344
+ "none",
345
+ result["next_action"],
346
+ plan_goal_for_workstream(plan_path, args.goal),
347
+ args.resume_notes,
348
+ )
349
+ result.update({"repo": str(repo), "plan": str(plan_path)})
350
+ write_json(args.output, result)
351
+
352
+
353
+ def command_workstream_upsert(args):
354
+ repo = Path(args.repo).resolve()
355
+ target = append_workstream_entry(
356
+ repo,
357
+ args.id,
358
+ args.status,
359
+ args.current_plan,
360
+ args.last_completed_plan,
361
+ args.next_action,
362
+ args.goal,
363
+ args.resume_notes,
364
+ )
365
+ result = {"repo": str(repo), "workstreams": str(target), "id": args.id, "status": "updated"}
366
+ write_json(args.output, result)
367
+
368
+
369
+ def command_knowledge_mark_written(args):
370
+ repo = Path(args.repo).resolve()
371
+ plan_path, _ = plan_path_from_arg(repo, args.plan)
372
+ fact = read_text_arg(args.fact, args.fact_file, "fact")
373
+ evidence = read_text_arg(args.evidence, args.evidence_file, "evidence")
374
+ mark_single_knowledge_item_written(
375
+ repo,
376
+ plan_path,
377
+ fact,
378
+ args.destination,
379
+ append=args.append,
380
+ knowledge_id=args.id,
381
+ evidence=evidence,
382
+ )
383
+ result = {
384
+ "repo": str(repo),
385
+ "plan": str(plan_path),
386
+ "marked_written": args.id or fact,
387
+ "destination": args.destination,
388
+ "evidence": evidence,
389
+ }
390
+ write_json(args.output, result)
391
+
392
+
393
+ def command_check(args):
394
+ repo = Path(args.repo).resolve()
395
+ result = check_harness(repo)
396
+ write_json(args.output, result)
397
+ if result["status"] != "pass":
398
+ raise SystemExit(1)
399
+
400
+
401
+ def command_evidence_prune(args):
402
+ repo = Path(args.repo).resolve()
403
+ candidates = evidence_prune_candidates(
404
+ repo,
405
+ root=args.root,
406
+ older_than_days=args.older_than_days,
407
+ )
408
+ removed = []
409
+ if args.apply:
410
+ for candidate in candidates:
411
+ path = repo / candidate["path"]
412
+ if path.exists() and path.is_file():
413
+ path.unlink()
414
+ removed.append(candidate["path"])
415
+ result = {
416
+ "repo": str(repo),
417
+ "root": args.root,
418
+ "older_than_days": args.older_than_days,
419
+ "mode": "apply" if args.apply else "dry-run",
420
+ "candidate_count": len(candidates),
421
+ "candidates": candidates,
422
+ "removed": removed,
423
+ }
424
+ write_json(args.output, result)
425
+
426
+
427
+ def command_clean(args):
428
+ repo = Path(args.repo).resolve()
429
+ candidates = git_tracked_harness_runtime_files(repo)
430
+ local_clean_candidates = []
431
+ for relative_dir in CLEAN_INIT_DIRS:
432
+ root = repo / relative_dir
433
+ if not root.exists():
434
+ continue
435
+ for path in sorted(root.rglob("*")):
436
+ if path.is_file() or path.is_symlink():
437
+ local_clean_candidates.append(str(path.relative_to(repo)))
438
+ gitignore = None
439
+ removed_from_index = []
440
+ cleaned = []
441
+ if args.apply:
442
+ gitignore = ensure_gitignore(repo)
443
+ cleaned = clean_init_state(repo)
444
+ removed_from_index = git_untrack_files(repo, candidates)
445
+ if gitignore["updated"]:
446
+ git_add_paths(repo, [gitignore["path"]])
447
+ result = {
448
+ "repo": str(repo),
449
+ "mode": "apply" if args.apply else "dry-run",
450
+ "tracked_candidate_count": len(candidates),
451
+ "tracked_candidates": candidates,
452
+ "local_candidate_count": len(local_clean_candidates),
453
+ "local_candidates": local_clean_candidates,
454
+ "gitignore": gitignore,
455
+ "removed_from_index": removed_from_index,
456
+ "cleaned": cleaned,
457
+ "next_steps": (
458
+ [
459
+ "Review staged changes with `git status --short` and `git diff --cached --stat`.",
460
+ "Commit and push to remove these files from the remote repository.",
461
+ ]
462
+ if args.apply
463
+ else ["Re-run with `--apply` to clean local harness runtime files, update .gitignore, and stage git index removals."]
464
+ ),
465
+ }
466
+ write_json(args.output, result)
467
+
468
+
469
+ def build_parser():
470
+ parser = argparse.ArgumentParser(description="Manage the harness repo scaffold.")
471
+ subparsers = parser.add_subparsers(dest="command", required=True)
472
+
473
+ analyze = subparsers.add_parser("analyze")
474
+ analyze.add_argument("--repo", required=True)
475
+ analyze.add_argument("--output")
476
+ analyze.set_defaults(func=command_analyze)
477
+
478
+ sample_answers = subparsers.add_parser("sample-answers")
479
+ sample_answers.add_argument("--analysis", required=True)
480
+ sample_answers.add_argument("--output")
481
+ sample_answers.set_defaults(func=command_sample_answers)
482
+
483
+ init = subparsers.add_parser("init")
484
+ init.add_argument("--repo", required=True)
485
+ init.add_argument("--answers", required=True)
486
+ init.add_argument("--output")
487
+ init.add_argument("--force", action="store_true")
488
+ init.set_defaults(func=command_init)
489
+
490
+ plan_start = subparsers.add_parser("plan-start")
491
+ plan_start.add_argument("--repo", required=True)
492
+ plan_start.add_argument("--slug", required=True)
493
+ plan_start.add_argument("--goal", required=True)
494
+ plan_start.add_argument("--output")
495
+ plan_start.set_defaults(func=command_plan_start)
496
+
497
+ acceptance_set = subparsers.add_parser("acceptance-set")
498
+ acceptance_set.add_argument("--repo", required=True)
499
+ acceptance_set.add_argument("--plan", required=True)
500
+ acceptance_set.add_argument("--product", required=True)
501
+ acceptance_set.add_argument("--ux", required=True)
502
+ acceptance_set.add_argument("--architecture", required=True)
503
+ acceptance_set.add_argument("--reliability", required=True)
504
+ acceptance_set.add_argument("--security", required=True)
505
+ acceptance_set.add_argument("--output")
506
+ acceptance_set.set_defaults(func=command_acceptance_set)
507
+
508
+ knowledge_log = subparsers.add_parser("knowledge-log")
509
+ knowledge_log.add_argument("--repo", required=True)
510
+ knowledge_log.add_argument("--plan", required=True)
511
+ knowledge_log.add_argument("--fact")
512
+ knowledge_log.add_argument("--fact-file")
513
+ knowledge_log.add_argument("--destination", required=True)
514
+ knowledge_log.add_argument("--output")
515
+ knowledge_log.set_defaults(func=command_knowledge_log)
516
+
517
+ defect_log = subparsers.add_parser("defect-log")
518
+ defect_log.add_argument("--repo", required=True)
519
+ defect_log.add_argument("--plan", required=True)
520
+ defect_log.add_argument("--severity", choices=["P0", "P1", "P2", "P3"], required=True)
521
+ defect_log.add_argument("--summary")
522
+ defect_log.add_argument("--summary-file")
523
+ defect_log.add_argument("--evidence")
524
+ defect_log.add_argument("--evidence-file")
525
+ defect_log.add_argument("--output")
526
+ defect_log.set_defaults(func=command_defect_log)
527
+
528
+ defect_resolve = subparsers.add_parser("defect-resolve")
529
+ defect_resolve.add_argument("--repo", required=True)
530
+ defect_resolve.add_argument("--plan", required=True)
531
+ defect_resolve.add_argument("--id", required=True)
532
+ defect_resolve.add_argument("--fix-evidence")
533
+ defect_resolve.add_argument("--fix-evidence-file")
534
+ defect_resolve.add_argument("--output")
535
+ defect_resolve.set_defaults(func=command_defect_resolve)
536
+
537
+ knowledge_mark_written = subparsers.add_parser("knowledge-mark-written")
538
+ knowledge_mark_written.add_argument("--repo", required=True)
539
+ knowledge_mark_written.add_argument("--plan", required=True)
540
+ knowledge_mark_written.add_argument("--id")
541
+ knowledge_mark_written.add_argument("--fact")
542
+ knowledge_mark_written.add_argument("--fact-file")
543
+ knowledge_mark_written.add_argument("--destination")
544
+ knowledge_mark_written.add_argument("--evidence")
545
+ knowledge_mark_written.add_argument("--evidence-file")
546
+ knowledge_mark_written.add_argument("--append", action="store_true")
547
+ knowledge_mark_written.add_argument("--output")
548
+ knowledge_mark_written.set_defaults(func=command_knowledge_mark_written)
549
+
550
+ plan_close = subparsers.add_parser("plan-close")
551
+ plan_close.add_argument("--repo", required=True)
552
+ plan_close.add_argument("--plan", required=True)
553
+ plan_close.add_argument("--summary", required=True)
554
+ plan_close.add_argument("--force", action="store_true")
555
+ plan_close.add_argument("--output")
556
+ plan_close.set_defaults(func=command_plan_close)
557
+
558
+ quality_score = subparsers.add_parser("quality-score")
559
+ quality_score.add_argument("--repo", required=True)
560
+ quality_score.add_argument("--plan", required=True)
561
+ quality_score.add_argument("--minimum", type=float, default=8.0)
562
+ quality_score.add_argument("--product-correctness", type=float, required=True)
563
+ quality_score.add_argument("--ux-operator-clarity", type=float, required=True)
564
+ quality_score.add_argument("--architecture-maintainability", type=float, required=True)
565
+ quality_score.add_argument("--reliability-observability", type=float, required=True)
566
+ quality_score.add_argument("--security-data-handling", type=float, required=True)
567
+ quality_score.add_argument("--product-note", default="")
568
+ quality_score.add_argument("--ux-note", default="")
569
+ quality_score.add_argument("--architecture-note", default="")
570
+ quality_score.add_argument("--reliability-note", default="")
571
+ quality_score.add_argument("--security-note", default="")
572
+ quality_score.add_argument("--allow-empty-notes", action="store_true")
573
+ quality_score.add_argument("--output")
574
+ quality_score.set_defaults(func=command_quality_score)
575
+
576
+ continuation_set = subparsers.add_parser("continuation-set")
577
+ continuation_set.add_argument("--repo", required=True)
578
+ continuation_set.add_argument("--plan", required=True)
579
+ continuation_set.add_argument(
580
+ "--decision",
581
+ choices=["complete", "continue", "pause", "stop", "defer"],
582
+ required=True,
583
+ )
584
+ continuation_set.add_argument("--workstream")
585
+ continuation_set.add_argument("--next-target", default="none")
586
+ continuation_set.add_argument("--next-action", default="none")
587
+ continuation_set.add_argument("--closure-reason", default="none")
588
+ continuation_set.add_argument("--resume-notes", default="none")
589
+ continuation_set.add_argument("--goal", default="")
590
+ continuation_set.add_argument("--output")
591
+ continuation_set.set_defaults(func=command_continuation_set)
592
+
593
+ phase_set = subparsers.add_parser("phase-set")
594
+ phase_set.add_argument("--repo", required=True)
595
+ phase_set.add_argument("--plan", required=True)
596
+ phase_set.add_argument(
597
+ "--mode",
598
+ choices=["single-phase", "multi-phase", "paused", "completed", "stopped"],
599
+ required=True,
600
+ )
601
+ phase_set.add_argument("--workstream")
602
+ phase_set.add_argument("--current-phase")
603
+ phase_set.add_argument("--next-phase", default="none")
604
+ phase_set.add_argument("--continuation", default="none")
605
+ phase_set.add_argument("--next-action", default="none")
606
+ phase_set.add_argument("--closure-reason", default="none")
607
+ phase_set.add_argument("--resume-notes", default="none")
608
+ phase_set.add_argument("--output")
609
+ phase_set.set_defaults(func=command_phase_set)
610
+
611
+ workstream_upsert = subparsers.add_parser("workstream-upsert")
612
+ workstream_upsert.add_argument("--repo", required=True)
613
+ workstream_upsert.add_argument("--id", required=True)
614
+ workstream_upsert.add_argument(
615
+ "--status",
616
+ choices=["active", "paused", "completed", "stopped"],
617
+ required=True,
618
+ )
619
+ workstream_upsert.add_argument("--current-plan", default="none")
620
+ workstream_upsert.add_argument("--last-completed-plan", default="none")
621
+ workstream_upsert.add_argument("--next-action", required=True)
622
+ workstream_upsert.add_argument("--goal", default="")
623
+ workstream_upsert.add_argument("--resume-notes", default="")
624
+ workstream_upsert.add_argument("--output")
625
+ workstream_upsert.set_defaults(func=command_workstream_upsert)
626
+
627
+ check = subparsers.add_parser("check")
628
+ check.add_argument("--repo", required=True)
629
+ check.add_argument("--output")
630
+ check.set_defaults(func=command_check)
631
+
632
+ evidence_prune = subparsers.add_parser("evidence-prune")
633
+ evidence_prune.add_argument("--repo", required=True)
634
+ evidence_prune.add_argument("--root", default="docs/generated")
635
+ evidence_prune.add_argument("--older-than-days", type=int, default=14)
636
+ evidence_prune.add_argument("--apply", action="store_true")
637
+ evidence_prune.add_argument("--output")
638
+ evidence_prune.set_defaults(func=command_evidence_prune)
639
+
640
+ clean = subparsers.add_parser("clean")
641
+ clean.add_argument("--repo", required=True)
642
+ clean.add_argument("--apply", action="store_true")
643
+ clean.add_argument("--output")
644
+ clean.set_defaults(func=command_clean)
645
+
646
+ return parser
647
+
648
+
649
+ def main():
650
+ parser = build_parser()
651
+ args = parser.parse_args()
652
+ args.func(args)
653
+
654
+
655
+ if __name__ == "__main__":
656
+ main()