@xenonbyte/req-2-plan 0.4.4 → 0.5.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.
Files changed (32) hide show
  1. package/README.md +170 -125
  2. package/README.zh-CN.md +154 -108
  3. package/package.json +1 -1
  4. package/tools/r2p-archive +10 -0
  5. package/tools/r2p-execute +10 -0
  6. package/tools/workflow_cli/agent_shortcuts.py +310 -26
  7. package/tools/workflow_cli/agent_templates/claude/SKILL.md +2 -1
  8. package/tools/workflow_cli/agent_templates/claude/commands/r2p-archive.md +10 -0
  9. package/tools/workflow_cli/agent_templates/claude/commands/r2p-continue.md +1 -0
  10. package/tools/workflow_cli/agent_templates/claude/commands/r2p-execute.md +122 -0
  11. package/tools/workflow_cli/agent_templates/claude/commands/r2p-reopen.md +2 -2
  12. package/tools/workflow_cli/agent_templates/codex/skills/r2p-archive/SKILL.md +14 -0
  13. package/tools/workflow_cli/agent_templates/codex/skills/r2p-continue/SKILL.md +2 -0
  14. package/tools/workflow_cli/agent_templates/codex/skills/r2p-execute/SKILL.md +123 -0
  15. package/tools/workflow_cli/agent_templates/codex/skills/r2p-reopen/SKILL.md +2 -2
  16. package/tools/workflow_cli/agent_templates/gemini/commands/r2p-archive.toml +4 -0
  17. package/tools/workflow_cli/agent_templates/gemini/commands/r2p-continue.toml +1 -1
  18. package/tools/workflow_cli/agent_templates/gemini/commands/r2p-execute.toml +4 -0
  19. package/tools/workflow_cli/agent_templates/gemini/commands/r2p-reopen.toml +1 -1
  20. package/tools/workflow_cli/artifact.py +5 -2
  21. package/tools/workflow_cli/atomic.py +50 -0
  22. package/tools/workflow_cli/cli.py +229 -31
  23. package/tools/workflow_cli/gates.py +149 -2
  24. package/tools/workflow_cli/install.py +56 -3
  25. package/tools/workflow_cli/link_expander.py +9 -25
  26. package/tools/workflow_cli/markdown.py +18 -0
  27. package/tools/workflow_cli/models.py +19 -1
  28. package/tools/workflow_cli/stage_templates.py +2 -1
  29. package/tools/workflow_cli/state.py +5 -6
  30. package/tools/workflow_cli/tier.py +1 -1
  31. package/tools/workflow_cli/version.py +1 -1
  32. 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
@@ -14,8 +17,10 @@ import sys
14
17
  from datetime import datetime, timezone
15
18
  from pathlib import Path
16
19
 
20
+ from tools.workflow_cli.atomic import atomic_write_text
17
21
  from tools.workflow_cli.models import RunStatus, WorkId
18
- 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
19
24
 
20
25
  ACTIVE_POINTER_FILE = ".workflow-active"
21
26
 
@@ -41,10 +46,22 @@ def read_active_pointer(base_path: Path) -> dict | None:
41
46
  return data if data else None
42
47
 
43
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
+
44
61
  def write_active_pointer(base_path: Path, work_id: str, reason: str = "workflow_start") -> None:
45
62
  work_id = _validate_work_id(work_id)
63
+ _ensure_workspace_gitignore_or_exit(base_path, work_id)
46
64
  path = _pointer_path(base_path)
47
- path.parent.mkdir(parents=True, exist_ok=True)
48
65
  run_rel = f".req-to-plan/{work_id}/run.md"
49
66
  updated_at = datetime.now(timezone.utc).astimezone().isoformat()
50
67
  content = (
@@ -53,7 +70,7 @@ def write_active_pointer(base_path: Path, work_id: str, reason: str = "workflow_
53
70
  f"updated_at: {updated_at}\n"
54
71
  f"reason: {reason}\n"
55
72
  )
56
- path.write_text(content, encoding="utf-8")
73
+ atomic_write_text(path, content)
57
74
 
58
75
 
59
76
  def _validate_work_id(raw: str) -> str:
@@ -122,7 +139,9 @@ def generate_work_id(
122
139
  words = [f"run-{h}"]
123
140
 
124
141
  candidate = "-".join(words[:5])
125
- candidate = re.sub(r"-+", "-", candidate).strip("-")[:max_slug_len]
142
+ # Truncate first, then strip dashes: stripping before truncation can leave a
143
+ # trailing "-" at the slice boundary, producing an invalid WorkId.
144
+ candidate = re.sub(r"-+", "-", candidate)[:max_slug_len].strip("-")
126
145
  if len(candidate) < 2:
127
146
  import hashlib
128
147
  h = hashlib.md5(requirement.encode()).hexdigest()[:8]
@@ -133,18 +152,24 @@ def generate_work_id(
133
152
  if base_path is None:
134
153
  return base_id
135
154
 
136
- if not (base_path / ".req-to-plan" / base_id).exists():
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):
137
161
  return base_id
138
162
 
139
163
  for n in range(2, 100):
140
164
  suffix = f"-{n}"
141
- alt = f"{prefix}{candidate[:max_slug_len - len(suffix)]}{suffix}"
142
- if not (base_path / ".req-to-plan" / alt).exists():
165
+ alt_candidate = candidate[:max_slug_len - len(suffix)].rstrip("-")
166
+ alt = f"{prefix}{alt_candidate}{suffix}"
167
+ if not is_reserved(alt):
143
168
  return alt
144
169
 
145
170
  raise RuntimeError(
146
171
  f"Could not generate a unique work ID for {base_id!r} after 98 attempts. "
147
- "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."
148
173
  )
149
174
 
150
175
 
@@ -154,7 +179,7 @@ def generate_work_id(
154
179
 
155
180
 
156
181
  def is_terminal(status: RunStatus) -> bool:
157
- return status == RunStatus.CLOSED_AT_PLAN_CHECKPOINT
182
+ return status in (RunStatus.CLOSED_AT_PLAN_CHECKPOINT, RunStatus.ARCHIVED)
158
183
 
159
184
 
160
185
  # ---------------------------------------------------------------------------
@@ -176,6 +201,36 @@ def _shell_join(parts: list[str | Path]) -> str:
176
201
  return " ".join(shlex.quote(str(part)) for part in parts)
177
202
 
178
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
+
179
234
  def _repo_root() -> Path:
180
235
  return Path(__file__).resolve().parents[2]
181
236
 
@@ -217,10 +272,15 @@ def _tier_lock_command(base_path: Path, work_id: str, record) -> str:
217
272
 
218
273
 
219
274
  def _prepare_input_file(run_dir: Path, stage: str, suffix: str, seed: str = "") -> Path:
220
- path = run_dir / "inputs" / f"{stage}-{suffix}.md"
221
- path.parent.mkdir(parents=True, exist_ok=True)
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")
222
282
  if not path.exists():
223
- path.write_text(seed, encoding="utf-8")
283
+ atomic_write_text(path, seed)
224
284
  return path
225
285
 
226
286
 
@@ -234,12 +294,18 @@ def _prev_stage(stage):
234
294
  def _seed_for_stage(stage, tier, upstream_summary: str = "", context_summary: str = "") -> str:
235
295
  """Build the seed text for a stage content file: template + upstream summary + context pack."""
236
296
  from tools.workflow_cli.stage_templates import template_for
297
+ from tools.workflow_cli.markdown import strip_readonly_sections
237
298
  base = tier.base if tier is not None else None
238
299
  text = template_for(stage, base) if base is not None else ""
239
- if upstream_summary.strip():
300
+ # Upstream artifacts persist the read-only blocks they were seeded with
301
+ # (nothing strips them at store time). Strip them here, like every other
302
+ # consumer (gates/trace), so the freshly injected Upstream Summary / Project
303
+ # Context wrappers below are not duplicated or accumulated across stages.
304
+ upstream_summary = strip_readonly_sections(upstream_summary).strip()
305
+ if upstream_summary:
240
306
  text += (
241
307
  "\n## Upstream Summary (read-only)\n"
242
- + upstream_summary.strip()
308
+ + upstream_summary
243
309
  + "\n<!-- /r2p-read-only -->\n"
244
310
  )
245
311
  if context_summary.strip():
@@ -308,7 +374,9 @@ def _emit_checkpoint_stop(
308
374
  f"reason: forced subagent review required (tier modifier: {modifiers})\n"
309
375
  "note: you are authorized to spawn a read-only review subagent now; "
310
376
  "separate human approval is NOT required for this step\n"
311
- "next: have the review subagent audit the stage artifact, write its "
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 "
312
380
  "findings to review_file, then r2p-continue\n"
313
381
  )
314
382
  return
@@ -425,6 +493,7 @@ def _cmd_start(ns: argparse.Namespace, base_path: Path) -> None:
425
493
 
426
494
  work_id = generate_work_id(requirement, base_path)
427
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)
428
497
  exit_code = _run_cli(run_args, base_path)
429
498
  if exit_code != 0:
430
499
  sys.exit(exit_code)
@@ -458,9 +527,25 @@ def _cmd_continue(ns: argparse.Namespace, base_path: Path) -> None:
458
527
  s = record.status
459
528
  stage = record.current_stage.value
460
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
+
461
544
  if s == RunStatus.CLOSED_AT_PLAN_CHECKPOINT:
462
545
  print(f"done: run_closed\nwork_id: {work_id}\nplan: 07-plan.md\n"
463
- "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")
464
549
  sys.exit(0)
465
550
 
466
551
  if s == RunStatus.ACTIVE_STAGE_DRAFT:
@@ -681,16 +766,206 @@ def _cmd_switch(ns: argparse.Namespace, base_path: Path) -> None:
681
766
 
682
767
  def _cmd_reopen(ns: argparse.Namespace, base_path: Path) -> None:
683
768
  from_id = _validate_work_id(ns.from_id)
684
- exit_code = _run_cli(
685
- [
686
- "run-reopen",
687
- "--from", from_id,
688
- "--stage", ns.stage,
689
- "--reason", ns.reason,
690
- ],
691
- base_path,
692
- )
693
- sys.exit(exit_code)
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)
694
969
 
695
970
 
696
971
  def _cmd_gap_open(ns: argparse.Namespace, base_path: Path) -> None:
@@ -779,6 +1054,10 @@ def _build_parser() -> argparse.ArgumentParser:
779
1054
  p_reopen.add_argument("--stage", required=True)
780
1055
  p_reopen.add_argument("--reason", required=True)
781
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
+
782
1061
  p_tier_lock = sub.add_parser("tier-lock")
783
1062
  p_tier_lock.add_argument("--work-id", dest="work_id", required=True)
784
1063
  p_tier_lock.add_argument("--base", required=True, choices=["light", "standard"])
@@ -797,6 +1076,9 @@ def _build_parser() -> argparse.ArgumentParser:
797
1076
  p_gap_resolve.add_argument("--route-id", dest="route_id", required=True)
798
1077
  p_gap_resolve.add_argument("--confirm", action="store_true")
799
1078
 
1079
+ p_execute = sub.add_parser("execute")
1080
+ p_execute.add_argument("--work-id", dest="work_id", default=None)
1081
+
800
1082
  return parser
801
1083
 
802
1084
 
@@ -817,9 +1099,11 @@ def main(args: list[str] | None = None, base_path: Path | None = None) -> None:
817
1099
  "status": _cmd_status,
818
1100
  "switch": _cmd_switch,
819
1101
  "reopen": _cmd_reopen,
1102
+ "archive": _cmd_archive,
820
1103
  "tier-lock": _cmd_tier_lock,
821
1104
  "gap-open": _cmd_gap_open,
822
1105
  "gap-resolve": _cmd_gap_resolve,
1106
+ "execute": _cmd_execute,
823
1107
  }
824
1108
  handlers[ns.subcommand](ns, bp)
825
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.