@xenonbyte/req-2-plan 0.2.3
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/LICENSE +21 -0
- package/README.md +172 -0
- package/README.zh-CN.md +158 -0
- package/bin/r2p.js +38 -0
- package/docs/req-to-plan-design.md +277 -0
- package/package.json +47 -0
- package/requirements.txt +1 -0
- package/tools/r2p +10 -0
- package/tools/r2p-continue +10 -0
- package/tools/r2p-gap-open +10 -0
- package/tools/r2p-gap-resolve +10 -0
- package/tools/r2p-reopen +10 -0
- package/tools/r2p-start +10 -0
- package/tools/r2p-status +10 -0
- package/tools/r2p-switch +10 -0
- package/tools/r2p-tier-lock +10 -0
- package/tools/workflow_cli/__init__.py +0 -0
- package/tools/workflow_cli/__main__.py +5 -0
- package/tools/workflow_cli/agent_shortcuts.py +778 -0
- package/tools/workflow_cli/agent_templates/claude/SKILL.md +34 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-continue.md +16 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-gap-open.md +8 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-gap-resolve.md +8 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-reopen.md +8 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-start.md +10 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-status.md +8 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-switch.md +8 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-tier-lock.md +8 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-continue/SKILL.md +12 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-gap-open/SKILL.md +12 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-gap-resolve/SKILL.md +12 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-reopen/SKILL.md +12 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-start/SKILL.md +14 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-status/SKILL.md +12 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-switch/SKILL.md +12 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-tier-lock/SKILL.md +12 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-continue.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-gap-open.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-gap-resolve.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-reopen.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-start.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-status.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-switch.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-tier-lock.toml +4 -0
- package/tools/workflow_cli/artifact.py +228 -0
- package/tools/workflow_cli/cli.py +1779 -0
- package/tools/workflow_cli/gates.py +471 -0
- package/tools/workflow_cli/install.py +900 -0
- package/tools/workflow_cli/install_cli.py +158 -0
- package/tools/workflow_cli/link_expander.py +102 -0
- package/tools/workflow_cli/models.py +504 -0
- package/tools/workflow_cli/output.py +91 -0
- package/tools/workflow_cli/repo_baseline.py +137 -0
- package/tools/workflow_cli/state.py +621 -0
- package/tools/workflow_cli/tier.py +201 -0
- package/tools/workflow_cli/tier_keywords.yaml +45 -0
- package/tools/workflow_cli/version.py +1 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
r2p lifecycle binary — install/uninstall/status CLI.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(args=None):
|
|
13
|
+
parser = argparse.ArgumentParser(prog="r2p", description="r2p lifecycle binary")
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"--version", "-v", action="store_true", help="Print r2p version and exit"
|
|
16
|
+
)
|
|
17
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
18
|
+
|
|
19
|
+
# install
|
|
20
|
+
p_install = subparsers.add_parser(
|
|
21
|
+
"install", help="Install agent integration templates (all platforms by default)"
|
|
22
|
+
)
|
|
23
|
+
p_install.add_argument(
|
|
24
|
+
"--platform",
|
|
25
|
+
default=None,
|
|
26
|
+
help="Platform or comma-separated platform list (default: all)",
|
|
27
|
+
)
|
|
28
|
+
p_install.set_defaults(func=_cmd_install)
|
|
29
|
+
|
|
30
|
+
# uninstall
|
|
31
|
+
p_uninstall = subparsers.add_parser(
|
|
32
|
+
"uninstall", help="Uninstall agent integration (all platforms by default)"
|
|
33
|
+
)
|
|
34
|
+
p_uninstall.add_argument(
|
|
35
|
+
"--platform",
|
|
36
|
+
default=None,
|
|
37
|
+
help="Platform or comma-separated platform list (default: all)",
|
|
38
|
+
)
|
|
39
|
+
p_uninstall.set_defaults(func=_cmd_uninstall)
|
|
40
|
+
|
|
41
|
+
# status
|
|
42
|
+
p_status = subparsers.add_parser(
|
|
43
|
+
"status", help="Report install status per platform (read-only)"
|
|
44
|
+
)
|
|
45
|
+
p_status.add_argument("--json", action="store_true", help="Machine-readable JSON output")
|
|
46
|
+
p_status.set_defaults(func=_cmd_status)
|
|
47
|
+
|
|
48
|
+
# version
|
|
49
|
+
p_version = subparsers.add_parser("version", help="Print r2p version")
|
|
50
|
+
p_version.set_defaults(func=_cmd_version)
|
|
51
|
+
|
|
52
|
+
# help
|
|
53
|
+
subparsers.add_parser("help", help="Print this command list")
|
|
54
|
+
|
|
55
|
+
parsed = parser.parse_args(args)
|
|
56
|
+
if parsed.version:
|
|
57
|
+
_cmd_version(parsed)
|
|
58
|
+
return
|
|
59
|
+
if parsed.command in (None, "help"):
|
|
60
|
+
parser.print_help()
|
|
61
|
+
return
|
|
62
|
+
parsed.func(parsed)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _make_service():
|
|
66
|
+
from tools.workflow_cli.install import InstallService
|
|
67
|
+
|
|
68
|
+
repo_root = Path(__file__).parent.parent.parent
|
|
69
|
+
manifest_root = Path.home() / ".req-to-plan"
|
|
70
|
+
return InstallService(repo_root=repo_root, manifest_root=manifest_root)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _cmd_install(args):
|
|
74
|
+
from tools.workflow_cli.install import SUPPORTED_PLATFORMS
|
|
75
|
+
|
|
76
|
+
service = _make_service()
|
|
77
|
+
platforms = _parse_platforms(args.platform, SUPPORTED_PLATFORMS)
|
|
78
|
+
for platform in platforms:
|
|
79
|
+
try:
|
|
80
|
+
manifest = service.install(platform)
|
|
81
|
+
except ValueError as exc:
|
|
82
|
+
print(f"Error: {exc}")
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
|
|
85
|
+
n = len(manifest.get("installed_paths", []))
|
|
86
|
+
manifest_path = (
|
|
87
|
+
Path.home() / ".req-to-plan" / "install" / f"{platform}.yaml"
|
|
88
|
+
)
|
|
89
|
+
print(
|
|
90
|
+
f"install: platform={platform!r} "
|
|
91
|
+
f"installed_paths={n} "
|
|
92
|
+
f"manifest={manifest_path}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _cmd_uninstall(args):
|
|
97
|
+
from tools.workflow_cli.install import SUPPORTED_PLATFORMS
|
|
98
|
+
|
|
99
|
+
service = _make_service()
|
|
100
|
+
platforms = _parse_platforms(args.platform, SUPPORTED_PLATFORMS)
|
|
101
|
+
for platform in platforms:
|
|
102
|
+
try:
|
|
103
|
+
result = service.uninstall(platform)
|
|
104
|
+
except FileNotFoundError as exc:
|
|
105
|
+
if args.platform is None:
|
|
106
|
+
continue
|
|
107
|
+
print(f"Error: {exc}")
|
|
108
|
+
sys.exit(1)
|
|
109
|
+
print(
|
|
110
|
+
f"uninstall: platform={platform!r} "
|
|
111
|
+
f"removed={len(result['removed'])} paths"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _cmd_status(args):
|
|
116
|
+
service = _make_service()
|
|
117
|
+
reports = service.status()
|
|
118
|
+
if args.json:
|
|
119
|
+
print(json.dumps(reports, indent=2, sort_keys=True))
|
|
120
|
+
return
|
|
121
|
+
if not reports:
|
|
122
|
+
print("status: no platforms installed")
|
|
123
|
+
return
|
|
124
|
+
for r in reports:
|
|
125
|
+
line = f"status: platform={r['platform']!r} state={r['status']}"
|
|
126
|
+
if r.get("r2p_version"):
|
|
127
|
+
line += f" version={r['r2p_version']!r}"
|
|
128
|
+
if r["issues"]:
|
|
129
|
+
line += f" issues={r['issues']}"
|
|
130
|
+
print(line)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _cmd_version(args):
|
|
134
|
+
from tools.workflow_cli.version import R2P_VERSION
|
|
135
|
+
|
|
136
|
+
print(R2P_VERSION)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _parse_platforms(raw: str | None, supported: tuple[str, ...]) -> list[str]:
|
|
140
|
+
if raw is None:
|
|
141
|
+
return list(supported)
|
|
142
|
+
platforms: list[str] = []
|
|
143
|
+
for part in raw.split(","):
|
|
144
|
+
platform = part.strip()
|
|
145
|
+
if platform and platform not in platforms:
|
|
146
|
+
platforms.append(platform)
|
|
147
|
+
if not platforms:
|
|
148
|
+
print("Error: --platform must name at least one platform")
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
invalid = [p for p in platforms if p not in supported]
|
|
151
|
+
if invalid:
|
|
152
|
+
print(f"Error: Unknown platform(s): {invalid!r}. Supported: {supported}")
|
|
153
|
+
sys.exit(1)
|
|
154
|
+
return platforms
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
main()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from urllib import request, error as urllib_error
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LinkStatus(str, Enum):
|
|
10
|
+
REACHABLE = "reachable"
|
|
11
|
+
UNREACHABLE = "unreachable"
|
|
12
|
+
REQUIRES_AUTH = "requires_auth"
|
|
13
|
+
LOCAL_FOUND = "local_found"
|
|
14
|
+
LOCAL_MISSING = "local_missing"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class LinkExpansionResult:
|
|
19
|
+
url: str
|
|
20
|
+
status: LinkStatus
|
|
21
|
+
content_preview: str = ""
|
|
22
|
+
error: str = ""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_URL_PATTERN = re.compile(r'https?://[^\s\)\]\>\"\']+')
|
|
26
|
+
_LOCAL_DOT_PATTERN = re.compile(r'(?:^|[\s\(\[])(\./[^\s\)\]\>\"\']+|\.{2}/[^\s\)\]\>\"\']+)')
|
|
27
|
+
_LOCAL_SUBDIR_PATTERN = re.compile(r'(?:^|[\s\(\[])([a-zA-Z][a-zA-Z0-9_\-]*/[a-zA-Z0-9_\-][a-zA-Z0-9_\-./]*\.md)')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def extract_links(text: str) -> list[str]:
|
|
31
|
+
urls = _URL_PATTERN.findall(text)
|
|
32
|
+
dot_paths = [m.strip() for m in _LOCAL_DOT_PATTERN.findall(text)]
|
|
33
|
+
subdir_paths = [m.strip() for m in _LOCAL_SUBDIR_PATTERN.findall(text)]
|
|
34
|
+
seen: set[str] = set()
|
|
35
|
+
result = []
|
|
36
|
+
for item in urls + dot_paths + subdir_paths:
|
|
37
|
+
if item and item not in seen:
|
|
38
|
+
seen.add(item)
|
|
39
|
+
result.append(item)
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _fetch_url(url: str) -> LinkExpansionResult:
|
|
44
|
+
try:
|
|
45
|
+
req = request.Request(url, headers={"User-Agent": "r2p-link-expander/1.0"})
|
|
46
|
+
with request.urlopen(req, timeout=5) as resp:
|
|
47
|
+
content = resp.read(500).decode("utf-8", errors="replace")
|
|
48
|
+
return LinkExpansionResult(url=url, status=LinkStatus.REACHABLE, content_preview=content)
|
|
49
|
+
except urllib_error.HTTPError as e:
|
|
50
|
+
if e.code in (401, 403):
|
|
51
|
+
return LinkExpansionResult(url=url, status=LinkStatus.REQUIRES_AUTH, error=str(e))
|
|
52
|
+
return LinkExpansionResult(url=url, status=LinkStatus.UNREACHABLE, error=str(e))
|
|
53
|
+
except Exception as e:
|
|
54
|
+
return LinkExpansionResult(url=url, status=LinkStatus.UNREACHABLE, error=str(e))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _expand_local(path_str: str, base_path: Path | None) -> LinkExpansionResult:
|
|
58
|
+
if base_path is not None:
|
|
59
|
+
base = base_path.resolve()
|
|
60
|
+
candidate = (base / path_str).resolve()
|
|
61
|
+
if not candidate.is_relative_to(base):
|
|
62
|
+
return LinkExpansionResult(
|
|
63
|
+
url=path_str,
|
|
64
|
+
status=LinkStatus.LOCAL_MISSING,
|
|
65
|
+
error=f"Local path is outside base path: {candidate}",
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
candidate = Path(path_str).resolve()
|
|
69
|
+
|
|
70
|
+
if candidate.exists() and candidate.is_file():
|
|
71
|
+
try:
|
|
72
|
+
preview = candidate.read_text(encoding="utf-8", errors="ignore")[:500]
|
|
73
|
+
return LinkExpansionResult(url=path_str, status=LinkStatus.LOCAL_FOUND,
|
|
74
|
+
content_preview=preview)
|
|
75
|
+
except (PermissionError, OSError) as e:
|
|
76
|
+
return LinkExpansionResult(url=path_str, status=LinkStatus.LOCAL_MISSING, error=str(e))
|
|
77
|
+
return LinkExpansionResult(url=path_str, status=LinkStatus.LOCAL_MISSING,
|
|
78
|
+
error=f"File not found: {candidate}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def expand_links(
|
|
82
|
+
text: str,
|
|
83
|
+
base_path: Path | None = None,
|
|
84
|
+
fetch_urls: bool = True,
|
|
85
|
+
) -> list[LinkExpansionResult]:
|
|
86
|
+
results = []
|
|
87
|
+
links = extract_links(text)
|
|
88
|
+
seen: set[str] = set()
|
|
89
|
+
for link in links:
|
|
90
|
+
if link in seen:
|
|
91
|
+
continue
|
|
92
|
+
seen.add(link)
|
|
93
|
+
if link.startswith("http://") or link.startswith("https://"):
|
|
94
|
+
if fetch_urls:
|
|
95
|
+
results.append(_fetch_url(link))
|
|
96
|
+
else:
|
|
97
|
+
results.append(LinkExpansionResult(
|
|
98
|
+
url=link, status=LinkStatus.UNREACHABLE, error="URL fetching disabled"
|
|
99
|
+
))
|
|
100
|
+
else:
|
|
101
|
+
results.append(_expand_local(link, base_path))
|
|
102
|
+
return results
|