← Files taskplaneARCHIVED FILE
taskplane/tp.py
8.87 KB · Oct 2, 2026 · 00:29 UTC
#!/usr/bin/env python3
"""Taskplane: human-gated delivery, native observations, graph and dashboard."""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import subprocess
import sys
from typing import Any
if sys.version_info < (3, 10):
raise SystemExit("Taskplane requires Python 3.10 or newer")
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "taskplane"))
from taskplane import depgraph, flow, graph_primitives, primitives, storage, workspace_binding
def _git(workspace: str, *args: str) -> str:
result = subprocess.run(["git", *args], cwd=workspace, text=True,
encoding="utf-8", errors="replace", capture_output=True)
if result.returncode:
raise ValueError(result.stderr.strip() or "Git could not read this workspace")
return result.stdout
def _changed(workspace: str, base: str) -> list[str]:
return sorted(set(_git_paths(workspace, "diff", "--name-only", "-z", base, "--")
+ _git_paths(workspace, "ls-files", "-z", "--others", "--exclude-standard")))
def _git_paths(workspace: str, *args: str) -> list[str]:
result = subprocess.run(["git", *args], cwd=workspace, capture_output=True)
if result.returncode:
raise ValueError(os.fsdecode(result.stderr).strip() or "Git could not read this workspace")
return [os.fsdecode(path) for path in result.stdout.split(b"\0") if path]
def _version(verify: bool) -> dict[str, Any]:
manifests = [p for p in (ROOT / ".codex-plugin/plugin.json", ROOT / ".claude-plugin/plugin.json") if p.is_file()]
versions = [json.loads(p.read_text())["version"] for p in manifests]
market = ROOT / ".claude-plugin/marketplace.json"
if market.is_file():
data = json.loads(market.read_text())
versions.extend([data["version"], data["plugins"][0]["version"]])
result: dict[str, Any] = {"version": versions[0]}
if verify:
result["ok"] = len(set(versions)) == 1
return result
def main(argv: list[str] | None = None) -> int:
arguments = list(sys.argv[1:] if argv is None else argv)
if arguments[:1] == ["workspace"]:
return workspace_binding.main(arguments[1:])
if arguments[:1] == ["flow"]:
return flow.main(arguments[1:], compact=True)
if arguments and arguments[0] in flow.HOOK_NAMES:
if len(arguments) != 1:
print(json.dumps({"error": "Named native hooks accept their event on stdin only."}))
return 2
return flow.run_hook(arguments[0])
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
commands.add_parser("flow", help="Human-gated delivery and observations; use flow --help")
commands.add_parser("workspace", help="Inspect, bind or recover the selected workspace; use workspace --help")
version = commands.add_parser("version", help="Report the installed version")
version.add_argument("--verify", action="store_true")
help_command = commands.add_parser("help", help="Show the supported commands")
help_command.add_argument("--md", action="store_true")
graph = commands.add_parser("graph", help="Scan, inspect, and render source dependencies")
graph.add_argument("--workspace")
actions = graph.add_subparsers(dest="action", required=True)
scan = actions.add_parser("scan")
scan.add_argument("--decompose", action="store_true")
scan.add_argument("--strict", action="store_true")
impact = actions.add_parser("impact")
impact.add_argument("--files")
impact.add_argument("--base", default="HEAD")
impact.add_argument("--depth", type=int, default=3)
impact.add_argument("--json", action="store_true")
html = actions.add_parser("html")
html.add_argument("--files")
html.add_argument("--base", default="HEAD")
html.add_argument("--out")
html.add_argument("--focus")
edge = actions.add_parser("edge")
edge.add_argument("src")
edge.add_argument("dst")
edge.add_argument("--kind", default="runtime")
edge.add_argument("--note", default="")
edge.add_argument("--confidence", default="high")
board = commands.add_parser("dashboard", help="Render the shared delivery dashboard")
board.add_argument("--workspace")
board.add_argument("--out")
board.add_argument("--run")
review = commands.add_parser("review", help="Capture a source inventory for native review")
review.add_argument("action", choices=["start"])
review.add_argument("--workspace")
review.add_argument("--scope", choices=["repository", "diff"], default="diff")
review.add_argument("--base", default="HEAD")
review.add_argument("--paths", help="Comma-separated source paths")
lens = commands.add_parser("lens", help="Suggest lenses from source signals")
lens.add_argument("--workspace")
lens.add_argument("--files", required=True)
lens.add_argument("--stage")
args = parser.parse_args(arguments)
try:
if args.command == "help":
if args.md:
print((ROOT / "docs/cli-reference.md").read_text(), end="")
else:
parser.print_help()
return 0
if args.command == "version":
result = _version(args.verify)
print(json.dumps(result, indent=2))
return 0 if result.get("ok", True) else 1
workspace = str(workspace_binding.resolve_workspace(args.workspace))
workspace_binding.ensure(workspace)
if args.command == "graph":
if args.action == "scan":
data = depgraph.scan(workspace, decompose=args.decompose)
flow.record_scan(Path(workspace), data)
result = {"modules": len(data["modules"]), "edges": len(data["edges"]),
"files": len(data["files"]), "stored": depgraph._path(workspace)}
if args.decompose:
result["components"] = len(data.get("components", []))
quality = depgraph.scan_quality(data)
if quality.get("degraded"):
result["graph_quality"] = quality
print(json.dumps(result, indent=2))
return int(args.strict and bool(quality.get("degraded")))
if args.action == "edge":
result = depgraph.record_edge(workspace, args.src, args.dst,
kind=args.kind, note=args.note, confidence=args.confidence)
else:
files = args.files.split(",") if args.files else _changed(workspace, args.base)
if args.action == "impact":
result = depgraph.impact(workspace, files, max_depth=args.depth)
else:
if not depgraph.load(workspace).get("modules"):
raise ValueError("No graph recorded; run graph scan first")
print(depgraph.to_html(workspace, files, out=args.out, focus=args.focus))
return 0
elif args.command == "dashboard":
output = flow.publish_dashboard(Path(workspace), args.run,
output=Path(args.out) if args.out else None, select=bool(args.run))
print(str(output))
return 0
elif args.command == "lens":
result = graph_primitives.route_verdicts(workspace, args.files.split(","), stage=args.stage)
else:
head = _git(workspace, "rev-parse", "HEAD").strip()
if args.scope == "repository":
if _git(workspace, "diff", "HEAD", "--").strip():
raise ValueError("Tracked source differs from HEAD; use a diff review")
files = _git_paths(workspace, "ls-files", "-z")
else:
files = _changed(workspace, args.base)
if args.paths:
wanted = args.paths.split(",")
files = [p for p in files if p in wanted]
files = [p for p in files if not p.startswith(".taskplane/")]
for filename in files:
(Path(workspace) / filename).resolve().relative_to(Path(workspace))
if not files:
raise ValueError("No selected source; use --scope repository to review tracked source")
patch = "" if args.scope == "repository" else _git(workspace, "--literal-pathspecs", "diff", args.base, "--", *files)
source = {"head": head, "scope": args.scope, "base": args.base,
"files": files, "patch": patch}
output = Path(storage.tp_dir(workspace)) / "review-source.json"
primitives.atomic_json(output, source)
result = {"status": "ready", "source": str(output), "files": len(files)}
print(json.dumps(result, indent=2))
return 0
except (OSError, ValueError, KeyError, primitives.StateError) as exc:
print(f"taskplane: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: 34c4c3d764d0d91d51acfdb14649b0e84297d61bd6fbefc8529c9be8053201b6