@codyswann/lisa 2.270.0 → 2.272.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.
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +8 -3
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/health/agentic.d.ts +73 -0
- package/dist/health/agentic.d.ts.map +1 -0
- package/dist/health/agentic.js +328 -0
- package/dist/health/agentic.js.map +1 -0
- package/dist/health/contract.js +2 -3
- package/dist/health/contract.js.map +1 -1
- package/dist/health/deadline.d.ts.map +1 -1
- package/dist/health/deadline.js +0 -1
- package/dist/health/deadline.js.map +1 -1
- package/dist/health/index.d.ts +1 -0
- package/dist/health/index.d.ts.map +1 -1
- package/dist/health/index.js +1 -0
- package/dist/health/index.js.map +1 -1
- package/package.json +3 -2
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-agent-ready/SKILL.md +58 -2
- package/plugins/lisa/skills/lisa-agent-ready/SKILL.md +58 -2
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-agent-ready/SKILL.md +58 -2
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/skills/lisa-agent-ready/SKILL.md +58 -2
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-agent-ready/SKILL.md +58 -2
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/skills/lisa-agent-ready/SKILL.md +58 -2
- package/scripts/verify-health-agentic-built.mjs +842 -0
- package/scripts/verify-health-contract-built.mjs +24 -4
- package/ui/README.md +15 -11
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Empirical proof for the shipped optional agentic Health composition API. */
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import {
|
|
7
|
+
lstat,
|
|
8
|
+
mkdir,
|
|
9
|
+
mkdtemp,
|
|
10
|
+
readFile,
|
|
11
|
+
readdir,
|
|
12
|
+
readlink,
|
|
13
|
+
rm,
|
|
14
|
+
symlink,
|
|
15
|
+
writeFile,
|
|
16
|
+
} from "node:fs/promises";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
let assertions = 0;
|
|
21
|
+
const check = (condition, message) => {
|
|
22
|
+
assertions += 1;
|
|
23
|
+
assert.ok(condition, message);
|
|
24
|
+
};
|
|
25
|
+
const equal = (actual, expected, message) => {
|
|
26
|
+
assertions += 1;
|
|
27
|
+
assert.deepEqual(actual, expected, message);
|
|
28
|
+
};
|
|
29
|
+
const findingState = finding => ({
|
|
30
|
+
check: finding.check,
|
|
31
|
+
layer: finding.layer,
|
|
32
|
+
status: finding.status,
|
|
33
|
+
reason: finding.reason,
|
|
34
|
+
});
|
|
35
|
+
const deterministicState = result => ({
|
|
36
|
+
schemaVersion: result.schemaVersion,
|
|
37
|
+
projectRoot: result.projectRoot,
|
|
38
|
+
mode: result.mode,
|
|
39
|
+
startedAt: result.startedAt,
|
|
40
|
+
completedAt: result.completedAt,
|
|
41
|
+
summary: result.summary,
|
|
42
|
+
findings: result.findings,
|
|
43
|
+
});
|
|
44
|
+
const printStateDump = (name, payload) => {
|
|
45
|
+
const serialized = JSON.stringify({ name, ...payload });
|
|
46
|
+
check(
|
|
47
|
+
Buffer.byteLength(serialized, "utf8") <= 16 * 1024,
|
|
48
|
+
`${name} state dump stays within 16 KiB`
|
|
49
|
+
);
|
|
50
|
+
console.log(serialized);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const root = process.cwd();
|
|
54
|
+
const workspace = await mkdtemp(path.join(tmpdir(), "lisa-health-agentic-"));
|
|
55
|
+
const project = path.join(workspace, "host");
|
|
56
|
+
const isolatedBin = path.join(workspace, "bin");
|
|
57
|
+
const gitExecutable = execFileSync("which", ["git"], {
|
|
58
|
+
encoding: "utf8",
|
|
59
|
+
}).trim();
|
|
60
|
+
const healthFile = path.join(project, ".lisa", "health", "latest.json");
|
|
61
|
+
const startedAt = Date.now();
|
|
62
|
+
|
|
63
|
+
const run = (executable, argv, options = {}) =>
|
|
64
|
+
execFileSync(executable, argv, {
|
|
65
|
+
cwd: project,
|
|
66
|
+
encoding: "utf8",
|
|
67
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
68
|
+
...options,
|
|
69
|
+
});
|
|
70
|
+
const git = (...argv) => run(gitExecutable, argv);
|
|
71
|
+
|
|
72
|
+
/** Capture every non-git working-tree inode, mode, target, and file digest. */
|
|
73
|
+
async function snapshotTree(directory) {
|
|
74
|
+
const records = [];
|
|
75
|
+
const visit = async (absolute, relative) => {
|
|
76
|
+
const entries = await readdir(absolute, { withFileTypes: true });
|
|
77
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (relative.length === 0 && entry.name === ".git") continue;
|
|
80
|
+
const childRelative =
|
|
81
|
+
relative.length === 0 ? entry.name : `${relative}/${entry.name}`;
|
|
82
|
+
const childAbsolute = path.join(absolute, entry.name);
|
|
83
|
+
const stat = await lstat(childAbsolute);
|
|
84
|
+
const mode = stat.mode & 0o7777;
|
|
85
|
+
if (stat.isSymbolicLink()) {
|
|
86
|
+
records.push([
|
|
87
|
+
childRelative,
|
|
88
|
+
"symlink",
|
|
89
|
+
mode,
|
|
90
|
+
await readlink(childAbsolute),
|
|
91
|
+
]);
|
|
92
|
+
} else if (stat.isDirectory()) {
|
|
93
|
+
records.push([childRelative, "directory", mode]);
|
|
94
|
+
await visit(childAbsolute, childRelative);
|
|
95
|
+
} else if (stat.isFile()) {
|
|
96
|
+
records.push([
|
|
97
|
+
childRelative,
|
|
98
|
+
"file",
|
|
99
|
+
mode,
|
|
100
|
+
createHash("sha256")
|
|
101
|
+
.update(await readFile(childAbsolute))
|
|
102
|
+
.digest("hex"),
|
|
103
|
+
]);
|
|
104
|
+
} else {
|
|
105
|
+
records.push([childRelative, "special", mode]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
await visit(directory, "");
|
|
110
|
+
return records;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Read the exact material ruleset documents shipped by the fixture stack. */
|
|
114
|
+
async function rulesetDocuments() {
|
|
115
|
+
const groups = await Promise.all(
|
|
116
|
+
["all", "typescript"].map(async type => {
|
|
117
|
+
const directory = path.join(root, type, "github-rulesets");
|
|
118
|
+
return Promise.all(
|
|
119
|
+
(await readdir(directory, { withFileTypes: true }))
|
|
120
|
+
.filter(entry => entry.isFile())
|
|
121
|
+
.map(async entry => {
|
|
122
|
+
const parsed = JSON.parse(
|
|
123
|
+
await readFile(path.join(directory, entry.name), "utf8")
|
|
124
|
+
);
|
|
125
|
+
return {
|
|
126
|
+
name: parsed.name,
|
|
127
|
+
target: parsed.target,
|
|
128
|
+
enforcement: parsed.enforcement,
|
|
129
|
+
conditions: parsed.conditions,
|
|
130
|
+
rules: parsed.rules,
|
|
131
|
+
};
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
return Object.values(
|
|
137
|
+
Object.fromEntries(groups.flat().map(document => [document.name, document]))
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Run one collection and prove it cannot mutate project, storage, git, or exit state. */
|
|
142
|
+
async function assertPureCollection(collect) {
|
|
143
|
+
const beforeTree = await snapshotTree(project);
|
|
144
|
+
const beforeStatus = git("status", "--porcelain=v1", "--untracked-files=all");
|
|
145
|
+
const beforeExitCode = process.exitCode;
|
|
146
|
+
const beforeHealthFile = await lstat(healthFile)
|
|
147
|
+
.then(() => true)
|
|
148
|
+
.catch(() => false);
|
|
149
|
+
const result = await collect();
|
|
150
|
+
equal(
|
|
151
|
+
await snapshotTree(project),
|
|
152
|
+
beforeTree,
|
|
153
|
+
"agentic composition preserves every working-tree inode and byte"
|
|
154
|
+
);
|
|
155
|
+
equal(
|
|
156
|
+
git("status", "--porcelain=v1", "--untracked-files=all"),
|
|
157
|
+
beforeStatus,
|
|
158
|
+
"agentic composition preserves git status"
|
|
159
|
+
);
|
|
160
|
+
equal(
|
|
161
|
+
process.exitCode,
|
|
162
|
+
beforeExitCode,
|
|
163
|
+
"agentic composition preserves process exit state"
|
|
164
|
+
);
|
|
165
|
+
equal(
|
|
166
|
+
await lstat(healthFile)
|
|
167
|
+
.then(() => true)
|
|
168
|
+
.catch(() => false),
|
|
169
|
+
beforeHealthFile,
|
|
170
|
+
"agentic composition never persists latest.json"
|
|
171
|
+
);
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Return the artifact-first bounded evaluator used by the empirical journey. */
|
|
176
|
+
function artifactEvaluator(request, signal) {
|
|
177
|
+
check(!signal.aborted, "evaluator receives a live abort signal");
|
|
178
|
+
check(Object.isFrozen(request), "evaluator request is frozen");
|
|
179
|
+
check(
|
|
180
|
+
Object.isFrozen(request.deterministicFindings) &&
|
|
181
|
+
request.deterministicFindings.every(Object.isFrozen),
|
|
182
|
+
"deterministic evidence is deeply frozen"
|
|
183
|
+
);
|
|
184
|
+
check(
|
|
185
|
+
Object.isFrozen(request.config) &&
|
|
186
|
+
Object.isFrozen(request.config.quality) &&
|
|
187
|
+
Object.isFrozen(request.config.quality.mutation) &&
|
|
188
|
+
Object.isFrozen(request.config.quality.mutation.gate),
|
|
189
|
+
"config projection is deeply frozen"
|
|
190
|
+
);
|
|
191
|
+
check(
|
|
192
|
+
Object.isFrozen(request.artifacts) &&
|
|
193
|
+
request.artifacts.every(Object.isFrozen),
|
|
194
|
+
"artifact evidence is deeply frozen"
|
|
195
|
+
);
|
|
196
|
+
equal(
|
|
197
|
+
Object.hasOwn(request, "projectRoot"),
|
|
198
|
+
false,
|
|
199
|
+
"bounded evaluator request exposes no filesystem capability"
|
|
200
|
+
);
|
|
201
|
+
equal(
|
|
202
|
+
Reflect.set(request.config.quality.mutation.gate, "enabled", true),
|
|
203
|
+
false,
|
|
204
|
+
"frozen config resists evaluator mutation"
|
|
205
|
+
);
|
|
206
|
+
if (request.artifacts.length > 0) {
|
|
207
|
+
equal(
|
|
208
|
+
Reflect.set(request.artifacts[0], "content", "tampered"),
|
|
209
|
+
false,
|
|
210
|
+
"frozen artifacts resist evaluator mutation"
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
check(
|
|
214
|
+
request.artifacts.every(
|
|
215
|
+
artifact =>
|
|
216
|
+
!path.isAbsolute(artifact.path) &&
|
|
217
|
+
!artifact.path.split("/").includes("..") &&
|
|
218
|
+
Buffer.byteLength(artifact.content, "utf8") <= 128 * 1024
|
|
219
|
+
),
|
|
220
|
+
"every evaluator artifact is relative and individually bounded"
|
|
221
|
+
);
|
|
222
|
+
check(
|
|
223
|
+
request.artifacts.reduce(
|
|
224
|
+
(total, artifact) => total + Buffer.byteLength(artifact.content, "utf8"),
|
|
225
|
+
0
|
|
226
|
+
) <=
|
|
227
|
+
512 * 1024,
|
|
228
|
+
"aggregate evaluator evidence is bounded"
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const judgments = [];
|
|
232
|
+
const override = request.artifacts.find(
|
|
233
|
+
artifact => artifact.kind === "eslint-override"
|
|
234
|
+
);
|
|
235
|
+
const managedArtifact = request.artifacts.find(
|
|
236
|
+
artifact => artifact.kind === "managed-drift"
|
|
237
|
+
);
|
|
238
|
+
const workflows = request.artifacts.filter(
|
|
239
|
+
artifact => artifact.kind === "workflow"
|
|
240
|
+
);
|
|
241
|
+
const mutationDisabled =
|
|
242
|
+
request.config.quality.mutation.gate.enabled === false;
|
|
243
|
+
const managedDrift = request.deterministicFindings.find(
|
|
244
|
+
finding =>
|
|
245
|
+
finding.check === "templates.managed" && finding.status === "fail"
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (mutationDisabled) {
|
|
249
|
+
judgments.push({
|
|
250
|
+
check: "agentic.disabled-mutation-gate",
|
|
251
|
+
reason:
|
|
252
|
+
"The mutation-testing gate is disabled without a justification in the bounded configuration evidence.",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (
|
|
256
|
+
override?.content.includes("no-explicit-any") === true &&
|
|
257
|
+
!/\b(?:reason|justification)\b/iu.test(override.content)
|
|
258
|
+
) {
|
|
259
|
+
judgments.push({
|
|
260
|
+
check: "agentic.eslint.override",
|
|
261
|
+
reason:
|
|
262
|
+
"eslint.config.local.ts disables no-explicit-any without a recorded justification.",
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
for (const workflow of workflows) {
|
|
266
|
+
if (
|
|
267
|
+
/skip_jobs:\s*['"]?lint['"]?/u.test(workflow.content) &&
|
|
268
|
+
!/\b(?:reason|justification)\b/iu.test(workflow.content)
|
|
269
|
+
) {
|
|
270
|
+
judgments.push({
|
|
271
|
+
check: "agentic.ci.skip.lint",
|
|
272
|
+
reason: `${workflow.path} skips lint without a recorded justification.`,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (
|
|
277
|
+
managedDrift?.reason.includes("eslint.config.ts") === true &&
|
|
278
|
+
/\b(?:reason|justification)\b/iu.test(override?.content ?? "") &&
|
|
279
|
+
managedArtifact?.content.includes("no-explicit-any") === true &&
|
|
280
|
+
/["']off["']/u.test(managedArtifact.content)
|
|
281
|
+
) {
|
|
282
|
+
judgments.push({
|
|
283
|
+
check: "agentic.intentional-drift",
|
|
284
|
+
reason:
|
|
285
|
+
"The bounded managed ESLint drift disables no-explicit-any despite the recorded temporary justification.",
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return Promise.resolve({ status: "completed", judgments });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
await mkdir(project, { recursive: true });
|
|
293
|
+
await mkdir(isolatedBin, { recursive: true });
|
|
294
|
+
await symlink(gitExecutable, path.join(isolatedBin, "git"));
|
|
295
|
+
await writeFile(
|
|
296
|
+
path.join(project, "package.json"),
|
|
297
|
+
`${JSON.stringify(
|
|
298
|
+
{
|
|
299
|
+
name: "agentic-health-fixture",
|
|
300
|
+
private: true,
|
|
301
|
+
devDependencies: { harperdb: "4.6.3", typescript: "5.9.3" },
|
|
302
|
+
},
|
|
303
|
+
null,
|
|
304
|
+
2
|
|
305
|
+
)}\n`
|
|
306
|
+
);
|
|
307
|
+
await writeFile(
|
|
308
|
+
path.join(project, ".lisa.config.json"),
|
|
309
|
+
`${JSON.stringify(
|
|
310
|
+
{
|
|
311
|
+
tracker: "github",
|
|
312
|
+
github: { org: "example", repo: "health-fixture" },
|
|
313
|
+
harness: "claude",
|
|
314
|
+
},
|
|
315
|
+
null,
|
|
316
|
+
2
|
|
317
|
+
)}\n`
|
|
318
|
+
);
|
|
319
|
+
await mkdir(path.join(project, "harper-app"), { recursive: true });
|
|
320
|
+
await writeFile(
|
|
321
|
+
path.join(project, "harper-app", "config.yaml"),
|
|
322
|
+
"graphqlSchema: ./schema.graphql\njsResource: true\nstatic: ./web\n"
|
|
323
|
+
);
|
|
324
|
+
await writeFile(
|
|
325
|
+
path.join(project, "harper-app", "schema.graphql"),
|
|
326
|
+
"type Query { health: Boolean! }\n"
|
|
327
|
+
);
|
|
328
|
+
git("init", "--initial-branch=main");
|
|
329
|
+
git("config", "user.name", "Lisa Health Verifier");
|
|
330
|
+
git("config", "user.email", "health-verifier@example.invalid");
|
|
331
|
+
git("add", ".");
|
|
332
|
+
git("commit", "-m", "fixture bootstrap");
|
|
333
|
+
|
|
334
|
+
run(
|
|
335
|
+
process.execPath,
|
|
336
|
+
[
|
|
337
|
+
path.join(root, "dist/index.js"),
|
|
338
|
+
"--no-update-check",
|
|
339
|
+
"apply",
|
|
340
|
+
"--yes",
|
|
341
|
+
"--harness=claude",
|
|
342
|
+
project,
|
|
343
|
+
],
|
|
344
|
+
{
|
|
345
|
+
cwd: root,
|
|
346
|
+
env: {
|
|
347
|
+
...process.env,
|
|
348
|
+
LISA_BOOTSTRAP: "1",
|
|
349
|
+
LISA_SKIP_UPDATE_CHECK: "1",
|
|
350
|
+
PATH: isolatedBin,
|
|
351
|
+
},
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
run(
|
|
355
|
+
process.execPath,
|
|
356
|
+
[path.join(root, "dist/index.js"), "sync", project, "--json"],
|
|
357
|
+
{
|
|
358
|
+
cwd: root,
|
|
359
|
+
env: {
|
|
360
|
+
...process.env,
|
|
361
|
+
LISA_SKIP_UPDATE_CHECK: "1",
|
|
362
|
+
PATH: isolatedBin,
|
|
363
|
+
},
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
const version = JSON.parse(
|
|
367
|
+
await readFile(path.join(root, "package.json"), "utf8")
|
|
368
|
+
).version;
|
|
369
|
+
await writeFile(
|
|
370
|
+
path.join(project, ".claude", ".lisa-plugins-synced"),
|
|
371
|
+
`${version}\n`
|
|
372
|
+
);
|
|
373
|
+
git("add", ".");
|
|
374
|
+
git("commit", "-m", "apply Lisa fixture");
|
|
375
|
+
|
|
376
|
+
const health = await import("@codyswann/lisa/health");
|
|
377
|
+
check(
|
|
378
|
+
typeof health.runHealth === "function",
|
|
379
|
+
"built package exports runHealth"
|
|
380
|
+
);
|
|
381
|
+
const expectedRulesets = await rulesetDocuments();
|
|
382
|
+
const settings = JSON.parse(
|
|
383
|
+
await readFile(path.join(project, ".claude", "settings.json"), "utf8")
|
|
384
|
+
);
|
|
385
|
+
const installedPlugins = Object.entries(settings.enabledPlugins)
|
|
386
|
+
.filter(([, enabled]) => enabled === true)
|
|
387
|
+
.map(([plugin]) => plugin);
|
|
388
|
+
const offline = {
|
|
389
|
+
readRulesets: async () => expectedRulesets,
|
|
390
|
+
readHooksPath: async () => ".husky",
|
|
391
|
+
readInstalledPlugins: async () => installedPlugins,
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
let disabledCalls = 0;
|
|
395
|
+
const deterministic = await assertPureCollection(() =>
|
|
396
|
+
health.runHealth(project, {
|
|
397
|
+
...offline,
|
|
398
|
+
agentic: {
|
|
399
|
+
enabled: false,
|
|
400
|
+
evaluator: async () => {
|
|
401
|
+
disabledCalls += 1;
|
|
402
|
+
throw new Error("disabled evaluator must not run");
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
})
|
|
406
|
+
);
|
|
407
|
+
const full = await assertPureCollection(() =>
|
|
408
|
+
health.runHealth(project, {
|
|
409
|
+
...offline,
|
|
410
|
+
agentic: { enabled: true, evaluator: artifactEvaluator },
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
const completedEmpty = await assertPureCollection(() =>
|
|
414
|
+
health.runHealth(project, {
|
|
415
|
+
...offline,
|
|
416
|
+
agentic: {
|
|
417
|
+
enabled: true,
|
|
418
|
+
evaluator: async () => ({ status: "completed", judgments: [] }),
|
|
419
|
+
},
|
|
420
|
+
})
|
|
421
|
+
);
|
|
422
|
+
equal(disabledCalls, 0, "disabled agentic pass never calls evaluator");
|
|
423
|
+
equal(
|
|
424
|
+
deterministic.mode,
|
|
425
|
+
"deterministic",
|
|
426
|
+
"disabled pass stays deterministic"
|
|
427
|
+
);
|
|
428
|
+
equal(full.mode, "full", "enabled completed pass reports full mode");
|
|
429
|
+
equal(
|
|
430
|
+
completedEmpty.mode,
|
|
431
|
+
"full",
|
|
432
|
+
"completed evaluator with no judgments still reports full mode"
|
|
433
|
+
);
|
|
434
|
+
equal(
|
|
435
|
+
completedEmpty.findings.filter(finding => finding.layer === "agentic"),
|
|
436
|
+
[],
|
|
437
|
+
"completed evaluator with no judgments fabricates no agentic finding"
|
|
438
|
+
);
|
|
439
|
+
equal(
|
|
440
|
+
full.findings.filter(finding => finding.layer === "deterministic"),
|
|
441
|
+
deterministic.findings,
|
|
442
|
+
"composition preserves deterministic facts exactly"
|
|
443
|
+
);
|
|
444
|
+
check(
|
|
445
|
+
full.findings.some(
|
|
446
|
+
finding =>
|
|
447
|
+
finding.check === "agentic.disabled-mutation-gate" &&
|
|
448
|
+
finding.status === "warn"
|
|
449
|
+
),
|
|
450
|
+
"config-backed disabled mutation gate reaches the evaluator"
|
|
451
|
+
);
|
|
452
|
+
check(
|
|
453
|
+
Object.isFrozen(full) &&
|
|
454
|
+
Object.isFrozen(full.findings) &&
|
|
455
|
+
full.findings.every(Object.isFrozen) &&
|
|
456
|
+
Object.isFrozen(full.summary) &&
|
|
457
|
+
Object.isFrozen(full.summary.counts),
|
|
458
|
+
"composed result is deeply frozen"
|
|
459
|
+
);
|
|
460
|
+
printStateDump("agentic-api-diff", {
|
|
461
|
+
disabled: {
|
|
462
|
+
mode: deterministic.mode,
|
|
463
|
+
agenticFindings: deterministic.findings
|
|
464
|
+
.filter(finding => finding.layer === "agentic")
|
|
465
|
+
.map(findingState),
|
|
466
|
+
},
|
|
467
|
+
enabled: {
|
|
468
|
+
mode: full.mode,
|
|
469
|
+
agenticFindings: full.findings
|
|
470
|
+
.filter(finding => finding.layer === "agentic")
|
|
471
|
+
.map(findingState),
|
|
472
|
+
},
|
|
473
|
+
completedWithNoJudgments: {
|
|
474
|
+
mode: completedEmpty.mode,
|
|
475
|
+
agenticFindings: completedEmpty.findings
|
|
476
|
+
.filter(finding => finding.layer === "agentic")
|
|
477
|
+
.map(findingState),
|
|
478
|
+
},
|
|
479
|
+
deterministicFindingsPreserved:
|
|
480
|
+
JSON.stringify(
|
|
481
|
+
full.findings.filter(finding => finding.layer === "deterministic")
|
|
482
|
+
) === JSON.stringify(deterministic.findings),
|
|
483
|
+
});
|
|
484
|
+
console.log("[EVIDENCE: state-dump: agentic-api-diff]");
|
|
485
|
+
|
|
486
|
+
const localEslintPath = path.join(project, "eslint.config.local.ts");
|
|
487
|
+
const localEslintOriginal = await readFile(localEslintPath);
|
|
488
|
+
await writeFile(
|
|
489
|
+
localEslintPath,
|
|
490
|
+
'export default [{ rules: { "@typescript-eslint/no-explicit-any": "off" } }];\n'
|
|
491
|
+
);
|
|
492
|
+
const unjustified = await assertPureCollection(() =>
|
|
493
|
+
health.runHealth(project, {
|
|
494
|
+
...offline,
|
|
495
|
+
agentic: { enabled: true, evaluator: artifactEvaluator },
|
|
496
|
+
})
|
|
497
|
+
);
|
|
498
|
+
const overrideWarning = unjustified.findings.find(
|
|
499
|
+
finding => finding.check === "agentic.eslint.override"
|
|
500
|
+
);
|
|
501
|
+
equal(overrideWarning?.layer, "agentic", "override warning is agentic");
|
|
502
|
+
equal(overrideWarning?.status, "warn", "override judgment can never fail");
|
|
503
|
+
check(
|
|
504
|
+
overrideWarning?.reason.includes("eslint.config.local.ts") === true,
|
|
505
|
+
"override warning carries operator-readable reasoning"
|
|
506
|
+
);
|
|
507
|
+
printStateDump("unjustified-override-warning", {
|
|
508
|
+
warning: findingState(overrideWarning),
|
|
509
|
+
});
|
|
510
|
+
console.log("[EVIDENCE: state-dump: unjustified-override-warning]");
|
|
511
|
+
await writeFile(localEslintPath, localEslintOriginal);
|
|
512
|
+
|
|
513
|
+
const ciPath = path.join(project, ".github", "workflows", "ci.yml");
|
|
514
|
+
const ciOriginal = await readFile(ciPath, "utf8");
|
|
515
|
+
await writeFile(
|
|
516
|
+
ciPath,
|
|
517
|
+
ciOriginal.replace("skip_jobs: ''", "skip_jobs: 'lint'")
|
|
518
|
+
);
|
|
519
|
+
const unreasonedSkip = await assertPureCollection(() =>
|
|
520
|
+
health.runHealth(project, {
|
|
521
|
+
...offline,
|
|
522
|
+
agentic: { enabled: true, evaluator: artifactEvaluator },
|
|
523
|
+
})
|
|
524
|
+
);
|
|
525
|
+
check(
|
|
526
|
+
unreasonedSkip.findings.some(
|
|
527
|
+
finding =>
|
|
528
|
+
finding.check === "agentic.ci.skip.lint" &&
|
|
529
|
+
finding.reason.includes("ci.yml")
|
|
530
|
+
),
|
|
531
|
+
"unreasoned skipped job produces a named warning"
|
|
532
|
+
);
|
|
533
|
+
await writeFile(
|
|
534
|
+
ciPath,
|
|
535
|
+
ciOriginal.replace(
|
|
536
|
+
" skip_jobs: ''",
|
|
537
|
+
" # justification: lint is temporarily enforced by a protected external gate\n skip_jobs: 'lint'"
|
|
538
|
+
)
|
|
539
|
+
);
|
|
540
|
+
const reasonedSkip = await assertPureCollection(() =>
|
|
541
|
+
health.runHealth(project, {
|
|
542
|
+
...offline,
|
|
543
|
+
agentic: { enabled: true, evaluator: artifactEvaluator },
|
|
544
|
+
})
|
|
545
|
+
);
|
|
546
|
+
check(
|
|
547
|
+
!reasonedSkip.findings.some(
|
|
548
|
+
finding => finding.check === "agentic.ci.skip.lint"
|
|
549
|
+
),
|
|
550
|
+
"recorded skip justification clears the job warning"
|
|
551
|
+
);
|
|
552
|
+
printStateDump("skipped-job-reason-clears", {
|
|
553
|
+
undocumented: unreasonedSkip.findings
|
|
554
|
+
.filter(finding => finding.check === "agentic.ci.skip.lint")
|
|
555
|
+
.map(findingState),
|
|
556
|
+
documented: reasonedSkip.findings
|
|
557
|
+
.filter(finding => finding.check === "agentic.ci.skip.lint")
|
|
558
|
+
.map(findingState),
|
|
559
|
+
});
|
|
560
|
+
console.log("[EVIDENCE: state-dump: skipped-job-reason-clears]");
|
|
561
|
+
await writeFile(ciPath, ciOriginal);
|
|
562
|
+
|
|
563
|
+
const managedEslintPath = path.join(project, "eslint.config.ts");
|
|
564
|
+
const managedEslintOriginal = await readFile(managedEslintPath);
|
|
565
|
+
const managedJustification =
|
|
566
|
+
'// justification: temporary vendor migration\nexport default [{ rules: { "@typescript-eslint/no-explicit-any": "off" } }];\n';
|
|
567
|
+
await writeFile(localEslintPath, managedJustification);
|
|
568
|
+
const observedManagedArtifacts = [];
|
|
569
|
+
const managedEvidenceEvaluator = (request, signal) => {
|
|
570
|
+
const managed = request.artifacts.find(
|
|
571
|
+
artifact => artifact.kind === "managed-drift"
|
|
572
|
+
);
|
|
573
|
+
observedManagedArtifacts.push(managed);
|
|
574
|
+
return artifactEvaluator(request, signal);
|
|
575
|
+
};
|
|
576
|
+
const benignManagedContent = Buffer.concat([
|
|
577
|
+
managedEslintOriginal,
|
|
578
|
+
Buffer.from("\n// formatting-only managed drift\n"),
|
|
579
|
+
]);
|
|
580
|
+
const suspiciousManagedContent = Buffer.concat([
|
|
581
|
+
managedEslintOriginal,
|
|
582
|
+
Buffer.from(
|
|
583
|
+
'\nvoid { "@typescript-eslint/no-explicit-any": "off" }; // suspicious managed drift\n'
|
|
584
|
+
),
|
|
585
|
+
]);
|
|
586
|
+
await writeFile(managedEslintPath, benignManagedContent);
|
|
587
|
+
const benignDrift = await assertPureCollection(() =>
|
|
588
|
+
health.runHealth(project, {
|
|
589
|
+
...offline,
|
|
590
|
+
agentic: { enabled: true, evaluator: managedEvidenceEvaluator },
|
|
591
|
+
})
|
|
592
|
+
);
|
|
593
|
+
await writeFile(managedEslintPath, suspiciousManagedContent);
|
|
594
|
+
const suspiciousDrift = await assertPureCollection(() =>
|
|
595
|
+
health.runHealth(project, {
|
|
596
|
+
...offline,
|
|
597
|
+
agentic: { enabled: true, evaluator: managedEvidenceEvaluator },
|
|
598
|
+
})
|
|
599
|
+
);
|
|
600
|
+
equal(
|
|
601
|
+
observedManagedArtifacts.map(artifact => artifact?.path),
|
|
602
|
+
["eslint.config.ts", "eslint.config.ts"],
|
|
603
|
+
"both drift judgments inspect the bounded managed ESLint artifact"
|
|
604
|
+
);
|
|
605
|
+
equal(
|
|
606
|
+
observedManagedArtifacts.map(artifact => artifact?.content),
|
|
607
|
+
[
|
|
608
|
+
benignManagedContent.toString("utf8"),
|
|
609
|
+
suspiciousManagedContent.toString("utf8"),
|
|
610
|
+
],
|
|
611
|
+
"managed-drift evidence carries the current confined content"
|
|
612
|
+
);
|
|
613
|
+
check(
|
|
614
|
+
benignDrift.findings.some(
|
|
615
|
+
finding =>
|
|
616
|
+
finding.check === "templates.managed" && finding.status === "fail"
|
|
617
|
+
),
|
|
618
|
+
"benign agentic judgment cannot erase deterministic managed drift"
|
|
619
|
+
);
|
|
620
|
+
check(
|
|
621
|
+
!benignDrift.findings.some(
|
|
622
|
+
finding => finding.check === "agentic.intentional-drift"
|
|
623
|
+
),
|
|
624
|
+
"benign managed content produces no drift judgment"
|
|
625
|
+
);
|
|
626
|
+
const suspiciousDriftWarning = suspiciousDrift.findings.find(
|
|
627
|
+
finding => finding.check === "agentic.intentional-drift"
|
|
628
|
+
);
|
|
629
|
+
equal(
|
|
630
|
+
suspiciousDriftWarning?.status,
|
|
631
|
+
"warn",
|
|
632
|
+
"suspicious rule-disabling managed content produces a warning"
|
|
633
|
+
);
|
|
634
|
+
equal(
|
|
635
|
+
await readFile(localEslintPath, "utf8"),
|
|
636
|
+
managedJustification,
|
|
637
|
+
"both managed-drift judgments use the same recorded justification"
|
|
638
|
+
);
|
|
639
|
+
printStateDump("managed-drift-content-judgment", {
|
|
640
|
+
sameJustification: true,
|
|
641
|
+
benign: {
|
|
642
|
+
contentSha256: createHash("sha256")
|
|
643
|
+
.update(benignManagedContent)
|
|
644
|
+
.digest("hex"),
|
|
645
|
+
warnings: benignDrift.findings
|
|
646
|
+
.filter(finding => finding.check === "agentic.intentional-drift")
|
|
647
|
+
.map(findingState),
|
|
648
|
+
},
|
|
649
|
+
suspicious: {
|
|
650
|
+
contentSha256: createHash("sha256")
|
|
651
|
+
.update(suspiciousManagedContent)
|
|
652
|
+
.digest("hex"),
|
|
653
|
+
warnings: suspiciousDrift.findings
|
|
654
|
+
.filter(finding => finding.check === "agentic.intentional-drift")
|
|
655
|
+
.map(findingState),
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
await writeFile(managedEslintPath, managedEslintOriginal);
|
|
659
|
+
await writeFile(localEslintPath, localEslintOriginal);
|
|
660
|
+
|
|
661
|
+
let timeoutEvaluatorStarted = 0;
|
|
662
|
+
let timeoutAbortObserved = 0;
|
|
663
|
+
let unavailableProjection;
|
|
664
|
+
const degradationCases = [
|
|
665
|
+
{
|
|
666
|
+
name: "unavailable",
|
|
667
|
+
evaluator: async () => ({ status: "unavailable" }),
|
|
668
|
+
timeoutMs: 1_000,
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
name: "throw",
|
|
672
|
+
evaluator: async () => {
|
|
673
|
+
throw new Error("secret evaluator failure must not escape");
|
|
674
|
+
},
|
|
675
|
+
timeoutMs: 1_000,
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
name: "malformed",
|
|
679
|
+
evaluator: async () => ({
|
|
680
|
+
status: "completed",
|
|
681
|
+
judgments: [{ check: "templates.managed", reason: "collision" }],
|
|
682
|
+
}),
|
|
683
|
+
timeoutMs: 1_000,
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
name: "timeout",
|
|
687
|
+
evaluator: async (_request, signal) =>
|
|
688
|
+
new Promise(resolve => {
|
|
689
|
+
timeoutEvaluatorStarted += 1;
|
|
690
|
+
signal.addEventListener(
|
|
691
|
+
"abort",
|
|
692
|
+
() => {
|
|
693
|
+
timeoutAbortObserved += 1;
|
|
694
|
+
resolve({
|
|
695
|
+
status: "completed",
|
|
696
|
+
judgments: [
|
|
697
|
+
{
|
|
698
|
+
check: "agentic.late-timeout",
|
|
699
|
+
reason:
|
|
700
|
+
"This late judgment must be discarded after cancellation.",
|
|
701
|
+
},
|
|
702
|
+
],
|
|
703
|
+
});
|
|
704
|
+
},
|
|
705
|
+
{ once: true }
|
|
706
|
+
);
|
|
707
|
+
}),
|
|
708
|
+
timeoutMs: 400,
|
|
709
|
+
},
|
|
710
|
+
];
|
|
711
|
+
for (const scenario of degradationCases) {
|
|
712
|
+
const scenarioStarted = Date.now();
|
|
713
|
+
const degraded = await assertPureCollection(() =>
|
|
714
|
+
health.runHealth(project, {
|
|
715
|
+
...offline,
|
|
716
|
+
agentic: {
|
|
717
|
+
enabled: true,
|
|
718
|
+
evaluator: scenario.evaluator,
|
|
719
|
+
timeoutMs: scenario.timeoutMs,
|
|
720
|
+
},
|
|
721
|
+
})
|
|
722
|
+
);
|
|
723
|
+
equal(
|
|
724
|
+
degraded.mode,
|
|
725
|
+
"deterministic",
|
|
726
|
+
`${scenario.name} evaluator degrades to deterministic mode`
|
|
727
|
+
);
|
|
728
|
+
check(
|
|
729
|
+
degraded.findings.every(finding => finding.layer === "deterministic"),
|
|
730
|
+
`${scenario.name} evaluator fabricates no judgment finding`
|
|
731
|
+
);
|
|
732
|
+
check(
|
|
733
|
+
!JSON.stringify(degraded).includes("secret evaluator failure"),
|
|
734
|
+
`${scenario.name} evaluator leaks no raw failure`
|
|
735
|
+
);
|
|
736
|
+
if (scenario.name === "timeout") {
|
|
737
|
+
equal(
|
|
738
|
+
timeoutEvaluatorStarted,
|
|
739
|
+
1,
|
|
740
|
+
"timeout proof reaches the evaluator after evidence collection"
|
|
741
|
+
);
|
|
742
|
+
equal(
|
|
743
|
+
timeoutAbortObserved,
|
|
744
|
+
1,
|
|
745
|
+
"never-settling evaluator observes the shared abort signal"
|
|
746
|
+
);
|
|
747
|
+
check(
|
|
748
|
+
!degraded.findings.some(
|
|
749
|
+
finding => finding.check === "agentic.late-timeout"
|
|
750
|
+
),
|
|
751
|
+
"a judgment resolved during abort is discarded"
|
|
752
|
+
);
|
|
753
|
+
check(
|
|
754
|
+
Date.now() - scenarioStarted < 1_500,
|
|
755
|
+
"never-settling evaluator returns inside its bounded deadline plus fixture overhead"
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
if (scenario.name === "unavailable") {
|
|
759
|
+
unavailableProjection = {
|
|
760
|
+
mode: degraded.mode,
|
|
761
|
+
findings: degraded.findings.map(findingState),
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
printStateDump("unavailable-evaluator-degrades", {
|
|
766
|
+
unavailable: unavailableProjection,
|
|
767
|
+
agenticFindings: unavailableProjection.findings.filter(
|
|
768
|
+
finding => finding.layer === "agentic"
|
|
769
|
+
),
|
|
770
|
+
});
|
|
771
|
+
console.log("[EVIDENCE: state-dump: unavailable-evaluator-degrades]");
|
|
772
|
+
|
|
773
|
+
const escapingEvidence = path.join(workspace, "escaping-eslint-evidence.ts");
|
|
774
|
+
await writeFile(
|
|
775
|
+
escapingEvidence,
|
|
776
|
+
"// reason: this content is outside the project and must never reach the evaluator\nexport default [];\n"
|
|
777
|
+
);
|
|
778
|
+
await rm(localEslintPath, { force: true });
|
|
779
|
+
await symlink(escapingEvidence, localEslintPath);
|
|
780
|
+
let hostileEvaluatorCalls = 0;
|
|
781
|
+
const hostileNow = () => new Date("2026-07-20T12:00:00.000Z");
|
|
782
|
+
try {
|
|
783
|
+
const hostileBaseline = await assertPureCollection(() =>
|
|
784
|
+
health.runHealth(project, {
|
|
785
|
+
...offline,
|
|
786
|
+
now: hostileNow,
|
|
787
|
+
agentic: { enabled: false },
|
|
788
|
+
})
|
|
789
|
+
);
|
|
790
|
+
const hostileDegraded = await assertPureCollection(() =>
|
|
791
|
+
health.runHealth(project, {
|
|
792
|
+
...offline,
|
|
793
|
+
now: hostileNow,
|
|
794
|
+
agentic: {
|
|
795
|
+
enabled: true,
|
|
796
|
+
evaluator: async () => {
|
|
797
|
+
hostileEvaluatorCalls += 1;
|
|
798
|
+
return { status: "completed", judgments: [] };
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
})
|
|
802
|
+
);
|
|
803
|
+
equal(
|
|
804
|
+
hostileEvaluatorCalls,
|
|
805
|
+
0,
|
|
806
|
+
"escaping symlink evidence is rejected before evaluator invocation"
|
|
807
|
+
);
|
|
808
|
+
equal(
|
|
809
|
+
deterministicState(hostileDegraded),
|
|
810
|
+
deterministicState(hostileBaseline),
|
|
811
|
+
"unsafe evidence degrades to the exact deterministic-only projection"
|
|
812
|
+
);
|
|
813
|
+
equal(
|
|
814
|
+
hostileDegraded.mode,
|
|
815
|
+
"deterministic",
|
|
816
|
+
"unsafe evidence retains deterministic mode"
|
|
817
|
+
);
|
|
818
|
+
check(
|
|
819
|
+
hostileDegraded.findings.every(
|
|
820
|
+
finding => finding.layer === "deterministic"
|
|
821
|
+
),
|
|
822
|
+
"unsafe evidence fabricates no agentic finding"
|
|
823
|
+
);
|
|
824
|
+
} finally {
|
|
825
|
+
await rm(localEslintPath, { force: true });
|
|
826
|
+
await writeFile(localEslintPath, localEslintOriginal);
|
|
827
|
+
}
|
|
828
|
+
equal(
|
|
829
|
+
await readFile(localEslintPath),
|
|
830
|
+
localEslintOriginal,
|
|
831
|
+
"hostile evidence fixture restores the original local override"
|
|
832
|
+
);
|
|
833
|
+
console.log("[EVIDENCE: hostile-agentic-degradation]");
|
|
834
|
+
console.log("[EVIDENCE: no-agentic-side-effects]");
|
|
835
|
+
|
|
836
|
+
const elapsedMs = Date.now() - startedAt;
|
|
837
|
+
check(elapsedMs < 120_000, "built agentic proof completes under two minutes");
|
|
838
|
+
check(assertions > 0, "proof must execute assertions");
|
|
839
|
+
console.log(`health-agentic assertions=${assertions} elapsedMs=${elapsedMs}`);
|
|
840
|
+
} finally {
|
|
841
|
+
await rm(workspace, { recursive: true, force: true });
|
|
842
|
+
}
|