@ikon85/agent-workflow-kit 0.26.1 → 0.27.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 (33) hide show
  1. package/.agents/skills/ask-matt/SKILL.md +1 -2
  2. package/.agents/skills/board-to-waves/SKILL.md +2 -2
  3. package/.agents/skills/improve-codebase-architecture/SKILL.md +0 -2
  4. package/.agents/skills/setup-workflow/board-sync.md +11 -0
  5. package/.claude/skills/ask-matt/SKILL.md +1 -2
  6. package/.claude/skills/board-to-waves/SKILL.md +2 -2
  7. package/.claude/skills/improve-codebase-architecture/SKILL.md +0 -2
  8. package/.claude/skills/setup-pre-commit/SKILL.md +3 -0
  9. package/.claude/skills/setup-pre-commit/scripts/pre-commit.template.sh +6 -0
  10. package/.claude/skills/setup-workflow/board-sync.md +11 -0
  11. package/.claude/skills/skill-manifest.json +1 -9
  12. package/PROVENANCE.md +5 -4
  13. package/README.md +39 -17
  14. package/agent-workflow-kit.package.json +297 -49
  15. package/docs/agents/board-sync.md +3 -0
  16. package/docs/workflow.html +259 -0
  17. package/docs/workflow.png +0 -0
  18. package/package.json +1 -1
  19. package/scripts/board-sync.py +18 -13
  20. package/scripts/board_config.py +7 -0
  21. package/scripts/build-kit.mjs +2 -1
  22. package/scripts/build-kit.test.mjs +44 -1
  23. package/scripts/test_board_sync_wave_title.py +108 -0
  24. package/scripts/test_skill_precommit_template.py +30 -0
  25. package/src/commands/init.mjs +12 -1
  26. package/src/commands/update.mjs +2 -1
  27. package/src/lib/bundle.mjs +12 -6
  28. package/src/lib/manifest.mjs +13 -3
  29. package/src/lib/updateReconcile.mjs +22 -8
  30. package/.agents/skills/zoom-out/SKILL.md +0 -7
  31. package/.agents/skills/zoom-out/THIRD-PARTY-NOTICES.md +0 -29
  32. package/.claude/skills/zoom-out/SKILL.md +0 -7
  33. package/.claude/skills/zoom-out/THIRD-PARTY-NOTICES.md +0 -29
@@ -61,6 +61,9 @@ The workflow board is [Agent Workflow Kit](https://github.com/users/iKon85/proje
61
61
  "headings": {
62
62
  "vorBau": "Clarify Before Build"
63
63
  },
64
+ "titles": {
65
+ "wavePrefix": "Welle"
66
+ },
64
67
  "wrapup": {
65
68
  "remoteBranchSweep": false
66
69
  }
@@ -0,0 +1,259 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>agent-workflow-kit — Workflow</title>
7
+ <style>
8
+ :root {
9
+ --page-bg: #F5F1E8;
10
+ --surface: #FAFAF7;
11
+ --ink: #1F2532;
12
+ --muted: #5B6579;
13
+ --border: #E8E4D8;
14
+ --accent-deep: #5C5630;
15
+
16
+ --plan: #4A6280; --plan-bg: #DDE3EC; --plan-text: #2F4258;
17
+ --exec: #5C7A4A; --exec-bg: #E8EDE0; --exec-text: #3D5230;
18
+ --land: #C2902B; --land-bg: #F2E5C7; --land-text: #7A5B1D;
19
+ --learn: #8A95A6; --learn-bg: #E8EBF0; --learn-text: #5B6579;
20
+ --gate: #A8432D; --gate-bg: #F2DAD0; --gate-text: #6E2B1D;
21
+ --door: #5B6579; --door-bg: #EFEDE4; --door-text: #454E60;
22
+ }
23
+ @media (prefers-color-scheme: dark) {
24
+ :root {
25
+ --page-bg:#1F2532; --surface:#2E3548; --ink:#F5F1E8; --muted:#A6B0BF;
26
+ --border:rgba(255,255,255,0.14); --accent-deep:#B5A86B;
27
+ --plan:#7C9BB2; --plan-bg:#2E3850; --plan-text:#8FA9C4;
28
+ --exec:#82A36C; --exec-bg:#2A3A26; --exec-text:#94B57E;
29
+ --land:#DBAC4F; --land-bg:#3A3220; --land-text:#DBAC4F;
30
+ --learn:#8A95A6; --learn-bg:#2A3142; --learn-text:#9CA6B5;
31
+ --gate:#D9554F; --gate-bg:#3A2620; --gate-text:#EC8780;
32
+ --door:#8A95A6; --door-bg:#272E3E; --door-text:#A6B0BF;
33
+ }
34
+ }
35
+ :root[data-theme="dark"] {
36
+ --page-bg:#1F2532; --surface:#2E3548; --ink:#F5F1E8; --muted:#A6B0BF;
37
+ --border:rgba(255,255,255,0.14); --accent-deep:#B5A86B;
38
+ --plan:#7C9BB2; --plan-bg:#2E3850; --plan-text:#8FA9C4;
39
+ --exec:#82A36C; --exec-bg:#2A3A26; --exec-text:#94B57E;
40
+ --land:#DBAC4F; --land-bg:#3A3220; --land-text:#DBAC4F;
41
+ --learn:#8A95A6; --learn-bg:#2A3142; --learn-text:#9CA6B5;
42
+ --gate:#D9554F; --gate-bg:#3A2620; --gate-text:#EC8780;
43
+ --door:#8A95A6; --door-bg:#272E3E; --door-text:#A6B0BF;
44
+ }
45
+ :root[data-theme="light"] {
46
+ --page-bg:#F5F1E8; --surface:#FAFAF7; --ink:#1F2532; --muted:#5B6579;
47
+ --border:#E8E4D8; --accent-deep:#5C5630;
48
+ --plan:#4A6280; --plan-bg:#DDE3EC; --plan-text:#2F4258;
49
+ --exec:#5C7A4A; --exec-bg:#E8EDE0; --exec-text:#3D5230;
50
+ --land:#C2902B; --land-bg:#F2E5C7; --land-text:#7A5B1D;
51
+ --learn:#8A95A6; --learn-bg:#E8EBF0; --learn-text:#5B6579;
52
+ --gate:#A8432D; --gate-bg:#F2DAD0; --gate-text:#6E2B1D;
53
+ --door:#5B6579; --door-bg:#EFEDE4; --door-text:#454E60;
54
+ }
55
+
56
+ * { box-sizing: border-box; }
57
+ body {
58
+ margin: 0; background: var(--page-bg); color: var(--ink);
59
+ font-family: Charter, "Bitstream Charter", "Iowan Old Style", Georgia, serif;
60
+ font-size: 17px; line-height: 1.6; overflow-x: hidden;
61
+ }
62
+ .wrap { max-width: 1010px; margin: 0 auto; padding: 40px 24px 80px; }
63
+ h1,h3 { font-family: system-ui, "Segoe UI", -apple-system, sans-serif; }
64
+ h1 { font-size: 1.9rem; font-weight: 650; letter-spacing: -0.015em; margin: 0 0 8px; text-wrap: balance; }
65
+ p { margin: 0 0 1em; max-width: 72ch; }
66
+ a { color: var(--accent-deep); }
67
+ .eyebrow {
68
+ font-family: ui-monospace, Menlo, monospace; font-size: 0.72rem;
69
+ letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin-bottom: 12px;
70
+ }
71
+ .lede { color: var(--muted); font-size: 1.05rem; max-width: 66ch; }
72
+
73
+ .legend { display: flex; flex-wrap: wrap; gap: 8px 18px; margin: 18px 0 22px; }
74
+ .key { display: flex; align-items: center; gap: 7px; font-family: system-ui, sans-serif; font-size: 0.8rem; color: var(--muted); }
75
+ .dot { width: 11px; height: 11px; border-radius: 3px; background: var(--c); flex: none; }
76
+
77
+ .station {
78
+ font-family: ui-monospace, Menlo, monospace; font-size: 0.73rem;
79
+ background: var(--soft, var(--page-bg)); color: var(--sc, var(--ink));
80
+ border: 1px solid var(--border); border-radius: 5px; padding: 4px 8px; white-space: nowrap;
81
+ }
82
+ .station.plain { --soft: var(--page-bg); --sc: var(--muted); font-style: italic; }
83
+ .st-plan { --soft: var(--plan-bg); --sc: var(--plan-text); border-color: color-mix(in srgb, var(--plan) 40%, var(--border)); }
84
+ .st-exec { --soft: var(--exec-bg); --sc: var(--exec-text); border-color: color-mix(in srgb, var(--exec) 40%, var(--border)); }
85
+ .st-land { --soft: var(--land-bg); --sc: var(--land-text); border-color: color-mix(in srgb, var(--land) 40%, var(--border)); }
86
+ .st-learn { --soft: var(--learn-bg); --sc: var(--learn-text); border-color: color-mix(in srgb, var(--learn) 40%, var(--border)); }
87
+ .st-gate { --soft: var(--gate-bg); --sc: var(--gate-text); border-color: color-mix(in srgb, var(--gate) 50%, var(--border)); font-weight: 600; }
88
+ .st-door { --soft: var(--door-bg); --sc: var(--door-text); border-color: color-mix(in srgb, var(--door) 40%, var(--border)); }
89
+ .arrow { color: var(--muted); font-family: system-ui, sans-serif; font-size: 0.85rem; padding: 0 2px; user-select: none; }
90
+ .decision { font-size: 0.76rem; color: var(--muted); font-style: italic; padding: 0 4px; }
91
+
92
+ .subway { display: grid; grid-template-columns: 158px 1fr; border: 1px solid var(--border);
93
+ border-radius: 8px; background: var(--surface); overflow: hidden; }
94
+ .sw-row { display: contents; }
95
+ .sw-label { padding: 14px 14px; border-bottom: 1px solid var(--border); border-right: 1px solid var(--border);
96
+ font-family: system-ui, sans-serif; font-size: 0.8rem; font-weight: 650; color: var(--lc, var(--muted));
97
+ display: flex; flex-direction: column; gap: 2px; justify-content: center; }
98
+ .sw-label small { font-weight: 400; color: var(--muted); font-size: 0.72rem; }
99
+ .sw-track { padding: 13px 16px; border-bottom: 1px solid var(--border); display: flex; flex-wrap: wrap;
100
+ align-items: center; gap: 7px 4px; background: linear-gradient(to right, color-mix(in srgb, var(--lbg, transparent) 45%, transparent), transparent 55%); }
101
+ .sw-row:last-of-type .sw-label, .sw-row:last-of-type .sw-track { border-bottom: none; }
102
+
103
+ .inter { grid-column: 1 / -1; padding: 12px 16px 14px; border-top: 1px dashed var(--border); }
104
+ .inter .i-title { font-family: system-ui, sans-serif; font-weight: 650; font-size: 0.82rem; color: var(--muted); margin-bottom: 8px; }
105
+ .inter .i-line { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 4px; font-size: 0.85rem; color: var(--muted); margin-bottom: 7px; }
106
+ .inter .i-line:last-child { margin-bottom: 0; }
107
+
108
+ footer { margin-top: 28px; font-size: 0.85rem; color: var(--muted); font-family: system-ui, sans-serif; }
109
+
110
+ @media (max-width: 640px) {
111
+ .subway { grid-template-columns: 1fr; }
112
+ .sw-label { border-right: none; padding-bottom: 4px; }
113
+ .sw-track { border-bottom: 1px solid var(--border); }
114
+ }
115
+ </style>
116
+ </head>
117
+ <body>
118
+ <div class="wrap">
119
+ <div class="eyebrow">agent-workflow-kit · workflow</div>
120
+ <h1>One loop, many doors</h1>
121
+ <p class="lede">Every piece of work — idea, plan, backlog, bug — boards its own line, and every line converges on the same spine: <em>implement → wrapup → retro</em>. implement drives tdd internally — red → green → refactor at pre-agreed seams, code-review before landing.</p>
122
+
123
+ <div class="legend">
124
+ <span class="key"><span class="dot" style="--c: var(--door)"></span> Door / input</span>
125
+ <span class="key"><span class="dot" style="--c: var(--plan)"></span> Plan</span>
126
+ <span class="key"><span class="dot" style="--c: var(--exec)"></span> Execute</span>
127
+ <span class="key"><span class="dot" style="--c: var(--land)"></span> Land</span>
128
+ <span class="key"><span class="dot" style="--c: var(--learn)"></span> Learn</span>
129
+ <span class="key"><span class="dot" style="--c: var(--gate)"></span> Gate</span>
130
+ </div>
131
+
132
+ <div class="subway">
133
+ <div class="sw-row">
134
+ <div class="sw-label" style="--lc: var(--door-text); --lbg: var(--door-bg);">router<small>size unclear? start here</small></div>
135
+ <div class="sw-track" style="--lbg: var(--door-bg);">
136
+ <span class="station st-plan">scale-check</span>
137
+ <span class="decision">3–6 questions</span><span class="arrow">→</span>
138
+ <span class="station plain">Program → program line</span><span class="arrow">/</span>
139
+ <span class="station plain">Feature → main line</span><span class="arrow">/</span>
140
+ <span class="station plain">Direct-Slice → implement</span><span class="arrow">/</span>
141
+ <span class="station plain">Bug → repair line</span>
142
+ </div>
143
+ </div>
144
+ <div class="sw-row">
145
+ <div class="sw-label" style="--lc: var(--plan-text); --lbg: var(--plan-bg);">main line<small>idea → shipped</small></div>
146
+ <div class="sw-track" style="--lbg: var(--plan-bg);">
147
+ <span class="station plain">idea / plan / issue</span><span class="arrow">→</span>
148
+ <span class="station st-gate">grill? — see grill line</span><span class="arrow">→</span>
149
+ <span class="station st-plan">to-prd</span><span class="arrow">→</span>
150
+ <span class="station st-plan">to-issues</span><span class="arrow">→</span>
151
+ <span class="station st-gate">gate</span><span class="arrow">→</span>
152
+ <span class="station st-exec">implement</span>
153
+ <span class="decision">drives tdd inside</span><span class="arrow">→</span>
154
+ <span class="station st-land">wrapup</span><span class="arrow">→</span>
155
+ <span class="station st-learn">retro</span>
156
+ </div>
157
+ </div>
158
+ <div class="sw-row">
159
+ <div class="sw-label" style="--lc: var(--gate-text); --lbg: var(--gate-bg);">grill line<small>as deep as the stakes</small></div>
160
+ <div class="sw-track" style="--lbg: var(--gate-bg);">
161
+ <span class="station plain">mechanical change → none</span><span class="arrow">/</span>
162
+ <span class="station st-gate">grill-me</span>
163
+ <span class="decision">relentless interview, one question at a time</span><span class="arrow">/</span>
164
+ <span class="station st-gate">grill-with-docs</span>
165
+ <span class="decision">+ CONTEXT.md, ADRs updated inline</span><span class="arrow">/</span>
166
+ <span class="station st-gate">grill-me-codex · grill-with-docs-codex</span>
167
+ <span class="decision">+ Act 2: Codex reviews PLAN.md adversarially until APPROVED</span>
168
+ </div>
169
+ </div>
170
+ <div class="sw-row">
171
+ <div class="sw-label" style="--lc: var(--gate-text); --lbg: var(--gate-bg);">codex line<small>second model, high stakes</small></div>
172
+ <div class="sw-track" style="--lbg: var(--gate-bg);">
173
+ <span class="station st-gate">codex-review</span>
174
+ <span class="decision">plan already exists — cross-model review only</span><span class="arrow">·</span>
175
+ <span class="station st-exec">codex-build</span>
176
+ <span class="decision">Act 3: Codex implements the frozen spec, Claude verifies diff + proof</span>
177
+ </div>
178
+ </div>
179
+ <div class="sw-row">
180
+ <div class="sw-label" style="--lc: var(--plan-text); --lbg: var(--plan-bg);">program line<small>too big for one wave</small></div>
181
+ <div class="sw-track" style="--lbg: var(--plan-bg);">
182
+ <span class="station st-plan">scale-check</span><span class="arrow">→</span>
183
+ <span class="station st-gate">grill</span><span class="arrow">→</span>
184
+ <span class="station st-plan">to-waves</span>
185
+ <span class="decision">preview gate · zero board writes until yes</span><span class="arrow">→</span>
186
+ <span class="station st-exec">orchestrate-wave</span>
187
+ <span class="decision">per wave, often AFK</span><span class="arrow">→</span>
188
+ <span class="station st-land">wrapup</span><span class="arrow">→</span>
189
+ <span class="station st-learn">retro</span>
190
+ </div>
191
+ </div>
192
+ <div class="sw-row">
193
+ <div class="sw-label" style="--lc: var(--plan-text); --lbg: var(--plan-bg);">board line<small>it grew bottom-up</small></div>
194
+ <div class="sw-track" style="--lbg: var(--plan-bg);">
195
+ <span class="station plain">loose backlog</span><span class="arrow">→</span>
196
+ <span class="station st-plan">board-to-waves</span>
197
+ <span class="decision">wave-stub candidates, no number yet</span><span class="arrow">→</span>
198
+ <span class="station st-plan">to-prd</span>
199
+ <span class="decision">matures the stub</span><span class="arrow">→</span>
200
+ <span class="station st-plan">to-issues</span>
201
+ <span class="decision">≥2 slices → promotes to a numbered wave anchor</span><span class="arrow">→</span>
202
+ <span class="station plain">joins the program line</span>
203
+ </div>
204
+ </div>
205
+ <div class="sw-row">
206
+ <div class="sw-label" style="--lc: var(--gate-text); --lbg: var(--gate-bg);">repair line<small>something's broken</small></div>
207
+ <div class="sw-track" style="--lbg: var(--gate-bg);">
208
+ <span class="station plain">bug / anomaly</span><span class="arrow">→</span>
209
+ <span class="station st-door">diagnose</span>
210
+ <span class="decision">reproduce → isolate → fix → prove</span><span class="arrow">→</span>
211
+ <span class="station st-exec">implement</span><span class="arrow">→</span>
212
+ <span class="station st-land">wrapup</span>
213
+ </div>
214
+ </div>
215
+ <div class="sw-row">
216
+ <div class="sw-label" style="--lc: var(--exec-text); --lbg: var(--exec-bg);">structure line<small>code fights back</small></div>
217
+ <div class="sw-track" style="--lbg: var(--exec-bg);">
218
+ <span class="station plain">change fighting the codebase</span><span class="arrow">→</span>
219
+ <span class="station st-door">improve-codebase-architecture</span><span class="arrow">→</span>
220
+ <span class="station st-plan">to-issues</span>
221
+ <span class="decision">plan a slice — or refactor in place</span>
222
+ </div>
223
+ </div>
224
+ <div class="sw-row">
225
+ <div class="sw-label" style="--lc: var(--learn-text); --lbg: var(--learn-bg);">learn line<small>retro's real output</small></div>
226
+ <div class="sw-track" style="--lbg: var(--learn-bg);">
227
+ <span class="station st-learn">retro</span>
228
+ <span class="decision">user + agent friction, symmetric</span><span class="arrow">→</span>
229
+ <span class="station st-gate">CLAUDE.md rule</span><span class="arrow">/</span>
230
+ <span class="station st-land">spec-self-critique layer · ADR</span><span class="arrow">/</span>
231
+ <span class="station st-learn">memory</span><span class="arrow">/</span>
232
+ <span class="station st-plan">board issue</span>
233
+ <span class="decision">per-patch approval · Meta section in PR body</span>
234
+ </div>
235
+ </div>
236
+
237
+ <div class="inter">
238
+ <div class="i-title">Interchanges</div>
239
+ <div class="i-line">a slice hinging on an unknown inserts <span class="station st-gate">verify-spike / decision-gate</span> before its build</div>
240
+ <div class="i-line">unclear path inserts <span class="station st-exec">prototype</span> — throwaway that answers the design question first</div>
241
+ <div class="i-line">inside implement, <span class="station st-exec">tdd</span> runs red → green → refactor at pre-agreed seams</div>
242
+ <div class="i-line">before every PR, <span class="station st-land">local-ci</span> runs the full gate; the <span class="station st-gate">pre-commit / pre-push hooks</span> fire automatically once installed</div>
243
+ <div class="i-line">lost? <span class="station st-door">ask-matt</span> routes you to the right skill</div>
244
+ <div class="i-line">one-time setup, off-map: <span class="station st-door">setup-workflow</span> <span class="station st-door">git-guardrails</span> <span class="station st-door">setup-pre-commit</span></div>
245
+ </div>
246
+ </div>
247
+
248
+ <footer>
249
+ How waves, programs, and slices relate — the altitude model — lives on the
250
+ <a href="methodology.html">methodology page</a>.
251
+ </footer>
252
+ </div>
253
+ <script>
254
+ // Capture helper: ?theme=dark|light pins the palette for PNG rendering.
255
+ var t = new URLSearchParams(location.search).get("theme");
256
+ if (t === "dark" || t === "light") document.documentElement.dataset.theme = t;
257
+ </script>
258
+ </body>
259
+ </html>
package/docs/workflow.png CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikon85/agent-workflow-kit",
3
- "version": "0.26.1",
3
+ "version": "0.27.0",
4
4
  "description": "Portable AI-agent workflow skills (plan → execute → land → learn) for Claude Code & Codex — grilling, TDD, diagnosis, two-axis code review, cross-model Codex review, design & domain-modeling, plus a skill router (ask-matt). npx init/update/diff/uninstall.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,7 +35,7 @@ from typing import Optional
35
35
  sys.path.insert(0, str(Path(__file__).resolve().parent))
36
36
  from board_config import ( # noqa: E402
37
37
  ConfigError, load_board_config, phase_field_id, program_type_label,
38
- status_roles, STATUS_ROLE_KEYS,
38
+ status_roles, wave_title_prefix, STATUS_ROLE_KEYS,
39
39
  )
40
40
  # stamp-batch / field-value / promote-guards — pure logic, see module docstring.
41
41
  from board_fields import ( # noqa: E402
@@ -414,9 +414,14 @@ def promote_label_args(issue: int, strip: Optional[list[str]] = None) -> list[st
414
414
  return args
415
415
 
416
416
 
417
- # Existing `Welle <N> ` prefix (any number, em-dash/en-dash/hyphen separator) —
418
- # replaced on re-promote so the wave number never doubles up.
419
- _WAVE_PREFIX_RE = re.compile(r"^\s*Welle\s+\d+\s*[—–-]\s*", re.IGNORECASE)
417
+ # The wave-anchor title prefix word comes from the profile (`titles.wavePrefix`,
418
+ # literal default "Welle" see board_config.wave_title_prefix).
419
+ WAVE_TITLE_PREFIX = wave_title_prefix(_CFG)
420
+ # Existing `<prefix> <N> — ` prefix (any number, em-dash/en-dash/hyphen
421
+ # separator) — replaced on re-promote so the wave number never doubles up.
422
+ _WAVE_PREFIX_RE = re.compile(
423
+ rf"^\s*{re.escape(WAVE_TITLE_PREFIX)}\s+\d+\s*[—–-]\s*", re.IGNORECASE
424
+ )
420
425
  # Leading conventional-commit token (`fix:`, `feat(ui):`, `chore!:`) — anchors
421
426
  # don't carry these, so strip it when present. Matches only a lowercase token,
422
427
  # so prose titles like "Supabase-Residuen entfernen: …" are left intact.
@@ -424,18 +429,18 @@ _CONVENTIONAL_PREFIX_RE = re.compile(r"^[a-z]+(\([^)]*\))?!?:\s*")
424
429
 
425
430
 
426
431
  def wave_title(current: str, wave: int) -> str:
427
- """Title for a wave anchor: `Welle <N> — <Thema>`.
432
+ """Title for a wave anchor: `<prefix> <N> — <topic>`.
428
433
 
429
434
  Strips a leading conventional-commit prefix FIRST, then any existing
430
- `Welle X — ` prefix underneath it (idempotent re-promote), then re-prefixes
431
- with the given wave. Order matters: a title like `fix: Welle 7 — X` only
432
- reveals its `Welle 7 — ` prefix once the conventional prefix is gone
433
- stripping Wave first leaves it intact and doubles up into
434
- `Welle 29 — Welle 7 — X`.
435
+ `<prefix> X — ` prefix underneath it (idempotent re-promote), then
436
+ re-prefixes with the given wave. Order matters: a title like
437
+ `fix: <prefix> 7 — X` only reveals its wave prefix once the conventional
438
+ prefix is gone — stripping Wave first leaves it intact and doubles up into
439
+ `<prefix> 29 — <prefix> 7 — X`.
435
440
  """
436
441
  thema = _CONVENTIONAL_PREFIX_RE.sub("", current)
437
442
  thema = _WAVE_PREFIX_RE.sub("", thema).strip()
438
- return f"Welle {wave} — {thema}"
443
+ return f"{WAVE_TITLE_PREFIX} {wave} — {thema}"
439
444
 
440
445
 
441
446
  def title_edit_args(issue: int, title: str) -> list[str]:
@@ -770,7 +775,7 @@ def cmd_promote(args) -> int:
770
775
  if args.dry_run:
771
776
  _print_dry(promote_label_args(args.issue))
772
777
  if rename:
773
- print(f"[dry-run] gh issue edit {args.issue} --title 'Welle {args.wave} — <Thema>'")
778
+ print(f"[dry-run] gh issue edit {args.issue} --title '{WAVE_TITLE_PREFIX} {args.wave} — <topic>'")
774
779
  _add_and_stamp(url, args.wave, args.status, None, None, None, dry_run=True)
775
780
  return 0
776
781
  labels = _issue_type_labels(args.issue)
@@ -1158,7 +1163,7 @@ def build_parser() -> argparse.ArgumentParser:
1158
1163
  help="explicit Wave (read `next-wave` first — no in-promote race)")
1159
1164
  _add_status_flags(pr)
1160
1165
  pr.add_argument("--no-rename", action="store_true",
1161
- help="keep the title as-is (skip the `Welle <N> — ` prefix)")
1166
+ help="keep the title as-is (skip the wave title prefix)")
1162
1167
  pr.add_argument("--dry-run", action="store_true")
1163
1168
  pr.set_defaults(func=cmd_promote)
1164
1169
 
@@ -126,6 +126,13 @@ def program_type_label(cfg: dict) -> str:
126
126
  return cfg.get("labels", {}).get("programType", "type:program")
127
127
 
128
128
 
129
+ def wave_title_prefix(cfg: dict) -> str:
130
+ """The `titles.wavePrefix` word that opens a wave anchor's title
131
+ (`<prefix> <N> — <topic>`), or the literal default `"Welle"` when the
132
+ profile hasn't set one — existing boards keep their titles unchanged."""
133
+ return cfg.get("titles", {}).get("wavePrefix", "Welle")
134
+
135
+
129
136
  # The semantic role keys of the workflow — schema, not
130
137
  # vocabulary; stable once shipped, extend additively, never rename. Single
131
138
  # home so CLI `choices`, docs, and future consumers reference one list.
@@ -45,7 +45,8 @@ async function shipBundle(repoRoot, distDir, bundleFiles) {
45
45
  const content = await copySource(repoRoot, distDir, file.src, file.dest, file.mode);
46
46
  entries.push({
47
47
  path: file.dest, kind: file.kind, ownerSkill: file.ownerSkill,
48
- surface: file.surface, sha256: sha256(content), mode: file.mode, origin: 'kit',
48
+ surface: file.surface, installRole: file.installRole,
49
+ sha256: sha256(content), mode: file.mode, origin: 'kit',
49
50
  });
50
51
  }
51
52
  return entries.sort((a, b) => a.path.localeCompare(b.path));
@@ -1,12 +1,14 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { access, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join, dirname } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { createHash } from 'node:crypto';
8
8
  import { execFileSync } from 'node:child_process';
9
9
  import { buildKit } from './build-kit.mjs';
10
+ import { init } from '../src/commands/init.mjs';
11
+ import { CONSUMER_MANIFEST_NAME, readManifest } from '../src/lib/manifest.mjs';
10
12
 
11
13
  const REPO = join(dirname(fileURLToPath(import.meta.url)), '..');
12
14
  const PUBLIC_CENSUS_UNIT = JSON.parse(await readFile(
@@ -17,6 +19,7 @@ async function withBuild(fn) {
17
19
  try { return await fn(dist, await buildKit({ repoRoot: REPO, distDir: dist })); }
18
20
  finally { await rm(dist, { recursive: true, force: true }); }
19
21
  }
22
+ const exists = (path) => access(path).then(() => true, () => false);
20
23
 
21
24
  test('historical v0.9.0 manifest remains an immutable golden fixture', async () => {
22
25
  const fixture = await readFile(join(REPO, 'test/fixtures/v0.9.0-agent-workflow-kit.package.json'));
@@ -33,6 +36,46 @@ test('current public SSOT builds deterministically', async () => {
33
36
  assert.equal(first, second);
34
37
  });
35
38
 
39
+ test('current build assigns every file an explicit install role', async () => {
40
+ await withBuild(async (dist) => {
41
+ const manifest = JSON.parse(await readFile(join(dist, 'agent-workflow-kit.package.json'), 'utf8'));
42
+ assert.ok(manifest.files.every(({ installRole }) => ['consumer', 'maintainer'].includes(installRole)));
43
+ assert.deepEqual(
44
+ manifest.files.filter(({ installRole }) => installRole === 'maintainer').map(({ path }) => path),
45
+ [
46
+ '.agents/skills/kit-release/SKILL.md',
47
+ '.claude/skills/kit-release/SKILL.md',
48
+ 'scripts/kit-release.mjs',
49
+ 'scripts/release-delta-guard.mjs',
50
+ ],
51
+ );
52
+ });
53
+ });
54
+
55
+ test('a built kit installs no maintainer-only release surface into a consumer', async () => {
56
+ await withBuild(async (dist) => {
57
+ const consumer = await mkdtemp(join(tmpdir(), 'awkit-role-consumer-'));
58
+ try {
59
+ await init({ kitRoot: dist, consumerRoot: consumer });
60
+ const maintainerPaths = [
61
+ '.agents/skills/kit-release/SKILL.md',
62
+ '.claude/skills/kit-release/SKILL.md',
63
+ 'scripts/kit-release.mjs',
64
+ 'scripts/release-delta-guard.mjs',
65
+ ];
66
+ assert.deepEqual(
67
+ await Promise.all(maintainerPaths.map((path) => exists(join(consumer, path)))),
68
+ [false, false, false, false],
69
+ );
70
+ const manifest = await readManifest(join(consumer, CONSUMER_MANIFEST_NAME));
71
+ assert.equal(manifest.installRole, 'consumer');
72
+ assert.ok(manifest.installed.every(({ installRole }) => installRole === 'consumer'));
73
+ } finally {
74
+ await rm(consumer, { recursive: true, force: true });
75
+ }
76
+ });
77
+ });
78
+
36
79
  test('current build contains post-tag public files and repository metadata', async () => {
37
80
  await withBuild(async (dist) => {
38
81
  await readFile(join(dist, '.agents/skills/codex-adapter-sync/SKILL.md'));
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env python3
2
+ """Wave anchor titles take their prefix from the board profile, not a hardcoded
3
+ German literal — an optional `titles.wavePrefix` key with the literal default
4
+ `"Welle"` so every existing profile keeps working unchanged."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import copy
9
+ import importlib.util
10
+ import json
11
+ import os
12
+ import tempfile
13
+ import unittest
14
+ from pathlib import Path
15
+
16
+
17
+ BASE_PROFILE = {
18
+ "repo": "example/kit",
19
+ "project": {"number": 1, "owner": "example", "nodeId": "PVT_test"},
20
+ "fields": {
21
+ "status": {"id": "STATUS", "options": {}},
22
+ "wave": "WAVE",
23
+ "cluster": "CLUSTER",
24
+ "specPath": "SPEC",
25
+ "planPath": "PLAN",
26
+ },
27
+ "labels": {
28
+ "readyForAgent": "ready-for-agent",
29
+ "typePrefix": "type:",
30
+ "clusterType": "type:cluster",
31
+ "waveStub": "wave-stub",
32
+ },
33
+ "branchPrefixes": ["feat", "fix"],
34
+ "prMarkers": {"partOf": "Part of", "retroMarker": "Retro", "retroValues": []},
35
+ "headings": {"vorBau": "Clarify Before Build"},
36
+ }
37
+
38
+
39
+ def load_board_sync(profile_dict: dict, module_name: str):
40
+ with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False) as profile:
41
+ profile.write("<!-- board-sync:profile -->\n```json\n")
42
+ json.dump(profile_dict, profile)
43
+ profile.write("\n```\n")
44
+ profile_path = profile.name
45
+ os.environ["BOARD_SYNC_PROFILE"] = profile_path
46
+ spec = importlib.util.spec_from_file_location(
47
+ module_name, Path(__file__).with_name("board-sync.py")
48
+ )
49
+ module = importlib.util.module_from_spec(spec)
50
+ assert spec.loader is not None
51
+ spec.loader.exec_module(module)
52
+ Path(profile_path).unlink()
53
+ return module
54
+
55
+
56
+ class WaveTitleDefaultPrefix(unittest.TestCase):
57
+ """A profile without `titles.wavePrefix` keeps the historical `Welle` prefix."""
58
+
59
+ @classmethod
60
+ def setUpClass(cls):
61
+ cls.bs = load_board_sync(copy.deepcopy(BASE_PROFILE), "board_sync_wave_default")
62
+
63
+ def test_default_prefix_is_welle(self):
64
+ self.assertEqual(self.bs.wave_title("Auth hardening", 7), "Welle 7 — Auth hardening")
65
+
66
+ def test_repromote_is_idempotent(self):
67
+ self.assertEqual(
68
+ self.bs.wave_title("Welle 7 — Auth hardening", 9),
69
+ "Welle 9 — Auth hardening",
70
+ )
71
+
72
+ def test_conventional_prefix_stripped_before_wave(self):
73
+ self.assertEqual(
74
+ self.bs.wave_title("fix: Welle 7 — Auth hardening", 29),
75
+ "Welle 29 — Auth hardening",
76
+ )
77
+
78
+
79
+ class WaveTitleProfilePrefix(unittest.TestCase):
80
+ """A profile with `titles.wavePrefix` drives the anchor title language."""
81
+
82
+ @classmethod
83
+ def setUpClass(cls):
84
+ profile = copy.deepcopy(BASE_PROFILE)
85
+ profile["titles"] = {"wavePrefix": "Wave"}
86
+ cls.bs = load_board_sync(profile, "board_sync_wave_custom")
87
+
88
+ def test_configured_prefix_is_used(self):
89
+ self.assertEqual(self.bs.wave_title("Auth hardening", 7), "Wave 7 — Auth hardening")
90
+
91
+ def test_repromote_is_idempotent_for_configured_prefix(self):
92
+ self.assertEqual(
93
+ self.bs.wave_title("Wave 7 — Auth hardening", 9),
94
+ "Wave 9 — Auth hardening",
95
+ )
96
+
97
+ def test_regex_metacharacters_in_prefix_are_literal(self):
98
+ profile = copy.deepcopy(BASE_PROFILE)
99
+ profile["titles"] = {"wavePrefix": "W+A"}
100
+ bs = load_board_sync(profile, "board_sync_wave_meta")
101
+ self.assertEqual(
102
+ bs.wave_title("W+A 3 — Auth hardening", 4),
103
+ "W+A 4 — Auth hardening",
104
+ )
105
+
106
+
107
+ if __name__ == "__main__":
108
+ unittest.main()
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ """The shipped pre-commit template must sanitize git's hook environment.
3
+
4
+ Git exports GIT_DIR/GIT_INDEX_FILE to hooks; a consumer gate that runs a test
5
+ suite whose child tests call git in their own temp repos would otherwise
6
+ redirect those calls onto the host repo's index (PR #121's incident)."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import unittest
11
+ from pathlib import Path
12
+
13
+ TEMPLATE = (
14
+ Path(__file__).resolve().parent.parent
15
+ / ".claude/skills/setup-pre-commit/scripts/pre-commit.template.sh"
16
+ )
17
+
18
+
19
+ class PreCommitTemplateSanitizesGitEnv(unittest.TestCase):
20
+ def test_template_unsets_git_env_before_gate(self):
21
+ text = TEMPLATE.read_text(encoding="utf-8")
22
+ unset_pos = text.find("unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX")
23
+ gate_pos = text.find(">>> your gate commands here <<<")
24
+ self.assertGreater(unset_pos, -1, "template must unset git's exported hook env")
25
+ self.assertGreater(gate_pos, -1, "template must keep the gate marker")
26
+ self.assertLess(unset_pos, gate_pos, "sanitize must happen before the gate runs")
27
+
28
+
29
+ if __name__ == "__main__":
30
+ unittest.main()
@@ -6,6 +6,8 @@ import { stubSentinel } from '../lib/sentinel.mjs';
6
6
  import { STUB_TARGETS } from '../lib/bundle.mjs';
7
7
  import {
8
8
  readManifest, writeManifest, emptyConsumerManifest,
9
+ filesForInstallRole, CONSUMER_INSTALL_ROLE,
10
+ indexByPath,
9
11
  PACKAGE_MANIFEST_NAME, CONSUMER_MANIFEST_NAME,
10
12
  } from '../lib/manifest.mjs';
11
13
 
@@ -25,11 +27,19 @@ export async function init({ kitRoot, consumerRoot, force = false }) {
25
27
  if (!pkg) throw new Error('kit package manifest not found');
26
28
  const prior = await readManifest(join(consumerRoot, CONSUMER_MANIFEST_NAME));
27
29
  const tracked = new Set((prior?.installed ?? []).map((e) => e.path));
30
+ const packageIdx = indexByPath(pkg, 'files');
28
31
 
29
32
  const result = { copied: [], skipped: [], seeded: [] };
30
33
  const installed = [];
31
34
 
32
- for (const f of pkg.files) {
35
+ for (const entry of prior?.installed ?? []) {
36
+ const packageEntry = packageIdx.get(entry.path);
37
+ if (packageEntry?.installRole === CONSUMER_INSTALL_ROLE || !packageEntry?.installRole) continue;
38
+ if (!await exists(join(consumerRoot, entry.path))) continue;
39
+ installed.push({ ...entry, installRole: packageEntry.installRole });
40
+ }
41
+
42
+ for (const f of filesForInstallRole(pkg)) {
33
43
  const dest = join(consumerRoot, f.path);
34
44
  if (await exists(dest) && !tracked.has(f.path) && !force) {
35
45
  result.skipped.push(f.path); // pre-existing untracked → never-clobber
@@ -39,6 +49,7 @@ export async function init({ kitRoot, consumerRoot, force = false }) {
39
49
  installed.push({
40
50
  path: f.path, kind: f.kind, ownerSkill: f.ownerSkill, surface: f.surface,
41
51
  installedSha256: await sha256File(dest), origin: 'kit',
52
+ installRole: CONSUMER_INSTALL_ROLE,
42
53
  });
43
54
  result.copied.push(f.path);
44
55
  }
@@ -94,7 +94,8 @@ function verifyRelease(identities, kitVersion) {
94
94
  }
95
95
 
96
96
  function hasUpstreamDelta(result) {
97
- return result.added.length + result.updated.length + result.deleted.length > 0;
97
+ return result.manifestChanged ||
98
+ result.added.length + result.updated.length + result.deleted.length > 0;
98
99
  }
99
100
 
100
101
  async function terminal(result, state, history, transition) {