@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,691 +0,0 @@
1
- #!/usr/bin/env python3
2
- """setup-workflow seed + idempotency-rule spec (Welle 26 / Slice 5 / #989).
3
-
4
- `/setup-workflow` is a prompt-driven skill, so its interactive run is verified
5
- live (mock fresh-fill + in-repo idempotent re-run). This file pins the two parts
6
- that ARE machine-checkable:
7
-
8
- 1. The seed templates it ships are structurally valid for their consumers, so a
9
- fresh fill cannot produce a project layer that makes a downstream skill warn.
10
- 2. The sentinel-based idempotency rule, as a reference implementation + the
11
- canonical example table the skill's prose must satisfy (Codex R1 #12 strict
12
- `state=` header; R2 #2 terminal `not-applicable`; R3 legacy no-sentinel skip).
13
-
14
- It does NOT test Claude's execution of the prose — that is the live-verify.
15
-
16
- Run: python3 scripts/test_skill_setup_workflow_seeds.py
17
- """
18
- import json
19
- import re
20
- import subprocess
21
- import tempfile
22
- import time
23
- import unittest
24
- from pathlib import Path
25
-
26
- REPO = Path(__file__).resolve().parent.parent
27
- SKILL = REPO / ".claude/skills/setup-workflow"
28
-
29
- SENTINEL_RE = re.compile(
30
- r"^<!--\s*setup-workflow:\s*state=(stub|filled|not-applicable)"
31
- r"(?:;\s*mode=(github-projects-v2|none))?\s*-->\s*$"
32
- )
33
-
34
-
35
- # ---- Reference implementation of the documented idempotency rule -------------
36
- def classify(first_line, is_empty):
37
- """Return the action the skill must take for one target file.
38
-
39
- first_line: the file's first line, or None if the file does not exist.
40
- is_empty: True if the file is empty/whitespace-only.
41
- """
42
- if first_line is None:
43
- return "create"
44
- m = SENTINEL_RE.match(first_line.strip())
45
- if m:
46
- return "fill" if m.group(1) == "stub" else "skip"
47
- # No sentinel: non-empty legacy file is left alone; empty is fillable.
48
- return "create" if is_empty else "skip"
49
-
50
-
51
- def update_workflow_action(
52
- provider, choice, destination_exists, pull_requests_allowed=True,
53
- ):
54
- """Reference decision table for the prompt-driven setup contract."""
55
- if provider != "github" or destination_exists or choice != "enable":
56
- return "skip"
57
- return "create" if pull_requests_allowed else "skip"
58
-
59
-
60
- def board_branch_action(tracker, boards_found, project_scope, user_choice):
61
- """Reference decision table for Section D's board branch (#24).
62
-
63
- tracker: the Section A choice.
64
- boards_found: how many owner projects the discovery read found, or None when
65
- the read itself failed.
66
- project_scope: does `gh` carry the write scope board creation needs?
67
- user_choice: the answer to the creation offer ("yes" / "no" / None).
68
-
69
- "discover" fills the profile from an existing board, "create" runs the
70
- bootstrap helper after an explicit yes, "stub" is the retryable stub path.
71
- Creation is offered on exactly one branch — no board, with the scope — and a
72
- decline, a missing scope, ambiguity, or a failed read all stay on the stub.
73
- """
74
- if tracker != "github":
75
- return "not-applicable"
76
- if boards_found is None or boards_found > 1:
77
- return "stub"
78
- if boards_found == 1:
79
- return "discover"
80
- if not project_scope:
81
- return "stub"
82
- return "create" if user_choice == "yes" else "stub"
83
-
84
-
85
- def load_census_setup_effects():
86
- """Parse the shipped seed's executable census transition contract."""
87
- seed = (SKILL / "census.md").read_text(encoding="utf-8")
88
- match = re.search(
89
- r"```json census-setup-effects\n(.*?)\n```", seed, re.DOTALL,
90
- )
91
- if not match:
92
- raise AssertionError("census seed has no structured setup-effects contract")
93
- rows = json.loads(match.group(1))
94
- return {row["state"]: row for row in rows}
95
-
96
-
97
- def resolve_census_state(**inputs):
98
- """Ask the shipped #49 API for a state; do not duplicate its state machine."""
99
- script = (
100
- "import { resolveCensusState } from './scripts/census/index.mjs';"
101
- "process.stdout.write(resolveCensusState(JSON.parse(process.argv[1])));"
102
- )
103
- result = subprocess.run(
104
- ["node", "--input-type=module", "-e", script, json.dumps(inputs)],
105
- cwd=REPO, check=True, capture_output=True, text=True,
106
- )
107
- return result.stdout
108
-
109
-
110
- def run_shipped_test(relative_path):
111
- """Run a real shipped integration test as proof, without marker files."""
112
- subprocess.run(
113
- ["node", "--test", relative_path], cwd=REPO, check=True,
114
- capture_output=True, text=True,
115
- )
116
-
117
-
118
- def apply_census_setup_effect(root, effect, writes):
119
- """Reconcile setup-owned files and delegate engine work to #49/#50."""
120
- paths = {
121
- "choice": root / "docs/agents/census.md",
122
- "profile": root / ".census/profile.json",
123
- "active": root / ".census/active.json",
124
- "scanner": root / ".census/local-scanner.mjs",
125
- "scanner-test": root / ".census/local-scanner.test.mjs",
126
- "hook": root / ".git/hooks/census-check",
127
- "gate": root / ".github/workflows/census-check.yml",
128
- }
129
- trace = []
130
- states = []
131
-
132
- def write_if_changed(path, content):
133
- data = content.encode("utf-8")
134
- if path.exists() and path.read_bytes() == data:
135
- return
136
- path.parent.mkdir(parents=True, exist_ok=True)
137
- path.write_bytes(data)
138
- writes.append(path.relative_to(root).as_posix())
139
-
140
- def replace_if_changed(path, content):
141
- data = content.encode("utf-8")
142
- if path.exists() and path.read_bytes() == data:
143
- return
144
- path.parent.mkdir(parents=True, exist_ok=True)
145
- staged = path.with_name(path.name + ".setup-workflow.tmp")
146
- staged.write_bytes(data)
147
- staged.replace(path)
148
- writes.append(path.relative_to(root).as_posix())
149
-
150
- def remove_if_present(path, operation):
151
- if path.exists():
152
- path.unlink()
153
- writes.append(path.relative_to(root).as_posix())
154
- trace.append(f"{operation}:removed")
155
- else:
156
- trace.append(f"{operation}:no-op")
157
-
158
- for operation in effect["operations"]:
159
- if operation == "reconcile-choice-doc":
160
- choice = effect["choice"]
161
- if not paths["choice"].exists():
162
- content = (
163
- "<!-- setup-workflow: state=filled -->\n"
164
- f"<!-- census: choice={choice} -->\n\n"
165
- + (SKILL / "census.md").read_text(encoding="utf-8")
166
- )
167
- write_if_changed(paths["choice"], content)
168
- trace.append("reconcile-choice-doc")
169
- elif operation == "adopt-choice-doc":
170
- if not paths["choice"].exists():
171
- raise AssertionError("existing census needs a documented census path")
172
- trace.append("adopt-choice-doc")
173
- elif operation == "reconcile-minimal-profile":
174
- if not paths["profile"].exists():
175
- write_if_changed(paths["profile"], json.dumps({
176
- "schemaVersion": 1, "enabled": True,
177
- "decisions": [], "localScanners": [], "overrides": [],
178
- }, sort_keys=True) + "\n")
179
- trace.append("reconcile-minimal-profile")
180
- elif operation == "remove-kit-hook":
181
- remove_if_present(paths["hook"], operation)
182
- elif operation == "remove-kit-gate":
183
- remove_if_present(paths["gate"], operation)
184
- elif operation == "update-profile-disabled":
185
- profile = json.loads(paths["profile"].read_text(encoding="utf-8"))
186
- profile["enabled"] = False
187
- replace_if_changed(paths["profile"], json.dumps(profile, sort_keys=True) + "\n")
188
- trace.append("update-profile-disabled")
189
- elif operation == "derive-state":
190
- enabled = False
191
- if paths["profile"].exists():
192
- enabled = json.loads(paths["profile"].read_text(encoding="utf-8"))["enabled"]
193
- states.append(resolve_census_state(
194
- enabled=enabled, hasActive=paths["active"].exists(),
195
- ))
196
- trace.append("derive-state")
197
- elif operation == "run-foundation-self-test":
198
- run_shipped_test("scripts/census/state.test.mjs")
199
- trace.append("run-foundation-self-test")
200
- elif operation == "delegate-census-update":
201
- skill = (REPO / ".claude/skills/census-update/SKILL.md").read_text(encoding="utf-8")
202
- for token in ("activateCensus", "resolveCensusState", "scripts/census/index.mjs"):
203
- if token not in skill:
204
- raise AssertionError(f"census-update contract missing {token}")
205
- trace.append("delegate-census-update")
206
- elif operation == "run-census-update-contract":
207
- run_shipped_test("scripts/test_census_update_contract.test.mjs")
208
- trace.append("run-census-update-contract")
209
- else:
210
- raise AssertionError(f"unknown census setup operation: {operation}")
211
- return paths, trace, states
212
-
213
-
214
- class IdempotencyRule(unittest.TestCase):
215
- CASES = [
216
- # (first_line, is_empty, expected)
217
- (None, False, "create"), # missing
218
- ("<!-- setup-workflow: state=stub -->", False, "fill"), # stub
219
- ("<!-- setup-workflow: state=filled -->", False, "skip"), # filled
220
- ("<!-- setup-workflow: state=stub; mode=github-projects-v2 -->", False, "fill"),
221
- ("<!-- setup-workflow: state=filled; mode=github-projects-v2 -->", False, "skip"),
222
- ("<!-- setup-workflow: state=not-applicable; mode=none -->", False, "skip"),
223
- ("# Board sync — GitHub Projects field-IDs", False, "skip"), # legacy non-empty
224
- ("", True, "create"), # empty no-sentinel
225
- ]
226
-
227
- def test_canonical_examples(self):
228
- for first, empty, expected in self.CASES:
229
- self.assertEqual(
230
- classify(first, empty), expected, f"{first!r} empty={empty}"
231
- )
232
-
233
- def test_filled_file_mentioning_marker_in_body_is_not_refilled(self):
234
- # Only the FIRST line decides; a body mention must not flip it (Codex R1 #12).
235
- first = "<!-- setup-workflow: state=filled -->"
236
- self.assertEqual(classify(first, False), "skip")
237
-
238
-
239
- class SeedTemplatesValid(unittest.TestCase):
240
- def test_prod_readiness_choice_and_transaction_contract_is_complete(self):
241
- for surface in (".claude", ".agents"):
242
- text = (REPO / surface / "skills/setup-workflow/SKILL.md").read_text(
243
- encoding="utf-8"
244
- )
245
- for token in (
246
- "Configure now", "Configure later",
247
- "decision set prodTarget pending",
248
- "wrapup.deployReport omitted (prodTarget pending)",
249
- "`prodTarget` does not permit this choice",
250
- "Preview the exact local `## Prod` replacement",
251
- "external mutation retains its own separate",
252
- "activeBlocks` must be exactly `[\"deployReport\"]`",
253
- ):
254
- self.assertIn(token, text, f"{surface} missing {token!r}")
255
- preserve = re.search(
256
- r"Preserve each file's\s+`## Workflow` and `## Agent skills` "
257
- r"blocks byte-for-byte",
258
- text,
259
- )
260
- self.assertIsNotNone(preserve, surface)
261
-
262
- def test_census_effect_contract_executes_every_transition_and_repeats_without_writes(self):
263
- effects = load_census_setup_effects()
264
- self.assertEqual(
265
- set(effects),
266
- {"missing", "yes", "later", "no", "existing", "explicit-enable", "disable"},
267
- )
268
- self.assertEqual(effects["explicit-enable"]["actor"], "census-update")
269
-
270
- for state, effect in effects.items():
271
- with self.subTest(state=state), tempfile.TemporaryDirectory() as tmp:
272
- root = Path(tmp)
273
- initial = {}
274
- if state in ("existing", "explicit-enable", "disable"):
275
- initial = {
276
- "choice": (
277
- b'<!-- setup-workflow: state=filled -->\n'
278
- b'<!-- census: choice=yes -->\n\nconsumer notes stay exact\n'
279
- ),
280
- "profile": (
281
- b'{"enabled":true,"consumerKey":"keep exactly",'
282
- b'"decisions":["keep"],"overrides":[{"keep":true}]}\n'
283
- ),
284
- "active": b'{"consumerSnapshot":"keep exactly"}\n',
285
- "scanner": b"export const consumerScanner = true;\n",
286
- "scanner-test": b"consumer scanner test\n",
287
- }
288
- if state == "explicit-enable":
289
- initial["profile"] = b'{"enabled":false,"consumerKey":"keep"}\n'
290
- initial.pop("active")
291
- for name, data in initial.items():
292
- path = {
293
- "choice": root / "docs/agents/census.md",
294
- "profile": root / ".census/profile.json",
295
- "active": root / ".census/active.json",
296
- "scanner": root / ".census/local-scanner.mjs",
297
- "scanner-test": root / ".census/local-scanner.test.mjs",
298
- }[name]
299
- path.parent.mkdir(parents=True, exist_ok=True)
300
- path.write_bytes(data)
301
- if state in ("existing", "disable"):
302
- for path in (
303
- root / ".git/hooks/census-check",
304
- root / ".github/workflows/census-check.yml",
305
- ):
306
- path.parent.mkdir(parents=True, exist_ok=True)
307
- path.write_text("kit-owned census enforcement\n", encoding="utf-8")
308
-
309
- writes = []
310
- paths, trace, states = apply_census_setup_effect(root, effect, writes)
311
-
312
- if state == "missing":
313
- self.assertEqual(writes, [])
314
- if state == "yes":
315
- choice = paths["choice"].read_text(encoding="utf-8")
316
- self.assertTrue(choice.startswith(
317
- "<!-- setup-workflow: state=filled -->\n"
318
- "<!-- census: choice=yes -->\n"
319
- ))
320
- profile = json.loads(paths["profile"].read_text(encoding="utf-8"))
321
- self.assertEqual(profile, {
322
- "schemaVersion": 1, "enabled": True,
323
- "decisions": [], "localScanners": [], "overrides": [],
324
- })
325
- self.assertEqual(states, ["bootstrap"])
326
- self.assertIn("run-foundation-self-test", trace)
327
- if state in ("yes", "later", "no"):
328
- self.assertFalse(paths["active"].exists())
329
- self.assertFalse(paths["hook"].exists())
330
- self.assertFalse(paths["gate"].exists())
331
- if state in ("later", "no"):
332
- self.assertFalse(paths["profile"].exists())
333
- self.assertIn(
334
- f"<!-- census: choice={state} -->",
335
- paths["choice"].read_text(encoding="utf-8"),
336
- )
337
- if state == "existing":
338
- self.assertEqual(writes, [])
339
- for name, expected in initial.items():
340
- self.assertEqual(paths[name].read_bytes(), expected)
341
- if state == "explicit-enable":
342
- self.assertEqual(writes, [])
343
- for name, expected in initial.items():
344
- self.assertEqual(paths[name].read_bytes(), expected)
345
- self.assertEqual(
346
- trace,
347
- ["delegate-census-update", "run-census-update-contract"],
348
- )
349
- if state == "disable":
350
- profile = json.loads(paths["profile"].read_text(encoding="utf-8"))
351
- self.assertFalse(profile["enabled"])
352
- self.assertEqual(profile["consumerKey"], "keep exactly")
353
- self.assertEqual(profile["decisions"], ["keep"])
354
- self.assertEqual(profile["overrides"], [{"keep": True}])
355
- for name in ("choice", "active", "scanner", "scanner-test"):
356
- self.assertEqual(paths[name].read_bytes(), initial[name])
357
- self.assertFalse(paths["hook"].exists())
358
- self.assertFalse(paths["gate"].exists())
359
- self.assertEqual(states, ["disabled"])
360
- self.assertLess(
361
- next(i for i, value in enumerate(trace) if value.startswith("remove-kit-hook:")),
362
- trace.index("update-profile-disabled"),
363
- )
364
- self.assertLess(
365
- next(i for i, value in enumerate(trace) if value.startswith("remove-kit-gate:")),
366
- trace.index("update-profile-disabled"),
367
- )
368
-
369
- before = {
370
- str(path.relative_to(root)): (path.read_bytes(), path.stat().st_mtime_ns)
371
- for path in root.rglob("*") if path.is_file()
372
- }
373
- time.sleep(0.01)
374
- repeat_writes = []
375
- apply_census_setup_effect(root, effect, repeat_writes)
376
- after = {
377
- str(path.relative_to(root)): (path.read_bytes(), path.stat().st_mtime_ns)
378
- for path in root.rglob("*") if path.is_file()
379
- }
380
- self.assertEqual(effect["repeat"], "no-write")
381
- self.assertEqual(repeat_writes, [])
382
- self.assertEqual(after, before)
383
-
384
- def test_census_seed_covers_the_complete_setup_state_matrix(self):
385
- seed = (SKILL / "census.md").read_text(encoding="utf-8")
386
- rows = {}
387
- for line in seed.splitlines():
388
- match = re.match(r"^\| `([^`]+)` \| (.+) \| (.+) \|$", line)
389
- if match:
390
- rows[match.group(1)] = f"{match.group(2)} {match.group(3)}"
391
- for state in (
392
- "missing", "yes", "later", "no", "existing",
393
- "explicit-enable", "disable",
394
- ):
395
- self.assertIn(state, rows)
396
-
397
- expected_terms = {
398
- "missing": ("ask `yes / later / no`", "do not infer", "no hook or gate"),
399
- "yes": ("enabled: true", "active snapshot absent", "self-test", "bootstrap"),
400
- "later": ("deferral", "setup rerun is a no-op", "`census-update`"),
401
- "no": ("opt-out", "`disabled`", "do not create census files, hooks, or gates"),
402
- "existing": ("Adopt", "without replacing", "Preserve every existing byte"),
403
- "explicit-enable": ("`census-update`", "without rerunning setup", "no write"),
404
- "disable": ("enabled: false", "remove census hooks/gates", "separately approves"),
405
- }
406
- for state, terms in expected_terms.items():
407
- for term in terms:
408
- self.assertIn(term, rows[state], f"{state} missing {term!r}")
409
-
410
- def test_census_yes_is_an_honest_bootstrap_not_activation(self):
411
- skill = (SKILL / "SKILL.md").read_text(encoding="utf-8")
412
- seed = (SKILL / "census.md").read_text(encoding="utf-8")
413
-
414
- self.assertIn("optional census choice", skill.split("---", 2)[1])
415
- self.assertIn("Section A3 — Optional project census", skill)
416
- self.assertIn("[census.md](./census.md)", skill)
417
- for token in (
418
- ".census/profile.json", ".census/active.json", "enabled: true",
419
- "bootstrap", "not yet meaningful", "self-test",
420
- "Setup itself never calls `activateCensus`",
421
- ):
422
- self.assertIn(token, seed)
423
- self.assertIn("must not install pre-commit, pre-push, CI, planning, or", seed)
424
-
425
- def test_census_deferral_adoption_enable_and_disable_are_safe_and_idempotent(self):
426
- skill = (SKILL / "SKILL.md").read_text(encoding="utf-8")
427
- seed = (SKILL / "census.md").read_text(encoding="utf-8")
428
-
429
- for token in (
430
- "retryable deferral", "explicit opt-out", "explicit `census-update`",
431
- "consumer-owned", "separate deletion approval", "no write",
432
- ):
433
- self.assertIn(token, seed)
434
- self.assertIn("Repeated runs are no-ops", skill)
435
- self.assertIn("setup never deletes consumer-owned files", seed.lower())
436
-
437
- def test_census_setup_surface_is_fully_mirrored_for_codex(self):
438
- codex = REPO / ".agents/skills/setup-workflow"
439
- for relative in ("SKILL.md", "census.md"):
440
- self.assertEqual(
441
- (SKILL / relative).read_text(encoding="utf-8"),
442
- (codex / relative).read_text(encoding="utf-8"),
443
- f"setup-workflow mirror drift in {relative}",
444
- )
445
-
446
- def test_update_workflow_provider_and_choice_fixtures(self):
447
- fixtures = [
448
- ("github", "enable", False, True, "create"),
449
- ("github", "opt-out", False, True, "skip"),
450
- ("github", "later", False, True, "skip"),
451
- ("github", "enable", True, True, "skip"),
452
- ("github", "enable", False, False, "skip"),
453
- ("gitlab", "enable", False, True, "skip"),
454
- ("local", "enable", False, True, "skip"),
455
- ]
456
- for provider, choice, exists, allowed, expected in fixtures:
457
- self.assertEqual(
458
- update_workflow_action(
459
- provider, choice, exists, allowed,
460
- ), expected,
461
- )
462
-
463
- def test_github_update_opt_in_is_provider_aware_and_idempotent(self):
464
- skill = (SKILL / "SKILL.md").read_text(encoding="utf-8")
465
- for token in (
466
- "Automatic Kit update pull requests",
467
- ".github/workflows/agent-workflow-kit-update.yml",
468
- "Opt out",
469
- "Ask later",
470
- "GitHub tracker",
471
- "skipped (already present)",
472
- "built-in Kit invariant validator",
473
- "no Consumer package script",
474
- "can_approve_pull_request_reviews",
475
- "Allow GitHub Actions to create and approve pull requests",
476
- "explicit confirmation",
477
- ):
478
- self.assertIn(token, skill)
479
- self.assertIn("do not create a GitHub workflow", skill)
480
-
481
- def test_github_update_workflow_has_safe_triggers_and_scoped_runner(self):
482
- workflow = (SKILL / "assets/agent-workflow-kit-update.yml").read_text(encoding="utf-8")
483
- for token in (
484
- "schedule:", "workflow_dispatch:", "contents: write",
485
- "pull-requests: write", "agent-workflow-kit-update-pr",
486
- "@ikon85/agent-workflow-kit@latest", "fetch-depth: 0",
487
- "node-version: 22.14", "Verify and upsert the Kit update pull request",
488
- ):
489
- self.assertIn(token, workflow)
490
- for forbidden in (
491
- "npm_token", "NPM_TOKEN", "auto-merge", "gh pr merge",
492
- "npm ci", "npm test", "package-lock.json",
493
- ):
494
- self.assertNotIn(forbidden, workflow)
495
-
496
- mirror = (REPO / ".agents/skills/setup-workflow/assets/agent-workflow-kit-update.yml")
497
- self.assertEqual(workflow, mirror.read_text(encoding="utf-8"))
498
-
499
- def test_update_pr_runner_is_an_installed_package_binary(self):
500
- package = json.loads((REPO / "package.json").read_text(encoding="utf-8"))
501
- self.assertEqual(package["bin"]["agent-workflow-kit-update-pr"], "scripts/kit-update-pr.mjs")
502
-
503
- def test_spec_completeness_seed_has_valid_self_critique_block(self):
504
- """A convention without a valid Trigger/Check/Korrektur block makes
505
- spec-self-critique point 8 warn — the seed must carry one (Codex R1 #14)."""
506
- t = (SKILL / "spec-completeness-seed.md").read_text(encoding="utf-8")
507
- self.assertIn("## Self-Critique-Check", t)
508
- for kw in ("Trigger", "Check", "Korrektur"):
509
- self.assertRegex(t, rf"\*\*{kw}", f"missing {kw} in Self-Critique-Check")
510
-
511
- def test_spec_self_critique_seed_has_12_point_anchors(self):
512
- """Stable retro-append anchors for points 1..12 (Codex R1 #15)."""
513
- t = (SKILL / "spec-self-critique-seed.md").read_text(encoding="utf-8")
514
- anchors = sorted(int(n) for n in re.findall(r"^## Point (\d+)\b", t, re.M))
515
- self.assertEqual(anchors, list(range(1, 13)))
516
-
517
- def test_orchestrate_wave_seed_has_section_contract(self):
518
- """The orchestrate-wave project-layer seed carries the exact §-section
519
- headings the skill's Phase-0 probe matches (#1958). If a heading is
520
- renamed here without updating the skeleton (or vice versa) the probe
521
- stops finding the section — this pins the contract on both sides."""
522
- seed = (SKILL / "orchestrate-wave-seed.md").read_text(encoding="utf-8")
523
- skeleton = (REPO / ".claude/skills/orchestrate-wave/SKILL.md").read_text(encoding="utf-8")
524
- sections = [
525
- "§Setup", "§Builder Commands", "§Builder Hard Rules",
526
- "§Integration Suites", "§Verify Recipe", "§Headless Login", "§Landing",
527
- ]
528
- for sec in sections:
529
- self.assertRegex(seed, rf"(?m)^#+ {re.escape(sec)}\b", f"seed missing heading {sec!r}")
530
- self.assertIn(sec, skeleton, f"skeleton no longer refers to {sec!r}")
531
-
532
- def test_board_sync_seed_documents_states_and_profile(self):
533
- t = (SKILL / "board-sync.md").read_text(encoding="utf-8")
534
- for token in ("state=filled", "state=stub", "state=not-applicable", "Status"):
535
- self.assertIn(token, t)
536
-
537
- def test_workflow_overview_seed_is_generic_and_linked_from_skill(self):
538
- """Fresh repos should get an entry-point map without Testreporter-local lore.
539
-
540
- The expected skill set is derived from the manifest's `entryPoint` flag
541
- (publish:true skills only) rather than a hand-maintained token list —
542
- it used to hardcode 5 names while the manifest already carried 13
543
- publish:true entry-point-worthy skills, so the seed silently drifted
544
- behind (#1890: `ask-matt`, the router, was missing entirely)."""
545
- manifest = json.loads((REPO / ".claude/skills/skill-manifest.json").read_text(encoding="utf-8"))
546
- expected = sorted(
547
- name for name, meta in manifest["skills"].items()
548
- if meta.get("publish") and meta.get("entryPoint")
549
- )
550
- self.assertGreaterEqual(len(expected), 10, "manifest entryPoint set looks too small")
551
-
552
- seed = (SKILL / "workflow-overview.md").read_text(encoding="utf-8")
553
- skill = (SKILL / "SKILL.md").read_text(encoding="utf-8")
554
-
555
- self.assertIn("# Workflow", seed)
556
- for name in expected:
557
- self.assertIn(name, seed, f"entry-point skill {name!r} (manifest entryPoint:true) missing from seed")
558
- self.assertIn("ask-matt", seed, "router entry must be named explicitly")
559
- self.assertNotIn("Testreporter", seed)
560
- self.assertIn("workflow-overview.md", skill)
561
- self.assertIn("## Workflow", skill)
562
-
563
- def test_triage_labels_seed_matches_two_label_reality(self):
564
- """The seed presented at setup-workflow fill-time must not contradict
565
- the triage skill's actual label vocabulary: it used to list five
566
- canonical roles as if all were active labels, but the triage skill
567
- only ever applies two — the rest live in board status. A fresh fill
568
- from the old seed taught agents to assign three dead labels (#1879)."""
569
- seed = (SKILL / "triage-labels.md").read_text(encoding="utf-8")
570
- triage_skill = (REPO / ".claude/skills/triage/SKILL.md").read_text(encoding="utf-8")
571
-
572
- for label in ("needs-info", "ready-for-agent"):
573
- self.assertIn(f"`{label}`", seed)
574
- self.assertIn(f"`{label}`", triage_skill)
575
-
576
- # The retired roles must not appear in the table portion (before the
577
- # board-status explainer) as if they were still active labels.
578
- table, sep, explainer = seed.partition("Board status is authoritative")
579
- self.assertTrue(sep, "seed must explain that board status is authoritative")
580
- for retired in ("needs-triage", "ready-for-human", "wontfix"):
581
- self.assertNotIn(f"`{retired}`", table)
582
-
583
- self.assertIn("Board status is authoritative", seed)
584
- self.assertIn("Board status is authoritative", triage_skill)
585
-
586
-
587
- class BoardCreationOffer(unittest.TestCase):
588
- """Section D offers board creation instead of refusing it (#24).
589
-
590
- The mechanics live in `scripts/board_bootstrap.py` (spec:
591
- `scripts/test_board_bootstrap.py`); what is pinned here is the prose
592
- contract: an explicit user gate, one creation path, and every other branch
593
- landing on the unchanged stub.
594
- """
595
-
596
- FIXTURES = [
597
- # tracker, boards, scope, choice, expected
598
- ("github", 0, True, "yes", "create"),
599
- ("github", 0, True, "no", "stub"),
600
- ("github", 0, True, None, "stub"),
601
- ("github", 0, False, "yes", "stub"),
602
- ("github", 1, True, None, "discover"),
603
- ("github", 2, True, "yes", "stub"),
604
- ("github", None, True, "yes", "stub"),
605
- ("gitlab", 0, True, "yes", "not-applicable"),
606
- ("local", 0, True, "yes", "not-applicable"),
607
- ]
608
-
609
- def test_decision_table(self):
610
- for tracker, boards, scope, choice, expected in self.FIXTURES:
611
- self.assertEqual(
612
- board_branch_action(tracker, boards, scope, choice), expected,
613
- f"{tracker}/{boards}/{scope}/{choice}",
614
- )
615
-
616
- def test_skill_offers_creation_on_both_surfaces(self):
617
- for surface in (".claude", ".agents"):
618
- skill = (REPO / surface / "skills/setup-workflow/SKILL.md").read_text(
619
- encoding="utf-8")
620
- for token in (
621
- "python3 scripts/board_bootstrap.py create",
622
- "--dry-run",
623
- "never create a board without an explicit yes",
624
- "Create it now",
625
- "Not now",
626
- "gh auth refresh -s project,read:project",
627
- "state=filled",
628
- ):
629
- self.assertIn(token, skill, f"{surface} missing {token!r}")
630
- # The refuted rationale must be gone: the CLI *can* provision the
631
- # Status options via `--single-select-options`.
632
- self.assertNotIn("cannot provision the Status options", skill)
633
-
634
- def test_skill_routes_every_other_branch_to_the_unchanged_stub(self):
635
- skill = (SKILL / "SKILL.md").read_text(encoding="utf-8")
636
- fallback = skill.split("**Fallback", 1)
637
- self.assertEqual(len(fallback), 2, "the stub fallback must survive")
638
- for token in ("declined", "scope", "read failure", "state=stub", "Retryable"):
639
- self.assertIn(token, fallback[1][:1200], f"stub path missing {token!r}")
640
- self.assertIn("never hand-write a `state=filled` profile", skill)
641
-
642
- def test_seed_documents_the_offer_and_keeps_the_manual_path(self):
643
- seed = (SKILL / "board-sync.md").read_text(encoding="utf-8")
644
- self.assertIn("board_bootstrap.py", seed)
645
- self.assertIn("gh auth refresh -s project,read:project", seed)
646
- self.assertIn("Re-run `/setup-workflow`", seed)
647
-
648
-
649
- class SentinelProtectsFilledProfile(unittest.TestCase):
650
- """A consumer's filled board profile must never be rewritten on rerun."""
651
-
652
- def test_filled_public_profile_is_left_unchanged_on_rerun(self):
653
- real_profile = REPO / "docs/agents/board-sync.md"
654
- text = real_profile.read_text(encoding="utf-8")
655
- first_line = text.splitlines()[0]
656
- self.assertEqual(
657
- classify(first_line, False), "skip",
658
- "a filled project profile must classify as skip, never re-fillable",
659
- )
660
- # Prove this is genuinely filled (not an accidental stub), otherwise
661
- # the "skip" assertion above would be vacuous.
662
- self.assertIn('"repo": "iKon85/agent-workflow-kit"', text)
663
- self.assertIn('"vorBau": "Clarify Before Build"', text)
664
-
665
- def test_unfilled_stub_gets_english_defaults(self):
666
- stub_first_line = "<!-- setup-workflow: state=stub; mode=github-projects-v2 -->"
667
- self.assertEqual(classify(stub_first_line, False), "fill")
668
-
669
- seed = (SKILL / "board-sync.md").read_text(encoding="utf-8")
670
- self.assertIn('"retroValues": ["ran", "skipped"]', seed)
671
- self.assertIn('"vorBau": "Clarify Before Build"', seed)
672
- self.assertNotIn("gefahren", seed)
673
- self.assertNotIn("Vor Bau zu klären", seed)
674
-
675
-
676
- class FreshFillProducesValidSentinels(unittest.TestCase):
677
- """Simulate the write step: prepend the documented sentinel to a template
678
- body and assert the result is a well-formed project-layer file."""
679
-
680
- def test_prepended_sentinel_parses(self):
681
- body = (SKILL / "spec-self-critique-seed.md").read_text(encoding="utf-8")
682
- for state, mode in (("stub", None), ("filled", "github-projects-v2"),
683
- ("not-applicable", "none")):
684
- sentinel = f"<!-- setup-workflow: state={state}" + (
685
- f"; mode={mode}" if mode else "") + " -->"
686
- written = sentinel + "\n" + body
687
- self.assertRegex(written.splitlines()[0], SENTINEL_RE)
688
-
689
-
690
- if __name__ == "__main__":
691
- unittest.main(verbosity=2)