@ikon85/agent-workflow-kit 0.43.0 → 0.44.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/grill-me/SKILL.md +3 -1
- package/.agents/skills/grill-with-docs/SKILL.md +3 -1
- package/.agents/skills/kit-update/SKILL.md +9 -11
- package/.agents/skills/orchestrate-wave/SKILL.md +8 -8
- package/.agents/skills/setup-workflow/SKILL.md +20 -18
- package/.agents/skills/setup-workflow/board-sync.md +4 -1
- package/.agents/skills/setup-workflow/workflow-advisories.md +3 -2
- package/.agents/skills/setup-workflow/worktree-lifecycle.md +36 -31
- package/.agents/skills/to-issues/SKILL.md +43 -0
- package/.agents/skills/wrapup/SKILL.md +98 -54
- package/.claude/skills/grill-me/SKILL.md +3 -1
- package/.claude/skills/grill-me-codex/SKILL.md +3 -1
- package/.claude/skills/grill-with-docs/SKILL.md +3 -1
- package/.claude/skills/grill-with-docs-codex/SKILL.md +3 -1
- package/.claude/skills/kit-update/SKILL.md +9 -11
- package/.claude/skills/orchestrate-wave/SKILL.md +8 -8
- package/.claude/skills/setup-workflow/SKILL.md +20 -18
- package/.claude/skills/setup-workflow/board-sync.md +4 -1
- package/.claude/skills/setup-workflow/workflow-advisories.md +3 -2
- package/.claude/skills/setup-workflow/worktree-lifecycle.md +36 -31
- package/.claude/skills/to-issues/SKILL.md +43 -0
- package/.claude/skills/wrapup/SKILL.md +98 -54
- package/README.md +249 -7
- package/agent-workflow-kit.package.json +42 -42
- package/package.json +16 -12
- package/scripts/marker_lib.py +3 -0
- package/scripts/profile_globs.py +26 -45
- package/scripts/release-delta-guard.mjs +26 -9
- package/scripts/worktree-lifecycle/README.md +57 -160
- package/scripts/worktree-lifecycle/capabilities.json +2 -3
- package/scripts/worktree-lifecycle/classify.py +480 -0
- package/scripts/worktree-lifecycle/cleanup.py +9 -29
- package/scripts/worktree-lifecycle/core.py +68 -1429
- package/scripts/worktree-lifecycle/ignore_seed.py +1 -1
- package/scripts/worktree-lifecycle/profile.py +46 -29
- package/scripts/worktree-lifecycle/setup.py +7 -19
- package/scripts/wrapup-land.py +1292 -545
- package/src/consumer-migrations.json +1 -16
- package/src/lib/bundle.mjs +31 -1
- package/docs/adr/0001-consumer-divergence-policy.md +0 -53
- package/docs/adr/0002-capability-gated-orchestration.md +0 -70
- package/docs/adr/0003-kit-core-and-project-extension-lifecycle.md +0 -63
- package/docs/adr/0004-release-intent-is-a-version-tag.md +0 -64
- package/docs/adr/0005-to-issues-is-the-planning-facade.md +0 -42
- package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +0 -95
- package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +0 -88
- package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +0 -84
- package/docs/adr/0009-teardown-authority-is-stateless-repository-classification.md +0 -101
- package/docs/adr/0010-model-roster-replaces-the-optimization-dial.md +0 -87
- package/docs/agents/board-sync.md +0 -74
- package/docs/agents/code-review.md +0 -14
- package/docs/agents/workflow-capabilities.json +0 -28
- package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
- package/docs/research/benchlm-routing-source.md +0 -198
- package/docs/research/consumer-owned-protocol-files.md +0 -238
- package/docs/research/frontend-agent-benchmarks.md +0 -282
- package/docs/research/model-effort-routing-benchmarks.md +0 -261
- package/docs/research/provider-neutral-agent-routing.md +0 -207
- package/docs/research/wave-152-consumer-acceptance.md +0 -98
- package/docs/research/wave-43-script-hook-census.md +0 -128
- package/scripts/build-kit.mjs +0 -117
- package/scripts/build-kit.test.mjs +0 -207
- package/scripts/census/census.test.mjs +0 -252
- package/scripts/census/state.test.mjs +0 -15
- package/scripts/census/transaction.test.mjs +0 -103
- package/scripts/census-contract.test.mjs +0 -227
- package/scripts/check-kit-staleness.mjs +0 -45
- package/scripts/check-kit-staleness.test.mjs +0 -19
- package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -165
- package/scripts/codex-exec.test.mjs +0 -871
- package/scripts/grill-census-wiring-guard.mjs +0 -87
- package/scripts/grill-census-wiring-guard.test.mjs +0 -107
- package/scripts/kit-release.test.mjs +0 -273
- package/scripts/kit-update-pr.test.mjs +0 -151
- package/scripts/lib/audit-refs.mjs +0 -146
- package/scripts/lib/scrub.mjs +0 -113
- package/scripts/lib/scrub.test.mjs +0 -187
- package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
- package/scripts/portability_profile_scan.py +0 -53
- package/scripts/release-delta-guard.test.mjs +0 -174
- package/scripts/release-parity.test.mjs +0 -90
- package/scripts/release-state.test.mjs +0 -360
- package/scripts/test_anchor_table.py +0 -69
- package/scripts/test_board_bootstrap.py +0 -348
- package/scripts/test_board_sync.py +0 -208
- package/scripts/test_board_sync_create_idempotency.py +0 -117
- package/scripts/test_board_sync_wave_title.py +0 -120
- package/scripts/test_census_backstop.py +0 -712
- package/scripts/test_census_forward_contract.py +0 -91
- package/scripts/test_census_update_contract.test.mjs +0 -454
- package/scripts/test_codex_adapter_sync_contract.py +0 -169
- package/scripts/test_dist_kit_smoke.py +0 -220
- package/scripts/test_drift_guard_diagnostics.py +0 -295
- package/scripts/test_issue_claim_contract.py +0 -169
- package/scripts/test_kit_docs_language_census.py +0 -178
- package/scripts/test_marker_lib.py +0 -162
- package/scripts/test_orchestrate_wave_contract.py +0 -381
- package/scripts/test_pr_body_check.py +0 -264
- package/scripts/test_profile_globs.py +0 -280
- package/scripts/test_program_planning_contract.py +0 -133
- package/scripts/test_release_authorization_contract.py +0 -101
- package/scripts/test_render_anchor.py +0 -267
- package/scripts/test_retro_wrapup_contract.py +0 -153
- package/scripts/test_skill_code_review_seed.py +0 -89
- package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
- package/scripts/test_skill_frontmatter_lint.py +0 -234
- package/scripts/test_skill_gh_lint.py +0 -124
- package/scripts/test_skill_language_census.py +0 -254
- package/scripts/test_skill_optional_readiness.py +0 -171
- package/scripts/test_skill_portability_lint.py +0 -866
- package/scripts/test_skill_precommit_template.py +0 -30
- package/scripts/test_skill_publish_audit.py +0 -358
- package/scripts/test_skill_readiness_contract.py +0 -264
- package/scripts/test_skill_readiness_preflight.py +0 -180
- package/scripts/test_skill_required_readiness.py +0 -233
- package/scripts/test_skill_selfcontainment_lint.py +0 -255
- package/scripts/test_skill_setup_workflow_seeds.py +0 -691
- package/scripts/test_skill_stale_name_lint.py +0 -126
- package/scripts/test_skill_surface_refs.py +0 -300
- package/scripts/test_skill_trailing_artifact_lint.py +0 -71
- package/scripts/test_tdd_contract.py +0 -78
- package/scripts/test_worktree_ignore_seed.py +0 -320
- package/scripts/test_worktree_setup_base_guard.py +0 -140
- package/scripts/test_worktree_wrapup_contract.py +0 -1737
- package/scripts/test_wrapup_land.py +0 -428
- package/scripts/worktree-lifecycle/session.py +0 -1857
|
@@ -1,267 +0,0 @@
|
|
|
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)
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
"""Contract tests for retro enforcement and safe wrapup workflow chaining."""
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
import re
|
|
5
|
-
import unittest
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
ROOT = Path(__file__).resolve().parents[1]
|
|
9
|
-
SURFACES = (".claude", ".agents")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def skill(surface: str, name: str) -> str:
|
|
13
|
-
return (ROOT / surface / "skills" / name / "SKILL.md").read_text(
|
|
14
|
-
encoding="utf-8"
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def contract_text(surface: str, name: str) -> str:
|
|
19
|
-
"""Return prose with Markdown emphasis removed for wording assertions."""
|
|
20
|
-
return re.sub(r"\s+", " ", skill(surface, name).replace("**", ""))
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class RetroEnforcementContract(unittest.TestCase):
|
|
24
|
-
def test_mechanical_check_precedes_target_and_weight_ladder(self):
|
|
25
|
-
for surface in SURFACES:
|
|
26
|
-
with self.subTest(surface=surface):
|
|
27
|
-
text = contract_text(surface, "retro")
|
|
28
|
-
mechanical = text.index("### 3a. Mechanical enforcement check")
|
|
29
|
-
ladder = text.index("### 3b. Determine target + weight")
|
|
30
|
-
self.assertLess(mechanical, ladder)
|
|
31
|
-
|
|
32
|
-
def test_machine_checkable_findings_cannot_stop_at_prose(self):
|
|
33
|
-
required = (
|
|
34
|
-
"Could a machine decide this",
|
|
35
|
-
"implement the enforcement",
|
|
36
|
-
"tracked enforcement issue",
|
|
37
|
-
"explicit trade-off",
|
|
38
|
-
)
|
|
39
|
-
for surface in SURFACES:
|
|
40
|
-
with self.subTest(surface=surface):
|
|
41
|
-
text = contract_text(surface, "retro")
|
|
42
|
-
for phrase in required:
|
|
43
|
-
self.assertIn(phrase, text)
|
|
44
|
-
|
|
45
|
-
def test_capability_route_and_exact_sanitized_approval_survive(self):
|
|
46
|
-
required = (
|
|
47
|
-
"generic or project-specific",
|
|
48
|
-
"recommend `own`",
|
|
49
|
-
"contribute status <path> --surface=retro",
|
|
50
|
-
"Missing, disabled, invalid, or unverifiable configuration",
|
|
51
|
-
"never infer maintainer status",
|
|
52
|
-
"sanitized exact preview",
|
|
53
|
-
"separate explicit approval",
|
|
54
|
-
"docs/agents/skills/<skill>.md",
|
|
55
|
-
)
|
|
56
|
-
for surface in SURFACES:
|
|
57
|
-
with self.subTest(surface=surface):
|
|
58
|
-
text = contract_text(surface, "retro")
|
|
59
|
-
for phrase in required:
|
|
60
|
-
self.assertIn(phrase, text)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class WrapupChainingContract(unittest.TestCase):
|
|
64
|
-
def test_prod_readiness_degrades_reporting_without_blocking_landing(self):
|
|
65
|
-
required = (
|
|
66
|
-
"readiness.mjs check --skill wrapup --json",
|
|
67
|
-
"Prod readiness is pending or missing; deploy reporting omitted.",
|
|
68
|
-
"landing continues normally",
|
|
69
|
-
"never authorizes the agent to invent or configure a deploy target",
|
|
70
|
-
"<!-- readiness:block deployReport -->",
|
|
71
|
-
"<!-- readiness:end -->",
|
|
72
|
-
)
|
|
73
|
-
for surface in SURFACES:
|
|
74
|
-
with self.subTest(surface=surface):
|
|
75
|
-
text = skill(surface, "wrapup")
|
|
76
|
-
for phrase in required:
|
|
77
|
-
self.assertIn(phrase, text)
|
|
78
|
-
self.assertEqual(text.count("<!-- readiness:block deployReport -->"), 1)
|
|
79
|
-
self.assertEqual(text.count("<!-- readiness:end -->"), 1)
|
|
80
|
-
|
|
81
|
-
def test_retro_is_model_invocable_on_both_surfaces(self):
|
|
82
|
-
for surface in SURFACES:
|
|
83
|
-
with self.subTest(surface=surface):
|
|
84
|
-
frontmatter = skill(surface, "retro").split("---", 2)[1]
|
|
85
|
-
self.assertIn("disable-model-invocation: false", frontmatter)
|
|
86
|
-
|
|
87
|
-
def test_affirmative_retro_gate_chains_without_landing(self):
|
|
88
|
-
required = (
|
|
89
|
-
"invoke the `retro` skill immediately in this run",
|
|
90
|
-
"land nothing in this run",
|
|
91
|
-
"fresh explicit `$wrapup` or `/wrapup` invocation",
|
|
92
|
-
)
|
|
93
|
-
for surface in SURFACES:
|
|
94
|
-
with self.subTest(surface=surface):
|
|
95
|
-
text = contract_text(surface, "wrapup")
|
|
96
|
-
for phrase in required:
|
|
97
|
-
self.assertIn(phrase, text)
|
|
98
|
-
|
|
99
|
-
def test_wrapup_does_not_forbid_its_affirmative_retro_chain(self):
|
|
100
|
-
for surface in SURFACES:
|
|
101
|
-
with self.subTest(surface=surface):
|
|
102
|
-
text = contract_text(surface, "wrapup")
|
|
103
|
-
self.assertNotIn("never run by this skill", text.lower())
|
|
104
|
-
|
|
105
|
-
def test_only_model_invocable_non_deploying_workflows_chain(self):
|
|
106
|
-
required = (
|
|
107
|
-
"model-invocable",
|
|
108
|
-
"non-deploying",
|
|
109
|
-
"return control to the user",
|
|
110
|
-
"state the reason",
|
|
111
|
-
)
|
|
112
|
-
for surface in SURFACES:
|
|
113
|
-
with self.subTest(surface=surface):
|
|
114
|
-
text = contract_text(surface, "wrapup")
|
|
115
|
-
for phrase in required:
|
|
116
|
-
self.assertIn(phrase, text)
|
|
117
|
-
|
|
118
|
-
def test_wrapup_remains_manual_and_program_propagation_remains_present(self):
|
|
119
|
-
for surface in SURFACES:
|
|
120
|
-
with self.subTest(surface=surface):
|
|
121
|
-
text = skill(surface, "wrapup")
|
|
122
|
-
self.assertIn("disable-model-invocation: true", text)
|
|
123
|
-
self.assertIn(
|
|
124
|
-
"user's direct `$wrapup` or `/wrapup` input IS the explicit",
|
|
125
|
-
text,
|
|
126
|
-
)
|
|
127
|
-
self.assertIn("program-sync", text)
|
|
128
|
-
self.assertIn("Phasen-Gates", text)
|
|
129
|
-
|
|
130
|
-
def test_wrapup_accepts_only_direct_user_dollar_or_slash_invocations(self):
|
|
131
|
-
required = (
|
|
132
|
-
"direct `$wrapup` or `/wrapup`",
|
|
133
|
-
"Natural-language requests",
|
|
134
|
-
"indirect skill chaining",
|
|
135
|
-
"autonomous invocation",
|
|
136
|
-
"fresh explicit `$wrapup` or `/wrapup` invocation",
|
|
137
|
-
)
|
|
138
|
-
for surface in SURFACES:
|
|
139
|
-
with self.subTest(surface=surface):
|
|
140
|
-
text = contract_text(surface, "wrapup")
|
|
141
|
-
for phrase in required:
|
|
142
|
-
self.assertIn(phrase, text)
|
|
143
|
-
|
|
144
|
-
def test_new_contract_is_reference_free(self):
|
|
145
|
-
for surface in SURFACES:
|
|
146
|
-
with self.subTest(surface=surface):
|
|
147
|
-
combined = skill(surface, "retro") + skill(surface, "wrapup")
|
|
148
|
-
self.assertNotIn("testreporter", combined.lower())
|
|
149
|
-
self.assertIsNone(re.search(r"#[0-9]{3,}", combined))
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if __name__ == "__main__":
|
|
153
|
-
unittest.main(verbosity=2)
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""code-review skill + its setup-workflow project-layer seed (Welle 49).
|
|
3
|
-
|
|
4
|
-
Companion to test_skill_setup_workflow_seeds.py (not touched here — a sibling
|
|
5
|
-
slice edits it concurrently). Pins the two machine-checkable parts specific to
|
|
6
|
-
this skill:
|
|
7
|
-
|
|
8
|
-
1. The seed template setup-workflow ships (`code-review.md`) is structurally
|
|
9
|
-
valid for the skill that reads it — same "structured-but-empty crust"
|
|
10
|
-
contract as the existing spec-self-critique / spec-completeness seeds.
|
|
11
|
-
2. The sentinel-based idempotency rule applies to it the same way (reference
|
|
12
|
-
implementation duplicated locally so this file has no import-order
|
|
13
|
-
dependency on the sibling-owned test module).
|
|
14
|
-
|
|
15
|
-
Run: python3 scripts/test_skill_code_review_seed.py
|
|
16
|
-
"""
|
|
17
|
-
import re
|
|
18
|
-
import unittest
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
|
|
21
|
-
REPO = Path(__file__).resolve().parent.parent
|
|
22
|
-
SETUP_WORKFLOW = REPO / ".claude/skills/setup-workflow"
|
|
23
|
-
CODE_REVIEW = REPO / ".claude/skills/code-review"
|
|
24
|
-
|
|
25
|
-
SENTINEL_RE = re.compile(
|
|
26
|
-
r"^<!--\s*setup-workflow:\s*state=(stub|filled|not-applicable)"
|
|
27
|
-
r"(?:;\s*mode=(github-projects-v2|none))?\s*-->\s*$"
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class SeedTemplateValid(unittest.TestCase):
|
|
32
|
-
def test_code_review_seed_has_both_headings(self):
|
|
33
|
-
"""The two structured-but-empty headings the `code-review` skill's
|
|
34
|
-
Standards axis expects to find once this file is filled."""
|
|
35
|
-
t = (SETUP_WORKFLOW / "code-review.md").read_text(encoding="utf-8")
|
|
36
|
-
self.assertIn("## Standards sources in this repo", t)
|
|
37
|
-
self.assertIn("## Adjacent review tooling", t)
|
|
38
|
-
|
|
39
|
-
def test_code_review_seed_is_generic(self):
|
|
40
|
-
"""No project-coupled tokens in a template shipped to every consumer."""
|
|
41
|
-
t = (SETUP_WORKFLOW / "code-review.md").read_text(encoding="utf-8")
|
|
42
|
-
for token in ("Testreporter", "iKon85", "/home/"):
|
|
43
|
-
self.assertNotIn(token, t)
|
|
44
|
-
self.assertNotRegex(t, r"#\d{3,5}\b")
|
|
45
|
-
self.assertNotRegex(t, r"\bHR\d+\b")
|
|
46
|
-
|
|
47
|
-
def test_setup_workflow_skill_registers_the_seed(self):
|
|
48
|
-
"""SKILL.md wires the target path + seed link into its Targets table
|
|
49
|
-
and process, so a reader (or the manifest-completeness lint's sibling
|
|
50
|
-
checks) can trace the seed from the skill body."""
|
|
51
|
-
skill = (SETUP_WORKFLOW / "SKILL.md").read_text(encoding="utf-8")
|
|
52
|
-
self.assertIn("docs/agents/code-review.md", skill)
|
|
53
|
-
self.assertIn("[code-review.md](./code-review.md)", skill)
|
|
54
|
-
|
|
55
|
-
def test_code_review_skill_names_its_project_layer_and_fallback(self):
|
|
56
|
-
"""The skill states the project-layer path + the /setup-workflow
|
|
57
|
-
fallback (self-containment-lint's LookupContract pattern)."""
|
|
58
|
-
body = (CODE_REVIEW / "SKILL.md").read_text(encoding="utf-8")
|
|
59
|
-
self.assertIn("docs/agents/code-review.md", body)
|
|
60
|
-
self.assertIn("/setup-workflow", body)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class FreshFillProducesValidSentinel(unittest.TestCase):
|
|
64
|
-
"""Simulate the write step: prepend the documented sentinel to the seed
|
|
65
|
-
body and assert the result is a well-formed project-layer file."""
|
|
66
|
-
|
|
67
|
-
def test_prepended_sentinel_parses(self):
|
|
68
|
-
body = (SETUP_WORKFLOW / "code-review.md").read_text(encoding="utf-8")
|
|
69
|
-
written = "<!-- setup-workflow: state=filled -->\n" + body
|
|
70
|
-
self.assertRegex(written.splitlines()[0], SENTINEL_RE)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class ManifestEntryValid(unittest.TestCase):
|
|
74
|
-
def test_code_review_is_own_generic_dual_surface_publish_true(self):
|
|
75
|
-
import json
|
|
76
|
-
|
|
77
|
-
manifest = json.loads(
|
|
78
|
-
(REPO / ".claude/skills/skill-manifest.json").read_text(encoding="utf-8")
|
|
79
|
-
)
|
|
80
|
-
entry = manifest["skills"].get("code-review")
|
|
81
|
-
self.assertIsNotNone(entry, "code-review missing from skill-manifest.json")
|
|
82
|
-
self.assertEqual(entry["class"], "generic")
|
|
83
|
-
self.assertIs(entry["publish"], True)
|
|
84
|
-
self.assertEqual(entry["provenance"], "own")
|
|
85
|
-
self.assertEqual(set(entry["surfaces"]), {"claude", "codex"})
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if __name__ == "__main__":
|
|
89
|
-
unittest.main(verbosity=2)
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
"""Guard the Claude-only cross-model skills' thin codex-exec lifecycle."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import re
|
|
5
|
-
import subprocess
|
|
6
|
-
import unittest
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
REPO = Path(__file__).resolve().parents[1]
|
|
11
|
-
MANIFEST = REPO / ".claude/skills/skill-manifest.json"
|
|
12
|
-
TARGETS = {
|
|
13
|
-
"codex-review": ("review", "read-only"),
|
|
14
|
-
"codex-build": ("build", "workspace-write"),
|
|
15
|
-
"grill-me-codex": ("review", "read-only"),
|
|
16
|
-
"grill-with-docs-codex": ("review", "read-only"),
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class CodexExecSkillLifecycleTests(unittest.TestCase):
|
|
21
|
-
@classmethod
|
|
22
|
-
def setUpClass(cls):
|
|
23
|
-
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))["skills"]
|
|
24
|
-
cls.skills = {}
|
|
25
|
-
for name, expected in TARGETS.items():
|
|
26
|
-
entry = manifest[name]
|
|
27
|
-
if entry["surfaces"] != ["claude"]:
|
|
28
|
-
raise AssertionError(f"{name} must remain Claude-only")
|
|
29
|
-
path = REPO / ".claude/skills" / name / "SKILL.md"
|
|
30
|
-
cls.skills[name] = (expected, path.read_text(encoding="utf-8"))
|
|
31
|
-
|
|
32
|
-
def test_all_four_skills_use_the_thin_wrapper_lifecycle(self):
|
|
33
|
-
self.assertEqual(len(self.skills), 4)
|
|
34
|
-
for name, ((profile, mode), body) in self.skills.items():
|
|
35
|
-
with self.subTest(skill=name):
|
|
36
|
-
self.assertIn(
|
|
37
|
-
f"scripts/codex-exec.sh new --profile {profile} --mode {mode}",
|
|
38
|
-
body,
|
|
39
|
-
)
|
|
40
|
-
self.assertRegex(body, r"scripts/codex-exec\.sh resume [\"']?\$RUN_ID")
|
|
41
|
-
self.assertRegex(body, r"scripts/codex-exec\.sh finalize [\"']?\$RUN_ID")
|
|
42
|
-
self.assertEqual(body.count("scripts/codex-exec.sh handle-failure"), 2)
|
|
43
|
-
self.assertNotRegex(
|
|
44
|
-
body,
|
|
45
|
-
r"scripts/codex-exec\.sh resume[^\n]*--mode",
|
|
46
|
-
"resume must inherit the persisted mode",
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
def test_new_and_resume_are_thin_ordered_and_valid_bash(self):
|
|
50
|
-
for name, (_, body) in self.skills.items():
|
|
51
|
-
blocks = re.findall(
|
|
52
|
-
r"```bash\n(if ROUND_RESULT=\$\(scripts/codex-exec\.sh (?:new|resume).*?\nfi)\n```",
|
|
53
|
-
body,
|
|
54
|
-
re.DOTALL,
|
|
55
|
-
)
|
|
56
|
-
self.assertEqual(len(blocks), 2, name)
|
|
57
|
-
for block in blocks:
|
|
58
|
-
first_line = block.splitlines()[0]
|
|
59
|
-
is_resume = "scripts/codex-exec.sh resume " in first_line
|
|
60
|
-
success, failure = block.split("\nelse\n", 1)
|
|
61
|
-
self.assertIn("CODEX_REPORT=", success)
|
|
62
|
-
self.assertNotIn("CODEX_REPORT=", failure)
|
|
63
|
-
self.assertIn("scripts/codex-exec.sh handle-failure", failure)
|
|
64
|
-
self.assertIn("|| :", failure)
|
|
65
|
-
self.assertNotIn("json.load", failure)
|
|
66
|
-
handler = failure.index("handle-failure")
|
|
67
|
-
surfaced = failure.index('printf \'%s\\n\' "$FAILURE_RESULT" >&2')
|
|
68
|
-
stopped = failure.index("exit 1")
|
|
69
|
-
self.assertLess(handler, surfaced)
|
|
70
|
-
self.assertLess(surfaced, stopped)
|
|
71
|
-
if is_resume:
|
|
72
|
-
self.assertIn('--run-id "$RUN_ID"', failure)
|
|
73
|
-
self.assertNotIn("\n RUN_ID=", success)
|
|
74
|
-
else:
|
|
75
|
-
self.assertNotIn("--run-id", failure)
|
|
76
|
-
self.assertLess(success.index("RUN_ID="), success.index("CODEX_REPORT="))
|
|
77
|
-
parsed = subprocess.run(
|
|
78
|
-
["bash", "-n"],
|
|
79
|
-
input=block,
|
|
80
|
-
text=True,
|
|
81
|
-
capture_output=True,
|
|
82
|
-
check=False,
|
|
83
|
-
)
|
|
84
|
-
self.assertEqual(parsed.returncode, 0, parsed.stderr)
|
|
85
|
-
|
|
86
|
-
def test_all_skills_abort_known_run_on_orchestration_cancellation(self):
|
|
87
|
-
for name, (_, body) in self.skills.items():
|
|
88
|
-
with self.subTest(skill=name):
|
|
89
|
-
cancellation = re.search(
|
|
90
|
-
r"[Cc]ancellation or a decision to stop (?:orchestration|delegation)"
|
|
91
|
-
r".*?scripts/codex-exec\.sh abort \"\$RUN_ID\"",
|
|
92
|
-
body,
|
|
93
|
-
re.DOTALL,
|
|
94
|
-
)
|
|
95
|
-
self.assertIsNotNone(cancellation)
|
|
96
|
-
self.assertRegex(body, r"scripts/codex-exec\.sh finalize [\"']?\$RUN_ID")
|
|
97
|
-
|
|
98
|
-
build = self.skills["codex-build"][1]
|
|
99
|
-
gate = build.split("## Step 5 — Human gate", 1)[1].split("## Hard rules", 1)[0]
|
|
100
|
-
self.assertLess(
|
|
101
|
-
gate.index("Rejected with another requested fix"),
|
|
102
|
-
gate.index("Cancellation or a decision to stop delegation"),
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
def test_no_skill_reimplements_codex_process_or_failure_mechanics(self):
|
|
106
|
-
forbidden = {
|
|
107
|
-
"copied round function": re.compile(r"\brun_codex_round\b"),
|
|
108
|
-
"copied status parser": re.compile(r"\bROUND_STATUS\b|\.get\(\"status\""),
|
|
109
|
-
"copied run-id cleanup": re.compile(r"\bFAILED_RUN_ID\b"),
|
|
110
|
-
"raw codex exec": re.compile(r"(?m)^\s*codex\s+exec\b"),
|
|
111
|
-
"manual liveness sleep": re.compile(r"\bsleep\s+90\b"),
|
|
112
|
-
"manual sandbox override": re.compile(r"\bsandbox_mode\s*="),
|
|
113
|
-
"manual process probe": re.compile(r"\bkill\s+-0\b"),
|
|
114
|
-
"manual codex pid": re.compile(r"\bCODEX_PID\b"),
|
|
115
|
-
}
|
|
116
|
-
for name, (_, body) in self.skills.items():
|
|
117
|
-
for label, pattern in forbidden.items():
|
|
118
|
-
with self.subTest(skill=name, forbidden=label):
|
|
119
|
-
self.assertIsNone(pattern.search(body))
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if __name__ == "__main__":
|
|
123
|
-
unittest.main()
|