logics-manager 2.1.2__tar.gz → 2.2.0__tar.gz

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 (27) hide show
  1. {logics_manager-2.1.2 → logics_manager-2.2.0}/PKG-INFO +1 -1
  2. {logics_manager-2.1.2 → logics_manager-2.2.0}/README.md +47 -4
  3. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager/assist.py +185 -21
  4. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager/cli.py +132 -12
  5. logics_manager-2.2.0/logics_manager/cli_output.py +18 -0
  6. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager/flow.py +1257 -83
  7. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager/index.py +3 -7
  8. logics_manager-2.2.0/logics_manager/insights.py +418 -0
  9. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager/mcp.py +50 -0
  10. logics_manager-2.2.0/logics_manager/path_utils.py +31 -0
  11. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager/sync.py +24 -12
  12. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager.egg-info/PKG-INFO +1 -1
  13. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager.egg-info/SOURCES.txt +3 -0
  14. {logics_manager-2.1.2 → logics_manager-2.2.0}/pyproject.toml +1 -1
  15. {logics_manager-2.1.2 → logics_manager-2.2.0}/LICENSE +0 -0
  16. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager/__init__.py +0 -0
  17. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager/__main__.py +0 -0
  18. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager/audit.py +0 -0
  19. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager/bootstrap.py +0 -0
  20. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager/config.py +0 -0
  21. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager/doctor.py +0 -0
  22. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager/lint.py +0 -0
  23. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager/termstyle.py +0 -0
  24. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager.egg-info/dependency_links.txt +0 -0
  25. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager.egg-info/entry_points.txt +0 -0
  26. {logics_manager-2.1.2 → logics_manager-2.2.0}/logics_manager.egg-info/top_level.txt +0 -0
  27. {logics_manager-2.1.2 → logics_manager-2.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: logics-manager
3
- Version: 2.1.2
3
+ Version: 2.2.0
4
4
  Summary: Canonical Logics CLI
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/AlexAgo83/logics-manager/actions/workflows/ci.yml/badge.svg)](https://github.com/AlexAgo83/logics-manager/actions/workflows/ci.yml)
4
4
  [![License](https://img.shields.io/github/license/AlexAgo83/logics-manager)](LICENSE)
5
- ![Version](https://img.shields.io/badge/version-v2.1.2-4C8BF5)
5
+ ![Version](https://img.shields.io/badge/version-v2.2.0-4C8BF5)
6
6
  ![VS Code](https://img.shields.io/badge/VS%20Code-1.86.0-007ACC?logo=visualstudiocode&logoColor=white)
7
7
  ![TypeScript](https://img.shields.io/badge/TypeScript-5.3.3-3178C6?logo=typescript&logoColor=white)
8
8
  ![Vitest](https://img.shields.io/badge/Vitest-2.1.8-6E9F18?logo=vitest&logoColor=white)
@@ -101,12 +101,55 @@ Useful commands:
101
101
 
102
102
  ```bash
103
103
  logics-manager flow list
104
- logics-manager flow promote request-to-backlog logics/request/req_001_example.md
105
- logics-manager flow promote backlog-to-task logics/backlog/item_001_example.md
106
- logics-manager flow finish task logics/tasks/task_001_example.md
104
+ logics-manager flow promote request-to-backlog req_001_example
105
+ logics-manager flow promote backlog-to-task item_001_example
106
+ logics-manager flow finish task task_001_example
107
107
  logics-manager sync context-pack req_001_example --format json
108
108
  ```
109
109
 
110
+ ### CLI Contracts
111
+
112
+ Workflow target arguments accept these forms:
113
+
114
+ - a workflow ref, such as `req_001_example`, `item_001_example`, or `task_001_example`;
115
+ - a repo-relative Markdown path under the matching Logics directory, such as `logics/request/req_001_example.md`;
116
+ - an absolute path only when it resolves inside the current repository.
117
+
118
+ Mutation commands reject `..` traversal and files outside the repository before writing. Output paths passed with `--out` must also be repo-relative and remain inside the repository after resolution. Configured log/cache paths in `logics.yaml` may be repo-relative or absolute, but absolute paths must still resolve inside the current repository.
119
+
120
+ When a command supports `--format json`, stdout is a machine-readable JSON payload. Human-oriented status, diagnostics, and progress text should not be mixed into stdout for JSON mode. This makes JSON-mode commands safe to pipe into tools such as `jq` or consume from scripts.
121
+
122
+ `--json` is a shorthand for `--format json` on commands that support JSON output.
123
+
124
+ JSON-capable operator commands:
125
+
126
+ | Command | Purpose | JSON output |
127
+ | --- | --- | --- |
128
+ | `logics-manager status` | Summarize open workflow docs and next actions. | `--format json` or `--json` |
129
+ | `logics-manager health` | Show workflow health counts and issue signals. | `--format json` or `--json` |
130
+ | `logics-manager followups` | List follow-up areas with request creation commands. | `--format json` or `--json` |
131
+ | `logics-manager product-consistency` | Check product brief lineage links. | `--format json` or `--json` |
132
+ | `logics-manager search <query>` | Search workflow docs directly. | `--format json` or `--json` |
133
+ | `logics-manager index` | Regenerate `logics/INDEX.md`. | `--format json` or `--json` |
134
+ | `logics-manager lint` | Validate doc shape and changed-doc hygiene. | `--format json` or `--json` |
135
+ | `logics-manager audit` | Validate workflow traceability and governance. | `--format json` or `--json` |
136
+ | `logics-manager sync ...` | Read, list, search, repair, and export workflow state. | `--format json` or `--json` on supported subcommands |
137
+ | `logics-manager assist ...` | Build review, validation, context, and runtime summaries. | `--format json` or `--json` on supported subcommands |
138
+ | `logics-manager flow ...` | Create, promote, split, close, finish, and list docs. | `--format json` or `--json` on supported subcommands |
139
+
140
+ Operator triage flow:
141
+
142
+ ```bash
143
+ logics-manager status --json
144
+ logics-manager health --json
145
+ logics-manager product-consistency --json
146
+ logics-manager followups --source-kind product --json
147
+ ```
148
+
149
+ Use `status` first when you need the next work signal. Use `health` for corpus-level anomalies. Use `product-consistency --strict` in release checks when active product briefs must have valid lineage. Use `followups` for open actionable follow-up areas; add `--include-closed` only when auditing historical docs.
150
+
151
+ Multi-file workflow mutations such as `flow promote`, `flow split`, and `flow finish` validate their direct inputs before writing. New workflow docs are created with exclusive filesystem writes, so an ID collision fails instead of overwriting an existing file; rerun the command to allocate a fresh ID after reviewing `git status`/`git diff`. They still operate on Markdown files in the working tree rather than through a database or transaction service; if the filesystem fails mid-write, recover with git status/diff and rerun after cleanup.
152
+
110
153
  To update the installed CLI later:
111
154
 
112
155
  ```bash
@@ -14,6 +14,7 @@ from typing import Any
14
14
  from .config import ConfigError, find_repo_root, load_repo_config
15
15
  from .doctor import doctor_payload
16
16
  from .lint import lint_payload
17
+ from .path_utils import resolve_repo_config_path, resolve_repo_output_path
17
18
  from .termstyle import colorize_help
18
19
 
19
20
 
@@ -88,8 +89,9 @@ def _hybrid_measurement_log(config: dict[str, object]) -> str:
88
89
  return str(_get_nested(config, "hybrid_assist", "measurement_log", default=DEFAULT_HYBRID_MEASUREMENT_LOG))
89
90
 
90
91
 
91
- def _repo_path(repo_root: Path, value: str | None, default: str) -> Path:
92
- return (repo_root / (value or default)).resolve()
92
+ def _repo_path(repo_root: Path, value: str | None, default: str, *, label: str) -> Path:
93
+ resolved, _relative = resolve_repo_config_path(repo_root, value or default, label=label)
94
+ return resolved
93
95
 
94
96
 
95
97
  def _parse_package_version(repo_root: Path) -> str:
@@ -641,6 +643,35 @@ def _git_changed_paths(repo_root: Path) -> list[str]:
641
643
  return [line.strip() for line in completed.stdout.splitlines() if line.strip()]
642
644
 
643
645
 
646
+ def _git_lines(repo_root: Path, args: list[str]) -> list[str]:
647
+ try:
648
+ completed = subprocess.run(
649
+ ["git", *args],
650
+ cwd=repo_root,
651
+ stdout=subprocess.PIPE,
652
+ stderr=subprocess.DEVNULL,
653
+ text=True,
654
+ check=False,
655
+ )
656
+ except OSError:
657
+ return []
658
+ if completed.returncode != 0:
659
+ return []
660
+ return [line.strip() for line in completed.stdout.splitlines() if line.strip()]
661
+
662
+
663
+ def _git_range_changed_paths(repo_root: Path, since: str) -> list[str]:
664
+ return sorted(set(_git_lines(repo_root, ["diff", "--name-only", "--relative=.", f"{since}..HEAD"])))
665
+
666
+
667
+ def _git_range_commits(repo_root: Path, since: str) -> list[dict[str, str]]:
668
+ commits: list[dict[str, str]] = []
669
+ for line in _git_lines(repo_root, ["log", "--oneline", f"{since}..HEAD"]):
670
+ commit, _, subject = line.partition(" ")
671
+ commits.append({"commit": commit, "subject": subject})
672
+ return commits
673
+
674
+
644
675
  def _is_low_risk_generated_path(path: str) -> bool:
645
676
  normalized = path.strip().replace("\\", "/")
646
677
  filename = normalized.rsplit("/", 1)[-1]
@@ -1346,6 +1377,12 @@ def build_parser() -> argparse.ArgumentParser:
1346
1377
  closure_summary.add_argument("--dry-run", action="store_true")
1347
1378
  closure_summary.set_defaults(func=cmd_closure_summary)
1348
1379
 
1380
+ handoff = sub.add_parser("handoff", help="Summarize commits, changed surfaces, Logics docs, validations, and next actions.")
1381
+ handoff.add_argument("--since", required=True)
1382
+ handoff.add_argument("--format", choices=("text", "json"), default="text")
1383
+ handoff.add_argument("--dry-run", action="store_true")
1384
+ handoff.set_defaults(func=cmd_handoff)
1385
+
1349
1386
  return parser
1350
1387
 
1351
1388
 
@@ -1417,11 +1454,15 @@ def _build_help() -> str:
1417
1454
  " closure-summary [ref]",
1418
1455
  " Summarize a delivered request, backlog item, or task.",
1419
1456
  " Flags: --format {text,json}, --dry-run",
1457
+ " handoff",
1458
+ " Summarize commits, changed surfaces, Logics docs, validations, and next actions.",
1459
+ " Flags: --since, --format {text,json}, --dry-run",
1420
1460
  "",
1421
1461
  "Examples:",
1422
1462
  " logics-manager assist runtime-status --format json",
1423
1463
  " logics-manager assist context request req_001_my_request --profile deep",
1424
1464
  " logics-manager assist request-draft --intent \"Improve onboarding\"",
1465
+ " logics-manager assist handoff --since HEAD~1",
1425
1466
  ]
1426
1467
  )
1427
1468
 
@@ -1527,6 +1568,21 @@ def _build_command_help(command: str) -> str:
1527
1568
  " --dry-run",
1528
1569
  ]
1529
1570
  )
1571
+ if command == "handoff":
1572
+ return "\n".join(
1573
+ [
1574
+ "Logics Assist Handoff",
1575
+ "Summarize commits, changed surfaces, Logics docs, validations, and next actions.",
1576
+ "",
1577
+ "Usage:",
1578
+ " logics-manager assist handoff --since <rev> [args...]",
1579
+ "",
1580
+ "Flags:",
1581
+ " --since",
1582
+ " --format {text,json}",
1583
+ " --dry-run",
1584
+ ]
1585
+ )
1530
1586
  if command == "roi-report":
1531
1587
  return "\n".join(
1532
1588
  [
@@ -2023,6 +2079,77 @@ def _build_closure_summary(repo_root: Path, ref: str | None) -> dict[str, object
2023
2079
  }
2024
2080
 
2025
2081
 
2082
+ def _doc_title_from_path(path: Path) -> str:
2083
+ try:
2084
+ lines = path.read_text(encoding="utf-8").splitlines()
2085
+ except OSError:
2086
+ return path.stem
2087
+ for line in lines:
2088
+ if line.startswith("## "):
2089
+ payload = line.removeprefix("## ").strip()
2090
+ if " - " in payload:
2091
+ return payload.split(" - ", 1)[1].strip()
2092
+ return payload
2093
+ return path.stem
2094
+
2095
+
2096
+ def _validation_lines_from_task(path: Path) -> list[str]:
2097
+ try:
2098
+ lines = path.read_text(encoding="utf-8").splitlines()
2099
+ except OSError:
2100
+ return []
2101
+ values: list[str] = []
2102
+ for line in _section_lines(lines, "Validation"):
2103
+ stripped = line.strip()
2104
+ if not stripped.startswith("- "):
2105
+ continue
2106
+ value = stripped[2:].strip()
2107
+ if value and not value.lower().startswith("run `") and not value.lower().startswith("run the "):
2108
+ values.append(value)
2109
+ return values
2110
+
2111
+
2112
+ def _build_handoff(repo_root: Path, since: str) -> dict[str, object]:
2113
+ changed_paths = _git_range_changed_paths(repo_root, since)
2114
+ commits = _git_range_commits(repo_root, since)
2115
+ surface = _build_changed_surface_summary(changed_paths)
2116
+ logics_docs: list[dict[str, object]] = []
2117
+ validations: list[str] = []
2118
+ for rel_path in changed_paths:
2119
+ if not rel_path.startswith("logics/") or not rel_path.endswith(".md"):
2120
+ continue
2121
+ path = repo_root / rel_path
2122
+ kind = path.parent.name
2123
+ entry = {
2124
+ "path": rel_path,
2125
+ "ref": path.stem,
2126
+ "kind": kind,
2127
+ "title": _doc_title_from_path(path),
2128
+ "status": _doc_status(path) if path.is_file() else "Unknown",
2129
+ }
2130
+ logics_docs.append(entry)
2131
+ if kind == "tasks":
2132
+ validations.extend(_validation_lines_from_task(path))
2133
+ next_actions = [
2134
+ "Run lint/audit if not already included in validation evidence.",
2135
+ "Review changed files before committing or handing off.",
2136
+ ]
2137
+ if any(path.startswith("logics_manager/") for path in changed_paths):
2138
+ next_actions.append("Run `PYTHONPATH=\"$PWD\" pytest python_tests -q` for Python CLI changes.")
2139
+ if any(path.startswith("src/") for path in changed_paths):
2140
+ next_actions.append("Run the TypeScript/vitest checks for extension changes.")
2141
+ return {
2142
+ "since": since,
2143
+ "commit_count": len(commits),
2144
+ "commits": commits,
2145
+ "changed_paths": changed_paths,
2146
+ "surface": surface,
2147
+ "logics_docs": logics_docs,
2148
+ "validations": sorted(set(validations)),
2149
+ "next_actions": next_actions,
2150
+ }
2151
+
2152
+
2026
2153
  def _build_context_pack(repo_root: Path, seed_ref: str, *, mode: str, profile: str) -> dict[str, object]:
2027
2154
  docs = _workflow_docs(repo_root)
2028
2155
  selected: list[Path] = []
@@ -2236,8 +2363,8 @@ def cmd_test_impact_summary(args: argparse.Namespace) -> dict[str, object]:
2236
2363
  def cmd_roi_report(args: argparse.Namespace) -> dict[str, object]:
2237
2364
  repo_root = find_repo_root(Path.cwd())
2238
2365
  config, config_path = load_repo_config(repo_root)
2239
- audit_log = _repo_path(repo_root, args.audit_log, _hybrid_audit_log(config))
2240
- measurement_log = _repo_path(repo_root, args.measurement_log, _hybrid_measurement_log(config))
2366
+ audit_log = _repo_path(repo_root, args.audit_log, _hybrid_audit_log(config), label="configured audit_log")
2367
+ measurement_log = _repo_path(repo_root, args.measurement_log, _hybrid_measurement_log(config), label="configured measurement_log")
2241
2368
  payload = _build_hybrid_roi_report(
2242
2369
  repo_root,
2243
2370
  audit_log=audit_log,
@@ -2251,13 +2378,16 @@ def cmd_roi_report(args: argparse.Namespace) -> dict[str, object]:
2251
2378
  payload["config_path"] = str(config_path.relative_to(repo_root)) if config_path is not None else None
2252
2379
 
2253
2380
  if args.out:
2254
- out_path = (repo_root / args.out).resolve()
2381
+ out_path, output_path = resolve_repo_output_path(repo_root, args.out)
2382
+ payload["output_path"] = output_path
2255
2383
  serialized = json.dumps(payload, indent=2, sort_keys=True) + "\n"
2256
2384
  if not args.dry_run:
2257
2385
  out_path.parent.mkdir(parents=True, exist_ok=True)
2258
2386
  out_path.write_text(serialized, encoding="utf-8")
2259
- print(f"Wrote {out_path.relative_to(repo_root)}")
2260
- payload["output_path"] = out_path.relative_to(repo_root).as_posix()
2387
+ if args.format == "json":
2388
+ print(json.dumps(payload, indent=2, sort_keys=True))
2389
+ else:
2390
+ print(f"Wrote {output_path}")
2261
2391
  elif args.format == "json":
2262
2392
  print(json.dumps(payload, indent=2, sort_keys=True))
2263
2393
  else:
@@ -2307,13 +2437,16 @@ def cmd_runtime_status(args: argparse.Namespace) -> dict[str, object]:
2307
2437
  }
2308
2438
 
2309
2439
  if args.out:
2310
- out_path = (repo_root / args.out).resolve()
2440
+ out_path, output_path = resolve_repo_output_path(repo_root, args.out)
2441
+ payload["output_path"] = output_path
2311
2442
  serialized = json.dumps(payload, indent=2, sort_keys=True) + "\n"
2312
2443
  if not args.dry_run:
2313
2444
  out_path.parent.mkdir(parents=True, exist_ok=True)
2314
2445
  out_path.write_text(serialized, encoding="utf-8")
2315
- print(f"Wrote {out_path.relative_to(repo_root)}")
2316
- payload["output_path"] = out_path.relative_to(repo_root).as_posix()
2446
+ if args.format == "json":
2447
+ print(json.dumps(payload, indent=2, sort_keys=True))
2448
+ else:
2449
+ print(f"Wrote {output_path}")
2317
2450
  elif args.format == "json":
2318
2451
  print(json.dumps(payload, indent=2, sort_keys=True))
2319
2452
  else:
@@ -2363,14 +2496,14 @@ def cmd_request_draft(args: argparse.Namespace) -> dict[str, object]:
2363
2496
  **_build_request_draft(repo_root, intent=args.intent),
2364
2497
  }
2365
2498
  if args.execution_mode == "execute":
2366
- out_path = repo_root / payload["path"]
2499
+ out_path, output_path = resolve_repo_output_path(repo_root, str(payload["path"]), label="output")
2367
2500
  if not args.dry_run:
2368
2501
  out_path.parent.mkdir(parents=True, exist_ok=True)
2369
2502
  out_path.write_text(payload["content"], encoding="utf-8")
2370
2503
  payload["written"] = True
2371
2504
  else:
2372
2505
  payload["written"] = False
2373
- payload["output_path"] = out_path.relative_to(repo_root).as_posix()
2506
+ payload["output_path"] = output_path
2374
2507
  else:
2375
2508
  payload["written"] = False
2376
2509
  if args.format == "json":
@@ -2409,14 +2542,14 @@ def cmd_spec_first_pass(args: argparse.Namespace) -> dict[str, object]:
2409
2542
  **_build_spec_first_pass(repo_root, args.ref),
2410
2543
  }
2411
2544
  if args.execution_mode == "execute":
2412
- out_path = repo_root / payload["path"]
2545
+ out_path, output_path = resolve_repo_output_path(repo_root, str(payload["path"]), label="output")
2413
2546
  if not args.dry_run:
2414
2547
  out_path.parent.mkdir(parents=True, exist_ok=True)
2415
2548
  out_path.write_text(payload["content"], encoding="utf-8")
2416
2549
  payload["written"] = True
2417
2550
  else:
2418
2551
  payload["written"] = False
2419
- payload["output_path"] = out_path.relative_to(repo_root).as_posix()
2552
+ payload["output_path"] = output_path
2420
2553
  else:
2421
2554
  payload["written"] = False
2422
2555
  if args.format == "json":
@@ -2455,16 +2588,16 @@ def cmd_backlog_groom(args: argparse.Namespace) -> dict[str, object]:
2455
2588
  **_build_backlog_groom(repo_root, args.ref),
2456
2589
  }
2457
2590
  if args.execution_mode == "execute":
2458
- out_path = repo_root / payload["path"]
2591
+ out_path, output_path = resolve_repo_output_path(repo_root, str(payload["path"]), label="output")
2459
2592
  if not args.dry_run:
2460
2593
  out_path.parent.mkdir(parents=True, exist_ok=True)
2461
2594
  out_path.write_text(payload["content"], encoding="utf-8")
2462
2595
  payload["written"] = True
2463
- request_path = repo_root / payload["request_path"]
2596
+ request_path, _request_output_path = resolve_repo_output_path(repo_root, str(payload["request_path"]), label="request_path")
2464
2597
  _append_section_bullets(request_path, "Backlog", [f"`{payload['ref']}`"], dry_run=False)
2465
2598
  else:
2466
2599
  payload["written"] = False
2467
- payload["output_path"] = out_path.relative_to(repo_root).as_posix()
2600
+ payload["output_path"] = output_path
2468
2601
  else:
2469
2602
  payload["written"] = False
2470
2603
  if args.format == "json":
@@ -2513,6 +2646,34 @@ def cmd_closure_summary(args: argparse.Namespace) -> dict[str, object]:
2513
2646
  return payload
2514
2647
 
2515
2648
 
2649
+ def cmd_handoff(args: argparse.Namespace) -> dict[str, object]:
2650
+ repo_root = find_repo_root(Path.cwd())
2651
+ config, config_path = load_repo_config(repo_root)
2652
+ payload = {
2653
+ "command": "assist",
2654
+ "kind": "handoff",
2655
+ "repo_root": repo_root.as_posix(),
2656
+ "config_path": str(config_path.relative_to(repo_root)) if config_path is not None else None,
2657
+ **_build_handoff(repo_root, args.since),
2658
+ }
2659
+ if args.format == "json":
2660
+ print(json.dumps(payload, indent=2, sort_keys=True))
2661
+ else:
2662
+ print(f"Handoff since {payload['since']}:")
2663
+ print(f"- commits: {payload['commit_count']}")
2664
+ print(f"- changed paths: {len(payload['changed_paths'])}")
2665
+ print(f"- primary surface: {payload['surface']['primary_category']}")
2666
+ for commit in payload["commits"][:8]:
2667
+ print(f"- commit: {commit['commit']} {commit['subject']}")
2668
+ for doc in payload["logics_docs"][:8]:
2669
+ print(f"- logics: {doc['ref']} [{doc['status']}] {doc['path']}")
2670
+ for validation in payload["validations"][:8]:
2671
+ print(f"- validation: {validation}")
2672
+ for action in payload["next_actions"]:
2673
+ print(f"- next: {action}")
2674
+ return payload
2675
+
2676
+
2516
2677
  def cmd_context(args: argparse.Namespace) -> dict[str, object]:
2517
2678
  repo_root = find_repo_root(Path.cwd())
2518
2679
  config, config_path = load_repo_config(repo_root)
@@ -2554,13 +2715,16 @@ def cmd_context(args: argparse.Namespace) -> dict[str, object]:
2554
2715
  }
2555
2716
 
2556
2717
  if args.out:
2557
- out_path = (repo_root / args.out).resolve()
2718
+ out_path, output_path = resolve_repo_output_path(repo_root, args.out)
2719
+ payload["output_path"] = output_path
2558
2720
  serialized = json.dumps(payload, indent=2, sort_keys=True) + "\n"
2559
2721
  if not args.dry_run:
2560
2722
  out_path.parent.mkdir(parents=True, exist_ok=True)
2561
2723
  out_path.write_text(serialized, encoding="utf-8")
2562
- print(f"Wrote {out_path.relative_to(repo_root)}")
2563
- payload["output_path"] = out_path.relative_to(repo_root).as_posix()
2724
+ if args.format == "json":
2725
+ print(json.dumps(payload, indent=2, sort_keys=True))
2726
+ else:
2727
+ print(f"Wrote {output_path}")
2564
2728
  elif args.format == "json":
2565
2729
  print(json.dumps(payload, indent=2, sort_keys=True))
2566
2730
  else:
@@ -2576,7 +2740,7 @@ def main(argv: list[str]) -> int:
2576
2740
  if not argv or argv[0] in HELP_FLAGS:
2577
2741
  _print_help(_build_help())
2578
2742
  return 0
2579
- if argv[0] in {"runtime-status", "context", "request-draft", "spec-first-pass", "backlog-groom", "closure-summary", "roi-report", "diff-risk", "commit-plan", "changed-surface-summary", "doc-consistency", "review-checklist", "validation-checklist", "validation-summary", "test-impact-summary", "claude-bridges", "claude-instructions", "next-step"} and len(argv) > 1 and argv[1] in HELP_FLAGS:
2743
+ if argv[0] in {"runtime-status", "context", "request-draft", "spec-first-pass", "backlog-groom", "closure-summary", "handoff", "roi-report", "diff-risk", "commit-plan", "changed-surface-summary", "doc-consistency", "review-checklist", "validation-checklist", "validation-summary", "test-impact-summary", "claude-bridges", "claude-instructions", "next-step"} and len(argv) > 1 and argv[1] in HELP_FLAGS:
2580
2744
  _print_help(_build_command_help(argv[0]))
2581
2745
  return 0
2582
2746
  parser = build_parser()