@grifhinz/logics-manager 2.1.2 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -4
- package/VERSION +1 -1
- package/clients/README.md +9 -0
- package/clients/shared-web/media/css/board.css +658 -0
- package/clients/shared-web/media/css/details.css +457 -0
- package/clients/shared-web/media/css/layout.css +123 -0
- package/clients/shared-web/media/css/toolbar.css +576 -0
- package/clients/shared-web/media/harnessApi.js +324 -0
- package/clients/shared-web/media/hostApi.js +213 -0
- package/clients/shared-web/media/hostApiContract.js +55 -0
- package/clients/shared-web/media/icon.png +0 -0
- package/clients/shared-web/media/layoutController.js +246 -0
- package/clients/shared-web/media/logics.svg +7 -0
- package/clients/shared-web/media/logicsModel.js +910 -0
- package/clients/shared-web/media/main.css +112 -0
- package/clients/shared-web/media/main.js +3 -0
- package/clients/shared-web/media/mainApp.js +1005 -0
- package/clients/shared-web/media/mainCore.js +604 -0
- package/clients/shared-web/media/mainInteractionHandlers.js +324 -0
- package/clients/shared-web/media/mainInteractions.js +378 -0
- package/clients/shared-web/media/renderBoard.js +3 -0
- package/clients/shared-web/media/renderBoardApp.js +1339 -0
- package/clients/shared-web/media/renderDetails.js +685 -0
- package/clients/shared-web/media/renderMarkdown.js +449 -0
- package/clients/shared-web/media/toolsPanelLayout.js +172 -0
- package/clients/shared-web/media/uiStatus.js +54 -0
- package/clients/shared-web/media/webviewChrome.js +405 -0
- package/clients/shared-web/media/webviewPersistence.js +116 -0
- package/clients/shared-web/media/webviewSelectors.js +491 -0
- package/clients/viewer/README.md +5 -0
- package/clients/viewer/browser-host.js +847 -0
- package/clients/viewer/index.html +237 -0
- package/clients/viewer/viewer.css +433 -0
- package/logics_manager/assist.py +94 -63
- package/logics_manager/assist_handoff.py +132 -0
- package/logics_manager/assist_surface.py +38 -0
- package/logics_manager/cli.py +152 -12
- package/logics_manager/cli_output.py +18 -0
- package/logics_manager/flow.py +1360 -84
- package/logics_manager/flow_evidence.py +63 -0
- package/logics_manager/index.py +3 -7
- package/logics_manager/insights.py +418 -0
- package/logics_manager/mcp.py +50 -0
- package/logics_manager/path_utils.py +31 -0
- package/logics_manager/sync.py +24 -12
- package/logics_manager/update_check.py +138 -0
- package/logics_manager/viewer.py +533 -0
- package/package.json +12 -6
- package/pyproject.toml +1 -1
package/logics_manager/assist.py
CHANGED
|
@@ -11,9 +11,12 @@ import subprocess
|
|
|
11
11
|
from shutil import which
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
from .assist_handoff import build_handoff as _build_handoff
|
|
15
|
+
from .assist_surface import build_changed_surface_summary as _build_changed_surface_summary
|
|
14
16
|
from .config import ConfigError, find_repo_root, load_repo_config
|
|
15
17
|
from .doctor import doctor_payload
|
|
16
18
|
from .lint import lint_payload
|
|
19
|
+
from .path_utils import resolve_repo_config_path, resolve_repo_output_path
|
|
17
20
|
from .termstyle import colorize_help
|
|
18
21
|
|
|
19
22
|
|
|
@@ -88,8 +91,9 @@ def _hybrid_measurement_log(config: dict[str, object]) -> str:
|
|
|
88
91
|
return str(_get_nested(config, "hybrid_assist", "measurement_log", default=DEFAULT_HYBRID_MEASUREMENT_LOG))
|
|
89
92
|
|
|
90
93
|
|
|
91
|
-
def _repo_path(repo_root: Path, value: str | None, default: str) -> Path:
|
|
92
|
-
|
|
94
|
+
def _repo_path(repo_root: Path, value: str | None, default: str, *, label: str) -> Path:
|
|
95
|
+
resolved, _relative = resolve_repo_config_path(repo_root, value or default, label=label)
|
|
96
|
+
return resolved
|
|
93
97
|
|
|
94
98
|
|
|
95
99
|
def _parse_package_version(repo_root: Path) -> str:
|
|
@@ -723,7 +727,7 @@ def _render_diff_risk_text(payload: dict[str, object]) -> str:
|
|
|
723
727
|
def _summarize_commit_scope(changed_paths: list[str]) -> tuple[str, str]:
|
|
724
728
|
if not changed_paths:
|
|
725
729
|
return "root", "No changes detected; nothing to commit."
|
|
726
|
-
if any(path.startswith("src/") for path in changed_paths):
|
|
730
|
+
if any(path.startswith("clients/vscode/src/") or path.startswith("clients/shared-web/media/") for path in changed_paths):
|
|
727
731
|
return "plugin", "Plugin surface changes detected."
|
|
728
732
|
if any(path.startswith("logics_manager/") for path in changed_paths):
|
|
729
733
|
return "python-runtime", "Native Logics manager changes detected."
|
|
@@ -765,55 +769,20 @@ def _build_commit_plan(changed_paths: list[str]) -> dict[str, object]:
|
|
|
765
769
|
}
|
|
766
770
|
|
|
767
771
|
|
|
768
|
-
def _build_changed_surface_summary(changed_paths: list[str]) -> dict[str, object]:
|
|
769
|
-
category_counter: Counter[str] = Counter()
|
|
770
|
-
for path in changed_paths:
|
|
771
|
-
normalized = path.replace("\\", "/")
|
|
772
|
-
if normalized.startswith("src/"):
|
|
773
|
-
category_counter["plugin"] += 1
|
|
774
|
-
elif normalized.startswith("logics_manager/"):
|
|
775
|
-
category_counter["python-runtime"] += 1
|
|
776
|
-
elif normalized.startswith("logics/"):
|
|
777
|
-
category_counter["workflow-docs"] += 1
|
|
778
|
-
elif normalized.startswith("tests/") or "/tests/" in normalized or normalized.startswith("python_tests/"):
|
|
779
|
-
category_counter["tests"] += 1
|
|
780
|
-
elif normalized.endswith(".md"):
|
|
781
|
-
category_counter["docs"] += 1
|
|
782
|
-
else:
|
|
783
|
-
category_counter["other"] += 1
|
|
784
|
-
primary = category_counter.most_common(1)[0][0] if category_counter else "clean"
|
|
785
|
-
summary = {
|
|
786
|
-
"clean": "No changed surface was detected.",
|
|
787
|
-
"plugin": "The plugin surface is the dominant change area.",
|
|
788
|
-
"python-runtime": "The native Python runtime is the dominant change area.",
|
|
789
|
-
"workflow-docs": "Workflow documentation is the dominant change area.",
|
|
790
|
-
"tests": "Tests are the dominant change area.",
|
|
791
|
-
"docs": "Markdown documentation is the dominant change area.",
|
|
792
|
-
"other": "Mixed repository changes are present.",
|
|
793
|
-
}.get(primary, "Mixed repository changes are present.")
|
|
794
|
-
return {
|
|
795
|
-
"summary": summary,
|
|
796
|
-
"primary_category": primary,
|
|
797
|
-
"counts": dict(sorted(category_counter.items())),
|
|
798
|
-
"changed_paths": changed_paths,
|
|
799
|
-
"review_recommended": primary not in {"clean", "docs"} and bool(changed_paths),
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
|
|
803
772
|
def _build_validation_checklist(changed_paths: list[str]) -> dict[str, object]:
|
|
804
773
|
surface = _build_changed_surface_summary(changed_paths)
|
|
805
774
|
checks: list[str] = [
|
|
806
|
-
"Run `python3 -m pytest
|
|
775
|
+
"Run `python3 -m pytest tests/python/test_logics_manager_cli.py -q`.",
|
|
807
776
|
"Run `python3 -m compileall logics_manager`.",
|
|
808
777
|
"Run `npm run lint:logics`.",
|
|
809
778
|
]
|
|
810
|
-
if any(path.startswith("src/") for path in changed_paths):
|
|
779
|
+
if any(path.startswith("clients/vscode/src/") or path.startswith("clients/shared-web/media/") for path in changed_paths):
|
|
811
780
|
checks.append("Run the plugin test suite that exercises the VS Code entrypoints.")
|
|
812
781
|
if any(path.startswith("logics_manager/") for path in changed_paths):
|
|
813
782
|
checks.append("Smoke-test `python3 -m logics_manager --help` and the affected native subcommands.")
|
|
814
783
|
if any(path.startswith("logics/") for path in changed_paths):
|
|
815
784
|
checks.append("Run `python3 -m logics_manager lint --require-status` and inspect the workflow docs manually.")
|
|
816
|
-
if any(path.startswith("tests/") or path.startswith("
|
|
785
|
+
if any(path.startswith("tests/") or path.startswith("tests/python/") for path in changed_paths):
|
|
817
786
|
checks.append("Run the focused affected tests before broad regression sweeps.")
|
|
818
787
|
if not changed_paths:
|
|
819
788
|
checks.append("No validation needed beyond a clean smoke check; there are no tracked changes.")
|
|
@@ -829,15 +798,15 @@ def _build_test_impact_summary(changed_paths: list[str]) -> dict[str, object]:
|
|
|
829
798
|
categories = _build_changed_surface_summary(changed_paths)["counts"]
|
|
830
799
|
recommended: list[str] = []
|
|
831
800
|
if "python-runtime" in categories:
|
|
832
|
-
recommended.append("python3 -m pytest
|
|
801
|
+
recommended.append("python3 -m pytest tests/python/test_logics_manager_cli.py -q")
|
|
833
802
|
if "plugin" in categories:
|
|
834
803
|
recommended.append("npm run lint")
|
|
835
804
|
if "workflow-docs" in categories:
|
|
836
805
|
recommended.append("npm run lint:logics")
|
|
837
806
|
if "tests" in categories:
|
|
838
|
-
recommended.append("python3 -m pytest
|
|
807
|
+
recommended.append("python3 -m pytest tests/python/test_logics_manager_cli.py -q")
|
|
839
808
|
if not recommended:
|
|
840
|
-
recommended.append("python3 -m pytest
|
|
809
|
+
recommended.append("python3 -m pytest tests/python/test_logics_manager_cli.py -q")
|
|
841
810
|
return {
|
|
842
811
|
"summary": "Recommended test order derived from the current change surface.",
|
|
843
812
|
"categories": categories,
|
|
@@ -1346,6 +1315,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1346
1315
|
closure_summary.add_argument("--dry-run", action="store_true")
|
|
1347
1316
|
closure_summary.set_defaults(func=cmd_closure_summary)
|
|
1348
1317
|
|
|
1318
|
+
handoff = sub.add_parser("handoff", help="Summarize commits, changed surfaces, Logics docs, validations, and next actions.")
|
|
1319
|
+
handoff.add_argument("--since", required=True)
|
|
1320
|
+
handoff.add_argument("--format", choices=("text", "json"), default="text")
|
|
1321
|
+
handoff.add_argument("--dry-run", action="store_true")
|
|
1322
|
+
handoff.set_defaults(func=cmd_handoff)
|
|
1323
|
+
|
|
1349
1324
|
return parser
|
|
1350
1325
|
|
|
1351
1326
|
|
|
@@ -1417,11 +1392,15 @@ def _build_help() -> str:
|
|
|
1417
1392
|
" closure-summary [ref]",
|
|
1418
1393
|
" Summarize a delivered request, backlog item, or task.",
|
|
1419
1394
|
" Flags: --format {text,json}, --dry-run",
|
|
1395
|
+
" handoff",
|
|
1396
|
+
" Summarize commits, changed surfaces, Logics docs, validations, and next actions.",
|
|
1397
|
+
" Flags: --since, --format {text,json}, --dry-run",
|
|
1420
1398
|
"",
|
|
1421
1399
|
"Examples:",
|
|
1422
1400
|
" logics-manager assist runtime-status --format json",
|
|
1423
1401
|
" logics-manager assist context request req_001_my_request --profile deep",
|
|
1424
1402
|
" logics-manager assist request-draft --intent \"Improve onboarding\"",
|
|
1403
|
+
" logics-manager assist handoff --since HEAD~1",
|
|
1425
1404
|
]
|
|
1426
1405
|
)
|
|
1427
1406
|
|
|
@@ -1527,6 +1506,21 @@ def _build_command_help(command: str) -> str:
|
|
|
1527
1506
|
" --dry-run",
|
|
1528
1507
|
]
|
|
1529
1508
|
)
|
|
1509
|
+
if command == "handoff":
|
|
1510
|
+
return "\n".join(
|
|
1511
|
+
[
|
|
1512
|
+
"Logics Assist Handoff",
|
|
1513
|
+
"Summarize commits, changed surfaces, Logics docs, validations, and next actions.",
|
|
1514
|
+
"",
|
|
1515
|
+
"Usage:",
|
|
1516
|
+
" logics-manager assist handoff --since <rev> [args...]",
|
|
1517
|
+
"",
|
|
1518
|
+
"Flags:",
|
|
1519
|
+
" --since",
|
|
1520
|
+
" --format {text,json}",
|
|
1521
|
+
" --dry-run",
|
|
1522
|
+
]
|
|
1523
|
+
)
|
|
1530
1524
|
if command == "roi-report":
|
|
1531
1525
|
return "\n".join(
|
|
1532
1526
|
[
|
|
@@ -2236,8 +2230,8 @@ def cmd_test_impact_summary(args: argparse.Namespace) -> dict[str, object]:
|
|
|
2236
2230
|
def cmd_roi_report(args: argparse.Namespace) -> dict[str, object]:
|
|
2237
2231
|
repo_root = find_repo_root(Path.cwd())
|
|
2238
2232
|
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))
|
|
2233
|
+
audit_log = _repo_path(repo_root, args.audit_log, _hybrid_audit_log(config), label="configured audit_log")
|
|
2234
|
+
measurement_log = _repo_path(repo_root, args.measurement_log, _hybrid_measurement_log(config), label="configured measurement_log")
|
|
2241
2235
|
payload = _build_hybrid_roi_report(
|
|
2242
2236
|
repo_root,
|
|
2243
2237
|
audit_log=audit_log,
|
|
@@ -2251,13 +2245,16 @@ def cmd_roi_report(args: argparse.Namespace) -> dict[str, object]:
|
|
|
2251
2245
|
payload["config_path"] = str(config_path.relative_to(repo_root)) if config_path is not None else None
|
|
2252
2246
|
|
|
2253
2247
|
if args.out:
|
|
2254
|
-
out_path = (repo_root
|
|
2248
|
+
out_path, output_path = resolve_repo_output_path(repo_root, args.out)
|
|
2249
|
+
payload["output_path"] = output_path
|
|
2255
2250
|
serialized = json.dumps(payload, indent=2, sort_keys=True) + "\n"
|
|
2256
2251
|
if not args.dry_run:
|
|
2257
2252
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2258
2253
|
out_path.write_text(serialized, encoding="utf-8")
|
|
2259
|
-
|
|
2260
|
-
|
|
2254
|
+
if args.format == "json":
|
|
2255
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
2256
|
+
else:
|
|
2257
|
+
print(f"Wrote {output_path}")
|
|
2261
2258
|
elif args.format == "json":
|
|
2262
2259
|
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
2263
2260
|
else:
|
|
@@ -2307,13 +2304,16 @@ def cmd_runtime_status(args: argparse.Namespace) -> dict[str, object]:
|
|
|
2307
2304
|
}
|
|
2308
2305
|
|
|
2309
2306
|
if args.out:
|
|
2310
|
-
out_path = (repo_root
|
|
2307
|
+
out_path, output_path = resolve_repo_output_path(repo_root, args.out)
|
|
2308
|
+
payload["output_path"] = output_path
|
|
2311
2309
|
serialized = json.dumps(payload, indent=2, sort_keys=True) + "\n"
|
|
2312
2310
|
if not args.dry_run:
|
|
2313
2311
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2314
2312
|
out_path.write_text(serialized, encoding="utf-8")
|
|
2315
|
-
|
|
2316
|
-
|
|
2313
|
+
if args.format == "json":
|
|
2314
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
2315
|
+
else:
|
|
2316
|
+
print(f"Wrote {output_path}")
|
|
2317
2317
|
elif args.format == "json":
|
|
2318
2318
|
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
2319
2319
|
else:
|
|
@@ -2363,14 +2363,14 @@ def cmd_request_draft(args: argparse.Namespace) -> dict[str, object]:
|
|
|
2363
2363
|
**_build_request_draft(repo_root, intent=args.intent),
|
|
2364
2364
|
}
|
|
2365
2365
|
if args.execution_mode == "execute":
|
|
2366
|
-
out_path = repo_root
|
|
2366
|
+
out_path, output_path = resolve_repo_output_path(repo_root, str(payload["path"]), label="output")
|
|
2367
2367
|
if not args.dry_run:
|
|
2368
2368
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2369
2369
|
out_path.write_text(payload["content"], encoding="utf-8")
|
|
2370
2370
|
payload["written"] = True
|
|
2371
2371
|
else:
|
|
2372
2372
|
payload["written"] = False
|
|
2373
|
-
payload["output_path"] =
|
|
2373
|
+
payload["output_path"] = output_path
|
|
2374
2374
|
else:
|
|
2375
2375
|
payload["written"] = False
|
|
2376
2376
|
if args.format == "json":
|
|
@@ -2409,14 +2409,14 @@ def cmd_spec_first_pass(args: argparse.Namespace) -> dict[str, object]:
|
|
|
2409
2409
|
**_build_spec_first_pass(repo_root, args.ref),
|
|
2410
2410
|
}
|
|
2411
2411
|
if args.execution_mode == "execute":
|
|
2412
|
-
out_path = repo_root
|
|
2412
|
+
out_path, output_path = resolve_repo_output_path(repo_root, str(payload["path"]), label="output")
|
|
2413
2413
|
if not args.dry_run:
|
|
2414
2414
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2415
2415
|
out_path.write_text(payload["content"], encoding="utf-8")
|
|
2416
2416
|
payload["written"] = True
|
|
2417
2417
|
else:
|
|
2418
2418
|
payload["written"] = False
|
|
2419
|
-
payload["output_path"] =
|
|
2419
|
+
payload["output_path"] = output_path
|
|
2420
2420
|
else:
|
|
2421
2421
|
payload["written"] = False
|
|
2422
2422
|
if args.format == "json":
|
|
@@ -2455,16 +2455,16 @@ def cmd_backlog_groom(args: argparse.Namespace) -> dict[str, object]:
|
|
|
2455
2455
|
**_build_backlog_groom(repo_root, args.ref),
|
|
2456
2456
|
}
|
|
2457
2457
|
if args.execution_mode == "execute":
|
|
2458
|
-
out_path = repo_root
|
|
2458
|
+
out_path, output_path = resolve_repo_output_path(repo_root, str(payload["path"]), label="output")
|
|
2459
2459
|
if not args.dry_run:
|
|
2460
2460
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2461
2461
|
out_path.write_text(payload["content"], encoding="utf-8")
|
|
2462
2462
|
payload["written"] = True
|
|
2463
|
-
request_path = repo_root
|
|
2463
|
+
request_path, _request_output_path = resolve_repo_output_path(repo_root, str(payload["request_path"]), label="request_path")
|
|
2464
2464
|
_append_section_bullets(request_path, "Backlog", [f"`{payload['ref']}`"], dry_run=False)
|
|
2465
2465
|
else:
|
|
2466
2466
|
payload["written"] = False
|
|
2467
|
-
payload["output_path"] =
|
|
2467
|
+
payload["output_path"] = output_path
|
|
2468
2468
|
else:
|
|
2469
2469
|
payload["written"] = False
|
|
2470
2470
|
if args.format == "json":
|
|
@@ -2513,6 +2513,34 @@ def cmd_closure_summary(args: argparse.Namespace) -> dict[str, object]:
|
|
|
2513
2513
|
return payload
|
|
2514
2514
|
|
|
2515
2515
|
|
|
2516
|
+
def cmd_handoff(args: argparse.Namespace) -> dict[str, object]:
|
|
2517
|
+
repo_root = find_repo_root(Path.cwd())
|
|
2518
|
+
config, config_path = load_repo_config(repo_root)
|
|
2519
|
+
payload = {
|
|
2520
|
+
"command": "assist",
|
|
2521
|
+
"kind": "handoff",
|
|
2522
|
+
"repo_root": repo_root.as_posix(),
|
|
2523
|
+
"config_path": str(config_path.relative_to(repo_root)) if config_path is not None else None,
|
|
2524
|
+
**_build_handoff(repo_root, args.since),
|
|
2525
|
+
}
|
|
2526
|
+
if args.format == "json":
|
|
2527
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
2528
|
+
else:
|
|
2529
|
+
print(f"Handoff since {payload['since']}:")
|
|
2530
|
+
print(f"- commits: {payload['commit_count']}")
|
|
2531
|
+
print(f"- changed paths: {len(payload['changed_paths'])}")
|
|
2532
|
+
print(f"- primary surface: {payload['surface']['primary_category']}")
|
|
2533
|
+
for commit in payload["commits"][:8]:
|
|
2534
|
+
print(f"- commit: {commit['commit']} {commit['subject']}")
|
|
2535
|
+
for doc in payload["logics_docs"][:8]:
|
|
2536
|
+
print(f"- logics: {doc['ref']} [{doc['status']}] {doc['path']}")
|
|
2537
|
+
for validation in payload["validations"][:8]:
|
|
2538
|
+
print(f"- validation: {validation}")
|
|
2539
|
+
for action in payload["next_actions"]:
|
|
2540
|
+
print(f"- next: {action}")
|
|
2541
|
+
return payload
|
|
2542
|
+
|
|
2543
|
+
|
|
2516
2544
|
def cmd_context(args: argparse.Namespace) -> dict[str, object]:
|
|
2517
2545
|
repo_root = find_repo_root(Path.cwd())
|
|
2518
2546
|
config, config_path = load_repo_config(repo_root)
|
|
@@ -2554,13 +2582,16 @@ def cmd_context(args: argparse.Namespace) -> dict[str, object]:
|
|
|
2554
2582
|
}
|
|
2555
2583
|
|
|
2556
2584
|
if args.out:
|
|
2557
|
-
out_path = (repo_root
|
|
2585
|
+
out_path, output_path = resolve_repo_output_path(repo_root, args.out)
|
|
2586
|
+
payload["output_path"] = output_path
|
|
2558
2587
|
serialized = json.dumps(payload, indent=2, sort_keys=True) + "\n"
|
|
2559
2588
|
if not args.dry_run:
|
|
2560
2589
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2561
2590
|
out_path.write_text(serialized, encoding="utf-8")
|
|
2562
|
-
|
|
2563
|
-
|
|
2591
|
+
if args.format == "json":
|
|
2592
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
2593
|
+
else:
|
|
2594
|
+
print(f"Wrote {output_path}")
|
|
2564
2595
|
elif args.format == "json":
|
|
2565
2596
|
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
2566
2597
|
else:
|
|
@@ -2576,7 +2607,7 @@ def main(argv: list[str]) -> int:
|
|
|
2576
2607
|
if not argv or argv[0] in HELP_FLAGS:
|
|
2577
2608
|
_print_help(_build_help())
|
|
2578
2609
|
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:
|
|
2610
|
+
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
2611
|
_print_help(_build_command_help(argv[0]))
|
|
2581
2612
|
return 0
|
|
2582
2613
|
parser = build_parser()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from .assist_surface import build_changed_surface_summary
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _git_lines(repo_root: Path, args: list[str]) -> list[str]:
|
|
10
|
+
try:
|
|
11
|
+
completed = subprocess.run(
|
|
12
|
+
["git", *args],
|
|
13
|
+
cwd=repo_root,
|
|
14
|
+
stdout=subprocess.PIPE,
|
|
15
|
+
stderr=subprocess.DEVNULL,
|
|
16
|
+
text=True,
|
|
17
|
+
check=False,
|
|
18
|
+
)
|
|
19
|
+
except OSError:
|
|
20
|
+
return []
|
|
21
|
+
if completed.returncode != 0:
|
|
22
|
+
return []
|
|
23
|
+
return [line.strip() for line in completed.stdout.splitlines() if line.strip()]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _git_range_changed_paths(repo_root: Path, since: str) -> list[str]:
|
|
27
|
+
return sorted(set(_git_lines(repo_root, ["diff", "--name-only", f"{since}..HEAD"])))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _git_range_commits(repo_root: Path, since: str) -> list[dict[str, str]]:
|
|
31
|
+
commits: list[dict[str, str]] = []
|
|
32
|
+
for line in _git_lines(repo_root, ["log", "--oneline", f"{since}..HEAD"]):
|
|
33
|
+
commit, _, subject = line.partition(" ")
|
|
34
|
+
commits.append({"commit": commit, "subject": subject})
|
|
35
|
+
return commits
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _section_lines(lines: list[str], heading: str) -> list[str]:
|
|
39
|
+
start_idx = None
|
|
40
|
+
target = heading.strip().lower()
|
|
41
|
+
for idx, line in enumerate(lines):
|
|
42
|
+
if line.startswith("# ") and line[2:].strip().lower() == target:
|
|
43
|
+
start_idx = idx + 1
|
|
44
|
+
break
|
|
45
|
+
if start_idx is None:
|
|
46
|
+
return []
|
|
47
|
+
out: list[str] = []
|
|
48
|
+
for idx in range(start_idx, len(lines)):
|
|
49
|
+
line = lines[idx]
|
|
50
|
+
if line.startswith("# "):
|
|
51
|
+
break
|
|
52
|
+
out.append(line)
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _doc_status(path: Path) -> str:
|
|
57
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
58
|
+
stripped = line.strip()
|
|
59
|
+
if stripped.startswith("> Status:"):
|
|
60
|
+
return stripped.split(":", 1)[1].strip()
|
|
61
|
+
return "Unknown"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _doc_title_from_path(path: Path) -> str:
|
|
65
|
+
try:
|
|
66
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
67
|
+
except OSError:
|
|
68
|
+
return path.stem
|
|
69
|
+
for line in lines:
|
|
70
|
+
if line.startswith("## "):
|
|
71
|
+
payload = line.removeprefix("## ").strip()
|
|
72
|
+
if " - " in payload:
|
|
73
|
+
return payload.split(" - ", 1)[1].strip()
|
|
74
|
+
return payload
|
|
75
|
+
return path.stem
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _validation_lines_from_task(path: Path) -> list[str]:
|
|
79
|
+
try:
|
|
80
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
81
|
+
except OSError:
|
|
82
|
+
return []
|
|
83
|
+
values: list[str] = []
|
|
84
|
+
for line in _section_lines(lines, "Validation"):
|
|
85
|
+
stripped = line.strip()
|
|
86
|
+
if not stripped.startswith("- "):
|
|
87
|
+
continue
|
|
88
|
+
value = stripped[2:].strip()
|
|
89
|
+
if value and not value.lower().startswith("run `") and not value.lower().startswith("run the "):
|
|
90
|
+
values.append(value)
|
|
91
|
+
return values
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_handoff(repo_root: Path, since: str) -> dict[str, object]:
|
|
95
|
+
changed_paths = _git_range_changed_paths(repo_root, since)
|
|
96
|
+
commits = _git_range_commits(repo_root, since)
|
|
97
|
+
surface = build_changed_surface_summary(changed_paths)
|
|
98
|
+
logics_docs: list[dict[str, object]] = []
|
|
99
|
+
validations: list[str] = []
|
|
100
|
+
for rel_path in changed_paths:
|
|
101
|
+
if not rel_path.startswith("logics/") or not rel_path.endswith(".md"):
|
|
102
|
+
continue
|
|
103
|
+
path = repo_root / rel_path
|
|
104
|
+
kind = path.parent.name
|
|
105
|
+
entry = {
|
|
106
|
+
"path": rel_path,
|
|
107
|
+
"ref": path.stem,
|
|
108
|
+
"kind": kind,
|
|
109
|
+
"title": _doc_title_from_path(path),
|
|
110
|
+
"status": _doc_status(path) if path.is_file() else "Unknown",
|
|
111
|
+
}
|
|
112
|
+
logics_docs.append(entry)
|
|
113
|
+
if kind == "tasks":
|
|
114
|
+
validations.extend(_validation_lines_from_task(path))
|
|
115
|
+
next_actions = [
|
|
116
|
+
"Run lint/audit if not already included in validation evidence.",
|
|
117
|
+
"Review changed files before committing or handing off.",
|
|
118
|
+
]
|
|
119
|
+
if any(path.startswith("logics_manager/") for path in changed_paths):
|
|
120
|
+
next_actions.append("Run `PYTHONPATH=\"$PWD\" pytest tests/python -q` for Python CLI changes.")
|
|
121
|
+
if any(path.startswith("clients/vscode/src/") or path.startswith("clients/shared-web/media/") for path in changed_paths):
|
|
122
|
+
next_actions.append("Run the TypeScript/vitest checks for extension changes.")
|
|
123
|
+
return {
|
|
124
|
+
"since": since,
|
|
125
|
+
"commit_count": len(commits),
|
|
126
|
+
"commits": commits,
|
|
127
|
+
"changed_paths": changed_paths,
|
|
128
|
+
"surface": surface,
|
|
129
|
+
"logics_docs": logics_docs,
|
|
130
|
+
"validations": sorted(set(validations)),
|
|
131
|
+
"next_actions": next_actions,
|
|
132
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_changed_surface_summary(changed_paths: list[str]) -> dict[str, object]:
|
|
7
|
+
category_counter: Counter[str] = Counter()
|
|
8
|
+
for path in changed_paths:
|
|
9
|
+
normalized = path.replace("\\", "/")
|
|
10
|
+
if normalized.startswith("clients/vscode/src/") or normalized.startswith("clients/shared-web/media/"):
|
|
11
|
+
category_counter["plugin"] += 1
|
|
12
|
+
elif normalized.startswith("logics_manager/"):
|
|
13
|
+
category_counter["python-runtime"] += 1
|
|
14
|
+
elif normalized.startswith("logics/"):
|
|
15
|
+
category_counter["workflow-docs"] += 1
|
|
16
|
+
elif normalized.startswith("tests/") or "/tests/" in normalized or normalized.startswith("tests/python/"):
|
|
17
|
+
category_counter["tests"] += 1
|
|
18
|
+
elif normalized.endswith(".md"):
|
|
19
|
+
category_counter["docs"] += 1
|
|
20
|
+
else:
|
|
21
|
+
category_counter["other"] += 1
|
|
22
|
+
primary = category_counter.most_common(1)[0][0] if category_counter else "clean"
|
|
23
|
+
summary = {
|
|
24
|
+
"clean": "No changed surface was detected.",
|
|
25
|
+
"plugin": "The plugin surface is the dominant change area.",
|
|
26
|
+
"python-runtime": "The native Python runtime is the dominant change area.",
|
|
27
|
+
"workflow-docs": "Workflow documentation is the dominant change area.",
|
|
28
|
+
"tests": "Tests are the dominant change area.",
|
|
29
|
+
"docs": "Markdown documentation is the dominant change area.",
|
|
30
|
+
"other": "Mixed repository changes are present.",
|
|
31
|
+
}.get(primary, "Mixed repository changes are present.")
|
|
32
|
+
return {
|
|
33
|
+
"summary": summary,
|
|
34
|
+
"primary_category": primary,
|
|
35
|
+
"counts": dict(sorted(category_counter.items())),
|
|
36
|
+
"changed_paths": changed_paths,
|
|
37
|
+
"review_recommended": primary not in {"clean", "docs"} and bool(changed_paths),
|
|
38
|
+
}
|