@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.
@@ -48,6 +48,7 @@ import {
48
48
  import { leadPrompt, nudgeText, restartPrompt, stallDue, stepKey, stepLabel } from './loop.mjs';
49
49
  import { goalOf, nextText } from './mission.mjs';
50
50
  import { contextOf, decisionCards } from './decisions.mjs';
51
+ import { DEFAULT_TEAM_UI, TEAM_ID, teamFrame } from './teamview.mjs';
51
52
  import { glowOf, mapStoryOf, touchOf } from './codemap.mjs';
52
53
  import { harnessSettings } from '../lib/harness.mjs';
53
54
  import {
@@ -67,7 +68,8 @@ let reviewTurn = null; // 'next' until the lead's review turn starts, then its i
67
68
  let approving = null; // the open Approve… confirmation: { request, reason, error }
68
69
  let result = null; // what the last merge reported
69
70
  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
71
+ let teamSince = null; // TEAM-6: since you last looked, as read when the Team pane's tab opened
72
+ let teamUi = { ...DEFAULT_TEAM_UI }; // TEAM-7: the Team pane's own state
71
73
  let boardSynced = -Infinity; // TEAM-12: when the board was last synced
72
74
  let projectRead = -Infinity; // MAP-5: when the whole project's map was last read
73
75
  const BOARD_EVERY_MS = 10 * 60000;
@@ -176,6 +178,14 @@ export function register(on) {
176
178
  immediate: true,
177
179
  })
178
180
  .catch(unavailable('harness'));
181
+ await $.command
182
+ .register({
183
+ name: 'team',
184
+ description:
185
+ "code-kit's team: who's at work on what, claims, intents, drift and the team's setup",
186
+ immediate: true,
187
+ })
188
+ .catch(unavailable('team'));
179
189
  rateLimits = (await $.session.usage().catch(() => null))?.rateLimits ?? [];
180
190
  const cli = `${$.plugin.root}/bin/code-kit.mjs`;
181
191
  const cwd = e.cwd ?? (await $.session.cwd());
@@ -267,8 +277,8 @@ export function register(on) {
267
277
  }
268
278
  }
269
279
  if (paneUi.tab === 'map') trailData = (await json('trail')) ?? trailData;
270
- // TEAM-6: since you last looked, read once each time the Team tab opens, which marks it read.
271
- if (paneUi.tab === 'team' && model.check?.team && !teamSince)
280
+ // TEAM-6: since you last looked, read once each time its tab opens, which marks it read.
281
+ if (teamUi.tab === 'activity' && model.check?.team && !teamSince)
272
282
  teamSince = (await json('team', 'since')) ?? { people: {} };
273
283
  // JOURNAL-1, DECIDE-2: the lineage, for the Journal tab and the decisions' cards.
274
284
  const deciding = held.size > 0 || (model.requests?.open ?? []).length > 0;
@@ -412,6 +422,8 @@ export function register(on) {
412
422
  // TRACE-3: the map follows the specs too.
413
423
  specs ? await listing(specs) : '',
414
424
  await stamp('.claude/state/requests.jsonl'),
425
+ // TEAM-7: a live provider (the hub's helper) rewrites the team's state as events arrive.
426
+ await stamp('.claude/state/team.json'),
415
427
  await stamp('.claude/state/merge-queue.json'),
416
428
  await listing('.claude/approvals'),
417
429
  await listing('.claude/state/stop-blocks'),
@@ -578,6 +590,22 @@ export function register(on) {
578
590
  await act.reload();
579
591
  },
580
592
  keepClaim: (line) => act.teamAct(['team', 'keep', line.claim.story], 'Keeping the claim'),
593
+ // TEAM-9: the tech lead hands a claim, with its lease, to someone else.
594
+ reassignTo: async (story, handle) => {
595
+ const to = handle.trim().replace(/^@/, '');
596
+ if (!to) return;
597
+ teamUi = { ...teamUi, reassign: null, handle: '' };
598
+ await act.teamAct(['team', 'reassign', story, to], `Reassigning ${story}`);
599
+ },
600
+ // TEAM-13: the team's setup is changed through its init, by the lead, with the person.
601
+ teamSetup: async () => {
602
+ await $.prompt
603
+ .submit({ text: "Run /code-kit:team-init: I want to change this project's team setup." })
604
+ .catch((err) => {
605
+ notice = `Couldn't ask the lead: ${err?.message ?? err}`;
606
+ });
607
+ $.ui.invalidate('ui.render');
608
+ },
581
609
  releaseClaim: (line) =>
582
610
  act.teamAct(['team', 'release', line.claim.story], 'Releasing the claim'),
583
611
  // TEAM-10: agreed, the lead records it where agents read it, through a pull request.
@@ -599,11 +627,43 @@ export function register(on) {
599
627
  ['intend', 'drop', String(line.intent.id), '--reason', 'dropped in the code-kit pane'],
600
628
  'Dropping the intent',
601
629
  ),
602
- // TEAM-7: the Team tab, opened from the band.
603
- team: async () => {
604
- paneUi = { ...paneUi, tab: 'team', story: null };
605
- if (!(await $.ui.panes()).some((p) => p.id === PANE_ID))
606
- await $.ui.open({ id: PANE_ID, title: 'Lanes', focus: true, closeOnEscape: true });
630
+ // TEAM-7: the Team pane, opened from the band (on what the line is about) or /team (which closes
631
+ // it when it's open).
632
+ team: async (line, { toggle = false } = {}) => {
633
+ const open = (await $.ui.panes()).some((p) => p.id === TEAM_ID);
634
+ if (open && toggle) {
635
+ await $.ui.close({ id: TEAM_ID });
636
+ return {};
637
+ }
638
+ if (toggle || !model.state) await act.reload();
639
+ if (model.state?.kind === 'none') return { text: NOT_CODE_KIT };
640
+ if (!model.check?.team)
641
+ return {
642
+ 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.",
643
+ };
644
+ const tab =
645
+ line?.kind === 'teamIntent'
646
+ ? 'intents'
647
+ : line?.kind === 'teamDrift'
648
+ ? 'drift'
649
+ : ['teamStale', 'teamCollision'].includes(line?.kind)
650
+ ? 'claims'
651
+ : line
652
+ ? 'people'
653
+ : teamUi.tab;
654
+ teamUi = { ...teamUi, tab };
655
+ // Opening it reads since-you-last-looked again, which marks it read.
656
+ if (tab !== 'activity' || !open) teamSince = null;
657
+ if (!open) {
658
+ const placed = await $.ui.open({
659
+ id: TEAM_ID,
660
+ title: 'Team',
661
+ focus: true,
662
+ closeOnEscape: true,
663
+ });
664
+ if (!placed?.isPlaced && placed?.reason)
665
+ return { text: `The Team pane is waiting: ${placed.reason}` };
666
+ }
607
667
  await act.readMaps();
608
668
  $.ui.invalidate('ui.render');
609
669
  return {};
@@ -1089,7 +1149,7 @@ export function register(on) {
1089
1149
  await act.stalls();
1090
1150
  await act.animate();
1091
1151
  const now = await act.fingerprint();
1092
- const paneOpen = (await $.ui.panes()).some((p) => p.id === PANE_ID);
1152
+ const paneOpen = (await $.ui.panes()).some((p) => [PANE_ID, TEAM_ID].includes(p.id));
1093
1153
  quiet += 1;
1094
1154
  if (now === seen && quiet < (paneOpen ? 5 : 30)) return;
1095
1155
  quiet = 0;
@@ -1113,6 +1173,9 @@ export function register(on) {
1113
1173
  text: `code-kit is still starting; try /${name} again in a moment.`,
1114
1174
  });
1115
1175
  on('command.run', { command: 'lanes' }, async ($, e) => (act ? act.lanes() : starting('lanes')));
1176
+ on('command.run', { command: 'team' }, async ($, e) =>
1177
+ act ? act.team(null, { toggle: true }) : starting('team'),
1178
+ );
1116
1179
  on('command.run', { command: 'harness' }, async ($, e) =>
1117
1180
  act ? act.harness() : starting('harness'),
1118
1181
  );
@@ -1539,8 +1602,6 @@ export function register(on) {
1539
1602
  needs: linesNow(now).filter((l) => !l.mission),
1540
1603
  cards: cardsNow(now),
1541
1604
  journal: journalNow(),
1542
- team: model.team ?? null,
1543
- teamSince,
1544
1605
  now,
1545
1606
  live: liveNow(now),
1546
1607
  held: [...held.values()],
@@ -1567,7 +1628,6 @@ export function register(on) {
1567
1628
  $.ui.resolve(e),
1568
1629
  {
1569
1630
  onTab: (tab) => {
1570
- if (tab !== 'team') teamSince = null;
1571
1631
  paneUi = { ...paneUi, tab };
1572
1632
  $.ui.invalidate('ui.render');
1573
1633
  act?.readMaps().then(() => $.ui.invalidate('ui.render'));
@@ -1704,6 +1764,38 @@ export function register(on) {
1704
1764
  }),
1705
1765
  );
1706
1766
 
1767
+ // TEAM-7: the Team pane.
1768
+ on('ui.render', { component: 'Pane', requestId: TEAM_ID }, async ($, e) =>
1769
+ teamFrame(
1770
+ {
1771
+ team: model.team ?? null,
1772
+ since: teamSince,
1773
+ config: model.check?.team ? { ...model.check.team, lanes: model.check.lanes ?? {} } : null,
1774
+ ui: teamUi,
1775
+ },
1776
+ $.ui.resolve(e),
1777
+ {
1778
+ onTab: (tab) => {
1779
+ if (tab !== 'activity') teamSince = null;
1780
+ teamUi = { ...teamUi, tab, reassign: null, handle: '' };
1781
+ $.ui.invalidate('ui.render');
1782
+ act?.readMaps().then(() => $.ui.invalidate('ui.render'));
1783
+ },
1784
+ onNeed: (id, line) => act?.[id]?.(line),
1785
+ onReassign: (story) => {
1786
+ teamUi = { ...teamUi, reassign: story, handle: '' };
1787
+ $.ui.invalidate('ui.render');
1788
+ },
1789
+ onHandle: (handle) => {
1790
+ teamUi = { ...teamUi, handle };
1791
+ $.ui.invalidate('ui.render');
1792
+ },
1793
+ onReassignTo: (story, handle) => act?.reassignTo(story, handle),
1794
+ onSetup: () => act?.teamSetup(),
1795
+ },
1796
+ ),
1797
+ );
1798
+
1707
1799
  on('ui.render', { component: 'Pane', requestId: RESULT_ID }, async ($, e) =>
1708
1800
  resultPane(result, $.ui.resolve(e)),
1709
1801
  );
@@ -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',
@@ -3017,7 +3019,32 @@ test("TEAM-8 when the shared repository can't be reached the band says so as a n
3017
3019
  await band.unmount();
3018
3020
  });
3019
3021
 
3020
- test('TEAM-6 and TEAM-7 the Team tab: since you last looked, then each person with their lanes, claims, pulls and intents', async ($, on) => {
3022
+ 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) => {
3023
+ const w = project();
3024
+ w.team = teamOf('sam');
3025
+ (w.check as any).team = { techLead: 'alex' };
3026
+ await start($, on, w);
3027
+ await teamCmd($);
3028
+ expect(w.opened).toEqual([TEAM]);
3029
+ const ui = await pane($, TEAM);
3030
+ expect((await ui.find({ key: 'team-header' }))?.text).toMatch(
3031
+ /tech lead alex[\s\S]*you are sam · own web[\s\S]*2 claimed · 1 intent open · 1 pull request/,
3032
+ );
3033
+ expect((await ui.find({ key: 'team-person-alex' }))?.text).toMatch(/alex · tech lead/);
3034
+ expect((await ui.find({ key: 'team-person-sam' }))?.text).toMatch(
3035
+ /sam · you · owns web[\s\S]*ST-4 · stale since Tuesday/,
3036
+ );
3037
+ expect((await ui.find({ key: 'team-person-kim' }))?.text).toMatch(
3038
+ /#14 Booking list \(web\/st-9\)[\s\S]*intends: Split the booking form/,
3039
+ );
3040
+ // GitHub's own provider has no drift checks, so no Drift tab.
3041
+ expect(await ui.find({ key: 'team-tab-drift' })).toBeUndefined();
3042
+ await ui.unmount();
3043
+ await teamCmd($);
3044
+ expect(w.closed).toEqual([TEAM]);
3045
+ });
3046
+
3047
+ 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
3048
  const w = project();
3022
3049
  w.team = teamOf('sam');
3023
3050
  w.teamSince = {
@@ -3027,28 +3054,147 @@ test('TEAM-6 and TEAM-7 the Team tab: since you last looked, then each person wi
3027
3054
  };
3028
3055
  (w.check as any).team = { techLead: 'alex' };
3029
3056
  await start($, on, w);
3030
- await lanes($);
3031
- const ui = await pane($, PANE);
3032
- expect((await ui.find({ key: 'tab-team' }))?.props.hotkey).toBe('8');
3033
- await press($, 'tab-team', PANE);
3057
+ await teamCmd($);
3058
+ const ui = await pane($, TEAM);
3059
+ const reads = () => w.acts.filter((a) => a[0] === 'team' && a[1] === 'since').length;
3060
+ expect(reads()).toBe(0);
3061
+ expect((await ui.find({ key: 'team-tab-activity' }))?.props.hotkey).toBe('2');
3062
+ await press($, 'team-tab-activity', TEAM);
3034
3063
  expect(w.acts).toContainEqual(['team', 'since', '--json']);
3035
3064
  expect((await ui.find({ key: 'team-since' }))?.text).toMatch(
3036
- /kim: pushed 2 commits to web\/st-9/,
3037
- );
3038
- expect((await ui.find({ key: 'team-person-alex' }))?.text).toMatch(/alex · tech lead/);
3039
- expect((await ui.find({ key: 'team-person-sam' }))?.text).toMatch(
3040
- /sam · you · owns web[\s\S]*ST-4 · stale since Tuesday/,
3065
+ /kim[\s\S]*pushed 2 commits to web\/st-9/,
3041
3066
  );
3042
- expect((await ui.find({ key: 'team-person-kim' }))?.text).toMatch(
3043
- /#14 Booking list \(web\/st-9\)[\s\S]*intends: Split the booking form/,
3067
+ await press($, 'team-tab-people', TEAM);
3068
+ await press($, 'team-tab-activity', TEAM);
3069
+ expect(reads()).toBe(2);
3070
+ await ui.unmount();
3071
+ });
3072
+
3073
+ test("TEAM-9 the Claims tab: Keep and Release on yours, and nothing on others'", async ($, on) => {
3074
+ const w = project();
3075
+ w.team = teamOf('sam');
3076
+ (w.check as any).team = { techLead: 'alex' };
3077
+ await start($, on, w);
3078
+ await teamCmd($);
3079
+ const ui = await pane($, TEAM);
3080
+ await press($, 'team-tab-claims', TEAM);
3081
+ expect((await ui.find({ key: 'team-claims-ST-4' }))?.text).toMatch(
3082
+ /ST-4[\s\S]*sam \(you\)[\s\S]*stale since Tuesday[\s\S]*leases apps\/web\/booking\/\*\*/,
3044
3083
  );
3045
- await press($, 'team-release-ST-4', PANE);
3084
+ expect(await ui.find({ key: 'team-release-ST-9' })).toBeUndefined();
3085
+ expect(await ui.find({ key: 'team-reassign-ST-4' })).toBeUndefined();
3086
+ await press($, 'team-keep-ST-4', TEAM);
3087
+ expect(w.acts).toContainEqual(['team', 'keep', 'ST-4']);
3088
+ await press($, 'team-release-ST-4', TEAM);
3046
3089
  expect(w.acts).toContainEqual(['team', 'release', 'ST-4']);
3047
3090
  await ui.unmount();
3048
3091
  });
3049
3092
 
3050
- test('TEAM-7 without a team there is no Team tab', async ($, on) => {
3093
+ test("TEAM-9 the tech lead releases or reassigns anyone's claim from the Claims tab", async ($, on) => {
3051
3094
  const w = project();
3095
+ w.team = teamOf('alex');
3096
+ (w.check as any).team = { techLead: 'alex' };
3097
+ await start($, on, w);
3098
+ await teamCmd($);
3099
+ const ui = await pane($, TEAM);
3100
+ await press($, 'team-tab-claims', TEAM);
3101
+ await press($, 'team-release-ST-9', TEAM);
3102
+ expect(w.acts).toContainEqual(['team', 'release', 'ST-9']);
3103
+ expect(await ui.find({ key: 'team-reassign-to-ST-9' })).toBeUndefined();
3104
+ await press($, 'team-reassign-ST-9', TEAM);
3105
+ expect(await ui.find({ key: 'team-reassign-to-ST-9' })).toBeDefined();
3106
+ await $.ui.input({ plugin: 'code-kit', key: 'team-reassign-to-ST-9', text: '@sam' });
3107
+ expect(w.acts).toContainEqual(['team', 'reassign', 'ST-9', 'sam']);
3108
+ expect(await ui.find({ key: 'team-reassign-to-ST-9' })).toBeUndefined();
3109
+ await ui.unmount();
3110
+ });
3111
+
3112
+ test('TEAM-5 the Intents tab lists the open intents, with Agree and Drop for the tech lead', async ($, on) => {
3113
+ const w = project();
3114
+ w.team = teamOf('alex');
3115
+ (w.check as any).team = { techLead: 'alex' };
3116
+ await start($, on, w);
3117
+ await teamCmd($);
3118
+ const ui = await pane($, TEAM);
3119
+ await press($, 'team-tab-intents', TEAM);
3120
+ expect((await ui.find({ key: 'team-intents-12' }))?.text).toMatch(
3121
+ /#12[\s\S]*Split the booking form[\s\S]*kim[\s\S]*touches apps\/web\/\*\*/,
3122
+ );
3123
+ await press($, 'team-drop-12', TEAM);
3124
+ expect(w.acts).toContainEqual([
3125
+ 'intend',
3126
+ 'drop',
3127
+ '12',
3128
+ '--reason',
3129
+ 'dropped in the code-kit pane',
3130
+ ]);
3131
+ await ui.unmount();
3132
+ });
3133
+
3134
+ test('TEAM-5 a developer sees the intents without Agree or Drop', async ($, on) => {
3135
+ const w = project();
3136
+ w.team = teamOf('sam');
3137
+ (w.check as any).team = { techLead: 'alex' };
3138
+ await start($, on, w);
3139
+ await teamCmd($);
3140
+ const ui = await pane($, TEAM);
3141
+ await press($, 'team-tab-intents', TEAM);
3142
+ expect(await ui.find({ key: 'team-intents-12' })).toBeDefined();
3143
+ expect(await ui.find({ key: 'team-agree-12' })).toBeUndefined();
3144
+ await ui.unmount();
3145
+ });
3146
+
3147
+ 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) => {
3148
+ const w = project();
3149
+ w.team = {
3150
+ ...teamOf('sam'),
3151
+ provider: {
3152
+ name: 'GitHub',
3153
+ live: false,
3154
+ detail: 'claims are refs on origin, intents are discussions in "Intents"',
3155
+ },
3156
+ setup: [
3157
+ 'Discussions are off for this repository. Turn them on in its Settings › Features, add a "Intents" category.',
3158
+ ],
3159
+ };
3160
+ (w.check as any).team = {
3161
+ techLead: 'alex',
3162
+ staleDays: 3,
3163
+ intents: 'Intents',
3164
+ provider: 'github',
3165
+ owners: { web: ['sam'] },
3166
+ };
3167
+ await start($, on, w);
3168
+ await teamCmd($);
3169
+ const ui = await pane($, TEAM);
3170
+ expect((await ui.find({ key: 'team-provider' }))?.text).toBe(
3171
+ 'Through GitHub: claims are refs on origin, intents are discussions in "Intents"',
3172
+ );
3173
+ expect((await ui.find({ key: 'team-setup-count' }))?.text).toBe(
3174
+ '1 thing to set up: see Setup (6).',
3175
+ );
3176
+ await press($, 'team-tab-setup', TEAM);
3177
+ expect((await ui.find({ key: 'team-config' }))?.text).toMatch(
3178
+ /Tech lead\s+alex[\s\S]*Provider\s+GitHub[\s\S]*Lane web\s+sam[\s\S]*Unowned lanes\s+api, core/,
3179
+ );
3180
+ expect((await ui.find({ key: 'team-todo' }))?.text).toMatch(/Discussions are off/);
3181
+ await press($, 'team-setup', TEAM);
3182
+ expect(w.prompts.at(-1)).toMatch(/\/code-kit:team-init/);
3183
+ await ui.unmount();
3184
+ });
3185
+
3186
+ test('TEAM-13 without a team, /team says how to set one up, and opens nothing', async ($, on) => {
3187
+ const w = project();
3188
+ await start($, on, w);
3189
+ const said = await teamCmd($);
3190
+ expect(said.text).toMatch(/isn't set up as a team[\s\S]*\/code-kit:team-init/);
3191
+ expect(w.opened).toEqual([]);
3192
+ });
3193
+
3194
+ test('TEAM-7 the Lanes pane has no Team tab: the team has its own pane', async ($, on) => {
3195
+ const w = project();
3196
+ w.team = teamOf('sam');
3197
+ (w.check as any).team = { techLead: 'alex' };
3052
3198
  await start($, on, w);
3053
3199
  await lanes($);
3054
3200
  const ui = await pane($, PANE);
@@ -3056,6 +3202,100 @@ test('TEAM-7 without a team there is no Team tab', async ($, on) => {
3056
3202
  await ui.unmount();
3057
3203
  });
3058
3204
 
3205
+ test('a live provider says so in the Team pane, by its own name, and has a Drift tab', async ($, on) => {
3206
+ const w = project();
3207
+ w.team = {
3208
+ ...teamOf('kim'),
3209
+ provider: {
3210
+ name: 'the code-kit teams hub',
3211
+ live: true,
3212
+ account: 'kim',
3213
+ detail: 'claims, leases and intents as they happen',
3214
+ },
3215
+ findings: [],
3216
+ };
3217
+ (w.check as any).team = { techLead: 'alex' };
3218
+ await start($, on, w);
3219
+ await teamCmd($);
3220
+ const ui = await pane($, TEAM);
3221
+ expect((await ui.find({ key: 'team-provider' }))?.text).toBe(
3222
+ 'Through the code-kit teams hub · live · signed in as kim: claims, leases and intents as they happen',
3223
+ );
3224
+ await press($, 'team-tab-drift', TEAM);
3225
+ expect(
3226
+ await ui.find({ type: 'Text', text: 'No drift found on the branches at work.' }),
3227
+ ).toBeDefined();
3228
+ await ui.unmount();
3229
+ });
3230
+
3231
+ 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) => {
3232
+ const w = project();
3233
+ const at = new Date().toISOString();
3234
+ w.team = {
3235
+ ...teamOf('kim'),
3236
+ fallback: "The code-kit teams hub can't be reached.",
3237
+ sessions: [
3238
+ {
3239
+ login: 'kim',
3240
+ branch: 'web/st-9',
3241
+ agents: [{ type: 'web-engineer', story: 'ST-9', tool: 'Edit' }],
3242
+ seen: at,
3243
+ away: false,
3244
+ },
3245
+ { login: 'sam', branch: 'api/st-4', agents: [], seen: at, away: true },
3246
+ ],
3247
+ findings: [
3248
+ {
3249
+ id: 1,
3250
+ branch: 'web/st-9',
3251
+ by: 'kim',
3252
+ conflictsWith: 'spec docs/specs/02-booking.md',
3253
+ severity: 'blocker',
3254
+ text: 'BOOK-2 now holds prices in cents.',
3255
+ at,
3256
+ },
3257
+ {
3258
+ id: 2,
3259
+ branch: 'api/st-4',
3260
+ by: 'sam',
3261
+ conflictsWith: 'branch web/st-9',
3262
+ severity: 'note',
3263
+ text: 'only a note',
3264
+ at,
3265
+ },
3266
+ ],
3267
+ };
3268
+ (w.check as any).team = { techLead: 'alex' };
3269
+ await start($, on, w);
3270
+ const band = await bandUi($);
3271
+ expect(
3272
+ await band.find({
3273
+ type: 'Text',
3274
+ text: "team: the team's provider can't be reached, so claims are checked through GitHub until it's back",
3275
+ }),
3276
+ ).toBeDefined();
3277
+ expect(
3278
+ await band.find({ type: 'Text', text: 'drift on web/st-9: BOOK-2 now holds prices in cents.' }),
3279
+ ).toBeDefined();
3280
+ expect(await band.find({ type: 'Text', text: /only a note/ })).toBeUndefined();
3281
+ // The drift line's Team button (the second: the collision's comes first) opens the Team pane on the drift.
3282
+ await press($, 'band-team-2', 'above-prompt');
3283
+ expect(w.opened).toEqual([TEAM]);
3284
+ const ui = await pane($, TEAM);
3285
+ expect((await ui.find({ key: 'team-provider' }))?.text).toMatch(
3286
+ /can't be reached \(The code-kit teams hub can't be reached\.\): claims are checked through GitHub/,
3287
+ );
3288
+ expect((await ui.find({ key: 'team-drift-web/st-9' }))?.text).toMatch(
3289
+ /web\/st-9 · kim[\s\S]*blocker · spec docs\/specs\/02-booking\.md: BOOK-2 now holds prices in cents\./,
3290
+ );
3291
+ await press($, 'team-tab-people', TEAM);
3292
+ expect((await ui.find({ key: 'team-person-kim' }))?.text).toMatch(
3293
+ /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\./,
3294
+ );
3295
+ expect((await ui.find({ key: 'team-person-sam' }))?.text).toMatch(/away \(last seen/);
3296
+ await ui.unmount();
3297
+ });
3298
+
3059
3299
  test('TEAM-12 with a board, the mod syncs it in the background, at most every 10 minutes', async ($, on) => {
3060
3300
  const w = project();
3061
3301
  w.team = teamOf('sam');