@codyswann/lisa 2.277.3 → 2.278.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 (127) hide show
  1. package/dist/cli/index.d.ts.map +1 -1
  2. package/dist/cli/index.js +5 -2
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/cli/setup-project.d.ts.map +1 -1
  5. package/dist/cli/setup-project.js +12 -0
  6. package/dist/cli/setup-project.js.map +1 -1
  7. package/dist/cli/ui-automations-adapters.d.ts +22 -0
  8. package/dist/cli/ui-automations-adapters.d.ts.map +1 -1
  9. package/dist/cli/ui-automations-adapters.js +24 -12
  10. package/dist/cli/ui-automations-adapters.js.map +1 -1
  11. package/dist/cli/ui-automations.d.ts +1 -0
  12. package/dist/cli/ui-automations.d.ts.map +1 -1
  13. package/dist/cli/ui-automations.js +1 -0
  14. package/dist/cli/ui-automations.js.map +1 -1
  15. package/dist/cli/ui-cmd.d.ts +4 -1
  16. package/dist/cli/ui-cmd.d.ts.map +1 -1
  17. package/dist/cli/ui-cmd.js +72 -17
  18. package/dist/cli/ui-cmd.js.map +1 -1
  19. package/dist/cli/ui-confined-project-read.d.ts +22 -0
  20. package/dist/cli/ui-confined-project-read.d.ts.map +1 -0
  21. package/dist/cli/ui-confined-project-read.js +38 -0
  22. package/dist/cli/ui-confined-project-read.js.map +1 -0
  23. package/dist/cli/ui-deploy-pipeline.d.ts.map +1 -1
  24. package/dist/cli/ui-deploy-pipeline.js +4 -15
  25. package/dist/cli/ui-deploy-pipeline.js.map +1 -1
  26. package/dist/cli/ui-detected-stacks.d.ts +9 -8
  27. package/dist/cli/ui-detected-stacks.d.ts.map +1 -1
  28. package/dist/cli/ui-detected-stacks.js +111 -0
  29. package/dist/cli/ui-detected-stacks.js.map +1 -1
  30. package/dist/cli/ui-github-repo-gh.d.ts +6 -0
  31. package/dist/cli/ui-github-repo-gh.d.ts.map +1 -1
  32. package/dist/cli/ui-github-repo-gh.js.map +1 -1
  33. package/dist/cli/ui-github-repo-map.d.ts.map +1 -1
  34. package/dist/cli/ui-github-repo-map.js +26 -1
  35. package/dist/cli/ui-github-repo-map.js.map +1 -1
  36. package/dist/cli/ui-setup-readiness-config.d.ts +28 -0
  37. package/dist/cli/ui-setup-readiness-config.d.ts.map +1 -0
  38. package/dist/cli/ui-setup-readiness-config.js +116 -0
  39. package/dist/cli/ui-setup-readiness-config.js.map +1 -0
  40. package/dist/cli/ui-setup-readiness-contract.d.ts +32 -0
  41. package/dist/cli/ui-setup-readiness-contract.d.ts.map +1 -0
  42. package/dist/cli/ui-setup-readiness-contract.js +61 -0
  43. package/dist/cli/ui-setup-readiness-contract.js.map +1 -0
  44. package/dist/cli/ui-setup-readiness-install.d.ts +24 -0
  45. package/dist/cli/ui-setup-readiness-install.d.ts.map +1 -0
  46. package/dist/cli/ui-setup-readiness-install.js +207 -0
  47. package/dist/cli/ui-setup-readiness-install.js.map +1 -0
  48. package/dist/cli/ui-setup-readiness-local.d.ts +40 -0
  49. package/dist/cli/ui-setup-readiness-local.d.ts.map +1 -0
  50. package/dist/cli/ui-setup-readiness-local.js +235 -0
  51. package/dist/cli/ui-setup-readiness-local.js.map +1 -0
  52. package/dist/cli/ui-setup-readiness-remote.d.ts +30 -0
  53. package/dist/cli/ui-setup-readiness-remote.d.ts.map +1 -0
  54. package/dist/cli/ui-setup-readiness-remote.js +108 -0
  55. package/dist/cli/ui-setup-readiness-remote.js.map +1 -0
  56. package/dist/cli/ui-setup-readiness.d.ts +52 -0
  57. package/dist/cli/ui-setup-readiness.d.ts.map +1 -0
  58. package/dist/cli/ui-setup-readiness.js +125 -0
  59. package/dist/cli/ui-setup-readiness.js.map +1 -0
  60. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  61. package/dist/core/upstream-evidence-manifest.js +14 -2
  62. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  63. package/dist/health/read-only-fs.d.ts +7 -0
  64. package/dist/health/read-only-fs.d.ts.map +1 -1
  65. package/dist/health/read-only-fs.js +40 -1
  66. package/dist/health/read-only-fs.js.map +1 -1
  67. package/dist/sync/registry.d.ts +14 -0
  68. package/dist/sync/registry.d.ts.map +1 -1
  69. package/dist/sync/registry.js +54 -15
  70. package/dist/sync/registry.js.map +1 -1
  71. package/package.json +1 -1
  72. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  74. package/plugins/lisa/scripts/automation-status-codex-adapter.mjs +270 -23
  75. package/plugins/lisa-agy/plugin.json +1 -1
  76. package/plugins/lisa-agy/scripts/automation-status-codex-adapter.mjs +270 -23
  77. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  79. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  80. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-copilot/scripts/automation-status-codex-adapter.mjs +270 -23
  84. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-cursor/scripts/automation-status-codex-adapter.mjs +270 -23
  86. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  88. package/plugins/lisa-expo-agy/plugin.json +1 -1
  89. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  93. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  94. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  98. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  99. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  103. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  104. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  105. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  106. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  108. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  109. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  110. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  111. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  113. package/plugins/lisa-rails-agy/plugin.json +1 -1
  114. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  115. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  116. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  118. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  119. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  120. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  121. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  123. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  124. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  125. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  126. package/plugins/src/base/scripts/automation-status-codex-adapter.mjs +270 -23
  127. package/ui/index.html +1 -1
@@ -11,9 +11,11 @@
11
11
  */
12
12
 
13
13
  import fs from "node:fs/promises";
14
+ import { constants as fsConstants } from "node:fs";
14
15
  import os from "node:os";
15
16
  import path from "node:path";
16
17
  import { execFile } from "node:child_process";
18
+ import { setTimeout as delay } from "node:timers/promises";
17
19
  import { promisify } from "node:util";
18
20
 
19
21
  import { compareAutomationFleet } from "./automation-status-contract-drift.mjs";
@@ -34,6 +36,12 @@ const RUN_FAILURE_PATTERN =
34
36
  const NEGATED_FAILURE_PATTERN =
35
37
  /\b(no|without)\s+(?:recent\s+)?(?:fail(?:ure|ed)|errors?|exceptions?)\b/i;
36
38
  const execFileAsync = promisify(execFile);
39
+ const MAX_SCHEDULER_ENTRIES = 1_000;
40
+ const MAX_MATCHING_AUTOMATIONS = 100;
41
+ const MAX_AUTOMATION_TOML_BYTES = 256 * 1024;
42
+ const MAX_AUTOMATION_MEMORY_BYTES = 512 * 1024;
43
+ const READ_CHUNK_BYTES = 64 * 1024;
44
+ const UNSAFE_SCHEDULER_FILE = "Unsafe Codex scheduler file";
37
45
 
38
46
  /**
39
47
  * Git location env vars that, when inherited, override the `-C <dir>` flag and
@@ -101,7 +109,9 @@ async function execGitWithRetry(args, options, attempts = 4) {
101
109
  if (!transient || attempt >= attempts) {
102
110
  throw error;
103
111
  }
104
- await new Promise(resolve => setTimeout(resolve, 2 ** attempt * 25));
112
+ await delay(2 ** attempt * 25, undefined, {
113
+ signal: options?.signal,
114
+ });
105
115
  }
106
116
  }
107
117
  }
@@ -223,29 +233,149 @@ export async function inspectCodexAutomationFleet(input) {
223
233
  * @param {{
224
234
  * readonly automationsDir?: string
225
235
  * readonly automationPrefix: string
236
+ * readonly signal?: AbortSignal
226
237
  * }} input
227
238
  * @returns {Promise<readonly ObservedCodexAutomation[]>}
228
239
  */
229
240
  export async function listCodexAutomations(input) {
230
- const automationsDir =
241
+ input.signal?.throwIfAborted();
242
+ const requestedRoot =
231
243
  input.automationsDir ?? resolveDefaultCodexAutomationsDir();
232
- const dirEntries = await fs.readdir(automationsDir, {
233
- withFileTypes: true,
234
- });
244
+ const automationsRoot = await fs.realpath(requestedRoot);
245
+ input.signal?.throwIfAborted();
246
+ const automationDirs = await listConfinedAutomationDirectories(
247
+ automationsRoot,
248
+ input.automationPrefix,
249
+ input.signal
250
+ );
251
+ return await readCodexAutomations(automationDirs, input.signal);
252
+ }
235
253
 
236
- const automationDirs = dirEntries
237
- .filter(
238
- entry =>
239
- entry.isDirectory() && entry.name.startsWith(input.automationPrefix)
240
- )
241
- .map(entry => path.join(automationsDir, entry.name))
242
- .sort((left, right) => left.localeCompare(right));
254
+ /**
255
+ * Enumerate a bounded number of direct, regular scheduler directories.
256
+ * @param {string} automationsRoot Canonical scheduler root.
257
+ * @param {string} automationPrefix Expected repository automation prefix.
258
+ * @param {AbortSignal | undefined} signal Probe cancellation signal.
259
+ * @returns {Promise<readonly string[]>} Canonical matching directories.
260
+ */
261
+ async function listConfinedAutomationDirectories(
262
+ automationsRoot,
263
+ automationPrefix,
264
+ signal
265
+ ) {
266
+ const directory = await fs.opendir(automationsRoot);
267
+ try {
268
+ const names = await readBoundedDirectoryNames(
269
+ directory,
270
+ automationPrefix,
271
+ signal
272
+ );
273
+ const directories = await Promise.all(
274
+ names.map(name => confinedAutomationDirectory(automationsRoot, name))
275
+ );
276
+ signal?.throwIfAborted();
277
+ return directories.toSorted((left, right) => left.localeCompare(right));
278
+ } finally {
279
+ await closeDirectory(directory);
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Close a scheduler directory while tolerating runtimes that auto-close it
285
+ * after async iteration.
286
+ * @param {import("node:fs").Dir} directory Open scheduler directory handle.
287
+ * @returns {Promise<void>}
288
+ */
289
+ async function closeDirectory(directory) {
290
+ try {
291
+ await directory.close();
292
+ } catch (error) {
293
+ if (error?.code !== "ERR_DIR_CLOSED") throw error;
294
+ }
295
+ }
243
296
 
244
- const automations = await Promise.all(
245
- automationDirs.map(dir => readCodexAutomation(dir))
297
+ /**
298
+ * Consume directory entries with total and prefix-match bounds.
299
+ * @param {import("node:fs").Dir} directory Open scheduler directory handle.
300
+ * @param {string} automationPrefix Expected repository automation prefix.
301
+ * @param {AbortSignal | undefined} signal Probe cancellation signal.
302
+ * @param {number} [entryCount] Entries consumed so far.
303
+ * @param {readonly string[]} [matchingNames] Matching directory names.
304
+ * @returns {Promise<readonly string[]>} Bounded matching names.
305
+ */
306
+ async function readBoundedDirectoryNames(
307
+ directory,
308
+ automationPrefix,
309
+ signal,
310
+ entryCount = 0,
311
+ matchingNames = []
312
+ ) {
313
+ signal?.throwIfAborted();
314
+ if (entryCount >= MAX_SCHEDULER_ENTRIES) {
315
+ const overflow = await directory.read();
316
+ if (overflow !== null) {
317
+ throw new Error("Codex scheduler enumeration exceeds entry limit");
318
+ }
319
+ return matchingNames;
320
+ }
321
+ const entry = await directory.read();
322
+ signal?.throwIfAborted();
323
+ if (entry === null) return matchingNames;
324
+ const matches =
325
+ entry.isDirectory() && entry.name.startsWith(automationPrefix);
326
+ const nextNames = matches ? [...matchingNames, entry.name] : matchingNames;
327
+ if (nextNames.length > MAX_MATCHING_AUTOMATIONS) {
328
+ throw new Error("Codex scheduler automation count exceeds limit");
329
+ }
330
+ return await readBoundedDirectoryNames(
331
+ directory,
332
+ automationPrefix,
333
+ signal,
334
+ entryCount + 1,
335
+ nextNames
246
336
  );
337
+ }
247
338
 
248
- return automations.filter(Boolean);
339
+ /**
340
+ * Verify one direct scheduler entry remains a confined real directory.
341
+ * @param {string} automationsRoot Canonical scheduler root.
342
+ * @param {string} name Direct entry name.
343
+ * @returns {Promise<string>} Canonical automation directory.
344
+ */
345
+ async function confinedAutomationDirectory(automationsRoot, name) {
346
+ const candidate = path.join(automationsRoot, name);
347
+ const stat = await fs.lstat(candidate);
348
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
349
+ throw new Error("Unsafe Codex scheduler directory");
350
+ }
351
+ const actual = await fs.realpath(candidate);
352
+ if (!actual.startsWith(`${automationsRoot}${path.sep}`)) {
353
+ throw new Error("Codex scheduler directory escapes root");
354
+ }
355
+ return actual;
356
+ }
357
+
358
+ /**
359
+ * Read scheduler entries sequentially to bound simultaneously open files.
360
+ * @param {readonly string[]} automationDirs Canonical automation directories.
361
+ * @param {AbortSignal | undefined} signal Probe cancellation signal.
362
+ * @param {number} [index] Next directory index.
363
+ * @param {readonly ObservedCodexAutomation[]} [results] Completed observations.
364
+ * @returns {Promise<readonly ObservedCodexAutomation[]>} Observations.
365
+ */
366
+ async function readCodexAutomations(
367
+ automationDirs,
368
+ signal,
369
+ index = 0,
370
+ results = []
371
+ ) {
372
+ signal?.throwIfAborted();
373
+ if (index >= automationDirs.length) return results;
374
+ const observation = await readCodexAutomation(automationDirs[index], signal);
375
+ return await readCodexAutomations(automationDirs, signal, index + 1, [
376
+ ...results,
377
+ observation,
378
+ ]);
249
379
  }
250
380
 
251
381
  /**
@@ -361,16 +491,130 @@ function findLatestAutomationMemoryBlock(lines) {
361
491
  };
362
492
  }
363
493
 
364
- async function readCodexAutomation(automationDir) {
365
- const tomlPath = path.join(automationDir, "automation.toml");
366
- const memoryPath = path.join(automationDir, "memory.md");
367
- const tomlContent = await fs.readFile(tomlPath, "utf8");
494
+ /**
495
+ * Read one stable bounded file without following a scheduler symlink or FIFO.
496
+ * @param {string} automationDir Canonical automation directory.
497
+ * @param {string} filename Direct scheduler filename.
498
+ * @param {number} maximumBytes Maximum allowed bytes.
499
+ * @param {AbortSignal | undefined} signal Probe cancellation signal.
500
+ * @param {boolean} [optional] Whether a missing file resolves to undefined.
501
+ * @returns {Promise<string | undefined>} Strict UTF-8 contents.
502
+ */
503
+ async function readConfinedSchedulerText(
504
+ automationDir,
505
+ filename,
506
+ maximumBytes,
507
+ signal,
508
+ optional = false
509
+ ) {
510
+ signal?.throwIfAborted();
511
+ const target = path.join(automationDir, filename);
512
+ const before = await fs.lstat(target).catch(error => {
513
+ if (optional && error?.code === "ENOENT") return null;
514
+ throw error;
515
+ });
516
+ if (before === null) return undefined;
517
+ assertBoundedSchedulerFile(before, maximumBytes);
518
+ const actual = await fs.realpath(target);
519
+ if (!actual.startsWith(`${automationDir}${path.sep}`)) {
520
+ throw new Error("Codex scheduler file escapes automation directory");
521
+ }
522
+ const handle = await fs.open(
523
+ target,
524
+ fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK
525
+ );
526
+ try {
527
+ const opened = await handle.stat();
528
+ assertBoundedSchedulerFile(opened, maximumBytes);
529
+ if (opened.dev !== before.dev || opened.ino !== before.ino) {
530
+ throw new Error("Codex scheduler file changed during inspection");
531
+ }
532
+ const bytes = await readBoundedSchedulerFile(handle, maximumBytes, signal);
533
+ const after = await handle.stat();
534
+ if (after.size !== opened.size || bytes.length !== after.size) {
535
+ throw new Error("Codex scheduler file changed during read");
536
+ }
537
+ signal?.throwIfAborted();
538
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
539
+ } finally {
540
+ await handle.close();
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Reject non-regular and oversized scheduler files before opening them.
546
+ * @param {import("node:fs").Stats} stat Scheduler file metadata.
547
+ * @param {number} maximumBytes Maximum allowed bytes.
548
+ * @returns {void}
549
+ */
550
+ function assertBoundedSchedulerFile(stat, maximumBytes) {
551
+ if (!stat.isFile() || stat.isSymbolicLink()) {
552
+ throw new Error(UNSAFE_SCHEDULER_FILE);
553
+ }
554
+ if (stat.size > maximumBytes) {
555
+ throw new Error("Codex scheduler file exceeds size limit");
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Read at most maximum plus one bytes from an already confined file handle.
561
+ * @param {import("node:fs/promises").FileHandle} handle Open file handle.
562
+ * @param {number} maximumBytes Maximum allowed bytes.
563
+ * @param {AbortSignal | undefined} signal Probe cancellation signal.
564
+ * @param {number} [offset] Current file offset.
565
+ * @returns {Promise<Buffer>} Bounded bytes.
566
+ */
567
+ async function readBoundedSchedulerFile(
568
+ handle,
569
+ maximumBytes,
570
+ signal,
571
+ offset = 0
572
+ ) {
573
+ signal?.throwIfAborted();
574
+ const remaining = maximumBytes + 1 - offset;
575
+ if (remaining <= 0) {
576
+ throw new Error("Codex scheduler file exceeds size limit");
577
+ }
578
+ const chunk = Buffer.allocUnsafe(Math.min(READ_CHUNK_BYTES, remaining));
579
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, offset);
580
+ signal?.throwIfAborted();
581
+ if (bytesRead === 0) return Buffer.alloc(0);
582
+ const current = chunk.subarray(0, bytesRead);
583
+ const tail = await readBoundedSchedulerFile(
584
+ handle,
585
+ maximumBytes,
586
+ signal,
587
+ offset + bytesRead
588
+ );
589
+ return Buffer.concat([current, tail], bytesRead + tail.length);
590
+ }
591
+
592
+ /**
593
+ * Read one confined Codex automation contract and optional memory.
594
+ * @param {string} automationDir Canonical automation directory.
595
+ * @param {AbortSignal | undefined} signal Probe cancellation signal.
596
+ * @returns {Promise<ObservedCodexAutomation>} Normalized observation.
597
+ */
598
+ async function readCodexAutomation(automationDir, signal) {
599
+ const tomlContent = await readConfinedSchedulerText(
600
+ automationDir,
601
+ "automation.toml",
602
+ MAX_AUTOMATION_TOML_BYTES,
603
+ signal
604
+ );
368
605
  const automation = parseAutomationToml(tomlContent);
369
- const memoryContent = await fs.readFile(memoryPath, "utf8").catch(() => "");
606
+ const memoryContent =
607
+ (await readConfinedSchedulerText(
608
+ automationDir,
609
+ "memory.md",
610
+ MAX_AUTOMATION_MEMORY_BYTES,
611
+ signal,
612
+ true
613
+ )) ?? "";
370
614
  const memory = parseCodexAutomationMemory(memoryContent);
371
615
  const cwd = Array.isArray(automation.cwds) ? automation.cwds[0] : null;
372
616
  const normalizedCwd = typeof cwd === "string" ? cwd : null;
373
- const cwdHealth = await inspectAutomationCwd(normalizedCwd);
617
+ const cwdHealth = await inspectAutomationCwd(normalizedCwd, signal);
374
618
 
375
619
  return {
376
620
  automationId:
@@ -546,7 +790,8 @@ function classifyAutomationRunSignal(input) {
546
790
  return null;
547
791
  }
548
792
 
549
- async function inspectAutomationCwd(cwd) {
793
+ async function inspectAutomationCwd(cwd, signal) {
794
+ signal?.throwIfAborted();
550
795
  if (!cwd) {
551
796
  return {
552
797
  status: "missing",
@@ -560,6 +805,7 @@ async function inspectAutomationCwd(cwd) {
560
805
  }
561
806
  throw error;
562
807
  });
808
+ signal?.throwIfAborted();
563
809
 
564
810
  if (!stat) {
565
811
  return {
@@ -578,7 +824,7 @@ async function inspectAutomationCwd(cwd) {
578
824
  try {
579
825
  const { stdout } = await execGitWithRetry(
580
826
  ["-C", cwd, "rev-parse", "--is-inside-work-tree", "--is-bare-repository"],
581
- { timeout: 5000 }
827
+ { timeout: 5000, signal }
582
828
  );
583
829
  const [insideWorkTree, bareRepository] = stdout.trim().split(/\r?\n/);
584
830
 
@@ -601,6 +847,7 @@ async function inspectAutomationCwd(cwd) {
601
847
  summary: `${cwd} is a valid Git work tree`,
602
848
  };
603
849
  } catch (error) {
850
+ signal?.throwIfAborted();
604
851
  return {
605
852
  status: "git_error",
606
853
  summary: `git could not inspect ${cwd}: ${String(error?.message ?? error)}`,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.277.3",
3
+ "version": "2.278.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.277.3",
3
+ "version": "2.278.0",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.277.3",
3
+ "version": "2.278.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.277.3",
3
+ "version": "2.278.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.277.3",
3
+ "version": "2.278.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.277.3",
3
+ "version": "2.278.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"