@ikon85/agent-workflow-kit 0.43.0 → 0.44.1

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