diffsplain 0.8.2 → 0.9.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.
Files changed (93) hide show
  1. package/README.md +10 -5
  2. package/dist/assets/{angular-html-CphzFC_v.js → angular-html-m_m91ZYB.js} +1 -1
  3. package/dist/assets/{angular-ts-Am0-mbFw.js → angular-ts-CO5Z--Rb.js} +1 -1
  4. package/dist/assets/{apl-Cs2rPKDN.js → apl-ogYYgLUu.js} +1 -1
  5. package/dist/assets/{astro-CxKUUn9h.js → astro-DAfY3cNR.js} +1 -1
  6. package/dist/assets/{blade-DqeCa6v5.js → blade-E6RzsLUV.js} +1 -1
  7. package/dist/assets/{c-DdI6lknk.js → c-kKdxhtHF.js} +1 -1
  8. package/dist/assets/{cobol-COuAGE2X.js → cobol-3oDDyFNf.js} +1 -1
  9. package/dist/assets/{coffee-BFlYT3Lp.js → coffee-dsiILuWe.js} +1 -1
  10. package/dist/assets/{cpp-nywLU0Np.js → cpp-DYyNFq__.js} +1 -1
  11. package/dist/assets/{crystal-BWp4dz1s.js → crystal-B7dVp9TF.js} +1 -1
  12. package/dist/assets/{css-CRb9tUpU.js → css-DlxJ0aex.js} +1 -1
  13. package/dist/assets/{edge-rUYSbJy4.js → edge-BjEVphkC.js} +1 -1
  14. package/dist/assets/{elixir-DOq9ybIH.js → elixir-0xTOtMcM.js} +1 -1
  15. package/dist/assets/{elm-C8e38E5-.js → elm-iQk_Wrxq.js} +1 -1
  16. package/dist/assets/{erb-BR9Q37Et.js → erb-BO74ztiE.js} +1 -1
  17. package/dist/assets/{git-rebase-Coo2LUtB.js → git-rebase-BN667QRY.js} +1 -1
  18. package/dist/assets/{glimmer-js-C7hX9jg6.js → glimmer-js-DTGUIHeS.js} +1 -1
  19. package/dist/assets/{glimmer-ts-CZLT0F0k.js → glimmer-ts-BU3oQn2d.js} +1 -1
  20. package/dist/assets/{glsl-BDZRv6e-.js → glsl-KLiqLJi2.js} +1 -1
  21. package/dist/assets/{graphql-DyzFc3y3.js → graphql-D_idcsoJ.js} +1 -1
  22. package/dist/assets/{hack-CWnO7WsF.js → hack-BXlsgPIr.js} +1 -1
  23. package/dist/assets/{haml-CvDj-ghO.js → haml-fP0aUg4-.js} +1 -1
  24. package/dist/assets/{handlebars-BnU_YcVA.js → handlebars-C1ZEaot-.js} +1 -1
  25. package/dist/assets/{html-CjB2TKKa.js → html-D04qIuup.js} +1 -1
  26. package/dist/assets/{html-derivative-CYQqEXS6.js → html-derivative-C6ai-9QL.js} +1 -1
  27. package/dist/assets/{http-NNQbnqIx.js → http-C7bz3ZvQ.js} +1 -1
  28. package/dist/assets/{hurl-DHaAA1BS.js → hurl-jL6B6Ahr.js} +1 -1
  29. package/dist/assets/index-BPaJGi-m.js +1574 -0
  30. package/dist/assets/index-VAyBpb3u.css +2 -0
  31. package/dist/assets/{java-D1J5V2CD.js → java-BVzvLCON.js} +1 -1
  32. package/dist/assets/{javascript-C3pX-HsE.js → javascript-UDbZHxCI.js} +1 -1
  33. package/dist/assets/{jinja-BY9CIY8g.js → jinja-DB-QKo2A.js} +1 -1
  34. package/dist/assets/{jison-Cr0H89gS.js → jison-B1ZbR2FF.js} +1 -1
  35. package/dist/assets/{json-CA3kKNfq.js → json-DV4WwJzT.js} +1 -1
  36. package/dist/assets/{jsx-BuMkR-Lj.js → jsx-DS6_VTvW.js} +1 -1
  37. package/dist/assets/{julia-C2sLwg3f.js → julia-C4tevET0.js} +1 -1
  38. package/dist/assets/{just-pXpkLBXv.js → just-CWMjefzB.js} +1 -1
  39. package/dist/assets/{latex-7v3EPOdF.js → latex-tyEFf_Af.js} +1 -1
  40. package/dist/assets/{liquid-CJZq0Z0t.js → liquid-C3d1X1rL.js} +1 -1
  41. package/dist/assets/{lua-BimBzvTH.js → lua-sXvrzLQY.js} +1 -1
  42. package/dist/assets/{marko-CCZHVnQN.js → marko-CQweucif.js} +1 -1
  43. package/dist/assets/{mdc-SLECNw2A.js → mdc-TrxLWF7s.js} +1 -1
  44. package/dist/assets/{nginx-DUwtUge2.js → nginx-ChE2nSNJ.js} +1 -1
  45. package/dist/assets/{nim-HD8o_Omp.js → nim-BY1CTRfx.js} +1 -1
  46. package/dist/assets/{perl-BGMeIkNk.js → perl-Cvj2PQQH.js} +1 -1
  47. package/dist/assets/{php-BL5xuY-v.js → php-CGmHp8HW.js} +1 -1
  48. package/dist/assets/{pug-C8G1U_u8.js → pug-DM4S0bRr.js} +1 -1
  49. package/dist/assets/{qml-BGVSyw1x.js → qml-DI89Al9K.js} +1 -1
  50. package/dist/assets/{r-BDnRcclC.js → r-Bs5_4yYh.js} +1 -1
  51. package/dist/assets/{razor-I8tbEIVJ.js → razor-DimzayhZ.js} +1 -1
  52. package/dist/assets/{regexp-D0yakB9Q.js → regexp-BLACz0cx.js} +1 -1
  53. package/dist/assets/{rst-DbTCn07T.js → rst-DcWzxQh2.js} +1 -1
  54. package/dist/assets/{ruby-BwgPFIcO.js → ruby-Dlat5E8d.js} +1 -1
  55. package/dist/assets/{sas-dN4ePfXU.js → sas-CCDmMaBY.js} +1 -1
  56. package/dist/assets/{scss-T7M1A4iM.js → scss-BPiU2nu6.js} +1 -1
  57. package/dist/assets/{shellscript-HFjBHS7_.js → shellscript-CKinVNrN.js} +1 -1
  58. package/dist/assets/{shellsession-DCAGskrx.js → shellsession-Cea8Pu5d.js} +1 -1
  59. package/dist/assets/{soy-BgT96TF9.js → soy--BRAPY4-.js} +1 -1
  60. package/dist/assets/{sql-DCIQeRzx.js → sql-DjKiCyWK.js} +1 -1
  61. package/dist/assets/{stata-Dn0tad0_.js → stata-CDvo3J6y.js} +1 -1
  62. package/dist/assets/{surrealql-CCgFEQfW.js → surrealql-C7b7E5sh.js} +1 -1
  63. package/dist/assets/{svelte-ITXimG8u.js → svelte-DO0tKtiK.js} +1 -1
  64. package/dist/assets/{templ-DJVZZqPw.js → templ-DhhmR_rv.js} +1 -1
  65. package/dist/assets/{tex-Bn4N-OtH.js → tex-Cn_V-Gg4.js} +1 -1
  66. package/dist/assets/{ts-tags-CxQ4PoC4.js → ts-tags-B6c-FTmA.js} +1 -1
  67. package/dist/assets/{tsx-CxwgcjkC.js → tsx-Cvtt2sJ-.js} +1 -1
  68. package/dist/assets/{twig-CtbyTyMH.js → twig-B7ZbCUmB.js} +1 -1
  69. package/dist/assets/{typescript-d6ru12L8.js → typescript-i3C6um2B.js} +1 -1
  70. package/dist/assets/{vue-RvqAsYDm.js → vue-DyPX0Vum.js} +1 -1
  71. package/dist/assets/{vue-html-BkHDVENe.js → vue-html-D362KpK3.js} +1 -1
  72. package/dist/assets/{vue-vine-DvS3Mlkn.js → vue-vine-D34cBbwO.js} +1 -1
  73. package/dist/assets/{xml-lzTq5A-V.js → xml-DJEk7u4n.js} +1 -1
  74. package/dist/assets/{xsl-BL0KXNWI.js → xsl-CoTtf90f.js} +1 -1
  75. package/dist/assets/{yaml-CF4XlBJS.js → yaml-Cv5epQ4-.js} +1 -1
  76. package/dist/index.html +2 -2
  77. package/package.json +11 -3
  78. package/scripts/agent-exclusions.mjs +10 -0
  79. package/scripts/agent-review.mjs +94 -0
  80. package/scripts/build-diff-data.mjs +235 -50
  81. package/scripts/cli-args.mjs +36 -9
  82. package/scripts/coding-agents.mjs +206 -79
  83. package/scripts/generate-summaries.mjs +864 -425
  84. package/scripts/local-target.mjs +106 -0
  85. package/scripts/present.mjs +205 -72
  86. package/scripts/review-chat-context.mjs +510 -0
  87. package/scripts/review-chat-controller.mjs +568 -0
  88. package/scripts/review-chat-provider.mjs +352 -0
  89. package/scripts/review-chat.mjs +8 -0
  90. package/scripts/serve-built.mjs +206 -24
  91. package/scripts/summary-path.mjs +32 -11
  92. package/dist/assets/index-C2p_1iAk.css +0 -2
  93. package/dist/assets/index-f9ekN94b.js +0 -1568
@@ -1,8 +1,8 @@
1
1
  import { constants } from 'node:fs';
2
2
  import { access } from 'node:fs/promises';
3
3
  import { spawnSync } from 'node:child_process';
4
+ import { createInterface } from 'node:readline';
4
5
  import {
5
- basename,
6
6
  delimiter,
7
7
  dirname,
8
8
  isAbsolute,
@@ -207,7 +207,7 @@ export async function findCommand(
207
207
  return findOnPath(command, env, platform);
208
208
  }
209
209
 
210
- export async function commandAvailable(command, options) {
210
+ async function commandAvailable(command, options) {
211
211
  return Boolean(await findCommand(command, options));
212
212
  }
213
213
 
@@ -234,44 +234,127 @@ export async function codingAgentAvailability(
234
234
  };
235
235
  }
236
236
 
237
- // fallow-ignore-next-line complexity -- validation and discovery share one public selector.
238
- export async function selectCodingAgent(
239
- requested,
240
- available = commandAvailable,
241
- ) {
242
- if (requested) {
243
- if (!codingAgents.includes(requested)) {
244
- throw new Error(
245
- `Unsupported agent "${requested}". Choose ${enabledCodingAgents.join(', ')}.`,
246
- );
247
- }
248
- const result = await available(requested);
249
- const availableResult = typeof result === 'object'
250
- ? result.available
251
- : result;
252
- if (!availableResult) {
253
- if (typeof result === 'object' && result.reason) {
254
- throw new Error(result.reason);
255
- }
256
- throw new Error(`Coding agent "${requested}" is not available.`);
257
- }
258
- return requested;
237
+ function availableResult(result) {
238
+ return typeof result === 'object' ? result.available : result;
239
+ }
240
+
241
+ function unavailableAgentError(requested, result) {
242
+ if (typeof result === 'object' && result.reason) {
243
+ return new Error(result.reason);
259
244
  }
245
+ return new Error(`Coding agent "${requested}" is not available.`);
246
+ }
247
+
248
+ function noAvailableAgentError(cursorReason) {
249
+ return new Error(
250
+ `No coding agent is available. Install one of: ${enabledCodingAgents.join(', ')}. Pass --agent NAME to choose an agent or --no-agent for a plain review.${cursorReason ? ` ${cursorReason}` : ''}`,
251
+ );
252
+ }
260
253
 
254
+ function selectionCancelledError() {
255
+ return new Error(
256
+ 'Agent selection was cancelled. Pass --agent NAME to choose an agent or --no-agent for a plain review.',
257
+ );
258
+ }
259
+
260
+ function nonInteractiveSelectionError() {
261
+ return new Error(
262
+ 'An interactive terminal is required to choose a coding agent. Pass --agent NAME to choose an agent or --no-agent for a plain review.',
263
+ );
264
+ }
265
+
266
+ async function selectRequestedCodingAgent(requested, available) {
267
+ if (!codingAgents.includes(requested)) {
268
+ throw new Error(
269
+ `Unsupported agent "${requested}". Choose ${enabledCodingAgents.join(', ')}.`,
270
+ );
271
+ }
272
+ const result = await available(requested);
273
+ if (!availableResult(result)) {
274
+ throw unavailableAgentError(requested, result);
275
+ }
276
+ return requested;
277
+ }
278
+
279
+ async function usableCodingAgents(available) {
280
+ const usable = [];
261
281
  let cursorReason;
262
282
  for (const agent of enabledCodingAgents) {
263
283
  const result = await available(agent);
264
- const availableResult = typeof result === 'object'
265
- ? result.available
266
- : result;
267
- if (availableResult) return agent;
268
- if (agent === 'cursor' && typeof result === 'object') {
284
+ if (availableResult(result)) {
285
+ usable.push(agent);
286
+ } else if (agent === 'cursor' && typeof result === 'object') {
269
287
  cursorReason = result.reason;
270
288
  }
271
289
  }
272
- throw new Error(
273
- `No coding agent is available. Install one of: ${enabledCodingAgents.join(', ')}.${cursorReason ? ` ${cursorReason}` : ''}`,
274
- );
290
+ return { usable, cursorReason };
291
+ }
292
+
293
+ function selectFromTerminal(agents, { input, output, signal }) {
294
+ output.write('Choose a coding agent:\n');
295
+ for (const [index, agent] of agents.entries()) {
296
+ output.write(`${index + 1}. ${agent}\n`);
297
+ }
298
+ output.write('Enter a number, or press Ctrl+C to cancel: ');
299
+
300
+ return new Promise((resolve, reject) => {
301
+ const picker = createInterface({ input, output, terminal: true });
302
+ let settled = false;
303
+ const finish = (callback, value) => {
304
+ if (settled) return;
305
+ settled = true;
306
+ signal?.removeEventListener('abort', onAbort);
307
+ picker.close();
308
+ callback(value);
309
+ };
310
+ const onAbort = () => {
311
+ finish(reject, selectionCancelledError());
312
+ };
313
+ const promptAgain = () => {
314
+ output.write(`Choose a number from 1 to ${agents.length}, or press Ctrl+C to cancel: `);
315
+ };
316
+ signal?.addEventListener('abort', onAbort, { once: true });
317
+ if (signal?.aborted) onAbort();
318
+ picker.on('SIGINT', () => {
319
+ finish(reject, selectionCancelledError());
320
+ });
321
+ picker.on('close', () => {
322
+ finish(reject, selectionCancelledError());
323
+ });
324
+ picker.on('line', (line) => {
325
+ const selected = Number(line.trim());
326
+ if (
327
+ !Number.isInteger(selected) ||
328
+ selected < 1 ||
329
+ selected > agents.length
330
+ ) {
331
+ promptAgain();
332
+ return;
333
+ }
334
+ finish(resolve, agents[selected - 1]);
335
+ });
336
+ });
337
+ }
338
+
339
+ export async function selectCodingAgent(
340
+ requested,
341
+ {
342
+ available = commandAvailable,
343
+ input = process.stdin,
344
+ output = process.stderr,
345
+ signal,
346
+ } = {},
347
+ ) {
348
+ if (requested) {
349
+ return selectRequestedCodingAgent(requested, available);
350
+ }
351
+
352
+ if (!input?.isTTY || !output?.isTTY) {
353
+ throw nonInteractiveSelectionError();
354
+ }
355
+ const { usable, cursorReason } = await usableCodingAgents(available);
356
+ if (!usable.length) throw noAvailableAgentError(cursorReason);
357
+ return selectFromTerminal(usable, { input, output, signal });
275
358
  }
276
359
 
277
360
  // fallow-ignore-next-line complexity -- each supported agent has one override rule.
@@ -391,6 +474,33 @@ export function parseAgentResponse(agent, stdout) {
391
474
  return parseJsonText(stdout, label);
392
475
  }
393
476
 
477
+ function codexIsolationArgs(snapshotOnly) {
478
+ return snapshotOnly
479
+ ? ['--ignore-user-config', '--ignore-rules']
480
+ : [];
481
+ }
482
+
483
+ function codexConfigArgs(snapshotOnly) {
484
+ return snapshotOnly
485
+ ? [
486
+ '--config',
487
+ 'mcp_servers={}',
488
+ '--config',
489
+ 'plugins={}',
490
+ '--config',
491
+ 'shell_environment_policy.inherit="none"',
492
+ '--config',
493
+ 'sandbox_workspace_write.network_access=false',
494
+ '--config',
495
+ 'web_search="disabled"',
496
+ ]
497
+ : [];
498
+ }
499
+
500
+ function codexRepoArgs(snapshotOnly) {
501
+ return snapshotOnly ? ['--skip-git-repo-check'] : [];
502
+ }
503
+
394
504
  function codexCommand({
395
505
  binary,
396
506
  model,
@@ -399,31 +509,22 @@ function codexCommand({
399
509
  schemaPath,
400
510
  summaryDirectory,
401
511
  summaryEnv,
512
+ snapshotOnly,
402
513
  }) {
403
514
  const args = [
404
515
  'exec',
405
516
  '--ephemeral',
406
517
  '--sandbox',
407
518
  'read-only',
408
- '--ignore-user-config',
409
- '--ignore-rules',
519
+ ...codexIsolationArgs(snapshotOnly),
410
520
  '--color',
411
521
  'never',
412
- '--skip-git-repo-check',
522
+ ...codexRepoArgs(snapshotOnly),
413
523
  '-C',
414
524
  summaryDirectory,
415
525
  '--output-schema',
416
526
  schemaPath,
417
- '--config',
418
- 'mcp_servers={}',
419
- '--config',
420
- 'plugins={}',
421
- '--config',
422
- 'shell_environment_policy.inherit="none"',
423
- '--config',
424
- 'sandbox_workspace_write.network_access=false',
425
- '--config',
426
- 'web_search="disabled"',
527
+ ...codexConfigArgs(snapshotOnly),
427
528
  ];
428
529
  if (model) args.push('--model', model);
429
530
  if (reasoning) {
@@ -449,6 +550,7 @@ function claudeCommand({
449
550
  schema,
450
551
  summaryDirectory,
451
552
  summaryEnv,
553
+ snapshotOnly,
452
554
  }) {
453
555
  const args = [
454
556
  '--print',
@@ -456,8 +558,7 @@ function claudeCommand({
456
558
  'json',
457
559
  '--json-schema',
458
560
  JSON.stringify(schema),
459
- '--tools',
460
- '',
561
+ ...(snapshotOnly ? ['--tools', ''] : ['--permission-mode', 'plan']),
461
562
  '--no-session-persistence',
462
563
  ];
463
564
  if (model) args.push('--model', model);
@@ -479,13 +580,14 @@ function copilotCommand({
479
580
  schema,
480
581
  summaryDirectory,
481
582
  summaryEnv,
583
+ snapshotOnly,
482
584
  }) {
483
585
  const schemaText = JSON.stringify(schema);
484
586
  const args = [
485
587
  '--silent',
486
- '--no-ask-user',
588
+ ...(snapshotOnly ? ['--no-ask-user'] : []),
487
589
  '--no-color',
488
- '--no-custom-instructions',
590
+ ...(snapshotOnly ? ['--no-custom-instructions'] : []),
489
591
  '--no-remote',
490
592
  '--no-remote-export',
491
593
  `--add-dir=${dirname(inputPath)}`,
@@ -504,6 +606,26 @@ function copilotCommand({
504
606
  };
505
607
  }
506
608
 
609
+ function openCodeIsolationArgs(snapshotOnly) {
610
+ return snapshotOnly ? ['--pure'] : [];
611
+ }
612
+
613
+ function openCodeEnvironment(snapshotOnly, summaryEnv) {
614
+ if (!snapshotOnly) return summaryEnv;
615
+ return {
616
+ ...summaryEnv,
617
+ OPENCODE_DB: ':memory:',
618
+ OPENCODE_CONFIG_CONTENT: JSON.stringify({
619
+ permission: { '*': 'deny' },
620
+ agent: {
621
+ build: {
622
+ permission: { '*': 'deny' },
623
+ },
624
+ },
625
+ }),
626
+ };
627
+ }
628
+
507
629
  function openCodeCommand({
508
630
  binary,
509
631
  model,
@@ -512,10 +634,11 @@ function openCodeCommand({
512
634
  schema,
513
635
  summaryDirectory,
514
636
  summaryEnv,
637
+ snapshotOnly,
515
638
  }) {
516
639
  const args = [
517
640
  'run',
518
- '--pure',
641
+ ...openCodeIsolationArgs(snapshotOnly),
519
642
  '--format',
520
643
  'json',
521
644
  '--dir',
@@ -533,24 +656,12 @@ function openCodeCommand({
533
656
  args,
534
657
  input: 'stdin',
535
658
  cwd: summaryDirectory,
536
- env: {
537
- ...summaryEnv,
538
- OPENCODE_DB: ':memory:',
539
- OPENCODE_CONFIG_CONTENT: JSON.stringify({
540
- permission: { '*': 'deny' },
541
- agent: {
542
- build: {
543
- permission: { '*': 'deny' },
544
- },
545
- },
546
- }),
547
- },
659
+ env: openCodeEnvironment(snapshotOnly, summaryEnv),
548
660
  };
549
661
  }
550
662
 
551
663
  function cursorCommand({
552
664
  binary,
553
- inputPath,
554
665
  model,
555
666
  prompt,
556
667
  schema,
@@ -572,26 +683,28 @@ function cursorCommand({
572
683
  ];
573
684
  if (model) args.push('--model', model);
574
685
  args.push(
575
- `${prompt}\n\nRead the snapshot JSON from ${basename(inputPath)} in this workspace. Return JSON that matches this schema:\n${JSON.stringify(schema)}`,
686
+ `${prompt}\n\nThe snapshot JSON follows this prompt on standard input. Return JSON that matches this schema:\n${JSON.stringify(schema)}`,
576
687
  );
577
688
  return {
578
689
  command: binary,
579
690
  args,
580
691
  input: 'stdin',
581
692
  cwd: summaryDirectory,
582
- env: {
583
- ...summaryEnv,
584
- ...(sourceEnv.XDG_CONFIG_HOME
585
- ? { XDG_CONFIG_HOME: sourceEnv.XDG_CONFIG_HOME }
586
- : {}),
587
- ...(sourceEnv.APPDATA ? { APPDATA: sourceEnv.APPDATA } : {}),
588
- ...(sourceEnv.CURSOR_API_KEY
589
- ? { CURSOR_API_KEY: sourceEnv.CURSOR_API_KEY }
590
- : {}),
591
- ...(sourceEnv.CURSOR_AUTH_TOKEN
592
- ? { CURSOR_AUTH_TOKEN: sourceEnv.CURSOR_AUTH_TOKEN }
593
- : {}),
594
- },
693
+ env: summaryEnv === sourceEnv
694
+ ? summaryEnv
695
+ : {
696
+ ...summaryEnv,
697
+ ...(sourceEnv.XDG_CONFIG_HOME
698
+ ? { XDG_CONFIG_HOME: sourceEnv.XDG_CONFIG_HOME }
699
+ : {}),
700
+ ...(sourceEnv.APPDATA ? { APPDATA: sourceEnv.APPDATA } : {}),
701
+ ...(sourceEnv.CURSOR_API_KEY
702
+ ? { CURSOR_API_KEY: sourceEnv.CURSOR_API_KEY }
703
+ : {}),
704
+ ...(sourceEnv.CURSOR_AUTH_TOKEN
705
+ ? { CURSOR_AUTH_TOKEN: sourceEnv.CURSOR_AUTH_TOKEN }
706
+ : {}),
707
+ },
595
708
  };
596
709
  }
597
710
 
@@ -604,8 +717,11 @@ export function agentCommand({
604
717
  schema,
605
718
  schemaPath,
606
719
  inputPath,
720
+ accessMode,
607
721
  env = process.env,
608
722
  }) {
723
+ const snapshotDirectory = dirname(inputPath);
724
+ const checkoutAccess = accessMode?.mode === 'checkout-read-only';
609
725
  const options = {
610
726
  binary,
611
727
  inputPath,
@@ -614,8 +730,9 @@ export function agentCommand({
614
730
  reasoning,
615
731
  schema,
616
732
  schemaPath,
617
- summaryDirectory: dirname(inputPath),
618
- summaryEnv: summaryAgentEnvironment(env),
733
+ summaryDirectory: checkoutAccess ? accessMode.root : snapshotDirectory,
734
+ summaryEnv: checkoutAccess ? env : summaryAgentEnvironment(env),
735
+ snapshotOnly: !checkoutAccess,
619
736
  sourceEnv: env,
620
737
  };
621
738
  if (agent === 'codex') return codexCommand(options);
@@ -624,3 +741,13 @@ export function agentCommand({
624
741
  if (agent === 'cursor') return cursorCommand(options);
625
742
  return openCodeCommand(options);
626
743
  }
744
+
745
+ const readOnlyWarnings = {
746
+ copilot: 'Warning: Copilot has no proven native read-only mode. It runs with your normal permissions.',
747
+ opencode: 'Warning: OpenCode has no proven native read-only mode. It runs with your normal permissions.',
748
+ };
749
+
750
+ export function agentReadOnlyWarning(agent, accessMode) {
751
+ if (accessMode?.mode !== 'checkout-read-only') return undefined;
752
+ return readOnlyWarnings[agent];
753
+ }