@xenonbyte/req-2-plan 0.4.1 → 0.4.2
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/package.json +1 -1
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-start.md +1 -1
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-start/SKILL.md +1 -1
- package/tools/workflow_cli/context_pack.py +34 -10
- package/tools/workflow_cli/gates.py +33 -13
- package/tools/workflow_cli/version.py +1 -1
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ description: Start a new requirement-to-PLAN workflow run
|
|
|
3
3
|
---
|
|
4
4
|
Run `{{R2P_BIN_DIR}}/r2p-start` with the requirement as a positional argument.
|
|
5
5
|
|
|
6
|
-
Usage: `{{R2P_BIN_DIR}}/r2p-start [--separate] ("<raw requirement>" | --file <path>)`
|
|
6
|
+
Usage: `{{R2P_BIN_DIR}}/r2p-start [--separate] [--repo-path <dir>] ("<raw requirement>" | --file <path>)`
|
|
7
7
|
|
|
8
8
|
To start from a requirement document, pass `--file <path>` instead of inline text — r2p reads the file contents as the requirement (the path itself is never stored as the requirement): `{{R2P_BIN_DIR}}/r2p-start [--separate] --file ./requirement.md`. `--file` and a positional requirement are mutually exclusive.
|
|
9
9
|
|
|
@@ -7,7 +7,7 @@ description: Start a new requirement-to-PLAN workflow run from a raw requirement
|
|
|
7
7
|
|
|
8
8
|
Run `{{R2P_BIN_DIR}}/r2p-start` with the requirement as a positional argument.
|
|
9
9
|
|
|
10
|
-
Usage: `{{R2P_BIN_DIR}}/r2p-start [--separate] ("<raw requirement>" | --file <path>)`
|
|
10
|
+
Usage: `{{R2P_BIN_DIR}}/r2p-start [--separate] [--repo-path <dir>] ("<raw requirement>" | --file <path>)`
|
|
11
11
|
|
|
12
12
|
To start from a requirement document, pass `--file <path>` instead of inline text — r2p reads the file contents as the requirement (the path itself is never stored as the requirement): `{{R2P_BIN_DIR}}/r2p-start [--separate] --file ./requirement.md`. `--file` and a positional requirement are mutually exclusive.
|
|
13
13
|
|
|
@@ -27,6 +27,18 @@ class ProjectContextPack:
|
|
|
27
27
|
source_dirs: list = field(default_factory=list)
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def _append_npm_dependencies(pack: ProjectContextPack, dependencies: object, *, dev: bool = False) -> None:
|
|
31
|
+
if not isinstance(dependencies, dict):
|
|
32
|
+
return
|
|
33
|
+
for name, version in dependencies.items():
|
|
34
|
+
if not isinstance(name, str) or not isinstance(version, str):
|
|
35
|
+
continue
|
|
36
|
+
dep = {"name": name, "version": version, "ecosystem": "npm"}
|
|
37
|
+
if dev:
|
|
38
|
+
dep["dev"] = True
|
|
39
|
+
pack.dependencies.append(dep)
|
|
40
|
+
|
|
41
|
+
|
|
30
42
|
def build_context_pack(repo_path: Path) -> ProjectContextPack:
|
|
31
43
|
repo_path = Path(repo_path).resolve()
|
|
32
44
|
baseline = scan_repo_baseline(repo_path)
|
|
@@ -41,15 +53,8 @@ def build_context_pack(repo_path: Path) -> ProjectContextPack:
|
|
|
41
53
|
scripts = data.get("scripts")
|
|
42
54
|
if isinstance(scripts, dict) and scripts.get("test"):
|
|
43
55
|
pack.test_commands.append("npm test")
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
for name, ver in dependencies.items():
|
|
47
|
-
pack.dependencies.append({"name": name, "version": ver, "ecosystem": "npm"})
|
|
48
|
-
dev_dependencies = data.get("devDependencies")
|
|
49
|
-
if isinstance(dev_dependencies, dict):
|
|
50
|
-
for name, ver in dev_dependencies.items():
|
|
51
|
-
pack.dependencies.append(
|
|
52
|
-
{"name": name, "version": ver, "ecosystem": "npm", "dev": True})
|
|
56
|
+
_append_npm_dependencies(pack, data.get("dependencies"))
|
|
57
|
+
_append_npm_dependencies(pack, data.get("devDependencies"), dev=True)
|
|
53
58
|
except (ValueError, OSError):
|
|
54
59
|
pass
|
|
55
60
|
|
|
@@ -84,6 +89,25 @@ def to_json(pack: ProjectContextPack) -> str:
|
|
|
84
89
|
return json.dumps(asdict(pack), indent=2, ensure_ascii=False)
|
|
85
90
|
|
|
86
91
|
|
|
92
|
+
_DEP_DISPLAY_CAP = 20
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _dependencies_markdown(dependencies: list) -> str:
|
|
96
|
+
"""Itemize dependencies (name, version, ecosystem, dev flag), capped to keep the seed small."""
|
|
97
|
+
if not dependencies:
|
|
98
|
+
return "- dependencies (0): none\n"
|
|
99
|
+
lines = [f"- dependencies ({len(dependencies)}):\n"]
|
|
100
|
+
for dep in dependencies[:_DEP_DISPLAY_CAP]:
|
|
101
|
+
label = " ".join(part for part in (dep.get("name", ""), dep.get("version", "")) if part)
|
|
102
|
+
ecosystem = dep.get("ecosystem", "")
|
|
103
|
+
suffix = f"{ecosystem}, dev" if dep.get("dev") else ecosystem
|
|
104
|
+
lines.append(f" - {label} ({suffix})\n")
|
|
105
|
+
hidden = len(dependencies) - _DEP_DISPLAY_CAP
|
|
106
|
+
if hidden > 0:
|
|
107
|
+
lines.append(f" - … and {hidden} more\n")
|
|
108
|
+
return "".join(lines)
|
|
109
|
+
|
|
110
|
+
|
|
87
111
|
def to_markdown(pack: ProjectContextPack) -> str:
|
|
88
112
|
return (
|
|
89
113
|
"# Project Context Pack\n\n"
|
|
@@ -93,7 +117,7 @@ def to_markdown(pack: ProjectContextPack) -> str:
|
|
|
93
117
|
f"- test_commands: {pack.test_commands or 'none'}\n"
|
|
94
118
|
f"- entrypoints: {pack.entrypoints or 'none'}\n"
|
|
95
119
|
f"- config_files: {pack.config_files or 'none'}\n"
|
|
96
|
-
f"
|
|
120
|
+
f"{_dependencies_markdown(pack.dependencies)}"
|
|
97
121
|
f"- source_dirs: {pack.source_dirs}\n"
|
|
98
122
|
)
|
|
99
123
|
|
|
@@ -290,14 +290,6 @@ def _find_next_plan_task_field_start(task_body: str, after: int) -> int | None:
|
|
|
290
290
|
return None
|
|
291
291
|
|
|
292
292
|
|
|
293
|
-
def _plan_task_field_value(task_body: str, field: str) -> str:
|
|
294
|
-
found = _find_plan_task_field(task_body, field)
|
|
295
|
-
if found is None:
|
|
296
|
-
return ""
|
|
297
|
-
match, _ = found
|
|
298
|
-
return match.group(1).strip()
|
|
299
|
-
|
|
300
|
-
|
|
301
293
|
def _iter_plan_task_bodies(content: str):
|
|
302
294
|
return heading_bounded_bodies(content, _PLAN_TASK_RE.match)
|
|
303
295
|
|
|
@@ -496,8 +488,10 @@ def _check_spec_refs_valid(run_dir: Path, content: str) -> list[str]:
|
|
|
496
488
|
|
|
497
489
|
|
|
498
490
|
# R10: Change Type is a closed operation-kind enum; 'new' is a legacy alias.
|
|
499
|
-
|
|
491
|
+
_FILE_CHANGE_TYPES = frozenset({"create", "modify", "delete"})
|
|
492
|
+
_CHANGE_TYPE_VALUES = _FILE_CHANGE_TYPES | {"non_code"}
|
|
500
493
|
_CHANGE_TYPE_ALIASES = {"new": "create"}
|
|
494
|
+
_TDD_APPLICABLE_VALUES = frozenset({"yes", "no"})
|
|
501
495
|
|
|
502
496
|
|
|
503
497
|
def _normalized_change_type(raw: str) -> str:
|
|
@@ -510,6 +504,11 @@ def _task_change_type(body: str) -> str:
|
|
|
510
504
|
return " ".join(_plan_task_field_body(body, "Change Type").split())
|
|
511
505
|
|
|
512
506
|
|
|
507
|
+
def _task_tdd_applicable(body: str) -> str:
|
|
508
|
+
"""Whitespace-normalized TDD Applicable field body (same line + continuation lines)."""
|
|
509
|
+
return " ".join(_plan_task_field_body(body, "TDD Applicable").split())
|
|
510
|
+
|
|
511
|
+
|
|
513
512
|
def _check_plan_task_fields(content: str) -> list[str]:
|
|
514
513
|
issues: list[str] = []
|
|
515
514
|
numbers: list[int] = []
|
|
@@ -523,10 +522,32 @@ def _check_plan_task_fields(content: str) -> list[str]:
|
|
|
523
522
|
if not _plan_task_field_body(body, field).strip():
|
|
524
523
|
issues.append(f"{label} is missing a non-empty '{field}:' field.")
|
|
525
524
|
raw_change_type = _task_change_type(body)
|
|
526
|
-
|
|
525
|
+
change_type = _normalized_change_type(raw_change_type)
|
|
526
|
+
if raw_change_type and change_type not in _CHANGE_TYPE_VALUES:
|
|
527
527
|
issues.append(
|
|
528
528
|
f"{label} has invalid 'Change Type: {raw_change_type}'; "
|
|
529
|
-
"allowed: create|modify|delete (alias: new = create)."
|
|
529
|
+
"allowed: create|modify|delete|non_code (alias: new = create)."
|
|
530
|
+
)
|
|
531
|
+
# R16: Change Type and Files must agree — file-op types need a real
|
|
532
|
+
# path; non_code is the only legal shape for no-file tasks.
|
|
533
|
+
files_body = _plan_task_field_body(body, "Files")
|
|
534
|
+
if files_body.strip():
|
|
535
|
+
file_paths = _plan_task_file_paths(files_body)
|
|
536
|
+
if change_type in _FILE_CHANGE_TYPES and not file_paths:
|
|
537
|
+
issues.append(
|
|
538
|
+
f"{label} has 'Change Type: {raw_change_type}' but 'Files:' lists "
|
|
539
|
+
"no real file path; use 'Change Type: non_code' for tasks that "
|
|
540
|
+
"touch no files."
|
|
541
|
+
)
|
|
542
|
+
elif change_type == "non_code" and file_paths:
|
|
543
|
+
issues.append(
|
|
544
|
+
f"{label} has 'Change Type: non_code' but 'Files:' lists a file path; "
|
|
545
|
+
"use create|modify|delete for file-touching tasks."
|
|
546
|
+
)
|
|
547
|
+
raw_tdd = _task_tdd_applicable(body)
|
|
548
|
+
if raw_tdd and raw_tdd.lower() not in _TDD_APPLICABLE_VALUES:
|
|
549
|
+
issues.append(
|
|
550
|
+
f"{label} has invalid 'TDD Applicable: {raw_tdd}'; allowed: yes|no."
|
|
530
551
|
)
|
|
531
552
|
if numbers:
|
|
532
553
|
if len(set(numbers)) != len(numbers):
|
|
@@ -575,8 +596,7 @@ def _plan_tasks_missing_code(content: str) -> bool:
|
|
|
575
596
|
return False
|
|
576
597
|
for body in bodies:
|
|
577
598
|
skeleton = _plan_task_field_body(body, "Skeleton")
|
|
578
|
-
|
|
579
|
-
if tdd_applicable.lower() == "yes" and not _has_complete_code_fence(skeleton):
|
|
599
|
+
if _task_tdd_applicable(body).lower() == "yes" and not _has_complete_code_fence(skeleton):
|
|
580
600
|
return True
|
|
581
601
|
return False
|
|
582
602
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
R2P_VERSION = "0.4.
|
|
1
|
+
R2P_VERSION = "0.4.2"
|