@warren-dean/code-kit 0.10.0 → 0.11.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +9 -0
- package/README.md +30 -28
- package/bin/code-kit.mjs +90 -2
- package/hooks/guard-bash.mjs +8 -0
- package/hooks/lib/team.mjs +62 -7
- package/hooks/mod/decisions.mjs +3 -0
- package/hooks/mod/panes.mjs +42 -49
- package/hooks/mod/register.mjs +104 -12
- package/hooks/mod/register.test.ts +255 -15
- package/hooks/mod/teamview.mjs +469 -74
- package/hooks/mod/view.mjs +14 -2
- package/package.json +1 -1
- package/skills/init/SKILL.md +2 -1
- package/skills/team-init/SKILL.md +59 -0
package/hooks/mod/teamview.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Teams in the mod (docs/specs/16-teams.md, TEAM-7, TEAM-9), the pure parts: what of the team's
|
|
2
|
-
// needs this person, as band lines, and the Team
|
|
3
|
-
// `code-kit team since --json`; the CLI and the hooks do the rest.
|
|
1
|
+
// Teams in the mod (docs/specs/16-teams.md, TEAM-7, TEAM-9, TEAM-13), the pure parts: what of the team's
|
|
2
|
+
// state needs this person, as band lines, and the Team pane. It reads `code-kit team --json` and
|
|
3
|
+
// `code-kit team since --json`; the CLI and the hooks do the rest. Whatever coordinates the team (GitHub,
|
|
4
|
+
// or a hosted provider) is only named as its state names itself.
|
|
4
5
|
import { globsOverlap, laneOfBranch } from '../lib/teamfacts.mjs';
|
|
5
6
|
|
|
6
7
|
const day = (iso) => {
|
|
@@ -38,6 +39,12 @@ export function teamLines(team, lanes = {}) {
|
|
|
38
39
|
text: "team: the shared repository can't be reached, so claims aren't checked",
|
|
39
40
|
actions: [],
|
|
40
41
|
});
|
|
42
|
+
else if (team.fallback)
|
|
43
|
+
lines.push({
|
|
44
|
+
kind: 'teamDown',
|
|
45
|
+
text: "team: the team's provider can't be reached, so claims are checked through GitHub until it's back",
|
|
46
|
+
actions: [],
|
|
47
|
+
});
|
|
41
48
|
const me = team.me;
|
|
42
49
|
if (!me) return lines;
|
|
43
50
|
const mine = team.claims.filter((c) => c.by === me);
|
|
@@ -83,6 +90,20 @@ export function teamLines(team, lanes = {}) {
|
|
|
83
90
|
: [{ id: 'team', label: 'Team' }],
|
|
84
91
|
});
|
|
85
92
|
}
|
|
93
|
+
// A provider's drift check (the hub's): findings on this person's branches or claimed stories.
|
|
94
|
+
const myBranches = new Set([
|
|
95
|
+
...team.pulls.filter((p) => p.by === me).map((p) => p.branch),
|
|
96
|
+
...(team.sessions ?? []).filter((s) => s.login === me).map((s) => s.branch),
|
|
97
|
+
]);
|
|
98
|
+
for (const f of (team.findings ?? []).filter(
|
|
99
|
+
(x) => x.severity !== 'note' && (x.by === me || myBranches.has(x.branch)),
|
|
100
|
+
))
|
|
101
|
+
lines.push({
|
|
102
|
+
kind: 'teamDrift',
|
|
103
|
+
text: `drift on ${f.branch}: ${f.text}`,
|
|
104
|
+
finding: f,
|
|
105
|
+
actions: [{ id: 'team', label: 'Team' }],
|
|
106
|
+
});
|
|
86
107
|
for (const p of team.pulls.filter((x) => x.by !== me && !x.draft)) {
|
|
87
108
|
const lane = laneOfBranch(p.branch);
|
|
88
109
|
if (lane && team.owners?.[lane]?.includes(me))
|
|
@@ -104,113 +125,386 @@ export function peopleOf(team) {
|
|
|
104
125
|
...team.claims.map((c) => c.by),
|
|
105
126
|
...team.intents.map((i) => i.by),
|
|
106
127
|
...team.pulls.map((p) => p.by),
|
|
128
|
+
...(team.sessions ?? []).map((s) => s.login),
|
|
107
129
|
team.me,
|
|
108
130
|
].filter(Boolean);
|
|
109
131
|
return [...new Set(named)];
|
|
110
132
|
}
|
|
111
133
|
|
|
134
|
+
export const TEAM_ID = 'code-kit-team';
|
|
135
|
+
|
|
136
|
+
/** TEAM-7: the Team pane's tabs. Drift is there only when the provider checks for it. */
|
|
137
|
+
export const TEAM_TABS = [
|
|
138
|
+
{ id: 'people', label: 'People', hotkey: '1' },
|
|
139
|
+
{ id: 'activity', label: 'Since you looked', hotkey: '2' },
|
|
140
|
+
{ id: 'claims', label: 'Claims', hotkey: '3' },
|
|
141
|
+
{ id: 'intents', label: 'Intents', hotkey: '4' },
|
|
142
|
+
{ id: 'drift', label: 'Drift', hotkey: '5' },
|
|
143
|
+
{ id: 'setup', label: 'Setup', hotkey: '6' },
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
export const DEFAULT_TEAM_UI = { tab: 'people', reassign: null, handle: '' };
|
|
147
|
+
|
|
148
|
+
const clock = (iso) =>
|
|
149
|
+
new Date(iso).toLocaleTimeString('en-AU', {
|
|
150
|
+
hour: '2-digit',
|
|
151
|
+
minute: '2-digit',
|
|
152
|
+
timeZone: 'UTC',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
/** What coordinates the team, and how it's doing, in a line: the provider's own name for itself. */
|
|
156
|
+
export function providerLine(team) {
|
|
157
|
+
const p = team.provider ?? null;
|
|
158
|
+
const name = p?.name ?? 'the team provider';
|
|
159
|
+
if (team.reachable === false)
|
|
160
|
+
return {
|
|
161
|
+
text: `${name} can't be reached${team.why ? ` (${team.why})` : ''}: claims and leases aren't checked until it can be.`,
|
|
162
|
+
color: 'yellow',
|
|
163
|
+
};
|
|
164
|
+
if (team.fallback)
|
|
165
|
+
return {
|
|
166
|
+
text: `${name} can't be reached (${team.fallback}): claims are checked through GitHub until it's back.`,
|
|
167
|
+
color: 'yellow',
|
|
168
|
+
};
|
|
169
|
+
const parts = [
|
|
170
|
+
`Through ${name}`,
|
|
171
|
+
p?.live ? 'live' : null,
|
|
172
|
+
p?.account ? `signed in as ${p.account}` : null,
|
|
173
|
+
].filter(Boolean);
|
|
174
|
+
return {
|
|
175
|
+
text: `${parts.join(' · ')}${p?.detail ? `: ${p.detail}` : ''}`,
|
|
176
|
+
color: p?.live ? 'green' : null,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
112
180
|
/**
|
|
113
|
-
* TEAM-6, TEAM-7: the Team
|
|
114
|
-
*
|
|
115
|
-
*
|
|
181
|
+
* TEAM-6, TEAM-7, TEAM-13: the Team pane. `view` is { team, since, config, ui }: `team` is
|
|
182
|
+
* `code-kit team --json`, `since` is `team since --json` (read when that tab opens), `config` the team
|
|
183
|
+
* section and lanes as `code-kit check --json` reports them. `on` acts: onTab(id), onNeed(id, line) as
|
|
184
|
+
* the band's buttons do, onReassign(story | null), onHandle(text), onReassignTo(story, handle) and
|
|
185
|
+
* onSetup() (asks the lead to run the team's init).
|
|
116
186
|
*/
|
|
117
|
-
export function
|
|
187
|
+
export function teamFrame(view, els, on) {
|
|
188
|
+
const { Box, Text, Button, Input } = els;
|
|
118
189
|
const text = (value, style = {}) => Text({ ...style, children: [value] });
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
rows.push(
|
|
123
|
-
text(
|
|
124
|
-
"The shared repository can't be reached: claims and leases aren't checked until it can be.",
|
|
125
|
-
{
|
|
126
|
-
color: 'yellow',
|
|
127
|
-
},
|
|
128
|
-
),
|
|
129
|
-
);
|
|
130
|
-
const sincePeople = Object.entries(since?.people ?? {});
|
|
131
|
-
rows.push(text('Since you last looked', { bold: true }));
|
|
132
|
-
rows.push(
|
|
133
|
-
Box({
|
|
134
|
-
key: 'team-since',
|
|
190
|
+
const { team, since = null, config = null, ui = DEFAULT_TEAM_UI } = view;
|
|
191
|
+
if (!team)
|
|
192
|
+
return Box({
|
|
135
193
|
flexDirection: 'column',
|
|
136
|
-
|
|
137
|
-
|
|
194
|
+
rowGap: 1,
|
|
195
|
+
children: [
|
|
196
|
+
text("The team's state couldn't be read: run code-kit team to see why.", {
|
|
197
|
+
color: 'yellow',
|
|
198
|
+
}),
|
|
199
|
+
Button({
|
|
200
|
+
key: 'team-setup',
|
|
201
|
+
label: 'Set up the team',
|
|
202
|
+
variant: 'primary',
|
|
203
|
+
onPress: () => on.onSetup(),
|
|
204
|
+
}),
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
const me = team.me;
|
|
208
|
+
const lead = team.techLead;
|
|
209
|
+
const drifts = Array.isArray(team.findings);
|
|
210
|
+
const tabs = TEAM_TABS.filter((t) => t.id !== 'drift' || drifts);
|
|
211
|
+
const tab = tabs.some((t) => t.id === ui.tab) ? ui.tab : 'people';
|
|
212
|
+
const myLanes = Object.entries(team.owners ?? {})
|
|
213
|
+
.filter(([, people]) => people.includes(me))
|
|
214
|
+
.map(([lane]) => lane);
|
|
215
|
+
const provider = providerLine(team);
|
|
216
|
+
const setup = team.setup ?? [];
|
|
217
|
+
const header = Box({
|
|
218
|
+
key: 'team-header',
|
|
219
|
+
flexDirection: 'column',
|
|
220
|
+
children: [
|
|
221
|
+
Box({
|
|
222
|
+
flexDirection: 'row',
|
|
223
|
+
columnGap: 2,
|
|
224
|
+
flexWrap: 'wrap',
|
|
225
|
+
children: [
|
|
226
|
+
text(`tech lead ${lead}`, { bold: true }),
|
|
227
|
+
text(
|
|
228
|
+
me
|
|
229
|
+
? `you are ${me}${me === lead ? ', the tech lead' : ''}${myLanes.length ? ` · own ${myLanes.join(', ')}` : ''}`
|
|
230
|
+
: "you aren't signed in to GitHub (gh auth login)",
|
|
231
|
+
me ? { dimColor: true } : { color: 'yellow' },
|
|
232
|
+
),
|
|
233
|
+
text(
|
|
234
|
+
`${team.claims.length} claimed · ${team.intents.length} intent${team.intents.length === 1 ? '' : 's'} open · ${team.pulls.length} pull request${team.pulls.length === 1 ? '' : 's'}`,
|
|
235
|
+
{ dimColor: true },
|
|
236
|
+
),
|
|
237
|
+
],
|
|
238
|
+
}),
|
|
239
|
+
Box({
|
|
240
|
+
key: 'team-provider',
|
|
241
|
+
children: [
|
|
242
|
+
text(provider.text, {
|
|
243
|
+
wrap: 'wrap',
|
|
244
|
+
...(provider.color ? { color: provider.color } : { dimColor: true }),
|
|
245
|
+
}),
|
|
246
|
+
],
|
|
247
|
+
}),
|
|
248
|
+
...(setup.length
|
|
249
|
+
? [
|
|
138
250
|
Box({
|
|
139
|
-
key:
|
|
251
|
+
key: 'team-setup-count',
|
|
140
252
|
children: [
|
|
141
253
|
text(
|
|
142
|
-
|
|
143
|
-
{
|
|
144
|
-
wrap: 'wrap',
|
|
145
|
-
},
|
|
254
|
+
`${setup.length} thing${setup.length === 1 ? '' : 's'} to set up: see Setup (6).`,
|
|
255
|
+
{ color: 'yellow' },
|
|
146
256
|
),
|
|
147
257
|
],
|
|
148
258
|
}),
|
|
149
|
-
|
|
150
|
-
: [
|
|
151
|
-
|
|
152
|
-
);
|
|
153
|
-
|
|
259
|
+
]
|
|
260
|
+
: []),
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
const tabRow = Box({
|
|
264
|
+
key: 'team-tabs',
|
|
265
|
+
flexDirection: 'row',
|
|
266
|
+
columnGap: 1,
|
|
267
|
+
children: tabs.map((t) =>
|
|
268
|
+
Button({
|
|
269
|
+
key: `team-tab-${t.id}`,
|
|
270
|
+
label: t.label,
|
|
271
|
+
hotkey: t.hotkey,
|
|
272
|
+
...(tab === t.id ? { variant: 'primary' } : { dimColor: true }),
|
|
273
|
+
onPress: () => on.onTab(t.id),
|
|
274
|
+
}),
|
|
275
|
+
),
|
|
276
|
+
});
|
|
277
|
+
const body =
|
|
278
|
+
tab === 'activity'
|
|
279
|
+
? activityTab(team, since, els)
|
|
280
|
+
: tab === 'claims'
|
|
281
|
+
? claimsTab(team, ui, els, on)
|
|
282
|
+
: tab === 'intents'
|
|
283
|
+
? intentsTab(team, els, on)
|
|
284
|
+
: tab === 'drift'
|
|
285
|
+
? driftTab(team, els)
|
|
286
|
+
: tab === 'setup'
|
|
287
|
+
? setupTab(team, config, els, on)
|
|
288
|
+
: peopleTab(team, els, on);
|
|
289
|
+
return Box({ flexDirection: 'column', rowGap: 1, children: [header, tabRow, ...body] });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Each person, the tech lead first: where they're at work, their claims, pull requests and intents. */
|
|
293
|
+
function peopleTab(team, { Box, Text }, on) {
|
|
294
|
+
const text = (value, style = {}) => Text({ ...style, children: [value] });
|
|
295
|
+
return peopleOf(team).map((who) => {
|
|
154
296
|
const lanes = Object.entries(team.owners ?? {})
|
|
155
297
|
.filter(([, people]) => people.includes(who))
|
|
156
298
|
.map(([lane]) => lane);
|
|
157
|
-
const claims = team.claims.filter((c) => c.by === who);
|
|
158
|
-
const pulls = team.pulls.filter((p) => p.by === who);
|
|
159
|
-
const intents = team.intents.filter((i) => i.by === who);
|
|
160
299
|
const role = [
|
|
161
300
|
who === team.techLead ? 'tech lead' : null,
|
|
162
301
|
who === team.me ? 'you' : null,
|
|
163
302
|
lanes.length ? `owns ${lanes.join(', ')}` : null,
|
|
164
303
|
].filter(Boolean);
|
|
304
|
+
const line = (key, value, style = {}) =>
|
|
305
|
+
Box({ key, children: [text(` ${value}`, { wrap: 'truncate-end', ...style })] });
|
|
165
306
|
const items = [
|
|
166
|
-
|
|
307
|
+
// A live provider's presence: where each of their sessions is, and what its agents are doing.
|
|
308
|
+
...(team.sessions ?? [])
|
|
309
|
+
.filter((x) => x.login === who)
|
|
310
|
+
.map((x, n) =>
|
|
311
|
+
line(
|
|
312
|
+
`team-session-${who}-${n}`,
|
|
313
|
+
x.away
|
|
314
|
+
? `away (last seen ${clock(x.seen)})`
|
|
315
|
+
: `at work on ${x.branch ?? 'no branch'}${x.agents.length ? `: ${x.agents.map((a) => `${a.type ?? 'an agent'}${a.story ? ` on ${a.story}` : ''}${a.tool ? ` (${a.tool})` : ''}`).join(', ')}` : ''}`,
|
|
316
|
+
x.away ? { dimColor: true } : { color: 'green' },
|
|
317
|
+
),
|
|
318
|
+
),
|
|
319
|
+
...(team.findings ?? [])
|
|
320
|
+
.filter((f) => f.by === who)
|
|
321
|
+
.map((f) =>
|
|
322
|
+
line(`team-finding-${f.id}`, `drift on ${f.branch} (${f.severity}): ${f.text}`, {
|
|
323
|
+
wrap: 'wrap',
|
|
324
|
+
color: f.severity === 'blocker' ? 'red' : f.severity === 'note' ? undefined : 'yellow',
|
|
325
|
+
}),
|
|
326
|
+
),
|
|
327
|
+
...team.claims
|
|
328
|
+
.filter((c) => c.by === who)
|
|
329
|
+
.map((c) =>
|
|
330
|
+
line(
|
|
331
|
+
`team-claim-${c.story}`,
|
|
332
|
+
`${c.story}${c.stale ? ` · stale since ${day(c.active)}` : ''}${c.lease.length ? ` · ${c.lease.join(', ')}` : ''}`,
|
|
333
|
+
c.stale ? { color: 'yellow' } : {},
|
|
334
|
+
),
|
|
335
|
+
),
|
|
336
|
+
...team.pulls
|
|
337
|
+
.filter((p) => p.by === who)
|
|
338
|
+
.map((p) =>
|
|
339
|
+
line(
|
|
340
|
+
`team-pull-${p.number}`,
|
|
341
|
+
`#${p.number} ${p.title} (${p.branch}${p.draft ? ', draft' : ''})`,
|
|
342
|
+
),
|
|
343
|
+
),
|
|
344
|
+
...team.intents
|
|
345
|
+
.filter((i) => i.by === who)
|
|
346
|
+
.map((i) =>
|
|
347
|
+
line(
|
|
348
|
+
`team-intent-${i.id}`,
|
|
349
|
+
`intends: ${i.about}${i.touches.length ? ` · ${i.touches.join(', ')}` : ''}`,
|
|
350
|
+
{
|
|
351
|
+
color: 'cyan',
|
|
352
|
+
},
|
|
353
|
+
),
|
|
354
|
+
),
|
|
355
|
+
];
|
|
356
|
+
return Box({
|
|
357
|
+
key: `team-person-${who}`,
|
|
358
|
+
flexDirection: 'column',
|
|
359
|
+
children: [
|
|
360
|
+
text(`${who}${role.length ? ` · ${role.join(' · ')}` : ''}`, { bold: true }),
|
|
361
|
+
...(items.length ? items : [text(' nothing open', { dimColor: true })]),
|
|
362
|
+
],
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** TEAM-6: what everyone did since you last looked; opening the tab marks it read. */
|
|
368
|
+
function activityTab(team, since, { Box, Text }) {
|
|
369
|
+
const text = (value, style = {}) => Text({ ...style, children: [value] });
|
|
370
|
+
const people = Object.entries(since?.people ?? {});
|
|
371
|
+
return [
|
|
372
|
+
Box({
|
|
373
|
+
key: 'team-since',
|
|
374
|
+
flexDirection: 'column',
|
|
375
|
+
children: [
|
|
376
|
+
text(
|
|
377
|
+
since?.since
|
|
378
|
+
? `Since ${new Date(since.since).toLocaleString('en-AU', { timeZone: 'UTC' })}`
|
|
379
|
+
: 'Since you last looked',
|
|
380
|
+
{ bold: true },
|
|
381
|
+
),
|
|
382
|
+
...(since?.github === false
|
|
383
|
+
? [text("GitHub's activity couldn't be read: claims only.", { color: 'yellow' })]
|
|
384
|
+
: []),
|
|
385
|
+
...(people.length
|
|
386
|
+
? people.map(([who, events]) =>
|
|
387
|
+
Box({
|
|
388
|
+
key: `since-${who}`,
|
|
389
|
+
flexDirection: 'column',
|
|
390
|
+
children: [
|
|
391
|
+
text(`${who}${who === team.me ? ' (you)' : ''}`, { bold: true }),
|
|
392
|
+
...events.map((e, n) =>
|
|
393
|
+
Box({
|
|
394
|
+
key: `since-${who}-${n}`,
|
|
395
|
+
children: [text(` ${e.text}`, { wrap: 'wrap' })],
|
|
396
|
+
}),
|
|
397
|
+
),
|
|
398
|
+
],
|
|
399
|
+
}),
|
|
400
|
+
)
|
|
401
|
+
: [text('Nothing new.', { dimColor: true })]),
|
|
402
|
+
],
|
|
403
|
+
}),
|
|
404
|
+
];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* TEAM-9: every claim, with Keep and Release on yours, and for the tech lead Release and Reassign on
|
|
409
|
+
* anyone's. Reassign asks for the new holder's handle.
|
|
410
|
+
*/
|
|
411
|
+
function claimsTab(team, ui, { Box, Text, Button, Input }, on) {
|
|
412
|
+
const text = (value, style = {}) => Text({ ...style, children: [value] });
|
|
413
|
+
if (!team.claims.length) return [text('Nothing is claimed.', { dimColor: true })];
|
|
414
|
+
const leading = team.me && team.me === team.techLead;
|
|
415
|
+
return team.claims.map((c) => {
|
|
416
|
+
const mine = c.by === team.me;
|
|
417
|
+
const line = { kind: 'teamStale', claim: c };
|
|
418
|
+
return Box({
|
|
419
|
+
key: `team-claims-${c.story}`,
|
|
420
|
+
flexDirection: 'column',
|
|
421
|
+
children: [
|
|
167
422
|
Box({
|
|
168
|
-
key: `team-claim-${c.story}`,
|
|
169
423
|
flexDirection: 'row',
|
|
170
424
|
columnGap: 1,
|
|
171
425
|
children: [
|
|
426
|
+
text(c.story, { bold: true }),
|
|
427
|
+
text(`${c.by}${mine ? ' (you)' : ''}`),
|
|
172
428
|
text(
|
|
173
|
-
|
|
174
|
-
|
|
429
|
+
c.stale ? `stale since ${day(c.active)}` : `since ${day(c.at)}`,
|
|
430
|
+
c.stale ? { color: 'yellow' } : { dimColor: true },
|
|
175
431
|
),
|
|
176
|
-
...(
|
|
432
|
+
...(mine
|
|
177
433
|
? [
|
|
178
434
|
Button({
|
|
179
435
|
key: `team-keep-${c.story}`,
|
|
180
436
|
label: 'Keep',
|
|
181
|
-
onPress: () => on.onNeed('keepClaim',
|
|
437
|
+
onPress: () => on.onNeed('keepClaim', line),
|
|
182
438
|
}),
|
|
439
|
+
]
|
|
440
|
+
: []),
|
|
441
|
+
...(mine || leading
|
|
442
|
+
? [
|
|
183
443
|
Button({
|
|
184
444
|
key: `team-release-${c.story}`,
|
|
185
445
|
label: 'Release',
|
|
186
|
-
onPress: () => on.onNeed('releaseClaim',
|
|
446
|
+
onPress: () => on.onNeed('releaseClaim', line),
|
|
447
|
+
}),
|
|
448
|
+
]
|
|
449
|
+
: []),
|
|
450
|
+
...(leading
|
|
451
|
+
? [
|
|
452
|
+
Button({
|
|
453
|
+
key: `team-reassign-${c.story}`,
|
|
454
|
+
label: 'Reassign',
|
|
455
|
+
onPress: () => on.onReassign(ui.reassign === c.story ? null : c.story),
|
|
187
456
|
}),
|
|
188
457
|
]
|
|
189
458
|
: []),
|
|
190
459
|
],
|
|
191
460
|
}),
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
key: `team-pull-${p.number}`,
|
|
196
|
-
children: [
|
|
197
|
-
text(` #${p.number} ${p.title} (${p.branch}${p.draft ? ', draft' : ''})`, {
|
|
198
|
-
wrap: 'truncate-end',
|
|
199
|
-
}),
|
|
200
|
-
],
|
|
461
|
+
text(` ${c.lease.length ? `leases ${c.lease.join(', ')}` : 'no lease'}`, {
|
|
462
|
+
dimColor: true,
|
|
463
|
+
wrap: 'truncate-end',
|
|
201
464
|
}),
|
|
465
|
+
...(leading && ui.reassign === c.story
|
|
466
|
+
? [
|
|
467
|
+
Input({
|
|
468
|
+
key: `team-reassign-to-${c.story}`,
|
|
469
|
+
label: `Give ${c.story} to`,
|
|
470
|
+
value: ui.handle ?? '',
|
|
471
|
+
placeholder: 'their GitHub handle',
|
|
472
|
+
submitLabel: 'Reassign',
|
|
473
|
+
autoFocus: true,
|
|
474
|
+
onInput: (v) => on.onHandle(v),
|
|
475
|
+
onSubmit: (v) => on.onReassignTo(c.story, v),
|
|
476
|
+
}),
|
|
477
|
+
]
|
|
478
|
+
: []),
|
|
479
|
+
],
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** TEAM-5: the open intents, with Agree and Drop for the tech lead. */
|
|
485
|
+
function intentsTab(team, { Box, Text, Button }, on) {
|
|
486
|
+
const text = (value, style = {}) => Text({ ...style, children: [value] });
|
|
487
|
+
if (!team.intents.length)
|
|
488
|
+
return [
|
|
489
|
+
text(
|
|
490
|
+
'No open intents. An agent raises one (code-kit intend) before changing what others rely on.',
|
|
491
|
+
{ dimColor: true },
|
|
202
492
|
),
|
|
203
|
-
|
|
493
|
+
];
|
|
494
|
+
const leading = team.me && team.me === team.techLead;
|
|
495
|
+
return team.intents.map((i) =>
|
|
496
|
+
Box({
|
|
497
|
+
key: `team-intents-${i.id}`,
|
|
498
|
+
flexDirection: 'column',
|
|
499
|
+
children: [
|
|
204
500
|
Box({
|
|
205
|
-
key: `team-intent-${i.id}`,
|
|
206
501
|
flexDirection: 'row',
|
|
207
502
|
columnGap: 1,
|
|
208
503
|
children: [
|
|
209
|
-
text(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
...(team.me === team.techLead && i.by !== team.me
|
|
504
|
+
text(`#${i.id}`, { bold: true }),
|
|
505
|
+
text(i.about, { color: 'cyan', wrap: 'truncate-end' }),
|
|
506
|
+
text(`${i.by}${i.by === team.me ? ' (you)' : ''}`, { dimColor: true }),
|
|
507
|
+
...(leading && i.by !== team.me
|
|
214
508
|
? [
|
|
215
509
|
Button({
|
|
216
510
|
key: `team-agree-${i.id}`,
|
|
@@ -226,18 +520,119 @@ export function teamTab(team, since, { Box, Text, Button }, on) {
|
|
|
226
520
|
: []),
|
|
227
521
|
],
|
|
228
522
|
}),
|
|
523
|
+
text(
|
|
524
|
+
` ${i.touches.length ? `touches ${i.touches.join(', ')}` : 'touches nothing named'}${i.url ? ` · ${i.url}` : ''}`,
|
|
525
|
+
{
|
|
526
|
+
dimColor: true,
|
|
527
|
+
wrap: 'truncate-end',
|
|
528
|
+
},
|
|
529
|
+
),
|
|
530
|
+
],
|
|
531
|
+
}),
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** A provider's drift checks (the hub's): each branch's findings, worst first. */
|
|
536
|
+
function driftTab(team, { Box, Text }) {
|
|
537
|
+
const text = (value, style = {}) => Text({ ...style, children: [value] });
|
|
538
|
+
const rank = { blocker: 0, concern: 1, note: 2 };
|
|
539
|
+
const findings = [...team.findings].sort((a, b) => rank[a.severity] - rank[b.severity]);
|
|
540
|
+
if (!findings.length)
|
|
541
|
+
return [text('No drift found on the branches at work.', { dimColor: true })];
|
|
542
|
+
const branches = [...new Set(findings.map((f) => f.branch))];
|
|
543
|
+
return branches.map((branch) =>
|
|
544
|
+
Box({
|
|
545
|
+
key: `team-drift-${branch}`,
|
|
546
|
+
flexDirection: 'column',
|
|
547
|
+
children: [
|
|
548
|
+
text(
|
|
549
|
+
`${branch}${findings.find((f) => f.branch === branch)?.by ? ` · ${findings.find((f) => f.branch === branch).by}` : ''}`,
|
|
550
|
+
{ bold: true },
|
|
551
|
+
),
|
|
552
|
+
...findings
|
|
553
|
+
.filter((f) => f.branch === branch)
|
|
554
|
+
.map((f) =>
|
|
555
|
+
Box({
|
|
556
|
+
key: `team-drift-${f.id}`,
|
|
557
|
+
children: [
|
|
558
|
+
text(` ${f.severity} · ${f.conflictsWith}: ${f.text}`, {
|
|
559
|
+
wrap: 'wrap',
|
|
560
|
+
color:
|
|
561
|
+
f.severity === 'blocker'
|
|
562
|
+
? 'red'
|
|
563
|
+
: f.severity === 'concern'
|
|
564
|
+
? 'yellow'
|
|
565
|
+
: undefined,
|
|
566
|
+
dimColor: f.severity === 'note',
|
|
567
|
+
}),
|
|
568
|
+
],
|
|
569
|
+
}),
|
|
570
|
+
),
|
|
571
|
+
],
|
|
572
|
+
}),
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** TEAM-13: the team's setup as the config has it, what's still missing, and a way to change it. */
|
|
577
|
+
function setupTab(team, config, { Box, Text, Button }, on) {
|
|
578
|
+
const text = (value, style = {}) => Text({ ...style, children: [value] });
|
|
579
|
+
const t = config ?? {};
|
|
580
|
+
const owners = Object.entries(team.owners ?? {});
|
|
581
|
+
const unowned = Object.keys(config?.lanes ?? {}).filter((l) => !(team.owners ?? {})[l]);
|
|
582
|
+
const rows = [
|
|
583
|
+
['Tech lead', team.techLead],
|
|
584
|
+
['Stale after', `${team.staleDays ?? t.staleDays ?? 3} working days without a push`],
|
|
585
|
+
['Intents', `discussions in "${t.intents ?? 'Intents'}"`],
|
|
586
|
+
[
|
|
587
|
+
'Provider',
|
|
588
|
+
team.provider?.name ?? (typeof t.provider === 'object' ? t.provider.command : 'GitHub'),
|
|
589
|
+
],
|
|
590
|
+
...(typeof t.provider === 'object' && t.provider.fallback
|
|
591
|
+
? [['Fallback', 'GitHub, while the provider is down']]
|
|
592
|
+
: []),
|
|
593
|
+
...(t.board ? [['Board', `${t.board.owner}/${t.board.number}`]] : []),
|
|
594
|
+
...owners.map(([lane, people]) => [`Lane ${lane}`, people.join(', ')]),
|
|
595
|
+
...(unowned.length
|
|
596
|
+
? [['Unowned lanes', `${unowned.join(', ')}: whoever claims a story first`]]
|
|
597
|
+
: []),
|
|
598
|
+
];
|
|
599
|
+
const setup = team.setup ?? [];
|
|
600
|
+
return [
|
|
601
|
+
Box({
|
|
602
|
+
key: 'team-config',
|
|
603
|
+
flexDirection: 'column',
|
|
604
|
+
children: rows.map(([k, v]) =>
|
|
605
|
+
Box({
|
|
606
|
+
key: `team-config-${k}`,
|
|
607
|
+
flexDirection: 'row',
|
|
608
|
+
columnGap: 1,
|
|
609
|
+
children: [text(k.padEnd(14), { dimColor: true }), text(v, { wrap: 'truncate-end' })],
|
|
610
|
+
}),
|
|
229
611
|
),
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
612
|
+
}),
|
|
613
|
+
Box({
|
|
614
|
+
key: 'team-todo',
|
|
615
|
+
flexDirection: 'column',
|
|
616
|
+
children: setup.length
|
|
617
|
+
? [
|
|
618
|
+
text('Still to set up', { bold: true }),
|
|
619
|
+
...setup.map((s, n) =>
|
|
620
|
+
Box({
|
|
621
|
+
key: `team-todo-${n}`,
|
|
622
|
+
children: [text(` ${s}`, { wrap: 'wrap', color: 'yellow' })],
|
|
623
|
+
}),
|
|
624
|
+
),
|
|
625
|
+
]
|
|
626
|
+
: [
|
|
627
|
+
text('Nothing missing: claims and intents work for everyone signed in.', {
|
|
628
|
+
color: 'green',
|
|
629
|
+
}),
|
|
630
|
+
],
|
|
631
|
+
}),
|
|
632
|
+
Button({
|
|
633
|
+
key: 'team-setup',
|
|
634
|
+
label: 'Change the team setup',
|
|
635
|
+
onPress: () => on.onSetup(),
|
|
636
|
+
}),
|
|
637
|
+
];
|
|
243
638
|
}
|
package/hooks/mod/view.mjs
CHANGED
|
@@ -856,10 +856,22 @@ export function band(lines, { Box, Text, Button }, onAction) {
|
|
|
856
856
|
};
|
|
857
857
|
const LIVE = { quiet: 'yellow', stalled: 'red' };
|
|
858
858
|
const LABEL = 8;
|
|
859
|
+
// Two lines can share a kind or an action (two drift findings, each with Team): the second and later
|
|
860
|
+
// get a number, so every row and button keeps its own key.
|
|
861
|
+
const counter = () => {
|
|
862
|
+
const seen = new Map();
|
|
863
|
+
return (key) => {
|
|
864
|
+
const n = (seen.get(key) ?? 0) + 1;
|
|
865
|
+
seen.set(key, n);
|
|
866
|
+
return n === 1 ? key : `${key}-${n}`;
|
|
867
|
+
};
|
|
868
|
+
};
|
|
869
|
+
const uniqueButton = counter();
|
|
870
|
+
const uniqueRow = counter();
|
|
859
871
|
const buttons = (line) =>
|
|
860
872
|
line.actions.map((a) =>
|
|
861
873
|
Button({
|
|
862
|
-
key: `band-${a.id}
|
|
874
|
+
key: uniqueButton(`band-${a.id}`),
|
|
863
875
|
label: a.hotkey ? `${a.hotkey} ${a.label}` : a.label,
|
|
864
876
|
hotkey: a.hotkey,
|
|
865
877
|
onPress: () => onAction(a.id, line),
|
|
@@ -900,7 +912,7 @@ export function band(lines, { Box, Text, Button }, onAction) {
|
|
|
900
912
|
const label = line.mission ? line.label : note ? 'note' : saidYou ? '' : 'you';
|
|
901
913
|
if (!line.mission && !note) saidYou = true;
|
|
902
914
|
return Box({
|
|
903
|
-
key: `band-${line.kind}
|
|
915
|
+
key: uniqueRow(`band-${line.kind}`),
|
|
904
916
|
flexDirection: 'row',
|
|
905
917
|
columnGap: 1,
|
|
906
918
|
children: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warren-dean/code-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Keeps AI coding agents inside your project's rules: lanes, layer rules, protected paths and proof before finish, enforced by Claude Code hooks and in CI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Warren Langeveldt",
|
package/skills/init/SKILL.md
CHANGED
|
@@ -138,7 +138,8 @@ Once they approve (with any changes):
|
|
|
138
138
|
- tell the person to make the `code-kit` check required on the protected branches.
|
|
139
139
|
7. Run `check` and `unowned` again, without `--config`.
|
|
140
140
|
8. Commit the setup straight onto the base branch (usually `main`), not a branch: while the base has no `.claude/code-kit.json` committed, the hooks let the lead make that one commit, so the rules the person just approved are in force on `main` and the lead can merge reviewed work from the start, with no first merge for anyone to make. The kit records the protected files in `.claude/approval-log.jsonl`. If the base already has rules (an update, section 4), commit on a branch as usual.
|
|
141
|
-
9.
|
|
141
|
+
9. **A team, only if there is one.** Not every developer works in a team. Ask once: "Do other developers work in this repository with their own Claude Code sessions?" If yes, run `/code-kit:team-init` now (it asks who the tech lead is and who owns which lanes, and writes the `team` section). If no, or not yet, leave it out and say `/code-kit:team-init` sets it up any time.
|
|
142
|
+
10. Set up each companion the config now wants (`"adapters": { "<name>": true }`) by running its init skill, for example `/context-graph:init`, which asks nothing. Do it before reporting, so it's in place before the coding loop starts; `code-kit next` also gives `setup` until it is.
|
|
142
143
|
|
|
143
144
|
Report what was set up, what's enforced from now on, how many existing violations the baseline holds, and the open questions. Say who does what from here: the person owns the intent and the rules; the lead owns delivery, so it dispatches, reviews, sends back and merges, and approves within the delegated limits, and only a real decision (scope, something outside those limits, a problem it couldn't solve) comes to the person.
|
|
144
145
|
|