diffsplain 0.8.3 → 0.10.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 (94) hide show
  1. package/README.md +26 -5
  2. package/dist/assets/{angular-html-e4D_THVy.js → angular-html-2u-u5H-N.js} +1 -1
  3. package/dist/assets/{angular-ts-Cx3870JF.js → angular-ts-4_HVGlKp.js} +1 -1
  4. package/dist/assets/{apl-DZuu22Wh.js → apl-icaNWZX3.js} +1 -1
  5. package/dist/assets/{astro-K-NemFAk.js → astro-HvIOE4vz.js} +1 -1
  6. package/dist/assets/{blade-ConbVu3U.js → blade-BpKJV8ev.js} +1 -1
  7. package/dist/assets/{c-DtmaBqln.js → c-BNvtxyRQ.js} +1 -1
  8. package/dist/assets/{cobol-BMHoPOfB.js → cobol-8js4hfxa.js} +1 -1
  9. package/dist/assets/{coffee-DfweJ19Y.js → coffee-DXg_piQ9.js} +1 -1
  10. package/dist/assets/{cpp-CPh_zIOW.js → cpp-BZDFAjyh.js} +1 -1
  11. package/dist/assets/{crystal-CAEX7C1J.js → crystal-aCF3ira4.js} +1 -1
  12. package/dist/assets/{css-C8ihGqCA.js → css-DfRtUkhE.js} +1 -1
  13. package/dist/assets/{edge-DVZPwQ9j.js → edge-vS6_AmQv.js} +1 -1
  14. package/dist/assets/{elixir-D03TKulP.js → elixir-Ce0nUHSq.js} +1 -1
  15. package/dist/assets/{elm-JUX-2xVV.js → elm-DmaeyizZ.js} +1 -1
  16. package/dist/assets/{erb-HbktAFZJ.js → erb-DD6OUyvt.js} +1 -1
  17. package/dist/assets/{git-rebase-Ccd_ELAP.js → git-rebase-HbJMn7iS.js} +1 -1
  18. package/dist/assets/{glimmer-js-BcinUfi2.js → glimmer-js-BRuqbNaV.js} +1 -1
  19. package/dist/assets/{glimmer-ts-DuN2UkN9.js → glimmer-ts-ZBpkdEEA.js} +1 -1
  20. package/dist/assets/{glsl-BWmCEPsR.js → glsl-CrQ4GScf.js} +1 -1
  21. package/dist/assets/{graphql-DJGYoBFW.js → graphql-Tn6PBdQY.js} +1 -1
  22. package/dist/assets/{hack-ZPqBE9w3.js → hack-CoApvrbo.js} +1 -1
  23. package/dist/assets/{haml-CZDZk6Gw.js → haml-COKq8Xjp.js} +1 -1
  24. package/dist/assets/{handlebars-d23PDS0n.js → handlebars-CAna2DTA.js} +1 -1
  25. package/dist/assets/{html-BgzX2DyB.js → html-Ch4xJ7YF.js} +1 -1
  26. package/dist/assets/{html-derivative-Bq6M-ZFw.js → html-derivative-0ITK-MAI.js} +1 -1
  27. package/dist/assets/{http-DVhMl_cW.js → http-DiMWQ8Qr.js} +1 -1
  28. package/dist/assets/{hurl-CbspBAqF.js → hurl-OcXymC0J.js} +1 -1
  29. package/dist/assets/index-BmcCug2M.js +1574 -0
  30. package/dist/assets/index-CADUNiTy.css +2 -0
  31. package/dist/assets/{java--YZbq0z2.js → java-CFb3qN4r.js} +1 -1
  32. package/dist/assets/{javascript-cvRuifRJ.js → javascript-CRmk2JrF.js} +1 -1
  33. package/dist/assets/{jinja-BeZmr3LP.js → jinja-Da6lDTEy.js} +1 -1
  34. package/dist/assets/{jison-qrpGZaxs.js → jison-Bnivzmtu.js} +1 -1
  35. package/dist/assets/{json-4xUzsNJn.js → json-m8vJP_KK.js} +1 -1
  36. package/dist/assets/{jsx-BlXrTlSg.js → jsx-C3HX_Sbu.js} +1 -1
  37. package/dist/assets/{julia-IN3jzmBc.js → julia-Ckg-wSHA.js} +1 -1
  38. package/dist/assets/{just-C_50nLQH.js → just-DCjIAyvO.js} +1 -1
  39. package/dist/assets/{latex-Cd-_2IS1.js → latex-CrYSm4pl.js} +1 -1
  40. package/dist/assets/{liquid-CEJf6Xg5.js → liquid-MQUFSPYU.js} +1 -1
  41. package/dist/assets/{lua-BlaV_n8i.js → lua-DcuzJJ77.js} +1 -1
  42. package/dist/assets/{marko-Cb-NPdc_.js → marko-4JNmQaB4.js} +1 -1
  43. package/dist/assets/{mdc-Dt4ywVAS.js → mdc-C_j8HvRL.js} +1 -1
  44. package/dist/assets/{nginx-C_6SKViP.js → nginx-ta3Gn1HE.js} +1 -1
  45. package/dist/assets/{nim-BbkBjoeZ.js → nim-BwWLst9M.js} +1 -1
  46. package/dist/assets/{perl-CYPU6TVa.js → perl-DfW7Uq5O.js} +1 -1
  47. package/dist/assets/{php-B6Oqilrc.js → php-DGf5G3Cf.js} +1 -1
  48. package/dist/assets/{pug-C9Poo2KU.js → pug-CWI-NaYz.js} +1 -1
  49. package/dist/assets/{qml-DkMURMPv.js → qml-Csblgh7U.js} +1 -1
  50. package/dist/assets/{r-Bm-Gfi0p.js → r-QGooaGDI.js} +1 -1
  51. package/dist/assets/{razor-B0LqOok3.js → razor-Dp9ihLYf.js} +1 -1
  52. package/dist/assets/{regexp-jAbMHzwe.js → regexp-DwUnLcVr.js} +1 -1
  53. package/dist/assets/{rst-BlWHfg8c.js → rst-BY8GIwDJ.js} +1 -1
  54. package/dist/assets/{ruby-Bt5lKwPL.js → ruby-VgzI9htT.js} +1 -1
  55. package/dist/assets/{sas-TuY2-8EF.js → sas-DYLsm-0j.js} +1 -1
  56. package/dist/assets/{scss-Dh1SsWlK.js → scss-tIFkeUQX.js} +1 -1
  57. package/dist/assets/{shellscript-CThiwmnp.js → shellscript-CHjuQ4Pl.js} +1 -1
  58. package/dist/assets/{shellsession-DQ5iRUWd.js → shellsession-Bv0S0hj9.js} +1 -1
  59. package/dist/assets/{soy-BfM-BrZG.js → soy-D6U1tZod.js} +1 -1
  60. package/dist/assets/{sql-F617W1uN.js → sql-CGz8kjKv.js} +1 -1
  61. package/dist/assets/{stata-Ctov3ki2.js → stata-C9ytERUq.js} +1 -1
  62. package/dist/assets/{surrealql-C-i2bJGB.js → surrealql-DRfRnaYu.js} +1 -1
  63. package/dist/assets/{svelte-oZT41pTl.js → svelte-wYnk_1sI.js} +1 -1
  64. package/dist/assets/{templ-CxX9Hf3e.js → templ-DWrGVE0u.js} +1 -1
  65. package/dist/assets/{tex-GyzJq8GJ.js → tex-CYTzoUJh.js} +1 -1
  66. package/dist/assets/{ts-tags-BugzKu0x.js → ts-tags-1TIXEGea.js} +1 -1
  67. package/dist/assets/{tsx-DLqwDEUq.js → tsx-a3BoAoOh.js} +1 -1
  68. package/dist/assets/{twig-DADiR9Vq.js → twig-e3dvNv6o.js} +1 -1
  69. package/dist/assets/{typescript-BlDIG_FO.js → typescript-BBhnpnTf.js} +1 -1
  70. package/dist/assets/{vue-COXmmg2A.js → vue-DzzVbu9q.js} +1 -1
  71. package/dist/assets/{vue-html-Cy_3KYj1.js → vue-html-quBNv38K.js} +1 -1
  72. package/dist/assets/{vue-vine-CUqOY8dY.js → vue-vine-CDuR69FE.js} +1 -1
  73. package/dist/assets/{xml-C9JY7Wf6.js → xml-DMifOLSe.js} +1 -1
  74. package/dist/assets/{xsl-DBCl8hSu.js → xsl-D8Ef-5HK.js} +1 -1
  75. package/dist/assets/{yaml-BbMebKkS.js → yaml-5c38gpmA.js} +1 -1
  76. package/dist/index.html +2 -2
  77. package/package.json +12 -3
  78. package/scripts/agent-config.mjs +184 -0
  79. package/scripts/agent-exclusions.mjs +10 -0
  80. package/scripts/agent-review.mjs +94 -0
  81. package/scripts/build-diff-data.mjs +211 -49
  82. package/scripts/cli-args.mjs +63 -9
  83. package/scripts/coding-agents.mjs +255 -77
  84. package/scripts/generate-summaries.mjs +872 -426
  85. package/scripts/local-target.mjs +106 -0
  86. package/scripts/present.mjs +333 -117
  87. package/scripts/review-chat-context.mjs +510 -0
  88. package/scripts/review-chat-controller.mjs +691 -0
  89. package/scripts/review-chat-provider.mjs +363 -0
  90. package/scripts/review-chat.mjs +8 -0
  91. package/scripts/serve-built.mjs +245 -27
  92. package/scripts/summary-path.mjs +32 -11
  93. package/dist/assets/index-C2p_1iAk.css +0 -2
  94. package/dist/assets/index-divlsuKs.js +0 -1568
@@ -1,13 +1,14 @@
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,
9
9
  join,
10
10
  } from 'node:path';
11
+ import { readConfiguredAgent } from './agent-config.mjs';
11
12
 
12
13
  export const codingAgentCapabilities = {
13
14
  codex: { binary: 'codex', model: true, reasoning: true },
@@ -207,7 +208,7 @@ export async function findCommand(
207
208
  return findOnPath(command, env, platform);
208
209
  }
209
210
 
210
- export async function commandAvailable(command, options) {
211
+ async function commandAvailable(command, options) {
211
212
  return Boolean(await findCommand(command, options));
212
213
  }
213
214
 
@@ -234,44 +235,177 @@ export async function codingAgentAvailability(
234
235
  };
235
236
  }
236
237
 
237
- // fallow-ignore-next-line complexity -- validation and discovery share one public selector.
238
- export async function selectCodingAgent(
238
+ function availableResult(result) {
239
+ return typeof result === 'object' ? result.available : result;
240
+ }
241
+
242
+ function unavailableAgentError(requested, result) {
243
+ if (typeof result === 'object' && result.reason) {
244
+ return new Error(result.reason);
245
+ }
246
+ return new Error(`Coding agent "${requested}" is not available.`);
247
+ }
248
+
249
+ function noAvailableAgentError(cursorReason) {
250
+ return new Error(
251
+ `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}` : ''}`,
252
+ );
253
+ }
254
+
255
+ function selectionCancelledError() {
256
+ return new Error(
257
+ 'Agent selection was cancelled. Pass --agent NAME to choose an agent or --no-agent for a plain review.',
258
+ );
259
+ }
260
+
261
+ function nonInteractiveSelectionError() {
262
+ return new Error(
263
+ 'An interactive terminal is required to choose a coding agent. Pass --agent NAME to choose an agent or --no-agent for a plain review.',
264
+ );
265
+ }
266
+
267
+ class CodingAgentSelectionError extends Error {
268
+ constructor(agent, message) {
269
+ super(message);
270
+ this.agent = agent;
271
+ }
272
+ }
273
+
274
+ export function codingAgentFromSelectionError(error) {
275
+ return error instanceof CodingAgentSelectionError
276
+ ? error.agent
277
+ : undefined;
278
+ }
279
+
280
+ function unsupportedSelectionError(requested, configured) {
281
+ if (configured) {
282
+ return new CodingAgentSelectionError(
283
+ requested,
284
+ `Configured coding agent "${requested}" is unsupported. Choose ${enabledCodingAgents.join(', ')} with diffsplain config agent NAME, or use --agent NAME for this run.`,
285
+ );
286
+ }
287
+ return new CodingAgentSelectionError(
288
+ requested,
289
+ `Unsupported agent "${requested}". Choose ${enabledCodingAgents.join(', ')}.`,
290
+ );
291
+ }
292
+
293
+ function unavailableSelectionError(requested, result, configured) {
294
+ if (!configured) {
295
+ return new CodingAgentSelectionError(
296
+ requested,
297
+ unavailableAgentError(requested, result).message,
298
+ );
299
+ }
300
+ const reason = typeof result === 'object' ? result.reason : undefined;
301
+ return new CodingAgentSelectionError(
302
+ requested,
303
+ `Configured coding agent "${requested}" is not available.${reason ? ` ${reason}` : ''} Change it with diffsplain config agent NAME, or use --agent NAME for this run.`,
304
+ );
305
+ }
306
+
307
+ async function selectRequestedCodingAgent(
239
308
  requested,
240
- available = commandAvailable,
309
+ available,
310
+ { configured = false } = {},
241
311
  ) {
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;
312
+ if (!codingAgents.includes(requested)) {
313
+ throw unsupportedSelectionError(requested, configured);
259
314
  }
315
+ const result = await available(requested);
316
+ if (!availableResult(result)) {
317
+ throw unavailableSelectionError(requested, result, configured);
318
+ }
319
+ return requested;
320
+ }
260
321
 
322
+ async function usableCodingAgents(available) {
323
+ const usable = [];
261
324
  let cursorReason;
262
325
  for (const agent of enabledCodingAgents) {
263
326
  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') {
327
+ if (availableResult(result)) {
328
+ usable.push(agent);
329
+ } else if (agent === 'cursor' && typeof result === 'object') {
269
330
  cursorReason = result.reason;
270
331
  }
271
332
  }
272
- throw new Error(
273
- `No coding agent is available. Install one of: ${enabledCodingAgents.join(', ')}.${cursorReason ? ` ${cursorReason}` : ''}`,
274
- );
333
+ return { usable, cursorReason };
334
+ }
335
+
336
+ function selectFromTerminal(agents, { input, output, signal }) {
337
+ output.write('Choose a coding agent:\n');
338
+ for (const [index, agent] of agents.entries()) {
339
+ output.write(`${index + 1}. ${agent}\n`);
340
+ }
341
+ output.write('Enter a number, or press Ctrl+C to cancel: ');
342
+
343
+ return new Promise((resolve, reject) => {
344
+ const picker = createInterface({ input, output, terminal: true });
345
+ let settled = false;
346
+ const finish = (callback, value) => {
347
+ if (settled) return;
348
+ settled = true;
349
+ signal?.removeEventListener('abort', onAbort);
350
+ picker.close();
351
+ callback(value);
352
+ };
353
+ const onAbort = () => {
354
+ finish(reject, selectionCancelledError());
355
+ };
356
+ const promptAgain = () => {
357
+ output.write(`Choose a number from 1 to ${agents.length}, or press Ctrl+C to cancel: `);
358
+ };
359
+ signal?.addEventListener('abort', onAbort, { once: true });
360
+ if (signal?.aborted) onAbort();
361
+ picker.on('SIGINT', () => {
362
+ finish(reject, selectionCancelledError());
363
+ });
364
+ picker.on('close', () => {
365
+ finish(reject, selectionCancelledError());
366
+ });
367
+ picker.on('line', (line) => {
368
+ const selected = Number(line.trim());
369
+ if (
370
+ !Number.isInteger(selected) ||
371
+ selected < 1 ||
372
+ selected > agents.length
373
+ ) {
374
+ promptAgain();
375
+ return;
376
+ }
377
+ finish(resolve, agents[selected - 1]);
378
+ });
379
+ });
380
+ }
381
+
382
+ export async function selectCodingAgent(
383
+ requested,
384
+ {
385
+ available = commandAvailable,
386
+ configuredAgent = readConfiguredAgent,
387
+ input = process.stdin,
388
+ output = process.stderr,
389
+ signal,
390
+ } = {},
391
+ ) {
392
+ if (requested !== undefined) {
393
+ return selectRequestedCodingAgent(requested, available);
394
+ }
395
+
396
+ const configured = await configuredAgent();
397
+ if (configured !== undefined) {
398
+ return selectRequestedCodingAgent(configured, available, {
399
+ configured: true,
400
+ });
401
+ }
402
+
403
+ if (!input?.isTTY || !output?.isTTY) {
404
+ throw nonInteractiveSelectionError();
405
+ }
406
+ const { usable, cursorReason } = await usableCodingAgents(available);
407
+ if (!usable.length) throw noAvailableAgentError(cursorReason);
408
+ return selectFromTerminal(usable, { input, output, signal });
275
409
  }
276
410
 
277
411
  // fallow-ignore-next-line complexity -- each supported agent has one override rule.
@@ -391,6 +525,33 @@ export function parseAgentResponse(agent, stdout) {
391
525
  return parseJsonText(stdout, label);
392
526
  }
393
527
 
528
+ function codexIsolationArgs(snapshotOnly) {
529
+ return snapshotOnly
530
+ ? ['--ignore-user-config', '--ignore-rules']
531
+ : [];
532
+ }
533
+
534
+ function codexConfigArgs(snapshotOnly) {
535
+ return snapshotOnly
536
+ ? [
537
+ '--config',
538
+ 'mcp_servers={}',
539
+ '--config',
540
+ 'plugins={}',
541
+ '--config',
542
+ 'shell_environment_policy.inherit="none"',
543
+ '--config',
544
+ 'sandbox_workspace_write.network_access=false',
545
+ '--config',
546
+ 'web_search="disabled"',
547
+ ]
548
+ : [];
549
+ }
550
+
551
+ function codexRepoArgs(snapshotOnly) {
552
+ return snapshotOnly ? ['--skip-git-repo-check'] : [];
553
+ }
554
+
394
555
  function codexCommand({
395
556
  binary,
396
557
  model,
@@ -399,31 +560,22 @@ function codexCommand({
399
560
  schemaPath,
400
561
  summaryDirectory,
401
562
  summaryEnv,
563
+ snapshotOnly,
402
564
  }) {
403
565
  const args = [
404
566
  'exec',
405
567
  '--ephemeral',
406
568
  '--sandbox',
407
569
  'read-only',
408
- '--ignore-user-config',
409
- '--ignore-rules',
570
+ ...codexIsolationArgs(snapshotOnly),
410
571
  '--color',
411
572
  'never',
412
- '--skip-git-repo-check',
573
+ ...codexRepoArgs(snapshotOnly),
413
574
  '-C',
414
575
  summaryDirectory,
415
576
  '--output-schema',
416
577
  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"',
578
+ ...codexConfigArgs(snapshotOnly),
427
579
  ];
428
580
  if (model) args.push('--model', model);
429
581
  if (reasoning) {
@@ -449,6 +601,7 @@ function claudeCommand({
449
601
  schema,
450
602
  summaryDirectory,
451
603
  summaryEnv,
604
+ snapshotOnly,
452
605
  }) {
453
606
  const args = [
454
607
  '--print',
@@ -456,8 +609,7 @@ function claudeCommand({
456
609
  'json',
457
610
  '--json-schema',
458
611
  JSON.stringify(schema),
459
- '--tools',
460
- '',
612
+ ...(snapshotOnly ? ['--tools', ''] : ['--permission-mode', 'plan']),
461
613
  '--no-session-persistence',
462
614
  ];
463
615
  if (model) args.push('--model', model);
@@ -479,13 +631,14 @@ function copilotCommand({
479
631
  schema,
480
632
  summaryDirectory,
481
633
  summaryEnv,
634
+ snapshotOnly,
482
635
  }) {
483
636
  const schemaText = JSON.stringify(schema);
484
637
  const args = [
485
638
  '--silent',
486
- '--no-ask-user',
639
+ ...(snapshotOnly ? ['--no-ask-user'] : []),
487
640
  '--no-color',
488
- '--no-custom-instructions',
641
+ ...(snapshotOnly ? ['--no-custom-instructions'] : []),
489
642
  '--no-remote',
490
643
  '--no-remote-export',
491
644
  `--add-dir=${dirname(inputPath)}`,
@@ -504,6 +657,26 @@ function copilotCommand({
504
657
  };
505
658
  }
506
659
 
660
+ function openCodeIsolationArgs(snapshotOnly) {
661
+ return snapshotOnly ? ['--pure'] : [];
662
+ }
663
+
664
+ function openCodeEnvironment(snapshotOnly, summaryEnv) {
665
+ if (!snapshotOnly) return summaryEnv;
666
+ return {
667
+ ...summaryEnv,
668
+ OPENCODE_DB: ':memory:',
669
+ OPENCODE_CONFIG_CONTENT: JSON.stringify({
670
+ permission: { '*': 'deny' },
671
+ agent: {
672
+ build: {
673
+ permission: { '*': 'deny' },
674
+ },
675
+ },
676
+ }),
677
+ };
678
+ }
679
+
507
680
  function openCodeCommand({
508
681
  binary,
509
682
  model,
@@ -512,10 +685,11 @@ function openCodeCommand({
512
685
  schema,
513
686
  summaryDirectory,
514
687
  summaryEnv,
688
+ snapshotOnly,
515
689
  }) {
516
690
  const args = [
517
691
  'run',
518
- '--pure',
692
+ ...openCodeIsolationArgs(snapshotOnly),
519
693
  '--format',
520
694
  'json',
521
695
  '--dir',
@@ -533,24 +707,12 @@ function openCodeCommand({
533
707
  args,
534
708
  input: 'stdin',
535
709
  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
- },
710
+ env: openCodeEnvironment(snapshotOnly, summaryEnv),
548
711
  };
549
712
  }
550
713
 
551
714
  function cursorCommand({
552
715
  binary,
553
- inputPath,
554
716
  model,
555
717
  prompt,
556
718
  schema,
@@ -572,26 +734,28 @@ function cursorCommand({
572
734
  ];
573
735
  if (model) args.push('--model', model);
574
736
  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)}`,
737
+ `${prompt}\n\nThe snapshot JSON follows this prompt on standard input. Return JSON that matches this schema:\n${JSON.stringify(schema)}`,
576
738
  );
577
739
  return {
578
740
  command: binary,
579
741
  args,
580
742
  input: 'stdin',
581
743
  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
- },
744
+ env: summaryEnv === sourceEnv
745
+ ? summaryEnv
746
+ : {
747
+ ...summaryEnv,
748
+ ...(sourceEnv.XDG_CONFIG_HOME
749
+ ? { XDG_CONFIG_HOME: sourceEnv.XDG_CONFIG_HOME }
750
+ : {}),
751
+ ...(sourceEnv.APPDATA ? { APPDATA: sourceEnv.APPDATA } : {}),
752
+ ...(sourceEnv.CURSOR_API_KEY
753
+ ? { CURSOR_API_KEY: sourceEnv.CURSOR_API_KEY }
754
+ : {}),
755
+ ...(sourceEnv.CURSOR_AUTH_TOKEN
756
+ ? { CURSOR_AUTH_TOKEN: sourceEnv.CURSOR_AUTH_TOKEN }
757
+ : {}),
758
+ },
595
759
  };
596
760
  }
597
761
 
@@ -604,8 +768,11 @@ export function agentCommand({
604
768
  schema,
605
769
  schemaPath,
606
770
  inputPath,
771
+ accessMode,
607
772
  env = process.env,
608
773
  }) {
774
+ const snapshotDirectory = dirname(inputPath);
775
+ const checkoutAccess = accessMode?.mode === 'checkout-read-only';
609
776
  const options = {
610
777
  binary,
611
778
  inputPath,
@@ -614,8 +781,9 @@ export function agentCommand({
614
781
  reasoning,
615
782
  schema,
616
783
  schemaPath,
617
- summaryDirectory: dirname(inputPath),
618
- summaryEnv: summaryAgentEnvironment(env),
784
+ summaryDirectory: checkoutAccess ? accessMode.root : snapshotDirectory,
785
+ summaryEnv: checkoutAccess ? env : summaryAgentEnvironment(env),
786
+ snapshotOnly: !checkoutAccess,
619
787
  sourceEnv: env,
620
788
  };
621
789
  if (agent === 'codex') return codexCommand(options);
@@ -624,3 +792,13 @@ export function agentCommand({
624
792
  if (agent === 'cursor') return cursorCommand(options);
625
793
  return openCodeCommand(options);
626
794
  }
795
+
796
+ const readOnlyWarnings = {
797
+ copilot: 'Warning: Copilot has no proven native read-only mode. It runs with your normal permissions.',
798
+ opencode: 'Warning: OpenCode has no proven native read-only mode. It runs with your normal permissions.',
799
+ };
800
+
801
+ export function agentReadOnlyWarning(agent, accessMode) {
802
+ if (accessMode?.mode !== 'checkout-read-only') return undefined;
803
+ return readOnlyWarnings[agent];
804
+ }