@codyswann/lisa 2.252.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 (98) 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/.codex-plugin/skills/lisa-automation-status/SKILL.md +14 -8
  5. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/SKILL.md +116 -1
  6. package/plugins/lisa/.codex-plugin/skills/lisa-tear-down-automations/SKILL.md +25 -5
  7. package/plugins/lisa/rules/eager/automation-runbook-contract.md +4 -1
  8. package/plugins/lisa/rules/reference/automation-runbook-contract.md +16 -7
  9. package/plugins/lisa/scripts/automation-run-record.mjs +262 -0
  10. package/plugins/lisa/scripts/automation-status-claude-adapter.mjs +36 -18
  11. package/plugins/lisa/scripts/automation-status-codex-adapter.mjs +50 -27
  12. package/plugins/lisa/scripts/automation-status-expected-fleet.mjs +149 -6
  13. package/plugins/lisa/skills/lisa-automation-status/SKILL.md +14 -8
  14. package/plugins/lisa/skills/lisa-setup-automations/SKILL.md +116 -1
  15. package/plugins/lisa/skills/lisa-tear-down-automations/SKILL.md +26 -6
  16. package/plugins/lisa/skills/lisa-tear-down-automations/agents/openai.yaml +2 -2
  17. package/plugins/lisa-agy/plugin.json +1 -1
  18. package/plugins/lisa-agy/scripts/automation-run-record.mjs +262 -0
  19. package/plugins/lisa-agy/scripts/automation-status-claude-adapter.mjs +36 -18
  20. package/plugins/lisa-agy/scripts/automation-status-codex-adapter.mjs +50 -27
  21. package/plugins/lisa-agy/scripts/automation-status-expected-fleet.mjs +149 -6
  22. package/plugins/lisa-agy/skills/lisa-automation-status/SKILL.md +14 -8
  23. package/plugins/lisa-agy/skills/lisa-setup-automations/SKILL.md +116 -1
  24. package/plugins/lisa-agy/skills/lisa-tear-down-automations/SKILL.md +26 -6
  25. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  28. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-copilot/rules/eager/automation-runbook-contract.md +4 -1
  32. package/plugins/lisa-copilot/rules/reference/automation-runbook-contract.md +16 -7
  33. package/plugins/lisa-copilot/scripts/automation-run-record.mjs +262 -0
  34. package/plugins/lisa-copilot/scripts/automation-status-claude-adapter.mjs +36 -18
  35. package/plugins/lisa-copilot/scripts/automation-status-codex-adapter.mjs +50 -27
  36. package/plugins/lisa-copilot/scripts/automation-status-expected-fleet.mjs +149 -6
  37. package/plugins/lisa-copilot/skills/lisa-automation-status/SKILL.md +14 -8
  38. package/plugins/lisa-copilot/skills/lisa-setup-automations/SKILL.md +116 -1
  39. package/plugins/lisa-copilot/skills/lisa-tear-down-automations/SKILL.md +26 -6
  40. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-cursor/rules/automation-runbook-contract-reference.mdc +16 -7
  42. package/plugins/lisa-cursor/rules/automation-runbook-contract.mdc +4 -1
  43. package/plugins/lisa-cursor/scripts/automation-run-record.mjs +262 -0
  44. package/plugins/lisa-cursor/scripts/automation-status-claude-adapter.mjs +36 -18
  45. package/plugins/lisa-cursor/scripts/automation-status-codex-adapter.mjs +50 -27
  46. package/plugins/lisa-cursor/scripts/automation-status-expected-fleet.mjs +149 -6
  47. package/plugins/lisa-cursor/skills/lisa-automation-status/SKILL.md +14 -8
  48. package/plugins/lisa-cursor/skills/lisa-setup-automations/SKILL.md +116 -1
  49. package/plugins/lisa-cursor/skills/lisa-tear-down-automations/SKILL.md +26 -6
  50. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-expo-agy/plugin.json +1 -1
  53. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  58. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  63. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  68. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  73. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-rails-agy/plugin.json +1 -1
  78. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  83. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  87. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  88. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  90. package/plugins/src/base/rules/eager/automation-runbook-contract.md +4 -1
  91. package/plugins/src/base/rules/reference/automation-runbook-contract.md +16 -7
  92. package/plugins/src/base/scripts/automation-run-record.mjs +262 -0
  93. package/plugins/src/base/scripts/automation-status-claude-adapter.mjs +36 -18
  94. package/plugins/src/base/scripts/automation-status-codex-adapter.mjs +50 -27
  95. package/plugins/src/base/scripts/automation-status-expected-fleet.mjs +149 -6
  96. package/plugins/src/base/skills/lisa-automation-status/SKILL.md +14 -8
  97. package/plugins/src/base/skills/lisa-setup-automations/SKILL.md +116 -1
  98. package/plugins/src/base/skills/lisa-tear-down-automations/SKILL.md +26 -6
package/package.json CHANGED
@@ -102,7 +102,7 @@
102
102
  "form-data": ">=4.0.6"
103
103
  },
104
104
  "name": "@codyswann/lisa",
105
- "version": "2.252.0",
105
+ "version": "2.254.0",
106
106
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
107
107
  "main": "dist/index.js",
108
108
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.252.0",
3
+ "version": "2.254.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.252.0",
3
+ "version": "2.254.0",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -16,14 +16,20 @@ Do **not** ask for confirmation once invoked. This skill inspects scheduler stat
16
16
 
17
17
  ## Scope
18
18
 
19
- Inspect only the Lisa automation fleet for the current project:
20
-
21
- - `intake-repair`
22
- - `intake-prd`
23
- - `intake-tickets`
24
- - `exploratory-bugs` when the current stack supports `exploratory-qa`
25
- - `exploratory-prds`
26
- - `monitor`
19
+ Inspect only the Lisa automation fleet for the current project. Derive that fleet from
20
+ `scripts/automation-status-expected-fleet.mjs` (`resolveExpectedAutomationFleet`), which resolves
21
+ exactly what `setup-automations` registers for this repo — including the stack-guarded
22
+ `exploratory-bugs` and the opt-in `learnings-audit` gardener. **Membership is registration, not a
23
+ roster** (`automation-runbook-contract`): carry no fixed list of loop names here, so a loop added to
24
+ or removed from the registration set flows through without editing this skill.
25
+
26
+ The gardener is opted into at registration time and has no config key, so **infer it from the
27
+ scheduler**: list the project's automations first, then call
28
+ `inferLearningsAuditRegistration({ automationPrefix, observedAutomationIds })` and pass the result
29
+ as `learningsAudit` when resolving the fleet. A `lisa-auto-<project>-learnings-audit` entry on the
30
+ scheduler means the project opted in, so the gardener is compared like any other loop; no such entry
31
+ means it is reported `UNSUPPORTED` (opted out), never `MISSING`. This skill stays read-only — it
32
+ infers, it never writes the flag anywhere.
27
33
 
28
34
  Resolve the expected project identifier, fleet naming prefix, queue arguments, cadence, and stack-support rules from the same contract used by `setup-automations` and `tear-down-automations`. Do **not** invent a second source of truth for fleet naming or queue resolution.
29
35
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: lisa-setup-automations
3
3
  description: "Set up the recurring Lisa…"
4
- allowed-tools: ["Skill", "Bash", "Read"]
4
+ allowed-tools: ["Skill", "Bash", "Read", "Write", "Edit"]
5
5
  ---
6
6
 
7
7
  # Set up Lisa automations: $ARGUMENTS
@@ -105,6 +105,19 @@ the scheduled commands (for example `/lisa:intake acme/frontend intake_mode=prd`
105
105
  config/read-context failure cannot silently redirect the cron. A short queueRepo is normalized to
106
106
  `github.org` before writing the automation.
107
107
 
108
+ **Registration prompt shape (so status can read the command back).** Whatever prose the prompt
109
+ carries, it must contain the **literal Lisa command on a line of its own**, exactly as scheduled and
110
+ with every `owner/repo` already baked in:
111
+
112
+ ```text
113
+ /lisa:intake acme/planning intake_mode=build
114
+ ```
115
+
116
+ This holds for loops that take **no arguments** too — `/lisa:monitor` and `/lisa:learnings:audit`
117
+ are written the same way, on their own line, with nothing after them. `/lisa:automation-status`
118
+ reads that line back to compare the live registration against this contract; a loop whose command
119
+ cannot be read back reports as drifted even though it is registered correctly.
120
+
108
121
  **Naming + scope (so teardown is precise).** Name each automation with the stable prefix
109
122
  `lisa-auto-<project>-` (e.g. `lisa-auto-<project>-intake-tickets`), where `<project>` identifies this
110
123
  repo, and scope each Codex automation to the durable project automation checkout described above.
@@ -115,6 +128,91 @@ repos (don't rely on a bare repo basename when it could collide; qualify it, e.g
115
128
  **Idempotent.** Re-running this skill updates the existing `lisa-auto-<project>-*` automations in
116
129
  place (same names) rather than creating duplicates.
117
130
 
131
+ ## Runbook scaffolding
132
+
133
+ Registration is what pulls a loop under the `automation-runbook-contract` rule, so registration is
134
+ what must produce its runbook. For **every automation actually registered above** — including the
135
+ conditionally-guarded `exploratory-bugs` and the opt-in `learnings-audit` — write a checked-in
136
+ runbook at:
137
+
138
+ ```text
139
+ .lisa/automations/<loop-id>.runbook.md
140
+ ```
141
+
142
+ `<loop-id>` is the automation's suffix, not its full name: `intake-tickets`, not
143
+ `lisa-auto-<project>-intake-tickets`. These files are **project knowledge and belong in git**, like
144
+ `.lisa/PROJECT_LEARNINGS.md` — commit them; they are not scratch output. Derive the set of runbooks
145
+ from what was registered on this run: a loop that was **skipped gets no runbook**, and a loop
146
+ registered later gets one when this skill next runs. Never write a runbook from a fixed list of
147
+ loop names.
148
+
149
+ Follow the `automation-runbook-contract` rule for the runbook's shape: its ten sections, in its
150
+ order, in its prose register. **Do not restate the template, the run outcomes, or the escalation
151
+ packet here** — cite the rule and instantiate it.
152
+
153
+ **File shape — two halves with different owners.**
154
+
155
+ 1. A **machine-resolved header block**, delimited exactly by
156
+ `<!-- lisa:machine-resolved:start -->` and `<!-- lisa:machine-resolved:end -->`, written first
157
+ in the file. Lisa owns it: on every run it is **rewritten wholesale** from the values resolved
158
+ above. It carries the project identity (`<owner>/<repo>` and the `<project>` token), the loop
159
+ id, the full automation name, the exact scheduled command with every `owner/repo` already baked
160
+ in (never a placeholder), the human cadence and its `rrule`, the resolved queue arguments, and
161
+ the resolved flag values that shaped the command (`auto-start-prds` / `auto-start-tickets` /
162
+ `learnings-audit`). Its **first line inside the delimiters is a visible sentence**, so the
163
+ ownership boundary survives rendering to markdown where HTML comments disappear:
164
+
165
+ ```text
166
+ Lisa rewrites this section on every /lisa:setup-automations run — edits here are lost. Your prose
167
+ belongs in the sections below.
168
+ ```
169
+
170
+ Immediately **below** the closing delimiter, write one more visible line — `Everything below is
171
+ yours.` — so an operator reading the rendered page knows where their half starts.
172
+ 2. The **ten contract sections** below it. Lisa owns them **only on first write**, then they belong
173
+ to the operator. Seed them deterministically, never improvised:
174
+ - **Intent, Sources of truth, Candidate selection, Scope/bounds, Retirement condition** come
175
+ from the per-loop seed table below.
176
+ - **Proof, Autonomous-vs-approval boundary, Escalation, Recovery, Next-run state** are derived
177
+ from **that loop's own SKILL.md** — named per loop in the table's last column — so the runbook
178
+ describes what the loop actually does rather than a generic paraphrase. Quote its behavior in
179
+ the operator's voice; if the skill file is unreadable, write one sentence saying the section
180
+ was not derivable and which file to read, and continue.
181
+
182
+ Every seeded sentence is written for a non-technical operator (`factory-model` rule 5).
183
+
184
+ ### Per-loop seed defaults
185
+
186
+ One line each; expand them into the operator's voice, keep the meaning.
187
+
188
+ | Loop | Intent | Sources of truth | Candidate selection | Scope/bounds | Retirement condition | Derive the other five from |
189
+ |---|---|---|---|---|---|---|
190
+ | **intake-repair** | Keeps the queues unstuck: work that stalled mid-flight gets diagnosed and moved again. | The PRD and build queues for this project, read through the tracker access layer. | Stuck or half-closed items — stalled in an in-progress role, terminal-labeled but still open, rollups whose children are all done — up to the `max_candidates` cap. | Only this project's configured queues; it repairs lifecycle state, never invents or closes real work. | Propose teardown when nothing has needed repair for a long quiet window AND this run found nothing. | `lisa-repair-intake/SKILL.md` |
191
+ | **intake-prd** | Keeps PRDs moving: a PRD a human marked ready gets validated and decomposed into work items. | The configured PRD queue and its lifecycle roles, read through the source access layer. | The oldest PRD in the ready role, one per run. | Only this project's PRD queue; it never authors product intent, only validates and decomposes it. | Propose teardown when no PRD has entered the queue for a long quiet window AND this run proposed nothing. | `lisa-intake/SKILL.md` |
192
+ | **intake-tickets** | Keeps the build queue moving: work items a human marked ready get built, reviewed, and shipped. | The configured tracker's build queue lanes plus each item's comments and linked PRs. | The oldest eligible leaf work item in the ready lane, one per run. | Only this repo's configured queue; containers with open children are repaired out, never built. | Propose teardown when no work item has entered this queue for a long quiet window AND this run proposed nothing. | `lisa-intake/SKILL.md` |
193
+ | **monitor** | Keeps production honest: observability regressions become tracked work instead of silent decay. | The connected observability providers, each through its access layer, plus this project's `monitor.gapTiers` config. | The regressions and coverage gaps this cycle surfaced, capped per run. | Only this project's own services and dashboards; it files findings, it never changes production. | Propose teardown when the project has no connected observability surfaces AND this run found nothing. | `lisa-monitor/SKILL.md` |
194
+ | **exploratory-prds** | Keeps the idea pipeline fed: the product's own gaps become PRDs when the PRD queue has room. | The PRD queue's pressure roles (the same ones `/lisa:queue-status` reports) and the project's own product surfaces. | One ideated PRD per run, and only when the pressure gate says the queue has capacity. | Never more than one PRD per run, and nothing at all while unresolved PRD pressure exists. | Propose teardown when the queue has been at pressure — or the project has shipped no new surface — across a long quiet window AND this run proposed nothing. | `lisa-project-ideation/SKILL.md` |
195
+ | **exploratory-bugs** | Keeps quality visible: bugs and usability problems get found by using the product, before a customer does. | The running application through its exploratory-qa surface, plus this project's existing bug queue for dedupe. | The findings of one exploratory session, capped per run and deduped against already-filed items. | Only this project's own app; it files tickets, it never changes product code. | Propose teardown when no session has produced a finding across a long quiet window AND this run found nothing. | `lisa-exploratory-qa/SKILL.md` (the stack's own copy) |
196
+ | **learnings-audit** | Keeps the project's knowledge true: stale, duplicated, or contradicted knowledge gets proposed for promotion, demotion, or retirement. | The learnings ledger, the rules trees, the skills, and the wiki — the project's knowledge surfaces. | The knowledge entries this audit judged stale, duplicated, or contradicted, capped per run. | It only proposes; every promote/demote/retire decision is a human-gated tracker ticket. | Propose teardown when the audit has proposed nothing across a long quiet window AND this run proposed nothing. | `lisa-learnings-audit/SKILL.md` |
197
+
198
+ A loop not in this table — one registered later — seeds all ten sections from its own SKILL.md the
199
+ same way. The table is seed prose for first write only; it is **not** a roster and never decides
200
+ which automations exist.
201
+
202
+ **Re-run rule (idempotent, never clobber).** When the file already exists: rewrite the
203
+ machine-resolved block in place and **leave every one of the ten sections exactly as found** — an
204
+ operator's edited Intent paragraph survives verbatim. If a contract section is **missing** from an
205
+ existing file, insert it **at its position in the contract's ten-section order** (never merely
206
+ appended at the end, which would leave the file permanently out of order) with its seeded default.
207
+ Never overwrite, reorder, reword, or delete prose already on disk. If the machine-resolved
208
+ delimiters are absent (a hand-written file), insert the block at the top and touch nothing else.
209
+
210
+ **Never a precondition.** A runbook that cannot be written is a **degradation, not a blocker**:
211
+ report the failure and continue registering automations. A loop with a missing runbook still runs —
212
+ on the contract's defaults, saying so in its one-line summary — per the contract's
213
+ never-block-always-degrade rule. Scaffolding failure never aborts setup and never leaves an
214
+ automation unregistered.
215
+
118
216
  ## Conditions / guards
119
217
 
120
218
  - **exploratory-bugs** is created only when the project ships an `exploratory-qa` command (the
@@ -134,3 +232,20 @@ place (same names) rather than creating duplicates.
134
232
 
135
233
  List each automation created or updated (name, the command it runs, cadence, and the resolved
136
234
  `auto-start-prds` / `auto-start-tickets` values), plus any automation skipped and why.
235
+
236
+ Then list each runbook written, by path (`.lisa/automations/<loop-id>.runbook.md`), saying for each
237
+ whether it was **created** (seeded fresh) or **refreshed** (machine-resolved block rewritten,
238
+ operator prose left untouched). Name every skipped loop again here and state plainly that it has no
239
+ runbook because it was not registered, and why — for example, "exploratory-bugs was skipped because
240
+ this project ships no exploratory-qa command, so no runbook was written for it."
241
+
242
+ Close with the two lines that tell the operator what to do next:
243
+
244
+ - When any runbook was newly created: "N new files were written under `.lisa/automations/` — commit
245
+ them so the whole team sees them."
246
+ - When a runbook could not be written: name which one and what failed, then the consequence and the
247
+ fix — "that loop runs on defaults and its summaries will lead with 'Runbook missing' — re-run
248
+ `/lisa:setup-automations` to fix." The automation itself is still registered and still runs.
249
+
250
+ Every line states the consequence and the action, in words a non-technical operator can act on
251
+ without reading code.
@@ -21,14 +21,34 @@ removes them with its **native** scheduling mechanism.
21
21
 
22
22
  ## Scope (remove only what setup created)
23
23
 
24
- - Remove the six automations `/setup-automations` creates for the current project, matched by the
25
- stable `lisa-auto-<project>-` name prefix: `intake-repair`, `intake-prd`, `intake-tickets`,
26
- `exploratory-bugs`, `exploratory-prds`, `monitor`.
24
+ - Remove **every** automation `/setup-automations` registered for the current project the whole
25
+ set found under the stable `lisa-auto-<project>-` name prefix, whatever it currently contains.
26
+ **Membership is registration, not a roster** (`automation-runbook-contract`): sweep the prefix and
27
+ remove what is there. Do **not** work from a fixed list of loop names — a list drifts the moment a
28
+ loop is added, which is exactly how the opt-in gardener came to be orphaned.
29
+ - This explicitly includes the opt-in **`learnings-audit`** gardener when it is registered:
30
+ `/setup-automations learnings-audit=true` registers it under the same prefix, so teardown removes
31
+ it with the rest. A conditionally-skipped loop (e.g. `exploratory-bugs` on a stack without
32
+ `exploratory-qa`) simply is not in the sweep.
27
33
  - **Never** remove automations for a different project, or any non-Lisa automation (e.g. unrelated
28
34
  crawlers/ingestors). Match strictly on the `lisa-auto-<project>-` prefix for THIS project; when in
29
35
  doubt about an automation's ownership, leave it and report it rather than deleting it.
30
- - **Idempotent** — an automation that is already absent is a no-op, not an error.
36
+ - **Idempotent** — an automation that is already absent is a no-op, not an error. Re-running when
37
+ the prefix sweep finds nothing is a clean, successful no-op.
38
+ - **Leave the runbooks alone.** The checked-in `.lisa/automations/<loop-id>.runbook.md` files that
39
+ `/setup-automations` scaffolded are project knowledge and the historical record of what these
40
+ loops did. Teardown removes scheduler registrations only; it never deletes, edits, or moves a
41
+ runbook file. An operator who wants them gone removes them deliberately, in git.
31
42
 
32
43
  ## Report
33
44
 
34
- List each automation removed, and any in the expected set that were already absent.
45
+ List each automation removed by name. For "already absent", compare against the one source of truth
46
+ — the fleet `scripts/automation-status-expected-fleet.mjs` (`resolveExpectedAutomationFleet`)
47
+ resolves for this project — and name anything it expects that the sweep did not find; that is a
48
+ no-op, not an error. Do not invent an expected set of your own.
49
+
50
+ Then state, in the operator's words, that the runbook files under `.lisa/automations/` were left on
51
+ disk **and why**: they are the written record of what those jobs did, kept on purpose, and if you
52
+ do not want them you delete them yourself in git. Finally, confirm that nothing outside this
53
+ project's `lisa-auto-<project>-` prefix was touched. Write it so a non-technical operator can
54
+ confirm what happened without reading code.
@@ -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
+ }