@codyswann/lisa 4.70.3 → 4.71.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 (143) 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/history-secret-evidence-shape.mjs +188 -0
  8. package/all/copy-overwrite/scripts/lib/history-secret-evidence.mjs +271 -0
  9. package/all/copy-overwrite/scripts/lib/history-secret-scanner.mjs +6 -296
  10. package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +289 -0
  11. package/all/copy-overwrite/scripts/lib/npm-update-authorization.mjs +235 -0
  12. package/all/copy-overwrite/scripts/lib/npm-update-broker-client.mjs +92 -0
  13. package/all/copy-overwrite/scripts/lib/npm-update-checkpoint.mjs +241 -0
  14. package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +278 -0
  15. package/all/copy-overwrite/scripts/lib/npm-update-controller-broker.mjs +234 -0
  16. package/all/copy-overwrite/scripts/lib/npm-update-controller-factory.mjs +278 -0
  17. package/all/copy-overwrite/scripts/lib/npm-update-controller-recipe.mjs +135 -0
  18. package/all/copy-overwrite/scripts/lib/npm-update-execution-adapter.mjs +292 -0
  19. package/all/copy-overwrite/scripts/lib/npm-update-gate-hooks.mjs +214 -0
  20. package/all/copy-overwrite/scripts/lib/npm-update-gate-install.mjs +267 -0
  21. package/all/copy-overwrite/scripts/lib/npm-update-gate-proof.mjs +99 -0
  22. package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +300 -0
  23. package/all/copy-overwrite/scripts/lib/npm-update-gh-dispatch.mjs +182 -0
  24. package/all/copy-overwrite/scripts/lib/npm-update-gh-grants.mjs +156 -0
  25. package/all/copy-overwrite/scripts/lib/npm-update-gh-requests.mjs +221 -0
  26. package/all/copy-overwrite/scripts/lib/npm-update-github.mjs +214 -0
  27. package/all/copy-overwrite/scripts/lib/npm-update-helper-graph.mjs +238 -0
  28. package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +126 -0
  29. package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +292 -0
  30. package/all/copy-overwrite/scripts/lib/npm-update-hook-installation.mjs +116 -0
  31. package/all/copy-overwrite/scripts/lib/npm-update-hook-preload.mjs +153 -0
  32. package/all/copy-overwrite/scripts/lib/npm-update-hook-provider.mjs +207 -0
  33. package/all/copy-overwrite/scripts/lib/npm-update-hook-read-client.mjs +164 -0
  34. package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +272 -0
  35. package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +157 -0
  36. package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +28 -0
  37. package/all/copy-overwrite/scripts/lib/npm-update-isolation.mjs +11 -0
  38. package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +171 -0
  39. package/all/copy-overwrite/scripts/lib/npm-update-leaf.mjs +194 -0
  40. package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +155 -0
  41. package/all/copy-overwrite/scripts/lib/npm-update-npm.mjs +190 -0
  42. package/all/copy-overwrite/scripts/lib/npm-update-object.mjs +112 -0
  43. package/all/copy-overwrite/scripts/lib/npm-update-orchestrator.mjs +273 -0
  44. package/all/copy-overwrite/scripts/lib/npm-update-owner.mjs +292 -0
  45. package/all/copy-overwrite/scripts/lib/npm-update-prepare.mjs +263 -0
  46. package/all/copy-overwrite/scripts/lib/npm-update-process-core.mjs +179 -0
  47. package/all/copy-overwrite/scripts/lib/npm-update-process.mjs +16 -0
  48. package/all/copy-overwrite/scripts/lib/npm-update-publication.mjs +258 -0
  49. package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +258 -0
  50. package/all/copy-overwrite/scripts/lib/npm-update-quality.mjs +153 -0
  51. package/all/copy-overwrite/scripts/lib/npm-update-recovery.mjs +264 -0
  52. package/all/copy-overwrite/scripts/lib/npm-update-runtime-archive.mjs +204 -0
  53. package/all/copy-overwrite/scripts/lib/npm-update-runtime-graph.mjs +185 -0
  54. package/all/copy-overwrite/scripts/lib/npm-update-runtime-transport.mjs +221 -0
  55. package/all/copy-overwrite/scripts/lib/npm-update-runtime.mjs +284 -0
  56. package/all/copy-overwrite/scripts/lib/npm-update-tool-launcher.mjs +287 -0
  57. package/all/copy-overwrite/scripts/lib/npm-update-worker-environment.mjs +161 -0
  58. package/all/copy-overwrite/scripts/lib/npm-update-worker-inspection.mjs +142 -0
  59. package/all/copy-overwrite/scripts/lib/npm-update-worker-lifecycle.mjs +256 -0
  60. package/all/copy-overwrite/scripts/lib/npm-update-worker-policy.mjs +266 -0
  61. package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +230 -36
  62. package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +161 -0
  63. package/all/copy-overwrite/scripts/lisa-history-secrets.mjs +8 -1
  64. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +89 -5
  65. package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +204 -0
  66. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +48 -123
  67. package/all/copy-overwrite/scripts/lisa-work-item.mjs +107 -41
  68. package/all/copy-overwrite/scripts/npm-updater-gate-runtime.json +1 -0
  69. package/all/copy-overwrite/scripts/npm-updater-gate-supervisor.c +121 -0
  70. package/all/copy-overwrite/scripts/npm-updater-gate.Dockerfile +49 -0
  71. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +1085 -0
  72. package/dist/codex/scripts/block-no-verify.sh +89 -5
  73. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  74. package/dist/core/lisa-owned-hash-ledger.js +620 -0
  75. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  76. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  77. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  78. package/dist/core/upstream-evidence-manifest.js +165 -13
  79. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  80. package/dist/opencode/plugin-templates/block-no-verify.sh +89 -5
  81. package/package.json +4 -4
  82. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  84. package/plugins/lisa/hooks/block-no-verify.agy.sh +89 -5
  85. package/plugins/lisa/hooks/block-no-verify.sh +89 -5
  86. package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +89 -5
  87. package/plugins/lisa-agy/plugin.json +1 -1
  88. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  90. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  91. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-copilot/hooks/block-no-verify.sh +89 -5
  95. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-cursor/hooks/block-no-verify.sh +89 -5
  97. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  99. package/plugins/lisa-expo-agy/plugin.json +1 -1
  100. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  104. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  105. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  106. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  109. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  110. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  111. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  113. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  114. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  115. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  116. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  119. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  120. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  121. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  123. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  124. package/plugins/lisa-rails-agy/plugin.json +1 -1
  125. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  126. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  127. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  128. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  129. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  130. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  131. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  132. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  133. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  134. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  135. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  136. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  137. package/plugins/src/base/hooks/block-no-verify.agy.sh +89 -5
  138. package/plugins/src/base/hooks/block-no-verify.sh +89 -5
  139. package/scripts/check-shipped-surface-removals.mjs +96 -24
  140. package/scripts/generate-npm-updater-helper-graph.mjs +184 -0
  141. package/scripts/two-channel-couplings.json +31 -7
  142. package/typescript/copy-contents/.husky/commit-msg +18 -0
  143. 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
+ }
@@ -25,6 +25,7 @@ export const main = async (
25
25
  const {
26
26
  HistorySecretError,
27
27
  objectWidth,
28
+ gitRead,
28
29
  parsePush,
29
30
  eventPairs,
30
31
  introducedCommits,
@@ -91,7 +92,13 @@ export const main = async (
91
92
  console.log("No introduced history. Scanner was not run.");
92
93
  return 0;
93
94
  }
94
- const result = scanCommits(commits, cwd, scanner);
95
+ const heads = pairs
96
+ .filter(pair => !/^0+$/u.test(pair.after))
97
+ .map(pair =>
98
+ gitRead(["rev-parse", "--verify", `${pair.after}^{commit}`], cwd)
99
+ )
100
+ .filter(head => commits.includes(head));
101
+ const result = scanCommits(commits, cwd, scanner, heads);
95
102
  console.log(JSON.stringify(result));
96
103
  if (result.findings.length > 0) {
97
104
  console.error(