@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.
- package/.agents/skills/ask-matt/SKILL.md +1 -2
- package/.agents/skills/board-to-waves/SKILL.md +2 -2
- package/.agents/skills/improve-codebase-architecture/SKILL.md +0 -2
- package/.agents/skills/setup-workflow/board-sync.md +11 -0
- package/.claude/skills/ask-matt/SKILL.md +1 -2
- package/.claude/skills/board-to-waves/SKILL.md +2 -2
- package/.claude/skills/improve-codebase-architecture/SKILL.md +0 -2
- package/.claude/skills/setup-pre-commit/SKILL.md +3 -0
- package/.claude/skills/setup-pre-commit/scripts/pre-commit.template.sh +6 -0
- package/.claude/skills/setup-workflow/board-sync.md +11 -0
- package/.claude/skills/skill-manifest.json +1 -9
- package/PROVENANCE.md +5 -4
- package/README.md +39 -17
- package/agent-workflow-kit.package.json +297 -49
- package/docs/agents/board-sync.md +3 -0
- package/docs/workflow.html +259 -0
- package/docs/workflow.png +0 -0
- package/package.json +1 -1
- package/scripts/board-sync.py +18 -13
- package/scripts/board_config.py +7 -0
- package/scripts/build-kit.mjs +2 -1
- package/scripts/build-kit.test.mjs +44 -1
- package/scripts/test_board_sync_wave_title.py +108 -0
- package/scripts/test_skill_precommit_template.py +30 -0
- package/src/commands/init.mjs +12 -1
- package/src/commands/update.mjs +2 -1
- package/src/lib/bundle.mjs +12 -6
- package/src/lib/manifest.mjs +13 -3
- package/src/lib/updateReconcile.mjs +22 -8
- package/.agents/skills/zoom-out/SKILL.md +0 -7
- package/.agents/skills/zoom-out/THIRD-PARTY-NOTICES.md +0 -29
- package/.claude/skills/zoom-out/SKILL.md +0 -7
- package/.claude/skills/zoom-out/THIRD-PARTY-NOTICES.md +0 -29
|
@@ -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.
|
|
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": {
|
package/scripts/board-sync.py
CHANGED
|
@@ -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
|
-
#
|
|
418
|
-
#
|
|
419
|
-
|
|
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:
|
|
432
|
+
"""Title for a wave anchor: `<prefix> <N> — <topic>`.
|
|
428
433
|
|
|
429
434
|
Strips a leading conventional-commit prefix FIRST, then any existing
|
|
430
|
-
|
|
431
|
-
with the given wave. Order matters: a title like
|
|
432
|
-
|
|
433
|
-
stripping Wave first leaves it intact and doubles up into
|
|
434
|
-
|
|
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"
|
|
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 '
|
|
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
|
|
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
|
|
package/scripts/board_config.py
CHANGED
|
@@ -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.
|
package/scripts/build-kit.mjs
CHANGED
|
@@ -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,
|
|
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()
|
package/src/commands/init.mjs
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/commands/update.mjs
CHANGED
|
@@ -94,7 +94,8 @@ function verifyRelease(identities, kitVersion) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
function hasUpstreamDelta(result) {
|
|
97
|
-
return result.
|
|
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) {
|