@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.
Files changed (126) hide show
  1. package/.agents/skills/grill-me/SKILL.md +3 -1
  2. package/.agents/skills/grill-with-docs/SKILL.md +3 -1
  3. package/.agents/skills/kit-update/SKILL.md +9 -11
  4. package/.agents/skills/orchestrate-wave/SKILL.md +8 -8
  5. package/.agents/skills/setup-workflow/SKILL.md +20 -18
  6. package/.agents/skills/setup-workflow/board-sync.md +4 -1
  7. package/.agents/skills/setup-workflow/workflow-advisories.md +3 -2
  8. package/.agents/skills/setup-workflow/worktree-lifecycle.md +36 -31
  9. package/.agents/skills/to-issues/SKILL.md +43 -0
  10. package/.agents/skills/wrapup/SKILL.md +98 -54
  11. package/.claude/skills/grill-me/SKILL.md +3 -1
  12. package/.claude/skills/grill-me-codex/SKILL.md +3 -1
  13. package/.claude/skills/grill-with-docs/SKILL.md +3 -1
  14. package/.claude/skills/grill-with-docs-codex/SKILL.md +3 -1
  15. package/.claude/skills/kit-update/SKILL.md +9 -11
  16. package/.claude/skills/orchestrate-wave/SKILL.md +8 -8
  17. package/.claude/skills/setup-workflow/SKILL.md +20 -18
  18. package/.claude/skills/setup-workflow/board-sync.md +4 -1
  19. package/.claude/skills/setup-workflow/workflow-advisories.md +3 -2
  20. package/.claude/skills/setup-workflow/worktree-lifecycle.md +36 -31
  21. package/.claude/skills/to-issues/SKILL.md +43 -0
  22. package/.claude/skills/wrapup/SKILL.md +98 -54
  23. package/README.md +249 -7
  24. package/agent-workflow-kit.package.json +42 -42
  25. package/package.json +16 -12
  26. package/scripts/marker_lib.py +3 -0
  27. package/scripts/profile_globs.py +26 -45
  28. package/scripts/release-delta-guard.mjs +26 -9
  29. package/scripts/worktree-lifecycle/README.md +57 -160
  30. package/scripts/worktree-lifecycle/capabilities.json +2 -3
  31. package/scripts/worktree-lifecycle/classify.py +480 -0
  32. package/scripts/worktree-lifecycle/cleanup.py +9 -29
  33. package/scripts/worktree-lifecycle/core.py +68 -1429
  34. package/scripts/worktree-lifecycle/ignore_seed.py +1 -1
  35. package/scripts/worktree-lifecycle/profile.py +46 -29
  36. package/scripts/worktree-lifecycle/setup.py +7 -19
  37. package/scripts/wrapup-land.py +1292 -545
  38. package/src/consumer-migrations.json +1 -16
  39. package/src/lib/bundle.mjs +31 -1
  40. package/docs/adr/0001-consumer-divergence-policy.md +0 -53
  41. package/docs/adr/0002-capability-gated-orchestration.md +0 -70
  42. package/docs/adr/0003-kit-core-and-project-extension-lifecycle.md +0 -63
  43. package/docs/adr/0004-release-intent-is-a-version-tag.md +0 -64
  44. package/docs/adr/0005-to-issues-is-the-planning-facade.md +0 -42
  45. package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +0 -95
  46. package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +0 -88
  47. package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +0 -84
  48. package/docs/adr/0009-teardown-authority-is-stateless-repository-classification.md +0 -101
  49. package/docs/adr/0010-model-roster-replaces-the-optimization-dial.md +0 -87
  50. package/docs/agents/board-sync.md +0 -74
  51. package/docs/agents/code-review.md +0 -14
  52. package/docs/agents/workflow-capabilities.json +0 -28
  53. package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
  54. package/docs/research/benchlm-routing-source.md +0 -198
  55. package/docs/research/consumer-owned-protocol-files.md +0 -238
  56. package/docs/research/frontend-agent-benchmarks.md +0 -282
  57. package/docs/research/model-effort-routing-benchmarks.md +0 -261
  58. package/docs/research/provider-neutral-agent-routing.md +0 -207
  59. package/docs/research/wave-152-consumer-acceptance.md +0 -98
  60. package/docs/research/wave-43-script-hook-census.md +0 -128
  61. package/scripts/build-kit.mjs +0 -117
  62. package/scripts/build-kit.test.mjs +0 -207
  63. package/scripts/census/census.test.mjs +0 -252
  64. package/scripts/census/state.test.mjs +0 -15
  65. package/scripts/census/transaction.test.mjs +0 -103
  66. package/scripts/census-contract.test.mjs +0 -227
  67. package/scripts/check-kit-staleness.mjs +0 -45
  68. package/scripts/check-kit-staleness.test.mjs +0 -19
  69. package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -165
  70. package/scripts/codex-exec.test.mjs +0 -871
  71. package/scripts/grill-census-wiring-guard.mjs +0 -87
  72. package/scripts/grill-census-wiring-guard.test.mjs +0 -107
  73. package/scripts/kit-release.test.mjs +0 -273
  74. package/scripts/kit-update-pr.test.mjs +0 -151
  75. package/scripts/lib/audit-refs.mjs +0 -146
  76. package/scripts/lib/scrub.mjs +0 -113
  77. package/scripts/lib/scrub.test.mjs +0 -187
  78. package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
  79. package/scripts/portability_profile_scan.py +0 -53
  80. package/scripts/release-delta-guard.test.mjs +0 -174
  81. package/scripts/release-parity.test.mjs +0 -90
  82. package/scripts/release-state.test.mjs +0 -360
  83. package/scripts/test_anchor_table.py +0 -69
  84. package/scripts/test_board_bootstrap.py +0 -348
  85. package/scripts/test_board_sync.py +0 -208
  86. package/scripts/test_board_sync_create_idempotency.py +0 -117
  87. package/scripts/test_board_sync_wave_title.py +0 -120
  88. package/scripts/test_census_backstop.py +0 -712
  89. package/scripts/test_census_forward_contract.py +0 -91
  90. package/scripts/test_census_update_contract.test.mjs +0 -454
  91. package/scripts/test_codex_adapter_sync_contract.py +0 -169
  92. package/scripts/test_dist_kit_smoke.py +0 -220
  93. package/scripts/test_drift_guard_diagnostics.py +0 -295
  94. package/scripts/test_issue_claim_contract.py +0 -169
  95. package/scripts/test_kit_docs_language_census.py +0 -178
  96. package/scripts/test_marker_lib.py +0 -162
  97. package/scripts/test_orchestrate_wave_contract.py +0 -381
  98. package/scripts/test_pr_body_check.py +0 -264
  99. package/scripts/test_profile_globs.py +0 -280
  100. package/scripts/test_program_planning_contract.py +0 -133
  101. package/scripts/test_release_authorization_contract.py +0 -101
  102. package/scripts/test_render_anchor.py +0 -267
  103. package/scripts/test_retro_wrapup_contract.py +0 -153
  104. package/scripts/test_skill_code_review_seed.py +0 -89
  105. package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
  106. package/scripts/test_skill_frontmatter_lint.py +0 -234
  107. package/scripts/test_skill_gh_lint.py +0 -124
  108. package/scripts/test_skill_language_census.py +0 -254
  109. package/scripts/test_skill_optional_readiness.py +0 -171
  110. package/scripts/test_skill_portability_lint.py +0 -866
  111. package/scripts/test_skill_precommit_template.py +0 -30
  112. package/scripts/test_skill_publish_audit.py +0 -358
  113. package/scripts/test_skill_readiness_contract.py +0 -264
  114. package/scripts/test_skill_readiness_preflight.py +0 -180
  115. package/scripts/test_skill_required_readiness.py +0 -233
  116. package/scripts/test_skill_selfcontainment_lint.py +0 -255
  117. package/scripts/test_skill_setup_workflow_seeds.py +0 -691
  118. package/scripts/test_skill_stale_name_lint.py +0 -126
  119. package/scripts/test_skill_surface_refs.py +0 -300
  120. package/scripts/test_skill_trailing_artifact_lint.py +0 -71
  121. package/scripts/test_tdd_contract.py +0 -78
  122. package/scripts/test_worktree_ignore_seed.py +0 -320
  123. package/scripts/test_worktree_setup_base_guard.py +0 -140
  124. package/scripts/test_worktree_wrapup_contract.py +0 -1737
  125. package/scripts/test_wrapup_land.py +0 -428
  126. 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()