@codyswann/lisa 3.11.9 → 3.12.0

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 (60) hide show
  1. package/all/copy-overwrite/scripts/lisa-command-envelope.mjs +10 -0
  2. package/all/copy-overwrite/scripts/lisa-destructive-guard.mjs +267 -0
  3. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  4. package/dist/core/upstream-evidence-manifest.js +7 -2
  5. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  6. package/package.json +1 -1
  7. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  8. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  9. package/plugins/lisa/rules/eager/reset-seed-coverage.md +2 -0
  10. package/plugins/lisa-agy/plugin.json +1 -1
  11. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  14. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-copilot/rules/eager/reset-seed-coverage.md +2 -0
  18. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cursor/rules/reset-seed-coverage.mdc +2 -0
  20. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-expo-agy/plugin.json +1 -1
  23. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  33. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  38. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  43. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-rails-agy/plugin.json +1 -1
  48. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  53. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  58. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/src/base/rules/eager/reset-seed-coverage.md +2 -0
@@ -25,6 +25,7 @@ import * as fs from "node:fs";
25
25
  import * as path from "node:path";
26
26
  import { fileURLToPath, pathToFileURL } from "node:url";
27
27
 
28
+ import { destructiveDenial } from "./lisa-destructive-guard.mjs";
28
29
  import { validateAgainstSchema } from "./lisa-schema-validate.mjs";
29
30
 
30
31
  /** Pinned envelope schema version. */
@@ -126,6 +127,15 @@ export function validateEnvelope(envelope) {
126
127
  if (envelope.mode === "declared-noop" && envelope.status !== "no-op") {
127
128
  errors.push('mode "declared-noop" requires status "no-op"');
128
129
  }
130
+ // The production boundary lives here, at the one interface every reset, seed,
131
+ // and verify adapter must pass through, rather than in each adapter. A
132
+ // destructive run against a production-resolved (or unresolvable) environment
133
+ // has no representable success envelope, so it can never exit 0 — reporting
134
+ // the refusal stays available, which is the only outcome that should be.
135
+ const denial = destructiveDenial(envelope);
136
+ if (denial && SUCCESS_STATUSES.includes(envelope.status)) {
137
+ errors.push(`${denial.code}: ${denial.message}`);
138
+ }
129
139
  return { valid: errors.length === 0, errors };
130
140
  }
131
141
 
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lisa-destructive-guard — the executable arm of `production-fails-closed`.
4
+ *
5
+ * The `reset-seed-coverage` contract already says the right thing: production
6
+ * is refused "with no override, escape hatch, or environment variable that
7
+ * changes the answer", `--dry-run` is mandatory before anything destructive,
8
+ * and a caller-supplied `--stage` is a *request* to be checked against
9
+ * server-resolved identity rather than the source of truth. Until this module
10
+ * existed, all three were prose — an agent reading a rule and choosing to stop.
11
+ * Measured conformance for that rung in this codebase was approximately zero,
12
+ * including by the agent that authored the rule. For a destructive,
13
+ * irreversible operation that is the wrong rung, so the decision moves here.
14
+ *
15
+ * Three properties make this a control rather than a suggestion:
16
+ *
17
+ * 1. **No override exists to be found.** There is no `force` parameter, no
18
+ * `allowProduction` field, and no environment-variable read anywhere in this
19
+ * file — a test greps the shipped source for the latter, so the claim cannot
20
+ * rot. A caller cannot pass the wrong thing because there is no right thing
21
+ * to pass. The tests assert the absence structurally, not behaviourally.
22
+ * 2. **Ambiguity resolves to refusal.** An environment identity that cannot be
23
+ * read is treated exactly like production. "I could not tell" must never be
24
+ * the cheaper answer than "it is production".
25
+ * 3. **A dry run is not a pass.** `dryRun: true` does not soften a production
26
+ * denial. Enumerating production state is still reaching into production,
27
+ * and allowing it would hand every caller a one-flag override.
28
+ *
29
+ * What this module deliberately does NOT claim: it cannot verify that the
30
+ * `environment` an adapter reports is the environment it actually connected to.
31
+ * An adapter that resolves its stage from a caller-supplied string and reports
32
+ * `"dev"` while pointed at production defeats every in-process check by
33
+ * construction. That hole is why the contract says platform enforcement beats
34
+ * process enforcement, and why the durable answer is a capability that is not
35
+ * deployed to production at all — see `docs/decisions/`. This module closes the
36
+ * narrower hole Lisa can close: an honest adapter can no longer report a
37
+ * successful destructive production run, in any repo, by any path.
38
+ * @module scripts/lisa-destructive-guard
39
+ */
40
+
41
+ /**
42
+ * Capabilities that mutate persistent state by definition.
43
+ *
44
+ * Membership is by capability name because a destructive adapter that fails
45
+ * before it deletes anything still reports zeroes, and a guard that keyed only
46
+ * on the counts would wave it through on the attempt that mattered.
47
+ */
48
+ export const DESTRUCTIVE_CAPABILITIES = Object.freeze([
49
+ "reset",
50
+ "reseed",
51
+ "reset-seed",
52
+ "seed",
53
+ "teardown",
54
+ "truncate",
55
+ "purge",
56
+ "drop",
57
+ "restore",
58
+ "migrate-down",
59
+ ]);
60
+
61
+ /**
62
+ * Substring marking an environment segment as production.
63
+ *
64
+ * Deliberately a substring rather than an exact match, so `preprod`,
65
+ * `prod-blue`, and `acme-prod-1` all classify as production. That
66
+ * over-includes: a genuine pre-production environment is refused too. For a
67
+ * control over irreversible operations, over-refusal is the correct direction
68
+ * to be wrong in, and a project that needs the distinction names its
69
+ * environments so the word does not appear.
70
+ */
71
+ const PRODUCTION_SUBSTRING = "prod";
72
+
73
+ /** Whole segments that mark production without containing the substring. */
74
+ const PRODUCTION_SEGMENTS = Object.freeze(["prd", "live"]);
75
+
76
+ /**
77
+ * Values that carry no environment identity.
78
+ *
79
+ * Each of these is something a resolver returns when it failed, and every one
80
+ * of them classifies as `unresolved`, which the guard then treats exactly as it
81
+ * treats production.
82
+ */
83
+ const UNRESOLVED_SENTINELS = new Set([
84
+ "",
85
+ "unknown",
86
+ "unresolved",
87
+ "undefined",
88
+ "null",
89
+ "n/a",
90
+ "na",
91
+ "none",
92
+ "-",
93
+ ]);
94
+
95
+ /** Flags that explicitly opt out of the mandatory dry run. */
96
+ const EXECUTE_FLAGS = Object.freeze(["--no-dry-run", "--execute"]);
97
+
98
+ /**
99
+ * Normalize an environment identity for comparison.
100
+ * @param {unknown} value - Candidate environment identity
101
+ * @returns {string} Lowercased, trimmed text, or "" when there is no text
102
+ */
103
+ function normalize(value) {
104
+ return typeof value === "string" ? value.trim().toLowerCase() : "";
105
+ }
106
+
107
+ /**
108
+ * Classify a server-resolved environment identity.
109
+ *
110
+ * Returns `unresolved` rather than guessing whenever the value is not text, is
111
+ * blank, or is one of the sentinels a failed resolver produces. Callers must
112
+ * treat `unresolved` as production; this function does not make that decision
113
+ * for them so that the two cases stay separately reportable.
114
+ * @param {unknown} value - The environment identity to classify
115
+ * @returns {"production"|"non-production"|"unresolved"} The classification
116
+ */
117
+ export function classifyEnvironment(value) {
118
+ const text = normalize(value);
119
+ if (typeof value !== "string" || UNRESOLVED_SENTINELS.has(text)) {
120
+ return "unresolved";
121
+ }
122
+ const segments = text.split(/[^a-z0-9]+/u).filter(Boolean);
123
+ if (segments.length === 0) {
124
+ return "unresolved";
125
+ }
126
+ const production = segments.some(
127
+ segment =>
128
+ segment.includes(PRODUCTION_SUBSTRING) ||
129
+ PRODUCTION_SEGMENTS.includes(segment)
130
+ );
131
+ return production ? "production" : "non-production";
132
+ }
133
+
134
+ /**
135
+ * Whether a run mutates persistent state.
136
+ *
137
+ * True when the capability is destructive by name, and also when a run of any
138
+ * capability reports rows deleted or created — a read-only adapter that mutated
139
+ * is still a mutation, and the guard should not depend on it being named
140
+ * honestly.
141
+ * @param {object} fields - An envelope or envelope-shaped object
142
+ * @returns {boolean} True when the run is destructive
143
+ */
144
+ export function isDestructive(fields) {
145
+ const capability = normalize(fields?.capability);
146
+ if (DESTRUCTIVE_CAPABILITIES.includes(capability)) {
147
+ return true;
148
+ }
149
+ const summary = fields?.summary ?? {};
150
+ return Number(summary.deleted) > 0 || Number(summary.created) > 0;
151
+ }
152
+
153
+ /**
154
+ * Build the denial for a resolved environment, or null when it is permitted.
155
+ * @param {unknown} environment - Server-resolved environment identity
156
+ * @returns {{code: string, message: string}|null} The denial, if any
157
+ */
158
+ function denyByEnvironment(environment) {
159
+ const classification = classifyEnvironment(environment);
160
+ if (classification === "unresolved") {
161
+ return {
162
+ code: "unresolved-environment",
163
+ message:
164
+ "the server-resolved environment could not be read, which fails closed exactly as production does — resolve the environment from deployment identity before any destructive operation",
165
+ };
166
+ }
167
+ if (classification === "production") {
168
+ return {
169
+ code: "production-forbidden",
170
+ message: `production is refused by the reset-seed contract with no override, and "${normalize(environment)}" classifies as production — a dry run is not an exemption`,
171
+ };
172
+ }
173
+ return null;
174
+ }
175
+
176
+ /**
177
+ * The denial implied by an envelope, or null when the envelope is permitted.
178
+ *
179
+ * This is the check the command envelope itself applies, so a destructive
180
+ * production run cannot be *reported* as a success by any adapter in any repo.
181
+ * @param {object} fields - An envelope or envelope-shaped object
182
+ * @returns {{code: string, message: string}|null} The denial, if any
183
+ */
184
+ export function destructiveDenial(fields) {
185
+ return isDestructive(fields) ? denyByEnvironment(fields?.environment) : null;
186
+ }
187
+
188
+ /**
189
+ * Decide whether a destructive invocation may proceed.
190
+ *
191
+ * Adapter-time counterpart of {@link destructiveDenial}, adding the two checks
192
+ * that need the caller's request alongside the resolved identity. The requested
193
+ * stage is never trusted: it is compared against server-resolved identity, and
194
+ * a production request is refused even when the resolver disagrees, because the
195
+ * disagreement itself means one of the two is wrong.
196
+ * @param {object} request - capability, summary, resolvedEnvironment, requestedStage, dryRun
197
+ * @returns {{allowed: boolean, denial: ({code: string, message: string}|null)}} The decision
198
+ */
199
+ export function assertDestructiveAllowed(request) {
200
+ if (!isDestructive(request)) {
201
+ return { allowed: true, denial: null };
202
+ }
203
+ const resolved = request?.resolvedEnvironment;
204
+ const environmentDenial = denyByEnvironment(resolved);
205
+ if (environmentDenial) {
206
+ return { allowed: false, denial: environmentDenial };
207
+ }
208
+ const requested = request?.requestedStage;
209
+ if (requested === undefined || requested === null) {
210
+ return { allowed: true, denial: null };
211
+ }
212
+ if (classifyEnvironment(requested) === "production") {
213
+ return {
214
+ allowed: false,
215
+ denial: {
216
+ code: "production-requested",
217
+ message: `the caller requested "${normalize(requested)}", which classifies as production; a requested stage never authorizes a destructive operation`,
218
+ },
219
+ };
220
+ }
221
+ if (normalize(requested) !== normalize(resolved)) {
222
+ return {
223
+ allowed: false,
224
+ denial: {
225
+ code: "stage-mismatch",
226
+ message: `the caller requested "${normalize(requested)}" but deployment identity resolved to "${normalize(resolved)}"; a stage is a request to be checked, never the source of truth`,
227
+ },
228
+ };
229
+ }
230
+ return { allowed: true, denial: null };
231
+ }
232
+
233
+ /**
234
+ * Read one `--flag value` / `--flag=value` option out of argv.
235
+ * @param {readonly string[]} argv - Arguments after the script name
236
+ * @param {string} flag - The long flag to read, including leading dashes
237
+ * @returns {string|null} The value, or null when the flag is absent or empty
238
+ */
239
+ function readOption(argv, flag) {
240
+ const inline = argv.find(entry => entry.startsWith(`${flag}=`));
241
+ if (inline) {
242
+ return inline.slice(flag.length + 1) || null;
243
+ }
244
+ const index = argv.indexOf(flag);
245
+ return index === -1 ? null : (argv[index + 1] ?? null);
246
+ }
247
+
248
+ /**
249
+ * Parse the arguments every destructive adapter accepts.
250
+ *
251
+ * `--dry-run` is the default rather than an opt-in: an adapter that forgets to
252
+ * parse anything, or is handed arguments it does not understand, enumerates
253
+ * instead of mutating. Mutating requires an explicit `--no-dry-run` or
254
+ * `--execute`, and passing `--dry-run` alongside either one keeps the dry run,
255
+ * so a stray flag can never silently upgrade a rehearsal into a real run.
256
+ * @param {readonly string[]} argv - Arguments after the script name
257
+ * @returns {{dryRun: boolean, requestedStage: (string|null), idempotencyKey: (string|null)}} Parsed arguments
258
+ */
259
+ export function parseDestructiveArgs(argv) {
260
+ const args = [...(argv ?? [])];
261
+ const optedOut = EXECUTE_FLAGS.some(flag => args.includes(flag));
262
+ return {
263
+ dryRun: args.includes("--dry-run") || !optedOut,
264
+ requestedStage: readOption(args, "--stage"),
265
+ idempotencyKey: readOption(args, "--idempotency-key"),
266
+ };
267
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"upstream-evidence-manifest.d.ts","sourceRoot":"","sources":["../../src/core/upstream-evidence-manifest.ts"],"names":[],"mappings":"AACA,iFAAiF;AACjF,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA4yEpE,CAAC;AAEL,4EAA4E;AAC5E,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAqiOjE,CAAC;AAEL,sFAAsF;AACtF,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAk6I/D,CAAC"}
1
+ {"version":3,"file":"upstream-evidence-manifest.d.ts","sourceRoot":"","sources":["../../src/core/upstream-evidence-manifest.ts"],"names":[],"mappings":"AACA,iFAAiF;AACjF,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA8yEpE,CAAC;AAEL,4EAA4E;AAC5E,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAyiOjE,CAAC;AAEL,sFAAsF;AACtF,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAk6I/D,CAAC"}
@@ -4,7 +4,8 @@ export const UPSTREAM_EVIDENCE_MANIFEST = Object.freeze({
4
4
  "all/copy-contents/.gitattributes": "9d3831007e681186a3673e1037ef3fc82980cab2fc04e27c0868e562912c9e9f",
5
5
  "all/copy-contents/gitignore": "8104ccd32d7e6137cae706511c5037d11d6b6045b8e2a9bb7b3a48a81c053cfa",
6
6
  "all/copy-overwrite/scripts/check-state-classification.mjs": "ac539349c86a7b5b0083ec0e6aaf0eb4c3e40fbd4dc31511c6460a2bad7af9a5",
7
- "all/copy-overwrite/scripts/lisa-command-envelope.mjs": "e06cb11f1418fe94564cea527a22b039f53b7ab10c1cd182264a31e52bba9fa8",
7
+ "all/copy-overwrite/scripts/lisa-command-envelope.mjs": "6b198f088cd3cd862d4357fbb069b1ad5d3afeb7be594f2bf1ff603b092a8354",
8
+ "all/copy-overwrite/scripts/lisa-destructive-guard.mjs": "4fb431a3c58f443e303d613934d023d9e95ddae5c42710bb7009f7c080c82a64",
8
9
  "all/copy-overwrite/scripts/lisa-enforcement-fallback.sh": "3049c2f1051c7c02dd60b62355964651e1565ebe6bdcb5e37634cd686cf1deb1",
9
10
  "all/copy-overwrite/scripts/lisa-floor-collisions.mjs": "2d11968f6852ab745cba939de03c6d6cb5f413975d2cfc8fc447bdd0b218e91a",
10
11
  "all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh": "22184f1dd1b96e818741bd4ae659446299535d7ce594817155edebac4172a7ba",
@@ -389,7 +390,7 @@ export const UPSTREAM_EVIDENCE_MANIFEST = Object.freeze({
389
390
  "plugins/src/base/rules/eager/rejection-detection.md": "e0a8e74f9b4eec378f7cc62072153b1a968af0e5e7a1af9473ab476d41c30211",
390
391
  "plugins/src/base/rules/eager/repo-scope-split.md": "a1702d6d0e9d80abc118b7c32a97ba9a988b195d86363b2d7a35e71752d9bd2e",
391
392
  "plugins/src/base/rules/eager/report-actionability.md": "7c4d5b2bea93b7cd5bb8694fa7c06f4ca8f94a19eb17832f1ccc5354b0207163",
392
- "plugins/src/base/rules/eager/reset-seed-coverage.md": "d028c8709d7d3a05b2f9bb8209a0d6bf1aef99e88d5e7d7132909e8d0d4c19d9",
393
+ "plugins/src/base/rules/eager/reset-seed-coverage.md": "ff47c2813439e03e5cd559d7bf32cc4c7ae83544fc5616db6b9ee0e5675b6ee4",
393
394
  "plugins/src/base/rules/eager/security-audit-handling.md": "1f6effe92be66736a0c9fab634c5fdd6ad1e789865faa67bb783c67ddb4ad490",
394
395
  "plugins/src/base/rules/eager/session-status-updates.md": "bbb42e86b8eb68334b8b9c0bc70beb4895a54015e097bf745fc92156b3e24766",
395
396
  "plugins/src/base/rules/eager/settled-decisions.md": "f5339c81b8d29f628830e6147314cbb941b9d9e6d87c3834d8fe7adc4b46ef4c",
@@ -1328,6 +1329,7 @@ export const UPSTREAM_SURFACE_MANIFEST = Object.freeze({
1328
1329
  "all/copy-contents/gitignore": true,
1329
1330
  "all/copy-overwrite/scripts/check-state-classification.mjs": true,
1330
1331
  "all/copy-overwrite/scripts/lisa-command-envelope.mjs": true,
1332
+ "all/copy-overwrite/scripts/lisa-destructive-guard.mjs": true,
1331
1333
  "all/copy-overwrite/scripts/lisa-enforcement-fallback.sh": true,
1332
1334
  "all/copy-overwrite/scripts/lisa-floor-collisions.mjs": true,
1333
1335
  "all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh": true,
@@ -1407,6 +1409,7 @@ export const UPSTREAM_SURFACE_MANIFEST = Object.freeze({
1407
1409
  "cdk/package-lisa/package.lisa.json": true,
1408
1410
  "commitlint.config.cjs": true,
1409
1411
  "docs/bdd-coverage-schema.md": true,
1412
+ "docs/design/reset-production-absence.md": true,
1410
1413
  "docs/design/uat-acceptance-verification-gate.md": true,
1411
1414
  "docs/kane-cli-integration.md": true,
1412
1415
  "docs/kane-cli-pilot.example.json": true,
@@ -7717,6 +7720,8 @@ export const UPSTREAM_SURFACE_MANIFEST = Object.freeze({
7717
7720
  "tests/unit/scripts/command-envelope.test.ts": true,
7718
7721
  "tests/unit/scripts/cross-pollinate.test.ts": true,
7719
7722
  "tests/unit/scripts/cursor-artifact-helpers.ts": true,
7723
+ "tests/unit/scripts/destructive-production-guard.test.ts": true,
7724
+ "tests/unit/scripts/destructive-production-unreachable.test.ts": true,
7720
7725
  "tests/unit/scripts/detect-stale-workflow-inputs-helpers.ts": true,
7721
7726
  "tests/unit/scripts/detect-stale-workflow-inputs.test.ts": true,
7722
7727
  "tests/unit/scripts/e2e-coverage.test.ts": true,