@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,30 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""The shipped pre-commit template must sanitize git's hook environment.
|
|
3
|
-
|
|
4
|
-
Git exports GIT_DIR/GIT_INDEX_FILE to hooks; a consumer gate that runs a test
|
|
5
|
-
suite whose child tests call git in their own temp repos would otherwise
|
|
6
|
-
redirect those calls onto the host repo's index (PR #121's incident)."""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import unittest
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
TEMPLATE = (
|
|
14
|
-
Path(__file__).resolve().parent.parent
|
|
15
|
-
/ ".claude/skills/setup-pre-commit/scripts/pre-commit.template.sh"
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class PreCommitTemplateSanitizesGitEnv(unittest.TestCase):
|
|
20
|
-
def test_template_unsets_git_env_before_gate(self):
|
|
21
|
-
text = TEMPLATE.read_text(encoding="utf-8")
|
|
22
|
-
unset_pos = text.find("unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX")
|
|
23
|
-
gate_pos = text.find(">>> your gate commands here <<<")
|
|
24
|
-
self.assertGreater(unset_pos, -1, "template must unset git's exported hook env")
|
|
25
|
-
self.assertGreater(gate_pos, -1, "template must keep the gate marker")
|
|
26
|
-
self.assertLess(unset_pos, gate_pos, "sanitize must happen before the gate runs")
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if __name__ == "__main__":
|
|
30
|
-
unittest.main()
|
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Fail-closed publish audit for dist-kit/ (Welle 26 / Slice 6 / #990, Step 13).
|
|
3
|
-
|
|
4
|
-
The scrub (scripts/lib/scrub.mjs) removes project-private tokens at build time;
|
|
5
|
-
THIS audit is the BACKSTOP — it scans the assembled dist-kit/ and DENIES the
|
|
6
|
-
publish if anything private survived. Scrub may be imperfect; this fails loud so
|
|
7
|
-
nothing private ever ships silently.
|
|
8
|
-
|
|
9
|
-
Deny classes (each with a negative fixture below):
|
|
10
|
-
- private repo slug `iKon85/Testreporter` + the bare project name
|
|
11
|
-
- board node/field IDs (`PVT…`) + status/wave/cluster option hashes
|
|
12
|
-
- private deploy domains (`*.iverra.de`), Coolify
|
|
13
|
-
- email addresses
|
|
14
|
-
- absolute home paths (`/home/<user>`)
|
|
15
|
-
- residual issue refs (`#NNN`) / hard-rule refs (`HRn`)
|
|
16
|
-
- unresolvable provenance cross-refs (`ADR-####` / `Welle N` / `Slice N`),
|
|
17
|
-
with a documented fixture/example allowlist (PROVENANCE_FIXTURE_SUFFIXES)
|
|
18
|
-
- `../` cross-skill reaches (skills/scripts/docs — except a static top-level
|
|
19
|
-
script import into the shipped `src/lib` deep-module seam, and CLI `src`
|
|
20
|
-
imports which legitimately use `../lib/…`)
|
|
21
|
-
- bare owner/maintainer names OUTSIDE the generated credit files
|
|
22
|
-
- high-entropy secrets (after exempting the manifest's own sha256 file-hashes)
|
|
23
|
-
|
|
24
|
-
Allowlisted: the PUBLIC repo slug `iKon85/agent-workflow-kit`.
|
|
25
|
-
File-exempt (credits legitimately name people): LICENSE, README.md, PROVENANCE.md,
|
|
26
|
-
THIRD-PARTY-NOTICES.md — but these still may NOT carry board IDs / domains /
|
|
27
|
-
secrets / the private repo slug.
|
|
28
|
-
|
|
29
|
-
Run: python3 scripts/test_skill_publish_audit.py
|
|
30
|
-
"""
|
|
31
|
-
import json
|
|
32
|
-
import re
|
|
33
|
-
import subprocess
|
|
34
|
-
import sys
|
|
35
|
-
import tempfile
|
|
36
|
-
import unittest
|
|
37
|
-
from pathlib import Path
|
|
38
|
-
|
|
39
|
-
REPO = Path(__file__).resolve().parent.parent
|
|
40
|
-
PUBLIC_CENSUS_UNIT = json.loads(
|
|
41
|
-
(REPO / "test/fixtures/census-consumers/public-unit.json").read_text(encoding="utf-8")
|
|
42
|
-
)["paths"]
|
|
43
|
-
PUBLIC_SLUG = "iKon85/agent-workflow-kit"
|
|
44
|
-
CREDIT_FILES = {"LICENSE", "README.md", "PROVENANCE.md", "THIRD-PARTY-NOTICES.md"}
|
|
45
|
-
PRIVATE_SKILLS = {
|
|
46
|
-
"drizzle", "migrations", "forecast-logic", "bug-bucketing", "iverra-brand",
|
|
47
|
-
"ui-ux-pro-max",
|
|
48
|
-
}
|
|
49
|
-
# pin SHAs are legitimate attribution (full forms exempted from high-entropy)
|
|
50
|
-
KNOWN_PIN_HASHES = {
|
|
51
|
-
"2bf70051928429983de3b5718d277150926f8c89",
|
|
52
|
-
"ba71f82e8469395d3f7c4ed824334b16676e87a0",
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
# (label, regex) — denied EVERYWHERE, even in credit files.
|
|
56
|
-
HARD_DENY = [
|
|
57
|
-
("private repo slug", re.compile(r"iKon85/Testreporter")),
|
|
58
|
-
("board node/field id", re.compile(r"PVT(?:SSF|F)?_[A-Za-z0-9]")),
|
|
59
|
-
("private deploy domain", re.compile(r"[A-Za-z0-9.-]*\.iverra\.de")),
|
|
60
|
-
("Coolify reference", re.compile(r"\bCoolify\b")),
|
|
61
|
-
("email address", re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")),
|
|
62
|
-
("absolute home path", re.compile(r"/home/[A-Za-z0-9._-]+")),
|
|
63
|
-
("home-encoded project slug", re.compile(r"-home-[a-z0-9]+-projects")),
|
|
64
|
-
("issue ref", re.compile(r"#\d{3,5}\b")),
|
|
65
|
-
("hard-rule ref", re.compile(r"\bHR\d+\b")),
|
|
66
|
-
("project name", re.compile(r"[Tt]estreporter")),
|
|
67
|
-
("kit issue ref", re.compile(r"\bkit#\d+\b")),
|
|
68
|
-
]
|
|
69
|
-
# Provenance cross-refs a kit consumer cannot resolve (no docs/adr dir, no wave
|
|
70
|
-
# history). Denied EVERYWHERE except the documented fixture/example files below,
|
|
71
|
-
# where a Welle/Slice/ADR token is legitimate CONTENT — synthetic PRD scenarios,
|
|
72
|
-
# a template placeholder, illustrative ADR names, or board-sync's wave-prefix
|
|
73
|
-
# parser examples — not project provenance. Keep in sync with scrub.mjs's PROV
|
|
74
|
-
# class (which strips citation-shaped provenance from every other body/script).
|
|
75
|
-
PROVENANCE_DENY = [
|
|
76
|
-
("ADR ref", re.compile(r"\bADR-\d{3,4}\b")),
|
|
77
|
-
("Welle ref", re.compile(r"\bWelle \d+\b")),
|
|
78
|
-
("Slice ref", re.compile(r"\bSlice \d+[a-z]?\b")),
|
|
79
|
-
]
|
|
80
|
-
# Matched by dest-path SUFFIX so both surfaces (.claude/skills, .agents/skills)
|
|
81
|
-
# resolve with one entry each.
|
|
82
|
-
PROVENANCE_FIXTURE_SUFFIXES = (
|
|
83
|
-
"spec-self-critique/scenarios.md", # synthetic Program-PRD fixtures
|
|
84
|
-
"to-issues/SKILL.md", # `Welle $WAVE / Slice 1a` title example
|
|
85
|
-
"improve-codebase-architecture/SKILL.md", # illustrative `ADR-0007` example
|
|
86
|
-
"setup-workflow/domain.md", # illustrative `ADR-0007` example
|
|
87
|
-
"docs/agents/wave-anchor-template.md", # `Slice 1` / `Welle <N>` placeholders
|
|
88
|
-
"scripts/board-sync.py", # `Welle 7 — X` wave-prefix parser examples
|
|
89
|
-
)
|
|
90
|
-
# denied only OUTSIDE credit files (scrub should have neutralized these)
|
|
91
|
-
BARE_PRIVATE = [
|
|
92
|
-
("bare owner iKon85", re.compile(r"iKon85")),
|
|
93
|
-
("maintainer name", re.compile(r"\bNiko\b")),
|
|
94
|
-
]
|
|
95
|
-
PARENT_REF = re.compile(r"\.\./")
|
|
96
|
-
SHIPPED_SRC_IMPORT = re.compile(
|
|
97
|
-
r"^\s*import\b.*\bfrom\s+['\"]\.\./src/lib/[A-Za-z0-9-]+\.mjs['\"];\s*$"
|
|
98
|
-
)
|
|
99
|
-
LONG_HEX = re.compile(r"\b[0-9a-f]{40,}\b")
|
|
100
|
-
GH_TOKEN = re.compile(r"\b(?:ghp|gho|ghu|ghs|github_pat)_[A-Za-z0-9_]{20,}")
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _parent_ref_scan_text(rel: str, text: str) -> str:
|
|
104
|
-
lines = text.splitlines()
|
|
105
|
-
if rel == "scripts/loc_offender_core.py":
|
|
106
|
-
# This is the portable path-traversal guard itself, not a cross-skill reach.
|
|
107
|
-
lines = [
|
|
108
|
-
line for line in lines
|
|
109
|
-
if 'p.startswith("../")' not in line and '"/../" in p' not in line
|
|
110
|
-
]
|
|
111
|
-
if rel.startswith("scripts/") and rel.endswith(".mjs"):
|
|
112
|
-
# Top-level shipped scripts may consume the kit's shipped deep modules.
|
|
113
|
-
# The exact static-import shape stays inside the bundle root; every other
|
|
114
|
-
# parent reach remains visible to the deny rule below.
|
|
115
|
-
lines = [line for line in lines if not SHIPPED_SRC_IMPORT.match(line)]
|
|
116
|
-
return "\n".join(lines)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def _known_hashes(root: Path) -> set:
|
|
120
|
-
hashes = set(KNOWN_PIN_HASHES)
|
|
121
|
-
pkg = root / "agent-workflow-kit.package.json"
|
|
122
|
-
if pkg.exists():
|
|
123
|
-
data = json.loads(pkg.read_text(encoding="utf-8"))
|
|
124
|
-
for f in data.get("files", []):
|
|
125
|
-
if "sha256" in f:
|
|
126
|
-
hashes.add(f["sha256"])
|
|
127
|
-
gitleaks = root / "scripts/security/gitleaks-profile.json"
|
|
128
|
-
if gitleaks.exists():
|
|
129
|
-
data = json.loads(gitleaks.read_text(encoding="utf-8"))
|
|
130
|
-
for platform in data.get("platforms", {}).values():
|
|
131
|
-
checksum = platform.get("sha256")
|
|
132
|
-
if isinstance(checksum, str):
|
|
133
|
-
hashes.add(checksum)
|
|
134
|
-
return hashes
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def _scan_file(rel: str, text: str, known_hashes: set) -> list:
|
|
138
|
-
out = []
|
|
139
|
-
base = Path(rel).name
|
|
140
|
-
credit = base in CREDIT_FILES
|
|
141
|
-
scan = text.replace(PUBLIC_SLUG, "") # allowlist the public slug
|
|
142
|
-
for label, pat in HARD_DENY:
|
|
143
|
-
if pat.search(scan):
|
|
144
|
-
out.append(f"{rel}: {label}")
|
|
145
|
-
if not rel.endswith(PROVENANCE_FIXTURE_SUFFIXES):
|
|
146
|
-
for label, pat in PROVENANCE_DENY:
|
|
147
|
-
if pat.search(scan):
|
|
148
|
-
out.append(f"{rel}: {label}")
|
|
149
|
-
if not credit:
|
|
150
|
-
for label, pat in BARE_PRIVATE:
|
|
151
|
-
if pat.search(scan):
|
|
152
|
-
out.append(f"{rel}: {label}")
|
|
153
|
-
if not rel.startswith("src/") and PARENT_REF.search(_parent_ref_scan_text(rel, text)):
|
|
154
|
-
out.append(f"{rel}: ../ parent reference")
|
|
155
|
-
for m in LONG_HEX.finditer(text):
|
|
156
|
-
if m.group(0) not in known_hashes:
|
|
157
|
-
out.append(f"{rel}: high-entropy hex (possible secret)")
|
|
158
|
-
break
|
|
159
|
-
if GH_TOKEN.search(text):
|
|
160
|
-
out.append(f"{rel}: GitHub-token-like string")
|
|
161
|
-
return out
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def audit_dir(root) -> list:
|
|
165
|
-
"""Return a list of violation strings ([] = clean = publishable)."""
|
|
166
|
-
root = Path(root)
|
|
167
|
-
known = _known_hashes(root)
|
|
168
|
-
violations = []
|
|
169
|
-
for f in sorted(root.rglob("*")):
|
|
170
|
-
if not f.is_file():
|
|
171
|
-
continue
|
|
172
|
-
rel = f.relative_to(root).as_posix()
|
|
173
|
-
raw = f.read_bytes()
|
|
174
|
-
if b"\x00" in raw: # binary — skip
|
|
175
|
-
continue
|
|
176
|
-
violations += _scan_file(rel, raw.decode("utf-8", errors="replace"), known)
|
|
177
|
-
# project-private skill dirs must never appear in the kit
|
|
178
|
-
for surface in (".claude/skills", ".agents/skills"):
|
|
179
|
-
d = root / surface
|
|
180
|
-
if d.is_dir():
|
|
181
|
-
for child in d.iterdir():
|
|
182
|
-
if child.is_dir() and child.name in PRIVATE_SKILLS:
|
|
183
|
-
violations.append(f"{surface}/{child.name}: project-private skill shipped")
|
|
184
|
-
return violations
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
# --------------------------------------------------------------------------- #
|
|
188
|
-
class AuditCatchesEachClass(unittest.TestCase):
|
|
189
|
-
"""Inject one violation per class into a clean fixture → it must be caught."""
|
|
190
|
-
|
|
191
|
-
def setUp(self):
|
|
192
|
-
self.dir = Path(tempfile.mkdtemp())
|
|
193
|
-
# a minimal CLEAN kit
|
|
194
|
-
(self.dir / "agent-workflow-kit.package.json").write_text(
|
|
195
|
-
json.dumps({"kitVersion": "0.1.0", "files": [
|
|
196
|
-
{"path": ".claude/skills/x/SKILL.md", "sha256": "a" * 64}]}),
|
|
197
|
-
encoding="utf-8")
|
|
198
|
-
self.skill = self.dir / ".claude/skills/x"
|
|
199
|
-
self.skill.mkdir(parents=True)
|
|
200
|
-
(self.skill / "SKILL.md").write_text("# clean skill\nuse `#<n>` and #0f172a\n",
|
|
201
|
-
encoding="utf-8")
|
|
202
|
-
|
|
203
|
-
def _body(self, content):
|
|
204
|
-
(self.skill / "SKILL.md").write_text(content, encoding="utf-8")
|
|
205
|
-
return audit_dir(self.dir)
|
|
206
|
-
|
|
207
|
-
def test_clean_passes(self):
|
|
208
|
-
self.assertEqual(audit_dir(self.dir), [])
|
|
209
|
-
|
|
210
|
-
def test_private_slug(self):
|
|
211
|
-
self.assertTrue(any("repo slug" in v for v in self._body("see iKon85/Testreporter")))
|
|
212
|
-
|
|
213
|
-
def test_board_id(self):
|
|
214
|
-
self.assertTrue(any("board" in v for v in self._body("id PVT_kwHOAuH31M4BVtcf")))
|
|
215
|
-
|
|
216
|
-
def test_domain(self):
|
|
217
|
-
self.assertTrue(any("domain" in v for v in self._body("curl testreporter.iverra.de")))
|
|
218
|
-
|
|
219
|
-
def test_email(self):
|
|
220
|
-
self.assertTrue(any("email" in v for v in self._body("ping a@b.com please")))
|
|
221
|
-
|
|
222
|
-
def test_home_path(self):
|
|
223
|
-
self.assertTrue(any("home path" in v for v in self._body("at /home/niko/x")))
|
|
224
|
-
|
|
225
|
-
def test_home_encoded_slug(self):
|
|
226
|
-
leak = '$HOME/.claude/projects/-home-niko-projects-x/memory'
|
|
227
|
-
self.assertTrue(any("home-encoded" in v for v in self._body(leak)))
|
|
228
|
-
|
|
229
|
-
def test_issue_ref(self):
|
|
230
|
-
self.assertTrue(any("issue ref" in v for v in self._body("fixed in #824 now")))
|
|
231
|
-
|
|
232
|
-
def test_hr_ref(self):
|
|
233
|
-
self.assertTrue(any("hard-rule" in v for v in self._body("per HR16 rule")))
|
|
234
|
-
|
|
235
|
-
def test_kit_issue_ref(self):
|
|
236
|
-
self.assertTrue(any("kit issue ref" in v for v in self._body("see kit#27 §2")))
|
|
237
|
-
|
|
238
|
-
def test_adr_ref(self):
|
|
239
|
-
self.assertTrue(any("ADR ref" in v for v in self._body("per ADR-0034 rule")))
|
|
240
|
-
|
|
241
|
-
def test_welle_ref(self):
|
|
242
|
-
self.assertTrue(any("Welle ref" in v for v in self._body("built in Welle 52")))
|
|
243
|
-
|
|
244
|
-
def test_slice_ref(self):
|
|
245
|
-
self.assertTrue(any("Slice ref" in v for v in self._body("done in Slice 1g")))
|
|
246
|
-
|
|
247
|
-
def test_parent_ref(self):
|
|
248
|
-
self.assertTrue(any("parent" in v for v in self._body("[x](../../other/SKILL.md)")))
|
|
249
|
-
|
|
250
|
-
def test_static_script_import_into_shipped_src_lib_is_allowed(self):
|
|
251
|
-
script = self.dir / "scripts/release.mjs"
|
|
252
|
-
script.parent.mkdir()
|
|
253
|
-
script.write_text(
|
|
254
|
-
"import { run } from '../src/lib/release-core.mjs';\nrun();\n",
|
|
255
|
-
encoding="utf-8",
|
|
256
|
-
)
|
|
257
|
-
self.assertEqual(audit_dir(self.dir), [])
|
|
258
|
-
|
|
259
|
-
def test_bare_owner_in_body(self):
|
|
260
|
-
self.assertTrue(any("iKon85" in v for v in self._body("authored by iKon85")))
|
|
261
|
-
|
|
262
|
-
def test_secret(self):
|
|
263
|
-
self.assertTrue(any("secret" in v for v in self._body("token " + "d" * 50)))
|
|
264
|
-
|
|
265
|
-
def test_gh_token(self):
|
|
266
|
-
self.assertTrue(any("token" in v for v in self._body("ghp_" + "A" * 30)))
|
|
267
|
-
|
|
268
|
-
def test_private_skill_dir_flagged(self):
|
|
269
|
-
(self.dir / ".claude/skills/drizzle").mkdir(parents=True)
|
|
270
|
-
self.assertTrue(any("project-private" in v for v in audit_dir(self.dir)))
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
class Exemptions(unittest.TestCase):
|
|
274
|
-
"""Public slug is allowlisted; credit files may legitimately name people."""
|
|
275
|
-
|
|
276
|
-
def setUp(self):
|
|
277
|
-
self.dir = Path(tempfile.mkdtemp())
|
|
278
|
-
(self.dir / "agent-workflow-kit.package.json").write_text(
|
|
279
|
-
'{"kitVersion":"0.1.0","files":[]}', encoding="utf-8")
|
|
280
|
-
|
|
281
|
-
def _write(self, name, content):
|
|
282
|
-
(self.dir / name).write_text(content, encoding="utf-8")
|
|
283
|
-
|
|
284
|
-
def test_public_slug_allowed(self):
|
|
285
|
-
self._write("README.md", "npx github:iKon85/agent-workflow-kit init")
|
|
286
|
-
self.assertEqual(audit_dir(self.dir), [])
|
|
287
|
-
|
|
288
|
-
def test_credit_file_may_name_maintainer(self):
|
|
289
|
-
self._write("LICENSE", "Copyright (c) 2026 Niko (iKon85)\nMatt Pocock\nChase AI")
|
|
290
|
-
self.assertEqual(audit_dir(self.dir), [])
|
|
291
|
-
|
|
292
|
-
def test_credit_file_still_denies_board_id(self):
|
|
293
|
-
self._write("LICENSE", "Niko (iKon85) PVT_leak")
|
|
294
|
-
self.assertTrue(any("board" in v for v in audit_dir(self.dir)))
|
|
295
|
-
|
|
296
|
-
def test_manifest_own_hashes_not_flagged_as_secret(self):
|
|
297
|
-
# the package manifest's 64-hex file hashes are exempt (they ARE the kit's hashes)
|
|
298
|
-
(self.dir / "agent-workflow-kit.package.json").write_text(
|
|
299
|
-
json.dumps({"kitVersion": "0.1.0", "files": [
|
|
300
|
-
{"path": "x", "sha256": "f" * 64}]}), encoding="utf-8")
|
|
301
|
-
self.assertEqual(audit_dir(self.dir), [])
|
|
302
|
-
|
|
303
|
-
def test_gitleaks_profile_checksums_are_verified_pin_metadata(self):
|
|
304
|
-
profile = self.dir / "scripts/security"
|
|
305
|
-
profile.mkdir(parents=True)
|
|
306
|
-
(profile / "gitleaks-profile.json").write_text(json.dumps({
|
|
307
|
-
"platforms": {"linux-x64": {"sha256": "e" * 64}},
|
|
308
|
-
}), encoding="utf-8")
|
|
309
|
-
self.assertEqual(audit_dir(self.dir), [])
|
|
310
|
-
|
|
311
|
-
def test_loc_offender_traversal_guard_allowed(self):
|
|
312
|
-
p = self.dir / "scripts"
|
|
313
|
-
p.mkdir()
|
|
314
|
-
(p / "loc_offender_core.py").write_text(
|
|
315
|
-
'if p == ".." or p.startswith("../") or "/../" in p or p.endswith("/.."):\n'
|
|
316
|
-
' return None\n',
|
|
317
|
-
encoding="utf-8")
|
|
318
|
-
self.assertEqual(audit_dir(self.dir), [])
|
|
319
|
-
|
|
320
|
-
def test_top_level_script_may_import_camel_case_shipped_deep_module(self):
|
|
321
|
-
p = self.dir / "scripts"
|
|
322
|
-
p.mkdir()
|
|
323
|
-
(p / "readiness.mjs").write_text(
|
|
324
|
-
"import { load } from '../src/lib/skillRegistry.mjs';\n",
|
|
325
|
-
encoding="utf-8")
|
|
326
|
-
self.assertEqual(audit_dir(self.dir), [])
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
class RealDistKitIsClean(unittest.TestCase):
|
|
330
|
-
"""Build dist-kit/ from SSOT and assert the audit passes (the publish gate)."""
|
|
331
|
-
|
|
332
|
-
def test_built_dist_kit_passes_audit(self):
|
|
333
|
-
build = REPO / "scripts/build-kit.mjs"
|
|
334
|
-
try:
|
|
335
|
-
r = subprocess.run(["node", str(build)], cwd=REPO,
|
|
336
|
-
capture_output=True, text=True, timeout=120)
|
|
337
|
-
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
|
|
338
|
-
self.skipTest(f"node/build unavailable: {e}")
|
|
339
|
-
self.assertEqual(r.returncode, 0, r.stderr)
|
|
340
|
-
violations = audit_dir(REPO / "dist-kit")
|
|
341
|
-
self.assertEqual(violations, [], "\n".join(violations[:40]))
|
|
342
|
-
|
|
343
|
-
def test_built_dist_kit_contains_publishable_census_without_foreign_reach(self):
|
|
344
|
-
build = subprocess.run(
|
|
345
|
-
["node", str(REPO / "scripts/build-kit.mjs")], cwd=REPO,
|
|
346
|
-
capture_output=True, text=True, timeout=120,
|
|
347
|
-
)
|
|
348
|
-
self.assertEqual(build.returncode, 0, build.stderr)
|
|
349
|
-
for rel in PUBLIC_CENSUS_UNIT:
|
|
350
|
-
path = REPO / "dist-kit" / rel
|
|
351
|
-
self.assertTrue(path.is_file(), f"missing published census resource: {rel}")
|
|
352
|
-
text = path.read_text(encoding="utf-8")
|
|
353
|
-
self.assertNotIn("tools/agent-workflow-kit", text)
|
|
354
|
-
self.assertNotIn("testreporter", text.lower())
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if __name__ == "__main__":
|
|
358
|
-
unittest.main(verbosity=2)
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Manifest-derived readiness declaration and marker grammar guard."""
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import re
|
|
7
|
-
import subprocess
|
|
8
|
-
import unittest
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
ROOT = Path(__file__).resolve().parents[1]
|
|
12
|
-
MANIFEST_PATH = ROOT / ".claude/skills/skill-manifest.json"
|
|
13
|
-
START = re.compile(r"^<!-- readiness:block ([a-z][A-Za-z0-9]*) -->$")
|
|
14
|
-
END = "<!-- readiness:end -->"
|
|
15
|
-
SURFACE = {"claude": ".claude/skills", "codex": ".agents/skills"}
|
|
16
|
-
FIXTURES = ROOT / "test/fixtures/readiness"
|
|
17
|
-
REQUIRED_V1 = {
|
|
18
|
-
"to-prd": ["issueTracker", "managedBoard"],
|
|
19
|
-
"to-issues": ["issueTracker", "managedBoard", "specCompleteness"],
|
|
20
|
-
"to-waves": ["issueTracker", "managedBoard", "specCompleteness"],
|
|
21
|
-
"board-to-waves": ["issueTracker", "managedBoard"],
|
|
22
|
-
"triage": ["issueTracker", "managedBoard", "triageLabels"],
|
|
23
|
-
"orchestrate-wave": ["issueTracker", "managedBoard"],
|
|
24
|
-
"local-ci": ["localCiRecipe"],
|
|
25
|
-
"project-release": ["projectReleaseProfile"],
|
|
26
|
-
"security-audit": ["securityAuditRunbook"],
|
|
27
|
-
}
|
|
28
|
-
OPTIONAL_V1 = {
|
|
29
|
-
"wrapup": {"deployReport": "prodTarget"},
|
|
30
|
-
"orchestrate-wave": {"projectRecipe": "orchestrateWaveRecipe"},
|
|
31
|
-
"spec-self-critique": {"projectEnrichment": "specCritiqueLayer"},
|
|
32
|
-
"code-review": {"projectEnrichment": "codeReviewLayer"},
|
|
33
|
-
"verify-spike": {"projectPlacement": "verifySpikeLayer"},
|
|
34
|
-
"audit-skills": {"projectChecks": "auditSkillsLayer"},
|
|
35
|
-
"git-worktree-recover": {"projectRecovery": "worktreeRecoveryLayer"},
|
|
36
|
-
}
|
|
37
|
-
COMPAT_UPDATE_SCRIPT = r"""
|
|
38
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
39
|
-
import { join } from 'node:path';
|
|
40
|
-
import { pathToFileURL } from 'node:url';
|
|
41
|
-
const [repo, fixturePath] = process.argv.slice(-2);
|
|
42
|
-
const load = async (path) => import(pathToFileURL(join(repo, path)));
|
|
43
|
-
const { init } = await load('src/commands/init.mjs');
|
|
44
|
-
const { update } = await load('src/commands/update.mjs');
|
|
45
|
-
const { checkSkill } = await load('scripts/readiness.mjs');
|
|
46
|
-
const { makeKit, makeEmptyDir, cleanup } = await load('test/helpers.mjs');
|
|
47
|
-
const { PACKAGE_MANIFEST_NAME, readManifest, writeManifest } = await load('src/lib/manifest.mjs');
|
|
48
|
-
const { sha256 } = await load('src/lib/hash.mjs');
|
|
49
|
-
const readinessPath = '.claude/skills/skill-manifest.json';
|
|
50
|
-
const fixture = JSON.parse(await readFile(fixturePath, 'utf8'));
|
|
51
|
-
const current = JSON.parse(await readFile(join(repo, readinessPath), 'utf8'));
|
|
52
|
-
const previous = structuredClone(current);
|
|
53
|
-
for (const skill of Object.keys(fixture.readinessFixture.skills)) {
|
|
54
|
-
previous.skills[skill].readiness = {};
|
|
55
|
-
}
|
|
56
|
-
const kit = await makeKit({ '.claude/skills/to-prd/SKILL.md': 'fixture\n' });
|
|
57
|
-
const consumer = await makeEmptyDir();
|
|
58
|
-
async function setKitReadiness(manifest) {
|
|
59
|
-
const candidateManifest = structuredClone(manifest);
|
|
60
|
-
for (const [name, declaration] of Object.entries(candidateManifest.skills)) {
|
|
61
|
-
declaration.publish = name === 'to-prd';
|
|
62
|
-
if (name === 'to-prd') declaration.surfaces = ['claude'];
|
|
63
|
-
}
|
|
64
|
-
const content = `${JSON.stringify(candidateManifest, null, 2)}\n`;
|
|
65
|
-
const path = join(kit, readinessPath);
|
|
66
|
-
await mkdir(join(kit, '.claude/skills'), { recursive: true });
|
|
67
|
-
await writeFile(path, content);
|
|
68
|
-
const pkg = await readManifest(join(kit, PACKAGE_MANIFEST_NAME));
|
|
69
|
-
const entry = pkg.files.find(({ path: candidate }) => candidate === readinessPath);
|
|
70
|
-
if (entry) entry.sha256 = sha256(content);
|
|
71
|
-
else pkg.files.push({ path: readinessPath, kind: 'doc', sha256: sha256(content), mode: 0o644, origin: 'kit' });
|
|
72
|
-
await writeManifest(join(kit, PACKAGE_MANIFEST_NAME), pkg);
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
await setKitReadiness(previous);
|
|
76
|
-
await init({ kitRoot: kit, consumerRoot: consumer });
|
|
77
|
-
for (const [relative, body] of Object.entries(fixture.readinessFixture.evidence)) {
|
|
78
|
-
const path = join(consumer, relative);
|
|
79
|
-
await mkdir(join(path, '..'), { recursive: true });
|
|
80
|
-
await writeFile(path, body);
|
|
81
|
-
}
|
|
82
|
-
const manifestPath = join(consumer, 'agent-workflow-kit.json');
|
|
83
|
-
const installed = await readManifest(manifestPath);
|
|
84
|
-
await writeManifest(manifestPath, {
|
|
85
|
-
...installed,
|
|
86
|
-
readinessDecisions: fixture.readinessDecisions ?? {},
|
|
87
|
-
readinessFixture: fixture.readinessFixture,
|
|
88
|
-
});
|
|
89
|
-
const before = {};
|
|
90
|
-
for (const skill of Object.keys(fixture.readinessFixture.skills)) {
|
|
91
|
-
before[skill] = await checkSkill({ root: consumer, skill, manifest: previous });
|
|
92
|
-
}
|
|
93
|
-
await setKitReadiness(current);
|
|
94
|
-
const identity = { name: '@ikon85/agent-workflow-kit', version: '0.1.0', tarballIntegrity: 'sha512-fixture', manifestSha256: 'fixture-manifest' };
|
|
95
|
-
const result = await update({
|
|
96
|
-
kitRoot: kit, consumerRoot: consumer,
|
|
97
|
-
releaseIdentities: { installed: { name: identity.name, version: identity.version, manifestSha256: identity.manifestSha256 }, npm: identity, github: identity },
|
|
98
|
-
verify: async () => {},
|
|
99
|
-
});
|
|
100
|
-
const afterManifest = await readManifest(manifestPath);
|
|
101
|
-
const after = {};
|
|
102
|
-
for (const skill of Object.keys(fixture.readinessFixture.skills)) {
|
|
103
|
-
after[skill] = await checkSkill({ root: consumer, skill, manifest: current });
|
|
104
|
-
}
|
|
105
|
-
console.log(JSON.stringify({ before, result, afterManifest, after }));
|
|
106
|
-
} finally {
|
|
107
|
-
await cleanup(kit, consumer);
|
|
108
|
-
}
|
|
109
|
-
"""
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def load_manifest() -> dict:
|
|
113
|
-
return json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def marker_sequence(body: str, declared: set[str]) -> list[str]:
|
|
117
|
-
sequence: list[str] = []
|
|
118
|
-
active: str | None = None
|
|
119
|
-
seen: set[str] = set()
|
|
120
|
-
for number, line in enumerate(body.splitlines(), 1):
|
|
121
|
-
match = START.fullmatch(line)
|
|
122
|
-
if match:
|
|
123
|
-
block = match.group(1)
|
|
124
|
-
if active:
|
|
125
|
-
raise AssertionError(f"nested readiness block at line {number}")
|
|
126
|
-
if block in seen:
|
|
127
|
-
raise AssertionError(f"duplicate readiness block {block}")
|
|
128
|
-
if block not in declared:
|
|
129
|
-
raise AssertionError(f"unknown readiness block {block}")
|
|
130
|
-
active = block
|
|
131
|
-
seen.add(block)
|
|
132
|
-
sequence.append(block)
|
|
133
|
-
elif line == END:
|
|
134
|
-
if not active:
|
|
135
|
-
raise AssertionError(f"unbalanced readiness end at line {number}")
|
|
136
|
-
active = None
|
|
137
|
-
elif "readiness:block" in line or "readiness:end" in line:
|
|
138
|
-
raise AssertionError(f"invalid readiness marker grammar at line {number}")
|
|
139
|
-
if active:
|
|
140
|
-
raise AssertionError(f"unbalanced readiness block {active}")
|
|
141
|
-
return sequence
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def validate_surface_sequences(sequences: list[list[str]]) -> None:
|
|
145
|
-
if any(sequence != sequences[0] for sequence in sequences[1:]):
|
|
146
|
-
raise AssertionError("cross-surface readiness marker drift")
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
class ReadinessContractTests(unittest.TestCase):
|
|
150
|
-
def setUp(self) -> None:
|
|
151
|
-
self.manifest = load_manifest()
|
|
152
|
-
self.catalog = self.manifest["readiness"]["capabilities"]
|
|
153
|
-
self.published = {
|
|
154
|
-
name: entry for name, entry in self.manifest["skills"].items()
|
|
155
|
-
if entry.get("publish")
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
def test_contract_and_skill_denominator_come_from_publish_manifest(self) -> None:
|
|
159
|
-
self.assertEqual(self.manifest["readiness"]["contractVersion"], 1)
|
|
160
|
-
manifest_source = (ROOT / "src/lib/manifest.mjs").read_text(encoding="utf-8")
|
|
161
|
-
version = re.search(r"READINESS_CONTRACT_VERSION = (\d+);", manifest_source)
|
|
162
|
-
self.assertIsNotNone(version)
|
|
163
|
-
self.assertEqual(int(version.group(1)), self.manifest["readiness"]["contractVersion"])
|
|
164
|
-
self.assertGreater(len(self.published), 0)
|
|
165
|
-
for name, entry in self.published.items():
|
|
166
|
-
self.assertTrue(entry.get("surfaces"), name)
|
|
167
|
-
|
|
168
|
-
def test_every_declaration_references_the_single_capability_catalog(self) -> None:
|
|
169
|
-
for skill, entry in self.published.items():
|
|
170
|
-
declaration = entry.get("readiness", {})
|
|
171
|
-
references = list(declaration.get("required", []))
|
|
172
|
-
references += list(declaration.get("optionalBlocks", {}).values())
|
|
173
|
-
for capability in references:
|
|
174
|
-
self.assertIn(capability, self.catalog, f"{skill}: {capability}")
|
|
175
|
-
blocks = list(declaration.get("optionalBlocks", {}))
|
|
176
|
-
self.assertEqual(len(blocks), len(set(blocks)), skill)
|
|
177
|
-
|
|
178
|
-
def test_locked_v1_policy_cannot_disappear_from_the_manifest(self) -> None:
|
|
179
|
-
actual_required = {
|
|
180
|
-
skill: entry["readiness"]["required"]
|
|
181
|
-
for skill, entry in self.published.items()
|
|
182
|
-
if entry.get("readiness", {}).get("required")
|
|
183
|
-
}
|
|
184
|
-
actual_optional = {
|
|
185
|
-
skill: entry["readiness"]["optionalBlocks"]
|
|
186
|
-
for skill, entry in self.published.items()
|
|
187
|
-
if entry.get("readiness", {}).get("optionalBlocks")
|
|
188
|
-
}
|
|
189
|
-
self.assertEqual(actual_required, REQUIRED_V1)
|
|
190
|
-
self.assertEqual(actual_optional, OPTIONAL_V1)
|
|
191
|
-
|
|
192
|
-
def test_every_declared_consumer_has_the_standard_preflight(self) -> None:
|
|
193
|
-
consumers = {
|
|
194
|
-
skill: entry for skill, entry in self.published.items()
|
|
195
|
-
if entry.get("readiness")
|
|
196
|
-
}
|
|
197
|
-
self.assertGreater(len(consumers), 0)
|
|
198
|
-
for skill, entry in consumers.items():
|
|
199
|
-
expected = f"node scripts/readiness.mjs check --skill {skill} --json"
|
|
200
|
-
for surface in entry["surfaces"]:
|
|
201
|
-
path = ROOT / SURFACE[surface] / skill / "SKILL.md"
|
|
202
|
-
body = path.read_text(encoding="utf-8")
|
|
203
|
-
self.assertEqual(body.count(expected), 1, f"{surface}:{skill}")
|
|
204
|
-
|
|
205
|
-
def test_markers_are_balanced_declared_and_surface_equal_when_activated(self) -> None:
|
|
206
|
-
for skill, entry in self.published.items():
|
|
207
|
-
declared = set(entry.get("readiness", {}).get("optionalBlocks", {}))
|
|
208
|
-
sequences: list[tuple[str, list[str]]] = []
|
|
209
|
-
for surface in entry["surfaces"]:
|
|
210
|
-
path = ROOT / SURFACE[surface] / skill / "SKILL.md"
|
|
211
|
-
self.assertTrue(path.is_file(), f"missing published surface: {path}")
|
|
212
|
-
sequence = marker_sequence(path.read_text(encoding="utf-8"), declared)
|
|
213
|
-
self.assertEqual(set(sequence), declared, f"undeclared marker coverage: {surface}:{skill}")
|
|
214
|
-
sequences.append((surface, sequence))
|
|
215
|
-
if len(sequences) > 1:
|
|
216
|
-
validate_surface_sequences([sequence for _, sequence in sequences])
|
|
217
|
-
|
|
218
|
-
def test_marker_parser_rejects_each_structural_failure(self) -> None:
|
|
219
|
-
declared = {"projectEnrichment"}
|
|
220
|
-
bad = [
|
|
221
|
-
"<!-- readiness:block projectEnrichment -->\n<!-- readiness:block projectEnrichment -->",
|
|
222
|
-
"<!-- readiness:block projectEnrichment -->\n<!-- readiness:end -->\n<!-- readiness:block projectEnrichment -->",
|
|
223
|
-
"<!-- readiness:end -->",
|
|
224
|
-
"<!-- readiness:block projectEnrichment -->",
|
|
225
|
-
"<!-- readiness:block unknownBlock -->\n<!-- readiness:end -->",
|
|
226
|
-
" <!-- readiness:block projectEnrichment -->",
|
|
227
|
-
]
|
|
228
|
-
for body in bad:
|
|
229
|
-
with self.subTest(body=body), self.assertRaises(AssertionError):
|
|
230
|
-
marker_sequence(body, declared)
|
|
231
|
-
with self.assertRaises(AssertionError):
|
|
232
|
-
validate_surface_sequences([["projectEnrichment"], []])
|
|
233
|
-
|
|
234
|
-
def test_legacy_and_current_consumers_keep_previously_ready_skill_cores(self) -> None:
|
|
235
|
-
fixtures = sorted(FIXTURES.glob("*/agent-workflow-kit.json"))
|
|
236
|
-
self.assertEqual([path.parent.name for path in fixtures], ["current", "legacy"])
|
|
237
|
-
for fixture_path in fixtures:
|
|
238
|
-
fixture = json.loads(fixture_path.read_text(encoding="utf-8"))
|
|
239
|
-
scenario = fixture["readinessFixture"]
|
|
240
|
-
with self.subTest(fixture=fixture_path.parent.name):
|
|
241
|
-
run = subprocess.run(
|
|
242
|
-
["node", "--input-type=module", "-e", COMPAT_UPDATE_SCRIPT,
|
|
243
|
-
str(ROOT), str(fixture_path)],
|
|
244
|
-
check=True, capture_output=True, text=True,
|
|
245
|
-
)
|
|
246
|
-
proof = json.loads(run.stdout)
|
|
247
|
-
self.assertEqual(proof["result"]["state"], "applied")
|
|
248
|
-
self.assertEqual(proof["result"]["availability"]["newlyBlocked"], [])
|
|
249
|
-
for skill, expected in scenario["skills"].items():
|
|
250
|
-
self.assertEqual(proof["before"][skill]["verdict"], "ready", skill)
|
|
251
|
-
verdict = proof["after"][skill]
|
|
252
|
-
self.assertNotEqual(verdict["verdict"], "blocked", skill)
|
|
253
|
-
self.assertEqual(verdict["verdict"], expected["verdict"], skill)
|
|
254
|
-
self.assertEqual(verdict["inactiveBlocks"], expected["inactiveBlocks"], skill)
|
|
255
|
-
self.assertEqual(
|
|
256
|
-
proof["afterManifest"].get("readinessDecisions", {}),
|
|
257
|
-
scenario["expectedDecisions"],
|
|
258
|
-
"unavailable behavior must not manufacture a decision",
|
|
259
|
-
)
|
|
260
|
-
self.assertEqual(proof["afterManifest"]["readinessFixture"], scenario)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if __name__ == "__main__":
|
|
264
|
-
unittest.main()
|