@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,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)