@codyswann/lisa 2.253.0 → 2.254.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/package.json +1 -1
  2. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  3. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  4. package/plugins/lisa/rules/eager/automation-runbook-contract.md +4 -1
  5. package/plugins/lisa/rules/reference/automation-runbook-contract.md +16 -7
  6. package/plugins/lisa/scripts/automation-run-record.mjs +262 -0
  7. package/plugins/lisa-agy/plugin.json +1 -1
  8. package/plugins/lisa-agy/scripts/automation-run-record.mjs +262 -0
  9. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  11. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  12. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-copilot/rules/eager/automation-runbook-contract.md +4 -1
  16. package/plugins/lisa-copilot/rules/reference/automation-runbook-contract.md +16 -7
  17. package/plugins/lisa-copilot/scripts/automation-run-record.mjs +262 -0
  18. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cursor/rules/automation-runbook-contract-reference.mdc +16 -7
  20. package/plugins/lisa-cursor/rules/automation-runbook-contract.mdc +4 -1
  21. package/plugins/lisa-cursor/scripts/automation-run-record.mjs +262 -0
  22. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  24. package/plugins/lisa-expo-agy/plugin.json +1 -1
  25. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  35. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  40. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  45. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails-agy/plugin.json +1 -1
  50. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  55. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  60. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/src/base/rules/eager/automation-runbook-contract.md +4 -1
  63. package/plugins/src/base/rules/reference/automation-runbook-contract.md +16 -7
  64. package/plugins/src/base/scripts/automation-run-record.mjs +262 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.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.253.0",
3
+ "version": "2.254.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.253.0",
3
+ "version": "2.254.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.253.0",
3
+ "version": "2.254.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.253.0",
3
+ "version": "2.254.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-phaser",
3
- "version": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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": "2.253.0",
3
+ "version": "2.254.0",
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"
@@ -50,7 +50,10 @@ loop itself is broken, not that a work item was blocked.
50
50
 
51
51
  Every run — including a trivial early termination — ends by naming exactly one run outcome plus a
52
52
  one-line operator summary of what happened, recorded where the status surface can read it —
53
- there is no silent exit. Silence and health must never look identical to an operator.
53
+ there is no silent exit. The shared local substrate is
54
+ `plugins/src/base/scripts/automation-run-record.mjs`, which writes bounded JSONL records under
55
+ `.lisa/automations/runs/<loop-id>.jsonl`. Silence and health must never look identical to an
56
+ operator.
54
57
 
55
58
  ## Never block, always degrade
56
59
 
@@ -121,8 +121,17 @@ there is no silent exit for an operator to misread as health. A run that ends wi
121
121
  recorded outcome is indistinguishable from a crashed scheduler, which is exactly the ambiguity this
122
122
  contract removes. This generalizes `lisa-improve-harness`'s result-record discipline — every
123
123
  terminal step posts its result record before stopping, so there is no silent exit — from one flow to
124
- every registered loop. (The run-record substrate that stores outcomes and summaries is named here
125
- only; it ships with that ticket (#1797), do not assume its file is present in this branch.)
124
+ every registered loop.
125
+
126
+ The local storage substrate is `plugins/src/base/scripts/automation-run-record.mjs`. It writes one
127
+ bounded JSONL file per loop at `.lisa/automations/runs/<loop-id>.jsonl`, with one object per line:
128
+ `{ ts, loop_id, outcome, summary, runbook, refs[], run_id }`. The helper rejects any outcome outside
129
+ the closed six-value vocabulary, requires a non-empty operator-readable summary, suppresses duplicate
130
+ re-appends for the same `run_id`, skips corrupt/truncated lines on read, and trims to the newest
131
+ configured records on write. The default bound is 50 records per loop, overridable via
132
+ `.lisa.config.json` / `.lisa.config.local.json` `automations.runHistory.maxEntries`. These records
133
+ are local scheduler observations, not project knowledge, so `.lisa/automations/runs/` is ignored;
134
+ the runbooks under `.lisa/automations/*.runbook.md` remain checked-in knowledge.
126
135
 
127
136
  ## A run outcome is not a work-item lifecycle terminal state
128
137
 
@@ -209,8 +218,8 @@ packet above. A degraded run never blocks other work and never leaves the run un
209
218
 
210
219
  ## Citing adjacent work
211
220
 
212
- Cite these by name; each ships with its own ticket, do not assume its file is present in this
213
- branch: the runbook scaffolding that instantiates this template (#1796), the run-record substrate
214
- that stores outcomes and summaries (#1797), and the evidence packet defined by the evidence PRD
215
- (#1738) — named here, with no evidence format defined by this contract. Rejection memory
216
- extends `rejection-detection`; this contract neither restates nor overrides it.
221
+ Cite these by name. The run-record substrate ships with that ticket (#1797), do not assume its file
222
+ is present in older branches. The evidence packet ships with that ticket, do not assume its file is present in this branch. Other adjacent artifacts keep their own lifecycle: the runbook scaffolding
223
+ that instantiates this template (#1796), and the evidence packet defined by the evidence PRD (#1738)
224
+ — named here, with no evidence format defined by this contract. Rejection memory extends
225
+ `rejection-detection`; this contract neither restates nor overrides it.
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Dependency-free run outcome recorder for registered Lisa automation loops.
4
+ *
5
+ * The file is local scheduler state, not project knowledge: one bounded JSONL
6
+ * file per loop under `.lisa/automations/runs/`.
7
+ */
8
+
9
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
10
+ import path from "node:path";
11
+
12
+ export const AUTOMATION_RUN_OUTCOMES = [
13
+ "nothing-needed",
14
+ "candidate-proposed",
15
+ "change-proved",
16
+ "approval-requested",
17
+ "recovery-required",
18
+ "policy-obsolete",
19
+ ];
20
+
21
+ export const DEFAULT_AUTOMATION_RUN_HISTORY_MAX_ENTRIES = 50;
22
+
23
+ /**
24
+ * @typedef {{
25
+ * readonly ts: string
26
+ * readonly loop_id: string
27
+ * readonly outcome: string
28
+ * readonly summary: string
29
+ * readonly runbook: string
30
+ * readonly refs: readonly string[]
31
+ * readonly run_id: string
32
+ * }} AutomationRunRecord
33
+ *
34
+ * @typedef {{
35
+ * readonly projectRoot?: string
36
+ * readonly loopId: string
37
+ * readonly outcome: string
38
+ * readonly summary: string
39
+ * readonly runbook: string
40
+ * readonly refs?: readonly string[]
41
+ * readonly runId?: string
42
+ * readonly ts?: string | Date
43
+ * readonly maxEntries?: number
44
+ * }} RecordAutomationRunInput
45
+ */
46
+
47
+ /**
48
+ * Record one automation-loop outcome, suppressing duplicate re-appends for the
49
+ * same `run_id` and trimming the file to the configured history bound.
50
+ *
51
+ * @param {RecordAutomationRunInput} input
52
+ * @returns {Promise<{ readonly path: string, readonly record: AutomationRunRecord, readonly records: readonly AutomationRunRecord[], readonly appended: boolean, readonly skippedCorruptLines: number, readonly maxEntries: number }>}
53
+ */
54
+ export async function recordAutomationRun(input) {
55
+ const projectRoot = path.resolve(input.projectRoot ?? process.cwd());
56
+ const maxEntries =
57
+ input.maxEntries ??
58
+ (await resolveAutomationRunHistoryMaxEntries(projectRoot));
59
+ const record = buildAutomationRunRecord(input);
60
+ const filePath = automationRunRecordPath(projectRoot, record.loop_id);
61
+ const readResult = await readAutomationRunRecords(filePath);
62
+
63
+ if (readResult.records.some(existing => existing.run_id === record.run_id)) {
64
+ return {
65
+ path: filePath,
66
+ record,
67
+ records: readResult.records,
68
+ appended: false,
69
+ skippedCorruptLines: readResult.skippedCorruptLines,
70
+ maxEntries,
71
+ };
72
+ }
73
+
74
+ const nextRecords = [...readResult.records, record].slice(-maxEntries);
75
+ await mkdir(path.dirname(filePath), { recursive: true });
76
+ await writeJsonlAtomically(filePath, nextRecords);
77
+
78
+ return {
79
+ path: filePath,
80
+ record,
81
+ records: nextRecords,
82
+ appended: true,
83
+ skippedCorruptLines: readResult.skippedCorruptLines,
84
+ maxEntries,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * @param {string} projectRoot
90
+ * @returns {Promise<number>}
91
+ */
92
+ export async function resolveAutomationRunHistoryMaxEntries(projectRoot) {
93
+ const globalConfig = await readJsonIfPresent(
94
+ path.join(projectRoot, ".lisa.config.json")
95
+ );
96
+ const localConfig = await readJsonIfPresent(
97
+ path.join(projectRoot, ".lisa.config.local.json")
98
+ );
99
+ const configured =
100
+ localConfig?.automations?.runHistory?.maxEntries ??
101
+ globalConfig?.automations?.runHistory?.maxEntries;
102
+
103
+ if (Number.isInteger(configured) && configured > 0) {
104
+ return configured;
105
+ }
106
+
107
+ return DEFAULT_AUTOMATION_RUN_HISTORY_MAX_ENTRIES;
108
+ }
109
+
110
+ /**
111
+ * @param {string} projectRoot
112
+ * @param {string} loopId
113
+ * @returns {string}
114
+ */
115
+ export function automationRunRecordPath(projectRoot, loopId) {
116
+ return path.join(
117
+ path.resolve(projectRoot),
118
+ ".lisa",
119
+ "automations",
120
+ "runs",
121
+ `${normalizeLoopId(loopId)}.jsonl`
122
+ );
123
+ }
124
+
125
+ /**
126
+ * @param {string} filePath
127
+ * @returns {Promise<{ readonly records: readonly AutomationRunRecord[], readonly skippedCorruptLines: number }>}
128
+ */
129
+ export async function readAutomationRunRecords(filePath) {
130
+ let content = "";
131
+ try {
132
+ content = await readFile(filePath, "utf8");
133
+ } catch (error) {
134
+ if (error?.code === "ENOENT") {
135
+ return { records: [], skippedCorruptLines: 0 };
136
+ }
137
+ throw error;
138
+ }
139
+
140
+ const records = [];
141
+ let skippedCorruptLines = 0;
142
+ for (const line of content.split(/\n/)) {
143
+ if (!line.trim()) {
144
+ continue;
145
+ }
146
+ try {
147
+ const parsed = JSON.parse(line);
148
+ records.push(validateStoredRecord(parsed));
149
+ } catch {
150
+ skippedCorruptLines += 1;
151
+ }
152
+ }
153
+
154
+ return { records, skippedCorruptLines };
155
+ }
156
+
157
+ /**
158
+ * @param {RecordAutomationRunInput} input
159
+ * @returns {AutomationRunRecord}
160
+ */
161
+ function buildAutomationRunRecord(input) {
162
+ const loopId = normalizeLoopId(input.loopId);
163
+ const summary = String(input.summary ?? "").trim();
164
+ const runbook = String(input.runbook ?? "").trim();
165
+ const refs = Array.isArray(input.refs)
166
+ ? input.refs.map(ref => String(ref))
167
+ : [];
168
+ const ts =
169
+ input.ts instanceof Date
170
+ ? input.ts.toISOString()
171
+ : input.ts
172
+ ? new Date(input.ts).toISOString()
173
+ : new Date().toISOString();
174
+ const runId = String(input.runId ?? `${loopId}:${ts}`).trim();
175
+
176
+ if (!AUTOMATION_RUN_OUTCOMES.includes(input.outcome)) {
177
+ throw new Error(
178
+ `Invalid automation run outcome "${input.outcome}". Valid outcomes: ${AUTOMATION_RUN_OUTCOMES.join(", ")}.`
179
+ );
180
+ }
181
+ if (!summary) {
182
+ throw new Error("Automation run summary is required.");
183
+ }
184
+ if (!runbook) {
185
+ throw new Error("Automation runbook path is required.");
186
+ }
187
+ if (!runId) {
188
+ throw new Error("Automation run_id is required.");
189
+ }
190
+
191
+ return {
192
+ ts,
193
+ loop_id: loopId,
194
+ outcome: input.outcome,
195
+ summary,
196
+ runbook,
197
+ refs,
198
+ run_id: runId,
199
+ };
200
+ }
201
+
202
+ /**
203
+ * @param {unknown} value
204
+ * @returns {AutomationRunRecord}
205
+ */
206
+ function validateStoredRecord(value) {
207
+ if (!value || typeof value !== "object") {
208
+ throw new Error("Automation run record must be an object.");
209
+ }
210
+ return buildAutomationRunRecord({
211
+ ts: String(value.ts ?? ""),
212
+ loopId: String(value.loop_id ?? ""),
213
+ outcome: String(value.outcome ?? ""),
214
+ summary: String(value.summary ?? ""),
215
+ runbook: String(value.runbook ?? ""),
216
+ refs: Array.isArray(value.refs) ? value.refs.map(ref => String(ref)) : [],
217
+ runId: String(value.run_id ?? ""),
218
+ });
219
+ }
220
+
221
+ /**
222
+ * @param {string} loopId
223
+ * @returns {string}
224
+ */
225
+ function normalizeLoopId(loopId) {
226
+ const normalized = String(loopId ?? "").trim();
227
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(normalized)) {
228
+ throw new Error(
229
+ "Automation loop_id must be a non-empty slug containing only letters, numbers, dots, underscores, and hyphens."
230
+ );
231
+ }
232
+ return normalized;
233
+ }
234
+
235
+ /**
236
+ * @param {string} filePath
237
+ * @returns {Promise<unknown | undefined>}
238
+ */
239
+ async function readJsonIfPresent(filePath) {
240
+ try {
241
+ return JSON.parse(await readFile(filePath, "utf8"));
242
+ } catch (error) {
243
+ if (error?.code === "ENOENT") {
244
+ return undefined;
245
+ }
246
+ if (error instanceof SyntaxError) {
247
+ return undefined;
248
+ }
249
+ throw error;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * @param {string} filePath
255
+ * @param {readonly AutomationRunRecord[]} records
256
+ */
257
+ async function writeJsonlAtomically(filePath, records) {
258
+ const content = `${records.map(record => JSON.stringify(record)).join("\n")}\n`;
259
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
260
+ await writeFile(tempPath, content, "utf8");
261
+ await rename(tempPath, filePath);
262
+ }