From 1b2ef369a63f5012870a03c8e1aee9ef3a3c2005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Urb=C3=A1nek?= Date: Mon, 1 Jun 2026 13:31:53 +0200 Subject: [PATCH] feat(pi): edpa_pi_create tool + /edpa:create-pi command & skill (2.2.0) Script-first PI-level file creation. create_pi.py is the single source of behavior: the edpa_pi_create MCP tool imports its create_pi() core, and the /edpa:create-pi command + edpa:create-pi skill shell out to its CLI -- same engine as the rest of EDPA (capacity_override.py <-> /edpa:capacity). Writes .edpa/iterations/.yaml (top-level pi: block). Rejects an iteration-level id (.N suffix) and overwrites; does not scaffold child iterations (those are edpa_iteration_create). Closes the gap where the PI parent had to be hand-written -- notably .yml was silently ignored by the loader. - bump 2.1.9 -> 2.2.0 (plugin.json, web/package.json + lock, tmpl methodology) - tests: test_create_pi.py (core + CLI + loader round-trip); edpa_pi_create added to MCP write/idempotency suites and the advertised-tool assertions - docs: mcp.md write-tools note + corrected read-only claim; playbook 1.5; RUNBOOK; plugin/README Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 40 +++++ docs/RUNBOOK.md | 1 + docs/mcp.md | 30 +++- docs/playbook.md | 10 +- plugin/.claude-plugin/plugin.json | 8 +- plugin/README.md | 14 +- plugin/commands/create-pi.md | 65 +++++++ plugin/edpa/scripts/create_pi.py | 249 ++++++++++++++++++++++++++ plugin/edpa/scripts/mcp_server.py | 59 ++++++ plugin/edpa/templates/edpa.yaml.tmpl | 2 +- plugin/skills/edpa-create-pi/SKILL.md | 72 ++++++++ tests/test_create_pi.py | 140 +++++++++++++++ tests/test_mcp_idempotency.py | 12 ++ tests/test_mcp_integration.py | 2 +- tests/test_mcp_server.py | 7 +- tests/test_mcp_write_tools.py | 58 +++++- web/package-lock.json | 4 +- web/package.json | 2 +- web/src/pages/en/setup.astro | 4 +- web/src/pages/setup.astro | 4 +- 20 files changed, 758 insertions(+), 25 deletions(-) create mode 100644 plugin/commands/create-pi.md create mode 100644 plugin/edpa/scripts/create_pi.py create mode 100644 plugin/skills/edpa-create-pi/SKILL.md create mode 100644 tests/test_create_pi.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e834c24..2326480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,45 @@ # Changelog +## 2.2.0 — 2026-06-01 — Create PI: edpa_pi_create tool + /edpa:create-pi command & skill + +Adds a first-class way to create the **PI-level metadata file** +`.edpa/iterations/.yaml` (top-level `pi:` block). Previously only +per-iteration files had tooling (`edpa_iteration_create`); the PI parent had to +be hand-written, which was error-prone — most notably the loader globs `*.yaml` +only, so a `PI-2026-1.yml` (short extension) is silently ignored and the PI +metadata (status, `pi_iterations`, dates) silently falls back to values derived +from the child iterations. + +Built **script-first**, matching the rest of EDPA: behavior lives in one script +and every interface delegates to it. + +### feat(pi): `create_pi.py` — single source of behavior +New `plugin/edpa/scripts/create_pi.py` with an importable `create_pi()` core +(validates a PI-level id, refuses to overwrite, atomic write of the `pi:` block) +plus a CLI (`--start/--end/--weeks/--iterations/--status/--no-commit`) that runs +continuity validation and auto-commits. Self-contained — no dependency on the +MCP layer, so it runs as a plain CLI and is importable by the server. + +### feat(mcp): `edpa_pi_create` tool +Thin delegate that imports `create_pi()` — no business logic in the handler +(write only; no commit, like the other MCP write tools). Inputs: `id` (required, +PI-level), `start_date`, `end_date`, `iteration_weeks`, `pi_iterations`, +`status`. Rejects an iteration-level id (`PI-YYYY-N.M`) and duplicates. The tool +surface is now 7 read + 8 write. + +### feat(skill+command): `/edpa:create-pi` and `edpa:create-pi` +Both shell out to `create_pi.py` (like `/edpa:capacity` → `capacity_override.py`). +The command takes explicit args; the skill auto-triggers on "create / start a +PI". Neither scaffolds the child iterations — those are added with +`edpa_iteration_create` (`.1 … .N`, last `type: IP`). + +### tests + docs +New `tests/test_create_pi.py` (core + CLI + loader round-trip); `edpa_pi_create` +added to the MCP write-tool and idempotency suites and the advertised-tool +assertions. `docs/mcp.md` gains a write-tools note (and the stale "read-only" +claim is corrected); `docs/playbook.md` §1.5, `docs/RUNBOOK.md`, and +`plugin/README.md` list the new tool / command / skill. + ## 2.1.9 — 2026-06-01 — Windows onboarding fixes (filelock, UTF-8 console + file I/O) `/edpa:edpa-setup` crashed on a fresh Windows box, surfaced by colleagues running diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md index 02e305a..3b2355e 100644 --- a/docs/RUNBOOK.md +++ b/docs/RUNBOOK.md @@ -15,6 +15,7 @@ YAML is the source of truth, git is the audit trail. GitHub Projects sync is | Command | Underlying script / skill | Status | |----------------------|------------------------------------|--------| | `/edpa:setup` | `.edpa/engine/scripts/project_setup.py` | ✅ vendors engine + seeds `.edpa/` (local-first) | +| `/edpa:create-pi` | `.edpa/engine/scripts/create_pi.py` | ✅ writes the PI-level `pi:` file (also `edpa_pi_create` MCP tool) | | `/edpa:close-iteration` | `.edpa/engine/scripts/engine.py` → `edpa-reports` skill | ✅ verified by `tests/test_invariants.py`, `tests/test_gate_allocation.py` | | `/edpa:reports` | `edpa-reports` skill (no script) | ✅ manual + skill execution | | `/edpa:board` | `.edpa/engine/scripts/board.py` | ✅ manual run | diff --git a/docs/mcp.md b/docs/mcp.md index 94ab7ba..f0c86d7 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -212,6 +212,30 @@ Returns: for items not yet closed. Both require timestamp fields synced from GitHub (see `sync pull` timestamp extraction). +### Write tools + +The server also exposes local-first **write** tools (V2). They mutate `.edpa/` +files directly (atomic tmp+rename) and do **not** commit or call the network — +the calling skill/command owns the commit. Full set: `edpa_item_create`, +`edpa_item_update`, `edpa_item_transition`, `edpa_item_link_parent`, +`edpa_iteration_create`, `edpa_iteration_close`, `edpa_pi_create`, +`edpa_people_upsert`. + +#### `edpa_pi_create` + +```json +{ "id": "PI-2026-2", "start_date": "2026-06-02", "iteration_weeks": 1, + "pi_iterations": 5, "status": "active" } +``` + +Creates the PI-level metadata file `.edpa/iterations/.yaml` (top-level +`pi:` block). Only `id` is required, and it must be **PI-level** (`PI-YYYY-N`) — +an iteration id with a `.N` suffix is rejected, as is overwriting an existing +PI. The filename is always `.yaml` (the loader globs `*.yaml`; a `.yml` is +silently ignored). Delegates to `create_pi.py`, the single source of behavior +also used by the `/edpa:create-pi` command and `edpa:create-pi` skill. Does not +scaffold child iterations — add those with `edpa_iteration_create`. + --- ## Production hardening (v1.3-beta) @@ -259,8 +283,10 @@ What changed from the v1.0–v1.2 prototype: ## Security model -- **Read-only.** No tool writes `.edpa/`. Bidirectional sync (`/edpa:sync`) - goes through the regular CLI, not MCP. +- **Local-first writes.** Read tools never mutate state. The V2 write tools + (`edpa_item_*`, `edpa_iteration_*`, `edpa_pi_create`, `edpa_people_upsert`) + write `.edpa/` files via atomic tmp+rename and do not commit or call the + network; the calling skill/command owns the commit. - **Path traversal blocked.** `item_id` parameter is the only user input that reaches the filesystem; the regex guard plus prefix→directory whitelist means a request like `{"item_id": "../etc/passwd"}` is rejected at the diff --git a/docs/playbook.md b/docs/playbook.md index 9347db0..24ab926 100644 --- a/docs/playbook.md +++ b/docs/playbook.md @@ -4,7 +4,7 @@ Kompletni prirucka pro nasazeni metodiky EDPA (Evidence-Driven Proportional Allo EDPA V2 je **local-first**: zdrojem pravdy je `.edpa/backlog/**/*.md` (YAML frontmatter), git je audit trail. GitHub je **volitelny** -- zadny GitHub Project, zadne org Issue Types, zadny obousmerny sync. -**Verze:** EDPA 2.1.9 +**Verze:** EDPA 2.2.0 **Posledni aktualizace:** 2026-06-01 --- @@ -215,7 +215,7 @@ project: governance: # Auto-razitkovano na verzi pluginu instalatorem. - methodology: "EDPA 2.1.9" + methodology: "EDPA 2.2.0" # Jedina vypocetni cesta od v1.14 (zadny simple/full/gates mode selector, # zadny audit_mode -- snapshoty vzdy nesou plny signals[] audit trail). @@ -289,6 +289,12 @@ Vytvoř obdobně `PI-2026-1.{2..5}.yaml`. Poslední iterace dostane překryvy, `weeks` × 7 ≈ rozdíl dat) hlídá `validate_iterations.py` i automatický PostToolUse hook. +> **Tip:** PI-level soubor (`pi:`) nemusíš psát ručně — založ ho příkazem +> `/edpa:create-pi PI-2026-1` (nebo MCP nástrojem `edpa_pi_create` / skillem +> `edpa:create-pi`). Validuje id, odmítne přepis a commitne. Per-iteration +> soubory přidávej přes `edpa_iteration_create`. Pozor: přípona musí být +> `.yaml`, ne `.yml` — loader `.yml` tiše ignoruje. + > **Odstraneno v 2.0.0:** sync blok (`github_org`, `github_project_number`, `sync_interval`) -- V2 je local-first, zadny GitHub Project se neprovisionuje. ### 1.6 Naplnit backlog diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index e084a01..a521998 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "edpa", - "version": "2.1.9", + "version": "2.2.0", "description": "EDPA — Evidence-Driven Proportional Allocation. Derive hours from local git evidence (commits, yaml edits, status transitions). Zero timesheets, mathematical guarantee, Monte Carlo calibrated CW weights. Local-first: .edpa/backlog/ YAML as source of truth, git as the audit trail. GitHub Projects sync optional.", "author": { "name": "TECHNOMATON", @@ -17,13 +17,15 @@ "./skills/edpa-engine", "./skills/edpa-reports", "./skills/edpa-autocalib", - "./skills/edpa-server" + "./skills/edpa-server", + "./skills/edpa-create-pi" ], "commands": [ "./commands/close-iteration.md", "./commands/board.md", "./commands/capacity.md", - "./commands/server.md" + "./commands/server.md", + "./commands/create-pi.md" ], "agents": [] } diff --git a/plugin/README.md b/plugin/README.md index a106230..1963d6e 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -58,20 +58,22 @@ plugin/ ├── requirements.txt # Python runtime deps (pyyaml, ruamel.yaml, mcp, openpyxl) ├── hooks/ │ └── hooks.json # SessionStart (install_deps) + PostToolUse (validate_on_save, post_commit) -├── skills/ # 6 skills, auto-discovered. Slug = name: field in SKILL.md frontmatter +├── skills/ # 7 skills, auto-discovered. Slug = name: field in SKILL.md frontmatter │ ├── edpa-setup/SKILL.md # → /edpa:setup — provision .edpa/ governance (engine, config, hooks, CI) │ ├── edpa-add/SKILL.md # → /edpa:add — create a backlog item (local-first, id_counters) │ ├── edpa-engine/SKILL.md # → /edpa:engine — evidence-driven calculation │ ├── edpa-reports/SKILL.md # → /edpa:reports — timesheets, exports, snapshots │ ├── edpa-autocalib/SKILL.md # → /edpa:autocalib — CW heuristic optimization (Monte Carlo + coord descent) -│ └── edpa-server/SKILL.md # → /edpa:server — optional PI-planning HTTP server (experimental) -├── commands/ # 4 slash commands, flat layout (no edpa/ subdir) +│ ├── edpa-server/SKILL.md # → /edpa:server — optional PI-planning HTTP server (experimental) +│ └── edpa-create-pi/SKILL.md # → /edpa:create-pi — create the PI-level metadata file (pi: block) +├── commands/ # 5 slash commands, flat layout (no edpa/ subdir) │ ├── close-iteration.md # → /edpa:close-iteration — capacity prep + engine + reports │ ├── board.md # → /edpa:board — HTML Kanban snapshot │ ├── capacity.md # → /edpa:capacity — per-iteration capacity overrides -│ └── server.md # → /edpa:server — start/stop PI-planning server +│ ├── server.md # → /edpa:server — start/stop PI-planning server +│ └── create-pi.md # → /edpa:create-pi — create the PI-level metadata file (pi: block) └── edpa/ - ├── scripts/ # 31 Python modules + ├── scripts/ # 32 Python modules │ ├── engine.py # Core engine (Score, DerivedHours, invariants) │ ├── mcp_server.py # MCP server for /edpa:status, /edpa:backlog, /edpa:iterations, /edpa:flow_metrics │ ├── calibrate_signals.py # CW signal-weights calibrator (Monte Carlo + coordinate descent) @@ -80,6 +82,7 @@ plugin/ │ ├── local_evidence.py # post-commit: commit_author + /contribute signals → evidence[] │ ├── sync_pr_contributions.py # CI: PR review/comment signals (edpa-contribution-sync) │ ├── capacity_override.py # per-iteration capacity overrides (/edpa:capacity) + │ ├── create_pi.py # create the PI-level metadata file (edpa_pi_create / /edpa:create-pi) │ ├── project_setup.py # provision .edpa/ governance (config, id_counters, --with-ci/hooks/rules) │ ├── traceability.py # Parent-chain validation │ ├── pi_close.py + velocity.py + transitions.py @@ -127,6 +130,7 @@ PR-thread signals (`pr_reviewer`, `issue_comment`) arrive only via the optional | `/edpa:close-iteration` | command | Capacity prep + engine + reports for an iteration | | `/edpa:capacity` | command | Per-iteration per-person capacity overrides (PTO, overtime) | | `/edpa:board` | command | HTML Kanban snapshot from local backlog | +| `/edpa:create-pi` | command | Create the PI-level `pi:` file (also `edpa:create-pi` skill + `edpa_pi_create` MCP tool) | ## Multi-developer setup — ID collision handling diff --git a/plugin/commands/create-pi.md b/plugin/commands/create-pi.md new file mode 100644 index 0000000..0ec9cc2 --- /dev/null +++ b/plugin/commands/create-pi.md @@ -0,0 +1,65 @@ +--- +description: Create the PI-level metadata file (.edpa/iterations/PI-YYYY-N.yaml) — the parent record of a Planning Interval +allowed-tools: Read, Bash +model: sonnet +--- + +# EDPA Create PI + +Create the **PI-level metadata file** `.edpa/iterations/.yaml` +(top-level `pi:` block) — the parent record for a Planning Interval. EDPA +reconstructs the PI list at runtime from `iterations/*.yaml` +(`_pi_loader.derive_pis`); without a `pi:` file the loader still works but +warns `missing_pi_yaml` and derives PI metadata from the child iterations — +so you lose explicit `status` / `pi_iterations` / dates. + +This wraps `create_pi.py`, the single source of behavior (the same engine the +`edpa_pi_create` MCP tool calls). It does **NOT** create the child iterations — +add those afterwards with `edpa_iteration_create` (`.1 … .N`, last `type: IP`). + +> **Filename gotcha:** the loader globs `*.yaml` only. A `PI-2026-1.yml` (short +> extension) is silently ignored. The script always writes `.yaml`. + +## Arguments + +`$ARGUMENTS` — the PI id plus optional flags: +- **id** (required) — PI-level, e.g. `PI-2026-2` (NOT an iteration id `PI-2026-2.1`) +- `--start YYYY-MM-DD` / `--end YYYY-MM-DD` — PI window (optional) +- `--weeks N` — iteration cadence in weeks (default 1) +- `--iterations N` — planned number of iterations in the PI (optional) +- `--status planning|active|closed` — default `planning` +- `--no-commit` — write the file but skip the git commit + +Examples: +- `PI-2026-2` +- `PI-2026-2 --start 2026-06-02 --end 2026-09-06 --weeks 1 --iterations 5 --status active` + +## Steps + +1. Parse `$ARGUMENTS`. Require a PI-level id (`PI-YYYY-N`, no `.iteration` + suffix). If the user gave an iteration id, tell them to use + `edpa_iteration_create` instead — this command creates the PI parent only. + +2. Run the script: + ```bash + python3 .edpa/engine/scripts/create_pi.py [--start …] [--end …] \ + [--weeks N] [--iterations N] [--status …] + ``` + It validates the id, refuses to overwrite an existing PI, writes the `pi:` + block atomically, runs continuity validation, and auto-commits + `chore(pi): create ` (pass `--no-commit` to skip). + +3. Report the created file, then suggest next steps: + - Create the child iterations `.1 … .N` via `edpa_iteration_create` + (the last one usually `type: IP`). + - Mark the running iteration `status: active` when it starts. + +## Notes + +- **Status lifecycle:** `planning → active → closed`. A PI's status is + otherwise derived from its iterations (`_pi_status_from_iterations`): active + if any iteration is active, closed once all are. +- For per-iteration files use `edpa_iteration_create`; to close an iteration + use `/edpa:close-iteration`. +- This command does not scaffold iterations by design — one explicit PI record, + iterations added deliberately. diff --git a/plugin/edpa/scripts/create_pi.py b/plugin/edpa/scripts/create_pi.py new file mode 100644 index 0000000..c12c7e7 --- /dev/null +++ b/plugin/edpa/scripts/create_pi.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +""" +EDPA PI creator — writes the PI-level metadata file +``.edpa/iterations/.yaml`` (top-level ``pi:`` block). + +This script is the single source of behavior for PI creation. The MCP tool +``edpa_pi_create`` imports :func:`create_pi`, and the ``/edpa:create-pi`` +command / ``edpa:create-pi`` skill shell out to this CLI — same engine, like +the rest of EDPA (``capacity_override.py`` <-> ``/edpa:capacity``, +``backlog.py`` <-> ``edpa:add``). + +A PI is the parent of per-iteration files (``PI-YYYY-N.1`` ...). This tool does +NOT scaffold those child iterations — create them with the iteration tooling +(``edpa_iteration_create``). The PI list is reconstructed at runtime from +``iterations/*.yaml`` by ``_pi_loader.derive_pis``; that loader globs ``*.yaml`` +only, so the file MUST end in ``.yaml`` — a ``.yml`` is silently ignored. + +Usage: + python3 create_pi.py PI-2026-2 + python3 create_pi.py PI-2026-2 --start 2026-06-02 --end 2026-09-06 \\ + --weeks 1 --iterations 5 --status active + python3 create_pi.py PI-2026-2 --no-commit +""" + +# NOTE: ``_console`` (which reconfigures stdout to UTF-8 as an import side +# effect) is imported lazily inside ``main()``, NOT at module top — because +# ``mcp_server`` imports :func:`create_pi` and must keep stdout pristine for +# JSON-RPC framing. Only the CLI opts into the UTF-8 reconfigure. +import argparse +import os +import re +import subprocess +import sys +import tempfile +from pathlib import Path + +try: + import yaml +except ImportError: + print("ERROR: PyYAML required. pip3 install pyyaml --break-system-packages", + file=sys.stderr) + sys.exit(2) + + +# -- console (mirrors capacity_override.py) ----------------------------------- +class C: + RESET = "\033[0m" + BOLD = "\033[1m" + DIM = "\033[2m" + OK = "\033[32m" + WARN = "\033[33m" + ERR = "\033[31m" + HEAD = "\033[38;5;147m" + + +def _isatty(): + return sys.stdout.isatty() and "NO_COLOR" not in os.environ + + +def _c(t, code): + return f"{code}{t}{C.RESET}" if _isatty() else t + + +def die(msg, code=1): + print(f"{_c('✗', C.ERR)} {msg}", file=sys.stderr) + sys.exit(code) + + +def info(msg): + print(f"{_c('·', C.DIM)} {msg}") + + +def ok(msg): + print(f"{_c('✓', C.OK)} {msg}") + + +def warn(msg): + print(f"{_c('⚠', C.WARN)} {msg}") + + +# -- core (importable; raises ValueError, never sys.exit) --------------------- +# PI-level id only — NO ``.iteration`` suffix. Mirrors the year/num shape of +# mcp_server.ITERATION_ID_RE but rejects the ``.N`` tail. +PI_ID_RE = re.compile(r"^PI-\d{4}-\d{1,2}$") +_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") +VALID_STATUSES = ("planning", "active", "closed") + + +def _write_yaml_atomic(path: Path, data: dict) -> None: + """tmp + rename; ``safe_dump(sort_keys=False, allow_unicode=True)``. + + Same shape as ``mcp_server._write_yaml_atomic`` — kept here so this script + has no dependency on the MCP layer and runs as a plain CLI. + """ + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + fd, tmp = tempfile.mkstemp(suffix=".yaml", prefix=f".{path.stem}_", + dir=str(path.parent)) + try: + with os.fdopen(fd, "w", encoding="utf-8") as f: + yaml.safe_dump(data, f, sort_keys=False, + default_flow_style=False, allow_unicode=True) + os.replace(tmp, path) + except Exception: + try: + os.unlink(tmp) + except OSError: + pass + raise + + +def create_pi(edpa_root, pi_id, *, start_date=None, end_date=None, + iteration_weeks=1, pi_iterations=None, status="planning") -> dict: + """Write the PI-level metadata file; return ``{"id", "path"}``. + + ``edpa_root`` is the ``.edpa/`` directory. ``pi_id`` must be PI-level + (``PI-YYYY-N``) — an iteration id with a ``.N`` suffix is rejected. Raises + ``ValueError`` on a bad/duplicate id or invalid field; callers map that to + their own channel (MCP -> ``_err``, CLI -> :func:`die`). Does not commit. + """ + edpa_root = Path(edpa_root) + if not isinstance(pi_id, str) or not PI_ID_RE.match(pi_id): + raise ValueError( + f"invalid PI id {pi_id!r}; expected PI-YYYY-N (e.g. PI-2026-1) " + f"with no .iteration suffix") + if status not in VALID_STATUSES: + raise ValueError( + f"invalid status {status!r}; expected one of {VALID_STATUSES}") + for label, val in (("start_date", start_date), ("end_date", end_date)): + if val is not None and not _DATE_RE.match(str(val)): + raise ValueError(f"{label} {val!r} must be YYYY-MM-DD") + if iteration_weeks is not None and (not isinstance(iteration_weeks, int) + or isinstance(iteration_weeks, bool) + or iteration_weeks < 1): + raise ValueError( + f"iteration_weeks must be an integer >= 1 (got {iteration_weeks!r})") + if pi_iterations is not None and (not isinstance(pi_iterations, int) + or isinstance(pi_iterations, bool) + or pi_iterations < 1): + raise ValueError( + f"pi_iterations must be an integer >= 1 (got {pi_iterations!r})") + + iter_path = edpa_root / "iterations" / f"{pi_id}.yaml" + if iter_path.exists(): + raise ValueError(f"PI {pi_id} already exists at {iter_path}") + + # Field order mirrors the playbook canonical shape. + pi_block: dict = {"id": pi_id, "status": status} + if iteration_weeks is not None: + pi_block["iteration_weeks"] = iteration_weeks + if pi_iterations is not None: + pi_block["pi_iterations"] = pi_iterations + if start_date is not None: + pi_block["start_date"] = start_date + if end_date is not None: + pi_block["end_date"] = end_date + + _write_yaml_atomic(iter_path, {"pi": pi_block}) + return {"id": pi_id, "path": str(iter_path)} + + +# -- CLI ---------------------------------------------------------------------- +def find_edpa_root() -> Path: + """Locate .edpa/ from cwd upward. Returns absolute path or dies.""" + cur = Path.cwd().resolve() + for parent in [cur, *cur.parents]: + if (parent / ".edpa").is_dir(): + return parent / ".edpa" + die("No .edpa/ directory found from current working directory upward.") + + +def run_validator(edpa_root: Path) -> bool: + """Run validate_iterations.py for continuity feedback (non-gating). + + Returns False if it reports errors. A lone PI with no child iterations is + valid (no ``missing_pi_yaml``), so this is informational, not a gate. + """ + script = Path(__file__).resolve().parent / "validate_iterations.py" + if not script.is_file(): + return True + rc = subprocess.run([sys.executable, str(script), str(edpa_root)], + capture_output=True, text=True) + if rc.stdout.strip(): + print(rc.stdout.strip()) + if rc.stderr.strip(): + print(rc.stderr.strip(), file=sys.stderr) + return rc.returncode == 0 + + +def main(argv=None) -> int: + try: # best-effort UTF-8 stdio on legacy Windows consoles (cp1250) — CLI only + import _console # noqa: F401 + except ImportError: + pass + ap = argparse.ArgumentParser( + description="EDPA PI creator — write the PI-level metadata file " + "(.edpa/iterations/.yaml)") + ap.add_argument("id", help="PI id, e.g. PI-2026-2 (no .iteration suffix)") + ap.add_argument("--start", dest="start_date", help="PI start date YYYY-MM-DD") + ap.add_argument("--end", dest="end_date", help="PI end date YYYY-MM-DD") + ap.add_argument("--weeks", dest="iteration_weeks", type=int, default=1, + help="iteration cadence in weeks (default 1)") + ap.add_argument("--iterations", dest="pi_iterations", type=int, + help="planned number of iterations in the PI") + ap.add_argument("--status", default="planning", choices=VALID_STATUSES, + help="PI status (default planning)") + ap.add_argument("--no-commit", action="store_true", + help="skip git add/commit (file mutation only)") + args = ap.parse_args(argv) + + edpa_root = find_edpa_root() + try: + result = create_pi( + edpa_root, args.id, + start_date=args.start_date, end_date=args.end_date, + iteration_weeks=args.iteration_weeks, + pi_iterations=args.pi_iterations, status=args.status) + except ValueError as e: + die(str(e)) + + rel = Path(result["path"]).relative_to(edpa_root.parent) + ok(f"Created PI {args.id} -> {rel}") + + run_validator(edpa_root) # informational continuity feedback (non-gating) + + if args.no_commit: + info(f"--no-commit: {rel} left uncommitted in the working tree") + else: + try: + from _auto_commit import maybe_commit + commit_status = maybe_commit( + [result["path"]], f"chore(pi): create {args.id}", + root=str(edpa_root.parent)) + if commit_status == "committed": + ok(f"Committed: chore(pi): create {args.id}") + elif commit_status == "skipped": + warn("auto-commit skipped (no git, or git user.name/email " + "unset) — commit manually.") + except ImportError: + warn("_auto_commit unavailable — commit manually.") + + info("Next: add child iterations (per-iteration files):") + print(f" {args.id}.1 ... via the iteration tooling (edpa_iteration_create);") + print(" the last iteration of a PI is usually type: IP.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plugin/edpa/scripts/mcp_server.py b/plugin/edpa/scripts/mcp_server.py index a43a54a..4489653 100644 --- a/plugin/edpa/scripts/mcp_server.py +++ b/plugin/edpa/scripts/mcp_server.py @@ -485,6 +485,32 @@ async def list_tools() -> list[Tool]: "additionalProperties": False, }, ), + Tool( + name="edpa_pi_create", + description=( + "Create the PI-level metadata file at " + ".edpa/iterations/{id}.yaml (top-level `pi:` block). The id " + "must be PI-level (PI-YYYY-N) — NOT an iteration id with a " + ".N suffix. Does NOT create child iterations (use " + "edpa_iteration_create); status defaults to 'planning'. " + "Delegates to create_pi.py — the single source of behavior " + "also used by the /edpa:create-pi command and skill." + ), + inputSchema={ + "type": "object", + "properties": { + "id": {"type": "string"}, + "start_date": {"type": "string", "pattern": r"^\d{4}-\d{2}-\d{2}$"}, + "end_date": {"type": "string", "pattern": r"^\d{4}-\d{2}-\d{2}$"}, + "iteration_weeks": {"type": "integer", "minimum": 1}, + "pi_iterations": {"type": "integer", "minimum": 1}, + "status": {"type": "string", "enum": ["planning", "active", "closed"]}, + "idempotency_key": {"type": "string", "maxLength": 128}, + }, + "required": ["id"], + "additionalProperties": False, + }, + ), Tool( name="edpa_people_upsert", description=( @@ -559,6 +585,8 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: return _handle_iteration_create(edpa_root, arguments) elif name == "edpa_iteration_close": return _handle_iteration_close(edpa_root, arguments) + elif name == "edpa_pi_create": + return _handle_pi_create(edpa_root, arguments) elif name == "edpa_people_upsert": return _handle_people_upsert(edpa_root, arguments) logger.warning("call_tool: unknown tool %s", name) @@ -1356,6 +1384,37 @@ def _handle_iteration_close(edpa_root: Path, args: dict) -> list[TextContent]: return _ok({"id": safe_id, "status": "closed"}) +@_idempotent("edpa_pi_create") +def _handle_pi_create(edpa_root: Path, args: dict) -> list[TextContent]: + """Create the PI-level metadata file by delegating to create_pi.py — the + single source of behavior (also driven by the /edpa:create-pi command). + + Write only; no git commit, consistent with the other MCP write tools (the + CLI layer is what commits). create_pi() raises ValueError on a bad/ + duplicate id or invalid field, which we surface via _err. + """ + sys.path.insert(0, str(Path(__file__).resolve().parent)) + try: + from create_pi import create_pi # noqa: E402 + result = create_pi( + edpa_root, + args.get("id", ""), + start_date=args.get("start_date"), + end_date=args.get("end_date"), + iteration_weeks=args.get("iteration_weeks", 1), + pi_iterations=args.get("pi_iterations"), + status=args.get("status", "planning"), + ) + except ValueError as exc: + return _err(str(exc)) + finally: + sys.path.pop(0) + + logger.info("edpa_pi_create: id=%s", result["id"]) + rel_path = str(Path(result["path"]).relative_to(edpa_root.parent)) + return _ok({"id": result["id"], "path": rel_path}) + + _PEOPLE_ALLOWED_FIELDS = frozenset({ "name", "role", "team", "fte", "capacity", "capacity_per_iteration", "github", "availability", "contract", diff --git a/plugin/edpa/templates/edpa.yaml.tmpl b/plugin/edpa/templates/edpa.yaml.tmpl index 88a0d11..d2f2c28 100644 --- a/plugin/edpa/templates/edpa.yaml.tmpl +++ b/plugin/edpa/templates/edpa.yaml.tmpl @@ -64,7 +64,7 @@ project: governance: # Auto-stamped to the installed plugin version by install.sh / project_setup.py. - methodology: "EDPA 2.1.9" + methodology: "EDPA 2.2.0" # v1.14: single calculation path. v1.17: extended with yaml_edit # signals — every commit on .edpa/backlog//.md in the # iteration window contributes structural credit (create / block_add diff --git a/plugin/skills/edpa-create-pi/SKILL.md b/plugin/skills/edpa-create-pi/SKILL.md new file mode 100644 index 0000000..a7e2941 --- /dev/null +++ b/plugin/skills/edpa-create-pi/SKILL.md @@ -0,0 +1,72 @@ +--- +name: edpa:create-pi +user-invocable: true +description: > + Create the PI-level metadata file .edpa/iterations/.yaml (the + top-level `pi:` block) — the parent record of a Planning Interval. Use when + the user wants to "create / start a PI", "založ PI", or "new planning + interval". Delegates to create_pi.py — the single source of behavior, also + exposed as the edpa_pi_create MCP tool. Validates the id, refuses to + overwrite, writes the pi: block, runs continuity validation, and + auto-commits. Does NOT scaffold the child iterations. +license: MIT +compatibility: Python 3.10+, MCP edpa server +allowed-tools: Read Bash(python3 *) Bash(git *) +--- + +# EDPA Create PI — Planning Interval metadata + +## What this does + +Writes `.edpa/iterations/.yaml` with a top-level `pi:` block. EDPA +reconstructs the PI list at runtime from `iterations/*.yaml` +(`_pi_loader.derive_pis`) — there is no `pis[]` block in `edpa.yaml`. Two file +shapes share that directory: + +- `PI-2026-1.yaml` → PI-level metadata (`pi:`) ← this skill +- `PI-2026-1.1.yaml` → per-iteration data (`iteration:`) ← `edpa_iteration_create` + +Without the `pi:` file the loader still builds the PI but emits a +`missing_pi_yaml` warning and derives metadata (status, weeks, count, dates) +from the iterations — so you can't declare the planned shape or force status. + +This is the single source of behavior used by both the `edpa_pi_create` MCP +tool and the `/edpa:create-pi` command. + +## Gotchas + +- **`.yaml`, never `.yml`** — the loader globs `*.yaml`; a `.yml` is silently + ignored. The script always writes `.yaml`. +- **PI-level id only** — `PI-YYYY-N` (e.g. `PI-2026-2`). An iteration id like + `PI-2026-2.1` is rejected; create iterations with `edpa_iteration_create`. +- **Status lifecycle** `planning → active → closed`. A PI's status is otherwise + derived from its iterations (`_pi_status_from_iterations`). + +## Steps + +1. Parse the request for the PI id + optional `--start` / `--end` / `--weeks` / + `--iterations` / `--status`. If the user gave an iteration id (`.N` suffix), + redirect them to `edpa_iteration_create` — this skill creates the PI parent. + +2. Run: + ```bash + python3 .edpa/engine/scripts/create_pi.py \ + [--start YYYY-MM-DD] [--end YYYY-MM-DD] [--weeks N] \ + [--iterations N] [--status planning|active|closed] + ``` + The script validates, refuses to overwrite, writes the `pi:` block + atomically, runs `validate_iterations.py` for continuity feedback, and + auto-commits `chore(pi): create ` (pass `--no-commit` to skip the commit). + +3. Report the created file and suggest next steps: + - Add child iterations `.1 … .N` via `edpa_iteration_create` + (the last one usually `type: IP`). + - Set the running iteration `status: active` when it starts. + +## What NOT to do + +- **Don't hand-write the PI file** — go through `create_pi.py` so id validation, + the `pi:` shape, atomic write, and commit stay on one path (the same path as + the `edpa_pi_create` MCP tool). +- **Don't use a `.yml` extension** — it's silently ignored by the loader. +- **Don't scaffold iterations here** — that's `edpa_iteration_create`, by design. diff --git a/tests/test_create_pi.py b/tests/test_create_pi.py new file mode 100644 index 0000000..84b02f3 --- /dev/null +++ b/tests/test_create_pi.py @@ -0,0 +1,140 @@ +"""Tests for create_pi.py — the PI-level metadata file creator. + +create_pi.py is the single source of behavior for PI creation: the +``edpa_pi_create`` MCP tool imports :func:`create_pi.create_pi`, and the +``/edpa:create-pi`` command and ``edpa:create-pi`` skill shell out to its CLI. +These cover the importable core (``create_pi``) and the CLI (subprocess), +mirroring ``test_capacity_overrides.py``. +""" +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import pytest +import yaml + +ROOT = Path(__file__).resolve().parent.parent +SCRIPTS = ROOT / "plugin" / "edpa" / "scripts" +sys.path.insert(0, str(SCRIPTS)) + +import create_pi as cp # noqa: E402 + + +@pytest.fixture +def edpa_root(tmp_path: Path) -> Path: + root = tmp_path / ".edpa" + (root / "iterations").mkdir(parents=True) + (root / "config").mkdir() + (root / "config" / "people.yaml").write_text( + yaml.safe_dump({"people": [ + {"id": "alice", "name": "Alice", "capacity_per_iteration": 40}, + ]}) + ) + return root + + +# -- core: create_pi() -------------------------------------------------------- + +def test_create_pi_writes_pi_block(edpa_root: Path) -> None: + result = cp.create_pi(edpa_root, "PI-2026-2", start_date="2026-07-06", + iteration_weeks=2, pi_iterations=5, status="active") + assert result["id"] == "PI-2026-2" + pi_path = edpa_root / "iterations" / "PI-2026-2.yaml" + assert pi_path.exists() + assert yaml.safe_load(pi_path.read_text()) == {"pi": { + "id": "PI-2026-2", "status": "active", "iteration_weeks": 2, + "pi_iterations": 5, "start_date": "2026-07-06", + }} + + +def test_create_pi_defaults(edpa_root: Path) -> None: + cp.create_pi(edpa_root, "PI-2026-3") + parsed = yaml.safe_load( + (edpa_root / "iterations" / "PI-2026-3.yaml").read_text()) + assert parsed["pi"]["status"] == "planning" + assert parsed["pi"]["iteration_weeks"] == 1 + assert "pi_iterations" not in parsed["pi"] + assert "start_date" not in parsed["pi"] + + +@pytest.mark.parametrize("bad", ["PI-2026-2.1", "bogus", "", "pi-2026-2"]) +def test_create_pi_rejects_non_pi_level_id(edpa_root: Path, bad: str) -> None: + with pytest.raises(ValueError): + cp.create_pi(edpa_root, bad) + + +def test_create_pi_rejects_duplicate(edpa_root: Path) -> None: + cp.create_pi(edpa_root, "PI-2026-2") + with pytest.raises(ValueError, match="already exists"): + cp.create_pi(edpa_root, "PI-2026-2") + + +def test_create_pi_rejects_bad_status(edpa_root: Path) -> None: + with pytest.raises(ValueError, match="status"): + cp.create_pi(edpa_root, "PI-2026-2", status="bogus") + + +def test_create_pi_rejects_bad_date(edpa_root: Path) -> None: + with pytest.raises(ValueError, match="start_date"): + cp.create_pi(edpa_root, "PI-2026-2", start_date="2026/07/06") + + +@pytest.mark.parametrize("weeks", [0, -1, "1"]) +def test_create_pi_rejects_bad_weeks(edpa_root: Path, weeks) -> None: + with pytest.raises(ValueError, match="iteration_weeks"): + cp.create_pi(edpa_root, "PI-2026-2", iteration_weeks=weeks) + + +# -- round-trip: file is consumed cleanly by the loader ---------------------- + +def test_created_pi_read_by_loader_no_missing_warning(edpa_root: Path) -> None: + from _pi_loader import derive_pis # noqa: E402 + cp.create_pi(edpa_root, "PI-2026-2", start_date="2026-07-06", + end_date="2026-08-02", iteration_weeks=1, pi_iterations=4, + status="active") + pis, diags = derive_pis(edpa_root) + pi = next((p for p in pis if p["id"] == "PI-2026-2"), None) + assert pi is not None + assert pi["status"] == "active" + assert pi["pi_iterations"] == 4 + # The pi: file exists, so no missing_pi_yaml warning. + assert not any(d.get("code") == "missing_pi_yaml" for d in diags) + + +# -- CLI: main() via subprocess ---------------------------------------------- + +def _run_cli(edpa_root: Path, *args: str) -> subprocess.CompletedProcess: + return subprocess.run( + [sys.executable, str(SCRIPTS / "create_pi.py"), *args], + cwd=str(edpa_root.parent), capture_output=True, text=True, + ) + + +def test_cli_creates_with_no_commit(edpa_root: Path) -> None: + r = _run_cli(edpa_root, "PI-2026-2", "--start", "2026-07-06", + "--weeks", "1", "--iterations", "5", "--no-commit") + assert r.returncode == 0, r.stderr + parsed = yaml.safe_load( + (edpa_root / "iterations" / "PI-2026-2.yaml").read_text()) + assert parsed["pi"]["id"] == "PI-2026-2" + assert parsed["pi"]["pi_iterations"] == 5 + + +def test_cli_rejects_iteration_level_id(edpa_root: Path) -> None: + r = _run_cli(edpa_root, "PI-2026-2.1", "--no-commit") + assert r.returncode == 1 + assert "PI-YYYY-N" in r.stderr + + +def test_cli_rejects_duplicate(edpa_root: Path) -> None: + assert _run_cli(edpa_root, "PI-2026-2", "--no-commit").returncode == 0 + r = _run_cli(edpa_root, "PI-2026-2", "--no-commit") + assert r.returncode == 1 + assert "already exists" in r.stderr + + +def test_cli_bad_status_is_argparse_error(edpa_root: Path) -> None: + r = _run_cli(edpa_root, "PI-2026-2", "--status", "bogus", "--no-commit") + assert r.returncode == 2 # argparse usage error diff --git a/tests/test_mcp_idempotency.py b/tests/test_mcp_idempotency.py index b02e392..5400820 100644 --- a/tests/test_mcp_idempotency.py +++ b/tests/test_mcp_idempotency.py @@ -24,6 +24,7 @@ _handle_item_transition, _handle_iteration_create, _handle_people_upsert, + _handle_pi_create, _idempotency_lookup, _idempotency_record, ) @@ -142,6 +143,17 @@ def test_iteration_create_idempotent(edpa_root: Path) -> None: assert first == second +def test_pi_create_idempotent(edpa_root: Path) -> None: + first = _parse(_handle_pi_create(edpa_root, { + "id": "PI-2026-2", "status": "active", "idempotency_key": "pi-1", + })) + # Second call would normally fail (already exists), but idempotency wins. + second = _parse(_handle_pi_create(edpa_root, { + "id": "PI-2026-2", "status": "active", "idempotency_key": "pi-1", + })) + assert first == second + + def test_people_upsert_idempotency_key_not_stored_as_field(edpa_root: Path) -> None: """idempotency_key must not leak into people.yaml.""" _parse(_handle_people_upsert(edpa_root, { diff --git a/tests/test_mcp_integration.py b/tests/test_mcp_integration.py index da57f23..124f9cc 100644 --- a/tests/test_mcp_integration.py +++ b/tests/test_mcp_integration.py @@ -229,7 +229,7 @@ def test_documented_tools_returned(self, edpa_workspace): "edpa_item_create", "edpa_item_update", "edpa_item_transition", "edpa_item_link_parent", "edpa_iteration_create", "edpa_iteration_close", - "edpa_people_upsert", + "edpa_pi_create", "edpa_people_upsert", }, f"Tool set drift: {names}" def test_edpa_item_requires_item_id(self, edpa_workspace): diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index 90ba021..4907c74 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -262,12 +262,13 @@ def test_handle_item_feature(): # --------------------------------------------------------------------------- def test_list_tools(): - """Returns the documented EDPA V2 tool surface (7 read + 7 write). + """Returns the documented EDPA V2 tool surface (7 read + 8 write). edpa_sync_people was removed in V2.0 along with sync_collaborators.py. + edpa_pi_create was added in 2.2.0. """ tools = asyncio.run(mcp_server.list_tools()) - assert len(tools) == 14 + assert len(tools) == 15 names = {t.name for t in tools} expected_read = {"edpa_status", "edpa_iterations", "edpa_people", @@ -276,7 +277,7 @@ def test_list_tools(): expected_write = {"edpa_item_create", "edpa_item_update", "edpa_item_transition", "edpa_item_link_parent", "edpa_iteration_create", "edpa_iteration_close", - "edpa_people_upsert"} + "edpa_pi_create", "edpa_people_upsert"} assert names == expected_read | expected_write # Verify each tool has a description and inputSchema diff --git a/tests/test_mcp_write_tools.py b/tests/test_mcp_write_tools.py index ce6ea20..e0d1fd9 100644 --- a/tests/test_mcp_write_tools.py +++ b/tests/test_mcp_write_tools.py @@ -1,12 +1,13 @@ """Tests for V2 MCP write tools (mcp_server.py + id_counter.py). -Covers all 7 write handlers: +Covers all 8 write handlers: - edpa_item_create - edpa_item_update - edpa_item_transition - edpa_item_link_parent - edpa_iteration_create - edpa_iteration_close + - edpa_pi_create - edpa_people_upsert Each test uses an isolated tmp .edpa/ tree (no shared state with the @@ -35,6 +36,7 @@ _handle_iteration_close, _handle_iteration_create, _handle_people_upsert, + _handle_pi_create, ) @@ -463,6 +465,60 @@ def test_iteration_create_rejects_bad_id(edpa_root: Path) -> None: })) +# --------------------------------------------------------------------------- +# edpa_pi_create (thin delegate to create_pi.py) +# --------------------------------------------------------------------------- + +def test_pi_create_writes_pi_block(edpa_root: Path) -> None: + data = _parse(_handle_pi_create(edpa_root, { + "id": "PI-2026-2", + "start_date": "2026-07-06", + "pi_iterations": 5, + "status": "active", + })) + assert data["id"] == "PI-2026-2" + assert data["path"] == ".edpa/iterations/PI-2026-2.yaml" + pi_path = edpa_root / "iterations" / "PI-2026-2.yaml" + assert pi_path.exists() + parsed = yaml.safe_load(pi_path.read_text()) + assert parsed["pi"]["id"] == "PI-2026-2" + assert parsed["pi"]["status"] == "active" + assert parsed["pi"]["iteration_weeks"] == 1 + assert parsed["pi"]["pi_iterations"] == 5 + assert parsed["pi"]["start_date"] == "2026-07-06" + # A PI-level file must NOT carry an iteration: block. + assert "iteration" not in parsed + + +def test_pi_create_defaults(edpa_root: Path) -> None: + """Minimal id → status planning, iteration_weeks 1, no dates/count.""" + _parse(_handle_pi_create(edpa_root, {"id": "PI-2026-4"})) + parsed = yaml.safe_load( + (edpa_root / "iterations" / "PI-2026-4.yaml").read_text()) + assert parsed["pi"]["status"] == "planning" + assert parsed["pi"]["iteration_weeks"] == 1 + assert "pi_iterations" not in parsed["pi"] + assert "start_date" not in parsed["pi"] + + +def test_pi_create_rejects_iteration_level_id(edpa_root: Path) -> None: + """An iteration id (with .N suffix) is not a PI-level id.""" + result = _handle_pi_create(edpa_root, {"id": "PI-2026-2.1"}) + assert _is_err(result) + assert "PI-YYYY-N" in result[0].text + + +def test_pi_create_rejects_bad_id(edpa_root: Path) -> None: + assert _is_err(_handle_pi_create(edpa_root, {"id": "bogus"})) + + +def test_pi_create_rejects_duplicate(edpa_root: Path) -> None: + _parse(_handle_pi_create(edpa_root, {"id": "PI-2026-2"})) + result = _handle_pi_create(edpa_root, {"id": "PI-2026-2"}) + assert _is_err(result) + assert "already exists" in result[0].text + + # --------------------------------------------------------------------------- # edpa_iteration_close # --------------------------------------------------------------------------- diff --git a/web/package-lock.json b/web/package-lock.json index 5812249..b5f4fcb 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "edpa-web", - "version": "2.1.9", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "edpa-web", - "version": "2.1.9", + "version": "2.2.0", "dependencies": { "@vercel/analytics": "^2.0.1", "astro": "^5.7.0", diff --git a/web/package.json b/web/package.json index 94fd6ea..6e81e64 100644 --- a/web/package.json +++ b/web/package.json @@ -1,7 +1,7 @@ { "name": "edpa-web", "type": "module", - "version": "2.1.9", + "version": "2.2.0", "private": true, "scripts": { "dev": "astro dev", diff --git a/web/src/pages/en/setup.astro b/web/src/pages/en/setup.astro index 83c387b..c0397e2 100644 --- a/web/src/pages/en/setup.astro +++ b/web/src/pages/en/setup.astro @@ -885,7 +885,7 @@ import Layout from '../../layouts/Layout.astro'; `# Generated: ${new Date().toISOString().split('T')[0]}`, '', 'edpa:', - ' version: "2.1.9"', + ' version: "2.2.0"', '', '# Optional: GitHub repo for contribution-sync (--with-ci). Local-first;', '# omit this block if you are not using the PR-signal CI workflow.', @@ -941,7 +941,7 @@ import Layout from '../../layouts/Layout.astro'; lines.push(' vat_id: ""'); lines.push(''); lines.push('governance:'); - lines.push(' methodology: "EDPA 2.1.9"'); + lines.push(' methodology: "EDPA 2.2.0"'); return lines.join('\n'); } diff --git a/web/src/pages/setup.astro b/web/src/pages/setup.astro index 7e7aae5..3ea0de2 100644 --- a/web/src/pages/setup.astro +++ b/web/src/pages/setup.astro @@ -885,7 +885,7 @@ import Layout from '../layouts/Layout.astro'; `# Generated: ${new Date().toISOString().split('T')[0]}`, '', 'edpa:', - ' version: "2.1.9"', + ' version: "2.2.0"', '', '# Optional: GitHub repo for contribution-sync (--with-ci). Local-first;', '# omit this block if you are not using the PR-signal CI workflow.', @@ -941,7 +941,7 @@ import Layout from '../layouts/Layout.astro'; lines.push(' vat_id: ""'); lines.push(''); lines.push('governance:'); - lines.push(' methodology: "EDPA 2.1.9"'); + lines.push(' methodology: "EDPA 2.2.0"'); return lines.join('\n'); }