@codyswann/lisa 2.219.1 → 2.220.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/learnings-document.d.ts +22 -0
- package/dist/core/learnings-document.d.ts.map +1 -0
- package/dist/core/learnings-document.js +106 -0
- package/dist/core/learnings-document.js.map +1 -0
- package/dist/core/learnings-writer.d.ts +1 -13
- package/dist/core/learnings-writer.d.ts.map +1 -1
- package/dist/core/learnings-writer.js +2 -70
- package/dist/core/learnings-writer.js.map +1 -1
- package/package.json +2 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- 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-cursor/.claude-plugin/plugin.json +1 -1
- 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/scripts/check-learnings-budget.ts +219 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Pure parsing and rendering for the canonical project-learnings document. */
|
|
2
|
+
import { type LearningEntry } from "./learnings-contract.js";
|
|
3
|
+
/**
|
|
4
|
+
* Render the complete file in one stable, machine-readable Markdown form.
|
|
5
|
+
* @param entries - Validated entries to serialize
|
|
6
|
+
* @returns Canonical Markdown plus JSONL document
|
|
7
|
+
*/
|
|
8
|
+
export declare function renderLearningsFile(entries: readonly LearningEntry[]): string;
|
|
9
|
+
/**
|
|
10
|
+
* Parse and revalidate an existing file before it participates in a write.
|
|
11
|
+
* @param content - Existing learnings document
|
|
12
|
+
* @returns Revalidated entries from the document
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseLearningsFile(content: string): LearningEntry[];
|
|
15
|
+
/**
|
|
16
|
+
* Enforce the shared entry-count and model-agnostic token upper bounds.
|
|
17
|
+
* @param content - Canonical document
|
|
18
|
+
* @param entryCount - Number of entries in the document
|
|
19
|
+
* @param context - Error-message prefix
|
|
20
|
+
*/
|
|
21
|
+
export declare function assertDocumentBudget(content: string, entryCount: number, context: string): void;
|
|
22
|
+
//# sourceMappingURL=learnings-document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"learnings-document.d.ts","sourceRoot":"","sources":["../../src/core/learnings-document.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,yBAAyB,CAAC;AAWjC;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,GAAG,MAAM,CAG7E;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,EAAE,CAsBnE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACd,IAAI,CAYN"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Pure parsing and rendering for the canonical project-learnings document. */
|
|
2
|
+
import { LEARNINGS_CONTRACT, estimateLearningTokens, } from "./learnings-contract.js";
|
|
3
|
+
import { validateLearningEntry } from "./learnings-entry.js";
|
|
4
|
+
const FILE_HEADER = `# Project Learnings
|
|
5
|
+
|
|
6
|
+
<!-- lisa-learnings-contract:v${LEARNINGS_CONTRACT.version} -->
|
|
7
|
+
|
|
8
|
+
`;
|
|
9
|
+
const JSON_FENCE_START = "```jsonl\n";
|
|
10
|
+
const JSON_FENCE_END = "\n```\n";
|
|
11
|
+
/**
|
|
12
|
+
* Render the complete file in one stable, machine-readable Markdown form.
|
|
13
|
+
* @param entries - Validated entries to serialize
|
|
14
|
+
* @returns Canonical Markdown plus JSONL document
|
|
15
|
+
*/
|
|
16
|
+
export function renderLearningsFile(entries) {
|
|
17
|
+
const lines = entries.map(entry => JSON.stringify(entry)).join("\n");
|
|
18
|
+
return `${FILE_HEADER}${JSON_FENCE_START}${lines}${JSON_FENCE_END}`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parse and revalidate an existing file before it participates in a write.
|
|
22
|
+
* @param content - Existing learnings document
|
|
23
|
+
* @returns Revalidated entries from the document
|
|
24
|
+
*/
|
|
25
|
+
export function parseLearningsFile(content) {
|
|
26
|
+
if (estimateLearningTokens(content) > LEARNINGS_CONTRACT.maxTokens) {
|
|
27
|
+
throw new Error(`Project learnings payload exceeds maxTokens ${LEARNINGS_CONTRACT.maxTokens}`);
|
|
28
|
+
}
|
|
29
|
+
if (!content.startsWith(`${FILE_HEADER}${JSON_FENCE_START}`) ||
|
|
30
|
+
!content.endsWith(JSON_FENCE_END)) {
|
|
31
|
+
throw new Error("Invalid project learnings file format");
|
|
32
|
+
}
|
|
33
|
+
const jsonStart = FILE_HEADER.length + JSON_FENCE_START.length;
|
|
34
|
+
const jsonEnd = content.length - JSON_FENCE_END.length;
|
|
35
|
+
const entries = parseJsonLines(content.slice(jsonStart, jsonEnd)).map(validateParsedLearningEntry);
|
|
36
|
+
if (new Set(entries.map(entry => entry.id)).size !== entries.length) {
|
|
37
|
+
throw new Error("Invalid project learnings payload: duplicate ids");
|
|
38
|
+
}
|
|
39
|
+
assertDocumentBudget(content, entries.length, "Project learnings payload");
|
|
40
|
+
return entries;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Enforce the shared entry-count and model-agnostic token upper bounds.
|
|
44
|
+
* @param content - Canonical document
|
|
45
|
+
* @param entryCount - Number of entries in the document
|
|
46
|
+
* @param context - Error-message prefix
|
|
47
|
+
*/
|
|
48
|
+
export function assertDocumentBudget(content, entryCount, context) {
|
|
49
|
+
if (entryCount > LEARNINGS_CONTRACT.maxEntries) {
|
|
50
|
+
throw new Error(`${context} exceeds maxEntries: measured ${entryCount}, allowed ${LEARNINGS_CONTRACT.maxEntries}`);
|
|
51
|
+
}
|
|
52
|
+
const estimatedTokens = estimateLearningTokens(content);
|
|
53
|
+
if (estimatedTokens > LEARNINGS_CONTRACT.maxTokens) {
|
|
54
|
+
throw new Error(`${context} exceeds maxTokens ${LEARNINGS_CONTRACT.maxTokens} (measured ${estimatedTokens})`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Add a stable entry identifier to validation failures.
|
|
59
|
+
* @param candidate - Parsed JSONL value
|
|
60
|
+
* @param index - Zero-based JSONL entry index
|
|
61
|
+
* @returns Validated learning entry
|
|
62
|
+
*/
|
|
63
|
+
function validateParsedLearningEntry(candidate, index) {
|
|
64
|
+
try {
|
|
65
|
+
return validateLearningEntry(candidate);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const id = readDiagnosticEntryId(candidate);
|
|
69
|
+
const label = id === undefined ? `entry ${index + 1}` : `entry ${JSON.stringify(id)}`;
|
|
70
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
71
|
+
throw new Error(`Invalid learning ${label}: ${detail}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Read a parsed entry id only when it is an inert own data property.
|
|
76
|
+
* @param candidate - Parsed JSONL value
|
|
77
|
+
* @returns Safe diagnostic id, when present
|
|
78
|
+
*/
|
|
79
|
+
function readDiagnosticEntryId(candidate) {
|
|
80
|
+
if (candidate === null || typeof candidate !== "object") {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, "id");
|
|
84
|
+
return descriptor !== undefined &&
|
|
85
|
+
"value" in descriptor &&
|
|
86
|
+
typeof descriptor.value === "string"
|
|
87
|
+
? descriptor.value
|
|
88
|
+
: undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Parse the strict JSONL payload with a stable, user-facing error.
|
|
92
|
+
* @param payload - JSONL payload
|
|
93
|
+
* @returns Parsed unknown entry values
|
|
94
|
+
*/
|
|
95
|
+
function parseJsonLines(payload) {
|
|
96
|
+
if (payload === "") {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
return payload.split("\n").map(line => JSON.parse(line));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
throw new Error("Invalid project learnings JSONL payload");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=learnings-document.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"learnings-document.js","sourceRoot":"","sources":["../../src/core/learnings-document.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type LearningEntry } from "./learnings-contract.js";
|
|
2
1
|
/**
|
|
3
2
|
* Persist one already-selected learning without generating or truncating it.
|
|
4
3
|
* @param projectRoot - Absolute path to the host project root
|
|
@@ -6,17 +5,6 @@ import { type LearningEntry } from "./learnings-contract.js";
|
|
|
6
5
|
* @returns Absolute path to the persisted learnings file
|
|
7
6
|
*/
|
|
8
7
|
export declare function persistLearningEntry(projectRoot: string, candidate: unknown): Promise<string>;
|
|
9
|
-
|
|
10
|
-
* Render the complete file in one stable, machine-readable Markdown form.
|
|
11
|
-
* @param entries - Validated entries to serialize
|
|
12
|
-
* @returns Canonical Markdown plus JSONL document
|
|
13
|
-
*/
|
|
14
|
-
export declare function renderLearningsFile(entries: readonly LearningEntry[]): string;
|
|
15
|
-
/**
|
|
16
|
-
* Parse and revalidate an existing file before it participates in a write.
|
|
17
|
-
* @param content - Existing learnings document
|
|
18
|
-
* @returns Revalidated entries from the document
|
|
19
|
-
*/
|
|
20
|
-
export declare function parseLearningsFile(content: string): LearningEntry[];
|
|
8
|
+
export { parseLearningsFile, renderLearningsFile, } from "./learnings-document.js";
|
|
21
9
|
export { validateLearningEntry } from "./learnings-entry.js";
|
|
22
10
|
//# sourceMappingURL=learnings-writer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"learnings-writer.d.ts","sourceRoot":"","sources":["../../src/core/learnings-writer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"learnings-writer.d.ts","sourceRoot":"","sources":["../../src/core/learnings-writer.ts"],"names":[],"mappings":"AAsBA;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,OAAO,GACjB,OAAO,CAAC,MAAM,CAAC,CA6BjB;AAED,OAAO,EACL,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -2,18 +2,11 @@
|
|
|
2
2
|
import * as fse from "fs-extra";
|
|
3
3
|
import { rename, writeFile } from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { assertDocumentBudget, parseLearningsFile, renderLearningsFile, } from "./learnings-document.js";
|
|
6
6
|
import { validateLearningEntry } from "./learnings-entry.js";
|
|
7
7
|
import { assertSafeLearningParents, readExistingLearnings, resolveSafeLearningTarget, } from "./learnings-file-safety.js";
|
|
8
8
|
import { withLearningTargetLock } from "./learnings-lock.js";
|
|
9
9
|
import { readProjectConfig, resolveProjectLearningsFile, } from "./project-config.js";
|
|
10
|
-
const FILE_HEADER = `# Project Learnings
|
|
11
|
-
|
|
12
|
-
<!-- lisa-learnings-contract:v${LEARNINGS_CONTRACT.version} -->
|
|
13
|
-
|
|
14
|
-
`;
|
|
15
|
-
const JSON_FENCE_START = "```jsonl\n";
|
|
16
|
-
const JSON_FENCE_END = "\n```\n";
|
|
17
10
|
/**
|
|
18
11
|
* Persist one already-selected learning without generating or truncating it.
|
|
19
12
|
* @param projectRoot - Absolute path to the host project root
|
|
@@ -47,37 +40,7 @@ export async function persistLearningEntry(projectRoot, candidate) {
|
|
|
47
40
|
return target;
|
|
48
41
|
});
|
|
49
42
|
}
|
|
50
|
-
|
|
51
|
-
* Render the complete file in one stable, machine-readable Markdown form.
|
|
52
|
-
* @param entries - Validated entries to serialize
|
|
53
|
-
* @returns Canonical Markdown plus JSONL document
|
|
54
|
-
*/
|
|
55
|
-
export function renderLearningsFile(entries) {
|
|
56
|
-
const lines = entries.map(entry => JSON.stringify(entry)).join("\n");
|
|
57
|
-
return `${FILE_HEADER}${JSON_FENCE_START}${lines}${JSON_FENCE_END}`;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Parse and revalidate an existing file before it participates in a write.
|
|
61
|
-
* @param content - Existing learnings document
|
|
62
|
-
* @returns Revalidated entries from the document
|
|
63
|
-
*/
|
|
64
|
-
export function parseLearningsFile(content) {
|
|
65
|
-
if (estimateLearningTokens(content) > LEARNINGS_CONTRACT.maxTokens) {
|
|
66
|
-
throw new Error(`Project learnings payload exceeds maxTokens ${LEARNINGS_CONTRACT.maxTokens}`);
|
|
67
|
-
}
|
|
68
|
-
if (!content.startsWith(`${FILE_HEADER}${JSON_FENCE_START}`) ||
|
|
69
|
-
!content.endsWith(JSON_FENCE_END)) {
|
|
70
|
-
throw new Error("Invalid project learnings file format");
|
|
71
|
-
}
|
|
72
|
-
const jsonStart = FILE_HEADER.length + JSON_FENCE_START.length;
|
|
73
|
-
const jsonEnd = content.length - JSON_FENCE_END.length;
|
|
74
|
-
const entries = parseJsonLines(content.slice(jsonStart, jsonEnd)).map(validateLearningEntry);
|
|
75
|
-
if (new Set(entries.map(entry => entry.id)).size !== entries.length) {
|
|
76
|
-
throw new Error("Invalid project learnings payload: duplicate ids");
|
|
77
|
-
}
|
|
78
|
-
assertDocumentBudget(content, entries.length, "Project learnings payload");
|
|
79
|
-
return entries;
|
|
80
|
-
}
|
|
43
|
+
export { parseLearningsFile, renderLearningsFile, } from "./learnings-document.js";
|
|
81
44
|
export { validateLearningEntry } from "./learnings-entry.js";
|
|
82
45
|
/**
|
|
83
46
|
* Build and budget-check the next canonical document.
|
|
@@ -94,35 +57,4 @@ function buildNextDocument(entries, entry) {
|
|
|
94
57
|
assertDocumentBudget(rendered, nextEntries.length, "Learnings file");
|
|
95
58
|
return rendered;
|
|
96
59
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Enforce the shared entry-count and model-agnostic token upper bounds.
|
|
99
|
-
* @param content - Canonical document
|
|
100
|
-
* @param entryCount - Number of entries in the document
|
|
101
|
-
* @param context - Error-message prefix
|
|
102
|
-
*/
|
|
103
|
-
function assertDocumentBudget(content, entryCount, context) {
|
|
104
|
-
if (entryCount > LEARNINGS_CONTRACT.maxEntries) {
|
|
105
|
-
throw new Error(`${context} exceeds maxEntries ${LEARNINGS_CONTRACT.maxEntries}`);
|
|
106
|
-
}
|
|
107
|
-
const estimatedTokens = estimateLearningTokens(content);
|
|
108
|
-
if (estimatedTokens > LEARNINGS_CONTRACT.maxTokens) {
|
|
109
|
-
throw new Error(`${context} exceeds maxTokens ${LEARNINGS_CONTRACT.maxTokens} (measured ${estimatedTokens})`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Parse the strict JSONL payload with a stable, user-facing error.
|
|
114
|
-
* @param payload - JSONL payload
|
|
115
|
-
* @returns Parsed unknown entry values
|
|
116
|
-
*/
|
|
117
|
-
function parseJsonLines(payload) {
|
|
118
|
-
if (payload === "") {
|
|
119
|
-
return [];
|
|
120
|
-
}
|
|
121
|
-
try {
|
|
122
|
-
return payload.split("\n").map(line => JSON.parse(line));
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
throw new Error("Invalid project learnings JSONL payload");
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
60
|
//# sourceMappingURL=learnings-writer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"learnings-writer.js","sourceRoot":"","sources":["../../src/core/learnings-writer.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"learnings-writer.js","sourceRoot":"","sources":["../../src/core/learnings-writer.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"lisa:commit-and-pr:local": "bash scripts/lisa-commit-and-pr-local.sh",
|
|
32
32
|
"prepublishOnly": "$npm_execpath run build",
|
|
33
33
|
"check:plugins": "bash scripts/check-plugins-sync.sh",
|
|
34
|
+
"check:learnings-budget": "bun scripts/check-learnings-budget.ts",
|
|
34
35
|
"check:rules-pairing": "bash scripts/check-rules-pairing.sh",
|
|
35
36
|
"check:workflow-inputs": "node scripts/detect-stale-workflow-inputs.mjs --project . --project typescript/create-only --project rails/create-only --json"
|
|
36
37
|
},
|
|
@@ -101,7 +102,7 @@
|
|
|
101
102
|
"form-data": ">=4.0.6"
|
|
102
103
|
},
|
|
103
104
|
"name": "@codyswann/lisa",
|
|
104
|
-
"version": "2.
|
|
105
|
+
"version": "2.220.0",
|
|
105
106
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
106
107
|
"main": "dist/index.js",
|
|
107
108
|
"exports": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.220.0",
|
|
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": "2.
|
|
3
|
+
"version": "2.220.0",
|
|
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": "2.
|
|
3
|
+
"version": "2.220.0",
|
|
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": "2.
|
|
3
|
+
"version": "2.220.0",
|
|
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": "2.
|
|
3
|
+
"version": "2.220.0",
|
|
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"
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/** CI gate for the canonical project learnings document and its hard budgets. */
|
|
2
|
+
import { constants, existsSync } from "node:fs";
|
|
3
|
+
import { open } from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import type * as LearningsModule from "@codyswann/lisa/learnings";
|
|
7
|
+
|
|
8
|
+
type LearningsCheckerModule = Pick<
|
|
9
|
+
typeof LearningsModule,
|
|
10
|
+
| "LEARNINGS_CONTRACT"
|
|
11
|
+
| "estimateLearningTokens"
|
|
12
|
+
| "parseLearningsFile"
|
|
13
|
+
| "renderLearningsFile"
|
|
14
|
+
| "validateLearningEntry"
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
const DEFAULT_LEARNINGS_FILE = path.resolve(
|
|
18
|
+
import.meta.dir,
|
|
19
|
+
"..",
|
|
20
|
+
"all",
|
|
21
|
+
"create-only",
|
|
22
|
+
".claude",
|
|
23
|
+
"rules",
|
|
24
|
+
"PROJECT_LEARNINGS.md"
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
/** Run the package-facing checker with zero or one explicit file path. */
|
|
28
|
+
async function main(): Promise<void> {
|
|
29
|
+
const arguments_ = process.argv.slice(2);
|
|
30
|
+
if (arguments_.length > 1) {
|
|
31
|
+
fail("Usage: bun run check:learnings-budget -- [PROJECT_LEARNINGS.md]");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const file =
|
|
35
|
+
arguments_.length === 0
|
|
36
|
+
? DEFAULT_LEARNINGS_FILE
|
|
37
|
+
: path.resolve(process.cwd(), arguments_[0] as string);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const {
|
|
41
|
+
LEARNINGS_CONTRACT,
|
|
42
|
+
estimateLearningTokens,
|
|
43
|
+
parseLearningsFile,
|
|
44
|
+
renderLearningsFile,
|
|
45
|
+
validateLearningEntry,
|
|
46
|
+
} = await loadLearningsModule();
|
|
47
|
+
const content = await readBoundedRegularFile(
|
|
48
|
+
file,
|
|
49
|
+
LEARNINGS_CONTRACT.maxTokens
|
|
50
|
+
);
|
|
51
|
+
const measuredTokens = estimateLearningTokens(content);
|
|
52
|
+
if (measuredTokens > LEARNINGS_CONTRACT.maxTokens) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`maxTokens exceeded: measured ${measuredTokens}, allowed ${LEARNINGS_CONTRACT.maxTokens}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const entries = parseLearningsFile(content);
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
validateLearningEntry(entry);
|
|
61
|
+
}
|
|
62
|
+
if (renderLearningsFile(entries) !== content) {
|
|
63
|
+
throw new Error("non-canonical project learnings format");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(
|
|
67
|
+
`${formatDiagnosticPath(file)}: learnings budget passed (${entries.length}/${LEARNINGS_CONTRACT.maxEntries} entries, ${measuredTokens}/${LEARNINGS_CONTRACT.maxTokens} maxTokens)`
|
|
68
|
+
);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
fail(`${formatDiagnosticPath(file)}: ${formatErrorDetail(error)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Render a caught failure without allowing filesystem paths or control bytes
|
|
76
|
+
* embedded in an Error message to forge additional terminal/CI output.
|
|
77
|
+
* @param error - Unknown thrown failure
|
|
78
|
+
* @returns Stable, single-line diagnostic detail
|
|
79
|
+
*/
|
|
80
|
+
function formatErrorDetail(error: unknown): string {
|
|
81
|
+
if (error !== null && typeof error === "object") {
|
|
82
|
+
const code = readOwnString(error, "code");
|
|
83
|
+
if (code !== undefined && /^[A-Z][A-Z0-9_]*$/u.test(code)) {
|
|
84
|
+
const syscall = readOwnString(error, "syscall");
|
|
85
|
+
return syscall === undefined
|
|
86
|
+
? `filesystem error ${code}`
|
|
87
|
+
: `filesystem error ${code} during ${escapeDiagnosticText(syscall)}`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
91
|
+
return escapeDiagnosticText(message);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Read one inert own string property without invoking an accessor. */
|
|
95
|
+
function readOwnString(candidate: object, key: string): string | undefined {
|
|
96
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
|
|
97
|
+
return descriptor !== undefined &&
|
|
98
|
+
"value" in descriptor &&
|
|
99
|
+
typeof descriptor.value === "string"
|
|
100
|
+
? descriptor.value
|
|
101
|
+
: undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Escape terminal controls while retaining ordinary diagnostic wording. */
|
|
105
|
+
function escapeDiagnosticText(value: string): string {
|
|
106
|
+
const jsonBody = JSON.stringify(value).slice(1, -1);
|
|
107
|
+
return jsonBody.replace(
|
|
108
|
+
/[\u007F-\u009F\u2028\u2029]/gu,
|
|
109
|
+
character => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Quote one path after applying terminal-safe JSON-style escaping. */
|
|
114
|
+
function formatDiagnosticPath(file: string): string {
|
|
115
|
+
return `"${escapeDiagnosticText(file)}"`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Read at most one byte beyond the hard budget from one verified regular-file
|
|
120
|
+
* handle. Non-blocking open prevents a FIFO path from stalling the CI gate.
|
|
121
|
+
* @param file - Absolute candidate learnings path
|
|
122
|
+
* @param maximumBytes - Shared executable maxTokens byte ceiling
|
|
123
|
+
* @returns Strictly decoded UTF-8 content within the byte ceiling
|
|
124
|
+
*/
|
|
125
|
+
async function readBoundedRegularFile(
|
|
126
|
+
file: string,
|
|
127
|
+
maximumBytes: number
|
|
128
|
+
): Promise<string> {
|
|
129
|
+
const handle = await open(file, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
130
|
+
try {
|
|
131
|
+
const before = await handle.stat({ bigint: true });
|
|
132
|
+
if (!before.isFile()) {
|
|
133
|
+
throw new Error("unsafe input: expected a regular file");
|
|
134
|
+
}
|
|
135
|
+
if (before.size > BigInt(maximumBytes)) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`maxTokens exceeded: measured ${before.size}, allowed ${maximumBytes}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const buffer = Buffer.allocUnsafe(maximumBytes + 1);
|
|
142
|
+
let bytesRead = 0;
|
|
143
|
+
while (bytesRead < buffer.length) {
|
|
144
|
+
const result = await handle.read(
|
|
145
|
+
buffer,
|
|
146
|
+
bytesRead,
|
|
147
|
+
buffer.length - bytesRead,
|
|
148
|
+
null
|
|
149
|
+
);
|
|
150
|
+
if (result.bytesRead === 0) {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
bytesRead += result.bytesRead;
|
|
154
|
+
}
|
|
155
|
+
if (bytesRead > maximumBytes) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`maxTokens exceeded: measured at least ${bytesRead}, allowed ${maximumBytes}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const after = await handle.stat({ bigint: true });
|
|
162
|
+
if (
|
|
163
|
+
after.dev !== before.dev ||
|
|
164
|
+
after.ino !== before.ino ||
|
|
165
|
+
after.size !== before.size ||
|
|
166
|
+
after.mtimeNs !== before.mtimeNs ||
|
|
167
|
+
after.ctimeNs !== before.ctimeNs
|
|
168
|
+
) {
|
|
169
|
+
throw new Error("unsafe input: file changed during bounded read");
|
|
170
|
+
}
|
|
171
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(
|
|
172
|
+
buffer.subarray(0, bytesRead)
|
|
173
|
+
);
|
|
174
|
+
} finally {
|
|
175
|
+
await handle.close();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Load the executable contract from current source in a checkout or compiled
|
|
181
|
+
* output in an npm package. The `.js` source specifier keeps Bun development
|
|
182
|
+
* runs aligned with TypeScript's NodeNext resolution while publishing no
|
|
183
|
+
* runtime dependency on the excluded `src` tree.
|
|
184
|
+
* @returns Canonical executable learnings module
|
|
185
|
+
*/
|
|
186
|
+
async function loadLearningsModule(): Promise<LearningsCheckerModule> {
|
|
187
|
+
const packageRoot = path.resolve(import.meta.dir, "..");
|
|
188
|
+
const sourceTypescript = path.join(
|
|
189
|
+
packageRoot,
|
|
190
|
+
"src",
|
|
191
|
+
"core",
|
|
192
|
+
"learnings.ts"
|
|
193
|
+
);
|
|
194
|
+
const runtimeRoot = path.join(
|
|
195
|
+
packageRoot,
|
|
196
|
+
existsSync(sourceTypescript) ? "src" : "dist",
|
|
197
|
+
"core"
|
|
198
|
+
);
|
|
199
|
+
const [contract, document, entry] = await Promise.all([
|
|
200
|
+
import(pathToFileURL(path.join(runtimeRoot, "learnings-contract.js")).href),
|
|
201
|
+
import(pathToFileURL(path.join(runtimeRoot, "learnings-document.js")).href),
|
|
202
|
+
import(pathToFileURL(path.join(runtimeRoot, "learnings-entry.js")).href),
|
|
203
|
+
]);
|
|
204
|
+
return {
|
|
205
|
+
LEARNINGS_CONTRACT: contract.LEARNINGS_CONTRACT,
|
|
206
|
+
estimateLearningTokens: contract.estimateLearningTokens,
|
|
207
|
+
parseLearningsFile: document.parseLearningsFile,
|
|
208
|
+
renderLearningsFile: document.renderLearningsFile,
|
|
209
|
+
validateLearningEntry: entry.validateLearningEntry,
|
|
210
|
+
} as LearningsCheckerModule;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Print one deterministic failure diagnostic and exit non-zero. */
|
|
214
|
+
function fail(message: string): never {
|
|
215
|
+
console.error(`check:learnings-budget: ${message}`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
await main();
|