@warren-dean/code-kit 0.4.1 → 0.6.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.
@@ -6,6 +6,7 @@ import { expect, mock, test } from 'claude-code/testing';
6
6
  const PANE = 'code-kit-lanes';
7
7
  const APPROVE = 'code-kit-approve';
8
8
  const RESULT = 'code-kit-result';
9
+ const SETTINGS = 'code-kit-settings';
9
10
 
10
11
  /** A project as the stubs report it; tests change it to move the world on. */
11
12
  function project() {
@@ -68,7 +69,8 @@ function project() {
68
69
  taken: [] as string[], // command names another command already holds
69
70
  stops: [] as any[],
70
71
  refs: ' aaa refs/heads/main\n bbb refs/heads/web/st-4\n',
71
- agents: [] as { type: string; status: string }[],
72
+ agents: [] as { type: string; status: string; id?: string; description?: string }[],
73
+ limits: [] as { kind: string; percentUsed: number }[],
72
74
  approveExit: 0,
73
75
  mergeExit: 0,
74
76
  answer: 'Merge',
@@ -78,6 +80,18 @@ function project() {
78
80
  prompts: [] as string[],
79
81
  acts: [] as string[][],
80
82
  runs: 0,
83
+ settings: [
84
+ {
85
+ key: 'autonomy',
86
+ value: 'autonomous',
87
+ default: 'autonomous',
88
+ about: 'How the lead loop acts',
89
+ },
90
+ { key: 'hold.minutes', value: 2, default: 2, about: 'How long a call is held' },
91
+ { key: 'agents.reviewer.on', value: false, default: false, about: 'A background reviewer' },
92
+ { key: 'agents.reviewer.model', value: null, default: null, about: "The reviewer's model" },
93
+ ] as any[],
94
+ setExit: 0,
81
95
  };
82
96
  }
83
97
  type World = ReturnType<typeof project>;
@@ -119,6 +133,29 @@ function stub(on: any, w: World) {
119
133
  ? ran(1, '', 'Nothing was merged. code-kit verify found 1 problem(s) on web/st-4')
120
134
  : ran(0, 'Merged web/st-4 into main.');
121
135
  }
136
+ if (sub === 'settings' && argv[3] === 'set') {
137
+ w.acts.push([...argv.slice(2)]);
138
+ if (w.setExit)
139
+ return ran(
140
+ 1,
141
+ '',
142
+ '"harness.hold.minutes" must be a number of minutes from 0 (off) to 30\nNothing was changed.',
143
+ );
144
+ const row = w.settings.find((s) => s.key === argv[4]);
145
+ const raw = argv[5];
146
+ row.value =
147
+ raw === 'null'
148
+ ? null
149
+ : raw === 'true'
150
+ ? true
151
+ : raw === 'false'
152
+ ? false
153
+ : /^\d+$/.test(raw)
154
+ ? Number(raw)
155
+ : raw;
156
+ return ran(0, `Set harness.${argv[4]} to ${raw}.`);
157
+ }
158
+ if (sub === 'settings') return ran(0, JSON.stringify(w.settings));
122
159
  return ran(1, '', `unexpected ${argv.join(' ')}`);
123
160
  });
124
161
  on('session.start', () => ({ cwd: '/work' }));
@@ -135,6 +172,19 @@ function stub(on: any, w: World) {
135
172
  w.refusal ? { deny: w.refusal } : { result: { stdout: '', stderr: '', interrupted: false } },
136
173
  );
137
174
  on('agent.list', () => ({ value: w.agents }));
175
+ on('session.usage', () => ({ value: { startedAt: 0, context: {}, rateLimits: w.limits } }));
176
+ on('session.measure', ($: any, e: any) => ({ changed: e.changed }));
177
+ // A model request, answered with the usage the test gave it.
178
+ on('turn.step', async function* ($: any, e: any) {
179
+ return {
180
+ turnId: e.turnId,
181
+ index: e.index,
182
+ answer: '',
183
+ toolUses: [],
184
+ stopReason: 'end_turn',
185
+ usage: e.messageCount === 0 ? null : { model: e.model, ...stepUsage(e.messageCount) },
186
+ };
187
+ } as any);
138
188
  on('fs.stat', () => ({ value: { kind: 'file', size: 1, mtimeMs: 1, isLink: false } }));
139
189
  on('fs.list', () => ({ value: [] }));
140
190
  on('session.cwd', () => ({ value: '/work' }));
@@ -163,6 +213,30 @@ function stub(on: any, w: World) {
163
213
  });
164
214
  }
165
215
 
216
+ /** A request of n thousand tokens, split as a model request's usage is. */
217
+ function stepUsage(thousands: number) {
218
+ const n = thousands * 1000;
219
+ return {
220
+ input_tokens: n * 0.3,
221
+ cache_creation_input_tokens: n * 0.1,
222
+ output_tokens: n * 0.1,
223
+ cache_read_input_tokens: n * 0.5,
224
+ };
225
+ }
226
+
227
+ /** One model request by an agent (or the lead) of so many thousand tokens, run to its end. */
228
+ async function request($: any, agentId: string | undefined, thousands: number) {
229
+ const s = $.turn.step({
230
+ turnId: `t-${agentId ?? 'lead'}`,
231
+ index: 0,
232
+ model: 'claude-sonnet-5-5',
233
+ messageCount: thousands, // the stub beneath answers with this many thousand tokens
234
+ ...(agentId ? { agentId } : {}),
235
+ });
236
+ for await (const _ of s);
237
+ return s.result;
238
+ }
239
+
166
240
  /** A session started on the project, with the mock clock. */
167
241
  async function start($: any, on: any, w: World) {
168
242
  stub(on, w);
@@ -336,7 +410,9 @@ test('ACT-1 and BAND-3 approving with the prefilled reason grants it for the lan
336
410
  const confirm = await pane($, APPROVE);
337
411
  const reason = 'Approve dayjs for the web lane: npm install dayjs';
338
412
  expect((await confirm.find({ key: 'approve-reason' }))?.props.value).toBe(reason);
339
- expect(await confirm.find({ type: 'Text', text: /for 60 minutes/ })).toBeDefined();
413
+ expect(
414
+ await confirm.find({ type: 'Text', text: /until the install is committed \(at most 7 days\)/ }),
415
+ ).toBeDefined();
340
416
  await press($, 'approve-confirm', APPROVE);
341
417
  expect(w.acts).toEqual([
342
418
  ['approve', 'dep-dayjs', '--lane', 'web', '--reason', reason, '--via', 'pane'],
@@ -558,7 +634,7 @@ test('CARD-1 a refused write is drawn as a card, its text a press away', async (
558
634
  test('CARD-1 a refusal an approval would allow has Approve…, which opens the confirmation', async ($, on) => {
559
635
  const w = project();
560
636
  w.refusal =
561
- 'Blocked: a new dependency (dayjs) needs a person\'s approval. Write a short change request for the lead.\n ! echo "<what you are approving>" > .claude/approvals/web/dep-dayjs\n(a person runs it; it allows installing it for the web lane, for 60 minutes)\nCommand: npm install dayjs';
637
+ 'Blocked: a new dependency (dayjs) needs a person\'s approval. Write a short change request for the lead.\n ! echo "<what you are approving>" > .claude/approvals/web/dep-dayjs\n(a person runs it; it allows installing it for the web lane, until the install is committed, at most 7 days)\nCommand: npm install dayjs';
562
638
  await start($, on, w);
563
639
  await $.tool.call({ tool: 'Bash', tool_use_id: 'tu-2', command: 'npm install dayjs' } as any);
564
640
  const ui = await toolResult($, 'tu-2', 'Bash');
@@ -622,7 +698,7 @@ test('CARD-4 outside a code-kit project, /approvals and /verify-branch say so',
622
698
  });
623
699
 
624
700
  const install =
625
- 'Blocked: a new dependency (dayjs) needs a person\'s approval. Write a short change request for the lead.\n ! echo "<what you are approving>" > .claude/approvals/dep-dayjs\n(a person runs it; it allows installing it for any agent, for 60 minutes)\nCommand: npm install dayjs';
701
+ 'Blocked: a new dependency (dayjs) needs a person\'s approval. Write a short change request for the lead.\n ! echo "<what you are approving>" > .claude/approvals/dep-dayjs\n(a person runs it; it allows installing it for any agent, until the install is committed, at most 7 days)\nCommand: npm install dayjs';
626
702
 
627
703
  test('CARD-1 a group holding a refusal unfolds, so its row can be the card', async ($, on) => {
628
704
  const w = project();
@@ -712,3 +788,228 @@ test(
712
788
  await ui.unmount();
713
789
  },
714
790
  );
791
+
792
+ // --- the harness settings -------------------------------------------------------------------------
793
+
794
+ const harness = ($: any) => $.command.run({ command: 'harness', args: '' });
795
+
796
+ test('SET-2 /harness opens the settings, each with its value, and /harness again closes it', async ($, on) => {
797
+ const w = project();
798
+ await start($, on, w);
799
+ await harness($);
800
+ expect(w.opened).toContain(SETTINGS);
801
+ const ui = await pane($, SETTINGS);
802
+ expect(await ui.find({ type: 'Text', text: 'hold.minutes' })).toBeDefined();
803
+ expect((await ui.find({ key: 'set-hold.minutes' }))?.props.value).toBe('2');
804
+ expect((await ui.find({ key: 'set-autonomy' }))?.props.value).toBe('autonomous');
805
+ await ui.unmount();
806
+ await harness($);
807
+ expect(w.closed).toContain(SETTINGS);
808
+ });
809
+
810
+ test('SET-2 a change asks for a reason, then is written by the CLI as the pane', async ($, on) => {
811
+ const w = project();
812
+ await start($, on, w);
813
+ await harness($);
814
+ const ui = await pane($, SETTINGS);
815
+ await $.ui.select({
816
+ plugin: 'code-kit',
817
+ key: 'set-autonomy',
818
+ value: 'propose',
819
+ requestId: SETTINGS,
820
+ });
821
+ expect(w.acts).toEqual([]);
822
+ expect(await ui.find({ type: 'Text', text: /Set autonomy to propose\?/ })).toBeDefined();
823
+ await $.ui.input({ plugin: 'code-kit', key: 'settings-reason', text: ' ' });
824
+ expect(w.acts).toEqual([]);
825
+ expect(await ui.find({ type: 'Text', text: /Give a reason/ })).toBeDefined();
826
+ await $.ui.input({ plugin: 'code-kit', key: 'settings-reason', text: 'watch it for a week' });
827
+ expect(w.acts).toEqual([
828
+ ['settings', 'set', 'autonomy', 'propose', '--reason', 'watch it for a week', '--via', 'pane'],
829
+ ]);
830
+ expect(await ui.find({ key: 'settings-confirm' })).toBeUndefined();
831
+ expect((await ui.find({ key: 'set-autonomy' }))?.props.value).toBe('propose');
832
+ await ui.unmount();
833
+ });
834
+
835
+ test('SET-2 a number is typed; one the CLI refuses shows why and changes nothing', async ($, on) => {
836
+ const w = project();
837
+ w.setExit = 1;
838
+ await start($, on, w);
839
+ await harness($);
840
+ const ui = await pane($, SETTINGS);
841
+ await $.ui.input({ plugin: 'code-kit', key: 'set-hold.minutes', text: '45' });
842
+ await $.ui.input({ plugin: 'code-kit', key: 'settings-reason', text: 'longer' });
843
+ expect(await ui.find({ type: 'Text', text: /from 0 \(off\) to 30/ })).toBeDefined();
844
+ expect(w.settings[1].value).toBe(2);
845
+ await press($, 'settings-cancel', SETTINGS);
846
+ expect(await ui.find({ key: 'settings-confirm' })).toBeUndefined();
847
+ await ui.unmount();
848
+ });
849
+
850
+ test("SET-2 the reviewer's model can go back to the session's", async ($, on) => {
851
+ const w = project();
852
+ w.settings[3].value = 'haiku';
853
+ await start($, on, w);
854
+ await harness($);
855
+ const ui = await pane($, SETTINGS);
856
+ expect(await ui.find({ type: 'Text', text: /default the session's/ })).toBeDefined();
857
+ await $.ui.select({
858
+ plugin: 'code-kit',
859
+ key: 'set-agents.reviewer.model',
860
+ value: '',
861
+ requestId: SETTINGS,
862
+ });
863
+ await press($, 'settings-ok', SETTINGS);
864
+ expect(w.acts).toEqual([]);
865
+ await $.ui.input({ plugin: 'code-kit', key: 'settings-reason', text: 'same as the lead' });
866
+ expect(w.acts[0].slice(0, 4)).toEqual(['settings', 'set', 'agents.reviewer.model', 'null']);
867
+ await ui.unmount();
868
+ });
869
+
870
+ test('CARD-4 outside a code-kit project, /harness says so and opens nothing', async ($, on) => {
871
+ const w = project();
872
+ w.check.exists = false;
873
+ w.check.valid = false;
874
+ await start($, on, w);
875
+ const said = await harness($);
876
+ expect(String(said?.text)).toMatch(/code-kit/);
877
+ expect(w.opened).not.toContain(SETTINGS);
878
+ });
879
+
880
+ // --- usage per lane ---------------------------------------------------------------------------------
881
+
882
+ const working = (id: string, type: string, description: string) => ({
883
+ id,
884
+ type,
885
+ description,
886
+ status: 'running',
887
+ });
888
+
889
+ test("USE-1 a lane agent's requests show on its lane's row and its story", async ($, on) => {
890
+ const w = project();
891
+ w.agents = [working('a1', 'web-engineer', 'Build ST-4: the booking form')];
892
+ const clock = await start($, on, w);
893
+ await lanes($);
894
+ await request($, 'a1', 120);
895
+ await request($, 'a1', 80);
896
+ await clock.advance(2000);
897
+ const ui = await pane($, PANE);
898
+ expect((await ui.find({ key: 'lane-web' }))?.text).toContain('200k');
899
+ expect((await ui.find({ key: 'usage-ST-4' }))?.text).toContain('200k');
900
+ expect((await ui.find({ key: 'lane-api' }))?.text).not.toMatch(/\d+k/);
901
+ await ui.unmount();
902
+ });
903
+
904
+ test("USE-1 the lead's requests are the lead's; an agent's brief names its story", async ($, on) => {
905
+ const w = project();
906
+ w.agents = [working('a2', 'core-engineer', 'ST-9 pricing rules')];
907
+ await start($, on, w);
908
+ await lanes($);
909
+ await request($, undefined, 30);
910
+ await request($, 'a2', 50);
911
+ const ui = await pane($, PANE);
912
+ expect((await ui.find({ key: 'lane-lead' }))?.text).toContain('30k');
913
+ expect((await ui.find({ key: 'lane-core' }))?.text).toContain('50k');
914
+ expect((await ui.find({ key: 'usage-ST-9' }))?.text).toContain('50k');
915
+ await ui.unmount();
916
+ });
917
+
918
+ test("USE-2 the background agents' share and the plan's 5-hour use", async ($, on) => {
919
+ const w = project();
920
+ w.limits = [{ kind: 'five_hour', percentUsed: 62 }];
921
+ w.agents = [
922
+ working('a1', 'web-engineer', 'ST-4 booking form'),
923
+ working('r1', 'code-reviewer', 'Review web/st-4'),
924
+ ];
925
+ await start($, on, w);
926
+ await lanes($);
927
+ await request($, 'a1', 150);
928
+ await request($, 'r1', 50);
929
+ const ui = await pane($, PANE);
930
+ expect(await ui.find({ type: 'Text', text: 'Background agents 50k (25%)' })).toBeDefined();
931
+ expect(await ui.find({ type: 'Text', text: '62% of the 5-hour window' })).toBeDefined();
932
+ expect(await ui.find({ type: 'Text', text: 'This session 200k tokens' })).toBeDefined();
933
+ await ui.unmount();
934
+ });
935
+
936
+ test('USE-3 a story past three times the median of three finished ones is flagged in the band', async ($, on) => {
937
+ const w = project();
938
+ w.stories = [
939
+ ...['ST-1', 'ST-2', 'ST-3'].map((id) => ({
940
+ id,
941
+ title: id,
942
+ lane: 'api',
943
+ state: 'done',
944
+ branch: `api/${id.toLowerCase()}`,
945
+ waitingOn: [],
946
+ })),
947
+ ...w.stories,
948
+ ];
949
+ w.agents = ['ST-1', 'ST-2', 'ST-3', 'ST-4'].map((id, i) =>
950
+ working(`a${i}`, i < 3 ? 'api-engineer' : 'web-engineer', `${id} work`),
951
+ );
952
+ await start($, on, w);
953
+ await request($, 'a0', 190);
954
+ await request($, 'a1', 200);
955
+ await request($, 'a2', 210);
956
+ await request($, 'a3', 500);
957
+ let ui = await bandUi($);
958
+ expect(await ui.find({ type: 'Text', text: /has used/ })).toBeUndefined();
959
+ await ui.unmount();
960
+ await request($, 'a3', 200);
961
+ ui = await bandUi($);
962
+ expect(await ui.find({ type: 'Text', text: 'ST-4 has used 3.5× the usual' })).toBeDefined();
963
+ await ui.unmount();
964
+ });
965
+
966
+ test('USE-3 with fewer than three finished, nothing is flagged', async ($, on) => {
967
+ const w = project();
968
+ w.stories = [
969
+ { id: 'ST-1', title: 'a', lane: 'api', state: 'done', branch: 'api/st-1', waitingOn: [] },
970
+ ...w.stories,
971
+ ];
972
+ w.agents = [working('a0', 'api-engineer', 'ST-1'), working('a1', 'web-engineer', 'ST-4')];
973
+ await start($, on, w);
974
+ await request($, 'a0', 10);
975
+ await request($, 'a1', 900);
976
+ const ui = await bandUi($);
977
+ expect(await ui.find({ type: 'Text', text: /has used/ })).toBeUndefined();
978
+ await ui.unmount();
979
+ });
980
+
981
+ test('USE-4 past the pause point the band says background agents are paused, and below it not', async ($, on) => {
982
+ const w = project();
983
+ await start($, on, w);
984
+ const measure = (percentUsed: number) =>
985
+ $.session.measure({
986
+ context: {} as any,
987
+ rateLimits: [{ kind: 'five_hour', percentUsed }],
988
+ changed: ['rateLimits'],
989
+ });
990
+ await measure(82);
991
+ let ui = await bandUi($);
992
+ expect(
993
+ await ui.find({ type: 'Text', text: 'background agents paused: plan at 82%' }),
994
+ ).toBeDefined();
995
+ await ui.unmount();
996
+ await measure(79);
997
+ ui = await bandUi($);
998
+ expect(await ui.find({ type: 'Text', text: /paused/ })).toBeUndefined();
999
+ await ui.unmount();
1000
+ });
1001
+
1002
+ test('USE-4 the pause point is the harness setting', async ($, on) => {
1003
+ const w = project();
1004
+ (w.check as any).harness = { background: { pauseAtPercent: 90 } };
1005
+ w.limits = [{ kind: 'five_hour', percentUsed: 85 }];
1006
+ await start($, on, w);
1007
+ let ui = await bandUi($);
1008
+ expect(await ui.find({ type: 'Text', text: /paused/ })).toBeUndefined();
1009
+ await ui.unmount();
1010
+ w.limits = [{ kind: 'five_hour', percentUsed: 91 }];
1011
+ await $.session.measure({ context: {} as any, rateLimits: w.limits, changed: ['rateLimits'] });
1012
+ ui = await bandUi($);
1013
+ expect(await ui.find({ type: 'Text', text: /plan at 91%/ })).toBeDefined();
1014
+ await ui.unmount();
1015
+ });