@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,712 +0,0 @@
1
- import importlib.util
2
- import json
3
- import os
4
- import subprocess
5
- import sys
6
- import tempfile
7
- import time
8
- import unittest
9
- from pathlib import Path
10
- from unittest.mock import patch
11
-
12
-
13
- ROOT = Path(__file__).resolve().parents[1]
14
- HOOKS = ROOT / ".claude" / "hooks"
15
- sys.path.insert(0, str(HOOKS))
16
- SPEC = importlib.util.spec_from_file_location("drift_guard", HOOKS / "drift-guard.py")
17
- DRIFT_GUARD = importlib.util.module_from_spec(SPEC)
18
- SPEC.loader.exec_module(DRIFT_GUARD)
19
-
20
-
21
- class CensusBackstopTest(unittest.TestCase):
22
- def make_repo(self):
23
- temporary = tempfile.TemporaryDirectory(prefix="awk-census-backstop-")
24
- root = Path(temporary.name)
25
- (root / "src").mkdir()
26
- (root / "package.json").write_text('{"name":"consumer"}\n', encoding="utf-8")
27
- (root / "src" / "index.mjs").write_text("export const ready = true;\n", encoding="utf-8")
28
- subprocess.run(["git", "init", "--quiet"], cwd=root, check=True)
29
- subprocess.run(["git", "add", "."], cwd=root, check=True)
30
- return temporary, root
31
-
32
- def enable(self, root, overrides=None, local_scanners=None):
33
- census = root / ".census"
34
- census.mkdir(exist_ok=True)
35
- (census / "profile.json").write_text(json.dumps({
36
- "schemaVersion": 1,
37
- "enabled": True,
38
- "decisions": [],
39
- "localScanners": local_scanners or [],
40
- "overrides": overrides or [],
41
- }) + "\n", encoding="utf-8")
42
-
43
- def test_execute_ready_missing_profile_failure_is_captured_and_exact(self):
44
- missing_profile = ROOT / "missing-board-profile.md"
45
- environment = os.environ.copy()
46
- environment["BOARD_SYNC_PROFILE"] = str(missing_profile)
47
-
48
- completed = subprocess.run(
49
- [
50
- sys.executable,
51
- str(ROOT / "scripts" / "execute-ready-check.py"),
52
- "--issue",
53
- "52",
54
- "--mode",
55
- "handoff",
56
- ],
57
- cwd=ROOT,
58
- env=environment,
59
- capture_output=True,
60
- text=True,
61
- )
62
-
63
- expected_stderr = (
64
- "[FAIL] execute-ready-check: Board-Profil nicht verfügbar — "
65
- f"board profile not found at {missing_profile} — run /setup-workflow "
66
- "to seed docs/agents/board-sync.md.\n"
67
- )
68
- diagnostics = (
69
- f"stdout:\n{completed.stdout}\n"
70
- f"stderr:\n{completed.stderr}"
71
- )
72
- self.assertEqual(completed.returncode, 1, diagnostics)
73
- self.assertEqual(completed.stdout, "", diagnostics)
74
- self.assertEqual(completed.stderr, expected_stderr, diagnostics)
75
-
76
- def activate_current(self, root):
77
- fresh = DRIFT_GUARD.scan_census_status(root)["fresh"]
78
- (root / ".census" / "active.json").write_text(
79
- json.dumps(fresh) + "\n", encoding="utf-8"
80
- )
81
-
82
- def test_census_bridge_uses_production_scale_budgets_and_clamps_proofs(self):
83
- temporary, root = self.make_repo()
84
- self.addCleanup(temporary.cleanup)
85
- self.enable(root)
86
- calls = []
87
-
88
- def complete_scan(*args, **kwargs):
89
- calls.append(kwargs)
90
- return subprocess.CompletedProcess(
91
- args=args[0], returncode=0,
92
- stdout=json.dumps({"state": "bootstrap"}), stderr="",
93
- )
94
-
95
- with patch.object(DRIFT_GUARD.subprocess, "run", side_effect=complete_scan):
96
- DRIFT_GUARD.scan_census_status(root)
97
- DRIFT_GUARD.scan_census_status(root, proof_timeout_ms=10**9)
98
-
99
- default_input = json.loads(calls[0]["input"])
100
- clamped_input = json.loads(calls[1]["input"])
101
- self.assertEqual(calls[0]["timeout"], 30)
102
- self.assertEqual(default_input["proofTimeoutMs"], 12_000)
103
- self.assertGreater(clamped_input["proofTimeoutMs"], 0)
104
- self.assertLess(clamped_input["proofTimeoutMs"], calls[1]["timeout"] * 1_000)
105
-
106
- for invalid in (False, 1.5, "12000"):
107
- with self.subTest(invalid=invalid):
108
- with self.assertRaisesRegex(ValueError, "proof timeout must be an integer"):
109
- DRIFT_GUARD.scan_census_status(root, proof_timeout_ms=invalid)
110
-
111
- def test_missing_disabled_bootstrap_and_offline_are_visible_but_fail_open(self):
112
- temporary, root = self.make_repo()
113
- self.addCleanup(temporary.cleanup)
114
-
115
- missing = DRIFT_GUARD.evaluate_census(root)
116
- self.assertEqual((missing["state"], missing["block_handoff"]), ("no_census", False))
117
-
118
- self.enable(root)
119
- profile = root / ".census" / "profile.json"
120
- body = json.loads(profile.read_text(encoding="utf-8"))
121
- body["enabled"] = False
122
- profile.write_text(json.dumps(body) + "\n", encoding="utf-8")
123
- disabled = DRIFT_GUARD.evaluate_census(root)
124
- self.assertEqual((disabled["state"], disabled["block_handoff"]), ("disabled", False))
125
-
126
- body["enabled"] = True
127
- profile.write_text(json.dumps(body) + "\n", encoding="utf-8")
128
- bootstrap = DRIFT_GUARD.evaluate_census(root)
129
- self.assertEqual((bootstrap["state"], bootstrap["block_handoff"]), ("bootstrap", False))
130
-
131
- with patch.object(DRIFT_GUARD, "scan_census_status", side_effect=OSError("node offline")):
132
- offline = DRIFT_GUARD.evaluate_census(root)
133
- self.assertEqual((offline["state"], offline["block_handoff"]), ("offline", False))
134
- self.assertIn("node offline", offline["detail"])
135
-
136
- def test_known_growth_unknown_topology_and_newer_builder_require_refresh(self):
137
- temporary, root = self.make_repo()
138
- self.addCleanup(temporary.cleanup)
139
- self.enable(root)
140
- self.activate_current(root)
141
- self.assertEqual(DRIFT_GUARD.evaluate_census(root)["state"], "current")
142
-
143
- (root / "packages" / "api" / "src").mkdir(parents=True)
144
- (root / "packages" / "api" / "src" / "index.mjs").write_text(
145
- "export const api = true;\n", encoding="utf-8"
146
- )
147
- subprocess.run(["git", "add", "packages"], cwd=root, check=True)
148
- growth = DRIFT_GUARD.evaluate_census(root)
149
- self.assertEqual((growth["state"], growth["block_handoff"]), ("refresh_required", True))
150
- self.assertIn("topology", growth["reasons"])
151
-
152
- self.activate_current(root)
153
- (root / "services" / "payments" / "src").mkdir(parents=True)
154
- (root / "services" / "payments" / "src" / "index.mjs").write_text(
155
- "export const payments = true;\n", encoding="utf-8"
156
- )
157
- unknown = DRIFT_GUARD.evaluate_census(root)
158
- self.assertEqual((unknown["state"], unknown["block_handoff"]), ("refresh_required", True))
159
- self.assertIn("open", unknown["reasons"])
160
-
161
- (root / "services").rename(root / "evidence-only")
162
- active_path = root / ".census" / "active.json"
163
- active = json.loads(active_path.read_text(encoding="utf-8"))
164
- active["fingerprints"]["builder"] = "older-builder"
165
- active_path.write_text(json.dumps(active) + "\n", encoding="utf-8")
166
- newer = DRIFT_GUARD.evaluate_census(root)
167
- self.assertEqual((newer["state"], newer["block_handoff"]), ("refresh_required", True))
168
- self.assertIn("builder", newer["reasons"])
169
-
170
- def test_change_local_override_never_greens_real_drift(self):
171
- temporary, root = self.make_repo()
172
- self.addCleanup(temporary.cleanup)
173
- self.enable(root, overrides=[{"scope": "this change", "reason": "generated path alias"}])
174
- self.activate_current(root)
175
- (root / "apps" / "web" / "src").mkdir(parents=True)
176
- (root / "apps" / "web" / "src" / "index.mjs").write_text(
177
- "export const web = true;\n", encoding="utf-8"
178
- )
179
- subprocess.run(["git", "add", "apps"], cwd=root, check=True)
180
-
181
- result = DRIFT_GUARD.evaluate_census(root)
182
-
183
- self.assertEqual((result["state"], result["block_handoff"]), ("refresh_required", True))
184
- self.assertEqual(result["overrides"], [
185
- {"scope": "this change", "reason": "generated path alias"}
186
- ])
187
-
188
- def test_override_requires_a_non_empty_text_reason(self):
189
- temporary, root = self.make_repo()
190
- self.addCleanup(temporary.cleanup)
191
- self.enable(root)
192
- self.activate_current(root)
193
- (root / "test").mkdir()
194
- (root / "test" / "proof.test.mjs").write_text("// evidence\n", encoding="utf-8")
195
- subprocess.run(["git", "add", "test"], cwd=root, check=True)
196
- baseline = DRIFT_GUARD.evaluate_census(root)
197
- self.assertTrue(baseline["mechanical_false_positive"])
198
-
199
- profile = root / ".census" / "profile.json"
200
- body = json.loads(profile.read_text(encoding="utf-8"))
201
- for invalid_reason in (True, "", " "):
202
- with self.subTest(reason=invalid_reason):
203
- body["overrides"] = [{
204
- "scope": "this change",
205
- "reason": invalid_reason,
206
- "topologyFingerprint": baseline["change_binding"],
207
- }]
208
- profile.write_text(json.dumps(body) + "\n", encoding="utf-8")
209
- result = DRIFT_GUARD.evaluate_census(root)
210
- self.assertFalse(result["override_applied"])
211
- self.assertTrue(result["block_handoff"])
212
-
213
- def test_profile_and_active_census_files_cannot_follow_foreign_symlinks(self):
214
- temporary, root = self.make_repo()
215
- foreign_tmp, foreign = self.make_repo()
216
- self.addCleanup(temporary.cleanup)
217
- self.addCleanup(foreign_tmp.cleanup)
218
- census = root / ".census"
219
- census.mkdir()
220
- foreign_marker = "FOREIGN-CENSUS-CONTENT-MUST-NOT-LEAK"
221
- foreign_profile = foreign / "profile.json"
222
- foreign_profile.write_text(json.dumps({
223
- "enabled": True,
224
- "overrides": [{"reason": foreign_marker, "scope": "this change"}],
225
- }), encoding="utf-8")
226
- (census / "profile.json").symlink_to(foreign_profile)
227
- (census / "active.json").write_text("{}\n", encoding="utf-8")
228
-
229
- profile_result = DRIFT_GUARD.evaluate_census(root)
230
-
231
- self.assertEqual((profile_result["state"], profile_result["block_handoff"]),
232
- ("failed", False))
233
- self.assertEqual(profile_result["overrides"], [])
234
- self.assertNotIn(foreign_marker, json.dumps(profile_result))
235
- completed = subprocess.run(
236
- [sys.executable, str(HOOKS / "drift-guard.py"), "--census-status"],
237
- cwd=root,
238
- capture_output=True,
239
- check=True,
240
- text=True,
241
- )
242
- self.assertNotIn(foreign_marker, completed.stdout + completed.stderr)
243
-
244
- (census / "profile.json").unlink()
245
- self.enable(root)
246
- (census / "active.json").unlink()
247
- foreign_active = foreign / "active.json"
248
- foreign_active.write_text(f'{{"secret":"{foreign_marker}"}}\n', encoding="utf-8")
249
- (census / "active.json").symlink_to(foreign_active)
250
-
251
- active_result = DRIFT_GUARD.evaluate_census(root)
252
-
253
- self.assertEqual((active_result["state"], active_result["block_handoff"]),
254
- ("failed", True))
255
- self.assertNotIn(foreign_marker, json.dumps(active_result))
256
- (census / "profile.json").unlink()
257
- missing_profile = DRIFT_GUARD.evaluate_census(root)
258
- self.assertEqual((missing_profile["state"], missing_profile["block_handoff"]),
259
- ("failed", False))
260
- self.assertNotIn(foreign_marker, json.dumps(missing_profile))
261
- self.enable(root)
262
- (root / ".handoff").mkdir()
263
- payload = {
264
- "tool_name": "Write",
265
- "tool_input": {
266
- "file_path": str(root / ".handoff" / "52.md"),
267
- "content": "Build [#52](https://github.com/iKon85/agent-workflow-kit/issues/52)",
268
- },
269
- }
270
- with patch.object(DRIFT_GUARD, "run_check", return_value={"deny_recommended": False}):
271
- blocked, message = DRIFT_GUARD.should_block(payload)
272
- self.assertTrue(blocked)
273
- self.assertIn("failed", message)
274
- self.assertNotIn(foreign_marker, message)
275
-
276
- def test_disabled_census_control_failures_stay_visible_but_fail_open(self):
277
- temporary, root = self.make_repo()
278
- foreign_tmp, foreign = self.make_repo()
279
- self.addCleanup(temporary.cleanup)
280
- self.addCleanup(foreign_tmp.cleanup)
281
- self.enable(root)
282
- profile_path = root / ".census" / "profile.json"
283
- profile = json.loads(profile_path.read_text(encoding="utf-8"))
284
- profile["enabled"] = False
285
- profile_path.write_text(json.dumps(profile) + "\n", encoding="utf-8")
286
- active_path = root / ".census" / "active.json"
287
-
288
- active_path.write_text("{not-json\n", encoding="utf-8")
289
- corrupt = DRIFT_GUARD.evaluate_census(root)
290
- self.assertEqual((corrupt["state"], corrupt["block_handoff"]),
291
- ("failed", False))
292
- self.assertEqual(corrupt["detail"], "census scan or active snapshot is invalid")
293
-
294
- active_path.unlink()
295
- foreign_active = foreign / "active.json"
296
- foreign_active.write_text('{"secret":"must-not-leak"}\n', encoding="utf-8")
297
- active_path.symlink_to(foreign_active)
298
- unreadable = DRIFT_GUARD.evaluate_census(root)
299
- self.assertEqual((unreadable["state"], unreadable["block_handoff"]),
300
- ("failed", False))
301
- self.assertNotIn("must-not-leak", json.dumps(unreadable))
302
-
303
- def test_local_scanner_proof_is_required_for_current(self):
304
- temporary, root = self.make_repo()
305
- foreign_tmp, foreign = self.make_repo()
306
- self.addCleanup(temporary.cleanup)
307
- self.addCleanup(foreign_tmp.cleanup)
308
- module = root / "scanner.mjs"
309
- test = root / "scanner.test.mjs"
310
- module.write_text("export function scanLocal() { return ['src']; }\n", encoding="utf-8")
311
- test.write_text("import { test } from 'node:test'; test('proof', () => {});\n",
312
- encoding="utf-8")
313
- subprocess.run(["git", "add", "scanner.mjs", "scanner.test.mjs"], cwd=root, check=True)
314
- scanner = {
315
- "surface": "src", "module": "scanner.mjs",
316
- "export": "scanLocal", "test": "scanner.test.mjs",
317
- }
318
- self.enable(root, local_scanners=[scanner])
319
- self.activate_current(root)
320
- self.assertEqual(DRIFT_GUARD.evaluate_census(root)["state"], "current")
321
-
322
- profile_path = root / ".census" / "profile.json"
323
- profile = json.loads(profile_path.read_text(encoding="utf-8"))
324
- foreign_module = foreign / "foreign.mjs"
325
- foreign_module.write_text("export function scanLocal() { return ['src']; }\n",
326
- encoding="utf-8")
327
- foreign_test = foreign / "foreign.test.mjs"
328
- foreign_test.write_text(
329
- "import { test } from 'node:test'; test('foreign', () => {});\n",
330
- encoding="utf-8",
331
- )
332
- invalid_records = [
333
- {**scanner, "module": "missing.mjs"},
334
- {**scanner, "module": "../escape.mjs"},
335
- {**scanner, "export": "missingExport"},
336
- {**scanner, "test": "missing.test.mjs"},
337
- {**scanner, "test": "../escape.test.mjs"},
338
- ]
339
- (root / "scanner-link.mjs").symlink_to(foreign_module)
340
- invalid_records.append({**scanner, "module": "scanner-link.mjs"})
341
- (root / "foreign-dir-link").symlink_to(foreign, target_is_directory=True)
342
- invalid_records.append({**scanner, "module": "foreign-dir-link/foreign.mjs"})
343
- (root / "scanner-link.test.mjs").symlink_to(foreign_test)
344
- invalid_records.append({**scanner, "test": "scanner-link.test.mjs"})
345
- module.write_text("export function scanLocal() { return ['another-surface']; }\n",
346
- encoding="utf-8")
347
- invalid_records.append(scanner)
348
-
349
- for index, record in enumerate(invalid_records):
350
- with self.subTest(case=index, record=record):
351
- if index == len(invalid_records) - 1:
352
- module.write_text(
353
- "export function scanLocal() { return ['another-surface']; }\n",
354
- encoding="utf-8",
355
- )
356
- else:
357
- module.write_text(
358
- "export function scanLocal() { return ['src']; }\n", encoding="utf-8"
359
- )
360
- profile["localScanners"] = [record]
361
- profile_path.write_text(json.dumps(profile) + "\n", encoding="utf-8")
362
- result = DRIFT_GUARD.evaluate_census(root)
363
- self.assertEqual((result["state"], result["block_handoff"]),
364
- ("refresh_required", True))
365
- self.assertIn("proof:src", result["reasons"])
366
-
367
- module.chmod(0)
368
- try:
369
- profile["localScanners"] = [scanner]
370
- profile_path.write_text(json.dumps(profile) + "\n", encoding="utf-8")
371
- unreadable = DRIFT_GUARD.evaluate_census(root)
372
- self.assertEqual((unreadable["state"], unreadable["block_handoff"]),
373
- ("refresh_required", True))
374
- self.assertIn("proof:src", unreadable["reasons"])
375
- finally:
376
- module.chmod(0o644)
377
-
378
- module.write_text("export function scanLocal() { return ['src']; }\n", encoding="utf-8")
379
- test.write_text("import { test } from 'node:test'; test('proof', () => { throw new Error('no'); });\n",
380
- encoding="utf-8")
381
- profile["localScanners"] = [scanner]
382
- profile_path.write_text(json.dumps(profile) + "\n", encoding="utf-8")
383
- failed_test = DRIFT_GUARD.evaluate_census(root)
384
- self.assertEqual((failed_test["state"], failed_test["block_handoff"]),
385
- ("refresh_required", True))
386
- self.assertIn("proof:src", failed_test["reasons"])
387
-
388
- def test_production_scale_proof_completes_and_hung_proofs_stay_bounded(self):
389
- temporary, root = self.make_repo()
390
- self.addCleanup(temporary.cleanup)
391
- module = root / "scanner.mjs"
392
- test = root / "scanner.test.mjs"
393
- module.write_text("export function scanLocal() { return ['src']; }\n", encoding="utf-8")
394
- test.write_text(
395
- "import { test } from 'node:test'; test('proof', () => {});\n",
396
- encoding="utf-8",
397
- )
398
- subprocess.run(["git", "add", "scanner.mjs", "scanner.test.mjs"], cwd=root, check=True)
399
- scanner = {
400
- "surface": "src", "module": "scanner.mjs",
401
- "export": "scanLocal", "test": "scanner.test.mjs",
402
- }
403
- self.enable(root, local_scanners=[scanner])
404
- self.activate_current(root)
405
-
406
- module.write_text(
407
- "export async function scanLocal() { "
408
- "await new Promise(resolve => setTimeout(resolve, 5200)); return ['src']; }\n",
409
- encoding="utf-8",
410
- )
411
- valid_started = time.monotonic()
412
- valid = DRIFT_GUARD.evaluate_census(root)
413
- valid_elapsed = time.monotonic() - valid_started
414
- self.assertEqual((valid["state"], valid["block_handoff"]), ("current", False))
415
- self.assertGreater(valid_elapsed, 5)
416
- self.assertLess(valid_elapsed, DRIFT_GUARD.CENSUS_BRIDGE_TIMEOUT_SECONDS)
417
-
418
- test.write_text(
419
- "import { test } from 'node:test'; "
420
- "test('hang', async () => new Promise(resolve => setTimeout(resolve, 250)));\n",
421
- encoding="utf-8",
422
- )
423
- module.write_text("export function scanLocal() { return ['src']; }\n", encoding="utf-8")
424
- timeout_started = time.monotonic()
425
- result = DRIFT_GUARD.evaluate_census(root, proof_timeout_ms=25)
426
- timeout_elapsed = time.monotonic() - timeout_started
427
-
428
- self.assertEqual((result["state"], result["block_handoff"]),
429
- ("refresh_required", True))
430
- self.assertIn("proof:src", result["reasons"])
431
- self.assertLess(timeout_elapsed, 2)
432
-
433
- test.write_text("import { test } from 'node:test'; test('proof', () => {});\n",
434
- encoding="utf-8")
435
- module.write_text(
436
- "export async function scanLocal() { return new Promise(() => {}); }\n",
437
- encoding="utf-8",
438
- )
439
- scanner_timeout_started = time.monotonic()
440
- scanner_timeout = DRIFT_GUARD.evaluate_census(root, proof_timeout_ms=25)
441
- scanner_timeout_elapsed = time.monotonic() - scanner_timeout_started
442
- self.assertEqual((scanner_timeout["state"], scanner_timeout["block_handoff"]),
443
- ("refresh_required", True))
444
- self.assertIn("proof:src", scanner_timeout["reasons"])
445
- self.assertLess(scanner_timeout_elapsed, 2)
446
-
447
- def test_activated_census_fails_closed_when_bridge_is_unavailable(self):
448
- temporary, root = self.make_repo()
449
- self.addCleanup(temporary.cleanup)
450
- self.enable(root)
451
- self.activate_current(root)
452
-
453
- with patch.object(DRIFT_GUARD, "scan_census_status",
454
- side_effect=subprocess.TimeoutExpired("node", 1)):
455
- activated = DRIFT_GUARD.evaluate_census(root)
456
- self.assertEqual((activated["state"], activated["block_handoff"]),
457
- ("offline", True))
458
-
459
- (root / ".census" / "active.json").unlink()
460
- with patch.object(DRIFT_GUARD, "scan_census_status",
461
- side_effect=subprocess.TimeoutExpired("node", 1)):
462
- bootstrap = DRIFT_GUARD.evaluate_census(root)
463
- self.assertEqual((bootstrap["state"], bootstrap["block_handoff"]),
464
- ("offline", False))
465
-
466
- self.activate_current(root)
467
- profile_path = root / ".census" / "profile.json"
468
- profile = json.loads(profile_path.read_text(encoding="utf-8"))
469
- profile["enabled"] = False
470
- profile_path.write_text(json.dumps(profile) + "\n", encoding="utf-8")
471
- with patch.object(DRIFT_GUARD, "scan_census_status",
472
- side_effect=subprocess.TimeoutExpired("node", 1)):
473
- disabled = DRIFT_GUARD.evaluate_census(root)
474
- self.assertEqual((disabled["state"], disabled["block_handoff"]),
475
- ("offline", False))
476
-
477
- def test_active_local_scanner_history_is_authoritative(self):
478
- temporary, root = self.make_repo()
479
- self.addCleanup(temporary.cleanup)
480
- module = root / "scanner.mjs"
481
- test = root / "scanner.test.mjs"
482
- module.write_text("export function scanLocal() { return ['src']; }\n", encoding="utf-8")
483
- test.write_text("import { test } from 'node:test'; test('proof', () => {});\n",
484
- encoding="utf-8")
485
- subprocess.run(["git", "add", "scanner.mjs", "scanner.test.mjs"], cwd=root, check=True)
486
- scanner = {
487
- "surface": "src", "module": "scanner.mjs",
488
- "export": "scanLocal", "test": "scanner.test.mjs",
489
- }
490
- self.enable(root, local_scanners=[scanner])
491
- self.activate_current(root)
492
- profile_path = root / ".census" / "profile.json"
493
- active_path = root / ".census" / "active.json"
494
-
495
- profile = json.loads(profile_path.read_text(encoding="utf-8"))
496
- for replacement in (None, "corrupt", [], [{**scanner, "export": "other"}]):
497
- with self.subTest(current=replacement):
498
- changed = dict(profile)
499
- if replacement is None:
500
- changed.pop("localScanners")
501
- else:
502
- changed["localScanners"] = replacement
503
- profile_path.write_text(json.dumps(changed) + "\n", encoding="utf-8")
504
- result = DRIFT_GUARD.evaluate_census(root)
505
- self.assertEqual((result["state"], result["block_handoff"]),
506
- ("refresh_required", True))
507
- self.assertTrue(any(reason.startswith("proof:") for reason in result["reasons"]))
508
-
509
- profile_path.write_text(json.dumps(profile) + "\n", encoding="utf-8")
510
- for replacement in (None, "corrupt", [], [{**scanner, "test": "other.test.mjs"}]):
511
- with self.subTest(active=replacement):
512
- active = json.loads(active_path.read_text(encoding="utf-8"))
513
- report = active.setdefault("profileReport", {})
514
- if replacement is None:
515
- report.pop("localScanners", None)
516
- else:
517
- report["localScanners"] = replacement
518
- active_path.write_text(json.dumps(active) + "\n", encoding="utf-8")
519
- result = DRIFT_GUARD.evaluate_census(root)
520
- self.assertEqual((result["state"], result["block_handoff"]),
521
- ("refresh_required", True))
522
- self.assertTrue(any(reason.startswith("proof:") for reason in result["reasons"]))
523
- self.activate_current(root)
524
-
525
- def test_local_scanner_export_requires_an_array_of_exact_surface_strings(self):
526
- temporary, root = self.make_repo()
527
- self.addCleanup(temporary.cleanup)
528
- module = root / "scanner.mjs"
529
- test = root / "scanner.test.mjs"
530
- test.write_text("import { test } from 'node:test'; test('proof', () => {});\n",
531
- encoding="utf-8")
532
- scanner = {
533
- "surface": "src", "module": "scanner.mjs",
534
- "export": "scanLocal", "test": "scanner.test.mjs",
535
- }
536
- self.enable(root, local_scanners=[scanner])
537
- invalid_results = (
538
- "'src'",
539
- "({ includes: () => true })",
540
- "['src', 42]",
541
- "['SRC']",
542
- )
543
- for expression in invalid_results:
544
- with self.subTest(expression=expression):
545
- module.write_text(
546
- f"export function scanLocal() {{ return {expression}; }}\n", encoding="utf-8"
547
- )
548
- subprocess.run(["git", "add", "scanner.mjs", "scanner.test.mjs"],
549
- cwd=root, check=True)
550
- self.activate_current(root)
551
- result = DRIFT_GUARD.evaluate_census(root)
552
- self.assertEqual((result["state"], result["block_handoff"]),
553
- ("refresh_required", True))
554
- self.assertIn("proof:src", result["reasons"])
555
-
556
- module.write_text("export function scanLocal() { return ['src']; }\n", encoding="utf-8")
557
- subprocess.run(["git", "add", "scanner.mjs"], cwd=root, check=True)
558
- self.activate_current(root)
559
- self.assertEqual(DRIFT_GUARD.evaluate_census(root)["state"], "current")
560
-
561
- def test_justified_change_local_override_bypasses_only_evidence_topology_noise(self):
562
- temporary, root = self.make_repo()
563
- self.addCleanup(temporary.cleanup)
564
- self.enable(root)
565
- self.activate_current(root)
566
- (root / "test").mkdir()
567
- (root / "test" / "new-proof.test.mjs").write_text(
568
- "// evidence only\n", encoding="utf-8"
569
- )
570
- subprocess.run(["git", "add", "test"], cwd=root, check=True)
571
-
572
- unbound = DRIFT_GUARD.evaluate_census(root)
573
- self.assertTrue(unbound["mechanical_false_positive"])
574
- self.assertFalse(unbound["override_applied"])
575
- profile = root / ".census" / "profile.json"
576
- body = json.loads(profile.read_text(encoding="utf-8"))
577
- body["overrides"] = [{
578
- "scope": "this change",
579
- "reason": "test-only evidence file",
580
- "topologyFingerprint": unbound["change_binding"],
581
- }]
582
- profile.write_text(json.dumps(body) + "\n", encoding="utf-8")
583
-
584
- result = DRIFT_GUARD.evaluate_census(root)
585
-
586
- self.assertEqual(result["state"], "refresh_required")
587
- self.assertEqual(result["reasons"], ["topology"])
588
- self.assertTrue(result["override_applied"])
589
- self.assertFalse(result["block_handoff"])
590
-
591
- (root / "test" / "later-proof.test.mjs").write_text(
592
- "// later evidence\n", encoding="utf-8"
593
- )
594
- subprocess.run(["git", "add", "test"], cwd=root, check=True)
595
- stale = DRIFT_GUARD.evaluate_census(root)
596
- self.assertNotEqual(stale["change_binding"], result["change_binding"])
597
- self.assertFalse(stale["override_applied"])
598
- self.assertTrue(stale["block_handoff"])
599
-
600
- def test_handoff_payload_uses_target_repo_when_cwd_is_elsewhere(self):
601
- target_tmp, target = self.make_repo()
602
- cwd_tmp, cwd_repo = self.make_repo()
603
- self.addCleanup(target_tmp.cleanup)
604
- self.addCleanup(cwd_tmp.cleanup)
605
- self.enable(target)
606
- self.activate_current(target)
607
- (target / "test").mkdir()
608
- (target / "test" / "proof.test.mjs").write_text("// evidence\n", encoding="utf-8")
609
- subprocess.run(["git", "add", "test"], cwd=target, check=True)
610
- (target / ".handoff").mkdir()
611
- payload = {
612
- "tool_name": "Write",
613
- "tool_input": {
614
- "file_path": str(target / ".handoff" / "52.md"),
615
- "content": "Build [#52](https://github.com/iKon85/agent-workflow-kit/issues/52)",
616
- },
617
- }
618
-
619
- previous = Path.cwd()
620
- try:
621
- os.chdir(cwd_repo / "src")
622
- with patch.object(DRIFT_GUARD, "run_check", return_value={"deny_recommended": False}):
623
- blocked, message = DRIFT_GUARD.should_block(payload)
624
- finally:
625
- os.chdir(previous)
626
-
627
- self.assertTrue(blocked)
628
- self.assertIn("CENSUS", message)
629
-
630
- def test_handoff_payload_rejects_symlink_escape_from_claimed_repo(self):
631
- target_tmp, target = self.make_repo()
632
- foreign_tmp, foreign = self.make_repo()
633
- self.addCleanup(target_tmp.cleanup)
634
- self.addCleanup(foreign_tmp.cleanup)
635
- self.enable(foreign)
636
- (foreign / "handoffs").mkdir()
637
- (target / ".handoff").symlink_to(foreign / "handoffs", target_is_directory=True)
638
- payload = {
639
- "tool_name": "Write",
640
- "tool_input": {
641
- "file_path": str(target / ".handoff" / "52.md"),
642
- "content": "Build [#52](https://github.com/iKon85/agent-workflow-kit/issues/52)",
643
- },
644
- }
645
-
646
- with patch.object(DRIFT_GUARD, "run_check", return_value={"deny_recommended": False}):
647
- blocked, message = DRIFT_GUARD.should_block(payload)
648
-
649
- self.assertTrue(blocked)
650
- self.assertIn("target repository", message)
651
-
652
- def test_cli_root_resolution_finds_git_root_from_subdirectory(self):
653
- temporary, root = self.make_repo()
654
- self.addCleanup(temporary.cleanup)
655
- self.enable(root)
656
-
657
- completed = subprocess.run(
658
- [sys.executable, str(HOOKS / "drift-guard.py"), "--census-status"],
659
- cwd=root / "src",
660
- capture_output=True,
661
- check=True,
662
- text=True,
663
- )
664
- status = json.loads(completed.stdout)
665
-
666
- self.assertEqual(status["state"], "bootstrap")
667
-
668
- def test_activated_refresh_blocks_build_handoff_but_not_normal_work(self):
669
- payload = {
670
- "tool_name": "Write",
671
- "tool_input": {
672
- "file_path": "/tmp/repo/.handoff/52.md",
673
- "content": "Build [#52](https://github.com/iKon85/agent-workflow-kit/issues/52)",
674
- },
675
- }
676
- refresh = {
677
- "state": "refresh_required",
678
- "block_handoff": True,
679
- "reasons": ["topology"],
680
- "overrides": [],
681
- }
682
- with patch.object(DRIFT_GUARD, "run_check", return_value={"deny_recommended": False}), \
683
- patch.object(DRIFT_GUARD, "evaluate_census", return_value=refresh):
684
- blocked, message = DRIFT_GUARD.should_block(payload)
685
- self.assertTrue(blocked)
686
- self.assertIn("CENSUS", message)
687
-
688
- normal_payload = {"tool_name": "Write", "tool_input": {"file_path": "/tmp/repo/notes.md"}}
689
- with patch.object(DRIFT_GUARD, "evaluate_census") as census:
690
- self.assertEqual(DRIFT_GUARD.should_block(normal_payload), (False, ""))
691
- census.assert_not_called()
692
-
693
- def test_cross_cutting_prd_and_kit_update_prose_use_the_same_backstop_contract(self):
694
- source_prd = (ROOT / ".claude/skills/to-prd/SKILL.md").read_text(encoding="utf-8")
695
- mirror_prd = (ROOT / ".agents/skills/to-prd/SKILL.md").read_text(encoding="utf-8")
696
- source_update = (ROOT / ".claude/skills/kit-update/SKILL.md").read_text(encoding="utf-8")
697
- mirror_update = (ROOT / ".agents/skills/kit-update/SKILL.md").read_text(encoding="utf-8")
698
-
699
- for prose in (source_prd, mirror_prd):
700
- self.assertIn("--census-status", prose)
701
- self.assertRegex(prose, r"cross-cutting[\s\S]*refresh_required[\s\S]*must not be locked")
702
- self.assertRegex(prose, r"disabled[\s\S]*no_census[\s\S]*manual walk")
703
- self.assertRegex(prose, r"change-local\s+override[\s\S]*mechanical\s+false positive")
704
- self.assertIn("topologyFingerprint", prose)
705
- for prose in (source_update, mirror_update):
706
- self.assertIn("--census-status", prose)
707
- self.assertRegex(prose, r"newer census builder[\s\S]*census-update")
708
- self.assertRegex(prose, r"never overwrite[\s\S]*consumer-owned census")
709
-
710
-
711
- if __name__ == "__main__":
712
- unittest.main()