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
This commit is contained in:
Paul O'Reilly
2026-10-04 09:36:04 +13:00
parent 08ca057687
commit 887578f0eb
8 changed files with 100 additions and 0 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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"

View File

@@ -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]

View File

@@ -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]

View File

@@ -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"

View File

@@ -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]

View File

@@ -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