@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
@@ -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
  ]
@@ -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