@ikon85/agent-workflow-kit 0.15.0 → 0.16.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.
@@ -17,6 +17,9 @@ Run: python3 scripts/test_skill_setup_workflow_seeds.py
17
17
  """
18
18
  import json
19
19
  import re
20
+ import subprocess
21
+ import tempfile
22
+ import time
20
23
  import unittest
21
24
  from pathlib import Path
22
25
 
@@ -55,6 +58,135 @@ def update_workflow_action(
55
58
  return "create" if prerequisites and pull_requests_allowed else "skip"
56
59
 
57
60
 
61
+ def load_census_setup_effects():
62
+ """Parse the shipped seed's executable census transition contract."""
63
+ seed = (SKILL / "census.md").read_text(encoding="utf-8")
64
+ match = re.search(
65
+ r"```json census-setup-effects\n(.*?)\n```", seed, re.DOTALL,
66
+ )
67
+ if not match:
68
+ raise AssertionError("census seed has no structured setup-effects contract")
69
+ rows = json.loads(match.group(1))
70
+ return {row["state"]: row for row in rows}
71
+
72
+
73
+ def resolve_census_state(**inputs):
74
+ """Ask the shipped #49 API for a state; do not duplicate its state machine."""
75
+ script = (
76
+ "import { resolveCensusState } from './scripts/census/index.mjs';"
77
+ "process.stdout.write(resolveCensusState(JSON.parse(process.argv[1])));"
78
+ )
79
+ result = subprocess.run(
80
+ ["node", "--input-type=module", "-e", script, json.dumps(inputs)],
81
+ cwd=REPO, check=True, capture_output=True, text=True,
82
+ )
83
+ return result.stdout
84
+
85
+
86
+ def run_shipped_test(relative_path):
87
+ """Run a real shipped integration test as proof, without marker files."""
88
+ subprocess.run(
89
+ ["node", "--test", relative_path], cwd=REPO, check=True,
90
+ capture_output=True, text=True,
91
+ )
92
+
93
+
94
+ def apply_census_setup_effect(root, effect, writes):
95
+ """Reconcile setup-owned files and delegate engine work to #49/#50."""
96
+ paths = {
97
+ "choice": root / "docs/agents/census.md",
98
+ "profile": root / ".census/profile.json",
99
+ "active": root / ".census/active.json",
100
+ "scanner": root / ".census/local-scanner.mjs",
101
+ "scanner-test": root / ".census/local-scanner.test.mjs",
102
+ "hook": root / ".git/hooks/census-check",
103
+ "gate": root / ".github/workflows/census-check.yml",
104
+ }
105
+ trace = []
106
+ states = []
107
+
108
+ def write_if_changed(path, content):
109
+ data = content.encode("utf-8")
110
+ if path.exists() and path.read_bytes() == data:
111
+ return
112
+ path.parent.mkdir(parents=True, exist_ok=True)
113
+ path.write_bytes(data)
114
+ writes.append(path.relative_to(root).as_posix())
115
+
116
+ def replace_if_changed(path, content):
117
+ data = content.encode("utf-8")
118
+ if path.exists() and path.read_bytes() == data:
119
+ return
120
+ path.parent.mkdir(parents=True, exist_ok=True)
121
+ staged = path.with_name(path.name + ".setup-workflow.tmp")
122
+ staged.write_bytes(data)
123
+ staged.replace(path)
124
+ writes.append(path.relative_to(root).as_posix())
125
+
126
+ def remove_if_present(path, operation):
127
+ if path.exists():
128
+ path.unlink()
129
+ writes.append(path.relative_to(root).as_posix())
130
+ trace.append(f"{operation}:removed")
131
+ else:
132
+ trace.append(f"{operation}:no-op")
133
+
134
+ for operation in effect["operations"]:
135
+ if operation == "reconcile-choice-doc":
136
+ choice = effect["choice"]
137
+ if not paths["choice"].exists():
138
+ content = (
139
+ "<!-- setup-workflow: state=filled -->\n"
140
+ f"<!-- census: choice={choice} -->\n\n"
141
+ + (SKILL / "census.md").read_text(encoding="utf-8")
142
+ )
143
+ write_if_changed(paths["choice"], content)
144
+ trace.append("reconcile-choice-doc")
145
+ elif operation == "adopt-choice-doc":
146
+ if not paths["choice"].exists():
147
+ raise AssertionError("existing census needs a documented census path")
148
+ trace.append("adopt-choice-doc")
149
+ elif operation == "reconcile-minimal-profile":
150
+ if not paths["profile"].exists():
151
+ write_if_changed(paths["profile"], json.dumps({
152
+ "schemaVersion": 1, "enabled": True,
153
+ "decisions": [], "localScanners": [], "overrides": [],
154
+ }, sort_keys=True) + "\n")
155
+ trace.append("reconcile-minimal-profile")
156
+ elif operation == "remove-kit-hook":
157
+ remove_if_present(paths["hook"], operation)
158
+ elif operation == "remove-kit-gate":
159
+ remove_if_present(paths["gate"], operation)
160
+ elif operation == "update-profile-disabled":
161
+ profile = json.loads(paths["profile"].read_text(encoding="utf-8"))
162
+ profile["enabled"] = False
163
+ replace_if_changed(paths["profile"], json.dumps(profile, sort_keys=True) + "\n")
164
+ trace.append("update-profile-disabled")
165
+ elif operation == "derive-state":
166
+ enabled = False
167
+ if paths["profile"].exists():
168
+ enabled = json.loads(paths["profile"].read_text(encoding="utf-8"))["enabled"]
169
+ states.append(resolve_census_state(
170
+ enabled=enabled, hasActive=paths["active"].exists(),
171
+ ))
172
+ trace.append("derive-state")
173
+ elif operation == "run-foundation-self-test":
174
+ run_shipped_test("scripts/census/state.test.mjs")
175
+ trace.append("run-foundation-self-test")
176
+ elif operation == "delegate-census-update":
177
+ skill = (REPO / ".claude/skills/census-update/SKILL.md").read_text(encoding="utf-8")
178
+ for token in ("activateCensus", "resolveCensusState", "scripts/census/index.mjs"):
179
+ if token not in skill:
180
+ raise AssertionError(f"census-update contract missing {token}")
181
+ trace.append("delegate-census-update")
182
+ elif operation == "run-census-update-contract":
183
+ run_shipped_test("scripts/test_census_update_contract.test.mjs")
184
+ trace.append("run-census-update-contract")
185
+ else:
186
+ raise AssertionError(f"unknown census setup operation: {operation}")
187
+ return paths, trace, states
188
+
189
+
58
190
  class IdempotencyRule(unittest.TestCase):
59
191
  CASES = [
60
192
  # (first_line, is_empty, expected)
@@ -81,6 +213,190 @@ class IdempotencyRule(unittest.TestCase):
81
213
 
82
214
 
83
215
  class SeedTemplatesValid(unittest.TestCase):
216
+ def test_census_effect_contract_executes_every_transition_and_repeats_without_writes(self):
217
+ effects = load_census_setup_effects()
218
+ self.assertEqual(
219
+ set(effects),
220
+ {"missing", "yes", "later", "no", "existing", "explicit-enable", "disable"},
221
+ )
222
+ self.assertEqual(effects["explicit-enable"]["actor"], "census-update")
223
+
224
+ for state, effect in effects.items():
225
+ with self.subTest(state=state), tempfile.TemporaryDirectory() as tmp:
226
+ root = Path(tmp)
227
+ initial = {}
228
+ if state in ("existing", "explicit-enable", "disable"):
229
+ initial = {
230
+ "choice": (
231
+ b'<!-- setup-workflow: state=filled -->\n'
232
+ b'<!-- census: choice=yes -->\n\nconsumer notes stay exact\n'
233
+ ),
234
+ "profile": (
235
+ b'{"enabled":true,"consumerKey":"keep exactly",'
236
+ b'"decisions":["keep"],"overrides":[{"keep":true}]}\n'
237
+ ),
238
+ "active": b'{"consumerSnapshot":"keep exactly"}\n',
239
+ "scanner": b"export const consumerScanner = true;\n",
240
+ "scanner-test": b"consumer scanner test\n",
241
+ }
242
+ if state == "explicit-enable":
243
+ initial["profile"] = b'{"enabled":false,"consumerKey":"keep"}\n'
244
+ initial.pop("active")
245
+ for name, data in initial.items():
246
+ path = {
247
+ "choice": root / "docs/agents/census.md",
248
+ "profile": root / ".census/profile.json",
249
+ "active": root / ".census/active.json",
250
+ "scanner": root / ".census/local-scanner.mjs",
251
+ "scanner-test": root / ".census/local-scanner.test.mjs",
252
+ }[name]
253
+ path.parent.mkdir(parents=True, exist_ok=True)
254
+ path.write_bytes(data)
255
+ if state in ("existing", "disable"):
256
+ for path in (
257
+ root / ".git/hooks/census-check",
258
+ root / ".github/workflows/census-check.yml",
259
+ ):
260
+ path.parent.mkdir(parents=True, exist_ok=True)
261
+ path.write_text("kit-owned census enforcement\n", encoding="utf-8")
262
+
263
+ writes = []
264
+ paths, trace, states = apply_census_setup_effect(root, effect, writes)
265
+
266
+ if state == "missing":
267
+ self.assertEqual(writes, [])
268
+ if state == "yes":
269
+ choice = paths["choice"].read_text(encoding="utf-8")
270
+ self.assertTrue(choice.startswith(
271
+ "<!-- setup-workflow: state=filled -->\n"
272
+ "<!-- census: choice=yes -->\n"
273
+ ))
274
+ profile = json.loads(paths["profile"].read_text(encoding="utf-8"))
275
+ self.assertEqual(profile, {
276
+ "schemaVersion": 1, "enabled": True,
277
+ "decisions": [], "localScanners": [], "overrides": [],
278
+ })
279
+ self.assertEqual(states, ["bootstrap"])
280
+ self.assertIn("run-foundation-self-test", trace)
281
+ if state in ("yes", "later", "no"):
282
+ self.assertFalse(paths["active"].exists())
283
+ self.assertFalse(paths["hook"].exists())
284
+ self.assertFalse(paths["gate"].exists())
285
+ if state in ("later", "no"):
286
+ self.assertFalse(paths["profile"].exists())
287
+ self.assertIn(
288
+ f"<!-- census: choice={state} -->",
289
+ paths["choice"].read_text(encoding="utf-8"),
290
+ )
291
+ if state == "existing":
292
+ self.assertEqual(writes, [])
293
+ for name, expected in initial.items():
294
+ self.assertEqual(paths[name].read_bytes(), expected)
295
+ if state == "explicit-enable":
296
+ self.assertEqual(writes, [])
297
+ for name, expected in initial.items():
298
+ self.assertEqual(paths[name].read_bytes(), expected)
299
+ self.assertEqual(
300
+ trace,
301
+ ["delegate-census-update", "run-census-update-contract"],
302
+ )
303
+ if state == "disable":
304
+ profile = json.loads(paths["profile"].read_text(encoding="utf-8"))
305
+ self.assertFalse(profile["enabled"])
306
+ self.assertEqual(profile["consumerKey"], "keep exactly")
307
+ self.assertEqual(profile["decisions"], ["keep"])
308
+ self.assertEqual(profile["overrides"], [{"keep": True}])
309
+ for name in ("choice", "active", "scanner", "scanner-test"):
310
+ self.assertEqual(paths[name].read_bytes(), initial[name])
311
+ self.assertFalse(paths["hook"].exists())
312
+ self.assertFalse(paths["gate"].exists())
313
+ self.assertEqual(states, ["disabled"])
314
+ self.assertLess(
315
+ next(i for i, value in enumerate(trace) if value.startswith("remove-kit-hook:")),
316
+ trace.index("update-profile-disabled"),
317
+ )
318
+ self.assertLess(
319
+ next(i for i, value in enumerate(trace) if value.startswith("remove-kit-gate:")),
320
+ trace.index("update-profile-disabled"),
321
+ )
322
+
323
+ before = {
324
+ str(path.relative_to(root)): (path.read_bytes(), path.stat().st_mtime_ns)
325
+ for path in root.rglob("*") if path.is_file()
326
+ }
327
+ time.sleep(0.01)
328
+ repeat_writes = []
329
+ apply_census_setup_effect(root, effect, repeat_writes)
330
+ after = {
331
+ str(path.relative_to(root)): (path.read_bytes(), path.stat().st_mtime_ns)
332
+ for path in root.rglob("*") if path.is_file()
333
+ }
334
+ self.assertEqual(effect["repeat"], "no-write")
335
+ self.assertEqual(repeat_writes, [])
336
+ self.assertEqual(after, before)
337
+
338
+ def test_census_seed_covers_the_complete_setup_state_matrix(self):
339
+ seed = (SKILL / "census.md").read_text(encoding="utf-8")
340
+ rows = {}
341
+ for line in seed.splitlines():
342
+ match = re.match(r"^\| `([^`]+)` \| (.+) \| (.+) \|$", line)
343
+ if match:
344
+ rows[match.group(1)] = f"{match.group(2)} {match.group(3)}"
345
+ for state in (
346
+ "missing", "yes", "later", "no", "existing",
347
+ "explicit-enable", "disable",
348
+ ):
349
+ self.assertIn(state, rows)
350
+
351
+ expected_terms = {
352
+ "missing": ("ask `yes / later / no`", "do not infer", "no hook or gate"),
353
+ "yes": ("enabled: true", "active snapshot absent", "self-test", "bootstrap"),
354
+ "later": ("deferral", "setup rerun is a no-op", "`census-update`"),
355
+ "no": ("opt-out", "`disabled`", "do not create census files, hooks, or gates"),
356
+ "existing": ("Adopt", "without replacing", "Preserve every existing byte"),
357
+ "explicit-enable": ("`census-update`", "without rerunning setup", "no write"),
358
+ "disable": ("enabled: false", "remove census hooks/gates", "separately approves"),
359
+ }
360
+ for state, terms in expected_terms.items():
361
+ for term in terms:
362
+ self.assertIn(term, rows[state], f"{state} missing {term!r}")
363
+
364
+ def test_census_yes_is_an_honest_bootstrap_not_activation(self):
365
+ skill = (SKILL / "SKILL.md").read_text(encoding="utf-8")
366
+ seed = (SKILL / "census.md").read_text(encoding="utf-8")
367
+
368
+ self.assertIn("optional census choice", skill.split("---", 2)[1])
369
+ self.assertIn("Section A3 — Optional project census", skill)
370
+ self.assertIn("[census.md](./census.md)", skill)
371
+ for token in (
372
+ ".census/profile.json", ".census/active.json", "enabled: true",
373
+ "bootstrap", "not yet meaningful", "self-test",
374
+ "Setup itself never calls `activateCensus`",
375
+ ):
376
+ self.assertIn(token, seed)
377
+ self.assertIn("must not install pre-commit, pre-push, CI, planning, or", seed)
378
+
379
+ def test_census_deferral_adoption_enable_and_disable_are_safe_and_idempotent(self):
380
+ skill = (SKILL / "SKILL.md").read_text(encoding="utf-8")
381
+ seed = (SKILL / "census.md").read_text(encoding="utf-8")
382
+
383
+ for token in (
384
+ "retryable deferral", "explicit opt-out", "explicit `census-update`",
385
+ "consumer-owned", "separate deletion approval", "no write",
386
+ ):
387
+ self.assertIn(token, seed)
388
+ self.assertIn("Repeated runs are no-ops", skill)
389
+ self.assertIn("setup never deletes consumer-owned files", seed.lower())
390
+
391
+ def test_census_setup_surface_is_fully_mirrored_for_codex(self):
392
+ codex = REPO / ".agents/skills/setup-workflow"
393
+ for relative in ("SKILL.md", "census.md"):
394
+ self.assertEqual(
395
+ (SKILL / relative).read_text(encoding="utf-8"),
396
+ (codex / relative).read_text(encoding="utf-8"),
397
+ f"setup-workflow mirror drift in {relative}",
398
+ )
399
+
84
400
  def test_update_workflow_provider_and_choice_fixtures(self):
85
401
  fixtures = [
86
402
  ("github", "enable", False, True, True, "create"),