@codyswann/lisa 4.71.0 → 4.71.2

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 (67) hide show
  1. package/all/copy-overwrite/scripts/lib/history-secret-scanner.mjs +46 -37
  2. package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +52 -35
  3. package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +80 -30
  4. package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +97 -0
  5. package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +24 -6
  6. package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +0 -4
  7. package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +37 -16
  8. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +11 -7
  9. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  10. package/dist/core/lisa-owned-hash-ledger.js +34 -0
  11. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  12. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  13. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  14. package/dist/core/upstream-evidence-manifest.js +14 -9
  15. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  16. package/package.json +4 -4
  17. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  23. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo-agy/plugin.json +1 -1
  30. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  50. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails-agy/plugin.json +1 -1
  55. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  60. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  65. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  67. package/rails/copy-overwrite/ast-grep/rules/ruby/no-unsafe-send.yml +17 -0
@@ -52,6 +52,18 @@ export const PINS = Object.freeze({
52
52
  const digest = bytes => createHash("sha256").update(bytes).digest("hex");
53
53
  const shellQuote = value => `'${value.replaceAll("'", "'\\''")}'`;
54
54
 
55
+ /** Keep vendor recreation private without changing the calling process's mask. */
56
+ export const scannerInvocation = (binary, argv) => ({
57
+ binary: "/bin/sh",
58
+ argv: [
59
+ "-c",
60
+ 'umask 077 || exit 1; exec "$@"',
61
+ "lisa-private-scanner",
62
+ binary,
63
+ ...argv,
64
+ ],
65
+ });
66
+
55
67
  /** Audit the actual vendor Git children: Gitleaks can swallow their failures. */
56
68
  const auditedGit = scratch => {
57
69
  const environment = gitEnvironment();
@@ -244,8 +256,8 @@ export const scanCommits = (commits, cwd, requested, heads = []) => {
244
256
  const report = join(scratch, "report.json");
245
257
  const ignore = join(scratch, "ignore");
246
258
  writeFileSync(config, "[extend]\nuseDefault = true\n", { mode: 0o600 });
247
- // Gitleaks truncates this existing file: its report keeps private mode even
248
- // when the caller's umask would otherwise create world-readable metadata.
259
+ // Gitleaks removes and recreates this file; its dedicated child sets umask
260
+ // before vendor creation, while the caller's mask remains unchanged.
249
261
  writeFileSync(report, "[]", { mode: 0o600 });
250
262
  writeFileSync(ignore, "", { mode: 0o600 });
251
263
  // An isolated bare view excludes caller ignore files even in Git metadata.
@@ -272,41 +284,38 @@ export const scanCommits = (commits, cwd, requested, heads = []) => {
272
284
  for (let offset = 0; offset < commits.length; offset += 100) {
273
285
  const group = commits.slice(offset, offset + 100);
274
286
  audit.begin();
275
- const result = spawnSync(
276
- scanner,
277
- [
278
- "git",
279
- source,
280
- "--config",
281
- config,
282
- "--gitleaks-ignore-path",
283
- ignore,
284
- "--ignore-gitleaks-allow",
285
- "--redact=100",
286
- "--no-banner",
287
- "--no-color",
288
- "--log-level",
289
- "error",
290
- "--platform",
291
- "none",
292
- "--exit-code",
293
- "42",
294
- "--timeout",
295
- "120",
296
- "--report-format",
297
- "json",
298
- "--report-path",
299
- report,
300
- "--log-opts",
301
- `--no-walk=unsorted --root --diff-merges=separate --no-ext-diff --no-textconv --no-renames ${group.join(" ")}`,
302
- ],
303
- {
304
- cwd: scratch,
305
- env: audit.environment,
306
- timeout: 130000,
307
- maxBuffer: 16 * 1024 * 1024,
308
- }
309
- );
287
+ const invocation = scannerInvocation(scanner, [
288
+ "git",
289
+ source,
290
+ "--config",
291
+ config,
292
+ "--gitleaks-ignore-path",
293
+ ignore,
294
+ "--ignore-gitleaks-allow",
295
+ "--redact=100",
296
+ "--no-banner",
297
+ "--no-color",
298
+ "--log-level",
299
+ "error",
300
+ "--platform",
301
+ "none",
302
+ "--exit-code",
303
+ "42",
304
+ "--timeout",
305
+ "120",
306
+ "--report-format",
307
+ "json",
308
+ "--report-path",
309
+ report,
310
+ "--log-opts",
311
+ `--no-walk=unsorted --root --diff-merges=separate --no-ext-diff --no-textconv --no-renames ${group.join(" ")}`,
312
+ ]);
313
+ const result = spawnSync(invocation.binary, invocation.argv, {
314
+ cwd: scratch,
315
+ env: audit.environment,
316
+ timeout: 130000,
317
+ maxBuffer: 16 * 1024 * 1024,
318
+ });
310
319
  if (
311
320
  result.error ||
312
321
  result.signal ||
@@ -42,6 +42,7 @@ import {
42
42
  ordinaryGateReceipt,
43
43
  } from "./npm-update-publication.mjs";
44
44
  import { executeCanonicalHelper } from "./npm-update-controller-factory.mjs";
45
+ import { withStage } from "./npm-update-invariants.mjs";
45
46
 
46
47
  const BOT = "github-actions[bot]";
47
48
  const MAIL = "41898282+github-actions[bot]@users.noreply.github.com";
@@ -143,7 +144,7 @@ export async function descriptorFor({
143
144
  async function stageProposal(context, env) {
144
145
  const { cwd, proposal } = context;
145
146
  const command = args => git(cwd, env, args, undefined, context.deadline);
146
- await baseline(cwd, env, proposal);
147
+ await withStage("gate-baseline", () => baseline(cwd, env, proposal));
147
148
  const parent = (await command(["rev-parse", "HEAD"])).stdout
148
149
  .toString()
149
150
  .trim();
@@ -154,8 +155,12 @@ async function stageProposal(context, env) {
154
155
  );
155
156
  const branch = `lisa/npm-${proposal.key}`;
156
157
  await command(["checkout", "-b", branch]);
157
- await executeCanonicalHelper(context, "stage-read", "link");
158
- await executeCanonicalHelper(context, "stage-read", "attach-branch");
158
+ await withStage("gate-link", () =>
159
+ executeCanonicalHelper(context, "stage-read", "link")
160
+ );
161
+ await withStage("gate-attach", () =>
162
+ executeCanonicalHelper(context, "stage-read", "attach-branch")
163
+ );
159
164
  for (const file of FILES)
160
165
  writeFileSync(join(cwd, file), proposal.files[file]);
161
166
  await command(["add", "--", ...FILES]);
@@ -263,38 +268,50 @@ async function pushProposal(context, env, branch, object) {
263
268
  export async function gateProposal(context) {
264
269
  const scoped = { ...context, deadline: Date.now() + 1_800_000 };
265
270
  const { proposal, policy, token, preview } = context;
266
- validateProposal(proposal, policy);
267
- assertHostedGate(context);
268
- validateGateProof(context);
269
- required(
270
- typeof token === "string" && token.length > 0,
271
- "readonly gate token is absent"
272
- );
273
- return withPrivateRoot(async root => {
274
- const env = {
275
- ...candidateEnvironment(root),
276
- GIT_AUTHOR_NAME: BOT,
277
- GIT_AUTHOR_EMAIL: MAIL,
278
- GIT_COMMITTER_NAME: BOT,
279
- GIT_COMMITTER_EMAIL: MAIL,
280
- GIT_AUTHOR_DATE: `${preview.epoch} +0000`,
281
- GIT_COMMITTER_DATE: `${preview.epoch} +0000`,
282
- };
283
- const branch = await stageProposal(scoped, env);
284
- const message = await installGateProof(context, root, args =>
285
- git(context.cwd, env, args, undefined, scoped.deadline)
271
+ await withStage("gate-validate", () => {
272
+ validateProposal(proposal, policy);
273
+ assertHostedGate(context);
274
+ validateGateProof(context);
275
+ required(
276
+ typeof token === "string" && token.length > 0,
277
+ "readonly gate token is absent"
286
278
  );
287
- const hosted = await createHostedGate(scoped, root, env);
288
- try {
289
- const gated = { ...scoped, hookInstallation: hosted.installation };
290
- const object = await committedProposal(
291
- gated,
292
- { ...hosted.env, LISA_NPM_HOOK_ROLE: "commit" },
293
- message
294
- );
295
- return await pushProposal(gated, hosted.env, branch, object);
296
- } finally {
297
- await hosted.close();
298
- }
299
279
  });
280
+ return withStage("gate-scratch", () =>
281
+ withPrivateRoot(async root => {
282
+ const env = {
283
+ ...candidateEnvironment(root),
284
+ GIT_AUTHOR_NAME: BOT,
285
+ GIT_AUTHOR_EMAIL: MAIL,
286
+ GIT_COMMITTER_NAME: BOT,
287
+ GIT_COMMITTER_EMAIL: MAIL,
288
+ GIT_AUTHOR_DATE: `${preview.epoch} +0000`,
289
+ GIT_COMMITTER_DATE: `${preview.epoch} +0000`,
290
+ };
291
+ const branch = await withStage("gate-stage", () =>
292
+ stageProposal(scoped, env)
293
+ );
294
+ const proofGit = args =>
295
+ git(context.cwd, env, args, undefined, scoped.deadline);
296
+ const message = await withStage("gate-proof", () =>
297
+ installGateProof(context, root, proofGit)
298
+ );
299
+ const hosted = await createHostedGate(scoped, root, env);
300
+ try {
301
+ const gated = { ...scoped, hookInstallation: hosted.installation };
302
+ const object = await withStage("gate-commit", () =>
303
+ committedProposal(
304
+ gated,
305
+ { ...hosted.env, LISA_NPM_HOOK_ROLE: "commit" },
306
+ message
307
+ )
308
+ );
309
+ return await withStage("gate-push", () =>
310
+ pushProposal(gated, hosted.env, branch, object)
311
+ );
312
+ } finally {
313
+ await withStage("gate-close", () => hosted.close());
314
+ }
315
+ })
316
+ );
300
317
  }
@@ -5,6 +5,7 @@
5
5
  import {
6
6
  existsSync,
7
7
  lstatSync,
8
+ realpathSync,
8
9
  readFileSync,
9
10
  mkdirSync,
10
11
  writeFileSync,
@@ -12,7 +13,7 @@ import {
12
13
  import { join, resolve } from "node:path";
13
14
  import { fileURLToPath } from "node:url";
14
15
  import { required } from "./npm-update-contract.mjs";
15
- import { runProcess, writeJson } from "./npm-update-process.mjs";
16
+ import { readBytes, runProcess, writeJson } from "./npm-update-process.mjs";
16
17
  import { qualifiedControllerGraph } from "./npm-update-helper.mjs";
17
18
  import {
18
19
  controllerTools,
@@ -20,6 +21,7 @@ import {
20
21
  } from "./npm-update-controller-factory.mjs";
21
22
  import { startHookReadBroker } from "./npm-update-hook-provider.mjs";
22
23
  import { originalHookInstallation } from "./npm-update-hook-installation.mjs";
24
+ import { withStage } from "./npm-update-invariants.mjs";
23
25
  import {
24
26
  sha256,
25
27
  PROPOSAL_PREDICATE,
@@ -109,18 +111,45 @@ async function installHostedDependencies(context, root, env) {
109
111
  return installed;
110
112
  }
111
113
 
114
+ /** Husky 8 declares its real installer in package metadata; dependency install remains script-disabled. */
115
+ function huskyInstaller(cwd) {
116
+ const root = join(cwd, "node_modules/husky");
117
+ const metadata = JSON.parse(
118
+ readBytes(join(root, "package.json"), 65_536, false)
119
+ );
120
+ const bin =
121
+ typeof metadata.bin === "string" ? metadata.bin : metadata.bin?.husky;
122
+ required(
123
+ metadata.name === "husky" &&
124
+ /^8\.\d+\.\d+$/.test(metadata.version) &&
125
+ typeof bin === "string" &&
126
+ /^(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_-]+\.js$/.test(bin),
127
+ "original Husky 8 installer declaration is unsupported"
128
+ );
129
+ const path = join(root, bin);
130
+ required(
131
+ realpathSync(root) === root &&
132
+ realpathSync(path) === path &&
133
+ lstatSync(path).isFile(),
134
+ "original Husky installer is unavailable or aliased"
135
+ );
136
+ return path;
137
+ }
138
+
112
139
  /** The original supported manager installs its own wrappers; no synthetic replacement hook is written. */
113
140
  async function installOriginalManager(context, env) {
114
141
  const lefthook = existsSync(join(context.cwd, "lefthook.yml"));
115
142
  if (lefthook)
116
143
  await nativeStep(context, env, "bundle", ["exec", "lefthook", "install"]);
117
144
  else {
118
- const husky = join(context.cwd, "node_modules/husky/bin.js");
119
145
  required(
120
- existsSync(husky) && existsSync(join(context.cwd, ".husky/pre-push")),
146
+ existsSync(join(context.cwd, ".husky/pre-push")),
121
147
  "original hook manager installation is unavailable"
122
148
  );
123
- await nativeStep(context, env, process.execPath, [husky]);
149
+ await nativeStep(context, env, process.execPath, [
150
+ huskyInstaller(context.cwd),
151
+ "install",
152
+ ]);
124
153
  }
125
154
  const git = args => nativeStep(context, env, "/usr/bin/git", args);
126
155
  const result = await git(["rev-parse", "--git-path", "hooks"]);
@@ -206,22 +235,9 @@ function nodeGateway(root, context) {
206
235
  return bin;
207
236
  }
208
237
 
209
- /** A separate read-only capability is available during original gates; it cannot acquire publisher/issuer permissions. */
210
- export async function createHostedGate(context, root, env) {
211
- assertHostedGate(context);
212
- const native = controllerTools(context.config.automationProvenance);
213
- const graph = await qualifiedControllerGraph(context.cwd, context.config, [
214
- "lisa-work-item.mjs",
215
- "lisa-automation-provenance.mjs",
216
- "lisa-rails-prepush.mjs",
217
- "lib/npm-update-hosted-hook.mjs",
218
- "lib/npm-update-hook-preload.mjs",
219
- ]);
220
- const installed = await installHostedDependencies(context, root, env);
221
- const installation = await installOriginalManager(context, installed);
222
- const scope = await proofScope(context, installed);
223
- mkdirSync(join(root, "gh-config"), { mode: 0o700 });
224
- const profile = {
238
+ /** The unchanged read-only subject is constructed separately so diagnostic wrapping stays within function budgets. */
239
+ function hookProfile(context, root, native, scope) {
240
+ return {
225
241
  version: 1,
226
242
  deadline: context.deadline,
227
243
  cwd: context.cwd,
@@ -239,16 +255,50 @@ export async function createHostedGate(context, root, env) {
239
255
  scope.proofs
240
256
  ),
241
257
  };
242
- const broker = await startHookReadBroker(profile, context.token);
258
+ }
259
+
260
+ /** A separate read-only capability is available during original gates; it cannot acquire publisher/issuer permissions. */
261
+ export async function createHostedGate(context, root, env) {
262
+ await withStage("gate-validate", () => assertHostedGate(context));
263
+ const native = await withStage("gate-tools", () =>
264
+ controllerTools(context.config.automationProvenance)
265
+ );
266
+ const graph = await withStage("gate-graph", () =>
267
+ qualifiedControllerGraph(context.cwd, context.config, [
268
+ "lisa-work-item.mjs",
269
+ "lisa-automation-provenance.mjs",
270
+ "lisa-rails-prepush.mjs",
271
+ "lib/npm-update-hosted-hook.mjs",
272
+ "lib/npm-update-hook-preload.mjs",
273
+ ])
274
+ );
275
+ const installed = await withStage("gate-install", () =>
276
+ installHostedDependencies(context, root, env)
277
+ );
278
+ const installation = await withStage("gate-hooks", () =>
279
+ installOriginalManager(context, installed)
280
+ );
281
+ const scope = await withStage("gate-scope", () =>
282
+ proofScope(context, installed)
283
+ );
284
+ const profile = await withStage("gate-scope", () => {
285
+ mkdirSync(join(root, "gh-config"), { mode: 0o700 });
286
+ return hookProfile(context, root, native, scope);
287
+ });
288
+ const broker = await withStage("gate-broker", () =>
289
+ startHookReadBroker(profile, context.token)
290
+ );
243
291
  try {
244
- const bin = nodeGateway(root, {
245
- version: 1,
246
- root,
247
- cwd: context.cwd,
248
- graph,
249
- native,
250
- reader: { root, path: broker.path, deadline: context.deadline },
251
- });
292
+ const bin = await withStage("gate-gateway", () =>
293
+ nodeGateway(root, {
294
+ version: 1,
295
+ root,
296
+ cwd: context.cwd,
297
+ graph,
298
+ native,
299
+ reader: { root, path: broker.path, deadline: context.deadline },
300
+ })
301
+ );
252
302
  return {
253
303
  installation,
254
304
  env: {
@@ -266,7 +316,7 @@ export async function createHostedGate(context, root, env) {
266
316
  close: broker.close,
267
317
  };
268
318
  } catch (error) {
269
- await broker.close();
319
+ await withStage("gate-close", () => broker.close());
270
320
  throw error;
271
321
  }
272
322
  }
@@ -2,6 +2,103 @@
2
2
  // Do not edit directly — durable changes belong upstream in Lisa.
3
3
 
4
4
  /** Pure shared diagnostics retain one error identity without importing producer orchestration. */
5
+ const stages = new Set([
6
+ "configuration",
7
+ "gate-input",
8
+ "gate-validate",
9
+ "gate-scratch",
10
+ "gate-stage",
11
+ "gate-baseline",
12
+ "gate-link",
13
+ "gate-attach",
14
+ "gate-proof",
15
+ "gate-tools",
16
+ "gate-graph",
17
+ "gate-install",
18
+ "gate-hooks",
19
+ "gate-scope",
20
+ "gate-broker",
21
+ "gate-gateway",
22
+ "gate-commit",
23
+ "gate-push",
24
+ "gate-close",
25
+ "gate-output",
26
+ ]);
27
+ const diagnostics = new WeakMap();
28
+ const signals = new Set([
29
+ "SIGTERM",
30
+ "SIGINT",
31
+ "SIGKILL",
32
+ "SIGHUP",
33
+ "SIGQUIT",
34
+ "SIGABRT",
35
+ "SIGPIPE",
36
+ "SIGSEGV",
37
+ "SIGBUS",
38
+ "SIGILL",
39
+ "SIGTRAP",
40
+ "SIGFPE",
41
+ ]);
42
+ const errnos = new Set([
43
+ "EACCES",
44
+ "ENOENT",
45
+ "ENOEXEC",
46
+ "EMFILE",
47
+ "ENFILE",
48
+ "EAGAIN",
49
+ "EIO",
50
+ "EPIPE",
51
+ ]);
52
+ const categories = new Set([
53
+ "exit",
54
+ "signal",
55
+ "cancelled",
56
+ "deadline",
57
+ "output-bound",
58
+ "input-failed",
59
+ "spawn-error",
60
+ ]);
61
+
62
+ /** Nested stages preserve the innermost observed boundary and the exact original thrown value. */
63
+ export async function withStage(stage, operation) {
64
+ required(stages.has(stage), "invalid updater diagnostic stage");
65
+ try {
66
+ return await operation();
67
+ } catch (error) {
68
+ if (
69
+ error !== null &&
70
+ (typeof error === "object" || typeof error === "function")
71
+ ) {
72
+ const prior = diagnostics.get(error) ?? {};
73
+ diagnostics.set(error, { ...prior, stage: prior.stage ?? stage });
74
+ }
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ /** Only native capture registers observed close facts; exception properties never establish provenance. */
80
+ export function recordNativeFailure(error, category, status, signal, errno) {
81
+ diagnostics.set(error, {
82
+ ...diagnostics.get(error),
83
+ native: categories.has(category) ? category : "unknown",
84
+ status:
85
+ Number.isInteger(status) && status >= 0 && status <= 255 ? status : null,
86
+ signal: signals.has(signal) ? signal : null,
87
+ errno: errnos.has(errno) ? errno : null,
88
+ });
89
+ }
90
+
91
+ /** Never inspect arbitrary messages, stacks, getters, commands, paths or captured child output. */
92
+ export function publicFailure(error) {
93
+ const facts = diagnostics.get(error);
94
+ const parts = [`stage=${facts?.stage ?? "unknown"}`];
95
+ if (facts?.native) {
96
+ parts.push(`native=${facts.native}`, `status=${facts.status ?? "null"}`);
97
+ if (facts.signal) parts.push(`signal=${facts.signal}`);
98
+ if (facts.errno) parts.push(`errno=${facts.errno}`);
99
+ }
100
+ return `npm updater failed: required policy, authorization, installation, gate or exact publication evidence was unavailable (${parts.join(" ")})`;
101
+ }
5
102
 
6
103
  /** Only trusted static boundary reasons may leave a phase as a public diagnostic. */
7
104
  export class UpdaterError extends Error {
@@ -5,6 +5,7 @@
5
5
  import { spawn } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { required } from "./npm-update-contract.mjs";
8
+ import { recordNativeFailure } from "./npm-update-invariants.mjs";
8
9
 
9
10
  /** The complete supervisor vector, including its control overhead, is bounded literal data. */
10
11
  function supervisedArguments(vector) {
@@ -104,27 +105,37 @@ function captureProcess(
104
105
  const errors = [];
105
106
  let size = 0;
106
107
  let failure;
108
+ let category;
109
+ let errno;
107
110
  let closed = false;
108
- const stop = reason => {
109
- failure ??= new Error(`npm updater: ${reason}`);
111
+ const stop = (reason, kind) => {
112
+ if (!failure) {
113
+ failure = new Error(`npm updater: ${reason}`);
114
+ category = kind;
115
+ }
110
116
  if (!closed && child.pid) child.kill("SIGTERM");
111
117
  };
112
- const abort = () => stop("child was cancelled");
118
+ const abort = () => stop("child was cancelled", "cancelled");
113
119
  abortSignal?.addEventListener("abort", abort, { once: true });
114
120
  if (abortSignal?.aborted) abort();
115
- const timer = setTimeout(() => stop("child exceeded deadline"), timeout);
121
+ const timer = setTimeout(
122
+ () => stop("child exceeded deadline", "deadline"),
123
+ timeout
124
+ );
116
125
  const collect = target => data => {
117
126
  size += data.length;
118
- if (size > maximum) stop("child output exceeded bound");
127
+ if (size > maximum) stop("child output exceeded bound", "output-bound");
119
128
  else target.push(data);
120
129
  };
121
130
  child.stdout.on("data", collect(chunks));
122
131
  child.stderr.on("data", collect(errors));
123
132
  child.on("error", error => {
124
133
  failure = error;
134
+ category = "spawn-error";
135
+ errno = error.code;
125
136
  });
126
137
  child.stdin.on("error", error => {
127
- if (error.code !== "EPIPE") stop("child input failed");
138
+ if (error.code !== "EPIPE") stop("child input failed", "input-failed");
128
139
  });
129
140
  child.stdin.end(input);
130
141
  child.on("close", (code, signal) => {
@@ -134,6 +145,13 @@ function captureProcess(
134
145
  if (failure || signal || !allowed.includes(code)) {
135
146
  const error =
136
147
  failure ?? new Error(`npm updater: child failed (${code ?? signal})`);
148
+ recordNativeFailure(
149
+ error,
150
+ category ?? (signal ? "signal" : "exit"),
151
+ code,
152
+ signal,
153
+ errno
154
+ );
137
155
  reject(
138
156
  Object.assign(error, {
139
157
  stdout: Buffer.concat(chunks),
@@ -17,7 +17,6 @@ import { descriptorFor } from "./npm-update-gate.mjs";
17
17
  import { assertGateTransport } from "./npm-update-gate-hooks.mjs";
18
18
  import {
19
19
  publicationBody,
20
- assertPublicationPolicy,
21
20
  publicationBacklink,
22
21
  publicationProof,
23
22
  } from "./npm-update-publication.mjs";
@@ -188,9 +187,6 @@ export async function publishProposal({
188
187
  return issue;
189
188
  };
190
189
  await authorize();
191
- assertPublicationPolicy(
192
- await api.request(`repos/${policy.repository}/actions/permissions/workflow`)
193
- );
194
190
  await publicationObjects(
195
191
  cwd,
196
192
  proposal,
@@ -11,11 +11,11 @@ import { join, resolve } from "node:path";
11
11
  import { invokedAsScript } from "./lib/invoked-as-script.mjs";
12
12
  import {
13
13
  required,
14
- UpdaterError,
15
14
  keys,
16
15
  validatePolicy,
17
16
  validateProposal,
18
17
  } from "./lib/npm-update-contract.mjs";
18
+ import { publicFailure, withStage } from "./lib/npm-update-invariants.mjs";
19
19
  import {
20
20
  readJson,
21
21
  writeJson,
@@ -119,6 +119,32 @@ async function publishPhase(
119
119
  process.stdout.write(`${result.status} ${result.url}\n`);
120
120
  }
121
121
 
122
+ /** Gate input/output classification preserves the original orchestration and authority order. */
123
+ async function gatePhase(
124
+ cwd,
125
+ output,
126
+ proposal,
127
+ policy,
128
+ config,
129
+ allocation,
130
+ preview
131
+ ) {
132
+ const result = await gateProposal({
133
+ cwd,
134
+ proposal,
135
+ policy,
136
+ allocation,
137
+ preview,
138
+ ...(await withStage("gate-input", () => readGateProof(output))),
139
+ token: process.env.GH_TOKEN,
140
+ config,
141
+ });
142
+ await withStage("gate-output", () =>
143
+ writeJson(join(output, "gated.json"), result)
144
+ );
145
+ process.stdout.write(`gated ${result.receipt.commit}\n`);
146
+ }
147
+
122
148
  /** Strict phase and fixed directory arguments are data, never a caller recipe. */
123
149
  export async function main(argv = process.argv.slice(2)) {
124
150
  required(
@@ -131,7 +157,9 @@ export async function main(argv = process.argv.slice(2)) {
131
157
  const [phase, checkout, directory] = argv;
132
158
  const cwd = resolve(checkout);
133
159
  const output = resolve(directory);
134
- const { config, policy } = await configuration(cwd);
160
+ const { config, policy } = await withStage("configuration", () =>
161
+ configuration(cwd)
162
+ );
135
163
  phaseDirectory(output);
136
164
  if (phase === "prepare") {
137
165
  const result = await prepareUpdate({ cwd, policy, config });
@@ -168,21 +196,16 @@ export async function main(argv = process.argv.slice(2)) {
168
196
  process.stdout.write(`${result.mode} checkpoint complete\n`);
169
197
  return;
170
198
  }
171
- if (phase === "gate") {
172
- const result = await gateProposal({
199
+ if (phase === "gate")
200
+ return gatePhase(
173
201
  cwd,
202
+ output,
174
203
  proposal,
175
204
  policy,
176
- allocation,
177
- preview,
178
- ...readGateProof(output),
179
- token: process.env.GH_TOKEN,
180
205
  config,
181
- });
182
- writeJson(join(output, "gated.json"), result);
183
- process.stdout.write(`gated ${result.receipt.commit}\n`);
184
- return;
185
- }
206
+ allocation,
207
+ preview
208
+ );
186
209
  return publishPhase(
187
210
  cwd,
188
211
  output,
@@ -197,8 +220,6 @@ export async function main(argv = process.argv.slice(2)) {
197
220
  if (invokedAsScript(import.meta.url))
198
221
  main().catch(error => {
199
222
  // 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
- );
223
+ process.stderr.write(`${publicFailure(error)}\n`);
203
224
  process.exitCode = 1;
204
225
  });