@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.
- package/.agents/skills/grill-me/SKILL.md +3 -1
- package/.agents/skills/grill-with-docs/SKILL.md +3 -1
- package/.agents/skills/kit-update/SKILL.md +9 -11
- package/.agents/skills/orchestrate-wave/SKILL.md +8 -8
- package/.agents/skills/setup-workflow/SKILL.md +20 -18
- package/.agents/skills/setup-workflow/board-sync.md +4 -1
- package/.agents/skills/setup-workflow/workflow-advisories.md +3 -2
- package/.agents/skills/setup-workflow/worktree-lifecycle.md +36 -31
- package/.agents/skills/to-issues/SKILL.md +43 -0
- package/.agents/skills/wrapup/SKILL.md +98 -54
- package/.claude/skills/grill-me/SKILL.md +3 -1
- package/.claude/skills/grill-me-codex/SKILL.md +3 -1
- package/.claude/skills/grill-with-docs/SKILL.md +3 -1
- package/.claude/skills/grill-with-docs-codex/SKILL.md +3 -1
- package/.claude/skills/kit-update/SKILL.md +9 -11
- package/.claude/skills/orchestrate-wave/SKILL.md +8 -8
- package/.claude/skills/setup-workflow/SKILL.md +20 -18
- package/.claude/skills/setup-workflow/board-sync.md +4 -1
- package/.claude/skills/setup-workflow/workflow-advisories.md +3 -2
- package/.claude/skills/setup-workflow/worktree-lifecycle.md +36 -31
- package/.claude/skills/to-issues/SKILL.md +43 -0
- package/.claude/skills/wrapup/SKILL.md +98 -54
- package/README.md +249 -7
- package/agent-workflow-kit.package.json +42 -42
- package/package.json +16 -12
- package/scripts/marker_lib.py +3 -0
- package/scripts/profile_globs.py +26 -45
- package/scripts/release-delta-guard.mjs +26 -9
- package/scripts/worktree-lifecycle/README.md +57 -160
- package/scripts/worktree-lifecycle/capabilities.json +2 -3
- package/scripts/worktree-lifecycle/classify.py +480 -0
- package/scripts/worktree-lifecycle/cleanup.py +9 -29
- package/scripts/worktree-lifecycle/core.py +68 -1429
- package/scripts/worktree-lifecycle/ignore_seed.py +1 -1
- package/scripts/worktree-lifecycle/profile.py +46 -29
- package/scripts/worktree-lifecycle/setup.py +7 -19
- package/scripts/wrapup-land.py +1292 -545
- package/src/consumer-migrations.json +1 -16
- package/src/lib/bundle.mjs +31 -1
- package/docs/adr/0001-consumer-divergence-policy.md +0 -53
- package/docs/adr/0002-capability-gated-orchestration.md +0 -70
- package/docs/adr/0003-kit-core-and-project-extension-lifecycle.md +0 -63
- package/docs/adr/0004-release-intent-is-a-version-tag.md +0 -64
- package/docs/adr/0005-to-issues-is-the-planning-facade.md +0 -42
- package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +0 -95
- package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +0 -88
- package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +0 -84
- package/docs/adr/0009-teardown-authority-is-stateless-repository-classification.md +0 -101
- package/docs/adr/0010-model-roster-replaces-the-optimization-dial.md +0 -87
- package/docs/agents/board-sync.md +0 -74
- package/docs/agents/code-review.md +0 -14
- package/docs/agents/workflow-capabilities.json +0 -28
- package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
- package/docs/research/benchlm-routing-source.md +0 -198
- package/docs/research/consumer-owned-protocol-files.md +0 -238
- package/docs/research/frontend-agent-benchmarks.md +0 -282
- package/docs/research/model-effort-routing-benchmarks.md +0 -261
- package/docs/research/provider-neutral-agent-routing.md +0 -207
- package/docs/research/wave-152-consumer-acceptance.md +0 -98
- package/docs/research/wave-43-script-hook-census.md +0 -128
- package/scripts/build-kit.mjs +0 -117
- package/scripts/build-kit.test.mjs +0 -207
- package/scripts/census/census.test.mjs +0 -252
- package/scripts/census/state.test.mjs +0 -15
- package/scripts/census/transaction.test.mjs +0 -103
- package/scripts/census-contract.test.mjs +0 -227
- package/scripts/check-kit-staleness.mjs +0 -45
- package/scripts/check-kit-staleness.test.mjs +0 -19
- package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -165
- package/scripts/codex-exec.test.mjs +0 -871
- package/scripts/grill-census-wiring-guard.mjs +0 -87
- package/scripts/grill-census-wiring-guard.test.mjs +0 -107
- package/scripts/kit-release.test.mjs +0 -273
- package/scripts/kit-update-pr.test.mjs +0 -151
- package/scripts/lib/audit-refs.mjs +0 -146
- package/scripts/lib/scrub.mjs +0 -113
- package/scripts/lib/scrub.test.mjs +0 -187
- package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
- package/scripts/portability_profile_scan.py +0 -53
- package/scripts/release-delta-guard.test.mjs +0 -174
- package/scripts/release-parity.test.mjs +0 -90
- package/scripts/release-state.test.mjs +0 -360
- package/scripts/test_anchor_table.py +0 -69
- package/scripts/test_board_bootstrap.py +0 -348
- package/scripts/test_board_sync.py +0 -208
- package/scripts/test_board_sync_create_idempotency.py +0 -117
- package/scripts/test_board_sync_wave_title.py +0 -120
- package/scripts/test_census_backstop.py +0 -712
- package/scripts/test_census_forward_contract.py +0 -91
- package/scripts/test_census_update_contract.test.mjs +0 -454
- package/scripts/test_codex_adapter_sync_contract.py +0 -169
- package/scripts/test_dist_kit_smoke.py +0 -220
- package/scripts/test_drift_guard_diagnostics.py +0 -295
- package/scripts/test_issue_claim_contract.py +0 -169
- package/scripts/test_kit_docs_language_census.py +0 -178
- package/scripts/test_marker_lib.py +0 -162
- package/scripts/test_orchestrate_wave_contract.py +0 -381
- package/scripts/test_pr_body_check.py +0 -264
- package/scripts/test_profile_globs.py +0 -280
- package/scripts/test_program_planning_contract.py +0 -133
- package/scripts/test_release_authorization_contract.py +0 -101
- package/scripts/test_render_anchor.py +0 -267
- package/scripts/test_retro_wrapup_contract.py +0 -153
- package/scripts/test_skill_code_review_seed.py +0 -89
- package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
- package/scripts/test_skill_frontmatter_lint.py +0 -234
- package/scripts/test_skill_gh_lint.py +0 -124
- package/scripts/test_skill_language_census.py +0 -254
- package/scripts/test_skill_optional_readiness.py +0 -171
- package/scripts/test_skill_portability_lint.py +0 -866
- package/scripts/test_skill_precommit_template.py +0 -30
- package/scripts/test_skill_publish_audit.py +0 -358
- package/scripts/test_skill_readiness_contract.py +0 -264
- package/scripts/test_skill_readiness_preflight.py +0 -180
- package/scripts/test_skill_required_readiness.py +0 -233
- package/scripts/test_skill_selfcontainment_lint.py +0 -255
- package/scripts/test_skill_setup_workflow_seeds.py +0 -691
- package/scripts/test_skill_stale_name_lint.py +0 -126
- package/scripts/test_skill_surface_refs.py +0 -300
- package/scripts/test_skill_trailing_artifact_lint.py +0 -71
- package/scripts/test_tdd_contract.py +0 -78
- package/scripts/test_worktree_ignore_seed.py +0 -320
- package/scripts/test_worktree_setup_base_guard.py +0 -140
- package/scripts/test_worktree_wrapup_contract.py +0 -1737
- package/scripts/test_wrapup_land.py +0 -428
- 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)
|