diff --git a/AGENTS.md b/AGENTS.md index 8b08265..9965809 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -204,9 +204,12 @@ read/cache/index layer that rebuilds from files. the completion/archive date; the frontmatter `id` does not change. **Ad Hoc Tasks:** small opportunistic fixes discovered during a session use -`workplans/ADHOC-YYYY-MM-DD.md` with task ids `ADHOC-YYYY-MM-DD-T01`, etc. Use -this only for low-risk work completed directly; create a normal workplan for -anything needing analysis, design, approval, dependencies, or multiple phases. +`workplans/ADHOC-YYYY-MM-DD.md`, workplan id +`STATE-WP-ADHOC-YYYY-MM-DD`, and task ids +`STATE-WP-ADHOC-YYYY-MM-DD-T01`, etc. Unqualified historic `ADHOC-*` ids are +grandfathered and must not be copied into new records. Use this only for +low-risk work completed directly; create a normal workplan for anything needing +analysis, design, approval, dependencies, or multiple phases. **Frontmatter:** diff --git a/scripts/project_rules/agents-codex.template b/scripts/project_rules/agents-codex.template index e53796a..9118037 100644 --- a/scripts/project_rules/agents-codex.template +++ b/scripts/project_rules/agents-codex.template @@ -150,9 +150,13 @@ read/cache/index layer that rebuilds from files. the completion/archive date; the frontmatter `id` does not change. **Ad Hoc Tasks:** small opportunistic fixes discovered during a session use -`workplans/ADHOC-YYYY-MM-DD.md` with task ids `ADHOC-YYYY-MM-DD-T01`, etc. Use -this only for low-risk work completed directly; create a normal workplan for -anything needing analysis, design, approval, dependencies, or multiple phases. +`workplans/ADHOC-YYYY-MM-DD.md`, workplan id +`{WP_PREFIX}-ADHOC-YYYY-MM-DD`, and task ids +`{WP_PREFIX}-ADHOC-YYYY-MM-DD-T01`, etc. `{WP_PREFIX}` includes its final `-WP` +token. Unqualified historic `ADHOC-*` ids are grandfathered and must not be +copied into new records. Use this only for low-risk work completed directly; +create a normal workplan for anything needing analysis, design, approval, +dependencies, or multiple phases. **Frontmatter:** diff --git a/scripts/project_rules/workplan-convention.template b/scripts/project_rules/workplan-convention.template index 976532d..2e4ed96 100644 --- a/scripts/project_rules/workplan-convention.template +++ b/scripts/project_rules/workplan-convention.template @@ -16,10 +16,13 @@ prefix: `YYMMDD-{WP_PREFIX}-NNNN-.md`. The frontmatter id remains unchanged; the prefix is only for quick visual reference. Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**: -`workplans/ADHOC-YYYY-MM-DD.md`, workplan slug `adhoc-YYYY-MM-DD`, and task ids -`ADHOC-YYYY-MM-DD-T01`, `T02`, etc. Use adhocs only for low-risk work completed -directly. Promote anything requiring analysis, design, approval, dependencies, or -multiple planned phases into a normal workplan. +`workplans/ADHOC-YYYY-MM-DD.md`, workplan id +`{WP_PREFIX}-ADHOC-YYYY-MM-DD`, and task ids +`{WP_PREFIX}-ADHOC-YYYY-MM-DD-T01`, `T02`, etc. `{WP_PREFIX}` includes its final +`-WP` token. Unqualified historic `ADHOC-*` ids are grandfathered and must not +be copied into new records. Use adhocs only for low-risk work completed directly. +Promote anything requiring analysis, design, approval, dependencies, or multiple +planned phases into a normal workplan. Ecosystem todos from other agents arrive as `[repo:{REPO_SLUG}]` hub tasks — visible at session start. Pick one up by creating the workplan file, committing, diff --git a/scripts/validate_repo_adr.py b/scripts/validate_repo_adr.py index 8f7bf5c..7f23ad0 100644 --- a/scripts/validate_repo_adr.py +++ b/scripts/validate_repo_adr.py @@ -74,8 +74,15 @@ SUPPORTED_WP_STATUSES = set(SUPPORTED_WORKSTREAM_STATUSES) VALID_TASK_STATUSES = set(CANONICAL_TASK_STATUSES) VALID_TASK_PRIORITIES = {"low", "medium", "high", "critical"} -_WP_ID_RE = re.compile(r"^(?:[A-Z]+-WP-\d+|ADHOC-\d{4}-\d{2}-\d{2})$") -_TASK_ID_RE = re.compile(r"^(?:[A-Z]+-WP-\d+|ADHOC-\d{4}-\d{2}-\d{2})-T\d+$") +_PREFIX_RE = r"[A-Z][A-Z0-9]*(?:-[A-Z][A-Z0-9]*)*" +_WP_ID_RE = re.compile( + rf"^(?:{_PREFIX_RE}-WP-\d+|{_PREFIX_RE}-WP-ADHOC-\d{{4}}-\d{{2}}-\d{{2}}" + r"|ADHOC-\d{4}-\d{2}-\d{2})$" +) +_TASK_ID_RE = re.compile( + rf"^(?:{_PREFIX_RE}-WP-\d+|{_PREFIX_RE}-WP-ADHOC-\d{{4}}-\d{{2}}-\d{{2}}" + r"|ADHOC-\d{4}-\d{2}-\d{2})-T\d+$" +) _TASK_BLOCK_RE = re.compile(r"```task\s*\n(.*?)\n```", re.DOTALL) _ARCHIVED_WP_RE = re.compile(r"^\d{6}-(.+\.md)$") @@ -223,12 +230,18 @@ def _check_workplan_file(wp_file: Path, report: Report) -> dict | None: wp_id = str(meta.get("id", "")) if not _WP_ID_RE.match(wp_id): report.add(Level.FAIL, "frontmatter-id-format", - f"id must match [A-Z]+-WP-\\d+ (e.g. CUST-WP-0001), got {wp_id!r}", fname) + "id must be a numbered or repository-qualified ad-hoc " + f"workplan id, got {wp_id!r}", fname) else: report.add(Level.PASS, "frontmatter-id-format", f"id={wp_id}", fname) # filename prefix - if wp_id and not canonical_fname.startswith(wp_id): + adhoc_filename_id = Path(canonical_fname).stem + qualified_adhoc_filename = ( + adhoc_filename_id.startswith("ADHOC-") + and wp_id.endswith(f"-WP-{adhoc_filename_id}") + ) + if wp_id and not canonical_fname.startswith(wp_id) and not qualified_adhoc_filename: report.add(Level.WARN, "filename-id-prefix", f"Filename should start with id '{wp_id}', got {fname!r}", fname) elif wp_id: @@ -260,7 +273,7 @@ def _check_workplan_file(wp_file: Path, report: Report) -> dict | None: report.add(Level.FAIL, "task-id", "Missing 'id' field", tref) elif not _TASK_ID_RE.match(t_id): report.add(Level.WARN, "task-id-format", - f"id {t_id!r} doesn't match [A-Z]+-WP-\\d+-T\\d+", tref) + f"id {t_id!r} is not a canonical or grandfathered task id", tref) t_status = str(task.get("status", "")) if not t_status: diff --git a/tests/test_validate_repo_adr.py b/tests/test_validate_repo_adr.py new file mode 100644 index 0000000..ac16ca6 --- /dev/null +++ b/tests/test_validate_repo_adr.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from scripts.validate_repo_adr import Report, _check_workplan_file + + +def test_repository_qualified_ad_hoc_id_matches_daily_filename(tmp_path) -> None: + workplans = tmp_path / "workplans" + workplans.mkdir() + path = workplans / "ADHOC-2026-08-23.md" + path.write_text( + """--- +id: ACTIVITY-WP-ADHOC-2026-08-23 +type: workplan +title: Daily repair +domain: infotech +status: finished +owner: codex +created: "2026-08-23" +--- + +## Repair + +```task +id: ACTIVITY-WP-ADHOC-2026-08-23-T01 +status: done +priority: low +``` +""", + encoding="utf-8", + ) + report = Report(repo_path=str(tmp_path)) + + _check_workplan_file(path, report) + + assert not report.failures + assert not [finding for finding in report.warnings if finding.check == "filename-id-prefix"] diff --git a/workplans/STATE-WP-0079-retirement-strangler.md b/workplans/STATE-WP-0079-retirement-strangler.md index 760de4b..3b21b18 100644 --- a/workplans/STATE-WP-0079-retirement-strangler.md +++ b/workplans/STATE-WP-0079-retirement-strangler.md @@ -356,6 +356,15 @@ that omit `domain_slug`. Two regression tests cover both paths; all 131 consistency-check tests pass. This is a compatibility fix until the generator moves or retires, not new permanent State Hub authority. +**Ad-hoc identity compatibility repair (2026-08-23):** the surviving project +instruction templates prescribed fleet-wide `ADHOC-YYYY-MM-DD` identifiers. +Activity Core and Net Kingdom used that form on the same day, and deterministic +derivation correctly refused the second record. Templates and the legacy ADR +validator now accept and prescribe `{WP_PREFIX}-ADHOC-YYYY-MM-DD` while keeping +the daily filename unchanged. A regression test covers the filename/id pairing. +This prevents new collisions without making State Hub the long-term convention +owner. + ## Retire legacy surfaces ```task