✨ feat(plan_progress): auto-detect env for blocked plans
This commit is contained in:
parent
0d9a8ec465
commit
a85453439f
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env python3
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
|
@ -8,9 +9,9 @@ PLAN_STATUS_START = "<!-- plan-status:start -->"
|
|||
PLAN_STATUS_END = "<!-- plan-status:end -->"
|
||||
PLAN_FILE_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})-.+\.md$")
|
||||
PLAN_LINE_RE = re.compile(
|
||||
r"^- \[(?P<check>[ xX])\] `(?P<plan>[^`]+)` (?P<status>done|blocked|pending)(?:: (?P<note>.*))?$"
|
||||
r"^- \[(?P<check>[ xX])\] `(?P<plan>[^`]+)` (?P<status>done|blocked|pending|in-progress|skipped)(?:: (?P<note>.*))?$"
|
||||
)
|
||||
VALID_STATUSES = {"done", "blocked", "pending"}
|
||||
VALID_STATUSES = {"done", "blocked", "pending", "in-progress", "skipped"}
|
||||
|
||||
|
||||
def usage() -> str:
|
||||
|
|
@ -22,7 +23,7 @@ def usage() -> str:
|
|||
"Options:\n"
|
||||
" -plans DIR\n"
|
||||
" -plan PATH\n"
|
||||
" -status done|blocked|pending\n"
|
||||
" -status done|blocked|pending|in-progress|skipped\n"
|
||||
" -progress FILE\n"
|
||||
" -note TEXT\n"
|
||||
" -h, -help Show this help.\n"
|
||||
|
|
@ -64,6 +65,12 @@ def render_plan_line(plan_key: str, status: str, note: Optional[str]) -> str:
|
|||
suffix += f": {note}"
|
||||
elif status == "pending":
|
||||
suffix = "pending"
|
||||
elif status == "in-progress":
|
||||
suffix = "in-progress"
|
||||
elif status == "skipped":
|
||||
suffix = "skipped"
|
||||
if note:
|
||||
suffix += f": {note}"
|
||||
else:
|
||||
suffix = "done"
|
||||
return f"- [{checked}] `{plan_key}` {suffix}"
|
||||
|
|
@ -125,6 +132,24 @@ def render_progress_lines(plans: list[str]) -> list[str]:
|
|||
return lines
|
||||
|
||||
|
||||
ENV_BLOCKED_RE = re.compile(r"^env:([^:]+):(.+)$")
|
||||
|
||||
|
||||
def parse_env_blocked_note(note: Optional[str]) -> Optional[tuple[str, str]]:
|
||||
"""Parse 'env:windows:Task2,Task4' format. Returns (env, tasks) or None."""
|
||||
if not note:
|
||||
return None
|
||||
match = ENV_BLOCKED_RE.match(note)
|
||||
if match:
|
||||
return match.group(1), match.group(2)
|
||||
return None
|
||||
|
||||
|
||||
def detect_env() -> Optional[str]:
|
||||
mapping = {"windows": "windows", "linux": "linux", "darwin": "darwin"}
|
||||
return mapping.get(platform.system().lower())
|
||||
|
||||
|
||||
def select_plan(plans_dir: Path, progress_path: Path) -> tuple[int, str]:
|
||||
if not plans_dir.is_dir():
|
||||
return 2, f"ERROR: plans dir not found: {plans_dir}"
|
||||
|
|
@ -155,10 +180,19 @@ def select_plan(plans_dir: Path, progress_path: Path) -> tuple[int, str]:
|
|||
progress_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
entries = parse_entries(lines, start_idx, end_idx)
|
||||
|
||||
for plan_key, status, _, _ in entries:
|
||||
if status == "pending":
|
||||
for plan_key, status, note, _ in entries:
|
||||
if status in ("pending", "in-progress"):
|
||||
return 0, (plans_dir / plan_key).as_posix()
|
||||
|
||||
# Check for env-blocked Plans if current environment is detected
|
||||
current_env = detect_env()
|
||||
if current_env:
|
||||
for plan_key, status, note, _ in entries:
|
||||
if status == "blocked":
|
||||
env_info = parse_env_blocked_note(note)
|
||||
if env_info and env_info[0] == current_env:
|
||||
return 0, (plans_dir / plan_key).as_posix()
|
||||
|
||||
return 2, "ERROR: no pending plans"
|
||||
|
||||
|
||||
|
|
@ -186,7 +220,7 @@ def record_status(plan: str, status: str, progress_path: Path, note: Optional[st
|
|||
entries = parse_entries(lines, start_idx, end_idx)
|
||||
|
||||
rendered_note = None
|
||||
if status == "blocked" and note:
|
||||
if status in ("blocked", "skipped") and note:
|
||||
rendered_note = normalize_note(note)
|
||||
|
||||
updated_line = render_plan_line(plan_key, status, rendered_note)
|
||||
|
|
|
|||
|
|
@ -78,28 +78,63 @@
|
|||
| `执行主循环`、`继续执行`、`下一个 Plan` | 常规模式 | 遇确认场景可询问用户 |
|
||||
| `自动执行所有 Plan` | 无交互模式 | 不询问,按规则自动处理 |
|
||||
|
||||
0. 选择 Plan:
|
||||
**Plan 状态**:
|
||||
|
||||
| 状态 | 含义 |
|
||||
| ----------- | ------------------------- |
|
||||
| pending | 待执行 |
|
||||
| in-progress | 执行中(崩溃恢复用) |
|
||||
| done | 已完成 |
|
||||
| blocked | 阻塞(需人工介入) |
|
||||
| skipped | 跳过(Plan 不再需要执行) |
|
||||
|
||||
> 说明:`skipped` 仅用于永久不再执行;如需恢复执行,需手动改回 `pending`。
|
||||
|
||||
**环境阻塞格式**:`blocked: env:<环境>:<Task列表>`
|
||||
|
||||
- 示例:`blocked: env:windows:Task2,Task4`
|
||||
- 含义:需要在指定环境执行列出的 Task
|
||||
- 约束:`Task` 列表使用英文逗号分隔,不要包含空格,便于解析
|
||||
|
||||
**流程**:
|
||||
|
||||
1. 检测环境:
|
||||
- 由 `plan_progress.py` 自动识别当前环境(`windows` / `linux` / `darwin`)
|
||||
2. 选择 Plan:
|
||||
- 运行 `python {{PLAYBOOK_SCRIPTS}}/plan_progress.py select -plans docs/plans -progress memory-bank/progress.md`
|
||||
- 如无可执行 Plan,跳到步骤 4
|
||||
1. 阅读 Plan:
|
||||
- 返回第一个可执行的 Plan:
|
||||
- `pending` 或 `in-progress` 的 Plan
|
||||
- `blocked: env:<当前环境>:...` 的 Plan(环境匹配时恢复执行)
|
||||
- 如无可执行 Plan,跳到步骤 7
|
||||
- **注意**:每次 select 会重新扫描 `docs/plans/` 目录,支持动态添加 Plan
|
||||
3. 标记开始:
|
||||
- 运行 `python {{PLAYBOOK_SCRIPTS}}/plan_progress.py record -plan <plan> -status in-progress -progress memory-bank/progress.md`
|
||||
4. 阅读 Plan:
|
||||
- 理解目标、子任务与验证标准
|
||||
2. 逐步执行:
|
||||
- 按顺序执行子任务
|
||||
- 每步完成后进行必要验证(测试/日志/diff)
|
||||
- 如果是从 `blocked: env:...` 恢复,只执行列出的 Task
|
||||
5. 逐步执行:
|
||||
- 按顺序执行 Task
|
||||
- 每个 Task 完成后进行必要验证(测试/日志/diff)
|
||||
- **Task 失败处理**:
|
||||
- 环境不匹配(`command not found`、路径不存在)→ 记录该 Task 及所需环境,**继续下一个 Task**
|
||||
- 其他阻塞 → 记录原因,跳到步骤 6 标记 Plan blocked
|
||||
- **安全红线**(明文密钥等)→ 立即停止,不继续后续 Plan
|
||||
- 遇到歧义/风险/决策点:
|
||||
- 常规模式:记录到回复中,可询问用户
|
||||
- 无交互模式:按「需要确认的场景」规则自动处理
|
||||
- 遇到阻塞:记录原因,跳到步骤 3 标记 blocked
|
||||
- **安全红线阻塞**(发现明文密钥等):立即停止,不继续后续 Plan
|
||||
3. 记录结果:
|
||||
- 完成:`python {{PLAYBOOK_SCRIPTS}}/plan_progress.py record -plan <plan> -status done -progress memory-bank/progress.md`
|
||||
- 阻塞:`python {{PLAYBOOK_SCRIPTS}}/plan_progress.py record -plan <plan> -status blocked -progress memory-bank/progress.md -note <原因>`
|
||||
- 回到步骤 0 继续下一个 Plan
|
||||
4. 汇总报告(所有 Plan 处理完毕后):
|
||||
- 列出已完成的 Plan
|
||||
- 列出阻塞的 Plan 及原因
|
||||
- 列出待确认的歧义/风险/决策点
|
||||
6. 记录结果:
|
||||
- 全部完成:`... -status done ...`
|
||||
- 有 Task 因环境跳过:`... -status blocked ... -note "env:<所需环境>:<Task列表>"`
|
||||
- 其他阻塞:`... -status blocked ... -note "<原因>"`
|
||||
- 跳过整个 Plan:`... -status skipped ... -note "<原因>"`
|
||||
- 回到步骤 2 继续下一个 Plan
|
||||
7. 汇总报告(所有 Plan 处理完毕后):
|
||||
- 已完成的 Plan
|
||||
- 阻塞/跳过的 Plan 及原因
|
||||
- 需要在其他环境执行的 Plan(`blocked: env:...`)
|
||||
- 待确认的歧义/风险/决策点
|
||||
- 如需记录重要决策,写入 `memory-bank/decisions.md`
|
||||
8. **结束**:主循环终止
|
||||
|
||||
## Plan 规则
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import subprocess
|
|||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
|
@ -18,6 +19,13 @@ def run_cli(*args, cwd=None):
|
|||
|
||||
|
||||
class PlanProgressCliTests(unittest.TestCase):
|
||||
def _current_env(self) -> str:
|
||||
system = platform.system().lower()
|
||||
mapping = {"windows": "windows", "linux": "linux", "darwin": "darwin"}
|
||||
if system not in mapping:
|
||||
self.skipTest(f"Unsupported environment: {system}")
|
||||
return mapping[system]
|
||||
|
||||
def test_select_seeds_progress_when_missing(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
|
|
@ -82,6 +90,42 @@ class PlanProgressCliTests(unittest.TestCase):
|
|||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||
self.assertEqual(result.stdout.strip(), "docs/plans/2026-01-02-b.md")
|
||||
|
||||
def test_select_returns_env_blocked_plan_without_flag(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
plans_dir = root / "docs" / "plans"
|
||||
plans_dir.mkdir(parents=True)
|
||||
(plans_dir / "2026-01-05-env.md").write_text("env", encoding="utf-8")
|
||||
|
||||
progress = root / "memory-bank" / "progress.md"
|
||||
progress.parent.mkdir(parents=True)
|
||||
env = self._current_env()
|
||||
progress.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"# Plan 状态",
|
||||
"",
|
||||
"<!-- plan-status:start -->",
|
||||
f"- [ ] `2026-01-05-env.md` blocked: env:{env}:Task1",
|
||||
"<!-- plan-status:end -->",
|
||||
"",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = run_cli(
|
||||
"select",
|
||||
"-plans",
|
||||
"docs/plans",
|
||||
"-progress",
|
||||
"memory-bank/progress.md",
|
||||
cwd=root,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||
self.assertEqual(result.stdout.strip(), "docs/plans/2026-01-05-env.md")
|
||||
|
||||
def test_record_updates_line(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
|
|
|
|||
Loading…
Reference in New Issue