af6e139d30
short-ship and long-ship share one ship-id record under .pouch/ship/<id>/. specifier writes PRDs into spec/; delivery-loop appends evidence pointers. Old names stay as deprecation stubs so existing installs keep working.
131 lines
4.1 KiB
Python
131 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Create a .pouch/ship/<id>/ record from templates. Refuse to overwrite."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from ship_lib import check_ship_id, skill_dir
|
|
from validate_ship import validate_ship_dir
|
|
|
|
|
|
def _replace(text: str, mapping: dict[str, str]) -> str:
|
|
for key, value in mapping.items():
|
|
text = text.replace(f"<{key}>", value)
|
|
text = text.replace(f"{{{{ {key} }}}}", value)
|
|
return text
|
|
|
|
|
|
def create_ship(
|
|
project_root: Path,
|
|
ship_id: str,
|
|
kind: str,
|
|
title: str,
|
|
*,
|
|
stop_at: str | None = None,
|
|
source_kind: str = "user",
|
|
source_ref: str | None = None,
|
|
) -> Path:
|
|
err = check_ship_id(ship_id)
|
|
if err:
|
|
raise SystemExit(err)
|
|
if kind not in {"short", "long"}:
|
|
raise SystemExit(f"kind 无效: {kind}")
|
|
title = title.strip()
|
|
if not title:
|
|
raise SystemExit("title 不能为空")
|
|
|
|
dest = project_root / ".pouch" / "ship" / ship_id
|
|
if dest.exists():
|
|
raise SystemExit(f"拒绝覆盖已有路径: {dest}")
|
|
|
|
templates = skill_dir() / "templates"
|
|
dest.mkdir(parents=True)
|
|
mapping = {
|
|
"ship-id": ship_id,
|
|
"title": title,
|
|
"kind": kind,
|
|
"short title": title,
|
|
}
|
|
|
|
current = _replace(
|
|
(templates / "current.template.yaml").read_text(encoding="utf-8"),
|
|
mapping,
|
|
)
|
|
current = current.replace("kind: short", f"kind: {kind}", 1)
|
|
current = current.replace("status: draft", "status: draft", 1)
|
|
if stop_at:
|
|
current = current.replace("stopAt: null", f"stopAt: {stop_at}")
|
|
if source_kind != "user":
|
|
current = current.replace("kind: user", f"kind: {source_kind}", 1)
|
|
if source_ref:
|
|
current = current.replace("ref: null", f"ref: {source_ref}", 1)
|
|
if kind == "long":
|
|
current = current.replace("status: draft", "status: spec_review", 1)
|
|
|
|
(dest / "current.yaml").write_text(current, encoding="utf-8")
|
|
(dest / "spec.md").write_text(
|
|
_replace((templates / "spec.template.md").read_text(encoding="utf-8"), mapping),
|
|
encoding="utf-8",
|
|
)
|
|
(dest / "tasks.yaml").write_text(
|
|
_replace(
|
|
(templates / "tasks.template.yaml").read_text(encoding="utf-8"), mapping
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
(dest / "status.md").write_text(
|
|
_replace(
|
|
(templates / "status.template.md").read_text(encoding="utf-8"),
|
|
{**mapping, "status": "spec_review" if kind == "long" else "draft",
|
|
"stopAt": stop_at or "null", "delivery.state": "change_ready"},
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
if kind == "long":
|
|
spec_dir = dest / "spec"
|
|
spec_dir.mkdir()
|
|
(spec_dir / "brief.md").write_text(f"# {title}\n\n", encoding="utf-8")
|
|
(spec_dir / "review.yaml").write_text("[]\n", encoding="utf-8")
|
|
(spec_dir / "decisions.yaml").write_text("[]\n", encoding="utf-8")
|
|
(spec_dir / "risks.yaml").write_text("[]\n", encoding="utf-8")
|
|
|
|
issues = validate_ship_dir(dest)
|
|
if issues:
|
|
shutil.rmtree(dest)
|
|
for item in issues:
|
|
print(f"✗ {item}", file=sys.stderr)
|
|
raise SystemExit("新建后校验失败,已删除不完整目录")
|
|
return dest
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="新建 ship 记录")
|
|
parser.add_argument("--project-root", required=True)
|
|
parser.add_argument("--ship-id", required=True)
|
|
parser.add_argument("--kind", choices=("short", "long"), required=True)
|
|
parser.add_argument("--title", required=True)
|
|
parser.add_argument("--stop-at", default=None)
|
|
parser.add_argument("--source-kind", default="user")
|
|
parser.add_argument("--source-ref", default=None)
|
|
args = parser.parse_args()
|
|
dest = create_ship(
|
|
Path(args.project_root).resolve(),
|
|
args.ship_id,
|
|
args.kind,
|
|
args.title,
|
|
stop_at=args.stop_at,
|
|
source_kind=args.source_kind,
|
|
source_ref=args.source_ref,
|
|
)
|
|
print(dest)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|