@ikon85/agent-workflow-kit 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/board-to-waves/SKILL.md +13 -3
- package/.agents/skills/to-issues/SKILL.md +102 -27
- package/.agents/skills/to-prd/SKILL.md +13 -3
- package/.agents/skills/to-waves/SKILL.md +16 -4
- package/.agents/skills/wrapup/SKILL.md +0 -1
- package/.claude/skills/board-to-waves/SKILL.md +13 -3
- package/.claude/skills/codex-build/SKILL.md +69 -23
- package/.claude/skills/codex-review/SKILL.md +47 -37
- package/.claude/skills/grill-me-codex/SKILL.md +45 -34
- package/.claude/skills/grill-with-docs-codex/SKILL.md +46 -34
- package/.claude/skills/to-issues/SKILL.md +102 -27
- package/.claude/skills/to-prd/SKILL.md +13 -3
- package/.claude/skills/to-waves/SKILL.md +16 -4
- package/.claude/skills/wrapup/SKILL.md +0 -1
- package/README.md +23 -0
- package/agent-workflow-kit.package.json +56 -16
- package/docs/research/wave-152-consumer-acceptance.md +98 -0
- package/package.json +1 -1
- package/scripts/board-sync.py +3 -7
- package/scripts/build-kit.test.mjs +42 -1
- package/scripts/codex-exec-scenarios/fake-codex.mjs +152 -0
- package/scripts/codex-exec.sh +566 -0
- package/scripts/codex-exec.test.mjs +815 -0
- package/scripts/codex_proc.py +602 -0
- package/scripts/find-by-marker.py +68 -0
- package/scripts/marker_lib.py +88 -0
- package/scripts/render-anchor.py +111 -0
- package/scripts/test_marker_lib.py +154 -0
- package/scripts/test_render_anchor.py +267 -0
- package/scripts/test_retro_wrapup_contract.py +6 -0
- package/scripts/test_skill_codex_exec_lifecycle.py +123 -0
- package/src/lib/bundle.mjs +10 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared exact-value HTML marker lookup for issue identity."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
MARKER_KINDS = frozenset({
|
|
12
|
+
"prd-source-id",
|
|
13
|
+
"program-leaf-source",
|
|
14
|
+
"program-stub-source",
|
|
15
|
+
"wave-stub-source",
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UnknownMarkerKind(ValueError):
|
|
20
|
+
"""Raised when a caller requests a marker outside the public grammar."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def marker_value(body: str, kind: str) -> str | None:
|
|
24
|
+
"""Return one allowlisted marker value from a body, trimmed exactly once."""
|
|
25
|
+
if kind not in MARKER_KINDS:
|
|
26
|
+
raise UnknownMarkerKind(f"unknown marker kind: {kind}")
|
|
27
|
+
match = re.search(rf"<!--\s*{re.escape(kind)}:\s*([^>]+?)\s*-->", body)
|
|
28
|
+
return match.group(1).strip() if match else None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def first_marker(body: str, kinds: tuple[str, ...]) -> tuple[str, str] | None:
|
|
32
|
+
"""Return the first body-ordered marker among validated allowlisted kinds."""
|
|
33
|
+
unknown = next((kind for kind in kinds if kind not in MARKER_KINDS), None)
|
|
34
|
+
if unknown:
|
|
35
|
+
raise UnknownMarkerKind(f"unknown marker kind: {unknown}")
|
|
36
|
+
alternatives = "|".join(re.escape(kind) for kind in kinds)
|
|
37
|
+
match = re.search(rf"<!--\s*({alternatives}):\s*([^>]+?)\s*-->", body)
|
|
38
|
+
return (match.group(1), match.group(2).strip()) if match else None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _issues_from_pages(raw: str) -> list[dict]:
|
|
42
|
+
pages = json.loads(raw)
|
|
43
|
+
if not pages:
|
|
44
|
+
return []
|
|
45
|
+
if isinstance(pages[0], dict):
|
|
46
|
+
return pages
|
|
47
|
+
return [issue for page in pages for issue in page]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def marker_verdict(issues: list[dict]) -> str:
|
|
51
|
+
"""Map exact identity matches onto the four-way consumer decision."""
|
|
52
|
+
if not issues:
|
|
53
|
+
return "create"
|
|
54
|
+
if len(issues) > 1:
|
|
55
|
+
return "STOP"
|
|
56
|
+
return "update" if issues[0]["state"] == "open" else "user-decision"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def find_by_marker(repo: str, kind: str, slug: str,
|
|
60
|
+
gh: Callable[[list[str]], str]) -> dict:
|
|
61
|
+
"""Scan every issue state and return exact marker matches plus a verdict."""
|
|
62
|
+
marker_value("", kind) # validate before making a network call
|
|
63
|
+
raw = gh([
|
|
64
|
+
"api", "--paginate", "--slurp", "--method", "GET",
|
|
65
|
+
f"repos/{repo}/issues", "-f", "state=all", "-f", "per_page=100",
|
|
66
|
+
])
|
|
67
|
+
matches = [
|
|
68
|
+
{"number": issue["number"], "state": issue["state"]}
|
|
69
|
+
for issue in _issues_from_pages(raw)
|
|
70
|
+
if "pull_request" not in issue
|
|
71
|
+
and marker_value(issue.get("body") or "", kind) == slug
|
|
72
|
+
]
|
|
73
|
+
return {
|
|
74
|
+
"count": len(matches),
|
|
75
|
+
"issues": matches,
|
|
76
|
+
"verdict": marker_verdict(matches),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def reconcile_after_create(repo: str, kind: str, slug: str,
|
|
81
|
+
created_number: int,
|
|
82
|
+
gh: Callable[[list[str]], str]) -> dict:
|
|
83
|
+
"""Re-scan after create and fail closed unless that open issue is unique."""
|
|
84
|
+
result = find_by_marker(repo, kind, slug, gh)
|
|
85
|
+
expected = [{"number": created_number, "state": "open"}]
|
|
86
|
+
if result["issues"] != expected:
|
|
87
|
+
result["verdict"] = "STOP"
|
|
88
|
+
return result
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Pure rendering for a lean Tier-2 anchor and its full PRD archive."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import NamedTuple
|
|
11
|
+
|
|
12
|
+
from marker_lib import marker_value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
PLAN_REVISION_RE = re.compile(
|
|
16
|
+
r"^\s*\*\*plan_revision:\*\*\s*(r\d+)\s*$"
|
|
17
|
+
)
|
|
18
|
+
PLAN_REVISION_LOOKALIKE_RE = re.compile(r"^\s*\*\*plan_revision:\*\*.*$")
|
|
19
|
+
HTML_MARKER_RE = re.compile(
|
|
20
|
+
r"^\s*<!--\s*(prd-source-id|prd-content-fp|prd):\s*([^>\r\n]+?)\s*-->\s*$"
|
|
21
|
+
)
|
|
22
|
+
HTML_COMMENT_RE = re.compile(r"^\s*<!--.*-->\s*$")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RenderedDocuments(NamedTuple):
|
|
26
|
+
"""The two byte-stable documents produced by one render."""
|
|
27
|
+
|
|
28
|
+
anchor_body: str
|
|
29
|
+
archive_body: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _canonical_marker(line: str) -> tuple[str, str] | None:
|
|
33
|
+
revision = PLAN_REVISION_RE.fullmatch(line)
|
|
34
|
+
if revision:
|
|
35
|
+
return ("plan_revision", revision.group(1))
|
|
36
|
+
marker = HTML_MARKER_RE.fullmatch(line)
|
|
37
|
+
if not marker:
|
|
38
|
+
return None
|
|
39
|
+
kind = marker.group(1)
|
|
40
|
+
if kind == "prd-source-id":
|
|
41
|
+
value = marker_value(line, kind)
|
|
42
|
+
return (kind, value) if value is not None else None
|
|
43
|
+
return (kind, "")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _strip_head_markers(source: str) -> tuple[list[str], str]:
|
|
47
|
+
revisions: list[str] = []
|
|
48
|
+
end = 0
|
|
49
|
+
kept: list[str] = []
|
|
50
|
+
for line in source.splitlines(keepends=True):
|
|
51
|
+
bare = line.rstrip("\r\n")
|
|
52
|
+
marker = _canonical_marker(bare)
|
|
53
|
+
if (
|
|
54
|
+
not bare.strip()
|
|
55
|
+
or marker
|
|
56
|
+
or HTML_COMMENT_RE.fullmatch(bare)
|
|
57
|
+
or PLAN_REVISION_LOOKALIKE_RE.fullmatch(bare)
|
|
58
|
+
):
|
|
59
|
+
end += len(line)
|
|
60
|
+
if marker:
|
|
61
|
+
if marker[0] == "plan_revision":
|
|
62
|
+
revisions.append(marker[1])
|
|
63
|
+
else:
|
|
64
|
+
kept.append(line)
|
|
65
|
+
continue
|
|
66
|
+
break
|
|
67
|
+
if not any(line.strip() for line in kept):
|
|
68
|
+
kept = []
|
|
69
|
+
while kept and not kept[0].strip():
|
|
70
|
+
kept.pop(0)
|
|
71
|
+
return revisions, "".join(kept) + source[end:]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def render_documents(anchor_template: str, prd_body: str) -> RenderedDocuments:
|
|
75
|
+
"""Return the filled anchor template and marker-free PRD archive."""
|
|
76
|
+
revisions, archive_source = _strip_head_markers(prd_body)
|
|
77
|
+
if len(revisions) != 1:
|
|
78
|
+
raise ValueError("source PRD head must have exactly one canonical plan_revision")
|
|
79
|
+
revision = revisions[0]
|
|
80
|
+
archive_header = (
|
|
81
|
+
f"📄 Full PRD (archive, {revision}) — "
|
|
82
|
+
"the body carries navigation/decisions only\n\n"
|
|
83
|
+
)
|
|
84
|
+
return RenderedDocuments(anchor_template, archive_header + archive_source)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
88
|
+
parser = argparse.ArgumentParser(
|
|
89
|
+
description="Render one filled Tier-2 anchor or its marker-free PRD archive."
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument("--template", type=Path, required=True)
|
|
92
|
+
parser.add_argument("--prd", type=Path, required=True)
|
|
93
|
+
parser.add_argument("--document", choices=("anchor", "archive"), required=True)
|
|
94
|
+
return parser.parse_args(argv)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main(argv: list[str] | None = None) -> int:
|
|
98
|
+
args = _parse_args(argv)
|
|
99
|
+
rendered = render_documents(
|
|
100
|
+
args.template.read_bytes().decode("utf-8"),
|
|
101
|
+
args.prd.read_bytes().decode("utf-8"),
|
|
102
|
+
)
|
|
103
|
+
output = (
|
|
104
|
+
rendered.anchor_body if args.document == "anchor" else rendered.archive_body
|
|
105
|
+
)
|
|
106
|
+
sys.stdout.buffer.write(output.encode("utf-8"))
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Behavior tests for marker identity lookup and reconciliation."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import importlib.util
|
|
8
|
+
import io
|
|
9
|
+
import subprocess
|
|
10
|
+
import unittest
|
|
11
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from unittest.mock import patch
|
|
14
|
+
|
|
15
|
+
from scripts.marker_lib import find_by_marker, reconcile_after_create
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
CLI_SPEC = importlib.util.spec_from_file_location(
|
|
19
|
+
"find_by_marker_cli_test", Path(__file__).with_name("find-by-marker.py"))
|
|
20
|
+
cli = importlib.util.module_from_spec(CLI_SPEC)
|
|
21
|
+
assert CLI_SPEC.loader is not None
|
|
22
|
+
CLI_SPEC.loader.exec_module(cli)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MarkerLookupTest(unittest.TestCase):
|
|
26
|
+
def test_pagination_joins_all_pages(self):
|
|
27
|
+
calls: list[list[str]] = []
|
|
28
|
+
|
|
29
|
+
def fake_gh(args: list[str]) -> str:
|
|
30
|
+
calls.append(args)
|
|
31
|
+
return json.dumps([
|
|
32
|
+
[{"number": 11, "state": "open", "body":
|
|
33
|
+
"<!-- prd-source-id: alpha -->"}],
|
|
34
|
+
[{"number": 12, "state": "closed", "body":
|
|
35
|
+
"<!-- prd-source-id: alpha -->"}],
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
result = find_by_marker("example/repo", "prd-source-id", "alpha", fake_gh)
|
|
39
|
+
|
|
40
|
+
self.assertEqual(result["count"], 2)
|
|
41
|
+
self.assertEqual(result["issues"], [
|
|
42
|
+
{"number": 11, "state": "open"},
|
|
43
|
+
{"number": 12, "state": "closed"},
|
|
44
|
+
])
|
|
45
|
+
self.assertIn("--paginate", calls[0])
|
|
46
|
+
self.assertIn("state=all", calls[0])
|
|
47
|
+
|
|
48
|
+
def test_pull_requests_are_discarded_before_matching(self):
|
|
49
|
+
marker = "<!-- wave-stub-source: same -->"
|
|
50
|
+
payload = [[
|
|
51
|
+
{"number": 21, "state": "open", "body": marker,
|
|
52
|
+
"pull_request": {"url": "https://api.example/pr/21"}},
|
|
53
|
+
{"number": 22, "state": "open", "body": marker},
|
|
54
|
+
]]
|
|
55
|
+
|
|
56
|
+
result = find_by_marker(
|
|
57
|
+
"example/repo", "wave-stub-source", "same",
|
|
58
|
+
lambda _args: json.dumps(payload),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self.assertEqual(result["issues"], [{"number": 22, "state": "open"}])
|
|
62
|
+
|
|
63
|
+
def test_closed_only_match_requires_user_decision(self):
|
|
64
|
+
payload = [[{"number": 31, "state": "closed", "body":
|
|
65
|
+
"<!-- prd-source-id: retired -->"}]]
|
|
66
|
+
|
|
67
|
+
result = find_by_marker(
|
|
68
|
+
"example/repo", "prd-source-id", "retired",
|
|
69
|
+
lambda _args: json.dumps(payload),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self.assertEqual(result["verdict"], "user-decision")
|
|
73
|
+
|
|
74
|
+
def test_marker_value_is_exact_not_a_prefix_match(self):
|
|
75
|
+
payload = [[
|
|
76
|
+
{"number": 41, "state": "open", "body":
|
|
77
|
+
"<!-- prd-source-id: alpha-long -->"},
|
|
78
|
+
{"number": 42, "state": "open", "body":
|
|
79
|
+
"<!-- prd-source-id: alpha -->"},
|
|
80
|
+
]]
|
|
81
|
+
|
|
82
|
+
result = find_by_marker(
|
|
83
|
+
"example/repo", "prd-source-id", "alpha",
|
|
84
|
+
lambda _args: json.dumps(payload),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
self.assertEqual(result["issues"], [{"number": 42, "state": "open"}])
|
|
88
|
+
self.assertEqual(result["verdict"], "update")
|
|
89
|
+
|
|
90
|
+
def test_post_create_rescan_stops_and_reports_both_duplicate_numbers(self):
|
|
91
|
+
marker = "<!-- program-leaf-source: program-x/1a -->"
|
|
92
|
+
responses = iter([
|
|
93
|
+
[[]],
|
|
94
|
+
[[
|
|
95
|
+
{"number": 101, "state": "open", "body": marker},
|
|
96
|
+
{"number": 102, "state": "open", "body": marker},
|
|
97
|
+
]],
|
|
98
|
+
])
|
|
99
|
+
calls: list[list[str]] = []
|
|
100
|
+
|
|
101
|
+
def fake_gh(args: list[str]) -> str:
|
|
102
|
+
calls.append(args)
|
|
103
|
+
return json.dumps(next(responses))
|
|
104
|
+
|
|
105
|
+
before = find_by_marker(
|
|
106
|
+
"example/repo", "program-leaf-source", "program-x/1a",
|
|
107
|
+
fake_gh,
|
|
108
|
+
)
|
|
109
|
+
after = reconcile_after_create(
|
|
110
|
+
"example/repo", "program-leaf-source", "program-x/1a", 101,
|
|
111
|
+
fake_gh,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
self.assertEqual(before["verdict"], "create")
|
|
115
|
+
self.assertEqual(after["verdict"], "STOP")
|
|
116
|
+
self.assertEqual([issue["number"] for issue in after["issues"]], [101, 102])
|
|
117
|
+
self.assertEqual(len(calls), 2)
|
|
118
|
+
|
|
119
|
+
def test_post_create_rescan_rejects_a_different_single_issue(self):
|
|
120
|
+
payload = [[{"number": 102, "state": "open", "body":
|
|
121
|
+
"<!-- prd-source-id: alpha -->"}]]
|
|
122
|
+
|
|
123
|
+
result = reconcile_after_create(
|
|
124
|
+
"example/repo", "prd-source-id", "alpha", 101,
|
|
125
|
+
lambda _args: json.dumps(payload),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self.assertEqual(result["verdict"], "STOP")
|
|
129
|
+
self.assertEqual(result["issues"], [{"number": 102, "state": "open"}])
|
|
130
|
+
|
|
131
|
+
def test_unknown_marker_kind_has_distinct_cli_error(self):
|
|
132
|
+
out, err = io.StringIO(), io.StringIO()
|
|
133
|
+
with redirect_stdout(out), redirect_stderr(err):
|
|
134
|
+
code = cli.main(
|
|
135
|
+
["--kind", "unknown-source", "--slug", "x"],
|
|
136
|
+
repo="example/repo",
|
|
137
|
+
gh=lambda _args: self.fail("unknown kinds must fail before scanning"),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
self.assertEqual(code, 2)
|
|
141
|
+
self.assertEqual(out.getvalue(), "")
|
|
142
|
+
self.assertIn("unknown marker kind", err.getvalue())
|
|
143
|
+
|
|
144
|
+
def test_cli_bounds_a_hanging_github_call(self):
|
|
145
|
+
with patch.object(
|
|
146
|
+
cli.subprocess, "run",
|
|
147
|
+
side_effect=subprocess.TimeoutExpired("gh", 30),
|
|
148
|
+
):
|
|
149
|
+
with self.assertRaisesRegex(RuntimeError, "timed out after 30s"):
|
|
150
|
+
cli._gh(["api", "--paginate", "repos/example/repo/issues"])
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
unittest.main()
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Behavior tests for the pure Tier-2 anchor renderer."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import importlib.util
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import unittest
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SCRIPTS = Path(__file__).resolve().parent
|
|
16
|
+
REPO = SCRIPTS.parent
|
|
17
|
+
SKILLS = (
|
|
18
|
+
REPO / ".claude/skills/to-issues/SKILL.md",
|
|
19
|
+
REPO / ".agents/skills/to-issues/SKILL.md",
|
|
20
|
+
)
|
|
21
|
+
sys.path.insert(0, str(SCRIPTS))
|
|
22
|
+
SPEC = importlib.util.spec_from_file_location(
|
|
23
|
+
"render_anchor", SCRIPTS / "render-anchor.py"
|
|
24
|
+
)
|
|
25
|
+
render_anchor = importlib.util.module_from_spec(SPEC)
|
|
26
|
+
assert SPEC.loader is not None
|
|
27
|
+
SPEC.loader.exec_module(render_anchor)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RenderDocumentsGoldenTest(unittest.TestCase):
|
|
31
|
+
def test_filled_template_and_prd_render_both_documents(self):
|
|
32
|
+
template = "**Welle 12 — Safer publish.**\n\n## Slices\n| K1 |\n"
|
|
33
|
+
prd = (
|
|
34
|
+
"<!-- prd-source-id: safer-publish -->\n"
|
|
35
|
+
"<!-- prd-content-fp: abc123 -->\n"
|
|
36
|
+
"**plan_revision:** r4\n"
|
|
37
|
+
"<!-- prd: awaiting-decomposition -->\n\n"
|
|
38
|
+
"# Safer publish\n\nFull rationale.\n"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
rendered = render_anchor.render_documents(template, prd)
|
|
42
|
+
|
|
43
|
+
self.assertEqual(rendered.anchor_body, template)
|
|
44
|
+
self.assertEqual(
|
|
45
|
+
rendered.archive_body,
|
|
46
|
+
"📄 Full PRD (archive, r4) — the body carries navigation/decisions only\n\n"
|
|
47
|
+
"# Safer publish\n\nFull rationale.\n",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def test_only_canonical_markers_in_the_head_block_are_stripped(self):
|
|
51
|
+
template = "Lean anchor\n"
|
|
52
|
+
prd = (
|
|
53
|
+
"<!-- wave-stub-source: safer-publish -->\n"
|
|
54
|
+
"<!-- prd-source-id: safer-publish -->\n"
|
|
55
|
+
"**plan_revision:** r5\n"
|
|
56
|
+
"<!-- prd: awaiting-decomposition -->\n\n"
|
|
57
|
+
"# PRD\n\n"
|
|
58
|
+
"```md\n**plan_revision:** fake\n"
|
|
59
|
+
"<!-- prd-source-id: quoted-example -->\n```\n\n"
|
|
60
|
+
"> **plan_revision:** quoted-fake\n"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
rendered = render_anchor.render_documents(template, prd)
|
|
64
|
+
|
|
65
|
+
self.assertEqual(
|
|
66
|
+
rendered.archive_body,
|
|
67
|
+
"📄 Full PRD (archive, r5) — the body carries navigation/decisions only\n\n"
|
|
68
|
+
"<!-- wave-stub-source: safer-publish -->\n\n# PRD\n\n"
|
|
69
|
+
"```md\n**plan_revision:** fake\n"
|
|
70
|
+
"<!-- prd-source-id: quoted-example -->\n```\n\n"
|
|
71
|
+
"> **plan_revision:** quoted-fake\n",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def test_duplicate_plan_revisions_fail_closed(self):
|
|
75
|
+
for revisions in (("r5", "r5"), ("r5", "r6")):
|
|
76
|
+
with self.subTest(revisions=revisions):
|
|
77
|
+
prd = (
|
|
78
|
+
f"**plan_revision:** {revisions[0]}\n"
|
|
79
|
+
f"**plan_revision:** {revisions[1]}\n\n# PRD\n"
|
|
80
|
+
)
|
|
81
|
+
with self.assertRaisesRegex(
|
|
82
|
+
ValueError, "exactly one canonical plan_revision"
|
|
83
|
+
):
|
|
84
|
+
render_anchor.render_documents("Lean anchor\n", prd)
|
|
85
|
+
|
|
86
|
+
def test_plan_revision_requires_numeric_canonical_value(self):
|
|
87
|
+
for lookalike in ("rfoo", "r5!"):
|
|
88
|
+
with self.subTest(lookalike=lookalike, valid_revision=True):
|
|
89
|
+
malformed = f"**plan_revision:** {lookalike}"
|
|
90
|
+
prd = f"{malformed}\n**plan_revision:** r5\n\n# PRD\n"
|
|
91
|
+
rendered = render_anchor.render_documents("Lean anchor\n", prd)
|
|
92
|
+
self.assertIn(malformed, rendered.archive_body)
|
|
93
|
+
|
|
94
|
+
with self.subTest(lookalike=lookalike, valid_revision=False):
|
|
95
|
+
with self.assertRaisesRegex(
|
|
96
|
+
ValueError, "exactly one canonical plan_revision"
|
|
97
|
+
):
|
|
98
|
+
render_anchor.render_documents(
|
|
99
|
+
"Lean anchor\n", f"**plan_revision:** {lookalike}\n# PRD\n"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def test_malformed_marker_lookalikes_are_preserved(self):
|
|
103
|
+
lookalikes = (
|
|
104
|
+
"<!-- prd-source-id: alpha --> trailing -->",
|
|
105
|
+
"<!-- prd-content-fp: abc > trailing -->",
|
|
106
|
+
"<!-- prd: program > trailing -->",
|
|
107
|
+
)
|
|
108
|
+
for lookalike in lookalikes:
|
|
109
|
+
with self.subTest(lookalike=lookalike):
|
|
110
|
+
prd = (
|
|
111
|
+
f"{lookalike}\n"
|
|
112
|
+
"<!-- prd-source-id: canonical -->\n"
|
|
113
|
+
"<!-- prd-content-fp: abc123 -->\n"
|
|
114
|
+
"**plan_revision:** r5\n"
|
|
115
|
+
"<!-- prd: awaiting-decomposition -->\n\n# PRD\n"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
rendered = render_anchor.render_documents("Lean anchor\n", prd)
|
|
119
|
+
|
|
120
|
+
self.assertIn(lookalike, rendered.archive_body)
|
|
121
|
+
self.assertNotIn(
|
|
122
|
+
"<!-- prd-source-id: canonical -->", rendered.archive_body
|
|
123
|
+
)
|
|
124
|
+
self.assertNotIn(
|
|
125
|
+
"<!-- prd-content-fp: abc123 -->", rendered.archive_body
|
|
126
|
+
)
|
|
127
|
+
self.assertNotIn(
|
|
128
|
+
"<!-- prd: awaiting-decomposition -->", rendered.archive_body
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def test_cli_emits_each_document_without_mutating_inputs(self):
|
|
132
|
+
with tempfile.TemporaryDirectory() as temporary:
|
|
133
|
+
root = Path(temporary)
|
|
134
|
+
template = root / "anchor.md"
|
|
135
|
+
prd = root / "prd.md"
|
|
136
|
+
template.write_text("Lean anchor\n", encoding="utf-8")
|
|
137
|
+
prd.write_text(
|
|
138
|
+
"**plan_revision:** r2\n\n# Full PRD\n", encoding="utf-8"
|
|
139
|
+
)
|
|
140
|
+
original = (template.read_bytes(), prd.read_bytes())
|
|
141
|
+
|
|
142
|
+
first = self._run_cli(template, prd, "archive")
|
|
143
|
+
second = self._run_cli(template, prd, "archive")
|
|
144
|
+
anchor = self._run_cli(template, prd, "anchor")
|
|
145
|
+
|
|
146
|
+
self.assertEqual(first.stdout, second.stdout)
|
|
147
|
+
self.assertEqual(anchor.stdout, b"Lean anchor\n")
|
|
148
|
+
self.assertEqual((template.read_bytes(), prd.read_bytes()), original)
|
|
149
|
+
|
|
150
|
+
def test_cli_archive_is_utf8_when_python_io_encoding_is_ascii(self):
|
|
151
|
+
with tempfile.TemporaryDirectory() as temporary:
|
|
152
|
+
root = Path(temporary)
|
|
153
|
+
template = root / "anchor.md"
|
|
154
|
+
prd = root / "prd.md"
|
|
155
|
+
template.write_text("Lean anchor\n", encoding="utf-8")
|
|
156
|
+
prd.write_text("**plan_revision:** r2\n\n# PRD\n", encoding="utf-8")
|
|
157
|
+
|
|
158
|
+
result = self._run_cli(
|
|
159
|
+
template,
|
|
160
|
+
prd,
|
|
161
|
+
"archive",
|
|
162
|
+
env={**os.environ, "PYTHONIOENCODING": "ascii"},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
self.assertTrue(result.stdout.startswith("📄".encode("utf-8")))
|
|
166
|
+
|
|
167
|
+
def _run_cli(
|
|
168
|
+
self, template: Path, prd: Path, document: str, env: dict | None = None
|
|
169
|
+
):
|
|
170
|
+
return subprocess.run(
|
|
171
|
+
[
|
|
172
|
+
sys.executable,
|
|
173
|
+
str(SCRIPTS / "render-anchor.py"),
|
|
174
|
+
"--template",
|
|
175
|
+
str(template),
|
|
176
|
+
"--prd",
|
|
177
|
+
str(prd),
|
|
178
|
+
"--document",
|
|
179
|
+
document,
|
|
180
|
+
],
|
|
181
|
+
check=True,
|
|
182
|
+
capture_output=True,
|
|
183
|
+
env=env,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class PromotionStateTableTest(unittest.TestCase):
|
|
188
|
+
EXPECTED_ROWS = {
|
|
189
|
+
"initial": (
|
|
190
|
+
"`S=yes`, `B=no`, `C=0`, `P=absent`",
|
|
191
|
+
"render + write body",
|
|
192
|
+
),
|
|
193
|
+
"body-written": (
|
|
194
|
+
"`S=n/a`, `B=yes`, `C=0`, `P=absent`",
|
|
195
|
+
"reconcile + write archive comment",
|
|
196
|
+
),
|
|
197
|
+
"comment-written": (
|
|
198
|
+
"`S=n/a`, `B=yes`, `C=exact-1`, `P=absent`",
|
|
199
|
+
"promote board state",
|
|
200
|
+
),
|
|
201
|
+
"promoted": (
|
|
202
|
+
"`S=n/a`, `B=yes`, `C=exact-1`, `P=complete`",
|
|
203
|
+
"no-op; continue publish audit",
|
|
204
|
+
),
|
|
205
|
+
}
|
|
206
|
+
EXPECTED_BOARD_OBSERVATIONS = {
|
|
207
|
+
"ordinary-prestate": "absent",
|
|
208
|
+
"stufe-1p-prestate": "absent",
|
|
209
|
+
"promoted": "complete",
|
|
210
|
+
"cluster-only": "partial",
|
|
211
|
+
"wrong-wave": "partial",
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
def test_all_four_observable_states_have_one_resume_action(self):
|
|
215
|
+
for path in SKILLS:
|
|
216
|
+
text = path.read_text(encoding="utf-8")
|
|
217
|
+
start = text.index("<!-- promotion-state-table:start -->")
|
|
218
|
+
end = text.index("<!-- promotion-state-table:end -->", start)
|
|
219
|
+
rows = {}
|
|
220
|
+
for line in text[start:end].splitlines():
|
|
221
|
+
if line.startswith("| `"):
|
|
222
|
+
cells = [cell.strip() for cell in line.strip("|").split("|")]
|
|
223
|
+
rows[cells[0].strip("`")] = (cells[1], cells[2])
|
|
224
|
+
self.assertEqual(rows, self.EXPECTED_ROWS, str(path))
|
|
225
|
+
|
|
226
|
+
def test_corrupt_comments_and_partial_board_writes_are_drift(self):
|
|
227
|
+
for path in SKILLS:
|
|
228
|
+
text = path.read_text(encoding="utf-8")
|
|
229
|
+
self.assertIn("`C=wrong-1`", text, str(path))
|
|
230
|
+
self.assertIn("`C=duplicates(<ids>)`", text, str(path))
|
|
231
|
+
self.assertIn("`P=partial`", text, str(path))
|
|
232
|
+
self.assertIn("drift and enters repair", text, str(path))
|
|
233
|
+
|
|
234
|
+
def test_stufe_1p_prestamp_is_not_partial_promotion(self):
|
|
235
|
+
for path in SKILLS:
|
|
236
|
+
text = path.read_text(encoding="utf-8")
|
|
237
|
+
start = text.index("<!-- promotion-board-observation-table:start -->")
|
|
238
|
+
end = text.index("<!-- promotion-board-observation-table:end -->", start)
|
|
239
|
+
rows = {}
|
|
240
|
+
for line in text[start:end].splitlines():
|
|
241
|
+
if line.startswith("| `"):
|
|
242
|
+
cells = [cell.strip() for cell in line.strip("|").split("|")]
|
|
243
|
+
rows[cells[0].strip("`")] = cells[2].strip("`")
|
|
244
|
+
self.assertEqual(rows, self.EXPECTED_BOARD_OBSERVATIONS, str(path))
|
|
245
|
+
|
|
246
|
+
def test_post_initial_body_drift_requires_an_operator_decision(self):
|
|
247
|
+
for path in SKILLS:
|
|
248
|
+
text = path.read_text(encoding="utf-8")
|
|
249
|
+
self.assertIn("outside the valid `initial` tuple", text, str(path))
|
|
250
|
+
self.assertIn("report the body diff", text, str(path))
|
|
251
|
+
self.assertRegex(text, r"explicit\s+operator decision", str(path))
|
|
252
|
+
self.assertNotIn(
|
|
253
|
+
"wrong/missing `B` → rerender and rewrite the body", text, str(path)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def test_stale_source_snapshot_stops_before_body_write(self):
|
|
257
|
+
for path in SKILLS:
|
|
258
|
+
text = path.read_text(encoding="utf-8")
|
|
259
|
+
self.assertRegex(text, r"Pre:\s+`S=yes`, `B=no`", str(path))
|
|
260
|
+
self.assertRegex(text, r"Post:\s+`S=n/a`, `B=yes`", str(path))
|
|
261
|
+
self.assertIn("`S=no`", text, str(path))
|
|
262
|
+
self.assertIn("re-fetch the remote body", text, str(path))
|
|
263
|
+
self.assertRegex(text, r"never\s+write the\s+stale render", str(path))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
if __name__ == "__main__":
|
|
267
|
+
unittest.main(verbosity=2)
|
|
@@ -77,6 +77,12 @@ class WrapupChainingContract(unittest.TestCase):
|
|
|
77
77
|
for phrase in required:
|
|
78
78
|
self.assertIn(phrase, text)
|
|
79
79
|
|
|
80
|
+
def test_wrapup_does_not_forbid_its_affirmative_retro_chain(self):
|
|
81
|
+
for surface in SURFACES:
|
|
82
|
+
with self.subTest(surface=surface):
|
|
83
|
+
text = contract_text(surface, "wrapup")
|
|
84
|
+
self.assertNotIn("never run by this skill", text.lower())
|
|
85
|
+
|
|
80
86
|
def test_only_model_invocable_non_deploying_workflows_chain(self):
|
|
81
87
|
required = (
|
|
82
88
|
"model-invocable",
|