@codyswann/lisa 3.47.6 → 3.47.8
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/lisa-owned-provenance.d.ts +13 -0
- package/dist/core/lisa-owned-provenance.d.ts.map +1 -1
- package/dist/core/lisa-owned-provenance.js +18 -0
- package/dist/core/lisa-owned-provenance.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +9 -4
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/strategies/copy-overwrite.d.ts +30 -4
- package/dist/strategies/copy-overwrite.d.ts.map +1 -1
- package/dist/strategies/copy-overwrite.js +63 -14
- package/dist/strategies/copy-overwrite.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/scripts/automation-run-record.mjs +124 -4
- package/plugins/lisa/scripts/automation-status-claude-adapter.mjs +9 -1
- package/plugins/lisa/scripts/automation-status-codex-adapter.mjs +9 -1
- package/plugins/lisa/scripts/automation-status-run-history.mjs +34 -3
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/automation-run-record.mjs +124 -4
- package/plugins/lisa-agy/scripts/automation-status-claude-adapter.mjs +9 -1
- package/plugins/lisa-agy/scripts/automation-status-codex-adapter.mjs +9 -1
- package/plugins/lisa-agy/scripts/automation-status-run-history.mjs +34 -3
- 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/scripts/automation-run-record.mjs +124 -4
- package/plugins/lisa-copilot/scripts/automation-status-claude-adapter.mjs +9 -1
- package/plugins/lisa-copilot/scripts/automation-status-codex-adapter.mjs +9 -1
- package/plugins/lisa-copilot/scripts/automation-status-run-history.mjs +34 -3
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/scripts/automation-run-record.mjs +124 -4
- package/plugins/lisa-cursor/scripts/automation-status-claude-adapter.mjs +9 -1
- package/plugins/lisa-cursor/scripts/automation-status-codex-adapter.mjs +9 -1
- package/plugins/lisa-cursor/scripts/automation-status-run-history.mjs +34 -3
- 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/scripts/automation-run-record.mjs +124 -4
- package/plugins/src/base/scripts/automation-status-claude-adapter.mjs +9 -1
- package/plugins/src/base/scripts/automation-status-codex-adapter.mjs +9 -1
- package/plugins/src/base/scripts/automation-status-run-history.mjs +34 -3
|
@@ -38,10 +38,10 @@ export declare class CopyOverwriteStrategy implements ICopyStrategy {
|
|
|
38
38
|
* That refresh is unconditional only where Lisa can prove the installed copy
|
|
39
39
|
* is behind. "Differs from mine" is not that proof — it is equally consistent
|
|
40
40
|
* with the host being *ahead*, which is how `acmeorga/frontend` had a guard
|
|
41
|
-
* they had hardened themselves silently replaced by a weaker upstream one.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* they had hardened themselves silently replaced by a weaker upstream one.
|
|
42
|
+
* That classification now runs in `apply`, before any branch here is chosen,
|
|
43
|
+
* so it covers the prompted routes too — see `preserveOwnedHostCopy`.
|
|
44
|
+
* Anything arriving here is either not Lisa's to judge, or provably behind.
|
|
45
45
|
* @param sourcePath - Packaged template path
|
|
46
46
|
* @param destPath - Installed file path
|
|
47
47
|
* @param relativePath - Repo-relative path for reporting
|
|
@@ -49,6 +49,25 @@ export declare class CopyOverwriteStrategy implements ICopyStrategy {
|
|
|
49
49
|
* @returns Whether the file was refreshed, preserved, or left out of date
|
|
50
50
|
*/
|
|
51
51
|
private applyNonInteractive;
|
|
52
|
+
/**
|
|
53
|
+
* Hold back a replacement of a Lisa-owned artifact on every route that has
|
|
54
|
+
* one, rather than on the single unattended route.
|
|
55
|
+
*
|
|
56
|
+
* Two conditions still let a replacement through. The file must be one Lisa
|
|
57
|
+
* owns outright — a `tsconfig.json` the project customised is not Lisa's to
|
|
58
|
+
* judge, and keeps its existing prompt-or-stale behaviour. And the operator
|
|
59
|
+
* must not have named it to `--refresh-templates`, which is them saying in
|
|
60
|
+
* advance "take upstream's version of these". That flag is the exit
|
|
61
|
+
* `describePreserved` tells them to use, so it has to actually work; the
|
|
62
|
+
* postinstall passes no flags, so honouring it here changes nothing for the
|
|
63
|
+
* unattended fleet.
|
|
64
|
+
* @param sourcePath - Packaged template path
|
|
65
|
+
* @param destPath - Installed file path
|
|
66
|
+
* @param relativePath - Repo-relative path for reporting
|
|
67
|
+
* @param context - Strategy context with config and ledger
|
|
68
|
+
* @returns A preserved result when the copy is kept, else undefined
|
|
69
|
+
*/
|
|
70
|
+
private preserveOwnedHostCopy;
|
|
52
71
|
/**
|
|
53
72
|
* Hold back a Lisa-owned refresh that cannot be proved to be an upgrade.
|
|
54
73
|
*
|
|
@@ -61,6 +80,13 @@ export declare class CopyOverwriteStrategy implements ICopyStrategy {
|
|
|
61
80
|
* bytes cannot be read they cannot be classified, and a classifier that
|
|
62
81
|
* defaults to "overwrite" when it is confused is the original defect wearing a
|
|
63
82
|
* different hat.
|
|
83
|
+
*
|
|
84
|
+
* That sentence described the intent and not the code until now: an
|
|
85
|
+
* unreadable file returned `undefined`, which is this method's word for
|
|
86
|
+
* "nothing to preserve", and the caller carried straight on to the overwrite.
|
|
87
|
+
* `filesIdentical` swallows its own read errors and answers "differs", so an
|
|
88
|
+
* unreadable Lisa-owned guard reached here on every apply and was replaced
|
|
89
|
+
* without ever being classified. Now it is kept, and the reason is named.
|
|
64
90
|
* @param sourcePath - Packaged template path
|
|
65
91
|
* @param destPath - Installed file path
|
|
66
92
|
* @param relativePath - Repo-relative path for reporting
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"copy-overwrite.d.ts","sourceRoot":"","sources":["../../src/strategies/copy-overwrite.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"copy-overwrite.d.ts","sourceRoot":"","sources":["../../src/strategies/copy-overwrite.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAS7D,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAG9E;;;;;;GAMG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IACzD,QAAQ,CAAC,IAAI,EAAG,gBAAgB,CAAU;IAE1C;;;;;;;OAOG;IACG,KAAK,CACT,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,mBAAmB,CAAC;IA4E/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;YACW,mBAAmB;IAwBjC;;;;;;;;;;;;;;;;;OAiBG;YACW,qBAAqB;IAkBnC;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;YACW,mBAAmB;CAkClC"}
|
|
@@ -2,7 +2,7 @@ import * as fse from "fs-extra";
|
|
|
2
2
|
import { copyFile, readFile } from "node:fs/promises";
|
|
3
3
|
import { mayRefreshTemplate } from "../core/config.js";
|
|
4
4
|
import { isLisaOwnedTemplate } from "../core/lisa-owned-templates.js";
|
|
5
|
-
import { classifyHostCopy, describePreserved, mayRefreshLisaOwned, } from "../core/lisa-owned-provenance.js";
|
|
5
|
+
import { classifyHostCopy, describePreserved, describeUnclassifiable, mayRefreshLisaOwned, } from "../core/lisa-owned-provenance.js";
|
|
6
6
|
import { filesIdentical, ensureParentDir } from "../utils/file-operations.js";
|
|
7
7
|
/**
|
|
8
8
|
* Copy-overwrite strategy: Replace file if exists (prompts on conflict)
|
|
@@ -34,6 +34,22 @@ export class CopyOverwriteStrategy {
|
|
|
34
34
|
if (await filesIdentical(sourcePath, destPath)) {
|
|
35
35
|
return { relativePath, strategy: this.name, action: "skipped" };
|
|
36
36
|
}
|
|
37
|
+
// Before any branch below decides *how* to replace the file, ask whether
|
|
38
|
+
// replacing it is an upgrade at all.
|
|
39
|
+
//
|
|
40
|
+
// This classification used to live inside `applyNonInteractive`, which is
|
|
41
|
+
// reached only when `skipGitCheck` is set — the postinstall's flag, not the
|
|
42
|
+
// command's. So `lisa apply` typed by an operator, `lisa apply --yes`, and
|
|
43
|
+
// every non-TTY run took the prompt branch instead and replaced the file
|
|
44
|
+
// without ever classifying it. Non-TTY is not the exotic case: with no
|
|
45
|
+
// usable stdin `createPrompter` returns `AutoAcceptPrompter`, whose
|
|
46
|
+
// `promptOverwrite` answers "yes" without asking anyone, which is what
|
|
47
|
+
// every scripted and agent-driven apply gets. The guard that stops a guard
|
|
48
|
+
// being silently downgraded could be walked around by running the command
|
|
49
|
+
// the normal way (#2577).
|
|
50
|
+
const preserved = await this.preserveOwnedHostCopy(sourcePath, destPath, relativePath, context);
|
|
51
|
+
if (preserved !== undefined)
|
|
52
|
+
return preserved;
|
|
37
53
|
// A non-interactive apply (postinstall / `--skip-git-check`) cannot prompt,
|
|
38
54
|
// and replacing a file the project may have customised without asking is
|
|
39
55
|
// not a decision this path gets to make. So the file is left alone — but
|
|
@@ -79,10 +95,10 @@ export class CopyOverwriteStrategy {
|
|
|
79
95
|
* That refresh is unconditional only where Lisa can prove the installed copy
|
|
80
96
|
* is behind. "Differs from mine" is not that proof — it is equally consistent
|
|
81
97
|
* with the host being *ahead*, which is how `acmeorga/frontend` had a guard
|
|
82
|
-
* they had hardened themselves silently replaced by a weaker upstream one.
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
98
|
+
* they had hardened themselves silently replaced by a weaker upstream one.
|
|
99
|
+
* That classification now runs in `apply`, before any branch here is chosen,
|
|
100
|
+
* so it covers the prompted routes too — see `preserveOwnedHostCopy`.
|
|
101
|
+
* Anything arriving here is either not Lisa's to judge, or provably behind.
|
|
86
102
|
* @param sourcePath - Packaged template path
|
|
87
103
|
* @param destPath - Installed file path
|
|
88
104
|
* @param relativePath - Repo-relative path for reporting
|
|
@@ -91,16 +107,10 @@ export class CopyOverwriteStrategy {
|
|
|
91
107
|
*/
|
|
92
108
|
async applyNonInteractive(sourcePath, destPath, relativePath, context) {
|
|
93
109
|
const { config, backupFile } = context;
|
|
94
|
-
|
|
95
|
-
if (!lisaOwned &&
|
|
110
|
+
if (!isLisaOwnedTemplate(relativePath) &&
|
|
96
111
|
!mayRefreshTemplate(relativePath, config.refreshTemplates)) {
|
|
97
112
|
return { relativePath, strategy: this.name, action: "stale" };
|
|
98
113
|
}
|
|
99
|
-
if (lisaOwned) {
|
|
100
|
-
const preserved = await this.preserveIfHostAhead(sourcePath, destPath, relativePath, context.hashLedger);
|
|
101
|
-
if (preserved !== undefined)
|
|
102
|
-
return preserved;
|
|
103
|
-
}
|
|
104
114
|
// Backed up first: opting in to an overwrite is not opting out of being
|
|
105
115
|
// able to undo it.
|
|
106
116
|
if (!config.dryRun) {
|
|
@@ -109,6 +119,32 @@ export class CopyOverwriteStrategy {
|
|
|
109
119
|
}
|
|
110
120
|
return { relativePath, strategy: this.name, action: "overwritten" };
|
|
111
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Hold back a replacement of a Lisa-owned artifact on every route that has
|
|
124
|
+
* one, rather than on the single unattended route.
|
|
125
|
+
*
|
|
126
|
+
* Two conditions still let a replacement through. The file must be one Lisa
|
|
127
|
+
* owns outright — a `tsconfig.json` the project customised is not Lisa's to
|
|
128
|
+
* judge, and keeps its existing prompt-or-stale behaviour. And the operator
|
|
129
|
+
* must not have named it to `--refresh-templates`, which is them saying in
|
|
130
|
+
* advance "take upstream's version of these". That flag is the exit
|
|
131
|
+
* `describePreserved` tells them to use, so it has to actually work; the
|
|
132
|
+
* postinstall passes no flags, so honouring it here changes nothing for the
|
|
133
|
+
* unattended fleet.
|
|
134
|
+
* @param sourcePath - Packaged template path
|
|
135
|
+
* @param destPath - Installed file path
|
|
136
|
+
* @param relativePath - Repo-relative path for reporting
|
|
137
|
+
* @param context - Strategy context with config and ledger
|
|
138
|
+
* @returns A preserved result when the copy is kept, else undefined
|
|
139
|
+
*/
|
|
140
|
+
async preserveOwnedHostCopy(sourcePath, destPath, relativePath, context) {
|
|
141
|
+
if (!isLisaOwnedTemplate(relativePath))
|
|
142
|
+
return undefined;
|
|
143
|
+
if (mayRefreshTemplate(relativePath, context.config.refreshTemplates)) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
return this.preserveIfHostAhead(sourcePath, destPath, relativePath, context.hashLedger);
|
|
147
|
+
}
|
|
112
148
|
/**
|
|
113
149
|
* Hold back a Lisa-owned refresh that cannot be proved to be an upgrade.
|
|
114
150
|
*
|
|
@@ -121,6 +157,13 @@ export class CopyOverwriteStrategy {
|
|
|
121
157
|
* bytes cannot be read they cannot be classified, and a classifier that
|
|
122
158
|
* defaults to "overwrite" when it is confused is the original defect wearing a
|
|
123
159
|
* different hat.
|
|
160
|
+
*
|
|
161
|
+
* That sentence described the intent and not the code until now: an
|
|
162
|
+
* unreadable file returned `undefined`, which is this method's word for
|
|
163
|
+
* "nothing to preserve", and the caller carried straight on to the overwrite.
|
|
164
|
+
* `filesIdentical` swallows its own read errors and answers "differs", so an
|
|
165
|
+
* unreadable Lisa-owned guard reached here on every apply and was replaced
|
|
166
|
+
* without ever being classified. Now it is kept, and the reason is named.
|
|
124
167
|
* @param sourcePath - Packaged template path
|
|
125
168
|
* @param destPath - Installed file path
|
|
126
169
|
* @param relativePath - Repo-relative path for reporting
|
|
@@ -132,8 +175,14 @@ export class CopyOverwriteStrategy {
|
|
|
132
175
|
readFile(destPath).catch(() => undefined),
|
|
133
176
|
readFile(sourcePath).catch(() => undefined),
|
|
134
177
|
]);
|
|
135
|
-
if (hostBytes === undefined || lisaBytes === undefined)
|
|
136
|
-
return
|
|
178
|
+
if (hostBytes === undefined || lisaBytes === undefined) {
|
|
179
|
+
return {
|
|
180
|
+
relativePath,
|
|
181
|
+
strategy: this.name,
|
|
182
|
+
action: "host-ahead",
|
|
183
|
+
note: describeUnclassifiable(relativePath, hostBytes === undefined),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
137
186
|
const verdict = classifyHostCopy(relativePath, hostBytes, lisaBytes, ledger);
|
|
138
187
|
if (mayRefreshLisaOwned(verdict))
|
|
139
188
|
return undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"copy-overwrite.js","sourceRoot":"","sources":["../../src/strategies/copy-overwrite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9E;;;;;;GAMG;AACH,MAAM,OAAO,qBAAqB;IACvB,IAAI,GAAG,gBAAyB,CAAC;IAE1C;;;;;;;OAOG;IACH,KAAK,CAAC,KAAK,CACT,UAAkB,EAClB,QAAgB,EAChB,YAAoB,EACpB,OAAwB;QAExB,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC;QACxD,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAElD,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;gBACnB,MAAM,eAAe,CAAC,QAAQ,CAAC,CAAC;gBAChC,MAAM,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;YACvC,CAAC;YACD,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;QACjE,CAAC;QAED,IAAI,MAAM,cAAc,CAAC,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC;YAC/C,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAClE,CAAC;QAED,4EAA4E;QAC5E,yEAAyE;QACzE,yEAAyE;QACzE,2CAA2C;QAC3C,EAAE;QACF,2EAA2E;QAC3E,yEAAyE;QACzE,0EAA0E;QAC1E,wEAAwE;QACxE,0EAA0E;QAC1E,wCAAwC;QACxC,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC,mBAAmB,CAC7B,UAAU,EACV,QAAQ,EACR,YAAY,EACZ,OAAO,CACR,CAAC;QACJ,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAClB,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;QACtE,CAAC;QAED,MAAM,eAAe,GAAG,MAAM,eAAe,CAC3C,YAAY,EACZ,UAAU,EACV,QAAQ,CACT,CAAC;QAEF,IAAI,eAAe,EAAE,CAAC;YACpB,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;YAC3B,MAAM,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;YACrC,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;QACtE,CAAC;QAED,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAClE,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACK,KAAK,CAAC,mBAAmB,CAC/B,UAAkB,EAClB,QAAgB,EAChB,YAAoB,EACpB,OAAwB;QAExB,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"copy-overwrite.js","sourceRoot":"","sources":["../../src/strategies/copy-overwrite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9E;;;;;;GAMG;AACH,MAAM,OAAO,qBAAqB;IACvB,IAAI,GAAG,gBAAyB,CAAC;IAE1C;;;;;;;OAOG;IACH,KAAK,CAAC,KAAK,CACT,UAAkB,EAClB,QAAgB,EAChB,YAAoB,EACpB,OAAwB;QAExB,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC;QACxD,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAElD,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;gBACnB,MAAM,eAAe,CAAC,QAAQ,CAAC,CAAC;gBAChC,MAAM,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;YACvC,CAAC;YACD,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;QACjE,CAAC;QAED,IAAI,MAAM,cAAc,CAAC,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC;YAC/C,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAClE,CAAC;QAED,yEAAyE;QACzE,qCAAqC;QACrC,EAAE;QACF,0EAA0E;QAC1E,4EAA4E;QAC5E,2EAA2E;QAC3E,yEAAyE;QACzE,uEAAuE;QACvE,oEAAoE;QACpE,uEAAuE;QACvE,2EAA2E;QAC3E,0EAA0E;QAC1E,0BAA0B;QAC1B,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,qBAAqB,CAChD,UAAU,EACV,QAAQ,EACR,YAAY,EACZ,OAAO,CACR,CAAC;QACF,IAAI,SAAS,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAE9C,4EAA4E;QAC5E,yEAAyE;QACzE,yEAAyE;QACzE,2CAA2C;QAC3C,EAAE;QACF,2EAA2E;QAC3E,yEAAyE;QACzE,0EAA0E;QAC1E,wEAAwE;QACxE,0EAA0E;QAC1E,wCAAwC;QACxC,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC,mBAAmB,CAC7B,UAAU,EACV,QAAQ,EACR,YAAY,EACZ,OAAO,CACR,CAAC;QACJ,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAClB,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;QACtE,CAAC;QAED,MAAM,eAAe,GAAG,MAAM,eAAe,CAC3C,YAAY,EACZ,UAAU,EACV,QAAQ,CACT,CAAC;QAEF,IAAI,eAAe,EAAE,CAAC;YACpB,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;YAC3B,MAAM,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;YACrC,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;QACtE,CAAC;QAED,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAClE,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACK,KAAK,CAAC,mBAAmB,CAC/B,UAAkB,EAClB,QAAgB,EAChB,YAAoB,EACpB,OAAwB;QAExB,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;QAEvC,IACE,CAAC,mBAAmB,CAAC,YAAY,CAAC;YAClC,CAAC,kBAAkB,CAAC,YAAY,EAAE,MAAM,CAAC,gBAAgB,CAAC,EAC1D,CAAC;YACD,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;QAChE,CAAC;QAED,wEAAwE;QACxE,mBAAmB;QACnB,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACnB,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;YAC3B,MAAM,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QACvC,CAAC;QACD,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IACtE,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACK,KAAK,CAAC,qBAAqB,CACjC,UAAkB,EAClB,QAAgB,EAChB,YAAoB,EACpB,OAAwB;QAExB,IAAI,CAAC,mBAAmB,CAAC,YAAY,CAAC;YAAE,OAAO,SAAS,CAAC;QACzD,IAAI,kBAAkB,CAAC,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACtE,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,IAAI,CAAC,mBAAmB,CAC7B,UAAU,EACV,QAAQ,EACR,YAAY,EACZ,OAAO,CAAC,UAAU,CACnB,CAAC;IACJ,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACK,KAAK,CAAC,mBAAmB,CAC/B,UAAkB,EAClB,QAAgB,EAChB,YAAoB,EACpB,MAAmB;QAEnB,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAC/C,QAAQ,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;YACzC,QAAQ,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;SAC5C,CAAC,CAAC;QACH,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YACvD,OAAO;gBACL,YAAY;gBACZ,QAAQ,EAAE,IAAI,CAAC,IAAI;gBACnB,MAAM,EAAE,YAAY;gBACpB,IAAI,EAAE,sBAAsB,CAAC,YAAY,EAAE,SAAS,KAAK,SAAS,CAAC;aACpE,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAG,gBAAgB,CAC9B,YAAY,EACZ,SAAS,EACT,SAAS,EACT,MAAM,CACP,CAAC;QACF,IAAI,mBAAmB,CAAC,OAAO,CAAC;YAAE,OAAO,SAAS,CAAC;QAEnD,OAAO;YACL,YAAY;YACZ,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,MAAM,EAAE,YAAY;YACpB,IAAI,EAAE,iBAAiB,CAAC,YAAY,EAAE,OAAO,CAAC;SAC/C,CAAC;IACJ,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
"zod-validation-error": "^4.0.0"
|
|
142
142
|
},
|
|
143
143
|
"name": "@codyswann/lisa",
|
|
144
|
-
"version": "3.47.
|
|
144
|
+
"version": "3.47.8",
|
|
145
145
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
146
146
|
"main": "dist/index.js",
|
|
147
147
|
"exports": {
|
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
* file per loop under `.lisa/automations/runs/`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
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
|
|
111
|
+
const allEntries = [
|
|
99
112
|
...readResult.entries,
|
|
100
113
|
{ line: JSON.stringify(record), record },
|
|
101
|
-
]
|
|
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) {
|
|
@@ -19,6 +19,7 @@ import fs from "node:fs/promises";
|
|
|
19
19
|
import path from "node:path";
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
|
+
automationRunQuarantinePath,
|
|
22
23
|
automationRunRecordPath,
|
|
23
24
|
readAutomationRunRecords,
|
|
24
25
|
} from "./automation-run-record.mjs";
|
|
@@ -59,6 +60,8 @@ const NO_RECORDED_RUNS_LINE = "no recorded runs yet";
|
|
|
59
60
|
* readonly recoveryRequiredStreak: number
|
|
60
61
|
* readonly recoverySummaries: readonly string[]
|
|
61
62
|
* readonly skippedCorruptLines: number
|
|
63
|
+
* readonly recordsPath: string
|
|
64
|
+
* readonly quarantinePath: string
|
|
62
65
|
* }} AutomationRunDisplay
|
|
63
66
|
*/
|
|
64
67
|
|
|
@@ -75,9 +78,9 @@ const NO_RECORDED_RUNS_LINE = "no recorded runs yet";
|
|
|
75
78
|
export async function resolveAutomationRunDisplay(input) {
|
|
76
79
|
const projectRoot = path.resolve(input.projectRoot ?? process.cwd());
|
|
77
80
|
const runbook = await resolveRunbookLine(projectRoot, input.runbookPath);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
const recordsPath = automationRunRecordPath(projectRoot, input.loopId);
|
|
82
|
+
const { records, skippedCorruptLines } =
|
|
83
|
+
await readAutomationRunRecords(recordsPath);
|
|
81
84
|
|
|
82
85
|
const newestFirst = records.toReversed();
|
|
83
86
|
const latest = records.at(-1);
|
|
@@ -101,6 +104,34 @@ export async function resolveAutomationRunDisplay(input) {
|
|
|
101
104
|
recoveryRequiredStreak: trailingRecovery.length,
|
|
102
105
|
recoverySummaries: trailingRecovery.map(record => record.summary),
|
|
103
106
|
skippedCorruptLines,
|
|
107
|
+
recordsPath,
|
|
108
|
+
quarantinePath: automationRunQuarantinePath(projectRoot, input.loopId),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Report a ledger holding rows this module cannot read as drift.
|
|
114
|
+
*
|
|
115
|
+
* `skippedCorruptLines` was computed on every read, returned by both the read
|
|
116
|
+
* and the write path, and destructured by every caller — and printed by none
|
|
117
|
+
* of them. A counter nobody reads is not a control, and this one guarded the
|
|
118
|
+
* only evidence that a loop's own history had been damaged (#2578).
|
|
119
|
+
*
|
|
120
|
+
* `DRIFTED`, not `FAILING`: the ledger is local scheduler state, and rows it
|
|
121
|
+
* cannot read say nothing about whether the loop is doing its job. Escalating
|
|
122
|
+
* it to a loop failure would misattribute a storage problem to the automation
|
|
123
|
+
* and bury the real health signal underneath it.
|
|
124
|
+
* @param {AutomationRunDisplay | undefined} runDisplay
|
|
125
|
+
* @returns {{ readonly status: "DRIFTED", readonly summary: string, readonly remediation: string } | null}
|
|
126
|
+
*/
|
|
127
|
+
export function resolveLedgerIntegrityFinding(runDisplay) {
|
|
128
|
+
if (!runDisplay || runDisplay.skippedCorruptLines < 1) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
status: "DRIFTED",
|
|
133
|
+
summary: `${runDisplay.skippedCorruptLines} stored run record(s) in this loop's history cannot be read`,
|
|
134
|
+
remediation: `Open ${runDisplay.recordsPath} and look at the lines that are not valid run records. They are left exactly as written and are left out of the history above, so the outcomes shown are an incomplete picture. Nothing deletes them: each is copied to ${runDisplay.quarantinePath} before the history bound would evict it. Fix or remove those lines and this stops being reported.`,
|
|
104
135
|
};
|
|
105
136
|
}
|
|
106
137
|
|