@codyswann/lisa 4.70.4 → 4.71.1

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 (139) hide show
  1. package/all/copy-overwrite/scripts/lib/automation-provenance-contract.mjs +174 -0
  2. package/all/copy-overwrite/scripts/lib/automation-provenance-local.mjs +279 -0
  3. package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +17 -7
  4. package/all/copy-overwrite/scripts/lib/github-attestation-provider.mjs +241 -0
  5. package/all/copy-overwrite/scripts/lib/github-attestation-recovery.mjs +283 -0
  6. package/all/copy-overwrite/scripts/lib/github-attestation-verifier.mjs +244 -0
  7. package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +289 -0
  8. package/all/copy-overwrite/scripts/lib/npm-update-authorization.mjs +235 -0
  9. package/all/copy-overwrite/scripts/lib/npm-update-broker-client.mjs +92 -0
  10. package/all/copy-overwrite/scripts/lib/npm-update-checkpoint.mjs +241 -0
  11. package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +278 -0
  12. package/all/copy-overwrite/scripts/lib/npm-update-controller-broker.mjs +234 -0
  13. package/all/copy-overwrite/scripts/lib/npm-update-controller-factory.mjs +278 -0
  14. package/all/copy-overwrite/scripts/lib/npm-update-controller-recipe.mjs +135 -0
  15. package/all/copy-overwrite/scripts/lib/npm-update-execution-adapter.mjs +292 -0
  16. package/all/copy-overwrite/scripts/lib/npm-update-gate-hooks.mjs +214 -0
  17. package/all/copy-overwrite/scripts/lib/npm-update-gate-install.mjs +267 -0
  18. package/all/copy-overwrite/scripts/lib/npm-update-gate-proof.mjs +99 -0
  19. package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +300 -0
  20. package/all/copy-overwrite/scripts/lib/npm-update-gh-dispatch.mjs +182 -0
  21. package/all/copy-overwrite/scripts/lib/npm-update-gh-grants.mjs +156 -0
  22. package/all/copy-overwrite/scripts/lib/npm-update-gh-requests.mjs +221 -0
  23. package/all/copy-overwrite/scripts/lib/npm-update-github.mjs +214 -0
  24. package/all/copy-overwrite/scripts/lib/npm-update-helper-graph.mjs +238 -0
  25. package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +126 -0
  26. package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +292 -0
  27. package/all/copy-overwrite/scripts/lib/npm-update-hook-installation.mjs +116 -0
  28. package/all/copy-overwrite/scripts/lib/npm-update-hook-preload.mjs +153 -0
  29. package/all/copy-overwrite/scripts/lib/npm-update-hook-provider.mjs +207 -0
  30. package/all/copy-overwrite/scripts/lib/npm-update-hook-read-client.mjs +164 -0
  31. package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +300 -0
  32. package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +157 -0
  33. package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +28 -0
  34. package/all/copy-overwrite/scripts/lib/npm-update-isolation.mjs +11 -0
  35. package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +171 -0
  36. package/all/copy-overwrite/scripts/lib/npm-update-leaf.mjs +194 -0
  37. package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +155 -0
  38. package/all/copy-overwrite/scripts/lib/npm-update-npm.mjs +190 -0
  39. package/all/copy-overwrite/scripts/lib/npm-update-object.mjs +112 -0
  40. package/all/copy-overwrite/scripts/lib/npm-update-orchestrator.mjs +273 -0
  41. package/all/copy-overwrite/scripts/lib/npm-update-owner.mjs +292 -0
  42. package/all/copy-overwrite/scripts/lib/npm-update-prepare.mjs +263 -0
  43. package/all/copy-overwrite/scripts/lib/npm-update-process-core.mjs +179 -0
  44. package/all/copy-overwrite/scripts/lib/npm-update-process.mjs +16 -0
  45. package/all/copy-overwrite/scripts/lib/npm-update-publication.mjs +258 -0
  46. package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +254 -0
  47. package/all/copy-overwrite/scripts/lib/npm-update-quality.mjs +153 -0
  48. package/all/copy-overwrite/scripts/lib/npm-update-recovery.mjs +264 -0
  49. package/all/copy-overwrite/scripts/lib/npm-update-runtime-archive.mjs +204 -0
  50. package/all/copy-overwrite/scripts/lib/npm-update-runtime-graph.mjs +185 -0
  51. package/all/copy-overwrite/scripts/lib/npm-update-runtime-transport.mjs +221 -0
  52. package/all/copy-overwrite/scripts/lib/npm-update-runtime.mjs +284 -0
  53. package/all/copy-overwrite/scripts/lib/npm-update-tool-launcher.mjs +287 -0
  54. package/all/copy-overwrite/scripts/lib/npm-update-worker-environment.mjs +161 -0
  55. package/all/copy-overwrite/scripts/lib/npm-update-worker-inspection.mjs +142 -0
  56. package/all/copy-overwrite/scripts/lib/npm-update-worker-lifecycle.mjs +256 -0
  57. package/all/copy-overwrite/scripts/lib/npm-update-worker-policy.mjs +266 -0
  58. package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +230 -36
  59. package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +161 -0
  60. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +89 -5
  61. package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +204 -0
  62. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +48 -123
  63. package/all/copy-overwrite/scripts/lisa-work-item.mjs +107 -41
  64. package/all/copy-overwrite/scripts/npm-updater-gate-runtime.json +1 -0
  65. package/all/copy-overwrite/scripts/npm-updater-gate-supervisor.c +121 -0
  66. package/all/copy-overwrite/scripts/npm-updater-gate.Dockerfile +49 -0
  67. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +1085 -0
  68. package/dist/codex/scripts/block-no-verify.sh +89 -5
  69. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  70. package/dist/core/lisa-owned-hash-ledger.js +604 -0
  71. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  72. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  73. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  74. package/dist/core/upstream-evidence-manifest.js +159 -11
  75. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  76. package/dist/opencode/plugin-templates/block-no-verify.sh +89 -5
  77. package/package.json +4 -4
  78. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  80. package/plugins/lisa/hooks/block-no-verify.agy.sh +89 -5
  81. package/plugins/lisa/hooks/block-no-verify.sh +89 -5
  82. package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +89 -5
  83. package/plugins/lisa-agy/plugin.json +1 -1
  84. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  86. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  87. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-copilot/hooks/block-no-verify.sh +89 -5
  91. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-cursor/hooks/block-no-verify.sh +89 -5
  93. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  95. package/plugins/lisa-expo-agy/plugin.json +1 -1
  96. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  100. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  101. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  105. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  106. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  110. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  111. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  113. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  115. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  116. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  120. package/plugins/lisa-rails-agy/plugin.json +1 -1
  121. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  123. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  124. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  125. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  126. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  127. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  128. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  129. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  130. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  131. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  132. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  133. package/plugins/src/base/hooks/block-no-verify.agy.sh +89 -5
  134. package/plugins/src/base/hooks/block-no-verify.sh +89 -5
  135. package/scripts/check-shipped-surface-removals.mjs +96 -24
  136. package/scripts/generate-npm-updater-helper-graph.mjs +184 -0
  137. package/scripts/two-channel-couplings.json +31 -7
  138. package/typescript/copy-contents/.husky/commit-msg +18 -0
  139. package/typescript/copy-overwrite/.prettierignore +2 -0
@@ -0,0 +1,174 @@
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
+ /** Closed canonical schemas and committed npm policy remain independent of local proof IO. */
5
+ import { requireProof } from "./github-attestation-verifier.mjs";
6
+
7
+ const FILES = ["package-lock.json", "package.json"];
8
+ const DESCRIPTOR_KEYS = [
9
+ "author",
10
+ "claimCommentId",
11
+ "claimSha256",
12
+ "committer",
13
+ "files",
14
+ "messageSha256",
15
+ "parent",
16
+ "policySha256",
17
+ "proposalKey",
18
+ "queue",
19
+ "runAttempt",
20
+ "runId",
21
+ "tree",
22
+ "updates",
23
+ "version",
24
+ "workItem",
25
+ ];
26
+ const HEX = /^[0-9a-f]{64}$/;
27
+ const OBJECT_ID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
28
+
29
+ /** Canonical bytes are shared by allocator and consumer, not inferred hashes. */
30
+ export function canonicalJson(value) {
31
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
32
+ if (value !== null && typeof value === "object") {
33
+ return `{${Object.keys(value)
34
+ .sort()
35
+ .map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
36
+ .join(",")}}`;
37
+ }
38
+ requireProof(
39
+ ["string", "boolean", "number"].includes(typeof value) || value === null,
40
+ "unsupported canonical value"
41
+ );
42
+ requireProof(
43
+ typeof value !== "number" || Number.isFinite(value),
44
+ "invalid canonical number"
45
+ );
46
+ return JSON.stringify(value);
47
+ }
48
+
49
+ /** Exact keys prevent an unsigned path/command override hiding in a descriptor. */
50
+ export function exactKeys(value, keys, subject) {
51
+ requireProof(
52
+ value !== null && typeof value === "object" && !Array.isArray(value),
53
+ `${subject} is not an object`
54
+ );
55
+ requireProof(
56
+ Object.keys(value).sort().join("\n") === [...keys].sort().join("\n"),
57
+ `${subject} fields differ`
58
+ );
59
+ }
60
+
61
+ /** Policy is committed at the actual proposal base, never a local/env override. */
62
+ export function trustedConfiguration(git) {
63
+ const config = JSON.parse(git(["show", "HEAD:.lisa.config.json"]));
64
+ const policy = config.automationProvenance;
65
+ requireProof(
66
+ policy?.enabled === true && policy.version === 1,
67
+ "trusted automation policy is absent"
68
+ );
69
+ requireProof(
70
+ typeof policy.repository === "string" &&
71
+ /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(policy.repository),
72
+ "invalid policy repository"
73
+ );
74
+ requireProof(
75
+ [policy.repositoryId, policy.ownerId, policy.claimActorId].every(value =>
76
+ /^[1-9]\d*$/.test(value)
77
+ ),
78
+ "invalid policy identity"
79
+ );
80
+ requireProof(
81
+ typeof policy.signerWorkflow === "string" &&
82
+ policy.signerWorkflow.endsWith("/.github/workflows/npm-updater.yml") &&
83
+ OBJECT_ID.test(policy.signerDigest),
84
+ "invalid trusted signer"
85
+ );
86
+ requireProof(
87
+ typeof policy.callerWorkflow === "string" &&
88
+ policy.callerWorkflow.startsWith(
89
+ `${policy.repository}/.github/workflows/`
90
+ ) &&
91
+ policy.callerWorkflow.endsWith(".yml"),
92
+ "invalid caller workflow"
93
+ );
94
+ requireProof(
95
+ Array.isArray(policy.allowedTriggers) &&
96
+ policy.allowedTriggers.length > 0 &&
97
+ policy.allowedTriggers.every(value =>
98
+ ["push", "schedule", "workflow_dispatch"].includes(value)
99
+ ),
100
+ "invalid trigger policy"
101
+ );
102
+ requireProof(
103
+ Number.isInteger(policy.maxAgeSeconds) &&
104
+ policy.maxAgeSeconds > 0 &&
105
+ policy.maxAgeSeconds <= 86400,
106
+ "invalid authorization age"
107
+ );
108
+ requireProof(HEX.test(policy.ghSha256), "missing official verifier identity");
109
+ return { config, policy };
110
+ }
111
+
112
+ /** Updates must be exactly the declared direct npm version changes. */
113
+ export function npmProposal(descriptor, git) {
114
+ const before = JSON.parse(git(["show", "HEAD:package.json"]));
115
+ const after = JSON.parse(git(["show", ":package.json"]));
116
+ const lock = JSON.parse(git(["show", ":package-lock.json"]));
117
+ requireProof(
118
+ [2, 3].includes(lock.lockfileVersion) && lock.packages?.[""],
119
+ "unsupported npm lock"
120
+ );
121
+ requireProof(
122
+ Array.isArray(descriptor.updates) &&
123
+ descriptor.updates.length > 0 &&
124
+ descriptor.updates.length <= 64,
125
+ "invalid update set"
126
+ );
127
+ const seen = new Set();
128
+ for (const update of descriptor.updates) {
129
+ exactKeys(update, ["name", "section", "from", "to"], "npm update");
130
+ requireProof(
131
+ typeof update.name === "string" &&
132
+ update.name.length <= 214 &&
133
+ /^(?:@[a-z0-9][a-z0-9_.-]*\/)?[a-z0-9][a-z0-9_.-]*$/.test(update.name),
134
+ "invalid npm package name"
135
+ );
136
+ requireProof(
137
+ ["dependencies", "devDependencies", "optionalDependencies"].includes(
138
+ update.section
139
+ ),
140
+ "unsupported npm section"
141
+ );
142
+ requireProof(
143
+ [update.from, update.to].every(
144
+ value =>
145
+ typeof value === "string" &&
146
+ /^[~^]?\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(value)
147
+ ) && update.from !== update.to,
148
+ "unsupported npm version change"
149
+ );
150
+ requireProof(
151
+ !seen.has(`${update.section}/${update.name}`) &&
152
+ before[update.section]?.[update.name] === update.from,
153
+ "duplicate/stale npm update"
154
+ );
155
+ seen.add(`${update.section}/${update.name}`);
156
+ before[update.section][update.name] = update.to;
157
+ requireProof(
158
+ lock.packages[""][update.section]?.[update.name] === update.to &&
159
+ lock.packages[`node_modules/${update.name}`]?.version ===
160
+ update.to.replace(/^[~^]/, ""),
161
+ "manifest/lock version differs"
162
+ );
163
+ }
164
+ requireProof(
165
+ canonicalJson(before) === canonicalJson(after),
166
+ "manifest changes undeclared fields"
167
+ );
168
+ requireProof(
169
+ lock.name === after.name && lock.version === after.version,
170
+ "lock root identity differs"
171
+ );
172
+ }
173
+
174
+ export { FILES, DESCRIPTOR_KEYS, HEX, OBJECT_ID };
@@ -0,0 +1,279 @@
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
+ /** Local proof snapshots and the genuine canonical resolver preserve exact final-read authority. */
5
+ import {
6
+ constants,
7
+ closeSync,
8
+ fstatSync,
9
+ lstatSync,
10
+ openSync,
11
+ readSync,
12
+ } from "node:fs";
13
+ import { dirname, resolve } from "node:path";
14
+ import { TextDecoder } from "node:util";
15
+ import { resolveWorkItemContext, run } from "../lisa-work-item.mjs";
16
+ import { requireProof, sha256 } from "./github-attestation-verifier.mjs";
17
+ import { boundedSpawnSync } from "./bounded-spawn.mjs";
18
+ import {
19
+ canonicalJson,
20
+ exactKeys,
21
+ FILES,
22
+ DESCRIPTOR_KEYS,
23
+ HEX,
24
+ OBJECT_ID,
25
+ } from "./automation-provenance-contract.mjs";
26
+
27
+ /** Refuse aliases and oversize files; snapshots remain private, regular files. */
28
+ export function privateBytes(file, maximum, privateFile = true) {
29
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
30
+ try {
31
+ const stat = fstatSync(fd);
32
+ requireProof(
33
+ stat.isFile() &&
34
+ stat.size <= maximum &&
35
+ (!privateFile || (stat.mode & 0o077) === 0),
36
+ "invalid regular bounded proof file"
37
+ );
38
+ const bytes = Buffer.alloc(maximum + 1);
39
+ let count = 0;
40
+ while (count < bytes.length) {
41
+ const added = readSync(fd, bytes, count, bytes.length - count, null);
42
+ if (added === 0) break;
43
+ count += added;
44
+ }
45
+ requireProof(count <= maximum, "proof file grew beyond limit");
46
+ return bytes.subarray(0, count);
47
+ } finally {
48
+ closeSync(fd);
49
+ }
50
+ }
51
+
52
+ /** Git reads always use the actual checkout and bounded shared runner. */
53
+ export function git(args) {
54
+ return run("git", args, {
55
+ timeout: 30_000,
56
+ maxBuffer: 1_048_576,
57
+ }).stdout.trim();
58
+ }
59
+
60
+ /** Alias and exact staged-blob checks run after descriptor metadata validation. */
61
+ function stagedFiles(descriptor) {
62
+ for (const file of FILES) {
63
+ const entry = git(["ls-files", "--stage", "--", file]);
64
+ requireProof(
65
+ entry.startsWith("100644 ") && entry.split("\n").length === 1,
66
+ "proposal file is not regular stage zero"
67
+ );
68
+ const bytes = run("git", ["show", `:${file}`], {
69
+ timeout: 30_000,
70
+ maxBuffer: 1_048_576,
71
+ }).stdout;
72
+ requireProof(
73
+ descriptor.files[file] === sha256(bytes),
74
+ "proposal blob differs"
75
+ );
76
+ }
77
+ }
78
+
79
+ /** Match complete message and Git metadata before any network/proof acceptance. */
80
+ export function localDescriptor(descriptor, messageBytes, reference, policy) {
81
+ // Parsing never supplies the signed digest. Reject malformed UTF8 only on
82
+ // this present-proof path; preserve any BOM as part of the actual message.
83
+ const message = new TextDecoder("utf-8", {
84
+ fatal: true,
85
+ ignoreBOM: true,
86
+ }).decode(messageBytes);
87
+ exactKeys(descriptor, DESCRIPTOR_KEYS, "descriptor");
88
+ requireProof(
89
+ descriptor.version === 1 &&
90
+ descriptor.runId === reference.runId &&
91
+ descriptor.runAttempt === reference.runAttempt,
92
+ "run reference differs"
93
+ );
94
+ requireProof(
95
+ [
96
+ descriptor.messageSha256,
97
+ descriptor.claimSha256,
98
+ descriptor.proposalKey,
99
+ descriptor.policySha256,
100
+ ].every(value => typeof value === "string" && HEX.test(value)),
101
+ "invalid descriptor digest"
102
+ );
103
+ requireProof(
104
+ typeof descriptor.claimCommentId === "string" &&
105
+ /^[1-9]\d*$/.test(descriptor.claimCommentId),
106
+ "invalid claim reference"
107
+ );
108
+ requireProof(
109
+ typeof descriptor.queue === "string" &&
110
+ /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(descriptor.queue),
111
+ "invalid tracker queue"
112
+ );
113
+ requireProof(
114
+ descriptor.messageSha256 === sha256(messageBytes),
115
+ "final commit message differs"
116
+ );
117
+ requireProof(
118
+ !/^(?:Co-authored-by|AI-Agent|AI-Model|AI-Effort):/im.test(message),
119
+ "automation proposal contains AI attribution"
120
+ );
121
+ requireProof(
122
+ descriptor.policySha256 === sha256(canonicalJson(policy)),
123
+ "trusted policy differs"
124
+ );
125
+ requireProof(
126
+ descriptor.parent === git(["rev-parse", "HEAD"]),
127
+ "proposal parent differs"
128
+ );
129
+ requireProof(
130
+ OBJECT_ID.test(descriptor.tree) && descriptor.tree === git(["write-tree"]),
131
+ "final staged tree differs"
132
+ );
133
+ requireProof(
134
+ descriptor.author === git(["var", "GIT_AUTHOR_IDENT"]) &&
135
+ descriptor.committer === git(["var", "GIT_COMMITTER_IDENT"]),
136
+ "actual commit identity differs"
137
+ );
138
+ const changed = git(["diff", "--cached", "--name-only", "--no-renames"])
139
+ .split("\n")
140
+ .sort();
141
+ requireProof(
142
+ changed.join("\n") === FILES.join("\n"),
143
+ "proposal changes foreign files"
144
+ );
145
+ exactKeys(descriptor.files, FILES, "proposal files");
146
+ stagedFiles(descriptor);
147
+ }
148
+
149
+ /** Snapshot only fixed private proof slots, refusing partial recovery before any authority checks. */
150
+ export function proofSnapshot() {
151
+ const directory = resolve(
152
+ git(["rev-parse", "--git-path", "lisa/automation-provenance"])
153
+ );
154
+ const stat = lstatSync(directory);
155
+ requireProof(
156
+ stat.isDirectory() && !stat.isSymbolicLink() && (stat.mode & 0o077) === 0,
157
+ "invalid private proof directory"
158
+ );
159
+ requireProof(
160
+ !lstatSync(dirname(directory)).isSymbolicLink(),
161
+ "aliased proof parent"
162
+ );
163
+ const paths = {
164
+ recovery: resolve(directory, "recovery.json"),
165
+ recoveryBundle: resolve(directory, "recovery-bundle.json"),
166
+ descriptor: resolve(directory, "descriptor.json"),
167
+ bundle: resolve(directory, "bundle.json"),
168
+ };
169
+ const optional = [paths.recovery, paths.recoveryBundle].map(file => {
170
+ try {
171
+ lstatSync(file);
172
+ return true;
173
+ } catch (error) {
174
+ if (error.code === "ENOENT") return false;
175
+ throw error;
176
+ }
177
+ });
178
+ requireProof(optional[0] === optional[1], "partial recovery proof");
179
+ const bytes = privateBytes(paths.descriptor, 65_536);
180
+ const bundleBytes = privateBytes(paths.bundle, 1_048_576);
181
+ const descriptor = JSON.parse(bytes.toString("utf8"));
182
+ requireProof(
183
+ bytes.toString("utf8") === `${canonicalJson(descriptor)}\n`,
184
+ "noncanonical/duplicate descriptor fields"
185
+ );
186
+
187
+ return { paths, optional, bytes, bundleBytes, descriptor };
188
+ }
189
+
190
+ /** Reuse the shipped resolver and retain current canonical lifecycle requirements. */
191
+ export function canonicalContext(message, config, policy, descriptor) {
192
+ const context = resolveWorkItemContext(message, {
193
+ config,
194
+ requireLive: true,
195
+ execute: (command, args, options) =>
196
+ run(command === "gh" ? policy.ghExecutable : command, args, {
197
+ ...options,
198
+ timeout: 30_000,
199
+ maxBuffer: 1_048_576,
200
+ env: { ...process.env, GH_HOST: "github.com" },
201
+ }),
202
+ });
203
+ requireProof(
204
+ context.provider === "github" &&
205
+ context.ref === descriptor.workItem &&
206
+ context.repository === descriptor.queue,
207
+ "canonical tracker scope differs"
208
+ );
209
+ requireProof(
210
+ context.issue.labels.some(
211
+ label => label.name === context.lifecycle.claimed
212
+ ),
213
+ "canonical leaf is not claimed"
214
+ );
215
+ requireProof(
216
+ !context.issue.labels.some(
217
+ label =>
218
+ context.lifecycle.roles.includes(label.name) &&
219
+ label.name !== context.lifecycle.claimed
220
+ ),
221
+ "canonical leaf has a competing lifecycle role"
222
+ );
223
+
224
+ return context;
225
+ }
226
+
227
+ /** Exact raw object prediction retains origin message, identities and epoch. */
228
+ export function predictedCommit(descriptor, messageBytes) {
229
+ requireProof(
230
+ Buffer.isBuffer(messageBytes) &&
231
+ sha256(messageBytes) === descriptor.messageSha256,
232
+ "origin message bytes differ"
233
+ );
234
+ const headers = `tree ${descriptor.tree}\nparent ${descriptor.parent}\nauthor ${descriptor.author}\ncommitter ${descriptor.committer}\n\n`;
235
+ const raw = Buffer.concat([Buffer.from(headers), messageBytes]);
236
+ const options = {
237
+ encoding: "utf8",
238
+ timeout: 30_000,
239
+ maxBuffer: 128,
240
+ };
241
+ const format = boundedSpawnSync(
242
+ "git",
243
+ ["rev-parse", "--show-object-format"],
244
+ options
245
+ );
246
+ requireProof(
247
+ !format.error &&
248
+ !format.signal &&
249
+ format.status === 0 &&
250
+ ["sha1\n", "sha256\n"].includes(format.stdout),
251
+ "current Git object format is unavailable"
252
+ );
253
+ const width = format.stdout === "sha1\n" ? 40 : 64;
254
+ requireProof(
255
+ [descriptor.parent, descriptor.tree].every(
256
+ value =>
257
+ typeof value === "string" &&
258
+ /^[a-f0-9]+$/.test(value) &&
259
+ value.length === width
260
+ ),
261
+ "origin object format differs"
262
+ );
263
+ // Git computes its native identifier; without -w this writes no object.
264
+ const result = boundedSpawnSync(
265
+ "git",
266
+ ["hash-object", "-t", "commit", "--stdin"],
267
+ { ...options, input: raw }
268
+ );
269
+ requireProof(
270
+ !result.error &&
271
+ !result.signal &&
272
+ result.status === 0 &&
273
+ typeof result.stdout === "string" &&
274
+ /^[a-f0-9]+\n$/.test(result.stdout) &&
275
+ result.stdout.length === width + 1,
276
+ "literal Git commit identity is unavailable"
277
+ );
278
+ return result.stdout.slice(0, -1);
279
+ }
@@ -854,13 +854,20 @@ function wasKilled(code) {
854
854
  * @param {number|null} code The exit code, or null when none was obtained.
855
855
  * @param {LoadReading|null} [load] The machine's load at diagnosis time, from
856
856
  * {@link machineLoad}. Omitted or null when it could not be read.
857
+ * @param {string|null|undefined} nativeSignal Signal observed by the OS supervisor.
857
858
  * @returns {Diagnosis} The verdict.
858
859
  */
859
- function killedVerdict(code, load) {
860
+ function killedVerdict(code, load, nativeSignal) {
860
861
  const named = SIGNAL_EXITS[code];
862
+ const observed =
863
+ code === null && Object.values(SIGNAL_EXITS).includes(nativeSignal)
864
+ ? nativeSignal
865
+ : undefined;
861
866
  const cause =
862
867
  named === undefined
863
- ? "a signal, and the runner obtained no exit code for it"
868
+ ? observed === undefined
869
+ ? "a signal, and the runner obtained no exit code for it"
870
+ : `${observed} — native signal; no exit code was returned`
864
871
  : `${named} — exit ${code} is 128 + ${code - 128}`;
865
872
  return {
866
873
  kind: DIAGNOSIS.KILLED,
@@ -1176,8 +1183,8 @@ function emptyTranscriptVerdict() {
1176
1183
  * @param {LoadReading|null} load The machine's load at diagnosis time.
1177
1184
  * @returns {object} What the failure was, before it is attributed.
1178
1185
  */
1179
- function classify(output, code, load, read, tempRoot) {
1180
- if (wasKilled(code)) return killedVerdict(code ?? null, load);
1186
+ function classify(output, code, load, read, tempRoot, nativeSignal) {
1187
+ if (wasKilled(code)) return killedVerdict(code ?? null, load, nativeSignal);
1181
1188
 
1182
1189
  if (typeof output !== "string") return unavailableVerdict();
1183
1190
 
@@ -1636,7 +1643,7 @@ export function declaredFailure(output, declarations) {
1636
1643
  * when the runner could not capture it.
1637
1644
  * @param {number|null|undefined} [code] The command's exit code. Omitted by a
1638
1645
  * caller that has none; a caller that HAS one must pass it, because a kill is
1639
- * only legible in the exit code and never in the output.
1646
+ * only legible in native status/signal metadata and never in the output.
1640
1647
  * @param {LoadReading|null} [load] The machine's load, for a kill's evidence
1641
1648
  * line. Defaults to reading this machine; pass `null` to suppress the line,
1642
1649
  * or a fixed reading to make the output deterministic.
@@ -1663,6 +1670,8 @@ export function declaredFailure(output, declarations) {
1663
1670
  * triggers a default. `null` is the documented suppression that keeps test
1664
1671
  * output deterministic, so it must survive. Hence the resolution moved to
1665
1672
  * the single consuming call site, where the two are still distinguishable.
1673
+ * @param {string|null|undefined} [nativeSignal] Actual OS-observed termination
1674
+ * signal. It explains a null exit status without manufacturing a numeric one.
1666
1675
  * @returns {Diagnosis} What the failure was, and whose it was.
1667
1676
  */
1668
1677
  export function diagnoseFailure(
@@ -1670,9 +1679,10 @@ export function diagnoseFailure(
1670
1679
  code,
1671
1680
  load = machineLoad(),
1672
1681
  read = readSourceFile,
1673
- tempRoot
1682
+ tempRoot,
1683
+ nativeSignal
1674
1684
  ) {
1675
- const verdict = classify(output, code, load, read, tempRoot);
1685
+ const verdict = classify(output, code, load, read, tempRoot, nativeSignal);
1676
1686
  // A verdict that already knows whose property it is keeps that answer. The
1677
1687
  // static table maps a KIND to a gate, which cannot express a failure whose
1678
1688
  // owner is whichever gate declared the shape that matched.