@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
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
3
+ // Do not edit directly — durable changes belong upstream in Lisa.
4
+
5
+ /**
6
+ * @file lisa-npm-updater.mjs
7
+ * @description Four phase entry points consume bounded data from a committed trusted host policy.
8
+ * @module npm-updater
9
+ */
10
+ import { join, resolve } from "node:path";
11
+ import { invokedAsScript } from "./lib/invoked-as-script.mjs";
12
+ import {
13
+ required,
14
+ UpdaterError,
15
+ keys,
16
+ validatePolicy,
17
+ validateProposal,
18
+ } from "./lib/npm-update-contract.mjs";
19
+ import {
20
+ readJson,
21
+ writeJson,
22
+ runProcess,
23
+ phaseDirectory,
24
+ } from "./lib/npm-update-process.mjs";
25
+ import { prepareUpdate } from "./lib/npm-update-prepare.mjs";
26
+ import { GitHub } from "./lib/npm-update-github.mjs";
27
+ import { allocateLeaf } from "./lib/npm-update-allocate.mjs";
28
+ import { publishProposal } from "./lib/npm-update-publish.mjs";
29
+ import {
30
+ prepareAuthorization,
31
+ finalizeAuthorization,
32
+ } from "./lib/npm-update-authorization.mjs";
33
+ import { gateProposal } from "./lib/npm-update-gate.mjs";
34
+ import { readGateProof } from "./lib/npm-update-gate-proof.mjs";
35
+ import { sha256 } from "./lib/github-attestation-verifier.mjs";
36
+
37
+ /** Only committed policy grants authority; ignored/local config cannot activate Actions. */
38
+ async function configuration(cwd) {
39
+ const result = await runProcess("git", ["show", "HEAD:.lisa.config.json"], {
40
+ cwd,
41
+ env: { PATH: process.env.PATH, HOME: "/nonexistent" },
42
+ maximum: 1_048_576,
43
+ });
44
+ const config = JSON.parse(result.stdout.toString());
45
+ required(
46
+ config.automationProvenance?.enabled === true,
47
+ "automation provenance must be explicitly configured on committed main"
48
+ );
49
+ const policy = validatePolicy(config.npmUpdater, config);
50
+ required(
51
+ config.automationProvenance.repository === policy.repository,
52
+ "committed authority repository differs"
53
+ );
54
+ return { config, policy };
55
+ }
56
+
57
+ /** Allocation creates its canonical leaf before previewing any attributed commit. */
58
+ async function allocatePhase(cwd, output, proposal, policy, config) {
59
+ const api = new GitHub(config.automationProvenance, process.env.GH_TOKEN);
60
+ const allocation = await allocateLeaf({
61
+ api,
62
+ proposal,
63
+ policy,
64
+ config,
65
+ cwd,
66
+ });
67
+ const result = await prepareAuthorization({
68
+ api,
69
+ cwd,
70
+ output,
71
+ proposal,
72
+ allocation,
73
+ policy,
74
+ config,
75
+ });
76
+ if (result.mode === "stale-outstanding")
77
+ writeJson(join(output, "authorization.json"), {
78
+ mode: result.mode,
79
+ workItem: allocation.workItem,
80
+ });
81
+ process.stdout.write(`${result.mode}\n`);
82
+ }
83
+
84
+ /** Publication consumes the fixed gated object and proof slots without changing authority order. */
85
+ async function publishPhase(
86
+ cwd,
87
+ output,
88
+ proposal,
89
+ policy,
90
+ config,
91
+ allocation,
92
+ preview
93
+ ) {
94
+ const gated = readJson(join(output, "gated.json"));
95
+ keys(gated, ["raw", "receipt"]);
96
+ required(
97
+ typeof gated.raw === "string" && gated.raw.length <= 90_000,
98
+ "bounded raw commit envelope required"
99
+ );
100
+ const api = new GitHub(config.automationProvenance, process.env.GH_TOKEN);
101
+ const result = await publishProposal({
102
+ api,
103
+ cwd,
104
+ proposal,
105
+ policy,
106
+ config,
107
+ allocation,
108
+ descriptor: preview.descriptor,
109
+ proof: {
110
+ descriptor: join(output, "descriptor.json"),
111
+ bundle: join(output, "bundle.json"),
112
+ recovery: join(output, "recovery.json"),
113
+ recoveryBundle: join(output, "recovery-bundle.json"),
114
+ },
115
+ receipt: gated.receipt,
116
+ raw: Buffer.from(gated.raw, "base64"),
117
+ });
118
+ writeJson(join(output, "published.json"), result);
119
+ process.stdout.write(`${result.status} ${result.url}\n`);
120
+ }
121
+
122
+ /** Strict phase and fixed directory arguments are data, never a caller recipe. */
123
+ export async function main(argv = process.argv.slice(2)) {
124
+ required(
125
+ argv.length === 3 &&
126
+ ["prepare", "allocate", "checkpoint", "gate", "publish"].includes(
127
+ argv[0]
128
+ ),
129
+ "usage: lisa-npm-updater.mjs <prepare|allocate|checkpoint|gate|publish> <owned-checkout> <private-phase-directory>"
130
+ );
131
+ const [phase, checkout, directory] = argv;
132
+ const cwd = resolve(checkout);
133
+ const output = resolve(directory);
134
+ const { config, policy } = await configuration(cwd);
135
+ phaseDirectory(output);
136
+ if (phase === "prepare") {
137
+ const result = await prepareUpdate({ cwd, policy, config });
138
+ writeJson(join(output, "prepare.json"), result);
139
+ if (result.status === "prepared")
140
+ writeJson(join(output, "proposal.json"), result.proposal);
141
+ process.stdout.write(`${result.status}\n`);
142
+ return;
143
+ }
144
+ const proposal = validateProposal(
145
+ readJson(join(output, "proposal.json")),
146
+ policy
147
+ );
148
+ if (phase === "allocate")
149
+ return allocatePhase(cwd, output, proposal, policy, config);
150
+ const allocation = readJson(join(output, "allocation.json"));
151
+ const preview = readJson(join(output, "preview.json"));
152
+ keys(preview, ["descriptor", "message", "epoch"]);
153
+ required(
154
+ sha256(preview.message) === preview.descriptor.messageSha256,
155
+ "preview final message differs"
156
+ );
157
+ if (phase === "checkpoint") {
158
+ const api = new GitHub(config.automationProvenance, process.env.GH_TOKEN);
159
+ const result = await finalizeAuthorization({
160
+ api,
161
+ output,
162
+ proposal,
163
+ allocation,
164
+ preview,
165
+ policy,
166
+ config,
167
+ });
168
+ process.stdout.write(`${result.mode} checkpoint complete\n`);
169
+ return;
170
+ }
171
+ if (phase === "gate") {
172
+ const result = await gateProposal({
173
+ cwd,
174
+ proposal,
175
+ policy,
176
+ allocation,
177
+ preview,
178
+ ...readGateProof(output),
179
+ token: process.env.GH_TOKEN,
180
+ config,
181
+ });
182
+ writeJson(join(output, "gated.json"), result);
183
+ process.stdout.write(`gated ${result.receipt.commit}\n`);
184
+ return;
185
+ }
186
+ return publishPhase(
187
+ cwd,
188
+ output,
189
+ proposal,
190
+ policy,
191
+ config,
192
+ allocation,
193
+ preview
194
+ );
195
+ }
196
+
197
+ if (invokedAsScript(import.meta.url))
198
+ main().catch(error => {
199
+ // Never print candidate/provider payloads or child output that may contain credentials.
200
+ process.stderr.write(
201
+ `${error instanceof UpdaterError ? error.message : "npm updater failed: required policy, authorization, installation, gate or exact publication evidence was unavailable"}\n`
202
+ );
203
+ process.exitCode = 1;
204
+ });
@@ -41,15 +41,7 @@
41
41
  * @module lisa-run-gates
42
42
  */
43
43
 
44
- import {
45
- existsSync,
46
- mkdirSync,
47
- mkdtempSync,
48
- readFileSync,
49
- rmSync,
50
- writeFileSync,
51
- } from "node:fs";
52
- import { tmpdir } from "node:os";
44
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
53
45
  import { dirname, join, relative } from "node:path";
54
46
  import { fileURLToPath } from "node:url";
55
47
 
@@ -323,20 +315,21 @@ function formatLine(state, gate, detail) {
323
315
  /**
324
316
  * Read an executor's answer, whichever of the two shapes it returned.
325
317
  *
326
- * An executor may answer with a bare exit code or with `{code, output}`. Both
327
- * are supported on purpose: the exit code is the whole verdict, and the output
328
- * is only ever used to EXPLAIN a verdict already reached. An executor that
318
+ * An executor may answer with a bare exit code or with `{code, output}` and an
319
+ * optional OS-observed signal. Both retain the actual exit status; output and
320
+ * signal identity only EXPLAIN a verdict already reached. An executor that
329
321
  * cannot capture output — a stub, a Windows shell, a run with capture switched
330
322
  * off — therefore loses the diagnosis and nothing else.
331
- * @param {number|null|undefined|{code: number|null, output: string|null}} raw
323
+ * @param {number|null|undefined|{code: number|null, output: string|null, signal?: string}} raw
332
324
  * Whatever the executor returned.
333
- * @returns {{code: number|null, output: string|null}} The normalised answer.
325
+ * @returns {{code: number|null, output: string|null, signal?: string}} The normalised answer.
334
326
  */
335
327
  function normaliseExec(raw) {
336
328
  if (raw !== null && typeof raw === "object") {
337
329
  return {
338
330
  code: typeof raw.code === "number" ? raw.code : null,
339
331
  output: typeof raw.output === "string" ? raw.output : null,
332
+ ...(typeof raw.signal === "string" ? { signal: raw.signal } : {}),
340
333
  };
341
334
  }
342
335
  return { code: typeof raw === "number" ? raw : null, output: null };
@@ -440,7 +433,7 @@ function stateFor(kind) {
440
433
  }
441
434
 
442
435
  function execute(gate, exec, declarations) {
443
- const { code, output } = normaliseExec(exec(gate.command, gate));
436
+ const { code, output, signal } = normaliseExec(exec(gate.command, gate));
444
437
  if (code === 0) {
445
438
  // Exit 0 is ADJACENT to "the property holds"; it is not the observation.
446
439
  // A command that exited 0 while its own output says it collected zero
@@ -472,14 +465,14 @@ function execute(gate, exec, declarations) {
472
465
  };
473
466
  }
474
467
  const shown = typeof code === "number" ? code : "terminated";
475
- // The code goes in as well as the output, and it is the half that matters
476
- // most: a kill is legible ONLY in the exit code. `exit 143` is `128 + 15`,
477
- // and on a saturated box it arrives carrying a truncated transcript that
468
+ // The actual status and signal go in as well as the output: a native kill
469
+ // retains null status, while a shell's `exit 143` is `128 + 15`. Either can
470
+ // arrive carrying a truncated transcript that
478
471
  // reads exactly like a real gate failure — which is how one `exit 1` came to
479
472
  // have three distinct causes with re-running a rational response to all of
480
473
  // them (CodySwannGT/lisa#2897).
481
474
  const diagnosis = withDeclarations(
482
- diagnoseFailure(output, code),
475
+ diagnoseFailure(output, code, undefined, undefined, undefined, signal),
483
476
  output,
484
477
  declarations
485
478
  );
@@ -1141,12 +1134,6 @@ function readPackageScripts(cwd) {
1141
1134
  }
1142
1135
  }
1143
1136
 
1144
- /**
1145
- * How much of a gate's output is kept for diagnosis. A suite of 14,000 tests
1146
- * prints megabytes; the signatures that matter are all near the end.
1147
- */
1148
- const CAPTURE_TAIL_BYTES = 512 * 1024;
1149
-
1150
1137
  /**
1151
1138
  * Hang-detector deadline for one gate command, in milliseconds.
1152
1139
  *
@@ -1218,7 +1205,7 @@ function runInterruption() {
1218
1205
  */
1219
1206
  function runProcessTree(command, options = {}) {
1220
1207
  const timeout = options.timeout ?? GATE_COMMAND_BUDGET_MS;
1221
- const { shell: _shell, ...childOptions } = options;
1208
+ const { shell: _shell, capture = false, ...childOptions } = options;
1222
1209
  // The supervisor already ends itself when THIS process dies; naming the
1223
1210
  // process above it is what covers the measured incident, where the hook was
1224
1211
  // killed and the runner — not the supervisor — was the orphan that kept the
@@ -1228,7 +1215,14 @@ function runProcessTree(command, options = {}) {
1228
1215
  : [];
1229
1216
  return boundedSpawnSync(
1230
1217
  process.execPath,
1231
- [PROCESS_TREE_RUNNER, `--timeout-ms=${timeout}`, ...watch, "--", command],
1218
+ [
1219
+ PROCESS_TREE_RUNNER,
1220
+ `--timeout-ms=${timeout}`,
1221
+ ...watch,
1222
+ ...(capture ? ["--capture-fd=3"] : []),
1223
+ "--",
1224
+ command,
1225
+ ],
1232
1226
  {
1233
1227
  ...childOptions,
1234
1228
  timeout: timeout + PROCESS_TREE_REAP_BUDGET_MS,
@@ -1260,7 +1254,7 @@ function killedOrRethrow(error) {
1260
1254
  * everything about capture is additive: if capture is unavailable or refused,
1261
1255
  * the exit code is produced by exactly the code path that produced it before.
1262
1256
  * @param {string} command The command line to run.
1263
- * @returns {{code: number|null, output: null}} Exit code; null when killed.
1257
+ * @returns {{code: number|null, output: null, signal?: string}} Exit code; null when killed.
1264
1258
  */
1265
1259
  function plainExec(command) {
1266
1260
  try {
@@ -1271,117 +1265,48 @@ function plainExec(command) {
1271
1265
  // Still reachable: a non-timeout `spawnSync` failure arrives on the result,
1272
1266
  // and it means nothing ran, which is the same "no verdict" answer.
1273
1267
  if (child.error) return { code: null, output: null };
1274
- return { code: child.status, output: null };
1268
+ return {
1269
+ code: child.status,
1270
+ output: null,
1271
+ ...(child.signal ? { signal: child.signal } : {}),
1272
+ };
1275
1273
  } catch (error) {
1276
1274
  return killedOrRethrow(error);
1277
1275
  }
1278
1276
  }
1279
1277
 
1280
1278
  /**
1281
- * Whether this machine can tee a gate's output without changing its verdict.
1282
- *
1283
- * Probed rather than assumed: on a shell without `tee` — Windows `cmd`, a
1284
- * stripped container — the wrapper below would produce no status file, and the
1285
- * runner would report every gate as terminated. A capability the runner cannot
1286
- * confirm is one it does not use.
1287
- * @returns {boolean} Whether to take the capturing path.
1288
- */
1289
- function captureAvailable() {
1290
- if (process.env.LISA_GATES_CAPTURE === "0") return false;
1291
- // The one child in this file that is NOT a project gate command, so it takes
1292
- // the shared default rather than the two-hour ceiling: `command -v tee`
1293
- // answers immediately or the shell is broken.
1294
- try {
1295
- const probe = boundedSpawnSync("sh", ["-c", "command -v tee"], {
1296
- stdio: "ignore",
1297
- });
1298
- return !probe.error && probe.status === 0;
1299
- } catch (error) {
1300
- // A probe killed at its deadline has not confirmed anything, and this
1301
- // function's whole contract is that an unconfirmed capability goes unused.
1302
- // Letting the throw escape would convert "cannot tell" into a crash, and
1303
- // the fallback path it guards answers the question perfectly well.
1304
- if (isChildTimeout(error)) return false;
1305
- throw error;
1306
- }
1307
- }
1308
-
1309
- /**
1310
- * Read back what the wrapper recorded, keeping only the tail of the log.
1311
- * @param {string} statusPath File the wrapper wrote the exit code into.
1312
- * @param {string} logPath File the wrapper tee'd the output into.
1313
- * @returns {{code: number|null, output: string|null}} The recorded answer.
1314
- */
1315
- function readCaptured(statusPath, logPath) {
1316
- let output = null;
1317
- try {
1318
- output = readFileSync(logPath, "utf8").slice(-CAPTURE_TAIL_BYTES);
1319
- } catch {
1320
- output = null;
1321
- }
1322
- try {
1323
- const code = Number.parseInt(readFileSync(statusPath, "utf8").trim(), 10);
1324
- // Fail closed. An unreadable status is not a zero: the one thing this
1325
- // runner may never do is turn "I do not know" into "it passed".
1326
- return { code: Number.isInteger(code) ? code : null, output };
1327
- } catch {
1328
- return { code: null, output };
1329
- }
1330
- }
1331
-
1332
- /**
1333
- * The default executor: run the command in a shell, streaming AND recording.
1334
- *
1335
- * stdio is inherited so a failing gate's own output reaches the operator
1336
- * unaltered and as it happens — a push gate runs for minutes and an operator
1337
- * needs to watch it, not receive it in a lump at the end. `tee` is what lets
1338
- * both be true: the operator sees the stream, and the runner keeps a copy to
1339
- * say WHICH failure it was rather than only that there was one.
1340
- *
1341
- * The price is that the command's stdout is a pipe rather than a terminal, so
1342
- * tools that colourise or animate for a TTY print their plain form. That is a
1343
- * deliberate trade: a plain, diagnosable failure beats a coloured, mute one.
1344
- * `LISA_GATES_CAPTURE=0` buys the colour back at the cost of the diagnosis.
1345
- *
1346
- * The exit code comes from a status file written INSIDE the pipeline, never
1347
- * from the pipeline itself — a pipeline reports `tee`'s status, which is
1348
- * almost always zero, and reading it would report every failing gate as
1349
- * passing. That is the single most dangerous mistake available here.
1350
- * @param {string} command The command line to run.
1351
- * @returns {{code: number|null, output: string|null}} Exit code and output.
1279
+ * Run a gate with live output and a supervisor-owned diagnostic tail.
1280
+ *
1281
+ * The extra pipe belongs only to the supervisor and its caller: the command
1282
+ * inherits stdin and two output pipes, never the diagnostic descriptor. No
1283
+ * child-writable status record participates in the verdict. Capture failures
1284
+ * retain the supervised OS result and never execute the command a second time.
1285
+ * Windows retains its existing native-job/plain route. Capture remains opt-out
1286
+ * for commands that require inherited terminal handles.
1287
+ * @param {string} command Shell source to run once.
1288
+ * @returns {{code: number|null, output: string|null, signal?: string}} OS status and diagnostic tail.
1352
1289
  */
1353
1290
  export function spawnExec(command) {
1354
- if (!captureAvailable()) return plainExec(command);
1355
- let dir;
1356
- try {
1357
- dir = mkdtempSync(join(tmpdir(), "lisa-gate-run-"));
1358
- } catch {
1291
+ if (process.env.LISA_GATES_CAPTURE === "0" || process.platform === "win32") {
1359
1292
  return plainExec(command);
1360
1293
  }
1361
- const logPath = join(dir, "output.log");
1362
- const statusPath = join(dir, "status");
1363
- // Newline-separated inside a group, so a command ending in a trailing
1364
- // comment or redirection still has `echo` run as its own statement.
1365
- // Paths travel as data, never as shell source. A TMPDIR containing a quote,
1366
- // newline, dollar sign, or command substitution must not be able to break
1367
- // this wrapper or execute content merely because mkdtemp inherited it.
1368
- const script = `{\n${command}\necho $? > "$LISA_GATE_STATUS_PATH"\n} 2>&1 | tee "$LISA_GATE_LOG_PATH"\n`;
1369
1294
  try {
1370
- const child = runProcessTree(script, {
1371
- env: {
1372
- ...process.env,
1373
- LISA_GATE_LOG_PATH: logPath,
1374
- LISA_GATE_STATUS_PATH: statusPath,
1375
- },
1376
- stdio: "inherit",
1295
+ const child = runProcessTree(command, {
1296
+ capture: true,
1297
+ stdio: ["inherit", "inherit", "inherit", "pipe"],
1377
1298
  timeout: GATE_COMMAND_BUDGET_MS,
1378
1299
  });
1379
1300
  if (child.error) return { code: null, output: null };
1380
- return readCaptured(statusPath, logPath);
1301
+ const captured = child.output?.[3];
1302
+ return {
1303
+ code: child.status,
1304
+ ...(child.signal ? { signal: child.signal } : {}),
1305
+ output:
1306
+ captured === null || captured === undefined ? null : String(captured),
1307
+ };
1381
1308
  } catch (error) {
1382
1309
  return killedOrRethrow(error);
1383
- } finally {
1384
- rmSync(dir, { force: true, recursive: true });
1385
1310
  }
1386
1311
  }
1387
1312
 
@@ -1016,7 +1016,7 @@ export function deployBranchEnvironments(config) {
1016
1016
  return new Map([[branch, PRODUCTION]]);
1017
1017
  }
1018
1018
 
1019
- function trackerContract(config = readConfig()) {
1019
+ export function trackerContract(config = readConfig()) {
1020
1020
  const provider = requireString(config.tracker, "tracker").toLowerCase();
1021
1021
  const identityRepo = currentRepoIdentity(config);
1022
1022
  if (provider === "github") {
@@ -1483,6 +1483,38 @@ export function soleWorkItem(text, contract, subject) {
1483
1483
  return refs[0];
1484
1484
  }
1485
1485
 
1486
+ /**
1487
+ * Resolve a required binding through the authoritative tracker contract.
1488
+ * Consumers supplying trusted committed configuration avoid reparsing trailers
1489
+ * or treating raw `current` output as a checked branch/queue identity.
1490
+ * This does not change ordinary offline/live validation semantics.
1491
+ * @param {string} message Complete final commit message.
1492
+ * @param {object} [options] Trusted configuration and strict live requirement.
1493
+ * @returns {object} Canonical identity and optional actual provider evidence.
1494
+ */
1495
+ export function resolveWorkItemContext(message, options = {}) {
1496
+ const contract = trackerContract(options.config ?? readConfig());
1497
+ const state = readState(false);
1498
+ const ref = soleWorkItem(message, contract, COMMIT_SUBJECT);
1499
+ assertStateBranch(state);
1500
+ assertStateMatches(ref, contract);
1501
+ if (options.requireLive && contract.provider !== "github") {
1502
+ throw new TrackingError("Automation provenance requires GitHub tracking");
1503
+ }
1504
+ const issue = options.requireLive
1505
+ ? githubIssue(ref, contract, options.execute ?? run)
1506
+ : undefined;
1507
+ return {
1508
+ ref,
1509
+ provider: contract.provider,
1510
+ repository: contract.repository,
1511
+ identityRepo: contract.identityRepo,
1512
+ branch: state.branch,
1513
+ lifecycle: contract.lifecycle,
1514
+ issue,
1515
+ };
1516
+ }
1517
+
1486
1518
  /**
1487
1519
  * The one work item a COMMIT message names.
1488
1520
  * @param {string} message Commit message.
@@ -1843,28 +1875,34 @@ function typeFromLabels(labels) {
1843
1875
  * @param {string|number} number GitHub issue number.
1844
1876
  * @returns {string[]} State of every sub-issue.
1845
1877
  */
1846
- function githubHierarchy(ref, contract, number) {
1878
+ /** Literal typed GraphQL fields are shared by the canonical reader and closed native transport. */
1879
+ export function githubHierarchyArgs(contract, number, after) {
1847
1880
  const [owner, repo] = contract.repository.split("/");
1848
1881
  const query =
1849
1882
  "query($owner:String!,$repo:String!,$number:Int!,$after:String){repository(owner:$owner,name:$repo){issue(number:$number){subIssues(first:100,after:$after){nodes{state}pageInfo{hasNextPage endCursor}}}}}";
1883
+ const args = [
1884
+ "api",
1885
+ "graphql",
1886
+ "-f",
1887
+ `query=${query}`,
1888
+ "-F",
1889
+ `owner=${owner}`,
1890
+ "-F",
1891
+ `repo=${repo}`,
1892
+ "-F",
1893
+ `number=${number}`,
1894
+ ];
1895
+ if (after) args.push("-F", `after=${after}`);
1896
+ return args;
1897
+ }
1898
+
1899
+ function githubHierarchy(ref, contract, number, execute = run) {
1850
1900
  const states = [];
1851
1901
  const cursor = { after: null };
1852
1902
 
1853
1903
  do {
1854
- const args = [
1855
- "api",
1856
- "graphql",
1857
- "-f",
1858
- `query=${query}`,
1859
- "-F",
1860
- `owner=${owner}`,
1861
- "-F",
1862
- `repo=${repo}`,
1863
- "-F",
1864
- `number=${number}`,
1865
- ];
1866
- if (cursor.after) args.push("-F", `after=${cursor.after}`);
1867
- const result = run("gh", args, { allowFailure: true });
1904
+ const args = githubHierarchyArgs(contract, number, cursor.after);
1905
+ const result = execute("gh", args, { allowFailure: true });
1868
1906
  if (result.status !== 0) throw githubFailure(result, ref);
1869
1907
 
1870
1908
  const response = safeJson(result.stdout, `GitHub issue ${ref} hierarchy`);
@@ -1952,20 +1990,25 @@ export function resetGhVersionCheck() {
1952
1990
  ghVersionChecked = false;
1953
1991
  }
1954
1992
 
1955
- function githubIssue(ref, contract) {
1956
- assertGhVersion();
1993
+ /** The body is required by recovery; all ordinary canonical validation fields remain present. */
1994
+ export function githubIssueViewArgs(repository, number) {
1995
+ return [
1996
+ "issue",
1997
+ "view",
1998
+ number,
1999
+ "--repo",
2000
+ repository,
2001
+ "--json",
2002
+ "number,url,state,body,labels,comments,closedByPullRequestsReferences",
2003
+ ];
2004
+ }
2005
+
2006
+ function githubIssue(ref, contract, execute = run) {
2007
+ assertGhVersion(execute);
1957
2008
  const number = ref.slice(ref.lastIndexOf("#") + 1);
1958
- const result = run(
2009
+ const result = execute(
1959
2010
  "gh",
1960
- [
1961
- "issue",
1962
- "view",
1963
- number,
1964
- "--repo",
1965
- contract.repository,
1966
- "--json",
1967
- "number,url,state,labels,comments,closedByPullRequestsReferences",
1968
- ],
2011
+ githubIssueViewArgs(contract.repository, number),
1969
2012
  {
1970
2013
  allowFailure: true,
1971
2014
  }
@@ -1985,7 +2028,7 @@ function githubIssue(ref, contract) {
1985
2028
  assertLeaf(
1986
2029
  ref,
1987
2030
  typeFromLabels(issue.labels),
1988
- githubHierarchy(ref, contract, number)
2031
+ githubHierarchy(ref, contract, number, execute)
1989
2032
  );
1990
2033
  return issue;
1991
2034
  }
@@ -2410,7 +2453,7 @@ export function textContainsBacklink(value, prUrl) {
2410
2453
  * @param {string} prUrl Pull request URL.
2411
2454
  * @returns {string} The comment body.
2412
2455
  */
2413
- function backlinkBody(prUrl) {
2456
+ export function backlinkBody(prUrl) {
2414
2457
  return `${MARKER} ${prUrl}`;
2415
2458
  }
2416
2459
 
@@ -2502,7 +2545,7 @@ function managedBacklinkTarget(body) {
2502
2545
  * @returns {{mine: unknown, others: number}} This PR's comment, and how many
2503
2546
  * managed comments belong to OTHER pull requests.
2504
2547
  */
2505
- function partitionBacklinks(comments, prUrl, bodyOf) {
2548
+ export function partitionBacklinks(comments, prUrl, bodyOf) {
2506
2549
  const mine = [];
2507
2550
  let others = 0;
2508
2551
  for (const comment of comments) {
@@ -2538,6 +2581,33 @@ export function backlinkReport(outcome, ref, prUrl) {
2538
2581
  );
2539
2582
  }
2540
2583
 
2584
+ /** Complete canonical pagination is shared by the writer and its scoped controller. */
2585
+ export function githubBacklinkListArgs(repository, number) {
2586
+ if (
2587
+ !/^[\w.-]+\/[\w.-]+$/.test(repository) ||
2588
+ !/^[1-9]\d*$/.test(String(number))
2589
+ )
2590
+ throw new Error("invalid canonical GitHub backlink repository/issue");
2591
+ return [
2592
+ "api",
2593
+ "--paginate",
2594
+ "--slurp",
2595
+ `repos/${repository}/issues/${number}/comments?per_page=100`,
2596
+ ];
2597
+ }
2598
+
2599
+ /** Complete slurped pages remain bounded and are shared by the canonical writer and controller grant. */
2600
+ export function githubBacklinkComments(pages) {
2601
+ if (
2602
+ !Array.isArray(pages) ||
2603
+ pages.length < 1 ||
2604
+ pages.length > 100 ||
2605
+ pages.some(page => !Array.isArray(page) || page.length > 100)
2606
+ )
2607
+ throw new Error("invalid or over-bound GitHub backlink pages");
2608
+ return pages.flat();
2609
+ }
2610
+
2541
2611
  /**
2542
2612
  * Establish the backlink on a GitHub issue.
2543
2613
  * @param {string} ref Canonical `owner/repo#number` reference.
@@ -2547,17 +2617,13 @@ export function backlinkReport(outcome, ref, prUrl) {
2547
2617
  */
2548
2618
  function githubBacklink(ref, prUrl) {
2549
2619
  const [repository, number] = ref.split("#");
2550
- const listing = run(
2551
- "gh",
2552
- [
2553
- "api",
2554
- "--paginate",
2555
- `repos/${repository}/issues/${number}/comments?per_page=100`,
2556
- ],
2557
- { allowFailure: true }
2558
- );
2620
+ const listing = run("gh", githubBacklinkListArgs(repository, number), {
2621
+ allowFailure: true,
2622
+ });
2559
2623
  if (listing.status !== 0) throw githubFailure(listing, ref);
2560
- const comments = safeJson(listing.stdout, `GitHub comments on ${ref}`);
2624
+ const comments = githubBacklinkComments(
2625
+ safeJson(listing.stdout, `GitHub comments on ${ref}`)
2626
+ );
2561
2627
  // This listing is the read-before-write, and it is fetched here rather than
2562
2628
  // passed in for exactly that reason: the version you last wrote is not the
2563
2629
  // version that is live on a surface other agents also comment on.