@codyswann/lisa 3.47.6 → 3.47.7

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 (74) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +6 -4
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa/scripts/automation-run-record.mjs +124 -4
  8. package/plugins/lisa/scripts/automation-status-claude-adapter.mjs +9 -1
  9. package/plugins/lisa/scripts/automation-status-codex-adapter.mjs +9 -1
  10. package/plugins/lisa/scripts/automation-status-run-history.mjs +34 -3
  11. package/plugins/lisa-agy/plugin.json +1 -1
  12. package/plugins/lisa-agy/scripts/automation-run-record.mjs +124 -4
  13. package/plugins/lisa-agy/scripts/automation-status-claude-adapter.mjs +9 -1
  14. package/plugins/lisa-agy/scripts/automation-status-codex-adapter.mjs +9 -1
  15. package/plugins/lisa-agy/scripts/automation-status-run-history.mjs +34 -3
  16. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  19. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/scripts/automation-run-record.mjs +124 -4
  23. package/plugins/lisa-copilot/scripts/automation-status-claude-adapter.mjs +9 -1
  24. package/plugins/lisa-copilot/scripts/automation-status-codex-adapter.mjs +9 -1
  25. package/plugins/lisa-copilot/scripts/automation-status-run-history.mjs +34 -3
  26. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cursor/scripts/automation-run-record.mjs +124 -4
  28. package/plugins/lisa-cursor/scripts/automation-status-claude-adapter.mjs +9 -1
  29. package/plugins/lisa-cursor/scripts/automation-status-codex-adapter.mjs +9 -1
  30. package/plugins/lisa-cursor/scripts/automation-status-run-history.mjs +34 -3
  31. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-expo-agy/plugin.json +1 -1
  34. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  44. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  49. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  54. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails-agy/plugin.json +1 -1
  59. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  64. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  69. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/src/base/scripts/automation-run-record.mjs +124 -4
  72. package/plugins/src/base/scripts/automation-status-claude-adapter.mjs +9 -1
  73. package/plugins/src/base/scripts/automation-status-codex-adapter.mjs +9 -1
  74. package/plugins/src/base/scripts/automation-status-run-history.mjs +34 -3
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Harper/Fabric-specific Lisa rules for TypeScript component apps.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "NestJS-specific skills and migration write-protection hooks.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "3.47.6",
3
+ "version": "3.47.7",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -6,7 +6,13 @@
6
6
  * file per loop under `.lisa/automations/runs/`.
7
7
  */
8
8
 
9
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
9
+ import {
10
+ appendFile,
11
+ mkdir,
12
+ readFile,
13
+ rename,
14
+ writeFile,
15
+ } from "node:fs/promises";
10
16
  import path from "node:path";
11
17
  import { realpathSync } from "node:fs";
12
18
  import { fileURLToPath } from "node:url";
@@ -58,7 +64,7 @@ export const DEFAULT_AUTOMATION_RUN_HISTORY_MAX_ENTRIES = 50;
58
64
  * same `run_id` and trimming the file to the configured history bound.
59
65
  *
60
66
  * @param {RecordAutomationRunInput} input
61
- * @returns {Promise<{ readonly path: string, readonly record: AutomationRunRecord, readonly records: readonly AutomationRunRecord[], readonly appended: boolean, readonly skippedCorruptLines: number, readonly maxEntries: number }>}
67
+ * @returns {Promise<{ readonly path: string, readonly record: AutomationRunRecord, readonly records: readonly AutomationRunRecord[], readonly appended: boolean, readonly skippedCorruptLines: number, readonly quarantinedLines: number, readonly quarantinePath: string, readonly maxEntries: number }>}
62
68
  */
63
69
  export async function recordAutomationRun(input) {
64
70
  // Write path only. Stored rows are re-validated on read through
@@ -78,6 +84,11 @@ export async function recordAutomationRun(input) {
78
84
  const filePath = automationRunRecordPath(projectRoot, record.loop_id);
79
85
  const readResult = await readAutomationRunRecords(filePath);
80
86
 
87
+ const quarantinePath = automationRunQuarantinePath(
88
+ projectRoot,
89
+ record.loop_id
90
+ );
91
+
81
92
  if (readResult.records.some(existing => existing.run_id === record.run_id)) {
82
93
  return {
83
94
  path: filePath,
@@ -85,6 +96,8 @@ export async function recordAutomationRun(input) {
85
96
  records: readResult.records,
86
97
  appended: false,
87
98
  skippedCorruptLines: readResult.skippedCorruptLines,
99
+ quarantinedLines: 0,
100
+ quarantinePath,
88
101
  maxEntries,
89
102
  };
90
103
  }
@@ -95,11 +108,38 @@ export async function recordAutomationRun(input) {
95
108
  // recognise came back as `[]`, and a row that failed validation came back as
96
109
  // nothing at all (#2682, #2578). Only the row being appended is serialised
97
110
  // here, so an append can no longer be a fleet-wide migration nobody ran.
98
- const nextEntries = [
111
+ const allEntries = [
99
112
  ...readResult.entries,
100
113
  { line: JSON.stringify(record), record },
101
- ].slice(-maxEntries);
114
+ ];
115
+ const nextEntries = allEntries.slice(-maxEntries);
116
+ // Keeping an unreadable row in place only postponed its deletion. The bound
117
+ // applies to every stored line, so a row this module could not parse sat in
118
+ // the window counting down, and somewhere around `maxEntries` appends later
119
+ // it fell off the front and was gone — with nothing said, and with
120
+ // `skippedCorruptLines` dropping back to 0 on the next read, so afterwards a
121
+ // destroyed row looked exactly like one that never existed (#2578).
122
+ //
123
+ // A row nobody can parse is the evidence of whatever went wrong, and it is
124
+ // needed precisely when it is unreadable. Evicting a WELL-FORMED row is the
125
+ // bounded-history contract working as designed; evicting one nobody could
126
+ // read is throwing away the only copy. So the second kind is copied to a
127
+ // sidecar first and the ledger stays bounded.
128
+ const quarantined = allEntries
129
+ .slice(0, Math.max(0, allEntries.length - maxEntries))
130
+ .filter(entry => entry.record === undefined)
131
+ .map(entry => entry.line);
132
+
102
133
  await mkdir(path.dirname(filePath), { recursive: true });
134
+ // Sidecar first, ledger second, and a sidecar failure aborts the append
135
+ // rather than being swallowed. A crash between the two writes leaves the row
136
+ // in BOTH files, which anyone reading them can reconcile; the other order
137
+ // leaves it in neither, which nobody can reconstruct. Failing to record one
138
+ // outcome is recoverable — the runbook contract says a recording failure is
139
+ // a degradation to report — and destroying the evidence is not.
140
+ if (quarantined.length > 0) {
141
+ await quarantineUnreadableLines(quarantinePath, quarantined);
142
+ }
103
143
  await writeJsonlAtomically(
104
144
  filePath,
105
145
  nextEntries.map(entry => entry.line)
@@ -111,10 +151,74 @@ export async function recordAutomationRun(input) {
111
151
  records: nextEntries.flatMap(entry => (entry.record ? [entry.record] : [])),
112
152
  appended: true,
113
153
  skippedCorruptLines: readResult.skippedCorruptLines,
154
+ quarantinedLines: quarantined.length,
155
+ quarantinePath,
114
156
  maxEntries,
115
157
  };
116
158
  }
117
159
 
160
+ /**
161
+ * Where rows this module could not read are kept once the history bound
162
+ * evicts them from the ledger.
163
+ *
164
+ * A sidecar rather than a bigger ledger: the ledger's bound is what keeps a
165
+ * status read cheap and its history honest, and unreadable rows are not run
166
+ * history — they are evidence, and evidence has no reason to expire.
167
+ * @param {string} projectRoot
168
+ * @param {string} loopId
169
+ * @returns {string}
170
+ */
171
+ export function automationRunQuarantinePath(projectRoot, loopId) {
172
+ return path.join(
173
+ path.resolve(projectRoot),
174
+ ".lisa",
175
+ "automations",
176
+ "runs",
177
+ `${normalizeLoopId(loopId)}.quarantine.jsonl`
178
+ );
179
+ }
180
+
181
+ /**
182
+ * Append evicted, unreadable rows verbatim to the quarantine sidecar.
183
+ *
184
+ * Appended rather than rewritten, and never parsed or re-serialised: whatever
185
+ * made a row unreadable is the thing worth keeping, so it is stored as the
186
+ * exact bytes that were on disk.
187
+ * @param {string} quarantinePath
188
+ * @param {readonly string[]} lines
189
+ * @returns {Promise<void>}
190
+ */
191
+ async function quarantineUnreadableLines(quarantinePath, lines) {
192
+ await mkdir(path.dirname(quarantinePath), { recursive: true });
193
+ await appendFile(quarantinePath, `${lines.join("\n")}\n`, "utf8");
194
+ }
195
+
196
+ /**
197
+ * One operator-readable sentence about rows the ledger holds that this module
198
+ * cannot read, or undefined when there are none.
199
+ *
200
+ * `skippedCorruptLines` existed, was returned by both the read and the write
201
+ * path, and was destructured by every caller — and printed by none of them.
202
+ * A counter nobody reads is not a control (#2578), so this is the wording the
203
+ * CLI and the status surface share, and neither has to invent its own.
204
+ * @param {{ readonly path: string, readonly skippedCorruptLines: number, readonly quarantinedLines: number, readonly quarantinePath: string }} input
205
+ * @returns {string | undefined}
206
+ */
207
+ export function describeUnreadableLedgerRows(input) {
208
+ const sentences = [];
209
+ if (input.quarantinedLines > 0) {
210
+ sentences.push(
211
+ `${input.path}: ${input.quarantinedLines} row(s) this run could not read reached the end of the bounded history. They were copied to ${input.quarantinePath} before the ledger was trimmed, so nothing was deleted.`
212
+ );
213
+ }
214
+ if (input.skippedCorruptLines > 0) {
215
+ sentences.push(
216
+ `${input.path}: ${input.skippedCorruptLines} stored row(s) are not readable as run records. They are kept exactly as written and are excluded from run history, and each will be copied to ${input.quarantinePath} before the history bound evicts it. Open the file, fix or remove those lines, and the message stops.`
217
+ );
218
+ }
219
+ return sentences.length > 0 ? sentences.join(" ") : undefined;
220
+ }
221
+
118
222
  /**
119
223
  * @param {string} projectRoot
120
224
  * @returns {Promise<number>}
@@ -501,8 +605,24 @@ export async function runAutomationRunRecordCli(argv) {
501
605
  outcome: result.record.outcome,
502
606
  loop_id: result.record.loop_id,
503
607
  run_id: result.record.run_id,
608
+ skipped_corrupt_lines: result.skippedCorruptLines,
609
+ quarantined_lines: result.quarantinedLines,
504
610
  })}\n`
505
611
  );
612
+ // Printed, not merely returned. Every registered loop runs this CLI on
613
+ // every cycle, and its stderr is read by whoever ran it — so this is the
614
+ // most frequently consulted surface the condition has, and it is the one
615
+ // that used to say nothing at all (#2578).
616
+ const notice = describeUnreadableLedgerRows(result);
617
+ if (notice !== undefined) {
618
+ process.stderr.write(`${notice}\n`);
619
+ }
620
+ // Still 0. The append SUCCEEDED, and reporting a successful write as a
621
+ // failure is the same defect pointed the other way — the loop would record
622
+ // a degradation that did not happen, on every cycle, until someone edited
623
+ // the file. The refusal that matters is already above: an append that
624
+ // cannot save an evicted row throws before the ledger is rewritten, and
625
+ // lands here as exit 1 with the reason.
506
626
  return 0;
507
627
  } catch (error) {
508
628
  process.stderr.write(`${error.message}\n`);
@@ -18,6 +18,7 @@ import {
18
18
  import {
19
19
  RUNBOOK_NOT_SCAFFOLDED_LINE,
20
20
  resolveAutomationRunDisplay,
21
+ resolveLedgerIntegrityFinding,
21
22
  resolveRecoveryEscalation,
22
23
  } from "./automation-status-run-history.mjs";
23
24
  import { resolveUnrecordedRunFinding } from "./automation-status-unrecorded-runs.mjs";
@@ -266,13 +267,20 @@ function createObservedStatusItem(input) {
266
267
  ? resolveObservedCadenceMs({ observed, expected })
267
268
  : null,
268
269
  });
270
+ // Ranked last of the three. A scheduler entry that is off, or a run the
271
+ // ledger never received, describes the loop; unreadable rows describe the
272
+ // ledger. Reporting the storage problem over either cause would bury the
273
+ // cause — but with nothing else to say, it is said rather than counted where
274
+ // nobody looks (#2578).
275
+ const ledgerIntegrity = resolveLedgerIntegrityFinding(runDisplay);
269
276
  const runSignal =
270
277
  primaryRunSignal ??
271
278
  (unrecorded && {
272
279
  status: /** @type {const} */ ("DRIFTED"),
273
280
  summary: unrecorded.summary,
274
281
  remediation: unrecorded.remediation,
275
- });
282
+ }) ??
283
+ ledgerIntegrity;
276
284
 
277
285
  const observedDetails = [comparison.observed];
278
286
  if (observed?.status) {
@@ -27,6 +27,7 @@ import {
27
27
  import {
28
28
  RUNBOOK_NOT_SCAFFOLDED_LINE,
29
29
  resolveAutomationRunDisplay,
30
+ resolveLedgerIntegrityFinding,
30
31
  resolveRecoveryEscalation,
31
32
  } from "./automation-status-run-history.mjs";
32
33
  import { resolveUnrecordedRunFinding } from "./automation-status-unrecorded-runs.mjs";
@@ -689,13 +690,20 @@ function createObservedStatusItem(input) {
689
690
  ? resolveObservedCadenceMs({ observed, expected })
690
691
  : null,
691
692
  });
693
+ // Ranked last of the three. A suspended scheduler entry, or a run the ledger
694
+ // never received, describes the loop; unreadable rows describe the ledger.
695
+ // Reporting the storage problem over either cause would bury the cause — but
696
+ // with nothing else to say, it is said rather than counted where nobody
697
+ // looks (#2578).
698
+ const ledgerIntegrity = resolveLedgerIntegrityFinding(runDisplay);
692
699
  const runSignal =
693
700
  primaryRunSignal ??
694
701
  (unrecorded && {
695
702
  status: /** @type {const} */ ("DRIFTED"),
696
703
  summary: unrecorded.summary,
697
704
  remediation: unrecorded.remediation,
698
- });
705
+ }) ??
706
+ ledgerIntegrity;
699
707
 
700
708
  const observedDetails = [comparison.observed];
701
709
  if (observed?.status) {