@codyswann/lisa 3.33.8 → 3.34.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 (107) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +26 -8
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +53 -3
  8. package/plugins/lisa/.codex-plugin/skills/lisa-intake/SKILL.md +17 -1
  9. package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +53 -5
  10. package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +48 -7
  11. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-build-intake/SKILL.md +24 -0
  12. package/plugins/lisa/rules/eager/automation-runbook-contract.md +2 -0
  13. package/plugins/lisa/rules/reference/automation-runbook-contract.md +19 -0
  14. package/plugins/lisa/scripts/automation-run-record.mjs +146 -33
  15. package/plugins/lisa/scripts/intake-blocker-reprobe.mjs +190 -0
  16. package/plugins/lisa/scripts/intake-prework-denominator.mjs +249 -0
  17. package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +53 -3
  18. package/plugins/lisa/skills/lisa-intake/SKILL.md +17 -1
  19. package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +53 -5
  20. package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +48 -7
  21. package/plugins/lisa/skills/lisa-tracker-build-intake/SKILL.md +24 -0
  22. package/plugins/lisa-agy/plugin.json +1 -1
  23. package/plugins/lisa-agy/scripts/automation-run-record.mjs +146 -33
  24. package/plugins/lisa-agy/scripts/intake-blocker-reprobe.mjs +190 -0
  25. package/plugins/lisa-agy/scripts/intake-prework-denominator.mjs +249 -0
  26. package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +53 -3
  27. package/plugins/lisa-agy/skills/lisa-intake/SKILL.md +17 -1
  28. package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +53 -5
  29. package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +48 -7
  30. package/plugins/lisa-agy/skills/lisa-tracker-build-intake/SKILL.md +24 -0
  31. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  34. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-copilot/rules/eager/automation-runbook-contract.md +2 -0
  38. package/plugins/lisa-copilot/rules/reference/automation-runbook-contract.md +19 -0
  39. package/plugins/lisa-copilot/scripts/automation-run-record.mjs +146 -33
  40. package/plugins/lisa-copilot/scripts/intake-blocker-reprobe.mjs +190 -0
  41. package/plugins/lisa-copilot/scripts/intake-prework-denominator.mjs +249 -0
  42. package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +53 -3
  43. package/plugins/lisa-copilot/skills/lisa-intake/SKILL.md +17 -1
  44. package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +53 -5
  45. package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +48 -7
  46. package/plugins/lisa-copilot/skills/lisa-tracker-build-intake/SKILL.md +24 -0
  47. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-cursor/rules/automation-runbook-contract-reference.mdc +19 -0
  49. package/plugins/lisa-cursor/rules/automation-runbook-contract.mdc +2 -0
  50. package/plugins/lisa-cursor/scripts/automation-run-record.mjs +146 -33
  51. package/plugins/lisa-cursor/scripts/intake-blocker-reprobe.mjs +190 -0
  52. package/plugins/lisa-cursor/scripts/intake-prework-denominator.mjs +249 -0
  53. package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +53 -3
  54. package/plugins/lisa-cursor/skills/lisa-intake/SKILL.md +17 -1
  55. package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +53 -5
  56. package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +48 -7
  57. package/plugins/lisa-cursor/skills/lisa-tracker-build-intake/SKILL.md +24 -0
  58. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-expo-agy/plugin.json +1 -1
  61. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  66. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  70. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  71. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  75. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  76. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  80. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  81. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  85. package/plugins/lisa-rails-agy/plugin.json +1 -1
  86. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  90. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  91. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  95. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  96. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  98. package/plugins/src/base/rules/eager/automation-runbook-contract.md +2 -0
  99. package/plugins/src/base/rules/reference/automation-runbook-contract.md +19 -0
  100. package/plugins/src/base/scripts/automation-run-record.mjs +146 -33
  101. package/plugins/src/base/scripts/intake-blocker-reprobe.mjs +190 -0
  102. package/plugins/src/base/scripts/intake-prework-denominator.mjs +249 -0
  103. package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +53 -3
  104. package/plugins/src/base/skills/lisa-intake/SKILL.md +17 -1
  105. package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +53 -5
  106. package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +48 -7
  107. package/plugins/src/base/skills/lisa-tracker-build-intake/SKILL.md +24 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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": "3.33.8",
3
+ "version": "3.34.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"
@@ -48,6 +48,8 @@ Exactly one per run:
48
48
  Health and operator action are **orthogonal** — a healthy run can still need an answer:
49
49
 
50
50
  - `nothing-needed` — the loop ran and found nothing to act on. **Healthy.** Operator action: none.
51
+ A queue scanner's `nothing-needed` summary must name the lanes it swept and the total open — a dry
52
+ queue and a wrong denominator are otherwise indistinguishable. See the reference body.
51
53
  - `candidate-proposed` — the loop proposed work (ticket, PRD, recommendation). **Healthy.** Operator
52
54
  action: review the proposed item and flip it ready when you want it built.
53
55
  - `change-proved` — the loop made a change and proved it with evidence. **Healthy.** Operator
@@ -120,6 +120,25 @@ recovery-required Lost access to the tracker; filed #1812 with the one decisio
120
120
  policy-obsolete No work has reached this queue in 30 days; proposed teardown in #1813.
121
121
  ```
122
122
 
123
+ ### `nothing-needed` states its denominator
124
+
125
+ A queue-scanning loop that reports nothing is only as trustworthy as the set it looked at, and a
126
+ summary of the form "scanned N items" hides the one thing worth checking: **which lanes N came
127
+ from.** So a `nothing-needed` summary from a queue scanner names the lanes it swept, the count in
128
+ each, and the total open — `Looked in every lane holding work that has not started — Backlog (20),
129
+ Todo (17), Ready (2), Blocked (61) — 100 items checked out of 343 still open.`
130
+
131
+ This is a measured requirement, not a style preference. One build lane reported 31 consecutive dry
132
+ cycles over a queue holding 61 unswept rows, at least one of them buildable for ~15 hours. Every
133
+ one of the 31 records was honest and every conclusion was false, because the scanner never said
134
+ what its denominator was. **A missing run record is noisy on inspection; a wrong denominator is
135
+ silent forever and looks exactly like a healthy dry queue.** Stating the swept set is what makes
136
+ the second failure mode visible at all.
137
+
138
+ The build-intake loop enforces this rather than requesting it: `automation-run-record.mjs` refuses
139
+ a `nothing-needed` row for `intake-tickets` unless the call carries a `--denominator` built by
140
+ `scripts/intake-prework-denominator.mjs`.
141
+
123
142
  **No silent exit.** Every run posts its outcome and its one-line summary before stopping —
124
143
  including the trivial early terminations (empty queue, nothing eligible, already-claimed) — so
125
144
  there is no silent exit for an operator to misread as health. A run that ends without a
@@ -11,6 +11,8 @@ import path from "node:path";
11
11
  import { realpathSync } from "node:fs";
12
12
  import { fileURLToPath } from "node:url";
13
13
 
14
+ import { assertDenominatorReported } from "./intake-prework-denominator.mjs";
15
+
14
16
  export const AUTOMATION_RUN_OUTCOMES = [
15
17
  "nothing-needed",
16
18
  "candidate-proposed",
@@ -33,6 +35,10 @@ export const DEFAULT_AUTOMATION_RUN_HISTORY_MAX_ENTRIES = 50;
33
35
  * readonly run_id: string
34
36
  * }} AutomationRunRecord
35
37
  *
38
+ * `refs` above is the AUTHORED contract. A row already on disk whose `refs` is
39
+ * some other shape is round-tripped as stored rather than coerced — see
40
+ * {@link preserveStoredRefs}.
41
+ *
36
42
  * @typedef {{
37
43
  * readonly projectRoot?: string
38
44
  * readonly loopId: string
@@ -55,6 +61,15 @@ export const DEFAULT_AUTOMATION_RUN_HISTORY_MAX_ENTRIES = 50;
55
61
  * @returns {Promise<{ readonly path: string, readonly record: AutomationRunRecord, readonly records: readonly AutomationRunRecord[], readonly appended: boolean, readonly skippedCorruptLines: number, readonly maxEntries: number }>}
56
62
  */
57
63
  export async function recordAutomationRun(input) {
64
+ // Write path only. Stored rows are re-validated on read through
65
+ // `validateStoredRecord`, and a history written before this contract existed
66
+ // must stay readable — so the denominator gate lives here, where a NEW row is
67
+ // minted, and never on the read path.
68
+ assertDenominatorReported({
69
+ loopId: input.loopId,
70
+ outcome: input.outcome,
71
+ denominator: input.extras?.denominator,
72
+ });
58
73
  const projectRoot = path.resolve(input.projectRoot ?? process.cwd());
59
74
  const maxEntries =
60
75
  input.maxEntries ??
@@ -74,14 +89,26 @@ export async function recordAutomationRun(input) {
74
89
  };
75
90
  }
76
91
 
77
- const nextRecords = [...readResult.records, record].slice(-maxEntries);
92
+ // Prior rows are carried across as the RAW LINES they were read as, never
93
+ // re-serialised from the parsed form. Re-serialising is what let an append
94
+ // rewrite rows it did not author: a stored `refs` shape the writer did not
95
+ // recognise came back as `[]`, and a row that failed validation came back as
96
+ // nothing at all (#2682, #2578). Only the row being appended is serialised
97
+ // here, so an append can no longer be a fleet-wide migration nobody ran.
98
+ const nextEntries = [
99
+ ...readResult.entries,
100
+ { line: JSON.stringify(record), record },
101
+ ].slice(-maxEntries);
78
102
  await mkdir(path.dirname(filePath), { recursive: true });
79
- await writeJsonlAtomically(filePath, nextRecords);
103
+ await writeJsonlAtomically(
104
+ filePath,
105
+ nextEntries.map(entry => entry.line)
106
+ );
80
107
 
81
108
  return {
82
109
  path: filePath,
83
110
  record,
84
- records: nextRecords,
111
+ records: nextEntries.flatMap(entry => (entry.record ? [entry.record] : [])),
85
112
  appended: true,
86
113
  skippedCorruptLines: readResult.skippedCorruptLines,
87
114
  maxEntries,
@@ -126,8 +153,12 @@ export function automationRunRecordPath(projectRoot, loopId) {
126
153
  }
127
154
 
128
155
  /**
156
+ * Read a ledger, keeping every non-blank line verbatim alongside its parsed
157
+ * form. `records` is the validated view for consumers; `entries` is what the
158
+ * append path rewrites, so a row this module cannot parse or validate survives
159
+ * instead of being deleted by the next unrelated append (#2578).
129
160
  * @param {string} filePath
130
- * @returns {Promise<{ readonly records: readonly AutomationRunRecord[], readonly skippedCorruptLines: number }>}
161
+ * @returns {Promise<{ readonly records: readonly AutomationRunRecord[], readonly entries: readonly { readonly line: string, readonly record?: AutomationRunRecord }[], readonly skippedCorruptLines: number }>}
131
162
  */
132
163
  export async function readAutomationRunRecords(filePath) {
133
164
  let content = "";
@@ -135,12 +166,13 @@ export async function readAutomationRunRecords(filePath) {
135
166
  content = await readFile(filePath, "utf8");
136
167
  } catch (error) {
137
168
  if (error?.code === "ENOENT") {
138
- return { records: [], skippedCorruptLines: 0 };
169
+ return { records: [], entries: [], skippedCorruptLines: 0 };
139
170
  }
140
171
  throw error;
141
172
  }
142
173
 
143
- const records = [];
174
+ /** @type {{ readonly line: string, readonly record?: AutomationRunRecord }[]} */
175
+ const entries = [];
144
176
  let skippedCorruptLines = 0;
145
177
  for (const line of content.split(/\n/)) {
146
178
  if (!line.trim()) {
@@ -148,13 +180,21 @@ export async function readAutomationRunRecords(filePath) {
148
180
  }
149
181
  try {
150
182
  const parsed = JSON.parse(line);
151
- records.push(validateStoredRecord(parsed));
183
+ entries.push({ line, record: validateStoredRecord(parsed) });
152
184
  } catch {
185
+ // Kept, not dropped. The caller rewrites the file from `entries`, so a
186
+ // row omitted here would be DELETED from disk by the next unrelated
187
+ // append (#2578). It stays exactly as read; only `records` excludes it.
188
+ entries.push({ line });
153
189
  skippedCorruptLines += 1;
154
190
  }
155
191
  }
156
192
 
157
- return { records, skippedCorruptLines };
193
+ return {
194
+ records: entries.flatMap(entry => (entry.record ? [entry.record] : [])),
195
+ entries,
196
+ skippedCorruptLines,
197
+ };
158
198
  }
159
199
 
160
200
  /**
@@ -162,12 +202,58 @@ export async function readAutomationRunRecords(filePath) {
162
202
  * @returns {AutomationRunRecord}
163
203
  */
164
204
  function buildAutomationRunRecord(input) {
205
+ return assembleAutomationRunRecord(input, normalizeAuthoredRefs(input.refs));
206
+ }
207
+
208
+ /**
209
+ * `refs` as an author supplied it.
210
+ *
211
+ * A shape this module cannot store as written is REJECTED, not quietly
212
+ * emptied. Coercing to `[]` is how a caller could record a run whose evidence
213
+ * links were dropped while the write still reported success (#2682).
214
+ *
215
+ * @param {unknown} refs
216
+ * @returns {readonly string[]}
217
+ */
218
+ function normalizeAuthoredRefs(refs) {
219
+ if (refs === undefined || refs === null) {
220
+ return [];
221
+ }
222
+ if (!Array.isArray(refs)) {
223
+ throw new Error(
224
+ `Automation run refs must be an array of strings; received ${typeof refs}.`
225
+ );
226
+ }
227
+ return refs.map(ref => String(ref));
228
+ }
229
+
230
+ /**
231
+ * `refs` as it was stored, round-tripped rather than coerced.
232
+ *
233
+ * A stored shape this module does not recognise — the object form
234
+ * `{tickets, prs, commits}` observed in the field — is returned unchanged.
235
+ * Coercing it to `[]` here destroyed the evidence links on every prior row at
236
+ * the next append: silently, retroactively, and irreversibly (#2682).
237
+ *
238
+ * @param {unknown} refs
239
+ * @returns {unknown}
240
+ */
241
+ function preserveStoredRefs(refs) {
242
+ if (refs === undefined || refs === null) {
243
+ return [];
244
+ }
245
+ return Array.isArray(refs) ? refs.map(ref => String(ref)) : refs;
246
+ }
247
+
248
+ /**
249
+ * @param {RecordAutomationRunInput} input
250
+ * @param {unknown} refs - Already-resolved `refs` for this record.
251
+ * @returns {AutomationRunRecord}
252
+ */
253
+ function assembleAutomationRunRecord(input, refs) {
165
254
  const loopId = normalizeLoopId(input.loopId);
166
255
  const summary = String(input.summary ?? "").trim();
167
256
  const runbook = String(input.runbook ?? "").trim();
168
- const refs = Array.isArray(input.refs)
169
- ? input.refs.map(ref => String(ref))
170
- : [];
171
257
  const ts =
172
258
  input.ts instanceof Date
173
259
  ? input.ts.toISOString()
@@ -240,21 +326,23 @@ function validateStoredRecord(value) {
240
326
  if (!value || typeof value !== "object") {
241
327
  throw new Error("Automation run record must be an object.");
242
328
  }
243
- return buildAutomationRunRecord({
244
- ts: String(value.ts ?? ""),
245
- loopId: String(value.loop_id ?? ""),
246
- outcome: String(value.outcome ?? ""),
247
- summary: String(value.summary ?? ""),
248
- runbook: String(value.runbook ?? ""),
249
- refs: Array.isArray(value.refs) ? value.refs.map(ref => String(ref)) : [],
250
- runId: String(value.run_id ?? ""),
251
- // Carry every unrecognised key forward. Without this, re-validating a
252
- // stored record on read silently strips it, and the next append writes the
253
- // stripped history back over the file (#2524).
254
- extras: Object.fromEntries(
255
- Object.entries(value).filter(([key]) => !KNOWN_RECORD_KEYS.has(key))
256
- ),
257
- });
329
+ return assembleAutomationRunRecord(
330
+ {
331
+ ts: String(value.ts ?? ""),
332
+ loopId: String(value.loop_id ?? ""),
333
+ outcome: String(value.outcome ?? ""),
334
+ summary: String(value.summary ?? ""),
335
+ runbook: String(value.runbook ?? ""),
336
+ runId: String(value.run_id ?? ""),
337
+ // Carry every unrecognised key forward. Without this, re-validating a
338
+ // stored record on read silently strips it, and the next append writes
339
+ // the stripped history back over the file (#2524).
340
+ extras: Object.fromEntries(
341
+ Object.entries(value).filter(([key]) => !KNOWN_RECORD_KEYS.has(key))
342
+ ),
343
+ },
344
+ preserveStoredRefs(value.refs)
345
+ );
258
346
  }
259
347
 
260
348
  /**
@@ -291,10 +379,10 @@ async function readJsonIfPresent(filePath) {
291
379
 
292
380
  /**
293
381
  * @param {string} filePath
294
- * @param {readonly AutomationRunRecord[]} records
382
+ * @param {readonly string[]} lines - Serialised rows, written verbatim.
295
383
  */
296
- async function writeJsonlAtomically(filePath, records) {
297
- const content = `${records.map(record => JSON.stringify(record)).join("\n")}\n`;
384
+ async function writeJsonlAtomically(filePath, lines) {
385
+ const content = `${lines.join("\n")}\n`;
298
386
  const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
299
387
  await writeFile(tempPath, content, "utf8");
300
388
  await rename(tempPath, filePath);
@@ -303,14 +391,19 @@ async function writeJsonlAtomically(filePath, records) {
303
391
  const CLI_USAGE = `Usage: node automation-run-record.mjs \\
304
392
  --loop-id <slug> --outcome <${AUTOMATION_RUN_OUTCOMES.join("|")}> \\
305
393
  --summary "<operator-readable one-liner>" --runbook <path> \\
306
- [--ref <url>]... [--run-id <id>] [--project-root <dir>]`;
394
+ [--ref <url>]... [--run-id <id>] [--project-root <dir>] \\
395
+ [--denominator '<json from buildIntakeDenominator()>']
396
+
397
+ --denominator is REQUIRED for a "nothing-needed" run on a build-intake loop: a
398
+ dry lane that does not say which lanes it looked in cannot be checked.`;
307
399
 
308
400
  /**
309
401
  * Translate a repeatable-flag argv into a {@link RecordAutomationRunInput}.
310
402
  *
311
403
  * Every flag takes a following value; `--ref` may repeat and accumulates into
312
- * `refs`. Unknown flags and value-less flags throw so a typo never silently
313
- * records the wrong thing.
404
+ * `refs`, and `--denominator` carries the swept-lane JSON into `extras`.
405
+ * Unknown flags and value-less flags throw so a typo never silently records the
406
+ * wrong thing.
314
407
  *
315
408
  * @param {readonly string[]} argv
316
409
  * @returns {RecordAutomationRunInput}
@@ -320,6 +413,8 @@ function parseAutomationRunRecordArgv(argv) {
320
413
  const single = {};
321
414
  /** @type {string[]} */
322
415
  const refs = [];
416
+ /** @type {Record<string, unknown>} */
417
+ const extras = {};
323
418
  const flags = {
324
419
  "--loop-id": "loopId",
325
420
  "--outcome": "outcome",
@@ -349,6 +444,10 @@ function parseAutomationRunRecordArgv(argv) {
349
444
  refs.push(takeValue());
350
445
  continue;
351
446
  }
447
+ if (flag === "--denominator") {
448
+ extras.denominator = parseDenominatorFlag(takeValue());
449
+ continue;
450
+ }
352
451
  const key = flags[flag];
353
452
  if (!key) {
354
453
  throw new Error(`Unknown flag "${flag}".`);
@@ -356,7 +455,21 @@ function parseAutomationRunRecordArgv(argv) {
356
455
  single[key] = takeValue();
357
456
  }
358
457
 
359
- return { ...single, refs };
458
+ return { ...single, refs, extras };
459
+ }
460
+
461
+ /**
462
+ * @param {string} value
463
+ * @returns {unknown}
464
+ */
465
+ function parseDenominatorFlag(value) {
466
+ try {
467
+ return JSON.parse(value);
468
+ } catch {
469
+ throw new Error(
470
+ "--denominator must be JSON produced by buildIntakeDenominator()."
471
+ );
472
+ }
360
473
  }
361
474
 
362
475
  /**