@codyswann/lisa 4.70.4 → 4.71.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.
Files changed (139) hide show
  1. package/all/copy-overwrite/scripts/lib/automation-provenance-contract.mjs +174 -0
  2. package/all/copy-overwrite/scripts/lib/automation-provenance-local.mjs +279 -0
  3. package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +17 -7
  4. package/all/copy-overwrite/scripts/lib/github-attestation-provider.mjs +241 -0
  5. package/all/copy-overwrite/scripts/lib/github-attestation-recovery.mjs +283 -0
  6. package/all/copy-overwrite/scripts/lib/github-attestation-verifier.mjs +244 -0
  7. package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +289 -0
  8. package/all/copy-overwrite/scripts/lib/npm-update-authorization.mjs +235 -0
  9. package/all/copy-overwrite/scripts/lib/npm-update-broker-client.mjs +92 -0
  10. package/all/copy-overwrite/scripts/lib/npm-update-checkpoint.mjs +241 -0
  11. package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +278 -0
  12. package/all/copy-overwrite/scripts/lib/npm-update-controller-broker.mjs +234 -0
  13. package/all/copy-overwrite/scripts/lib/npm-update-controller-factory.mjs +278 -0
  14. package/all/copy-overwrite/scripts/lib/npm-update-controller-recipe.mjs +135 -0
  15. package/all/copy-overwrite/scripts/lib/npm-update-execution-adapter.mjs +292 -0
  16. package/all/copy-overwrite/scripts/lib/npm-update-gate-hooks.mjs +214 -0
  17. package/all/copy-overwrite/scripts/lib/npm-update-gate-install.mjs +267 -0
  18. package/all/copy-overwrite/scripts/lib/npm-update-gate-proof.mjs +99 -0
  19. package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +300 -0
  20. package/all/copy-overwrite/scripts/lib/npm-update-gh-dispatch.mjs +182 -0
  21. package/all/copy-overwrite/scripts/lib/npm-update-gh-grants.mjs +156 -0
  22. package/all/copy-overwrite/scripts/lib/npm-update-gh-requests.mjs +221 -0
  23. package/all/copy-overwrite/scripts/lib/npm-update-github.mjs +214 -0
  24. package/all/copy-overwrite/scripts/lib/npm-update-helper-graph.mjs +238 -0
  25. package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +126 -0
  26. package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +292 -0
  27. package/all/copy-overwrite/scripts/lib/npm-update-hook-installation.mjs +116 -0
  28. package/all/copy-overwrite/scripts/lib/npm-update-hook-preload.mjs +153 -0
  29. package/all/copy-overwrite/scripts/lib/npm-update-hook-provider.mjs +207 -0
  30. package/all/copy-overwrite/scripts/lib/npm-update-hook-read-client.mjs +164 -0
  31. package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +300 -0
  32. package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +157 -0
  33. package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +28 -0
  34. package/all/copy-overwrite/scripts/lib/npm-update-isolation.mjs +11 -0
  35. package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +171 -0
  36. package/all/copy-overwrite/scripts/lib/npm-update-leaf.mjs +194 -0
  37. package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +155 -0
  38. package/all/copy-overwrite/scripts/lib/npm-update-npm.mjs +190 -0
  39. package/all/copy-overwrite/scripts/lib/npm-update-object.mjs +112 -0
  40. package/all/copy-overwrite/scripts/lib/npm-update-orchestrator.mjs +273 -0
  41. package/all/copy-overwrite/scripts/lib/npm-update-owner.mjs +292 -0
  42. package/all/copy-overwrite/scripts/lib/npm-update-prepare.mjs +263 -0
  43. package/all/copy-overwrite/scripts/lib/npm-update-process-core.mjs +179 -0
  44. package/all/copy-overwrite/scripts/lib/npm-update-process.mjs +16 -0
  45. package/all/copy-overwrite/scripts/lib/npm-update-publication.mjs +258 -0
  46. package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +254 -0
  47. package/all/copy-overwrite/scripts/lib/npm-update-quality.mjs +153 -0
  48. package/all/copy-overwrite/scripts/lib/npm-update-recovery.mjs +264 -0
  49. package/all/copy-overwrite/scripts/lib/npm-update-runtime-archive.mjs +204 -0
  50. package/all/copy-overwrite/scripts/lib/npm-update-runtime-graph.mjs +185 -0
  51. package/all/copy-overwrite/scripts/lib/npm-update-runtime-transport.mjs +221 -0
  52. package/all/copy-overwrite/scripts/lib/npm-update-runtime.mjs +284 -0
  53. package/all/copy-overwrite/scripts/lib/npm-update-tool-launcher.mjs +287 -0
  54. package/all/copy-overwrite/scripts/lib/npm-update-worker-environment.mjs +161 -0
  55. package/all/copy-overwrite/scripts/lib/npm-update-worker-inspection.mjs +142 -0
  56. package/all/copy-overwrite/scripts/lib/npm-update-worker-lifecycle.mjs +256 -0
  57. package/all/copy-overwrite/scripts/lib/npm-update-worker-policy.mjs +266 -0
  58. package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +230 -36
  59. package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +161 -0
  60. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +89 -5
  61. package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +204 -0
  62. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +48 -123
  63. package/all/copy-overwrite/scripts/lisa-work-item.mjs +107 -41
  64. package/all/copy-overwrite/scripts/npm-updater-gate-runtime.json +1 -0
  65. package/all/copy-overwrite/scripts/npm-updater-gate-supervisor.c +121 -0
  66. package/all/copy-overwrite/scripts/npm-updater-gate.Dockerfile +49 -0
  67. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +1085 -0
  68. package/dist/codex/scripts/block-no-verify.sh +89 -5
  69. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  70. package/dist/core/lisa-owned-hash-ledger.js +604 -0
  71. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  72. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  73. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  74. package/dist/core/upstream-evidence-manifest.js +159 -11
  75. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  76. package/dist/opencode/plugin-templates/block-no-verify.sh +89 -5
  77. package/package.json +4 -4
  78. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  80. package/plugins/lisa/hooks/block-no-verify.agy.sh +89 -5
  81. package/plugins/lisa/hooks/block-no-verify.sh +89 -5
  82. package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +89 -5
  83. package/plugins/lisa-agy/plugin.json +1 -1
  84. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  86. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  87. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-copilot/hooks/block-no-verify.sh +89 -5
  91. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-cursor/hooks/block-no-verify.sh +89 -5
  93. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  95. package/plugins/lisa-expo-agy/plugin.json +1 -1
  96. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  100. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  101. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  105. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  106. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  110. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  111. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  113. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  115. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  116. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  120. package/plugins/lisa-rails-agy/plugin.json +1 -1
  121. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  123. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  124. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  125. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  126. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  127. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  128. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  129. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  130. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  131. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  132. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  133. package/plugins/src/base/hooks/block-no-verify.agy.sh +89 -5
  134. package/plugins/src/base/hooks/block-no-verify.sh +89 -5
  135. package/scripts/check-shipped-surface-removals.mjs +96 -24
  136. package/scripts/generate-npm-updater-helper-graph.mjs +184 -0
  137. package/scripts/two-channel-couplings.json +31 -7
  138. package/typescript/copy-contents/.husky/commit-msg +18 -0
  139. package/typescript/copy-overwrite/.prettierignore +2 -0
@@ -3,7 +3,7 @@
3
3
  // Do not edit directly — durable changes belong upstream in Lisa.
4
4
 
5
5
  /**
6
- * Run one shell command in a process boundary and reap every descendant.
6
+ * Run one shell command or explicit literal argv in a process boundary and reap every descendant.
7
7
  *
8
8
  * Node's synchronous timeout signals only the direct child. A gate command is
9
9
  * a tree (shell, package manager, test workers), so killing only the shell
@@ -33,15 +33,87 @@
33
33
  */
34
34
 
35
35
  import { spawn, spawnSync } from "node:child_process";
36
+ import { writeSync } from "node:fs";
36
37
 
37
38
  import { invokedAsScript } from "./invoked-as-script.mjs";
38
39
  import { startWindowsProcessJob } from "./windows-process-job.mjs";
39
40
 
41
+ /** Diagnostic memory stays bounded independently of the complete live stream. */
42
+ const CAPTURE_TAIL_BYTES = 512 * 1024;
40
43
  const KILL_GRACE_MS = 750;
41
44
  const REAP_POLL_MS = 25;
42
45
  const WINDOWS_TIMEOUT_EXIT_CODE = 255;
43
46
  const TERMINATING_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
44
47
 
48
+ /** Same literal-dispatch envelope as the launcher; control overhead is not free. */
49
+ function snapshotArguments(argv) {
50
+ if (!Array.isArray(argv)) throw new Error("direct argv requires an array");
51
+ const copied = [...argv];
52
+ if (
53
+ copied.length > 512 ||
54
+ copied.some(
55
+ value =>
56
+ typeof value !== "string" ||
57
+ value.includes("\0") ||
58
+ Buffer.byteLength(value) > 65_536
59
+ ) ||
60
+ Buffer.byteLength(JSON.stringify(copied)) > 262_144
61
+ )
62
+ throw new Error("invalid or unbounded direct argument vector");
63
+ return copied;
64
+ }
65
+
66
+ /** The direct API fails before any handlers or child backend acquire ownership. */
67
+ function directInvocation(command, args, timeoutMs) {
68
+ if (!["linux", "darwin"].includes(process.platform))
69
+ throw new Error("direct argv requires Linux or macOS");
70
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 1_800_000)
71
+ throw new Error("invalid direct command deadline");
72
+ if (
73
+ typeof command !== "string" ||
74
+ command.length === 0 ||
75
+ !Array.isArray(args)
76
+ )
77
+ throw new Error(
78
+ "direct argv requires a nonempty executable and argument array"
79
+ );
80
+ return snapshotArguments([command, ...args]).slice(1);
81
+ }
82
+
83
+ /** Only the explicit new-mode prefix is closed; legacy parsing remains tolerant. */
84
+ function parseDirectArguments(argv, separator) {
85
+ snapshotArguments([...process.execArgv, process.argv[1], ...argv]);
86
+ const vector = snapshotArguments(argv);
87
+ if (separator < 0) throw new Error("direct argv requires a separator");
88
+ const flags = vector.slice(0, separator);
89
+ const timeouts = flags.filter(value => /^--timeout-ms=\d+$/.test(value));
90
+ const capture = flags.filter(value => value === "--capture-fd=3");
91
+ const watches = flags.filter(value => /^--watch-pid=\d+$/.test(value));
92
+ if (
93
+ flags.filter(value => value === "--direct-argv").length !== 1 ||
94
+ timeouts.length !== 1 ||
95
+ capture.length > 1 ||
96
+ watches.some(
97
+ value => !isWatchablePid(Number(value.slice("--watch-pid=".length)))
98
+ ) ||
99
+ flags.length !== 1 + timeouts.length + capture.length + watches.length
100
+ )
101
+ throw new Error("invalid direct supervisor control prefix");
102
+ const command = vector[separator + 1];
103
+ const timeoutMs = Number(timeouts[0].slice("--timeout-ms=".length));
104
+ return {
105
+ command,
106
+ timeoutMs,
107
+ directArgs: directInvocation(
108
+ command,
109
+ vector.slice(separator + 2),
110
+ timeoutMs
111
+ ),
112
+ watchPids: parseWatchPids(watches),
113
+ captureFd: capture.length === 1 ? 3 : undefined,
114
+ };
115
+ }
116
+
45
117
  /**
46
118
  * How often the supervisor asks whether the run it serves is still there.
47
119
  *
@@ -103,6 +175,8 @@ function parseArguments(argv) {
103
175
  const separator = argv.indexOf("--");
104
176
  const timeoutMs = Number(timeoutArg?.slice("--timeout-ms=".length));
105
177
  const flags = separator >= 0 ? argv.slice(0, separator) : argv;
178
+ if (flags.includes("--direct-argv"))
179
+ return parseDirectArguments(argv, separator);
106
180
  const command = separator >= 0 ? argv.slice(separator + 1).join(" ") : "";
107
181
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || !command) {
108
182
  throw new Error(
@@ -110,7 +184,20 @@ function parseArguments(argv) {
110
184
  "[--watch-pid=<pid>]... -- <command>"
111
185
  );
112
186
  }
113
- return { command, timeoutMs, watchPids: parseWatchPids(flags) };
187
+ const captureFlags = flags.filter(value => value.startsWith("--capture-fd="));
188
+ if (
189
+ captureFlags.length > 1 ||
190
+ captureFlags.some(value => value !== "--capture-fd=3")
191
+ ) {
192
+ throw new Error("capture requires the fixed caller descriptor 3");
193
+ }
194
+ return {
195
+ command,
196
+ timeoutMs,
197
+ watchPids: parseWatchPids(flags),
198
+ captureFd: captureFlags.length === 1 ? 3 : undefined,
199
+ directArgs: undefined,
200
+ };
114
201
  }
115
202
 
116
203
  /**
@@ -403,6 +490,79 @@ export async function reapTree(pid, controls = DEFAULT_REAP_CONTROLS) {
403
490
  throw new Error(`gate process tree ${pid} survived SIGKILL`);
404
491
  }
405
492
 
493
+ /**
494
+ * Bound pipe drainage after tree reap by the existing two-phase cleanup budget.
495
+ * A missing close notification cannot postpone a verdict indefinitely; it is
496
+ * cleanup failure, and retained pipes are detached before the failure returns.
497
+ * @param {import("node:child_process").ChildProcess} child Owned command shell.
498
+ * @param {Promise<unknown>} closed Already-armed output completion observation.
499
+ * @returns {Promise<void>} Confirmed drainage, or a terminal cleanup failure.
500
+ */
501
+ async function waitForOutputClose(child, closed) {
502
+ let timer;
503
+ try {
504
+ await Promise.race([
505
+ closed,
506
+ new Promise((_, reject) => {
507
+ timer = setTimeout(() => {
508
+ child.stdout?.destroy();
509
+ child.stderr?.destroy();
510
+ reject(
511
+ new Error("gate diagnostic output did not close after tree reap")
512
+ );
513
+ }, KILL_GRACE_MS * 2);
514
+ }),
515
+ ]);
516
+ } finally {
517
+ clearTimeout(timer);
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Forward command output immediately, retaining only a bounded diagnostic tail.
523
+ * The fixed caller descriptor is deliberately absent from the child's stdio.
524
+ * Waiting for close drains the pipes AFTER exit has already started tree reap.
525
+ * A broken diagnostic sink cannot replace the actual process verdict.
526
+ * @param {import("node:child_process").ChildProcess} child Owned command shell.
527
+ * @param {number|undefined} descriptor Optional caller-only descriptor.
528
+ * @returns {() => Promise<void>} Drain and publish diagnostics once.
529
+ */
530
+ function captureChildOutput(child, descriptor) {
531
+ if (descriptor === undefined) return async () => {};
532
+ let tail = Buffer.alloc(0);
533
+ let publication;
534
+ const closed = new Promise(resolve => child.once("close", resolve));
535
+ for (const stream of [child.stdout, child.stderr]) {
536
+ stream.on("data", chunk => {
537
+ tail =
538
+ chunk.length >= CAPTURE_TAIL_BYTES
539
+ ? Buffer.from(chunk.subarray(-CAPTURE_TAIL_BYTES))
540
+ : Buffer.concat([
541
+ tail.subarray(-Math.max(0, CAPTURE_TAIL_BYTES - chunk.length)),
542
+ chunk,
543
+ ]);
544
+ });
545
+ stream.pipe(process.stdout, { end: false });
546
+ }
547
+ const publish = async () => {
548
+ await waitForOutputClose(child, closed);
549
+ try {
550
+ let offset = 0;
551
+ while (offset < tail.length) {
552
+ offset += writeSync(descriptor, tail, offset, tail.length - offset);
553
+ }
554
+ } catch {
555
+ process.stderr.write(
556
+ "gate diagnostic capture unavailable; OS verdict retained\n"
557
+ );
558
+ }
559
+ };
560
+ return () => {
561
+ publication ??= publish();
562
+ return publication;
563
+ };
564
+ }
565
+
406
566
  /**
407
567
  * Run one command as a supervised process tree.
408
568
  *
@@ -410,7 +570,7 @@ export async function reapTree(pid, controls = DEFAULT_REAP_CONTROLS) {
410
570
  * terminating signal, and now the run it serves going away — and all three
411
571
  * share one reap, because a tree reaped twice is a tree whose second kill
412
572
  * lands on a recycled pid.
413
- * @param {string} command Shell source to supervise.
573
+ * @param {string} command Shell source, or literal executable when directArgs is present.
414
574
  * @param {number} timeoutMs Deadline for the whole tree.
415
575
  * @param {typeof reapTree} [reap] Injectable POSIX group reaper. Windows retains its native job owner.
416
576
  * @param {object} [options] Interrupt-watch seams, injectable for tests.
@@ -419,16 +579,32 @@ export async function reapTree(pid, controls = DEFAULT_REAP_CONTROLS) {
419
579
  * @param {number} [options.pollMs] How often to ask.
420
580
  * @param {(line: string) => void} [options.report] Where the report goes.
421
581
  * @param {number} [options.launchParentPid] Parent pid captured at start.
582
+ * @param {3} [options.captureFd] Fixed caller-only diagnostic descriptor.
583
+ * @param {readonly string[]} [options.directArgs] Literal arguments; even [] selects POSIX direct spawning.
422
584
  * @returns {Promise<{code: number|null, signal: string|null}>} The verdict.
423
585
  */
424
586
  export function supervise(command, timeoutMs, reap = reapTree, options = {}) {
587
+ // Snapshot before reading other option getters or arming handlers. The caller
588
+ // cannot change the selected argv while lifetime ownership is being set up.
589
+ const suppliedArgs = options.directArgs;
590
+ const directArgs =
591
+ suppliedArgs === undefined
592
+ ? undefined
593
+ : directInvocation(command, suppliedArgs, timeoutMs);
425
594
  const {
426
595
  watchPids = [],
427
596
  detect = interruptionReason,
428
597
  pollMs = ORPHAN_POLL_MS,
429
598
  report = line => process.stderr.write(`${line}\n`),
430
599
  launchParentPid = process.ppid,
600
+ captureFd,
431
601
  } = options;
602
+ if (
603
+ captureFd !== undefined &&
604
+ (captureFd !== 3 || process.platform === "win32")
605
+ ) {
606
+ throw new Error("diagnostic capture requires POSIX caller descriptor 3");
607
+ }
432
608
  return new Promise((resolve, reject) => {
433
609
  let windowsJob;
434
610
  let pendingSignal;
@@ -457,11 +633,16 @@ export function supervise(command, timeoutMs, reap = reapTree, options = {}) {
457
633
  windowsJob = startWindowsProcessJob(command);
458
634
  return windowsJob.child;
459
635
  }
460
- return spawn("/bin/sh", ["-c", command], {
461
- detached: true,
462
- env: process.env,
463
- stdio: "inherit",
464
- });
636
+ return spawn(
637
+ directArgs === undefined ? "/bin/sh" : command,
638
+ directArgs === undefined ? ["-c", command] : directArgs,
639
+ {
640
+ detached: true,
641
+ env: process.env,
642
+ stdio:
643
+ captureFd === undefined ? "inherit" : ["inherit", "pipe", "pipe"],
644
+ }
645
+ );
465
646
  } catch (error) {
466
647
  clearSignalHandlers();
467
648
  reject(error);
@@ -486,6 +667,7 @@ export function supervise(command, timeoutMs, reap = reapTree, options = {}) {
486
667
  return;
487
668
  }
488
669
 
670
+ const publishCapture = captureChildOutput(child, captureFd);
489
671
  let timedOut = false;
490
672
  let settled = false;
491
673
  let settling = false;
@@ -500,7 +682,7 @@ export function supervise(command, timeoutMs, reap = reapTree, options = {}) {
500
682
  clearSignalHandlers();
501
683
  };
502
684
  // Every exit path shares one reap. A signal that arrives while the direct
503
- // shell's close handler is reaping must join that operation instead of
685
+ // shell's exit handler is reaping must join that operation instead of
504
686
  // starting a second kill sequence or taking the default signal action.
505
687
  const reapOnce = () => {
506
688
  reapPromise ??= windowsJob ? windowsJob.reap() : reap(pid);
@@ -514,6 +696,7 @@ export function supervise(command, timeoutMs, reap = reapTree, options = {}) {
514
696
  // exits. Reap that residue fully before returning a verdict too.
515
697
  try {
516
698
  await reapOnce();
699
+ await publishCapture();
517
700
  settled = true;
518
701
  cleanup();
519
702
  resolve({ code, signal });
@@ -530,6 +713,8 @@ export function supervise(command, timeoutMs, reap = reapTree, options = {}) {
530
713
  // the handle and rejecting makes the supervisor terminal instead of
531
714
  // leaving its promise pending behind a live event-loop reference.
532
715
  child.unref();
716
+ child.stdout?.destroy();
717
+ child.stderr?.destroy();
533
718
  reject(
534
719
  new Error(
535
720
  `gate process-tree supervisor failed: ${error instanceof Error ? error.message : String(error)}`
@@ -547,12 +732,15 @@ export function supervise(command, timeoutMs, reap = reapTree, options = {}) {
547
732
  settling = true;
548
733
  clearDeadline();
549
734
  clearInterval(interruptWatch);
550
- reapOnce().then(() => {
551
- cleanup();
552
- report(interruptionReport(reason, pid));
553
- // Restore the signal-shaped exit after the detached tree is gone.
554
- process.kill(process.pid, signal);
555
- }, failReap);
735
+ reapOnce()
736
+ .then(async () => {
737
+ await publishCapture();
738
+ cleanup();
739
+ report(interruptionReport(reason, pid));
740
+ // Restore the signal-shaped exit after the detached tree is gone.
741
+ process.kill(process.pid, signal);
742
+ })
743
+ .catch(failReap);
556
744
  };
557
745
  const relaySignal = signal => {
558
746
  stopWith(`it received ${signal}`, signal);
@@ -579,21 +767,24 @@ export function supervise(command, timeoutMs, reap = reapTree, options = {}) {
579
767
  terminating = true;
580
768
  settling = true;
581
769
  clearDeadline();
582
- reapOnce().then(() => {
583
- cleanup();
584
- const verdict = timeoutVerdictForPlatform(process.platform);
585
- // A signal-shaped result is the existing gate runner vocabulary for
586
- // "no verdict". It also prevents an ordinary exit code such as 124
587
- // from being confused with a user command that returned that code.
588
- if (verdict.signal === null) {
589
- // Windows has no signal-shaped process result. 255 is a dedicated
590
- // supervisor timeout code with a documented (but unavoidable)
591
- // collision risk, well outside ordinary command exit conventions.
592
- process.exit(verdict.code);
593
- } else {
594
- process.kill(process.pid, verdict.signal);
595
- }
596
- }, failReap);
770
+ reapOnce()
771
+ .then(async () => {
772
+ await publishCapture();
773
+ cleanup();
774
+ const verdict = timeoutVerdictForPlatform(process.platform);
775
+ // A signal-shaped result is the existing gate runner vocabulary for
776
+ // "no verdict". It also prevents an ordinary exit code such as 124
777
+ // from being confused with a user command that returned that code.
778
+ if (verdict.signal === null) {
779
+ // Windows has no signal-shaped process result. 255 is a dedicated
780
+ // supervisor timeout code with a documented (but unavoidable)
781
+ // collision risk, well outside ordinary command exit conventions.
782
+ process.exit(verdict.code);
783
+ } else {
784
+ process.kill(process.pid, verdict.signal);
785
+ }
786
+ })
787
+ .catch(failReap);
597
788
  }, timeoutMs);
598
789
 
599
790
  dispatchSignal = relaySignal;
@@ -607,9 +798,9 @@ export function supervise(command, timeoutMs, reap = reapTree, options = {}) {
607
798
  reject(error);
608
799
  }, failReap);
609
800
  });
610
- child.once("close", (code, signal) => {
801
+ child.once("exit", (code, signal) => {
611
802
  // On timeout, wait for the forced group reap above before this supervisor
612
- // ends. Exiting on the direct shell's close is the race that used to leave
803
+ // ends. Exiting before the complete group reap is the race that used to leave
613
804
  // its descendants alive.
614
805
  if (!timedOut) void finish(code, signal);
615
806
  });
@@ -619,10 +810,13 @@ export function supervise(command, timeoutMs, reap = reapTree, options = {}) {
619
810
  }
620
811
 
621
812
  async function main() {
622
- const { command, timeoutMs, watchPids } = parseArguments(
623
- process.argv.slice(2)
624
- );
625
- const result = await supervise(command, timeoutMs, reapTree, { watchPids });
813
+ const { command, timeoutMs, watchPids, captureFd, directArgs } =
814
+ parseArguments(process.argv.slice(2));
815
+ const result = await supervise(command, timeoutMs, reapTree, {
816
+ watchPids,
817
+ captureFd,
818
+ directArgs,
819
+ });
626
820
  if (result.signal) {
627
821
  process.kill(process.pid, result.signal);
628
822
  } else {
@@ -0,0 +1,161 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+
4
+ /** Authenticated non-AI attribution supplements ordinary tracking and lint gates. */
5
+ import {
6
+ predictedCommit,
7
+ verifyRecoveryOrigin,
8
+ } from "./lib/github-attestation-recovery.mjs";
9
+ import { invokedAsScript } from "./lib/invoked-as-script.mjs";
10
+ import {
11
+ assertPinnedVerifier,
12
+ requireProof,
13
+ sha256,
14
+ verifyCurrentProvider,
15
+ verifyDescriptorAttestation,
16
+ } from "./lib/github-attestation-verifier.mjs";
17
+ import {
18
+ canonicalJson,
19
+ trustedConfiguration,
20
+ npmProposal,
21
+ } from "./lib/automation-provenance-contract.mjs";
22
+ import {
23
+ git,
24
+ privateBytes,
25
+ localDescriptor,
26
+ proofSnapshot,
27
+ canonicalContext,
28
+ } from "./lib/automation-provenance-local.mjs";
29
+ export { canonicalJson } from "./lib/automation-provenance-contract.mjs";
30
+
31
+ /** A present but malformed reference is a failure, even with ordinary AI trailers. */
32
+ export function automationReference(message) {
33
+ const lines = message
34
+ .split(/\r?\n/)
35
+ .filter(line => /^\s*Automation-Provenance\s*:/i.test(line));
36
+ if (lines.length === 0) return undefined;
37
+ requireProof(lines.length === 1, "ambiguous automation reference");
38
+ const match =
39
+ /^Automation-Provenance: actions\/([1-9]\d*)\/attempts\/([1-9]\d*)$/.exec(
40
+ lines[0]
41
+ );
42
+ requireProof(Boolean(match), "malformed automation reference");
43
+ return { runId: match[1], runAttempt: match[2] };
44
+ }
45
+
46
+ /** Fresh recovery is a fixed separate role; original proof never acquires current authority. */
47
+ function recoveryPermission(
48
+ policy,
49
+ descriptor,
50
+ context,
51
+ config,
52
+ bytes,
53
+ paths,
54
+ recoveryBytes,
55
+ recoveryBundle,
56
+ messageBytes
57
+ ) {
58
+ const recovery = JSON.parse(recoveryBytes.toString("utf8"));
59
+ requireProof(
60
+ recoveryBytes.toString("utf8") === `${canonicalJson(recovery)}\n`,
61
+ "noncanonical/duplicate recovery fields"
62
+ );
63
+ const npmPolicySha256 = sha256(canonicalJson(config.npmUpdater));
64
+ verifyRecoveryOrigin(policy, descriptor, recovery, sha256(bytes), paths, {
65
+ npmPolicySha256,
66
+ proposalKey: sha256(
67
+ canonicalJson({
68
+ repository: policy.repository,
69
+ target: "main",
70
+ ecosystem: "npm",
71
+ directory: ".",
72
+ parent: descriptor.parent,
73
+ policySha256: npmPolicySha256,
74
+ updates: descriptor.updates,
75
+ })
76
+ ),
77
+ leafBodySha256: sha256(context.issue.body),
78
+ commit: predictedCommit(descriptor, messageBytes),
79
+ maintainer: config.npmUpdater.maintainer,
80
+ recoverySha256: sha256(recoveryBytes),
81
+ });
82
+ requireProof(
83
+ privateBytes(paths.recovery, 65_536).equals(recoveryBytes) &&
84
+ privateBytes(paths.recoveryBundle, 1_048_576).equals(recoveryBundle),
85
+ "recovery proof changed during verification"
86
+ );
87
+ }
88
+
89
+ /** Verify fixed private proof against the final message, tree, binding and provider. */
90
+ export function verifyAutomationProvenance(messageFile) {
91
+ const messageBytes = privateBytes(messageFile, 65_536, false);
92
+ const message = messageBytes.toString("utf8");
93
+ const reference = automationReference(message);
94
+ if (!reference) return false;
95
+ const { config, policy } = trustedConfiguration(git);
96
+ const { paths, optional, bytes, bundleBytes, descriptor } = proofSnapshot();
97
+ localDescriptor(descriptor, messageBytes, reference, policy);
98
+ npmProposal(descriptor, git);
99
+ requireProof(
100
+ descriptor.proposalKey ===
101
+ sha256(
102
+ canonicalJson({
103
+ repository: policy.repository,
104
+ parent: descriptor.parent,
105
+ updates: descriptor.updates,
106
+ })
107
+ ),
108
+ "deterministic proposal key differs"
109
+ );
110
+ assertPinnedVerifier(policy);
111
+ const context = canonicalContext(message, config, policy, descriptor);
112
+ const recoveryBytes = optional[0]
113
+ ? privateBytes(paths.recovery, 65_536)
114
+ : null;
115
+ const recoveryBundle = optional[0]
116
+ ? privateBytes(paths.recoveryBundle, 1_048_576)
117
+ : null;
118
+ if (recoveryBytes) {
119
+ recoveryPermission(
120
+ policy,
121
+ descriptor,
122
+ context,
123
+ config,
124
+ bytes,
125
+ paths,
126
+ recoveryBytes,
127
+ recoveryBundle,
128
+ messageBytes
129
+ );
130
+ } else {
131
+ verifyDescriptorAttestation(policy, descriptor, paths, sha256(bytes));
132
+ verifyCurrentProvider(policy, descriptor);
133
+ }
134
+ requireProof(
135
+ privateBytes(paths.bundle, 1_048_576).equals(bundleBytes),
136
+ "origin bundle changed during verification"
137
+ );
138
+ requireProof(
139
+ privateBytes(paths.descriptor, 65_536).equals(bytes),
140
+ "descriptor changed during verification"
141
+ );
142
+ localDescriptor(
143
+ descriptor,
144
+ privateBytes(messageFile, 65_536, false),
145
+ reference,
146
+ policy
147
+ );
148
+ return true;
149
+ }
150
+
151
+ if (invokedAsScript(import.meta.url)) {
152
+ try {
153
+ const verified = verifyAutomationProvenance(process.argv[2]);
154
+ process.exitCode = verified ? 0 : 10;
155
+ } catch {
156
+ console.error(
157
+ "Invalid automation provenance: required proof, policy or provider evidence failed"
158
+ );
159
+ process.exitCode = 1;
160
+ }
161
+ }
@@ -152,6 +152,7 @@ fi
152
152
 
153
153
  if ! BLOCK_NO_VERIFY_COMMAND="$command_str" python3 - <<'PY'
154
154
  import os
155
+ import io
155
156
  import re
156
157
  import shlex
157
158
  import sys
@@ -1284,6 +1285,57 @@ def git_skips_verification(text, depth=0):
1284
1285
  return False
1285
1286
 
1286
1287
 
1288
+ def git_config_reads_key(tokens, index):
1289
+ """Whether this key is a read operand of this exact Git config invocation.
1290
+
1291
+ Keep the command boundary: a query cannot authorize the next command or
1292
+ turn a later setting into a read. Unknown options remain conservative.
1293
+ """
1294
+ start = index
1295
+ while start > 0 and tokens[start - 1] not in COMMAND_SEPARATORS:
1296
+ start -= 1
1297
+ end = index + 1
1298
+ while end < len(tokens) and tokens[end] not in COMMAND_SEPARATORS:
1299
+ end += 1
1300
+ program, args, opaque = command_word(tokens[start:end])
1301
+ if opaque or program != "git":
1302
+ return False
1303
+ found = subcommand_after_git(args, 0)
1304
+ if not found or found[0] != "config":
1305
+ return False
1306
+ argv = found[1]
1307
+ key_offset = index - (end - len(argv))
1308
+ read = bool(argv and argv[0] == "get")
1309
+ cursor = 1 if read else 0
1310
+ operands = []
1311
+ read_actions = {"--get", "--get-all", "--get-regexp", "--get-urlmatch"}
1312
+ modifiers = {
1313
+ "--local", "--global", "--system", "--worktree", "--includes",
1314
+ "--no-includes", "--show-origin", "--show-scope", "--null", "-z",
1315
+ "--name-only", "--all", "--regexp",
1316
+ }
1317
+ value_options = {"--file", "-f", "--blob", "--type", "--default", "--value"}
1318
+ while cursor < len(argv):
1319
+ token = argv[cursor]
1320
+ if token in read_actions:
1321
+ if read:
1322
+ return False
1323
+ read = True
1324
+ elif token in value_options:
1325
+ if cursor + 1 >= len(argv):
1326
+ return False
1327
+ cursor += 1
1328
+ elif token in modifiers:
1329
+ pass
1330
+ elif token.startswith("-"):
1331
+ if not any(token.startswith(option + "=") for option in value_options):
1332
+ return False
1333
+ else:
1334
+ operands.append(cursor)
1335
+ cursor += 1
1336
+ return bool(operands and operands[0] == key_offset and (read or len(operands) == 1))
1337
+
1338
+
1287
1339
  def token_bypass(tokens):
1288
1340
  """Whether any token disables hooks by environment or by git config.
1289
1341
 
@@ -1295,7 +1347,7 @@ def token_bypass(tokens):
1295
1347
  option, and an option belongs to an argv.
1296
1348
 
1297
1349
  Args:
1298
- tokens: Punctuation-stripped tokens of one command or script.
1350
+ tokens: Operator-aware tokens of one command or script.
1299
1351
 
1300
1352
  Returns:
1301
1353
  True when a token disables verification.
@@ -1321,7 +1373,9 @@ def token_bypass(tokens):
1321
1373
  if lowered.startswith("core.hookspath="):
1322
1374
  if not is_permitted_hooks_path(token.split("=", 1)[1]):
1323
1375
  return True
1324
- if lowered == "core.hookspath" and i + 1 < len(tokens):
1376
+ if (lowered == "core.hookspath" and i + 1 < len(tokens)
1377
+ and tokens[i + 1] not in COMMAND_SEPARATORS
1378
+ and not git_config_reads_key(tokens, i)):
1325
1379
  if not is_permitted_hooks_path(tokens[i + 1]):
1326
1380
  return True
1327
1381
  # `git --config-env=core.hooksPath=SOMEVAR` sets the same config, reading
@@ -1388,8 +1442,22 @@ def token_bypass(tokens):
1388
1442
  return False
1389
1443
 
1390
1444
 
1445
+ class OperatorStream(io.StringIO):
1446
+ """Remember punctuation read while shlex is inside a quote or escape."""
1447
+
1448
+ lexer = None
1449
+ literal_operator = False
1450
+
1451
+ def read(self, size=-1):
1452
+ value = super().read(size)
1453
+ if (self.lexer is not None and self.lexer.state in {"'", '"', "\\"}
1454
+ and any(char in "();|&<>" for char in value)):
1455
+ self.literal_operator = True
1456
+ return value
1457
+
1458
+
1391
1459
  def flat_tokens(text):
1392
- """Punctuation-stripped tokens, or None when the text does not lex.
1460
+ """Operator-aware tokens without printed data, or None on a lex failure.
1393
1461
 
1394
1462
  Args:
1395
1463
  text: A command line or a script's contents.
@@ -1398,9 +1466,25 @@ def flat_tokens(text):
1398
1466
  The token list, or None.
1399
1467
  """
1400
1468
  try:
1401
- tokens = shell_tokens(text)
1469
+ stream = OperatorStream(line_boundaries_as_separators(text))
1470
+ lexer = shlex.shlex(stream, posix=True, punctuation_chars=True)
1471
+ lexer.whitespace_split = True
1472
+ lexer.commenters = ""
1473
+ stream.lexer = lexer
1474
+ tokens = []
1475
+ while True:
1476
+ stream.literal_operator = False
1477
+ token = lexer.get_token()
1478
+ if token is None:
1479
+ break
1480
+ # shlex otherwise makes quoted ";" indistinguishable from a real
1481
+ # command boundary. Keep literal destinations as data, so a setter
1482
+ # cannot masquerade as a one-operand configuration read.
1483
+ if token in COMMAND_SEPARATORS and stream.literal_operator:
1484
+ token = "\x00" + token
1485
+ tokens.append(token)
1402
1486
  return [
1403
- token.strip("();|&")
1487
+ token
1404
1488
  for index, token in enumerate(tokens)
1405
1489
  if not printed_argument(tokens, index)
1406
1490
  ]