@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
@@ -163,6 +163,28 @@ export function verifyHistoricalProvider(
163
163
  return providerOrigin(policy, descriptor, true, execute);
164
164
  }
165
165
 
166
+ /** Cancellation authenticates an old origin against a separately observed current main. */
167
+ export function verifyStaleOriginProvider(
168
+ policy,
169
+ descriptor,
170
+ currentParent,
171
+ execute = boundedSpawnSync
172
+ ) {
173
+ requireProof(
174
+ typeof currentParent === "string" &&
175
+ /^[a-f0-9]{40}$/.test(currentParent) &&
176
+ currentParent !== descriptor.parent,
177
+ "cancellation requires a distinct current parent"
178
+ );
179
+ const read = endpoint =>
180
+ ghJson(policy, [API, HOST_FLAG, PUBLIC_HOST, endpoint], execute);
181
+ repositoryState(read, policy, currentParent);
182
+ return {
183
+ run: runState(read, policy, descriptor, true),
184
+ claim: claimState(read, policy, descriptor),
185
+ };
186
+ }
187
+
166
188
  /** Fresh permission checks its own invocation and the exact observed destination. */
167
189
  export function verifyRecoveryProvider(
168
190
  policy,
@@ -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 ||
@@ -8,6 +8,8 @@ import { locateCheckpoint } from "./npm-update-recovery.mjs";
8
8
  import { runProcess } from "./npm-update-process.mjs";
9
9
  import { sha256 } from "./github-attestation-verifier.mjs";
10
10
  import { registryVersion } from "./npm-update-npm.mjs";
11
+ import { discoverSupersession } from "./npm-update-supersession.mjs";
12
+ import { canonicalJson } from "../lisa-automation-provenance.mjs";
11
13
  import {
12
14
  buildDraft,
13
15
  qualityGates,
@@ -72,7 +74,7 @@ export async function filingEvidence(api, proposal, config, cwd) {
72
74
  }
73
75
 
74
76
  /** Same selected versions are discovered before deriving a new parent-bound key. */
75
- export async function discoverPrior(api, proposal) {
77
+ export async function discoverPrior(api, proposal, context) {
76
78
  const query = `repo:${proposal.repository} "${proposal.selectionKey}" is:issue in:body`;
77
79
  const found = await api.request(
78
80
  `search/issues?q=${encodeURIComponent(query)}&per_page=100`
@@ -80,12 +82,14 @@ export async function discoverPrior(api, proposal) {
80
82
  required(
81
83
  found.incomplete_results === false &&
82
84
  Number.isSafeInteger(found.total_count) &&
83
- found.total_count <= 1 &&
85
+ found.total_count <= (context ? 100 : 1) &&
84
86
  Array.isArray(found.items) &&
85
87
  found.items.length === found.total_count,
86
88
  "prior selection search is incomplete or ambiguous"
87
89
  );
88
90
  if (!found.total_count) return { status: "new" };
91
+ if (context && (found.total_count > 1 || found.items[0].state === "closed"))
92
+ return discoverSupersession(api, proposal, found.items, context);
89
93
  const issue = await api.issue(found.items[0].number);
90
94
  required(
91
95
  issue.state === "open" &&
@@ -254,7 +258,7 @@ export async function allocateLeaf({
254
258
  let proposal = suppliedProposal;
255
259
  validateProposal(proposal, policy);
256
260
  await api.main(proposal.parent);
257
- const prior = await discoverPrior(api, proposal);
261
+ const prior = await discoverPrior(api, proposal, { cwd, config });
258
262
  if (prior.status === "stale-outstanding")
259
263
  return {
260
264
  status: prior.status,
@@ -263,6 +267,12 @@ export async function allocateLeaf({
263
267
  };
264
268
  await api.assignable(policy.maintainer);
265
269
  const currentEvidence = await filingEvidence(api, proposal, config, cwd);
270
+ if (prior.supersession)
271
+ currentEvidence.supersession = {
272
+ workItem: prior.supersession.workItem,
273
+ cancellationSha256: sha256(`${canonicalJson(prior.supersession)}\n`),
274
+ proposalKey: prior.supersession.oldProposalKey,
275
+ };
266
276
  const checkpoint = prior.issue
267
277
  ? locateCheckpoint(prior.issue.comments, proposal, policy)
268
278
  : undefined;
@@ -0,0 +1,291 @@
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
+ /** Cancellation reads immutable old authority without granting that authority to new bytes. */
5
+ import { join } from "node:path";
6
+ import { writeFileSync } from "node:fs";
7
+ import { canonicalJson } from "../lisa-automation-provenance.mjs";
8
+ import {
9
+ required,
10
+ keys,
11
+ OBJECT,
12
+ validatePolicy,
13
+ validateProposal,
14
+ } from "./npm-update-contract.mjs";
15
+ import {
16
+ runProcess,
17
+ withPrivateRoot,
18
+ writeJson,
19
+ } from "./npm-update-process.mjs";
20
+ import { decodeCheckpoint } from "./npm-update-checkpoint.mjs";
21
+ import {
22
+ buildDraft,
23
+ historicalFiling,
24
+ inspectLeaf,
25
+ inspectCancelledLeaf,
26
+ qualityGates,
27
+ } from "./npm-update-leaf.mjs";
28
+ import { sha256 } from "./github-attestation-verifier.mjs";
29
+ import { verifyStaleOriginProvider } from "./github-attestation-provider.mjs";
30
+ import { verifyHistoricalDescriptor } from "./github-attestation-recovery.mjs";
31
+ import { DESCRIPTOR_KEYS } from "./automation-provenance-contract.mjs";
32
+
33
+ /** Only complete, digest-checked transport is considered; incomplete chunks grant nothing. */
34
+ export function completeCheckpoints(comments) {
35
+ required(
36
+ Array.isArray(comments) && comments.length <= 2000,
37
+ "checkpoint inventory exceeds bound"
38
+ );
39
+ const digests = new Set();
40
+ for (const comment of comments) {
41
+ const match = /^\[lisa-npm-checkpoint\] v1 ([a-f0-9]{64}) complete\n/.exec(
42
+ comment.body ?? ""
43
+ );
44
+ if (match) digests.add(match[1]);
45
+ }
46
+ required(digests.size <= 100, "completed checkpoint inventory exceeds bound");
47
+ return [...digests].map(digest => ({
48
+ digest,
49
+ payload: decodeCheckpoint(comments, digest),
50
+ }));
51
+ }
52
+
53
+ /** Named committed objects, never ignored config or candidate paths, select historical authority. */
54
+ export async function committedCancellationConfig(cwd, parent) {
55
+ required(
56
+ typeof parent === "string" && OBJECT.test(parent),
57
+ "invalid committed cancellation parent"
58
+ );
59
+ const result = await runProcess(
60
+ "git",
61
+ ["show", `${parent}:.lisa.config.json`],
62
+ {
63
+ cwd,
64
+ env: {
65
+ PATH: process.env.PATH,
66
+ HOME: "/nonexistent",
67
+ GIT_TERMINAL_PROMPT: "0",
68
+ },
69
+ maximum: 1_048_576,
70
+ }
71
+ );
72
+ const config = JSON.parse(result.stdout.toString());
73
+ required(
74
+ config.automationProvenance?.enabled === true,
75
+ "original committed authority is disabled"
76
+ );
77
+ validatePolicy(config.npmUpdater, config);
78
+ required(
79
+ config.automationProvenance.repository === config.npmUpdater.repository,
80
+ "original committed authority repository differs"
81
+ );
82
+ return config;
83
+ }
84
+
85
+ /** One original signed checkpoint is required, even if an unsigned filing or cancellation is present. */
86
+ function originCheckpoint(issue) {
87
+ const origins = completeCheckpoints(issue.comments).filter(
88
+ ({ payload }) => payload.preview?.descriptor
89
+ );
90
+ required(
91
+ origins.length === 1 && origins[0].payload.bundle.length > 0,
92
+ "complete original signed checkpoint is missing or ambiguous"
93
+ );
94
+ return origins[0];
95
+ }
96
+
97
+ /** Original chunk bytes must be the unedited Bot's posts during its witnessed run, not later copied markers. */
98
+ export function assertOriginTransport(comments, digest, policy, run) {
99
+ const start = Date.parse(run.run_started_at),
100
+ end = Date.parse(run.updated_at);
101
+ required(
102
+ Number.isFinite(start) && Number.isFinite(end) && start <= end,
103
+ "invalid original transport chronology"
104
+ );
105
+ const selected = comments.filter(comment =>
106
+ comment.body?.startsWith(`[lisa-npm-checkpoint] v1 ${digest} `)
107
+ );
108
+ required(selected.length > 0, "original transport is missing");
109
+ for (const comment of selected) {
110
+ const posted = Date.parse(comment.created_at);
111
+ required(
112
+ comment.user?.type === "Bot" &&
113
+ String(comment.user.id) === policy.claimActorId &&
114
+ comment.updated_at === comment.created_at &&
115
+ Number.isFinite(posted) &&
116
+ posted >= start - 60_000 &&
117
+ posted <= end + 60_000,
118
+ "original checkpoint was edited, copied or posted by a foreign actor"
119
+ );
120
+ }
121
+ }
122
+
123
+ /** Original claim, body, policy and proposal are checked separately from current authority. */
124
+ async function inspectOrigin(api, issue, payload, oldConfig, currentConfig) {
125
+ const policy = validatePolicy(oldConfig.npmUpdater, oldConfig);
126
+ const proposal = validateProposal(payload.proposal, policy);
127
+ const allocation = payload.allocation;
128
+ keys(allocation, [
129
+ "number",
130
+ "workItem",
131
+ "claimCommentId",
132
+ "claimSha256",
133
+ "draft",
134
+ "evidence",
135
+ "quality",
136
+ ]);
137
+ const evidence = historicalFiling(issue);
138
+ const draft = buildDraft(proposal, policy, oldConfig, evidence);
139
+ required(
140
+ canonicalJson(allocation.evidence) === canonicalJson(evidence) &&
141
+ canonicalJson(allocation.draft) === canonicalJson(draft) &&
142
+ allocation.number === issue.number &&
143
+ allocation.workItem === `${policy.repository}#${issue.number}`,
144
+ "original allocation differs"
145
+ );
146
+ // The current qualified classifier checks present holds; historical config only supplies the original draft.
147
+ const state = await (
148
+ issue.state === "open" ? inspectLeaf : inspectCancelledLeaf
149
+ )(issue, draft, policy, currentConfig);
150
+ required(
151
+ state.claimed &&
152
+ !state.unassigned &&
153
+ state.claims.length === 1 &&
154
+ String(state.claims[0].id) === allocation.claimCommentId &&
155
+ sha256(state.claims[0].body) === allocation.claimSha256 &&
156
+ String(state.claims[0].user?.id) === api.policy.claimActorId &&
157
+ state.claims[0].user?.type === "Bot",
158
+ "original managed claim changed"
159
+ );
160
+ qualityGates(
161
+ { ...draft, body: issue.body, title: issue.title },
162
+ proposal,
163
+ policy,
164
+ oldConfig,
165
+ evidence
166
+ );
167
+ return { policy, proposal, allocation };
168
+ }
169
+
170
+ /** Descriptor fields preserve the complete original immutable proposal and claim. */
171
+ function originalDescriptor(checkpoint, old, allocation, authority) {
172
+ const preview = checkpoint.payload.preview;
173
+ keys(preview, ["descriptor", "message", "epoch"]);
174
+ const d = preview.descriptor;
175
+ keys(d, DESCRIPTOR_KEYS);
176
+ required(
177
+ d.version === 1 &&
178
+ d.parent === old.parent &&
179
+ d.queue === old.repository &&
180
+ d.workItem === allocation.workItem &&
181
+ d.claimCommentId === allocation.claimCommentId &&
182
+ d.claimSha256 === allocation.claimSha256 &&
183
+ d.proposalKey === old.bindingKey &&
184
+ d.policySha256 === sha256(canonicalJson(authority)) &&
185
+ canonicalJson(d.files) === canonicalJson(old.hashes) &&
186
+ canonicalJson(d.updates) === canonicalJson(old.updates) &&
187
+ typeof preview.message === "string" &&
188
+ sha256(preview.message) === d.messageSha256 &&
189
+ checkpoint.digest === sha256(`${canonicalJson(d)}\n`),
190
+ "original descriptor differs"
191
+ );
192
+ return d;
193
+ }
194
+
195
+ /** The original fixed signing purpose and native transport chronology remain mandatory. */
196
+ async function verifyOriginalSubject(checkpoint, authority, d, issue, parent) {
197
+ await withPrivateRoot(async root => {
198
+ const paths = {
199
+ descriptor: join(root, "descriptor.json"),
200
+ bundle: join(root, "bundle.json"),
201
+ };
202
+ writeJson(paths.descriptor, d);
203
+ required(
204
+ Buffer.byteLength(checkpoint.payload.bundle) <= 1_048_576,
205
+ "origin bundle exceeds bound"
206
+ );
207
+ writeFileSync(paths.bundle, checkpoint.payload.bundle, {
208
+ mode: 0o600,
209
+ flag: "wx",
210
+ });
211
+ const observed = verifyStaleOriginProvider(authority, d, parent);
212
+ assertOriginTransport(
213
+ issue.comments,
214
+ checkpoint.digest,
215
+ authority,
216
+ observed.run
217
+ );
218
+ verifyHistoricalDescriptor(
219
+ authority,
220
+ d,
221
+ paths,
222
+ checkpoint.digest,
223
+ observed.run,
224
+ observed.claim
225
+ );
226
+ });
227
+ }
228
+
229
+ /** Proof reads use the actual original pin and original run, while live main remains the replacement parent. */
230
+ export async function authenticateStaleOrigin({
231
+ api,
232
+ cwd,
233
+ issue,
234
+ proposal,
235
+ config,
236
+ }) {
237
+ await api.main(proposal.parent);
238
+ const checkpoint = originCheckpoint(issue);
239
+ const old = checkpoint.payload.proposal;
240
+ required(
241
+ old.parent !== proposal.parent &&
242
+ old.selectionKey === proposal.selectionKey,
243
+ "origin is not this stale selection"
244
+ );
245
+ const oldConfig = await committedCancellationConfig(cwd, old.parent);
246
+ const authority = oldConfig.automationProvenance;
247
+ required(
248
+ [
249
+ "repository",
250
+ "repositoryId",
251
+ "ownerId",
252
+ "claimActorId",
253
+ "ghExecutable",
254
+ "ghSha256",
255
+ ].every(key => authority[key] === api.policy[key]),
256
+ "original and current repository/transport identity differs"
257
+ );
258
+ const inspected = await inspectOrigin(
259
+ api,
260
+ issue,
261
+ checkpoint.payload,
262
+ oldConfig,
263
+ config
264
+ );
265
+ const manifest = await runProcess(
266
+ "git",
267
+ ["show", `${old.parent}:package.json`],
268
+ {
269
+ cwd,
270
+ env: {
271
+ PATH: process.env.PATH,
272
+ HOME: "/nonexistent",
273
+ GIT_TERMINAL_PROMPT: "0",
274
+ },
275
+ maximum: 1_048_576,
276
+ }
277
+ );
278
+ required(
279
+ canonicalJson(JSON.parse(manifest.stdout.toString())) ===
280
+ canonicalJson(old.before),
281
+ "original committed manifest differs"
282
+ );
283
+ const d = originalDescriptor(
284
+ checkpoint,
285
+ old,
286
+ inspected.allocation,
287
+ authority
288
+ );
289
+ await verifyOriginalSubject(checkpoint, authority, d, issue, proposal.parent);
290
+ return { ...inspected, checkpoint, oldConfig, descriptor: d };
291
+ }