@xenonbyte/req-2-plan 0.4.5 → 0.5.1
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 +170 -125
- package/README.zh-CN.md +154 -108
- package/package.json +1 -1
- package/tools/r2p-archive +10 -0
- package/tools/r2p-execute +10 -0
- package/tools/workflow_cli/agent_shortcuts.py +295 -21
- package/tools/workflow_cli/agent_templates/claude/SKILL.md +2 -1
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-archive.md +10 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-continue.md +1 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-execute.md +122 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-reopen.md +2 -2
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-archive/SKILL.md +14 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-continue/SKILL.md +2 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-execute/SKILL.md +123 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-reopen/SKILL.md +2 -2
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-archive.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-continue.toml +1 -1
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-execute.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-reopen.toml +1 -1
- package/tools/workflow_cli/atomic.py +6 -1
- package/tools/workflow_cli/cli.py +224 -21
- package/tools/workflow_cli/gates.py +149 -2
- package/tools/workflow_cli/install.py +49 -2
- package/tools/workflow_cli/markdown.py +18 -0
- package/tools/workflow_cli/models.py +19 -1
- package/tools/workflow_cli/stage_templates.py +2 -1
- package/tools/workflow_cli/version.py +1 -1
- package/tools/workflow_cli/workspace.py +112 -0
|
@@ -7,6 +7,9 @@ Usage:
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import argparse
|
|
10
|
+
import contextlib
|
|
11
|
+
import io
|
|
12
|
+
import json
|
|
10
13
|
import re
|
|
11
14
|
import shutil
|
|
12
15
|
import shlex
|
|
@@ -16,7 +19,8 @@ from pathlib import Path
|
|
|
16
19
|
|
|
17
20
|
from tools.workflow_cli.atomic import atomic_write_text
|
|
18
21
|
from tools.workflow_cli.models import RunStatus, WorkId
|
|
19
|
-
from tools.workflow_cli.output import EXIT_CONFLICT
|
|
22
|
+
from tools.workflow_cli.output import EXIT_CONFLICT, is_json_mode
|
|
23
|
+
from tools.workflow_cli.workspace import ensure_workspace_gitignore
|
|
20
24
|
|
|
21
25
|
ACTIVE_POINTER_FILE = ".workflow-active"
|
|
22
26
|
|
|
@@ -42,10 +46,22 @@ def read_active_pointer(base_path: Path) -> dict | None:
|
|
|
42
46
|
return data if data else None
|
|
43
47
|
|
|
44
48
|
|
|
49
|
+
def _ensure_workspace_gitignore_or_exit(base_path: Path, work_id: str) -> None:
|
|
50
|
+
try:
|
|
51
|
+
ensure_workspace_gitignore(base_path)
|
|
52
|
+
except ValueError as exc:
|
|
53
|
+
print(
|
|
54
|
+
"blocked: unsafe_workspace_gitignore\n"
|
|
55
|
+
f"work_id: {work_id}\n"
|
|
56
|
+
f"reason: {exc}\n"
|
|
57
|
+
)
|
|
58
|
+
sys.exit(EXIT_CONFLICT)
|
|
59
|
+
|
|
60
|
+
|
|
45
61
|
def write_active_pointer(base_path: Path, work_id: str, reason: str = "workflow_start") -> None:
|
|
46
62
|
work_id = _validate_work_id(work_id)
|
|
63
|
+
_ensure_workspace_gitignore_or_exit(base_path, work_id)
|
|
47
64
|
path = _pointer_path(base_path)
|
|
48
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
65
|
run_rel = f".req-to-plan/{work_id}/run.md"
|
|
50
66
|
updated_at = datetime.now(timezone.utc).astimezone().isoformat()
|
|
51
67
|
content = (
|
|
@@ -136,19 +152,24 @@ def generate_work_id(
|
|
|
136
152
|
if base_path is None:
|
|
137
153
|
return base_id
|
|
138
154
|
|
|
139
|
-
|
|
155
|
+
r2p_dir = base_path / ".req-to-plan"
|
|
156
|
+
|
|
157
|
+
def is_reserved(work_id: str) -> bool:
|
|
158
|
+
return (r2p_dir / work_id).exists() or (r2p_dir / "archive" / work_id).exists()
|
|
159
|
+
|
|
160
|
+
if not is_reserved(base_id):
|
|
140
161
|
return base_id
|
|
141
162
|
|
|
142
163
|
for n in range(2, 100):
|
|
143
164
|
suffix = f"-{n}"
|
|
144
165
|
alt_candidate = candidate[:max_slug_len - len(suffix)].rstrip("-")
|
|
145
166
|
alt = f"{prefix}{alt_candidate}{suffix}"
|
|
146
|
-
if not (
|
|
167
|
+
if not is_reserved(alt):
|
|
147
168
|
return alt
|
|
148
169
|
|
|
149
170
|
raise RuntimeError(
|
|
150
171
|
f"Could not generate a unique work ID for {base_id!r} after 98 attempts. "
|
|
151
|
-
"Clean up old runs in .req-to-plan/ before starting a new one."
|
|
172
|
+
"Clean up old runs in .req-to-plan/ or .req-to-plan/archive/ before starting a new one."
|
|
152
173
|
)
|
|
153
174
|
|
|
154
175
|
|
|
@@ -158,7 +179,7 @@ def generate_work_id(
|
|
|
158
179
|
|
|
159
180
|
|
|
160
181
|
def is_terminal(status: RunStatus) -> bool:
|
|
161
|
-
return status
|
|
182
|
+
return status in (RunStatus.CLOSED_AT_PLAN_CHECKPOINT, RunStatus.ARCHIVED)
|
|
162
183
|
|
|
163
184
|
|
|
164
185
|
# ---------------------------------------------------------------------------
|
|
@@ -180,6 +201,36 @@ def _shell_join(parts: list[str | Path]) -> str:
|
|
|
180
201
|
return " ".join(shlex.quote(str(part)) for part in parts)
|
|
181
202
|
|
|
182
203
|
|
|
204
|
+
def _extract_cli_output_value(output: str, key: str) -> str | None:
|
|
205
|
+
stripped = output.strip()
|
|
206
|
+
if stripped.startswith("{"):
|
|
207
|
+
try:
|
|
208
|
+
payload = json.loads(stripped)
|
|
209
|
+
except json.JSONDecodeError:
|
|
210
|
+
payload = {}
|
|
211
|
+
value = payload.get(key)
|
|
212
|
+
if isinstance(value, str):
|
|
213
|
+
return value
|
|
214
|
+
|
|
215
|
+
prefix = f"{key}:"
|
|
216
|
+
for line in output.splitlines():
|
|
217
|
+
line = line.strip()
|
|
218
|
+
if line.startswith(prefix):
|
|
219
|
+
return line.partition(":")[2].strip()
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _json_payload_from_cli_output(output: str) -> dict[str, object]:
|
|
224
|
+
stripped = output.strip()
|
|
225
|
+
if not stripped:
|
|
226
|
+
return {}
|
|
227
|
+
try:
|
|
228
|
+
parsed = json.loads(stripped)
|
|
229
|
+
except json.JSONDecodeError:
|
|
230
|
+
return {}
|
|
231
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
232
|
+
|
|
233
|
+
|
|
183
234
|
def _repo_root() -> Path:
|
|
184
235
|
return Path(__file__).resolve().parents[2]
|
|
185
236
|
|
|
@@ -221,10 +272,15 @@ def _tier_lock_command(base_path: Path, work_id: str, record) -> str:
|
|
|
221
272
|
|
|
222
273
|
|
|
223
274
|
def _prepare_input_file(run_dir: Path, stage: str, suffix: str, seed: str = "") -> Path:
|
|
224
|
-
|
|
225
|
-
|
|
275
|
+
inputs_dir = run_dir / "inputs"
|
|
276
|
+
if inputs_dir.is_symlink():
|
|
277
|
+
raise ValueError("unsafe_input_file_symlink")
|
|
278
|
+
inputs_dir.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
path = inputs_dir / f"{stage}-{suffix}.md"
|
|
280
|
+
if path.is_symlink():
|
|
281
|
+
raise ValueError("unsafe_input_file_symlink")
|
|
226
282
|
if not path.exists():
|
|
227
|
-
path
|
|
283
|
+
atomic_write_text(path, seed)
|
|
228
284
|
return path
|
|
229
285
|
|
|
230
286
|
|
|
@@ -318,7 +374,9 @@ def _emit_checkpoint_stop(
|
|
|
318
374
|
f"reason: forced subagent review required (tier modifier: {modifiers})\n"
|
|
319
375
|
"note: you are authorized to spawn a read-only review subagent now; "
|
|
320
376
|
"separate human approval is NOT required for this step\n"
|
|
321
|
-
"next: have the review subagent audit the stage artifact
|
|
377
|
+
"next: have the review subagent audit the stage artifact for spec "
|
|
378
|
+
"compliance, code/design quality, AND any unresolved ambiguity / "
|
|
379
|
+
"undecided point (flag hedging that lacks a decision), write its "
|
|
322
380
|
"findings to review_file, then r2p-continue\n"
|
|
323
381
|
)
|
|
324
382
|
return
|
|
@@ -435,6 +493,7 @@ def _cmd_start(ns: argparse.Namespace, base_path: Path) -> None:
|
|
|
435
493
|
|
|
436
494
|
work_id = generate_work_id(requirement, base_path)
|
|
437
495
|
run_args = _build_run_start_args(work_id, requirement, file_path, getattr(ns, "repo_path", None))
|
|
496
|
+
_ensure_workspace_gitignore_or_exit(base_path, work_id)
|
|
438
497
|
exit_code = _run_cli(run_args, base_path)
|
|
439
498
|
if exit_code != 0:
|
|
440
499
|
sys.exit(exit_code)
|
|
@@ -468,9 +527,25 @@ def _cmd_continue(ns: argparse.Namespace, base_path: Path) -> None:
|
|
|
468
527
|
s = record.status
|
|
469
528
|
stage = record.current_stage.value
|
|
470
529
|
|
|
530
|
+
if s == RunStatus.EXECUTING:
|
|
531
|
+
ledger = run_path.parent / "execution" / "progress.md"
|
|
532
|
+
print(
|
|
533
|
+
"stop: resume_execution\n"
|
|
534
|
+
f"work_id: {work_id}\n"
|
|
535
|
+
f"ledger: {ledger}\n"
|
|
536
|
+
"next: resume the r2p-execute loop from the first unchecked task\n"
|
|
537
|
+
)
|
|
538
|
+
sys.exit(0)
|
|
539
|
+
|
|
540
|
+
if s == RunStatus.ARCHIVED:
|
|
541
|
+
print(f"done: archived\nwork_id: {work_id}\n")
|
|
542
|
+
sys.exit(0)
|
|
543
|
+
|
|
471
544
|
if s == RunStatus.CLOSED_AT_PLAN_CHECKPOINT:
|
|
472
545
|
print(f"done: run_closed\nwork_id: {work_id}\nplan: 07-plan.md\n"
|
|
473
|
-
"next: hand the PLAN to your executor\n"
|
|
546
|
+
"next: hand the PLAN to your executor\n"
|
|
547
|
+
f"to implement: r2p-execute --work-id {work_id}\n"
|
|
548
|
+
f"to archive: r2p-archive --work-id {work_id}\n")
|
|
474
549
|
sys.exit(0)
|
|
475
550
|
|
|
476
551
|
if s == RunStatus.ACTIVE_STAGE_DRAFT:
|
|
@@ -691,16 +766,206 @@ def _cmd_switch(ns: argparse.Namespace, base_path: Path) -> None:
|
|
|
691
766
|
|
|
692
767
|
def _cmd_reopen(ns: argparse.Namespace, base_path: Path) -> None:
|
|
693
768
|
from_id = _validate_work_id(ns.from_id)
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
769
|
+
_ensure_workspace_gitignore_or_exit(base_path, from_id)
|
|
770
|
+
output = io.StringIO()
|
|
771
|
+
with contextlib.redirect_stdout(output):
|
|
772
|
+
exit_code = _run_cli(
|
|
773
|
+
[
|
|
774
|
+
"run-reopen",
|
|
775
|
+
"--from", from_id,
|
|
776
|
+
"--stage", ns.stage,
|
|
777
|
+
"--reason", ns.reason,
|
|
778
|
+
],
|
|
779
|
+
base_path,
|
|
780
|
+
)
|
|
781
|
+
cli_output = output.getvalue()
|
|
782
|
+
json_mode = is_json_mode()
|
|
783
|
+
if cli_output and (exit_code != 0 or not json_mode):
|
|
784
|
+
print(cli_output, end="" if cli_output.endswith("\n") else "\n")
|
|
785
|
+
if exit_code != 0:
|
|
786
|
+
sys.exit(exit_code)
|
|
787
|
+
|
|
788
|
+
new_work_id = _extract_cli_output_value(cli_output, "new_work_id")
|
|
789
|
+
if not new_work_id:
|
|
790
|
+
print("blocked: reopen_output_missing_new_work_id\n")
|
|
791
|
+
sys.exit(EXIT_CONFLICT)
|
|
792
|
+
|
|
793
|
+
write_active_pointer(base_path, new_work_id, reason="workflow_reopen")
|
|
794
|
+
selected_run = f".req-to-plan/{new_work_id}/run.md"
|
|
795
|
+
if json_mode:
|
|
796
|
+
payload: dict[str, object] = {}
|
|
797
|
+
stripped = cli_output.strip()
|
|
798
|
+
if stripped:
|
|
799
|
+
try:
|
|
800
|
+
parsed = json.loads(stripped)
|
|
801
|
+
except json.JSONDecodeError:
|
|
802
|
+
parsed = {}
|
|
803
|
+
if isinstance(parsed, dict):
|
|
804
|
+
payload.update(parsed)
|
|
805
|
+
payload["selected_work_id"] = new_work_id
|
|
806
|
+
payload["selected_run"] = selected_run
|
|
807
|
+
payload["next"] = "r2p-continue"
|
|
808
|
+
print(json.dumps(payload, indent=2))
|
|
809
|
+
sys.exit(0)
|
|
810
|
+
|
|
811
|
+
print(f"selected_run: {selected_run}\nnext: r2p-continue\n")
|
|
812
|
+
sys.exit(0)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def _cmd_archive(ns: argparse.Namespace, base_path: Path) -> None:
|
|
816
|
+
work_id = ns.work_id
|
|
817
|
+
if not work_id:
|
|
818
|
+
pointer = read_active_pointer(base_path)
|
|
819
|
+
if not pointer:
|
|
820
|
+
print("no_selected_run: true\nnext: r2p-archive --work-id <id>\n")
|
|
821
|
+
sys.exit(1)
|
|
822
|
+
work_id = pointer["selected_work_id"]
|
|
823
|
+
work_id = _validate_work_id(work_id)
|
|
824
|
+
archive_args = ["run-archive", "--work-id", work_id]
|
|
825
|
+
if getattr(ns, "force", False):
|
|
826
|
+
archive_args.append("--force")
|
|
827
|
+
json_mode = is_json_mode()
|
|
828
|
+
cli_output = ""
|
|
829
|
+
if json_mode:
|
|
830
|
+
output = io.StringIO()
|
|
831
|
+
with contextlib.redirect_stdout(output):
|
|
832
|
+
exit_code = _run_cli(archive_args, base_path)
|
|
833
|
+
cli_output = output.getvalue()
|
|
834
|
+
else:
|
|
835
|
+
exit_code = _run_cli(archive_args, base_path)
|
|
836
|
+
if exit_code != 0:
|
|
837
|
+
if json_mode and cli_output:
|
|
838
|
+
print(cli_output, end="" if cli_output.endswith("\n") else "\n")
|
|
839
|
+
sys.exit(exit_code)
|
|
840
|
+
pointer = read_active_pointer(base_path)
|
|
841
|
+
if pointer and pointer.get("selected_work_id") == work_id:
|
|
842
|
+
_pointer_path(base_path).unlink(missing_ok=True)
|
|
843
|
+
if json_mode:
|
|
844
|
+
payload = _json_payload_from_cli_output(cli_output)
|
|
845
|
+
payload.setdefault("work_id", work_id)
|
|
846
|
+
payload["next"] = "r2p-status --all"
|
|
847
|
+
print(json.dumps(payload, indent=2))
|
|
848
|
+
sys.exit(0)
|
|
849
|
+
print(f"archived: {work_id}\nnext: r2p-status --all\n")
|
|
850
|
+
sys.exit(0)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _cmd_execute(ns: argparse.Namespace, base_path: Path) -> None:
|
|
854
|
+
work_id = ns.work_id
|
|
855
|
+
if not work_id:
|
|
856
|
+
pointer = read_active_pointer(base_path)
|
|
857
|
+
if not pointer:
|
|
858
|
+
print("no_selected_run: true\nnext: r2p-execute --work-id <id>\n")
|
|
859
|
+
sys.exit(1)
|
|
860
|
+
work_id = pointer["selected_work_id"]
|
|
861
|
+
work_id = _validate_work_id(work_id)
|
|
862
|
+
r2p_dir = base_path / ".req-to-plan"
|
|
863
|
+
run_dir = r2p_dir / work_id
|
|
864
|
+
if r2p_dir.is_symlink():
|
|
865
|
+
print(
|
|
866
|
+
"blocked: unsafe_workspace_dir_symlink\n"
|
|
867
|
+
f"work_id: {work_id}\n"
|
|
868
|
+
f"path: {r2p_dir}\n"
|
|
869
|
+
)
|
|
870
|
+
sys.exit(EXIT_CONFLICT)
|
|
871
|
+
if run_dir.is_symlink():
|
|
872
|
+
print(
|
|
873
|
+
"blocked: unsafe_run_dir_symlink\n"
|
|
874
|
+
f"work_id: {work_id}\n"
|
|
875
|
+
f"path: {run_dir}\n"
|
|
876
|
+
)
|
|
877
|
+
sys.exit(EXIT_CONFLICT)
|
|
878
|
+
run_path = run_dir / "run.md"
|
|
879
|
+
if not run_path.exists():
|
|
880
|
+
print(f"blocked: source_run_not_found\nwork_id: {work_id}\n")
|
|
881
|
+
sys.exit(7)
|
|
882
|
+
|
|
883
|
+
from tools.workflow_cli.state import RunStateManager
|
|
884
|
+
record = RunStateManager(run_dir).load()
|
|
885
|
+
plan = run_dir / "07-plan.md"
|
|
886
|
+
ledger = run_dir / "execution" / "progress.md"
|
|
887
|
+
|
|
888
|
+
if record.status == RunStatus.CLOSED_AT_PLAN_CHECKPOINT:
|
|
889
|
+
_ensure_workspace_gitignore_or_exit(base_path, work_id)
|
|
890
|
+
json_mode = is_json_mode()
|
|
891
|
+
cli_output = ""
|
|
892
|
+
if json_mode:
|
|
893
|
+
output = io.StringIO()
|
|
894
|
+
with contextlib.redirect_stdout(output):
|
|
895
|
+
code = _run_cli(["run-execute-start", "--work-id", work_id], base_path)
|
|
896
|
+
cli_output = output.getvalue()
|
|
897
|
+
else:
|
|
898
|
+
code = _run_cli(["run-execute-start", "--work-id", work_id], base_path)
|
|
899
|
+
if code != 0:
|
|
900
|
+
if json_mode and cli_output:
|
|
901
|
+
print(cli_output, end="" if cli_output.endswith("\n") else "\n")
|
|
902
|
+
sys.exit(code)
|
|
903
|
+
write_active_pointer(base_path, work_id, reason="execute_start")
|
|
904
|
+
next_step = (
|
|
905
|
+
"drive the r2p-execute skill (subagent-driven SDD loop) to "
|
|
906
|
+
"implement each PLAN-TASK in place on the current branch, then "
|
|
907
|
+
f"r2p-archive --work-id {work_id} when done"
|
|
908
|
+
)
|
|
909
|
+
if json_mode:
|
|
910
|
+
payload = _json_payload_from_cli_output(cli_output)
|
|
911
|
+
run_status = payload.get("status")
|
|
912
|
+
payload.update(
|
|
913
|
+
{
|
|
914
|
+
"status": "stop",
|
|
915
|
+
"reason": "execute_plan",
|
|
916
|
+
"work_id": work_id,
|
|
917
|
+
"plan": str(plan),
|
|
918
|
+
"ledger": str(ledger),
|
|
919
|
+
"next": next_step,
|
|
920
|
+
}
|
|
921
|
+
)
|
|
922
|
+
if isinstance(run_status, str):
|
|
923
|
+
payload["run_status"] = run_status
|
|
924
|
+
print(json.dumps(payload, indent=2))
|
|
925
|
+
sys.exit(0)
|
|
926
|
+
print(
|
|
927
|
+
"stop: execute_plan\n"
|
|
928
|
+
f"work_id: {work_id}\n"
|
|
929
|
+
f"plan: {plan}\n"
|
|
930
|
+
f"ledger: {ledger}\n"
|
|
931
|
+
f"next: {next_step}\n"
|
|
932
|
+
)
|
|
933
|
+
sys.exit(0)
|
|
934
|
+
|
|
935
|
+
if record.status == RunStatus.EXECUTING:
|
|
936
|
+
_ensure_workspace_gitignore_or_exit(base_path, work_id)
|
|
937
|
+
write_active_pointer(base_path, work_id, reason="execute_resume")
|
|
938
|
+
next_step = (
|
|
939
|
+
"resume the r2p-execute loop from the first unchecked task in "
|
|
940
|
+
f"the ledger, then r2p-archive --work-id {work_id} when done"
|
|
941
|
+
)
|
|
942
|
+
if is_json_mode():
|
|
943
|
+
print(
|
|
944
|
+
json.dumps(
|
|
945
|
+
{
|
|
946
|
+
"status": "stop",
|
|
947
|
+
"reason": "resume_execution",
|
|
948
|
+
"work_id": work_id,
|
|
949
|
+
"run_status": record.status.value,
|
|
950
|
+
"plan": str(plan),
|
|
951
|
+
"ledger": str(ledger),
|
|
952
|
+
"next": next_step,
|
|
953
|
+
},
|
|
954
|
+
indent=2,
|
|
955
|
+
)
|
|
956
|
+
)
|
|
957
|
+
sys.exit(0)
|
|
958
|
+
print(
|
|
959
|
+
"stop: resume_execution\n"
|
|
960
|
+
f"work_id: {work_id}\n"
|
|
961
|
+
f"plan: {plan}\n"
|
|
962
|
+
f"ledger: {ledger}\n"
|
|
963
|
+
f"next: {next_step}\n"
|
|
964
|
+
)
|
|
965
|
+
sys.exit(0)
|
|
966
|
+
|
|
967
|
+
print(f"blocked: plan_not_ready\nwork_id: {work_id}\nstatus: {record.status.value}\nnext: r2p-continue\n")
|
|
968
|
+
sys.exit(EXIT_CONFLICT)
|
|
704
969
|
|
|
705
970
|
|
|
706
971
|
def _cmd_gap_open(ns: argparse.Namespace, base_path: Path) -> None:
|
|
@@ -789,6 +1054,10 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
789
1054
|
p_reopen.add_argument("--stage", required=True)
|
|
790
1055
|
p_reopen.add_argument("--reason", required=True)
|
|
791
1056
|
|
|
1057
|
+
p_archive = sub.add_parser("archive")
|
|
1058
|
+
p_archive.add_argument("--work-id", dest="work_id", default=None)
|
|
1059
|
+
p_archive.add_argument("--force", action="store_true")
|
|
1060
|
+
|
|
792
1061
|
p_tier_lock = sub.add_parser("tier-lock")
|
|
793
1062
|
p_tier_lock.add_argument("--work-id", dest="work_id", required=True)
|
|
794
1063
|
p_tier_lock.add_argument("--base", required=True, choices=["light", "standard"])
|
|
@@ -807,6 +1076,9 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
807
1076
|
p_gap_resolve.add_argument("--route-id", dest="route_id", required=True)
|
|
808
1077
|
p_gap_resolve.add_argument("--confirm", action="store_true")
|
|
809
1078
|
|
|
1079
|
+
p_execute = sub.add_parser("execute")
|
|
1080
|
+
p_execute.add_argument("--work-id", dest="work_id", default=None)
|
|
1081
|
+
|
|
810
1082
|
return parser
|
|
811
1083
|
|
|
812
1084
|
|
|
@@ -827,9 +1099,11 @@ def main(args: list[str] | None = None, base_path: Path | None = None) -> None:
|
|
|
827
1099
|
"status": _cmd_status,
|
|
828
1100
|
"switch": _cmd_switch,
|
|
829
1101
|
"reopen": _cmd_reopen,
|
|
1102
|
+
"archive": _cmd_archive,
|
|
830
1103
|
"tier-lock": _cmd_tier_lock,
|
|
831
1104
|
"gap-open": _cmd_gap_open,
|
|
832
1105
|
"gap-resolve": _cmd_gap_resolve,
|
|
1106
|
+
"execute": _cmd_execute,
|
|
833
1107
|
}
|
|
834
1108
|
handlers[ns.subcommand](ns, bp)
|
|
835
1109
|
sys.exit(0)
|
|
@@ -18,7 +18,7 @@ Run each via Bash using the scripts in `{{R2P_BIN_DIR}}`:
|
|
|
18
18
|
| `{{R2P_BIN_DIR}}/r2p-tier-lock --work-id <id> --base <light\|standard> --confirm` | Lock the tier for a run |
|
|
19
19
|
| `{{R2P_BIN_DIR}}/r2p-status [--all]` | Inspect run state (read-only) |
|
|
20
20
|
| `{{R2P_BIN_DIR}}/r2p-switch --work-id <id>` | Switch active run pointer |
|
|
21
|
-
| `{{R2P_BIN_DIR}}/r2p-reopen --from <work-id> --stage <stage> --reason "<text>"` | Reopen a closed run |
|
|
21
|
+
| `{{R2P_BIN_DIR}}/r2p-reopen --from <work-id> --stage <stage> --reason "<text>"` | Reopen a closed or executing run |
|
|
22
22
|
| `{{R2P_BIN_DIR}}/r2p-gap-open --work-id <id> --owner-stage <stage> --required-action "<text>"` | Route an upstream gap back to its owner stage |
|
|
23
23
|
| `{{R2P_BIN_DIR}}/r2p-gap-resolve --work-id <id> --route-id <route-id>` | Resolve an open upstream-gap route after the owner stage re-passes gate-quality |
|
|
24
24
|
|
|
@@ -34,4 +34,5 @@ Run each via Bash using the scripts in `{{R2P_BIN_DIR}}`:
|
|
|
34
34
|
- Auto-closes: closes the run when the PLAN checkpoint is approved and no open routes remain
|
|
35
35
|
- Does NOT auto-mark artifacts ready and does NOT auto-approve checkpoints — those are human steps
|
|
36
36
|
- Standard-tier DESIGN: record any human technical choice in `## Decision Requests` as a `### DECISION-NNN` block (`Question:`/`Options:`/`Recommended:`/`Status: pending`); pending blocks `gate-quality` until a human selects (`Status: selected` + `Selected:`/`Rationale:`), or write exactly `none` when no decision is needed
|
|
37
|
+
- At a checkpoint, only approve a DESIGN/SPEC/PLAN artifact that has no unresolved ambiguity or undecided point: resolve it by evidence, or route it (DESIGN: `### DECISION-NNN`; SPEC/PLAN: `r2p-gap-open`) before approving.
|
|
37
38
|
3. When the run closes, hand the approved PLAN at `07-plan.md` directly to your executor — the PLAN is executor-neutral and needs no adaptation step.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Archive a closed or executing workflow run out of the active workspace
|
|
3
|
+
---
|
|
4
|
+
Run `{{R2P_BIN_DIR}}/r2p-archive` to archive the selected run, or pass a run explicitly.
|
|
5
|
+
|
|
6
|
+
Usage: `{{R2P_BIN_DIR}}/r2p-archive [--work-id <work-id>]`
|
|
7
|
+
|
|
8
|
+
The run must be `closed_at_plan_checkpoint` or `executing`. On success, it moves `.req-to-plan/<work-id>` to `.req-to-plan/archive/<work-id>` and clears the active selection for that run.
|
|
9
|
+
|
|
10
|
+
Use `{{R2P_BIN_DIR}}/r2p-status --all` afterward to inspect remaining active runs.
|
|
@@ -10,6 +10,7 @@ Behavior:
|
|
|
10
10
|
- Stops when human action is required: stage content generation, marking an artifact ready (`stage-ready`), Quality Gate failure or requested changes (repair), human checkpoint approval (`checkpoint-decide`), entry gate failure
|
|
11
11
|
- Stops with `needs_subagent_review` on forced-review runs (a `migration`/`safety`/`cross_project` modifier at `design`/`spec`/`plan`) when the required review file is missing. You are authorized to run a read-only review subagent yourself for this step — no separate human approval is needed
|
|
12
12
|
- Does NOT auto-mark artifacts ready and does NOT auto-approve checkpoints
|
|
13
|
+
- At a checkpoint, only approve a DESIGN/SPEC/PLAN artifact that has no unresolved ambiguity or undecided point: resolve it by evidence, or route it (DESIGN: `### DECISION-NNN`; SPEC/PLAN: `r2p-gap-open`) before approving
|
|
13
14
|
|
|
14
15
|
Call repeatedly until the output says `stop:` with a message indicating why the run paused. For `needs_content` and `needs_repair`, write the required artifact content into the printed `content_file`, then run the printed `next:` command exactly. For `needs_subagent_review`, run a review subagent to audit the stage artifact, write its findings to the printed `review_file`, then resume with `r2p-continue` (you do not need to ask for approval to spawn the review subagent). For other stops, run the printed `next:` command exactly. If an `alt:` command is shown, use it only when that alternate decision is intended. Resume with `r2p-continue` after completing that step.
|
|
15
16
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Execute a closed run's PLAN in place on the current branch via the subagent-driven SDD loop, then archive
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# r2p-execute — SDD Execution Loop
|
|
6
|
+
|
|
7
|
+
Execute each PLAN-TASK on the **current branch** using a fresh implementer subagent per task, a task-reviewer after each, and a whole-branch review at the end.
|
|
8
|
+
|
|
9
|
+
**Subagents are a hard prerequisite.** If this platform cannot dispatch subagents, fail explicitly and let the human decide — never silently fall back to sequential execution.
|
|
10
|
+
|
|
11
|
+
## Precondition Gate
|
|
12
|
+
|
|
13
|
+
Run `{{R2P_BIN_DIR}}/r2p-execute` and read its stop output. On a `closed_at_plan_checkpoint` run it transitions the run to `executing` (via the `run-execute-start` CLI command) and stops with `execute_plan`; on an already-`executing` run it stops with `resume_execution`; on any other status it stops with `plan_not_ready`.
|
|
14
|
+
|
|
15
|
+
- If the run status is `closed_at_plan_checkpoint` (first execution): the command transitions it to `executing` and returns the plan path.
|
|
16
|
+
- If the run status is already `executing` (resume after interruption): proceed directly to the plan path.
|
|
17
|
+
- Any other status → `plan_not_ready`. Stop and tell the user.
|
|
18
|
+
|
|
19
|
+
## In-Place Execution (no branch)
|
|
20
|
+
|
|
21
|
+
Work directly on the **current branch** — do NOT create a new branch, worktree, or protection boundary.
|
|
22
|
+
|
|
23
|
+
Before task 1, run `git status --short -- ':!.req-to-plan'` to check the code working tree (excluding `.req-to-plan/` state). If there are uncommitted changes, stop before dispatching Task 1 and ask the user to clean, stash, or commit that work, or to explicitly identify which dirty paths belong to this execution and approve task-only staging. Do not commit unrelated work. `push` and PR creation are out of scope; request them explicitly from the user.
|
|
24
|
+
|
|
25
|
+
## Pre-flight Plan Review
|
|
26
|
+
|
|
27
|
+
Before dispatching Task 1, read `07-plan.md` once and scan for:
|
|
28
|
+
- Tasks that contradict each other or the plan's Global Constraints
|
|
29
|
+
- Items the plan mandates that the review rubric would treat as a defect
|
|
30
|
+
|
|
31
|
+
Batch all findings into one question to the human **before** execution begins. If the scan is clean, proceed without comment.
|
|
32
|
+
If a finding requires PLAN, SPEC, or DESIGN repair, stop and ask the human to reopen from the affected stage rather than patching over it in execution.
|
|
33
|
+
|
|
34
|
+
## Per-Task Loop
|
|
35
|
+
|
|
36
|
+
For each PLAN-TASK (in order):
|
|
37
|
+
|
|
38
|
+
### 1. Extract task inline
|
|
39
|
+
|
|
40
|
+
Read the task text directly from `07-plan.md`. Note the task's `Skeleton`, `Steps`, `Spec References`, and `Verification` criteria.
|
|
41
|
+
|
|
42
|
+
### 2. Dispatch a fresh implementer subagent
|
|
43
|
+
|
|
44
|
+
Provide the subagent with:
|
|
45
|
+
- The task text (from `07-plan.md`)
|
|
46
|
+
- Scene-setting context (project, dependencies, architectural constraints)
|
|
47
|
+
- TDD instructions: follow `Skeleton`/`Steps`; prove `Verification` with evidence
|
|
48
|
+
- A report file path (`execution/task-N-report.md`)
|
|
49
|
+
|
|
50
|
+
The implementer must:
|
|
51
|
+
1. Implement exactly what the task specifies, following TDD
|
|
52
|
+
2. Satisfy the task's `Verification` criteria and attach evidence (test output, assertions)
|
|
53
|
+
3. Commit the work, staging only files intentionally changed for this PLAN-TASK
|
|
54
|
+
4. Self-review and report back
|
|
55
|
+
|
|
56
|
+
### 3. Handle implementer status
|
|
57
|
+
|
|
58
|
+
- **DONE / DONE_WITH_CONCERNS**: proceed to review
|
|
59
|
+
- **NEEDS_CONTEXT**: the implementer needs missing information — provide it and re-dispatch the fresh implementer subagent
|
|
60
|
+
- **BLOCKED**: assess the blocker; provide context, use a more capable model, or break the task into smaller pieces; escalate to the human if the plan itself is wrong
|
|
61
|
+
|
|
62
|
+
### 4. Ambiguity ladder
|
|
63
|
+
|
|
64
|
+
The fresh implementer subagent verifies-then-removes ambiguity by evidence and TDD. If it cannot resolve ambiguity:
|
|
65
|
+
- Return `NEEDS_CONTEXT` or `BLOCKED` and escalate to the human — never guess a vague implementation
|
|
66
|
+
- If the ambiguity is an upstream PLAN, SPEC, or DESIGN defect, stop and ask the human to choose an upstream repair path (for example, reopening from the affected stage) rather than patching over it in execution. Do not try to open a gap route from an `executing` run.
|
|
67
|
+
|
|
68
|
+
### 5. Write diff and dispatch task-reviewer
|
|
69
|
+
|
|
70
|
+
After the implementer reports DONE:
|
|
71
|
+
1. Record the diff inline: `git diff -U10 <base-commit> HEAD` (no external script needed)
|
|
72
|
+
2. Dispatch a task-reviewer subagent with:
|
|
73
|
+
- The task text and `Spec References` from `07-plan.md`
|
|
74
|
+
- The implementer's report
|
|
75
|
+
- The diff
|
|
76
|
+
- Global constraints from the plan
|
|
77
|
+
|
|
78
|
+
The task-reviewer returns two verdicts:
|
|
79
|
+
- **Spec compliance**: checked against `Spec References` + `Verification`
|
|
80
|
+
- **Code quality**: clean, tested, maintainable
|
|
81
|
+
|
|
82
|
+
### 6. Fix loop
|
|
83
|
+
|
|
84
|
+
- Dispatch fix subagents for Critical and Important findings
|
|
85
|
+
- Re-dispatch the task-reviewer after each fix wave
|
|
86
|
+
- Only when the task-reviewer is clean (both spec ✅ and quality Approved, and `Verification` satisfied), update the matching `execution/progress.md` checkbox from `- [ ] PLAN-TASK-NNN ...` to `- [x] PLAN-TASK-NNN ...` and append one line:
|
|
87
|
+
`Task N: complete (commits <base7>..<head7>, review clean)`
|
|
88
|
+
|
|
89
|
+
## Final Whole-Branch Review
|
|
90
|
+
|
|
91
|
+
After all tasks complete, dispatch a final whole-branch review subagent:
|
|
92
|
+
- Scope: all commits since the branch started (or since `closed_at_plan_checkpoint`)
|
|
93
|
+
- Include the diff (`git diff -U10 <merge-base> HEAD`)
|
|
94
|
+
- Dispatch fix subagents for any Critical/Important findings before marking done
|
|
95
|
+
- This whole-branch review is the merge gate
|
|
96
|
+
|
|
97
|
+
## Auto-Archive on Completion
|
|
98
|
+
|
|
99
|
+
When all tasks are done and the final whole-branch review is clean, call:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
{{R2P_BIN_DIR}}/r2p-archive --work-id <work_id from the precondition output>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Archiving is gated: `r2p-archive` refuses unless every PLAN-TASK from the PLAN is checked off (`- [x]`) in the ledger. Add `--force` only to archive an abandoned or superseded run.
|
|
106
|
+
|
|
107
|
+
Commits are already on the **current branch**. `push` and PR creation still require an explicit user request.
|
|
108
|
+
|
|
109
|
+
## Durable Progress
|
|
110
|
+
|
|
111
|
+
Track progress in `execution/progress.md` (not only in todos). On resume, read the ledger and skip tasks already marked complete.
|
|
112
|
+
|
|
113
|
+
## Error Reference
|
|
114
|
+
|
|
115
|
+
| Condition | Action |
|
|
116
|
+
|---|---|
|
|
117
|
+
| Status not `closed_at_plan_checkpoint` or `executing` | Stop: `plan_not_ready` |
|
|
118
|
+
| Implementer returns `NEEDS_CONTEXT` | Provide missing context, re-dispatch fresh implementer subagent |
|
|
119
|
+
| Upstream PLAN/SPEC/DESIGN defect found | Stop: ask the human to reopen/repair the upstream stage |
|
|
120
|
+
| Platform lacks subagent capability | Fail explicitly (subagents are a hard prerequisite) |
|
|
121
|
+
|
|
122
|
+
Use `r2p-status` to inspect progress without making changes.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Reopen a closed workflow run from a specific stage
|
|
2
|
+
description: Reopen a closed or executing workflow run from a specific stage
|
|
3
3
|
---
|
|
4
|
-
Run `{{R2P_BIN_DIR}}/r2p-reopen` to reopen a run that was closed at the PLAN checkpoint.
|
|
4
|
+
Run `{{R2P_BIN_DIR}}/r2p-reopen` to reopen a run that was closed at the PLAN checkpoint or is already executing. On success, it selects the reopened run as the active run.
|
|
5
5
|
|
|
6
6
|
Usage: `{{R2P_BIN_DIR}}/r2p-reopen --from <work-id> --stage <stage> --reason "<text>"`
|
|
7
7
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: r2p-archive
|
|
3
|
+
description: Use when the user asks to run r2p-archive, archive an r2p run, or move a closed or executing r2p workflow run out of the active workspace.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# r2p-archive
|
|
7
|
+
|
|
8
|
+
Run `{{R2P_BIN_DIR}}/r2p-archive` to archive the selected run, or pass a run explicitly.
|
|
9
|
+
|
|
10
|
+
Usage: `{{R2P_BIN_DIR}}/r2p-archive [--work-id <work-id>]`
|
|
11
|
+
|
|
12
|
+
The run must be `closed_at_plan_checkpoint` or `executing`. On success, it moves `.req-to-plan/<work-id>` to `.req-to-plan/archive/<work-id>` and clears the active selection for that run.
|
|
13
|
+
|
|
14
|
+
Use `{{R2P_BIN_DIR}}/r2p-status --all` afterward to inspect remaining active runs.
|
|
@@ -9,4 +9,6 @@ Run `{{R2P_BIN_DIR}}/r2p-continue` to resume the currently selected run.
|
|
|
9
9
|
|
|
10
10
|
Usage: `{{R2P_BIN_DIR}}/r2p-continue`
|
|
11
11
|
|
|
12
|
+
At a checkpoint, only approve a DESIGN/SPEC/PLAN artifact that has no unresolved ambiguity or undecided point: resolve it by evidence, or route it (DESIGN: `### DECISION-NNN`; SPEC/PLAN: `r2p-gap-open`) before approving.
|
|
13
|
+
|
|
12
14
|
Call repeatedly until the run reaches the closed state. For `needs_content` and `needs_repair` stops, write the required artifact content into the printed `content_file`, then run the printed `next:` command exactly. For a `needs_subagent_review` stop (forced-review runs: a `migration`/`safety`/`cross_project` modifier at `design`/`spec`/`plan`), run a read-only review subagent to audit the stage artifact yourself — no separate human approval is needed — write its findings to the printed `review_file`, then resume with `r2p-continue`. For other stops, run the printed `next:` command exactly. If an `alt:` command is shown, use it only when that alternate decision is intended. Use `r2p-status` to inspect progress.
|