@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.
Files changed (28) 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 +295 -21
  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/atomic.py +6 -1
  21. package/tools/workflow_cli/cli.py +224 -21
  22. package/tools/workflow_cli/gates.py +149 -2
  23. package/tools/workflow_cli/install.py +49 -2
  24. package/tools/workflow_cli/markdown.py +18 -0
  25. package/tools/workflow_cli/models.py +19 -1
  26. package/tools/workflow_cli/stage_templates.py +2 -1
  27. package/tools/workflow_cli/version.py +1 -1
  28. 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
- 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):
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 (base_path / ".req-to-plan" / alt).exists():
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 == RunStatus.CLOSED_AT_PLAN_CHECKPOINT
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
- path = run_dir / "inputs" / f"{stage}-{suffix}.md"
225
- 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")
226
282
  if not path.exists():
227
- path.write_text(seed, encoding="utf-8")
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, 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 "
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
- exit_code = _run_cli(
695
- [
696
- "run-reopen",
697
- "--from", from_id,
698
- "--stage", ns.stage,
699
- "--reason", ns.reason,
700
- ],
701
- base_path,
702
- )
703
- 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)
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.