@warren-dean/code-kit 0.10.0 → 0.11.1
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 +18 -0
- package/README.md +33 -31
- package/bin/code-kit.mjs +99 -3
- package/hooks/guard-bash.mjs +8 -0
- package/hooks/lib/rules.mjs +214 -4
- package/hooks/lib/team.mjs +62 -7
- package/hooks/mod/decisions.mjs +5 -1
- package/hooks/mod/loop.mjs +49 -0
- package/hooks/mod/panes.mjs +42 -49
- package/hooks/mod/register.mjs +122 -14
- package/hooks/mod/register.test.ts +283 -15
- package/hooks/mod/teamview.mjs +469 -74
- package/hooks/mod/view.mjs +59 -11
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +1 -1
- package/skills/init/SKILL.md +2 -1
- package/skills/review/SKILL.md +1 -1
- package/skills/team-init/SKILL.md +59 -0
package/hooks/mod/register.mjs
CHANGED
|
@@ -45,9 +45,19 @@ import {
|
|
|
45
45
|
sendBackPane,
|
|
46
46
|
toolLine,
|
|
47
47
|
} from './panes.mjs';
|
|
48
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
leadPrompt,
|
|
50
|
+
nudgeText,
|
|
51
|
+
restartPrompt,
|
|
52
|
+
stallDue,
|
|
53
|
+
stepKey,
|
|
54
|
+
stepLabel,
|
|
55
|
+
storiesInFlight,
|
|
56
|
+
withoutInFlight,
|
|
57
|
+
} from './loop.mjs';
|
|
49
58
|
import { goalOf, nextText } from './mission.mjs';
|
|
50
59
|
import { contextOf, decisionCards } from './decisions.mjs';
|
|
60
|
+
import { DEFAULT_TEAM_UI, TEAM_ID, teamFrame } from './teamview.mjs';
|
|
51
61
|
import { glowOf, mapStoryOf, touchOf } from './codemap.mjs';
|
|
52
62
|
import { harnessSettings } from '../lib/harness.mjs';
|
|
53
63
|
import {
|
|
@@ -67,7 +77,8 @@ let reviewTurn = null; // 'next' until the lead's review turn starts, then its i
|
|
|
67
77
|
let approving = null; // the open Approve… confirmation: { request, reason, error }
|
|
68
78
|
let result = null; // what the last merge reported
|
|
69
79
|
let notice = null; // why the person's last act failed
|
|
70
|
-
let teamSince = null; // TEAM-6: since you last looked, as read when the Team tab opened
|
|
80
|
+
let teamSince = null; // TEAM-6: since you last looked, as read when the Team pane's tab opened
|
|
81
|
+
let teamUi = { ...DEFAULT_TEAM_UI }; // TEAM-7: the Team pane's own state
|
|
71
82
|
let boardSynced = -Infinity; // TEAM-12: when the board was last synced
|
|
72
83
|
let projectRead = -Infinity; // MAP-5: when the whole project's map was last read
|
|
73
84
|
const BOARD_EVERY_MS = 10 * 60000;
|
|
@@ -176,6 +187,14 @@ export function register(on) {
|
|
|
176
187
|
immediate: true,
|
|
177
188
|
})
|
|
178
189
|
.catch(unavailable('harness'));
|
|
190
|
+
await $.command
|
|
191
|
+
.register({
|
|
192
|
+
name: 'team',
|
|
193
|
+
description:
|
|
194
|
+
"code-kit's team: who's at work on what, claims, intents, drift and the team's setup",
|
|
195
|
+
immediate: true,
|
|
196
|
+
})
|
|
197
|
+
.catch(unavailable('team'));
|
|
179
198
|
rateLimits = (await $.session.usage().catch(() => null))?.rateLimits ?? [];
|
|
180
199
|
const cli = `${$.plugin.root}/bin/code-kit.mjs`;
|
|
181
200
|
const cwd = e.cwd ?? (await $.session.cwd());
|
|
@@ -267,8 +286,8 @@ export function register(on) {
|
|
|
267
286
|
}
|
|
268
287
|
}
|
|
269
288
|
if (paneUi.tab === 'map') trailData = (await json('trail')) ?? trailData;
|
|
270
|
-
// TEAM-6: since you last looked, read once each time
|
|
271
|
-
if (
|
|
289
|
+
// TEAM-6: since you last looked, read once each time its tab opens, which marks it read.
|
|
290
|
+
if (teamUi.tab === 'activity' && model.check?.team && !teamSince)
|
|
272
291
|
teamSince = (await json('team', 'since')) ?? { people: {} };
|
|
273
292
|
// JOURNAL-1, DECIDE-2: the lineage, for the Journal tab and the decisions' cards.
|
|
274
293
|
const deciding = held.size > 0 || (model.requests?.open ?? []).length > 0;
|
|
@@ -412,6 +431,8 @@ export function register(on) {
|
|
|
412
431
|
// TRACE-3: the map follows the specs too.
|
|
413
432
|
specs ? await listing(specs) : '',
|
|
414
433
|
await stamp('.claude/state/requests.jsonl'),
|
|
434
|
+
// TEAM-7: a live provider (the hub's helper) rewrites the team's state as events arrive.
|
|
435
|
+
await stamp('.claude/state/team.json'),
|
|
415
436
|
await stamp('.claude/state/merge-queue.json'),
|
|
416
437
|
await listing('.claude/approvals'),
|
|
417
438
|
await listing('.claude/state/stop-blocks'),
|
|
@@ -578,6 +599,22 @@ export function register(on) {
|
|
|
578
599
|
await act.reload();
|
|
579
600
|
},
|
|
580
601
|
keepClaim: (line) => act.teamAct(['team', 'keep', line.claim.story], 'Keeping the claim'),
|
|
602
|
+
// TEAM-9: the tech lead hands a claim, with its lease, to someone else.
|
|
603
|
+
reassignTo: async (story, handle) => {
|
|
604
|
+
const to = handle.trim().replace(/^@/, '');
|
|
605
|
+
if (!to) return;
|
|
606
|
+
teamUi = { ...teamUi, reassign: null, handle: '' };
|
|
607
|
+
await act.teamAct(['team', 'reassign', story, to], `Reassigning ${story}`);
|
|
608
|
+
},
|
|
609
|
+
// TEAM-13: the team's setup is changed through its init, by the lead, with the person.
|
|
610
|
+
teamSetup: async () => {
|
|
611
|
+
await $.prompt
|
|
612
|
+
.submit({ text: "Run /code-kit:team-init: I want to change this project's team setup." })
|
|
613
|
+
.catch((err) => {
|
|
614
|
+
notice = `Couldn't ask the lead: ${err?.message ?? err}`;
|
|
615
|
+
});
|
|
616
|
+
$.ui.invalidate('ui.render');
|
|
617
|
+
},
|
|
581
618
|
releaseClaim: (line) =>
|
|
582
619
|
act.teamAct(['team', 'release', line.claim.story], 'Releasing the claim'),
|
|
583
620
|
// TEAM-10: agreed, the lead records it where agents read it, through a pull request.
|
|
@@ -599,11 +636,43 @@ export function register(on) {
|
|
|
599
636
|
['intend', 'drop', String(line.intent.id), '--reason', 'dropped in the code-kit pane'],
|
|
600
637
|
'Dropping the intent',
|
|
601
638
|
),
|
|
602
|
-
// TEAM-7: the Team
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
639
|
+
// TEAM-7: the Team pane, opened from the band (on what the line is about) or /team (which closes
|
|
640
|
+
// it when it's open).
|
|
641
|
+
team: async (line, { toggle = false } = {}) => {
|
|
642
|
+
const open = (await $.ui.panes()).some((p) => p.id === TEAM_ID);
|
|
643
|
+
if (open && toggle) {
|
|
644
|
+
await $.ui.close({ id: TEAM_ID });
|
|
645
|
+
return {};
|
|
646
|
+
}
|
|
647
|
+
if (toggle || !model.state) await act.reload();
|
|
648
|
+
if (model.state?.kind === 'none') return { text: NOT_CODE_KIT };
|
|
649
|
+
if (!model.check?.team)
|
|
650
|
+
return {
|
|
651
|
+
text: "This project isn't set up as a team: one developer, no claims or owners. To share it with a team, run /code-kit:team-init.",
|
|
652
|
+
};
|
|
653
|
+
const tab =
|
|
654
|
+
line?.kind === 'teamIntent'
|
|
655
|
+
? 'intents'
|
|
656
|
+
: line?.kind === 'teamDrift'
|
|
657
|
+
? 'drift'
|
|
658
|
+
: ['teamStale', 'teamCollision'].includes(line?.kind)
|
|
659
|
+
? 'claims'
|
|
660
|
+
: line
|
|
661
|
+
? 'people'
|
|
662
|
+
: teamUi.tab;
|
|
663
|
+
teamUi = { ...teamUi, tab };
|
|
664
|
+
// Opening it reads since-you-last-looked again, which marks it read.
|
|
665
|
+
if (tab !== 'activity' || !open) teamSince = null;
|
|
666
|
+
if (!open) {
|
|
667
|
+
const placed = await $.ui.open({
|
|
668
|
+
id: TEAM_ID,
|
|
669
|
+
title: 'Team',
|
|
670
|
+
focus: true,
|
|
671
|
+
closeOnEscape: true,
|
|
672
|
+
});
|
|
673
|
+
if (!placed?.isPlaced && placed?.reason)
|
|
674
|
+
return { text: `The Team pane is waiting: ${placed.reason}` };
|
|
675
|
+
}
|
|
607
676
|
await act.readMaps();
|
|
608
677
|
$.ui.invalidate('ui.render');
|
|
609
678
|
return {};
|
|
@@ -730,7 +799,14 @@ export function register(on) {
|
|
|
730
799
|
return act.submit(prompt, 'restart', story);
|
|
731
800
|
}
|
|
732
801
|
const ran = await $.process.run(['node', cli, 'next', '--json'], { cwd });
|
|
733
|
-
|
|
802
|
+
// LOOP-9: never a story an agent is already at work on, building or reviewing it.
|
|
803
|
+
const step =
|
|
804
|
+
ran.exitCode === 0
|
|
805
|
+
? withoutInFlight(
|
|
806
|
+
parseJson(ran.stdout),
|
|
807
|
+
storiesInFlight(await $.agent.list().catch(() => [])),
|
|
808
|
+
)
|
|
809
|
+
: null;
|
|
734
810
|
// MISSION-1: the band shows what the loop takes next.
|
|
735
811
|
loop.nextStep = step;
|
|
736
812
|
if (!step) {
|
|
@@ -1089,7 +1165,7 @@ export function register(on) {
|
|
|
1089
1165
|
await act.stalls();
|
|
1090
1166
|
await act.animate();
|
|
1091
1167
|
const now = await act.fingerprint();
|
|
1092
|
-
const paneOpen = (await $.ui.panes()).some((p) => p.id
|
|
1168
|
+
const paneOpen = (await $.ui.panes()).some((p) => [PANE_ID, TEAM_ID].includes(p.id));
|
|
1093
1169
|
quiet += 1;
|
|
1094
1170
|
if (now === seen && quiet < (paneOpen ? 5 : 30)) return;
|
|
1095
1171
|
quiet = 0;
|
|
@@ -1113,6 +1189,9 @@ export function register(on) {
|
|
|
1113
1189
|
text: `code-kit is still starting; try /${name} again in a moment.`,
|
|
1114
1190
|
});
|
|
1115
1191
|
on('command.run', { command: 'lanes' }, async ($, e) => (act ? act.lanes() : starting('lanes')));
|
|
1192
|
+
on('command.run', { command: 'team' }, async ($, e) =>
|
|
1193
|
+
act ? act.team(null, { toggle: true }) : starting('team'),
|
|
1194
|
+
);
|
|
1116
1195
|
on('command.run', { command: 'harness' }, async ($, e) =>
|
|
1117
1196
|
act ? act.harness() : starting('harness'),
|
|
1118
1197
|
);
|
|
@@ -1539,8 +1618,6 @@ export function register(on) {
|
|
|
1539
1618
|
needs: linesNow(now).filter((l) => !l.mission),
|
|
1540
1619
|
cards: cardsNow(now),
|
|
1541
1620
|
journal: journalNow(),
|
|
1542
|
-
team: model.team ?? null,
|
|
1543
|
-
teamSince,
|
|
1544
1621
|
now,
|
|
1545
1622
|
live: liveNow(now),
|
|
1546
1623
|
held: [...held.values()],
|
|
@@ -1567,7 +1644,6 @@ export function register(on) {
|
|
|
1567
1644
|
$.ui.resolve(e),
|
|
1568
1645
|
{
|
|
1569
1646
|
onTab: (tab) => {
|
|
1570
|
-
if (tab !== 'team') teamSince = null;
|
|
1571
1647
|
paneUi = { ...paneUi, tab };
|
|
1572
1648
|
$.ui.invalidate('ui.render');
|
|
1573
1649
|
act?.readMaps().then(() => $.ui.invalidate('ui.render'));
|
|
@@ -1704,6 +1780,38 @@ export function register(on) {
|
|
|
1704
1780
|
}),
|
|
1705
1781
|
);
|
|
1706
1782
|
|
|
1783
|
+
// TEAM-7: the Team pane.
|
|
1784
|
+
on('ui.render', { component: 'Pane', requestId: TEAM_ID }, async ($, e) =>
|
|
1785
|
+
teamFrame(
|
|
1786
|
+
{
|
|
1787
|
+
team: model.team ?? null,
|
|
1788
|
+
since: teamSince,
|
|
1789
|
+
config: model.check?.team ? { ...model.check.team, lanes: model.check.lanes ?? {} } : null,
|
|
1790
|
+
ui: teamUi,
|
|
1791
|
+
},
|
|
1792
|
+
$.ui.resolve(e),
|
|
1793
|
+
{
|
|
1794
|
+
onTab: (tab) => {
|
|
1795
|
+
if (tab !== 'activity') teamSince = null;
|
|
1796
|
+
teamUi = { ...teamUi, tab, reassign: null, handle: '' };
|
|
1797
|
+
$.ui.invalidate('ui.render');
|
|
1798
|
+
act?.readMaps().then(() => $.ui.invalidate('ui.render'));
|
|
1799
|
+
},
|
|
1800
|
+
onNeed: (id, line) => act?.[id]?.(line),
|
|
1801
|
+
onReassign: (story) => {
|
|
1802
|
+
teamUi = { ...teamUi, reassign: story, handle: '' };
|
|
1803
|
+
$.ui.invalidate('ui.render');
|
|
1804
|
+
},
|
|
1805
|
+
onHandle: (handle) => {
|
|
1806
|
+
teamUi = { ...teamUi, handle };
|
|
1807
|
+
$.ui.invalidate('ui.render');
|
|
1808
|
+
},
|
|
1809
|
+
onReassignTo: (story, handle) => act?.reassignTo(story, handle),
|
|
1810
|
+
onSetup: () => act?.teamSetup(),
|
|
1811
|
+
},
|
|
1812
|
+
),
|
|
1813
|
+
);
|
|
1814
|
+
|
|
1707
1815
|
on('ui.render', { component: 'Pane', requestId: RESULT_ID }, async ($, e) =>
|
|
1708
1816
|
resultPane(result, $.ui.resolve(e)),
|
|
1709
1817
|
);
|
|
@@ -8,6 +8,7 @@ const PANE = 'code-kit-lanes';
|
|
|
8
8
|
const APPROVE = 'code-kit-approve';
|
|
9
9
|
const RESULT = 'code-kit-result';
|
|
10
10
|
const SETTINGS = 'code-kit-settings';
|
|
11
|
+
const TEAM = 'code-kit-team';
|
|
11
12
|
|
|
12
13
|
/** A project as the stubs report it; tests change it to move the world on. */
|
|
13
14
|
function project() {
|
|
@@ -200,7 +201,7 @@ function stub(on: any, w: World) {
|
|
|
200
201
|
w.acts.push([...argv.slice(2)]);
|
|
201
202
|
return ran(0, JSON.stringify(w.teamSince));
|
|
202
203
|
}
|
|
203
|
-
if (sub === 'team' && ['keep', 'release', 'board'].includes(argv[3])) {
|
|
204
|
+
if (sub === 'team' && ['keep', 'release', 'board', 'reassign'].includes(argv[3])) {
|
|
204
205
|
w.acts.push([...argv.slice(2)]);
|
|
205
206
|
return ran(0, '');
|
|
206
207
|
}
|
|
@@ -430,6 +431,7 @@ const bandUi = ($: any) =>
|
|
|
430
431
|
const press = ($: any, key: string, requestId?: string) =>
|
|
431
432
|
$.ui.press({ plugin: 'code-kit', key, ...(requestId ? { requestId } : {}) });
|
|
432
433
|
const lanes = ($: any) => $.command.run({ command: 'lanes', args: '' });
|
|
434
|
+
const teamCmd = ($: any) => $.command.run({ command: 'team', args: '' });
|
|
433
435
|
const dayjs = {
|
|
434
436
|
at: '2026-10-06T00:00:00.000Z',
|
|
435
437
|
actor: 'web-engineer',
|
|
@@ -1525,6 +1527,34 @@ test('LOOP-1 and LOOP-2 with the lead idle, the loop dispatches every ready stor
|
|
|
1525
1527
|
await ui.unmount();
|
|
1526
1528
|
});
|
|
1527
1529
|
|
|
1530
|
+
test('LOOP-9 the loop never asks for a story an agent is still on: it waits, and goes on once the agent finishes', async ($, on) => {
|
|
1531
|
+
const w = project();
|
|
1532
|
+
w.next = { step: 'dispatch', args: 'ST-9', then: [] };
|
|
1533
|
+
const clock = await start($, on, w);
|
|
1534
|
+
await leadTurn($);
|
|
1535
|
+
await settle(clock);
|
|
1536
|
+
expect(w.prompts).toEqual(['Dispatch ST-9: run /code-kit:dispatch ST-9.']);
|
|
1537
|
+
// Dispatched, and ST-4 still being built: next (reading only branches) offers both again.
|
|
1538
|
+
w.agents = [
|
|
1539
|
+
{ id: 'a1', type: 'web-engineer', status: 'running', description: 'ST-4 booking form' },
|
|
1540
|
+
{ id: 'a2', type: 'core-engineer', status: 'running', description: 'ST-9 pricing' },
|
|
1541
|
+
];
|
|
1542
|
+
w.next = { step: 'review', args: 'ST-4', then: [{ step: 'dispatch', args: 'ST-9' }] };
|
|
1543
|
+
await leadTurn($, 't-2');
|
|
1544
|
+
await settle(clock);
|
|
1545
|
+
expect(w.prompts).toHaveLength(1);
|
|
1546
|
+
const band = await bandUi($);
|
|
1547
|
+
expect(
|
|
1548
|
+
await band.find({ type: 'Text', text: /waiting: ST-4, ST-9 are still with their agents/ }),
|
|
1549
|
+
).toBeDefined();
|
|
1550
|
+
await band.unmount();
|
|
1551
|
+
// The web agent finishes: ST-4 is reviewed; ST-9 is still being built, so it isn't dispatched again.
|
|
1552
|
+
w.agents = [{ ...w.agents[0], status: 'completed' }, w.agents[1]];
|
|
1553
|
+
await clock.advance(2000);
|
|
1554
|
+
await settle(clock);
|
|
1555
|
+
expect(w.prompts.slice(1)).toEqual(['Review ST-4: run /code-kit:review ST-4.']);
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1528
1558
|
test('LOOP-1 nothing is submitted mid-turn, or while the person has a draft, or twice for the same step', async ($, on) => {
|
|
1529
1559
|
const w = project();
|
|
1530
1560
|
const clock = await start($, on, w);
|
|
@@ -3017,7 +3047,32 @@ test("TEAM-8 when the shared repository can't be reached the band says so as a n
|
|
|
3017
3047
|
await band.unmount();
|
|
3018
3048
|
});
|
|
3019
3049
|
|
|
3020
|
-
test('TEAM-
|
|
3050
|
+
test('TEAM-7 /team opens the Team pane on its people: each with their lanes, claims, pulls and intents; /team again closes it', async ($, on) => {
|
|
3051
|
+
const w = project();
|
|
3052
|
+
w.team = teamOf('sam');
|
|
3053
|
+
(w.check as any).team = { techLead: 'alex' };
|
|
3054
|
+
await start($, on, w);
|
|
3055
|
+
await teamCmd($);
|
|
3056
|
+
expect(w.opened).toEqual([TEAM]);
|
|
3057
|
+
const ui = await pane($, TEAM);
|
|
3058
|
+
expect((await ui.find({ key: 'team-header' }))?.text).toMatch(
|
|
3059
|
+
/tech lead alex[\s\S]*you are sam · own web[\s\S]*2 claimed · 1 intent open · 1 pull request/,
|
|
3060
|
+
);
|
|
3061
|
+
expect((await ui.find({ key: 'team-person-alex' }))?.text).toMatch(/alex · tech lead/);
|
|
3062
|
+
expect((await ui.find({ key: 'team-person-sam' }))?.text).toMatch(
|
|
3063
|
+
/sam · you · owns web[\s\S]*ST-4 · stale since Tuesday/,
|
|
3064
|
+
);
|
|
3065
|
+
expect((await ui.find({ key: 'team-person-kim' }))?.text).toMatch(
|
|
3066
|
+
/#14 Booking list \(web\/st-9\)[\s\S]*intends: Split the booking form/,
|
|
3067
|
+
);
|
|
3068
|
+
// GitHub's own provider has no drift checks, so no Drift tab.
|
|
3069
|
+
expect(await ui.find({ key: 'team-tab-drift' })).toBeUndefined();
|
|
3070
|
+
await ui.unmount();
|
|
3071
|
+
await teamCmd($);
|
|
3072
|
+
expect(w.closed).toEqual([TEAM]);
|
|
3073
|
+
});
|
|
3074
|
+
|
|
3075
|
+
test('TEAM-6 since you last looked is read when its tab opens, which marks it read, and again each time it opens', async ($, on) => {
|
|
3021
3076
|
const w = project();
|
|
3022
3077
|
w.team = teamOf('sam');
|
|
3023
3078
|
w.teamSince = {
|
|
@@ -3027,35 +3082,248 @@ test('TEAM-6 and TEAM-7 the Team tab: since you last looked, then each person wi
|
|
|
3027
3082
|
};
|
|
3028
3083
|
(w.check as any).team = { techLead: 'alex' };
|
|
3029
3084
|
await start($, on, w);
|
|
3030
|
-
await
|
|
3031
|
-
const ui = await pane($,
|
|
3032
|
-
|
|
3033
|
-
|
|
3085
|
+
await teamCmd($);
|
|
3086
|
+
const ui = await pane($, TEAM);
|
|
3087
|
+
const reads = () => w.acts.filter((a) => a[0] === 'team' && a[1] === 'since').length;
|
|
3088
|
+
expect(reads()).toBe(0);
|
|
3089
|
+
expect((await ui.find({ key: 'team-tab-activity' }))?.props.hotkey).toBe('2');
|
|
3090
|
+
await press($, 'team-tab-activity', TEAM);
|
|
3034
3091
|
expect(w.acts).toContainEqual(['team', 'since', '--json']);
|
|
3035
3092
|
expect((await ui.find({ key: 'team-since' }))?.text).toMatch(
|
|
3036
|
-
/kim
|
|
3093
|
+
/kim[\s\S]*pushed 2 commits to web\/st-9/,
|
|
3037
3094
|
);
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
);
|
|
3042
|
-
|
|
3043
|
-
|
|
3095
|
+
await press($, 'team-tab-people', TEAM);
|
|
3096
|
+
await press($, 'team-tab-activity', TEAM);
|
|
3097
|
+
expect(reads()).toBe(2);
|
|
3098
|
+
await ui.unmount();
|
|
3099
|
+
});
|
|
3100
|
+
|
|
3101
|
+
test("TEAM-9 the Claims tab: Keep and Release on yours, and nothing on others'", async ($, on) => {
|
|
3102
|
+
const w = project();
|
|
3103
|
+
w.team = teamOf('sam');
|
|
3104
|
+
(w.check as any).team = { techLead: 'alex' };
|
|
3105
|
+
await start($, on, w);
|
|
3106
|
+
await teamCmd($);
|
|
3107
|
+
const ui = await pane($, TEAM);
|
|
3108
|
+
await press($, 'team-tab-claims', TEAM);
|
|
3109
|
+
expect((await ui.find({ key: 'team-claims-ST-4' }))?.text).toMatch(
|
|
3110
|
+
/ST-4[\s\S]*sam \(you\)[\s\S]*stale since Tuesday[\s\S]*leases apps\/web\/booking\/\*\*/,
|
|
3044
3111
|
);
|
|
3045
|
-
await
|
|
3112
|
+
expect(await ui.find({ key: 'team-release-ST-9' })).toBeUndefined();
|
|
3113
|
+
expect(await ui.find({ key: 'team-reassign-ST-4' })).toBeUndefined();
|
|
3114
|
+
await press($, 'team-keep-ST-4', TEAM);
|
|
3115
|
+
expect(w.acts).toContainEqual(['team', 'keep', 'ST-4']);
|
|
3116
|
+
await press($, 'team-release-ST-4', TEAM);
|
|
3046
3117
|
expect(w.acts).toContainEqual(['team', 'release', 'ST-4']);
|
|
3047
3118
|
await ui.unmount();
|
|
3048
3119
|
});
|
|
3049
3120
|
|
|
3050
|
-
test(
|
|
3121
|
+
test("TEAM-9 the tech lead releases or reassigns anyone's claim from the Claims tab", async ($, on) => {
|
|
3122
|
+
const w = project();
|
|
3123
|
+
w.team = teamOf('alex');
|
|
3124
|
+
(w.check as any).team = { techLead: 'alex' };
|
|
3125
|
+
await start($, on, w);
|
|
3126
|
+
await teamCmd($);
|
|
3127
|
+
const ui = await pane($, TEAM);
|
|
3128
|
+
await press($, 'team-tab-claims', TEAM);
|
|
3129
|
+
await press($, 'team-release-ST-9', TEAM);
|
|
3130
|
+
expect(w.acts).toContainEqual(['team', 'release', 'ST-9']);
|
|
3131
|
+
expect(await ui.find({ key: 'team-reassign-to-ST-9' })).toBeUndefined();
|
|
3132
|
+
await press($, 'team-reassign-ST-9', TEAM);
|
|
3133
|
+
expect(await ui.find({ key: 'team-reassign-to-ST-9' })).toBeDefined();
|
|
3134
|
+
await $.ui.input({ plugin: 'code-kit', key: 'team-reassign-to-ST-9', text: '@sam' });
|
|
3135
|
+
expect(w.acts).toContainEqual(['team', 'reassign', 'ST-9', 'sam']);
|
|
3136
|
+
expect(await ui.find({ key: 'team-reassign-to-ST-9' })).toBeUndefined();
|
|
3137
|
+
await ui.unmount();
|
|
3138
|
+
});
|
|
3139
|
+
|
|
3140
|
+
test('TEAM-5 the Intents tab lists the open intents, with Agree and Drop for the tech lead', async ($, on) => {
|
|
3141
|
+
const w = project();
|
|
3142
|
+
w.team = teamOf('alex');
|
|
3143
|
+
(w.check as any).team = { techLead: 'alex' };
|
|
3144
|
+
await start($, on, w);
|
|
3145
|
+
await teamCmd($);
|
|
3146
|
+
const ui = await pane($, TEAM);
|
|
3147
|
+
await press($, 'team-tab-intents', TEAM);
|
|
3148
|
+
expect((await ui.find({ key: 'team-intents-12' }))?.text).toMatch(
|
|
3149
|
+
/#12[\s\S]*Split the booking form[\s\S]*kim[\s\S]*touches apps\/web\/\*\*/,
|
|
3150
|
+
);
|
|
3151
|
+
await press($, 'team-drop-12', TEAM);
|
|
3152
|
+
expect(w.acts).toContainEqual([
|
|
3153
|
+
'intend',
|
|
3154
|
+
'drop',
|
|
3155
|
+
'12',
|
|
3156
|
+
'--reason',
|
|
3157
|
+
'dropped in the code-kit pane',
|
|
3158
|
+
]);
|
|
3159
|
+
await ui.unmount();
|
|
3160
|
+
});
|
|
3161
|
+
|
|
3162
|
+
test('TEAM-5 a developer sees the intents without Agree or Drop', async ($, on) => {
|
|
3163
|
+
const w = project();
|
|
3164
|
+
w.team = teamOf('sam');
|
|
3165
|
+
(w.check as any).team = { techLead: 'alex' };
|
|
3166
|
+
await start($, on, w);
|
|
3167
|
+
await teamCmd($);
|
|
3168
|
+
const ui = await pane($, TEAM);
|
|
3169
|
+
await press($, 'team-tab-intents', TEAM);
|
|
3170
|
+
expect(await ui.find({ key: 'team-intents-12' })).toBeDefined();
|
|
3171
|
+
expect(await ui.find({ key: 'team-agree-12' })).toBeUndefined();
|
|
3172
|
+
await ui.unmount();
|
|
3173
|
+
});
|
|
3174
|
+
|
|
3175
|
+
test("TEAM-13 the Setup tab shows the team's config and what's still missing, and asks the lead to change it", async ($, on) => {
|
|
3176
|
+
const w = project();
|
|
3177
|
+
w.team = {
|
|
3178
|
+
...teamOf('sam'),
|
|
3179
|
+
provider: {
|
|
3180
|
+
name: 'GitHub',
|
|
3181
|
+
live: false,
|
|
3182
|
+
detail: 'claims are refs on origin, intents are discussions in "Intents"',
|
|
3183
|
+
},
|
|
3184
|
+
setup: [
|
|
3185
|
+
'Discussions are off for this repository. Turn them on in its Settings › Features, add a "Intents" category.',
|
|
3186
|
+
],
|
|
3187
|
+
};
|
|
3188
|
+
(w.check as any).team = {
|
|
3189
|
+
techLead: 'alex',
|
|
3190
|
+
staleDays: 3,
|
|
3191
|
+
intents: 'Intents',
|
|
3192
|
+
provider: 'github',
|
|
3193
|
+
owners: { web: ['sam'] },
|
|
3194
|
+
};
|
|
3195
|
+
await start($, on, w);
|
|
3196
|
+
await teamCmd($);
|
|
3197
|
+
const ui = await pane($, TEAM);
|
|
3198
|
+
expect((await ui.find({ key: 'team-provider' }))?.text).toBe(
|
|
3199
|
+
'Through GitHub: claims are refs on origin, intents are discussions in "Intents"',
|
|
3200
|
+
);
|
|
3201
|
+
expect((await ui.find({ key: 'team-setup-count' }))?.text).toBe(
|
|
3202
|
+
'1 thing to set up: see Setup (6).',
|
|
3203
|
+
);
|
|
3204
|
+
await press($, 'team-tab-setup', TEAM);
|
|
3205
|
+
expect((await ui.find({ key: 'team-config' }))?.text).toMatch(
|
|
3206
|
+
/Tech lead\s+alex[\s\S]*Provider\s+GitHub[\s\S]*Lane web\s+sam[\s\S]*Unowned lanes\s+api, core/,
|
|
3207
|
+
);
|
|
3208
|
+
expect((await ui.find({ key: 'team-todo' }))?.text).toMatch(/Discussions are off/);
|
|
3209
|
+
await press($, 'team-setup', TEAM);
|
|
3210
|
+
expect(w.prompts.at(-1)).toMatch(/\/code-kit:team-init/);
|
|
3211
|
+
await ui.unmount();
|
|
3212
|
+
});
|
|
3213
|
+
|
|
3214
|
+
test('TEAM-13 without a team, /team says how to set one up, and opens nothing', async ($, on) => {
|
|
3051
3215
|
const w = project();
|
|
3052
3216
|
await start($, on, w);
|
|
3217
|
+
const said = await teamCmd($);
|
|
3218
|
+
expect(said.text).toMatch(/isn't set up as a team[\s\S]*\/code-kit:team-init/);
|
|
3219
|
+
expect(w.opened).toEqual([]);
|
|
3220
|
+
});
|
|
3221
|
+
|
|
3222
|
+
test('TEAM-7 the Lanes pane has no Team tab: the team has its own pane', async ($, on) => {
|
|
3223
|
+
const w = project();
|
|
3224
|
+
w.team = teamOf('sam');
|
|
3225
|
+
(w.check as any).team = { techLead: 'alex' };
|
|
3226
|
+
await start($, on, w);
|
|
3053
3227
|
await lanes($);
|
|
3054
3228
|
const ui = await pane($, PANE);
|
|
3055
3229
|
expect(await ui.find({ key: 'tab-team' })).toBeUndefined();
|
|
3056
3230
|
await ui.unmount();
|
|
3057
3231
|
});
|
|
3058
3232
|
|
|
3233
|
+
test('a live provider says so in the Team pane, by its own name, and has a Drift tab', async ($, on) => {
|
|
3234
|
+
const w = project();
|
|
3235
|
+
w.team = {
|
|
3236
|
+
...teamOf('kim'),
|
|
3237
|
+
provider: {
|
|
3238
|
+
name: 'the code-kit teams hub',
|
|
3239
|
+
live: true,
|
|
3240
|
+
account: 'kim',
|
|
3241
|
+
detail: 'claims, leases and intents as they happen',
|
|
3242
|
+
},
|
|
3243
|
+
findings: [],
|
|
3244
|
+
};
|
|
3245
|
+
(w.check as any).team = { techLead: 'alex' };
|
|
3246
|
+
await start($, on, w);
|
|
3247
|
+
await teamCmd($);
|
|
3248
|
+
const ui = await pane($, TEAM);
|
|
3249
|
+
expect((await ui.find({ key: 'team-provider' }))?.text).toBe(
|
|
3250
|
+
'Through the code-kit teams hub · live · signed in as kim: claims, leases and intents as they happen',
|
|
3251
|
+
);
|
|
3252
|
+
await press($, 'team-tab-drift', TEAM);
|
|
3253
|
+
expect(
|
|
3254
|
+
await ui.find({ type: 'Text', text: 'No drift found on the branches at work.' }),
|
|
3255
|
+
).toBeDefined();
|
|
3256
|
+
await ui.unmount();
|
|
3257
|
+
});
|
|
3258
|
+
|
|
3259
|
+
test("with a live provider (the hub), the band says when it fell back to GitHub, and names drift on your work; the Team tab shows who's at work and the drift found", async ($, on) => {
|
|
3260
|
+
const w = project();
|
|
3261
|
+
const at = new Date().toISOString();
|
|
3262
|
+
w.team = {
|
|
3263
|
+
...teamOf('kim'),
|
|
3264
|
+
fallback: "The code-kit teams hub can't be reached.",
|
|
3265
|
+
sessions: [
|
|
3266
|
+
{
|
|
3267
|
+
login: 'kim',
|
|
3268
|
+
branch: 'web/st-9',
|
|
3269
|
+
agents: [{ type: 'web-engineer', story: 'ST-9', tool: 'Edit' }],
|
|
3270
|
+
seen: at,
|
|
3271
|
+
away: false,
|
|
3272
|
+
},
|
|
3273
|
+
{ login: 'sam', branch: 'api/st-4', agents: [], seen: at, away: true },
|
|
3274
|
+
],
|
|
3275
|
+
findings: [
|
|
3276
|
+
{
|
|
3277
|
+
id: 1,
|
|
3278
|
+
branch: 'web/st-9',
|
|
3279
|
+
by: 'kim',
|
|
3280
|
+
conflictsWith: 'spec docs/specs/02-booking.md',
|
|
3281
|
+
severity: 'blocker',
|
|
3282
|
+
text: 'BOOK-2 now holds prices in cents.',
|
|
3283
|
+
at,
|
|
3284
|
+
},
|
|
3285
|
+
{
|
|
3286
|
+
id: 2,
|
|
3287
|
+
branch: 'api/st-4',
|
|
3288
|
+
by: 'sam',
|
|
3289
|
+
conflictsWith: 'branch web/st-9',
|
|
3290
|
+
severity: 'note',
|
|
3291
|
+
text: 'only a note',
|
|
3292
|
+
at,
|
|
3293
|
+
},
|
|
3294
|
+
],
|
|
3295
|
+
};
|
|
3296
|
+
(w.check as any).team = { techLead: 'alex' };
|
|
3297
|
+
await start($, on, w);
|
|
3298
|
+
const band = await bandUi($);
|
|
3299
|
+
expect(
|
|
3300
|
+
await band.find({
|
|
3301
|
+
type: 'Text',
|
|
3302
|
+
text: "team: the team's provider can't be reached, so claims are checked through GitHub until it's back",
|
|
3303
|
+
}),
|
|
3304
|
+
).toBeDefined();
|
|
3305
|
+
expect(
|
|
3306
|
+
await band.find({ type: 'Text', text: 'drift on web/st-9: BOOK-2 now holds prices in cents.' }),
|
|
3307
|
+
).toBeDefined();
|
|
3308
|
+
expect(await band.find({ type: 'Text', text: /only a note/ })).toBeUndefined();
|
|
3309
|
+
// The drift line's Team button (the second: the collision's comes first) opens the Team pane on the drift.
|
|
3310
|
+
await press($, 'band-team-2', 'above-prompt');
|
|
3311
|
+
expect(w.opened).toEqual([TEAM]);
|
|
3312
|
+
const ui = await pane($, TEAM);
|
|
3313
|
+
expect((await ui.find({ key: 'team-provider' }))?.text).toMatch(
|
|
3314
|
+
/can't be reached \(The code-kit teams hub can't be reached\.\): claims are checked through GitHub/,
|
|
3315
|
+
);
|
|
3316
|
+
expect((await ui.find({ key: 'team-drift-web/st-9' }))?.text).toMatch(
|
|
3317
|
+
/web\/st-9 · kim[\s\S]*blocker · spec docs\/specs\/02-booking\.md: BOOK-2 now holds prices in cents\./,
|
|
3318
|
+
);
|
|
3319
|
+
await press($, 'team-tab-people', TEAM);
|
|
3320
|
+
expect((await ui.find({ key: 'team-person-kim' }))?.text).toMatch(
|
|
3321
|
+
/at work on web\/st-9: web-engineer on ST-9 \(Edit\)[\s\S]*drift on web\/st-9 \(blocker\): BOOK-2 now holds prices in cents\./,
|
|
3322
|
+
);
|
|
3323
|
+
expect((await ui.find({ key: 'team-person-sam' }))?.text).toMatch(/away \(last seen/);
|
|
3324
|
+
await ui.unmount();
|
|
3325
|
+
});
|
|
3326
|
+
|
|
3059
3327
|
test('TEAM-12 with a board, the mod syncs it in the background, at most every 10 minutes', async ($, on) => {
|
|
3060
3328
|
const w = project();
|
|
3061
3329
|
w.team = teamOf('sam');
|