From 887578f0ebc37602b759b3e38def1285106d32e8 Mon Sep 17 00:00:00 2001 From: Paul O'Reilly Date: Sun, 4 Oct 2026 09:36:04 +1300 Subject: [PATCH] feat(templates): explicit cp_write on every kind: script node + audit test (WF-COMP-31d, item 26754227) cp_write: true -> state-tx (test-validate@1, frozen-fixture-emit@1) and, in the archived bug-ancestor-auto-advance@1, tag-set + state-tx. cp_write: false -> every other script node (verdict-gate, policy-pick, preflight-*, frozen-fixture-emit, parent-link-check). tests/invariants/test_cp_write_field_set_correctly.py pins the set. HOLD: do not merge until the CP release that adds WorkflowNode.cp_write. The current CP schema is extra="forbid" and rejects the new key (verified: nodes.0.cp_write extra_forbidden), so on the old CP these template edits are NOT inert. Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF --- .../bug-ancestor-auto-advance@1.yaml | 3 + .../workflow/feature-delivery-loop@1.yaml | 1 + .../workflow/frozen-fixture-emit@1.yaml | 2 + .../workflow/plan-review-loop@1.yaml | 1 + .../workflow/spec-review-loop@1.yaml | 1 + task-templates/workflow/test-validate@1.yaml | 4 + .../workflow/test-write-review-loop@1.yaml | 1 + .../test_cp_write_field_set_correctly.py | 87 +++++++++++++++++++ 8 files changed, 100 insertions(+) create mode 100644 tests/invariants/test_cp_write_field_set_correctly.py diff --git a/task-templates/workflow/_archived/bug-ancestor-auto-advance@1.yaml b/task-templates/workflow/_archived/bug-ancestor-auto-advance@1.yaml index fc90d33..a32f3ab 100644 --- a/task-templates/workflow/_archived/bug-ancestor-auto-advance@1.yaml +++ b/task-templates/workflow/_archived/bug-ancestor-auto-advance@1.yaml @@ -31,6 +31,7 @@ nodes: - id: parent-link-check kind: script cmd: scripts/workflow/parent-link-check + cp_write: false on_fail: refuse_dispatch runtime_env: CP_URL: "https://agents.oreillyit.nz/api" @@ -42,6 +43,7 @@ nodes: - id: tag-ai-resolve kind: script cmd: scripts/workflow/tag-set + cp_write: true depends_on: [parent-link-check] on_fail: escalate runtime_env: @@ -52,6 +54,7 @@ nodes: - id: state-tx kind: script cmd: scripts/workflow/state-tx + cp_write: true depends_on: [tag-ai-resolve] on_fail: escalate runtime_env: diff --git a/task-templates/workflow/feature-delivery-loop@1.yaml b/task-templates/workflow/feature-delivery-loop@1.yaml index 12b1a55..bbbf133 100644 --- a/task-templates/workflow/feature-delivery-loop@1.yaml +++ b/task-templates/workflow/feature-delivery-loop@1.yaml @@ -23,6 +23,7 @@ nodes: - id: policy-pick kind: script cmd: scripts/workflow/policy-pick + cp_write: false on_fail: escalate - id: agent kind: agent diff --git a/task-templates/workflow/frozen-fixture-emit@1.yaml b/task-templates/workflow/frozen-fixture-emit@1.yaml index 7857c17..c827171 100644 --- a/task-templates/workflow/frozen-fixture-emit@1.yaml +++ b/task-templates/workflow/frozen-fixture-emit@1.yaml @@ -18,10 +18,12 @@ nodes: - id: frozen-fixture-emit kind: script cmd: scripts/workflow/frozen-fixture-emit + cp_write: false on_fail: refuse_dispatch - id: state-tx kind: script cmd: scripts/workflow/state-tx + cp_write: true on_fail: continue runtime_env: CP_URL: "https://agents.oreillyit.nz/api" diff --git a/task-templates/workflow/plan-review-loop@1.yaml b/task-templates/workflow/plan-review-loop@1.yaml index 785c0fe..ad5e185 100644 --- a/task-templates/workflow/plan-review-loop@1.yaml +++ b/task-templates/workflow/plan-review-loop@1.yaml @@ -119,6 +119,7 @@ nodes: - id: judge-gate kind: script cmd: scripts/workflow/verdict-gate + cp_write: false depends_on: [judge-haiku] loop: body: [review-opus, review-minimax, improve-sonnet, judge-haiku, judge-gate] diff --git a/task-templates/workflow/spec-review-loop@1.yaml b/task-templates/workflow/spec-review-loop@1.yaml index fe5764e..00baa68 100644 --- a/task-templates/workflow/spec-review-loop@1.yaml +++ b/task-templates/workflow/spec-review-loop@1.yaml @@ -129,6 +129,7 @@ nodes: - id: judge-gate kind: script cmd: scripts/workflow/verdict-gate + cp_write: false depends_on: [judge-haiku] loop: body: [review-opus, review-minimax, improve-sonnet, judge-haiku, judge-gate] diff --git a/task-templates/workflow/test-validate@1.yaml b/task-templates/workflow/test-validate@1.yaml index 03da882..38fec1f 100644 --- a/task-templates/workflow/test-validate@1.yaml +++ b/task-templates/workflow/test-validate@1.yaml @@ -18,18 +18,22 @@ nodes: - id: preflight-test-collection kind: script cmd: scripts/workflow/preflight-test-collection + cp_write: false on_fail: refuse_dispatch - id: preflight-fixture-lint kind: script cmd: scripts/workflow/preflight-fixture-lint + cp_write: false on_fail: refuse_dispatch - id: preflight-test-fail-mode kind: script cmd: scripts/workflow/preflight-test-fail-mode + cp_write: false on_fail: continue - id: state-tx kind: script cmd: scripts/workflow/state-tx + cp_write: true on_fail: continue runtime_env: CP_URL: "https://agents.oreillyit.nz/api" diff --git a/task-templates/workflow/test-write-review-loop@1.yaml b/task-templates/workflow/test-write-review-loop@1.yaml index 221a019..48eee70 100644 --- a/task-templates/workflow/test-write-review-loop@1.yaml +++ b/task-templates/workflow/test-write-review-loop@1.yaml @@ -61,6 +61,7 @@ nodes: - id: review-gate kind: script cmd: scripts/workflow/verdict-gate + cp_write: false depends_on: [review-sonnet] loop: body: [write-minimax, review-sonnet, review-gate] diff --git a/tests/invariants/test_cp_write_field_set_correctly.py b/tests/invariants/test_cp_write_field_set_correctly.py new file mode 100644 index 0000000..2a6c3b3 --- /dev/null +++ b/tests/invariants/test_cp_write_field_set_correctly.py @@ -0,0 +1,87 @@ +"""Every shipped kind: script node declares cp_write explicitly (WF-COMP-31d). + +``cp_write: true`` selects the workflow-script-write/v1 composite (AU-55 +per-step token); ``cp_write: false`` selects workflow-script-nocp/v1 (no CP +credential). The declaration must match what the script actually does: +state-tx / state-tx-verdict / tag-set write the run's own item (true); every +other script, including parent-link-check and decompose-and-queue (whose +routes the AU-57 credential cannot serve), is false. + +Archived templates are included: the explicit declaration is the audit pin. +""" +from __future__ import annotations + +from pathlib import Path + +import pytest +import yaml + +_DIR = Path(__file__).resolve().parents[2] / "task-templates" + +_CP_WRITE_TRUE = {"state-tx", "state-tx-verdict", "tag-set"} +_CP_WRITE_FALSE = { + "verdict-gate", "policy-pick", "frozen-fixture-emit", + "preflight-test-collection", "preflight-fixture-lint", "preflight-test-fail-mode", + "parent-link-check", "decompose-and-queue", +} + + +def _script_nodes() -> list[tuple[str, str, dict]]: + out = [] + for p in sorted(_DIR.rglob("*.yaml")): + doc = yaml.safe_load(p.read_text()) or {} + if not isinstance(doc, dict): + continue + for n in doc.get("nodes") or []: + if isinstance(n, dict) and n.get("kind") == "script": + out.append((str(p.relative_to(_DIR)), n["id"], n)) + return out + + +_NODES = _script_nodes() +_IDS = [f"{f}::{nid}" for f, nid, _ in _NODES] + + +def test_script_nodes_discovered(): + files = {f for f, _, _ in _NODES} + assert { + "workflow/test-validate@1.yaml", + "workflow/frozen-fixture-emit@1.yaml", + "workflow/plan-review-loop@1.yaml", + "workflow/spec-review-loop@1.yaml", + "workflow/test-write-review-loop@1.yaml", + "workflow/feature-delivery-loop@1.yaml", + "workflow/_archived/bug-ancestor-auto-advance@1.yaml", + } <= files + + +@pytest.mark.parametrize("fname,nid,node", _NODES, ids=_IDS) +def test_script_node_declares_cp_write_explicitly_and_correctly(fname, nid, node): + assert "cp_write" in node, f"{fname}::{nid} must declare cp_write explicitly (WF-COMP-31d)" + assert isinstance(node["cp_write"], bool), f"{fname}::{nid} cp_write must be a bool" + script = node["cmd"].rsplit("/", 1)[-1] + assert script in _CP_WRITE_TRUE | _CP_WRITE_FALSE, ( + f"{fname}::{nid} runs unclassified script {script!r}; classify it in " + "this test (does it call the CP?) and AU-57 before shipping" + ) + assert node["cp_write"] is (script in _CP_WRITE_TRUE), ( + f"{fname}::{nid}: cp_write must be {script in _CP_WRITE_TRUE} for {script}" + ) + + +def test_wf_comp_31d_cp_write_field_audit_per_framework_template(): + """Spec WF-COMP-31d framework-repo test handle: pin the declarations.""" + got = {(f, nid): n["cp_write"] for f, nid, n in _NODES} + assert got[("workflow/test-validate@1.yaml", "state-tx")] is True + assert got[("workflow/frozen-fixture-emit@1.yaml", "state-tx")] is True + assert got[("workflow/frozen-fixture-emit@1.yaml", "frozen-fixture-emit")] is False + for nid in ("preflight-test-collection", "preflight-fixture-lint", "preflight-test-fail-mode"): + assert got[("workflow/test-validate@1.yaml", nid)] is False + assert got[("workflow/plan-review-loop@1.yaml", "judge-gate")] is False + assert got[("workflow/spec-review-loop@1.yaml", "judge-gate")] is False + assert got[("workflow/test-write-review-loop@1.yaml", "review-gate")] is False + assert got[("workflow/feature-delivery-loop@1.yaml", "policy-pick")] is False + arch = "workflow/_archived/bug-ancestor-auto-advance@1.yaml" + assert got[(arch, "parent-link-check")] is False + assert got[(arch, "tag-ai-resolve")] is True + assert got[(arch, "state-tx")] is True