@codyswann/lisa 4.71.2 → 4.71.4

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 (95) hide show
  1. package/all/copy-overwrite/scripts/lib/github-attestation-provider.mjs +22 -0
  2. package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +13 -3
  3. package/all/copy-overwrite/scripts/lib/npm-update-cancel-origin.mjs +291 -0
  4. package/all/copy-overwrite/scripts/lib/npm-update-cancellation-proof.mjs +327 -0
  5. package/all/copy-overwrite/scripts/lib/npm-update-cancellation.mjs +248 -0
  6. package/all/copy-overwrite/scripts/lib/npm-update-classifier-cache.mjs +94 -0
  7. package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +5 -0
  8. package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +9 -2
  9. package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +4 -0
  10. package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +1 -1
  11. package/all/copy-overwrite/scripts/lib/npm-update-leaf.mjs +29 -3
  12. package/all/copy-overwrite/scripts/lib/npm-update-supersession.mjs +274 -0
  13. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +122 -8
  14. package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +61 -5
  15. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +85 -10
  16. package/dist/cli/hook-refresh-files.d.ts +86 -0
  17. package/dist/cli/hook-refresh-files.d.ts.map +1 -0
  18. package/dist/cli/hook-refresh-files.js +209 -0
  19. package/dist/cli/hook-refresh-files.js.map +1 -0
  20. package/dist/cli/index.d.ts +3 -0
  21. package/dist/cli/index.d.ts.map +1 -1
  22. package/dist/cli/index.js +3 -0
  23. package/dist/cli/index.js.map +1 -1
  24. package/dist/cli/refresh-hooks-cmd.d.ts +27 -0
  25. package/dist/cli/refresh-hooks-cmd.d.ts.map +1 -0
  26. package/dist/cli/refresh-hooks-cmd.js +156 -0
  27. package/dist/cli/refresh-hooks-cmd.js.map +1 -0
  28. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  29. package/dist/core/lisa-owned-hash-ledger.js +78 -0
  30. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  31. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  32. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  33. package/dist/core/upstream-evidence-manifest.js +37 -14
  34. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  35. package/dist/opencode/plugin-templates/parity-safety-net.sh +122 -8
  36. package/package.json +4 -4
  37. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa/hooks/parity-safety-net.sh +122 -8
  40. package/plugins/lisa-agy/hooks/parity-safety-net.sh +122 -8
  41. package/plugins/lisa-agy/plugin.json +1 -1
  42. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  45. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +122 -8
  49. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +122 -8
  51. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-expo-agy/plugin.json +1 -1
  54. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  59. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  64. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  69. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  74. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-rails-agy/plugin.json +1 -1
  79. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  83. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  84. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  88. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  89. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  91. package/plugins/src/base/hooks/parity-safety-net.sh +122 -8
  92. package/rails/copy-overwrite/scripts/lisa-scratch-run.sh +54 -9
  93. package/scripts/build-plugins.sh +3 -0
  94. package/scripts/generate-scratch-supervisor-profile.mjs +75 -0
  95. package/scripts/two-channel-couplings.json +2 -2
@@ -0,0 +1,248 @@
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
+ /** Explicit hosted manual cancellation preserves the old leaf and issues no publication authority. */
5
+ import { join } from "node:path";
6
+ import { writeFileSync } from "node:fs";
7
+ import { canonicalJson } from "../lisa-automation-provenance.mjs";
8
+ import { required, keys } from "./npm-update-contract.mjs";
9
+ import { readBytes, readJson, writeJson } from "./npm-update-process.mjs";
10
+ import { assertHostedGate } from "./npm-update-hosted-gate.mjs";
11
+ import { sha256 } from "./github-attestation-verifier.mjs";
12
+ import { discoverPrior } from "./npm-update-allocate.mjs";
13
+ import { authenticateStaleOrigin } from "./npm-update-cancel-origin.mjs";
14
+ import {
15
+ recordedCancellation,
16
+ cancellationDestinationAbsent,
17
+ } from "./npm-update-supersession.mjs";
18
+ import {
19
+ assertManualCancellation,
20
+ cancellationRecord,
21
+ validateCancellation,
22
+ verifyCancellationAttestation,
23
+ } from "./npm-update-cancellation-proof.mjs";
24
+ import {
25
+ persistCheckpoint,
26
+ decodeCheckpoint,
27
+ } from "./npm-update-checkpoint.mjs";
28
+ import { withStage } from "./npm-update-invariants.mjs";
29
+
30
+ /** Native dispatch bytes are checked independently of reusable workflow input forwarding. */
31
+ export function assertDispatchCancellation(event, run, request, authority) {
32
+ required(
33
+ event &&
34
+ ["main", "refs/heads/main"].includes(event.ref) &&
35
+ event.repository?.full_name === authority.repository &&
36
+ String(event.repository.id) === authority.repositoryId &&
37
+ String(event.repository.owner?.id) === authority.ownerId &&
38
+ event.sender?.type === "User" &&
39
+ event.sender.id === run.triggering_actor?.id &&
40
+ event.sender.login === run.triggering_actor.login &&
41
+ event.inputs?.cancel_issue === String(request.number) &&
42
+ event.inputs.cancel_proposal_key === request.proposalKey &&
43
+ event.inputs.cancel_expected_main === request.parent,
44
+ "native manual dispatch intent differs"
45
+ );
46
+ }
47
+
48
+ /** The actual run and repository permission are read through the existing pinned default-token transport. */
49
+ async function manualAuthority(api, proposal, config, request) {
50
+ assertHostedGate({ policy: config.npmUpdater, proposal, config });
51
+ const invocation = {
52
+ runId: process.env.GITHUB_RUN_ID,
53
+ runAttempt: process.env.GITHUB_RUN_ATTEMPT,
54
+ };
55
+ required(
56
+ Object.values(invocation).every(
57
+ value => typeof value === "string" && /^[1-9]\d*$/.test(value)
58
+ ),
59
+ "native cancellation invocation is absent"
60
+ );
61
+ await api.main(proposal.parent);
62
+ const run = await api.request(
63
+ `repos/${proposal.repository}/actions/runs/${invocation.runId}/attempts/${invocation.runAttempt}`
64
+ );
65
+ const permission = await api.request(
66
+ `repos/${proposal.repository}/collaborators/${config.npmUpdater.maintainer}/permission`
67
+ );
68
+ const operatorId = assertManualCancellation(
69
+ run,
70
+ permission,
71
+ api.policy,
72
+ config.npmUpdater.maintainer,
73
+ proposal,
74
+ request,
75
+ invocation
76
+ );
77
+ const event = JSON.parse(
78
+ new TextDecoder("utf8", { fatal: true }).decode(
79
+ readBytes(process.env.GITHUB_EVENT_PATH, 1_048_576, false)
80
+ )
81
+ );
82
+ assertDispatchCancellation(event, run, request, api.policy);
83
+ return { invocation, operatorId };
84
+ }
85
+
86
+ /** Named stale origins are never substituted by a closed implementation ticket or another active leaf. */
87
+ async function cancellationState(context) {
88
+ const { api, cwd, proposal, config, request } = context;
89
+ const manual = await withStage("cancel-intent", () =>
90
+ manualAuthority(api, proposal, config, request)
91
+ );
92
+ const prior = await discoverPrior(api, proposal, { cwd, config });
93
+ const issue = await api.issue(request.number);
94
+ const origin = await withStage("cancel-origin", () =>
95
+ authenticateStaleOrigin({ api, cwd, issue, proposal, config })
96
+ );
97
+ required(
98
+ origin.proposal.key === request.proposalKey,
99
+ "named original proposal differs"
100
+ );
101
+ await cancellationDestinationAbsent(api, request.proposalKey);
102
+ const recorded = await recordedCancellation({
103
+ api,
104
+ cwd,
105
+ issue,
106
+ proposal,
107
+ config,
108
+ origin,
109
+ });
110
+ if (recorded) {
111
+ const record = recorded.payload.preview.cancellation;
112
+ required(
113
+ record.parent === proposal.parent &&
114
+ record.proposalKey === proposal.key &&
115
+ canonicalJson(record.proposalHashes) === canonicalJson(proposal.hashes),
116
+ "recorded replacement no longer matches"
117
+ );
118
+ } else
119
+ required(
120
+ issue.state === "open" &&
121
+ prior.status === "stale-outstanding" &&
122
+ prior.issue.number === request.number,
123
+ "only the active stale origin can be cancelled"
124
+ );
125
+ return { ...manual, issue, origin, recorded };
126
+ }
127
+
128
+ /** Preparation writes only a private prospective subject, never a provider lifecycle mutation. */
129
+ export async function prepareCancellation(context) {
130
+ const { output, proposal, config, request } = context;
131
+ const state = await cancellationState(context);
132
+ const record =
133
+ state.recorded?.payload.preview.cancellation ??
134
+ cancellationRecord(
135
+ state.origin,
136
+ proposal,
137
+ config,
138
+ state.invocation,
139
+ state.operatorId
140
+ );
141
+ validateCancellation(record, record);
142
+ writeJson(join(output, "cancellation.json"), record);
143
+ const mode = state.recorded ? "recorded" : "fresh";
144
+ if (state.recorded) {
145
+ const bytes = Buffer.from(state.recorded.payload.bundle);
146
+ required(bytes.length <= 1_048_576, "cancellation bundle exceeds bound");
147
+ writeFileSync(join(output, "cancellation-bundle.json"), bytes, {
148
+ mode: 0o600,
149
+ flag: "wx",
150
+ });
151
+ }
152
+ writeJson(join(output, "cancellation-state.json"), { mode, request });
153
+ return { mode };
154
+ }
155
+
156
+ /** Each chunk and closure rechecks current operator, main, holds, destination and immutable subject. */
157
+ async function authorizeCancellation(context, record, paths, state) {
158
+ const current = await cancellationState(context);
159
+ if (state.mode === "fresh") {
160
+ validateCancellation(
161
+ record,
162
+ cancellationRecord(
163
+ current.origin,
164
+ context.proposal,
165
+ context.config,
166
+ current.invocation,
167
+ current.operatorId
168
+ )
169
+ );
170
+ verifyCancellationAttestation(
171
+ context.api.policy,
172
+ record,
173
+ paths,
174
+ sha256(readBytes(paths.cancellation, 65_536))
175
+ );
176
+ } else
177
+ required(
178
+ state.mode === "recorded" &&
179
+ current.recorded &&
180
+ canonicalJson(current.recorded.payload.preview.cancellation) ===
181
+ canonicalJson(record),
182
+ "recorded cancellation authority differs"
183
+ );
184
+ return current;
185
+ }
186
+
187
+ /** Durable proof precedes a not_planned close; interruptions retain all evidence for the next manual run. */
188
+ export async function finalizeCancellation(context) {
189
+ const { api, output, proposal, request } = context;
190
+ const state = readJson(join(output, "cancellation-state.json"), 65_536);
191
+ keys(state, ["mode", "request"]);
192
+ required(
193
+ canonicalJson(state.request) === canonicalJson(request),
194
+ "cancellation intent changed"
195
+ );
196
+ const paths = {
197
+ cancellation: join(output, "cancellation.json"),
198
+ cancellationBundle: join(output, "cancellation-bundle.json"),
199
+ };
200
+ const record = readJson(paths.cancellation, 65_536);
201
+ const digest = sha256(readBytes(paths.cancellation, 65_536));
202
+ const current = await authorizeCancellation(context, record, paths, state);
203
+ const payload = current.recorded?.payload ?? {
204
+ version: 1,
205
+ proposal,
206
+ allocation: current.origin.allocation,
207
+ preview: { cancellation: record },
208
+ bundle: readBytes(paths.cancellationBundle, 1_048_576).toString("utf8"),
209
+ };
210
+ if (!current.recorded)
211
+ await withStage("cancel-checkpoint", () =>
212
+ persistCheckpoint(api, request.number, payload, digest, () =>
213
+ authorizeCancellation(context, record, paths, state)
214
+ )
215
+ );
216
+ const issue = (await authorizeCancellation(context, record, paths, state))
217
+ .issue;
218
+ required(
219
+ canonicalJson(decodeCheckpoint(issue.comments, digest)) ===
220
+ canonicalJson(payload),
221
+ "durable cancellation checkpoint differs"
222
+ );
223
+ if (issue.state === "open")
224
+ await withStage("cancel-close", () =>
225
+ api.request(
226
+ `repos/${proposal.repository}/issues/${request.number}`,
227
+ "PATCH",
228
+ { state: "closed", state_reason: "not_planned" }
229
+ )
230
+ );
231
+ const closed = await api.issue(request.number);
232
+ required(
233
+ closed.state === "closed" &&
234
+ closed.state_reason === "not_planned" &&
235
+ closed.closed_by?.type === "Bot" &&
236
+ String(closed.closed_by.id) === api.policy.claimActorId &&
237
+ closed.body === issue.body &&
238
+ closed.title === issue.title,
239
+ "cancellation closure readback differs"
240
+ );
241
+ await api.main(proposal.parent);
242
+ await cancellationDestinationAbsent(api, request.proposalKey);
243
+ return {
244
+ status: "cancelled",
245
+ number: request.number,
246
+ cancellationSha256: digest,
247
+ };
248
+ }
@@ -0,0 +1,94 @@
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
+ /** One invocation reuses authenticated code bytes, never mutable provider authorization. */
5
+ import { required, OBJECT } from "./npm-update-contract.mjs";
6
+
7
+ /** Fresh owner selection and regular-file reads precede every cached use. */
8
+ function currentBinding(config, owner, read, members) {
9
+ const selected = owner();
10
+ return {
11
+ root: selected.root,
12
+ source: selected.source,
13
+ version: selected.metadata.version,
14
+ signerDigest: config.automationProvenance?.signerDigest,
15
+ bytes: read(selected.root, members),
16
+ };
17
+ }
18
+
19
+ /** A cached import URL cannot authorize changed files or a replacement owner. */
20
+ function assertBinding(expected, actual) {
21
+ required(
22
+ ["root", "source", "version", "signerDigest"].every(
23
+ field => expected[field] === actual[field]
24
+ ) &&
25
+ expected.bytes.length === actual.bytes.length &&
26
+ expected.bytes.every((bytes, index) => bytes.equals(actual.bytes[index])),
27
+ "authenticated classifier identity or bytes changed"
28
+ );
29
+ }
30
+
31
+ /** A sibling awaiting the same import may have invalidated its URL meanwhile. */
32
+ function assertUsable(poisoned, ...roots) {
33
+ required(
34
+ roots.every(root => !poisoned.has(root)),
35
+ "authenticated classifier import lifetime is uncertain"
36
+ );
37
+ }
38
+
39
+ /** Each factory owns its inaccessible cache; another caller cannot preseed this instance. */
40
+ export function classifierMemo(owner, read, members) {
41
+ const entries = new WeakMap();
42
+ const poisoned = new Set();
43
+ const fixed = [...members];
44
+ return async (config, authenticate) => {
45
+ const current = currentBinding(config, owner, read, fixed);
46
+ assertUsable(poisoned, current.root);
47
+ // Without a source pin, the original qualifier must observe current HEAD each time.
48
+ if (
49
+ current.source &&
50
+ (typeof current.signerDigest !== "string" ||
51
+ !OBJECT.test(current.signerDigest))
52
+ ) {
53
+ const imported = await authenticate();
54
+ const observed = currentBinding(config, owner, read, fixed);
55
+ assertUsable(poisoned, current.root, imported.root, observed.root);
56
+ return imported.verdict;
57
+ }
58
+ let entry = entries.get(config);
59
+ if (entry) assertBinding(entry.binding, current);
60
+ else {
61
+ entry = {
62
+ binding: {
63
+ ...current,
64
+ bytes: current.bytes.map(bytes => Buffer.from(bytes)),
65
+ },
66
+ promise: Promise.resolve().then(authenticate),
67
+ };
68
+ entries.set(config, entry);
69
+ const pending = entry;
70
+ pending.promise.catch(() => {
71
+ if (entries.get(config) === pending) entries.delete(config);
72
+ });
73
+ }
74
+ const imported = await entry.promise;
75
+ let observed;
76
+ try {
77
+ // Authentication/import may yield; detect changes during that interval too.
78
+ observed = currentBinding(config, owner, read, fixed);
79
+ assertBinding(entry.binding, observed);
80
+ required(
81
+ imported.root === entry.binding.root,
82
+ "classifier import owner changed"
83
+ );
84
+ assertUsable(poisoned, entry.binding.root, imported.root, observed.root);
85
+ } catch (error) {
86
+ // ESM caches this URL: restoring files or another config cannot requalify it.
87
+ poisoned.add(entry.binding.root);
88
+ poisoned.add(imported.root);
89
+ if (observed) poisoned.add(observed.root);
90
+ throw error;
91
+ }
92
+ return imported.verdict;
93
+ };
94
+ }
@@ -42,7 +42,11 @@ const PRODUCER = [
42
42
  "allocate",
43
43
  "authorization",
44
44
  "broker-client",
45
+ "cancel-origin",
46
+ "cancellation",
47
+ "cancellation-proof",
45
48
  "checkpoint",
49
+ "classifier-cache",
46
50
  "contract",
47
51
  "controller-broker",
48
52
  "controller-factory",
@@ -81,6 +85,7 @@ const PRODUCER = [
81
85
  "publish",
82
86
  "quality",
83
87
  "recovery",
88
+ "supersession",
84
89
  "runtime-archive",
85
90
  "runtime-graph",
86
91
  "runtime-transport",
@@ -8,6 +8,7 @@ import { join } from "node:path";
8
8
  import { fileURLToPath, pathToFileURL } from "node:url";
9
9
  import { existsSync, lstatSync, realpathSync } from "node:fs";
10
10
  import { required, VERSION, OBJECT } from "./npm-update-contract.mjs";
11
+ import { classifierMemo } from "./npm-update-classifier-cache.mjs";
11
12
  import {
12
13
  readBytes,
13
14
  runProcess,
@@ -33,6 +34,7 @@ import {
33
34
  } from "./npm-update-helper-graph.mjs";
34
35
 
35
36
  const PACKAGE = "@codyswann/lisa";
37
+ const classifier = classifierMemo(helperOwner, helperBytes, MEMBERS);
36
38
 
37
39
  /** Native package resolution supports both upstream and actual emitted managed layouts. */
38
40
  export function helperOwner() {
@@ -277,7 +279,7 @@ export async function qualifiedControllerGraph(cwd, config, entries) {
277
279
  }
278
280
 
279
281
  /** No canonical package code is imported until independent authority qualifies its fixed graph. */
280
- export async function canonicalClassifier(config) {
282
+ async function authenticatedClassifier(config) {
281
283
  const owner = await qualifiedHelperFiles(config, MEMBERS);
282
284
  required(
283
285
  classifierGraphMatches(MEMBERS.map(member => owner.bytes.get(member))),
@@ -288,5 +290,10 @@ export async function canonicalClassifier(config) {
288
290
  typeof module.humanGateVerdict === "function",
289
291
  "canonical hold classifier is unavailable"
290
292
  );
291
- return module.humanGateVerdict;
293
+ return { root: owner.root, verdict: module.humanGateVerdict };
294
+ }
295
+
296
+ /** Current bytes must still match the independently authenticated imported closure. */
297
+ export function canonicalClassifier(config) {
298
+ return classifier(config, () => authenticatedClassifier(config));
292
299
  }
@@ -4,6 +4,10 @@
4
4
  /** Pure shared diagnostics retain one error identity without importing producer orchestration. */
5
5
  const stages = new Set([
6
6
  "configuration",
7
+ "cancel-intent",
8
+ "cancel-origin",
9
+ "cancel-checkpoint",
10
+ "cancel-close",
7
11
  "gate-input",
8
12
  "gate-validate",
9
13
  "gate-scratch",
@@ -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"