@codyswann/lisa 4.71.1 → 4.71.3

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 (76) hide show
  1. package/all/copy-overwrite/scripts/lib/github-attestation-provider.mjs +22 -0
  2. package/all/copy-overwrite/scripts/lib/history-secret-scanner.mjs +46 -37
  3. package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +13 -3
  4. package/all/copy-overwrite/scripts/lib/npm-update-cancel-origin.mjs +291 -0
  5. package/all/copy-overwrite/scripts/lib/npm-update-cancellation-proof.mjs +327 -0
  6. package/all/copy-overwrite/scripts/lib/npm-update-cancellation.mjs +248 -0
  7. package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +52 -35
  8. package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +4 -0
  9. package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +48 -26
  10. package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +101 -0
  11. package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +1 -1
  12. package/all/copy-overwrite/scripts/lib/npm-update-leaf.mjs +29 -3
  13. package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +24 -6
  14. package/all/copy-overwrite/scripts/lib/npm-update-supersession.mjs +274 -0
  15. package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +98 -21
  16. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +82 -12
  17. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  18. package/dist/core/lisa-owned-hash-ledger.js +86 -0
  19. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  20. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  21. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  22. package/dist/core/upstream-evidence-manifest.js +28 -14
  23. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  24. package/package.json +4 -4
  25. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-agy/plugin.json +1 -1
  28. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  31. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-expo-agy/plugin.json +1 -1
  38. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  43. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  48. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  53. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  58. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-rails-agy/plugin.json +1 -1
  63. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  68. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  73. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  75. package/rails/copy-overwrite/ast-grep/rules/ruby/no-unsafe-send.yml +17 -0
  76. package/scripts/two-channel-couplings.json +2 -2
@@ -42,6 +42,9 @@ const PRODUCER = [
42
42
  "allocate",
43
43
  "authorization",
44
44
  "broker-client",
45
+ "cancel-origin",
46
+ "cancellation",
47
+ "cancellation-proof",
45
48
  "checkpoint",
46
49
  "contract",
47
50
  "controller-broker",
@@ -81,6 +84,7 @@ const PRODUCER = [
81
84
  "publish",
82
85
  "quality",
83
86
  "recovery",
87
+ "supersession",
84
88
  "runtime-archive",
85
89
  "runtime-graph",
86
90
  "runtime-transport",
@@ -21,6 +21,7 @@ import {
21
21
  } from "./npm-update-controller-factory.mjs";
22
22
  import { startHookReadBroker } from "./npm-update-hook-provider.mjs";
23
23
  import { originalHookInstallation } from "./npm-update-hook-installation.mjs";
24
+ import { withStage } from "./npm-update-invariants.mjs";
24
25
  import {
25
26
  sha256,
26
27
  PROPOSAL_PREDICATE,
@@ -234,22 +235,9 @@ function nodeGateway(root, context) {
234
235
  return bin;
235
236
  }
236
237
 
237
- /** A separate read-only capability is available during original gates; it cannot acquire publisher/issuer permissions. */
238
- export async function createHostedGate(context, root, env) {
239
- assertHostedGate(context);
240
- const native = controllerTools(context.config.automationProvenance);
241
- const graph = await qualifiedControllerGraph(context.cwd, context.config, [
242
- "lisa-work-item.mjs",
243
- "lisa-automation-provenance.mjs",
244
- "lisa-rails-prepush.mjs",
245
- "lib/npm-update-hosted-hook.mjs",
246
- "lib/npm-update-hook-preload.mjs",
247
- ]);
248
- const installed = await installHostedDependencies(context, root, env);
249
- const installation = await installOriginalManager(context, installed);
250
- const scope = await proofScope(context, installed);
251
- mkdirSync(join(root, "gh-config"), { mode: 0o700 });
252
- 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 {
253
241
  version: 1,
254
242
  deadline: context.deadline,
255
243
  cwd: context.cwd,
@@ -267,16 +255,50 @@ export async function createHostedGate(context, root, env) {
267
255
  scope.proofs
268
256
  ),
269
257
  };
270
- 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
+ );
271
291
  try {
272
- const bin = nodeGateway(root, {
273
- version: 1,
274
- root,
275
- cwd: context.cwd,
276
- graph,
277
- native,
278
- reader: { root, path: broker.path, deadline: context.deadline },
279
- });
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
+ );
280
302
  return {
281
303
  installation,
282
304
  env: {
@@ -294,7 +316,7 @@ export async function createHostedGate(context, root, env) {
294
316
  close: broker.close,
295
317
  };
296
318
  } catch (error) {
297
- await broker.close();
319
+ await withStage("gate-close", () => broker.close());
298
320
  throw error;
299
321
  }
300
322
  }
@@ -2,6 +2,107 @@
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
+ "cancel-intent",
8
+ "cancel-origin",
9
+ "cancel-checkpoint",
10
+ "cancel-close",
11
+ "gate-input",
12
+ "gate-validate",
13
+ "gate-scratch",
14
+ "gate-stage",
15
+ "gate-baseline",
16
+ "gate-link",
17
+ "gate-attach",
18
+ "gate-proof",
19
+ "gate-tools",
20
+ "gate-graph",
21
+ "gate-install",
22
+ "gate-hooks",
23
+ "gate-scope",
24
+ "gate-broker",
25
+ "gate-gateway",
26
+ "gate-commit",
27
+ "gate-push",
28
+ "gate-close",
29
+ "gate-output",
30
+ ]);
31
+ const diagnostics = new WeakMap();
32
+ const signals = new Set([
33
+ "SIGTERM",
34
+ "SIGINT",
35
+ "SIGKILL",
36
+ "SIGHUP",
37
+ "SIGQUIT",
38
+ "SIGABRT",
39
+ "SIGPIPE",
40
+ "SIGSEGV",
41
+ "SIGBUS",
42
+ "SIGILL",
43
+ "SIGTRAP",
44
+ "SIGFPE",
45
+ ]);
46
+ const errnos = new Set([
47
+ "EACCES",
48
+ "ENOENT",
49
+ "ENOEXEC",
50
+ "EMFILE",
51
+ "ENFILE",
52
+ "EAGAIN",
53
+ "EIO",
54
+ "EPIPE",
55
+ ]);
56
+ const categories = new Set([
57
+ "exit",
58
+ "signal",
59
+ "cancelled",
60
+ "deadline",
61
+ "output-bound",
62
+ "input-failed",
63
+ "spawn-error",
64
+ ]);
65
+
66
+ /** Nested stages preserve the innermost observed boundary and the exact original thrown value. */
67
+ export async function withStage(stage, operation) {
68
+ required(stages.has(stage), "invalid updater diagnostic stage");
69
+ try {
70
+ return await operation();
71
+ } catch (error) {
72
+ if (
73
+ error !== null &&
74
+ (typeof error === "object" || typeof error === "function")
75
+ ) {
76
+ const prior = diagnostics.get(error) ?? {};
77
+ diagnostics.set(error, { ...prior, stage: prior.stage ?? stage });
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ /** Only native capture registers observed close facts; exception properties never establish provenance. */
84
+ export function recordNativeFailure(error, category, status, signal, errno) {
85
+ diagnostics.set(error, {
86
+ ...diagnostics.get(error),
87
+ native: categories.has(category) ? category : "unknown",
88
+ status:
89
+ Number.isInteger(status) && status >= 0 && status <= 255 ? status : null,
90
+ signal: signals.has(signal) ? signal : null,
91
+ errno: errnos.has(errno) ? errno : null,
92
+ });
93
+ }
94
+
95
+ /** Never inspect arbitrary messages, stacks, getters, commands, paths or captured child output. */
96
+ export function publicFailure(error) {
97
+ const facts = diagnostics.get(error);
98
+ const parts = [`stage=${facts?.stage ?? "unknown"}`];
99
+ if (facts?.native) {
100
+ parts.push(`native=${facts.native}`, `status=${facts.status ?? "null"}`);
101
+ if (facts.signal) parts.push(`signal=${facts.signal}`);
102
+ if (facts.errno) parts.push(`errno=${facts.errno}`);
103
+ }
104
+ return `npm updater failed: required policy, authorization, installation, gate or exact publication evidence was unavailable (${parts.join(" ")})`;
105
+ }
5
106
 
6
107
  /** Only trusted static boundary reasons may leave a phase as a public diagnostic. */
7
108
  export class UpdaterError extends Error {
@@ -142,7 +142,7 @@ Git history command: ${evidence.history.command}
142
142
  Git history outcome: ${evidence.history.result}
143
143
  All-state GitHub query: ${evidence.search.query}
144
144
  Outcome: every matching record was loaded completely; unrelated work was preserved and exact proposal recovery was checked separately.
145
- Matches are read completely before allocating this deterministic proposal. No parent or PRD lineage is declared; this build-ready standalone Task is one leaf.
145
+ Matches are read completely before allocating this deterministic proposal. No parent or PRD lineage is declared; this build-ready standalone Task is one leaf.${evidence.supersession ? `\nSupersedes cancelled proposal ${evidence.supersession.workItem}. Original proposal key: ${evidence.supersession.proposalKey}. Verified cancellation SHA256: ${evidence.supersession.cancellationSha256}. The previous claim, specification and checkpoint are preserved; this leaf has fresh parent-bound authority.` : ""}
146
146
 
147
147
  ## Validation Journey
148
148
 
@@ -82,12 +82,29 @@ export async function claimLeaf(
82
82
 
83
83
  /** Complete provider history and immutable human policy govern every mutation. */
84
84
  export async function inspectLeaf(issue, draft, policy, config) {
85
- const roles = leafRoles(config, policy.repository);
86
- const labels = issue.labels.map(label => label.name);
87
85
  required(
88
86
  issue.state === "open" && !issue.pull_request && issue.number > 0,
89
87
  "closed or nonissue leaf"
90
88
  );
89
+ return inspectLeafFields(issue, draft, policy, config);
90
+ }
91
+
92
+ /** Closed-state inspection never grants authority without a separately verified cancellation. */
93
+ export async function inspectCancelledLeaf(issue, draft, policy, config) {
94
+ required(
95
+ issue.state === "closed" &&
96
+ issue.state_reason === "not_planned" &&
97
+ !issue.pull_request &&
98
+ issue.number > 0,
99
+ "leaf is not explicitly cancelled"
100
+ );
101
+ return inspectLeafFields(issue, draft, policy, config);
102
+ }
103
+
104
+ /** Both fixed state entry points preserve the original complete ownership and hold checks. */
105
+ async function inspectLeafFields(issue, draft, policy, config) {
106
+ const roles = leafRoles(config, policy.repository);
107
+ const labels = issue.labels.map(label => label.name);
91
108
  required(
92
109
  issue.children.length === 0 &&
93
110
  issue.blockers.every(item => item.state === "closed"),
@@ -151,7 +168,16 @@ export function historicalFiling(issue) {
151
168
  const value = JSON.parse(
152
169
  new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes)
153
170
  );
154
- keys(value, ["history", "search", "registry", "labels", "main"]);
171
+ keys(value, [
172
+ "history",
173
+ "search",
174
+ "registry",
175
+ "labels",
176
+ "main",
177
+ ...(value.supersession ? ["supersession"] : []),
178
+ ]);
179
+ if (value.supersession)
180
+ keys(value.supersession, ["workItem", "cancellationSha256", "proposalKey"]);
155
181
  required(
156
182
  canonicalJson(value) === bytes.toString("utf8"),
157
183
  "original filing fields are noncanonical"
@@ -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),
@@ -0,0 +1,274 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+
4
+ /** Authenticated cancellation chains remain finite, exact and separate from ordinary recovery. */
5
+ import { canonicalJson } from "../lisa-automation-provenance.mjs";
6
+ import { required } from "./npm-update-contract.mjs";
7
+ import { validateProposal, keys } from "./npm-update-contract.mjs";
8
+ import { join } from "node:path";
9
+ import { writeFileSync } from "node:fs";
10
+ import { writeJson, withPrivateRoot } from "./npm-update-process.mjs";
11
+ import { sha256 } from "./github-attestation-verifier.mjs";
12
+ import {
13
+ completeCheckpoints,
14
+ committedCancellationConfig,
15
+ authenticateStaleOrigin,
16
+ assertOriginTransport,
17
+ } from "./npm-update-cancel-origin.mjs";
18
+ import {
19
+ cancellationRecord,
20
+ validateCancellation,
21
+ assertRecordedOperator,
22
+ verifyRecordedCancellation,
23
+ } from "./npm-update-cancellation-proof.mjs";
24
+
25
+ /** This pure topology function consumes already verified records; it grants no signature or provider authority. */
26
+ export function selectSupersession(issues, records, proposal) {
27
+ required(
28
+ issues.length > 0 && issues.length <= 100,
29
+ "supersession inventory exceeds bound"
30
+ );
31
+ const keys = new Map(issues.map(issue => [issue.key, issue]));
32
+ required(keys.size === issues.length, "duplicate proposal keys");
33
+ const active = issues.filter(issue => issue.state === "open");
34
+ required(
35
+ active.length <= 1 && records.size === issues.length - active.length,
36
+ "unverified closure or multiple active proposals"
37
+ );
38
+ const incoming = new Set();
39
+ for (const issue of issues.filter(item => item.state !== "open")) {
40
+ const record = records.get(issue.number);
41
+ required(
42
+ issue.state === "closed" &&
43
+ issue.state_reason === "not_planned" &&
44
+ record?.oldProposalKey === issue.key &&
45
+ record.proposalKey !== issue.key &&
46
+ !incoming.has(record.proposalKey),
47
+ "invalid or forked cancellation chain"
48
+ );
49
+ incoming.add(record.proposalKey);
50
+ }
51
+ const roots = issues.filter(issue => !incoming.has(issue.key));
52
+ required(roots.length === 1, "disconnected or cyclic cancellation chain");
53
+ let cursor = roots[0],
54
+ count = 0,
55
+ last;
56
+ while (cursor.state === "closed") {
57
+ required(++count <= issues.length, "cyclic cancellation chain");
58
+ last = records.get(cursor.number);
59
+ cursor = keys.get(last.proposalKey);
60
+ if (!cursor) break;
61
+ }
62
+ required(
63
+ count === records.size && (!active.length || cursor === active[0]),
64
+ "disconnected cancellation chain"
65
+ );
66
+ if (cursor)
67
+ return {
68
+ status:
69
+ cursor.parent === proposal.parent ? "existing" : "stale-outstanding",
70
+ issue: cursor.issue ?? cursor,
71
+ };
72
+ required(
73
+ last?.proposalKey === proposal.key &&
74
+ last.parent === proposal.parent &&
75
+ canonicalJson(last.proposalHashes) === canonicalJson(proposal.hashes),
76
+ "cancelled replacement no longer matches; preserve the chain"
77
+ );
78
+ return { status: "new", supersession: last };
79
+ }
80
+
81
+ /** Both branch and every historical PR must actually be absent before cancellation or reuse. */
82
+ export async function cancellationDestinationAbsent(api, proposalKey) {
83
+ required(
84
+ typeof proposalKey === "string" && /^[a-f0-9]{64}$/.test(proposalKey),
85
+ "invalid cancellation destination"
86
+ );
87
+ const branch = `lisa/npm-${proposalKey}`;
88
+ required(
89
+ (await api.maybe(
90
+ `repos/${api.policy.repository}/git/ref/heads/${branch}`
91
+ )) === null,
92
+ "published branch prevents cancellation"
93
+ );
94
+ const head = encodeURIComponent(
95
+ `${api.policy.repository.split("/")[0]}:${branch}`
96
+ );
97
+ const pulls = await api.list(
98
+ `repos/${api.policy.repository}/pulls?state=all&head=${head}`
99
+ );
100
+ required(pulls.length === 0, "published PR prevents cancellation");
101
+ }
102
+
103
+ /** Checkpoint discovery remains transport-only and refuses unknown complete roles. */
104
+ function cancellationCheckpoint(issue) {
105
+ const checkpoints = completeCheckpoints(issue.comments);
106
+ for (const { payload } of checkpoints)
107
+ required(
108
+ payload.preview === null ||
109
+ payload.preview?.descriptor ||
110
+ payload.preview?.cancellation,
111
+ "unknown completed checkpoint role"
112
+ );
113
+ const matches = checkpoints.filter(
114
+ ({ payload }) => payload.preview?.cancellation
115
+ );
116
+ required(matches.length <= 1, "ambiguous completed cancellation records");
117
+ return matches[0];
118
+ }
119
+
120
+ /** Provider operator and official signature independently authenticate the complete historical subject. */
121
+ async function verifyRecordedBundle(api, authority, checkpoint, record, issue) {
122
+ const run = await api.request(
123
+ `repos/${record.repository}/actions/runs/${record.runId}/attempts/${record.runAttempt}`
124
+ );
125
+ const permission = await api.request(
126
+ `repos/${record.repository}/collaborators/${record.maintainer}/permission`
127
+ );
128
+ assertRecordedOperator(run, permission, authority, record);
129
+ assertOriginTransport(issue.comments, checkpoint.digest, authority, run);
130
+ await withPrivateRoot(async root => {
131
+ const paths = {
132
+ cancellation: join(root, "cancellation.json"),
133
+ cancellationBundle: join(root, "bundle.json"),
134
+ };
135
+ writeJson(paths.cancellation, record);
136
+ required(
137
+ Buffer.byteLength(checkpoint.payload.bundle) <= 1_048_576,
138
+ "cancellation bundle exceeds bound"
139
+ );
140
+ writeFileSync(paths.cancellationBundle, checkpoint.payload.bundle, {
141
+ mode: 0o600,
142
+ flag: "wx",
143
+ });
144
+ verifyRecordedCancellation(
145
+ authority,
146
+ record,
147
+ paths,
148
+ checkpoint.digest,
149
+ run
150
+ );
151
+ });
152
+ }
153
+
154
+ /** A completion marker is only transport; original authority and a third fixed proof are independently required. */
155
+ export async function recordedCancellation({
156
+ api,
157
+ cwd,
158
+ issue,
159
+ proposal,
160
+ config,
161
+ origin,
162
+ }) {
163
+ const checkpoint = cancellationCheckpoint(issue);
164
+ if (!checkpoint) return undefined;
165
+ keys(checkpoint.payload.preview, ["cancellation"]);
166
+ const record = checkpoint.payload.preview.cancellation;
167
+ validateCancellation(record, record);
168
+ const historical = await committedCancellationConfig(cwd, record.parent);
169
+ const authority = historical.automationProvenance;
170
+ required(
171
+ [
172
+ "repository",
173
+ "repositoryId",
174
+ "ownerId",
175
+ "claimActorId",
176
+ "ghExecutable",
177
+ "ghSha256",
178
+ ].every(key => authority[key] === api.policy[key]) &&
179
+ historical.npmUpdater.maintainer === config.npmUpdater.maintainer,
180
+ "recorded cancellation authority differs"
181
+ );
182
+ const replacement = validateProposal(
183
+ checkpoint.payload.proposal,
184
+ historical.npmUpdater
185
+ );
186
+ required(
187
+ replacement.selectionKey === proposal.selectionKey,
188
+ "recorded cancellation selection differs"
189
+ );
190
+ const original =
191
+ origin ??
192
+ (await authenticateStaleOrigin({ api, cwd, issue, proposal, config }));
193
+ required(
194
+ canonicalJson(checkpoint.payload.allocation) ===
195
+ canonicalJson(original.allocation),
196
+ "recorded cancellation allocation differs"
197
+ );
198
+ const expected = cancellationRecord(
199
+ original,
200
+ replacement,
201
+ historical,
202
+ record,
203
+ record.operatorId
204
+ );
205
+ validateCancellation(record, expected);
206
+ required(
207
+ checkpoint.digest === sha256(`${canonicalJson(record)}\n`),
208
+ "cancellation subject digest differs"
209
+ );
210
+ await verifyRecordedBundle(api, authority, checkpoint, record, issue);
211
+ await api.main(proposal.parent);
212
+ await cancellationDestinationAbsent(api, record.oldProposalKey);
213
+ if (issue.state === "closed")
214
+ required(
215
+ issue.state_reason === "not_planned" &&
216
+ issue.closed_by?.type === "Bot" &&
217
+ String(issue.closed_by.id) === api.policy.claimActorId,
218
+ "closure is not the authenticated managed cancellation"
219
+ );
220
+ return checkpoint;
221
+ }
222
+
223
+ /** Every closed match must independently authenticate before a finite connected chain is accepted. */
224
+ export async function discoverSupersession(
225
+ api,
226
+ proposal,
227
+ found,
228
+ { cwd, config }
229
+ ) {
230
+ required(
231
+ found.length > 0 && found.length <= 100,
232
+ "supersession search exceeds bound"
233
+ );
234
+ const issues = [],
235
+ records = new Map();
236
+ for (const item of found) {
237
+ const issue = await api.issue(item.number);
238
+ required(
239
+ !issue.pull_request && typeof issue.body === "string",
240
+ "foreign supersession issue"
241
+ );
242
+ const one = (name, pattern) => {
243
+ const values = [
244
+ ...issue.body.matchAll(new RegExp(`^${name}: (${pattern})$`, "gm")),
245
+ ];
246
+ required(values.length === 1, "ambiguous supersession identity");
247
+ return values[0][1];
248
+ };
249
+ required(
250
+ one("Selection key", "[a-f0-9]{64}") === proposal.selectionKey,
251
+ "supersession selection differs"
252
+ );
253
+ issues.push({
254
+ number: issue.number,
255
+ state: issue.state,
256
+ state_reason: issue.state_reason,
257
+ key: one("Proposal key", "[a-f0-9]{64}"),
258
+ parent: one("Base parent", "[a-f0-9]{40}"),
259
+ issue,
260
+ });
261
+ if (issue.state !== "open") {
262
+ const checkpoint = await recordedCancellation({
263
+ api,
264
+ cwd,
265
+ issue,
266
+ proposal,
267
+ config,
268
+ });
269
+ required(checkpoint, "closed leaf lacks authenticated cancellation");
270
+ records.set(issue.number, checkpoint.payload.preview.cancellation);
271
+ }
272
+ }
273
+ return selectSupersession(issues, records, proposal);
274
+ }