@xenonbyte/req-2-plan 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -49,9 +49,13 @@ from tools.workflow_cli.gates import (
|
|
|
49
49
|
check_execution_complete,
|
|
50
50
|
)
|
|
51
51
|
from tools.workflow_cli.output import (
|
|
52
|
+
COMPACT_DETAIL_LIMIT,
|
|
53
|
+
COMPACT_FILE_LIST_LIMIT,
|
|
54
|
+
compact_human_list,
|
|
52
55
|
format_success,
|
|
53
56
|
format_error,
|
|
54
57
|
format_gate_result,
|
|
58
|
+
is_json_mode,
|
|
55
59
|
print_and_exit,
|
|
56
60
|
EXIT_OK,
|
|
57
61
|
EXIT_CLI_ERR,
|
|
@@ -84,10 +88,7 @@ def _load_run(work_id: str, base_path: Path | None = None):
|
|
|
84
88
|
no command — read-only or mutating — ever follows `.req-to-plan` or
|
|
85
89
|
`.req-to-plan/<id>` out of the workspace.
|
|
86
90
|
"""
|
|
87
|
-
|
|
88
|
-
_reject_symlink_or_exit(root / ".req-to-plan", "unsafe_workspace_dir_symlink")
|
|
89
|
-
run_dir = _get_run_dir(work_id, base_path)
|
|
90
|
-
_reject_symlink_or_exit(run_dir, f"Run directory is a symlink: {run_dir}")
|
|
91
|
+
run_dir = _reject_symlinked_run_paths(work_id, base_path)
|
|
91
92
|
mgr = RunStateManager(run_dir)
|
|
92
93
|
try:
|
|
93
94
|
return mgr.load(), mgr, run_dir
|
|
@@ -98,6 +99,53 @@ def _load_run(work_id: str, base_path: Path | None = None):
|
|
|
98
99
|
)
|
|
99
100
|
|
|
100
101
|
|
|
102
|
+
def _write_recovery_list(run_dir: Path, work_id: str, filename: str, items: list[str]) -> str | None:
|
|
103
|
+
logs_dir = run_dir / "logs"
|
|
104
|
+
if logs_dir.is_symlink():
|
|
105
|
+
return None
|
|
106
|
+
try:
|
|
107
|
+
logs_dir.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
recovery_path = logs_dir / filename
|
|
109
|
+
atomic_write_text(recovery_path, "\n".join(items) + "\n")
|
|
110
|
+
except OSError:
|
|
111
|
+
return None
|
|
112
|
+
return f".req-to-plan/{work_id}/logs/{filename}"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _human_list_payload(
|
|
116
|
+
*,
|
|
117
|
+
run_dir: Path,
|
|
118
|
+
work_id: str,
|
|
119
|
+
label: str,
|
|
120
|
+
items: list,
|
|
121
|
+
limit: int,
|
|
122
|
+
recovery_filename: str,
|
|
123
|
+
recovery_items: list[str] | None = None,
|
|
124
|
+
) -> dict:
|
|
125
|
+
if is_json_mode() or len(items) <= limit:
|
|
126
|
+
return {label: items}
|
|
127
|
+
|
|
128
|
+
recovery_path = _write_recovery_list(
|
|
129
|
+
run_dir,
|
|
130
|
+
work_id,
|
|
131
|
+
recovery_filename,
|
|
132
|
+
recovery_items if recovery_items is not None else [str(item) for item in items],
|
|
133
|
+
)
|
|
134
|
+
if recovery_path is None:
|
|
135
|
+
return {label: items}
|
|
136
|
+
|
|
137
|
+
payload = compact_human_list(
|
|
138
|
+
label=label,
|
|
139
|
+
items=items,
|
|
140
|
+
limit=limit,
|
|
141
|
+
recovery_path=recovery_path,
|
|
142
|
+
)
|
|
143
|
+
payload[f"{label}_summary"] = (
|
|
144
|
+
f"{payload[f'{label}_shown']} shown, {payload[f'{label}_total']} total"
|
|
145
|
+
)
|
|
146
|
+
return payload
|
|
147
|
+
|
|
148
|
+
|
|
101
149
|
def _validate_work_id(raw: str) -> WorkId:
|
|
102
150
|
"""Parse WorkId or exit with CLI error."""
|
|
103
151
|
try:
|
|
@@ -118,6 +166,20 @@ def _reject_symlink_or_exit(path: Path, message: str) -> None:
|
|
|
118
166
|
print_and_exit(format_error(message, exit_code=EXIT_CONFLICT), EXIT_CONFLICT)
|
|
119
167
|
|
|
120
168
|
|
|
169
|
+
def _reject_symlinked_run_paths(work_id, base_path: Path | None) -> Path:
|
|
170
|
+
"""Reject a symlinked workspace or run directory, then return the run dir.
|
|
171
|
+
|
|
172
|
+
Guards `.req-to-plan` and `.req-to-plan/<id>` up front (EXIT_CONFLICT) so no
|
|
173
|
+
command — read-only or mutating — ever follows either out of the workspace.
|
|
174
|
+
Call before any filesystem mutation that targets the run dir.
|
|
175
|
+
"""
|
|
176
|
+
root = base_path or Path.cwd()
|
|
177
|
+
_reject_symlink_or_exit(root / ".req-to-plan", "unsafe_workspace_dir_symlink")
|
|
178
|
+
run_dir = _get_run_dir(work_id, base_path)
|
|
179
|
+
_reject_symlink_or_exit(run_dir, f"Run directory is a symlink: {run_dir}")
|
|
180
|
+
return run_dir
|
|
181
|
+
|
|
182
|
+
|
|
121
183
|
def _validate_repo_path(raw: str) -> Path:
|
|
122
184
|
"""Return a repo path only when it is an existing directory."""
|
|
123
185
|
repo_path = Path(raw)
|
|
@@ -223,7 +285,7 @@ def _cmd_run_start(args):
|
|
|
223
285
|
EXIT_CLI_ERR,
|
|
224
286
|
)
|
|
225
287
|
repo_path = _validate_repo_path(args.repo_path) if args.repo_path else None
|
|
226
|
-
run_dir =
|
|
288
|
+
run_dir = _reject_symlinked_run_paths(work_id, args.base_path)
|
|
227
289
|
mgr = RunStateManager(run_dir)
|
|
228
290
|
|
|
229
291
|
run_dir_occupied = False
|
|
@@ -323,6 +385,15 @@ def _cmd_run_start(args):
|
|
|
323
385
|
def _cmd_run_resume(args):
|
|
324
386
|
record, mgr, run_dir = _load_run(args.work_id, args.base_path)
|
|
325
387
|
rc = record.resume_context
|
|
388
|
+
reread_targets = list(rc.required_reread_targets)
|
|
389
|
+
reread_payload = _human_list_payload(
|
|
390
|
+
run_dir=run_dir,
|
|
391
|
+
work_id=str(record.work_id),
|
|
392
|
+
label="required_reread_targets",
|
|
393
|
+
items=reread_targets,
|
|
394
|
+
limit=COMPACT_FILE_LIST_LIMIT,
|
|
395
|
+
recovery_filename="run-resume-reread-targets.txt",
|
|
396
|
+
)
|
|
326
397
|
print_and_exit(
|
|
327
398
|
format_success(
|
|
328
399
|
{
|
|
@@ -333,6 +404,7 @@ def _cmd_run_resume(args):
|
|
|
333
404
|
"next_operation": rc.next_allowed_operation,
|
|
334
405
|
"active_item": rc.active_item,
|
|
335
406
|
"resume_reason": rc.resume_reason,
|
|
407
|
+
**reread_payload,
|
|
336
408
|
},
|
|
337
409
|
message="Resume context",
|
|
338
410
|
),
|
|
@@ -1212,6 +1284,22 @@ def _cmd_status_run(args):
|
|
|
1212
1284
|
for s in record.stale_artifacts
|
|
1213
1285
|
]
|
|
1214
1286
|
outstanding_stale = [aa.stage.value for aa in record.active_artifacts if aa.status == "stale"]
|
|
1287
|
+
approved_checkpoints = [cp.stage.value for cp in record.approved_checkpoints]
|
|
1288
|
+
approved_payload = _human_list_payload(
|
|
1289
|
+
run_dir=run_dir,
|
|
1290
|
+
work_id=str(record.work_id),
|
|
1291
|
+
label="approved_checkpoints",
|
|
1292
|
+
items=approved_checkpoints,
|
|
1293
|
+
limit=COMPACT_DETAIL_LIMIT,
|
|
1294
|
+
recovery_filename="status-run-approved-checkpoints.txt",
|
|
1295
|
+
recovery_items=[
|
|
1296
|
+
(
|
|
1297
|
+
f"{cp.stage.value}\t{cp.artifact}\tv{cp.version}\t"
|
|
1298
|
+
f"{cp.approved_at}\t{cp.downstream_authorization}\t{cp.bundle_id or ''}"
|
|
1299
|
+
)
|
|
1300
|
+
for cp in record.approved_checkpoints
|
|
1301
|
+
],
|
|
1302
|
+
)
|
|
1215
1303
|
|
|
1216
1304
|
print_and_exit(
|
|
1217
1305
|
format_success(
|
|
@@ -1226,7 +1314,7 @@ def _cmd_status_run(args):
|
|
|
1226
1314
|
"open_routes_detail": open_routes_detail,
|
|
1227
1315
|
"stale_artifacts": stale_artifacts,
|
|
1228
1316
|
"outstanding_stale": outstanding_stale,
|
|
1229
|
-
|
|
1317
|
+
**approved_payload,
|
|
1230
1318
|
},
|
|
1231
1319
|
message="Run status",
|
|
1232
1320
|
),
|
|
@@ -2000,7 +2088,7 @@ def _cmd_context_build(args):
|
|
|
2000
2088
|
from tools.workflow_cli.context_pack import build_context_pack, write_context_pack
|
|
2001
2089
|
|
|
2002
2090
|
work_id = str(_validate_work_id(args.work_id))
|
|
2003
|
-
run_dir =
|
|
2091
|
+
run_dir = _reject_symlinked_run_paths(work_id, args.base_path)
|
|
2004
2092
|
if not run_dir.exists():
|
|
2005
2093
|
print_and_exit(
|
|
2006
2094
|
format_error(f"run not found: {work_id}", exit_code=EXIT_NOT_FOUND),
|
|
@@ -11,12 +11,38 @@ EXIT_REVIEW_REQ = 5 # forced subagent review required
|
|
|
11
11
|
EXIT_CONFLICT = 6 # state conflict (run already closed, etc.)
|
|
12
12
|
EXIT_NOT_FOUND = 7 # resource not found (run.md, artifact, etc.)
|
|
13
13
|
|
|
14
|
+
# Opt-in compact display limits. Default formatters remain uncapped.
|
|
15
|
+
COMPACT_DETAIL_LIMIT = 10
|
|
16
|
+
COMPACT_FILE_LIST_LIMIT = 15
|
|
17
|
+
|
|
14
18
|
|
|
15
19
|
def is_json_mode() -> bool:
|
|
16
20
|
"""Check if JSON output mode is enabled via R2P_JSON environment variable."""
|
|
17
21
|
return os.environ.get("R2P_JSON", "0") == "1"
|
|
18
22
|
|
|
19
23
|
|
|
24
|
+
def compact_human_list(
|
|
25
|
+
*,
|
|
26
|
+
label: str,
|
|
27
|
+
items: list,
|
|
28
|
+
limit: int,
|
|
29
|
+
recovery_path: str | None = None,
|
|
30
|
+
) -> dict:
|
|
31
|
+
"""Build an opt-in compact list payload without touching the filesystem."""
|
|
32
|
+
if limit < 0:
|
|
33
|
+
raise ValueError("limit must be non-negative")
|
|
34
|
+
|
|
35
|
+
visible_items = list(items[:limit])
|
|
36
|
+
result = {
|
|
37
|
+
label: visible_items,
|
|
38
|
+
f"{label}_shown": len(visible_items),
|
|
39
|
+
f"{label}_total": len(items),
|
|
40
|
+
}
|
|
41
|
+
if recovery_path:
|
|
42
|
+
result[f"{label}_full_list"] = recovery_path
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
|
|
20
46
|
def format_success(data: dict, message: str = "") -> str:
|
|
21
47
|
"""Format a success response."""
|
|
22
48
|
if is_json_mode():
|
|
@@ -1 +1 @@
|
|
|
1
|
-
R2P_VERSION = "0.5.
|
|
1
|
+
R2P_VERSION = "0.5.2"
|
|
@@ -14,7 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
|
|
15
15
|
from tools.workflow_cli.atomic import atomic_write_text
|
|
16
16
|
|
|
17
|
-
_WORKSPACE_GITIGNORE_LINES = ("/archive", "/.workflow-active")
|
|
17
|
+
_WORKSPACE_GITIGNORE_LINES = ("/archive", "/.workflow-active", "/*/logs/")
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def ensure_workspace_gitignore(base_path: Path) -> None:
|