@codyswann/lisa 2.255.0 → 2.256.1

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 (150) 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 +11 -3
  5. package/plugins/lisa/.codex-plugin/skills/lisa-github-validate-issue/SKILL.md +1 -1
  6. package/plugins/lisa/.codex-plugin/skills/lisa-github-write-issue/SKILL.md +9 -3
  7. package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +6 -2
  8. package/plugins/lisa/.codex-plugin/skills/lisa-jira-validate-ticket/SKILL.md +1 -1
  9. package/plugins/lisa/.codex-plugin/skills/lisa-jira-write-ticket/SKILL.md +9 -6
  10. package/plugins/lisa/.codex-plugin/skills/lisa-linear-validate-issue/SKILL.md +1 -1
  11. package/plugins/lisa/.codex-plugin/skills/lisa-linear-write-issue/SKILL.md +9 -3
  12. package/plugins/lisa/agents/github-agent.md +2 -2
  13. package/plugins/lisa/agents/jira-agent.md +2 -2
  14. package/plugins/lisa/agents/linear-agent.md +2 -2
  15. package/plugins/lisa/rules/eager/config-resolution.md +27 -8
  16. package/plugins/lisa/rules/eager/pre-flight-autofill.md +21 -7
  17. package/plugins/lisa/rules/reference/config-resolution.md +55 -16
  18. package/plugins/lisa/rules/reference/pre-flight-autofill.md +23 -8
  19. package/plugins/lisa/scripts/automation-status-claude-adapter.mjs +64 -8
  20. package/plugins/lisa/scripts/automation-status-codex-adapter.mjs +61 -5
  21. package/plugins/lisa/scripts/automation-status-report.mjs +30 -0
  22. package/plugins/lisa/scripts/automation-status-run-history.mjs +185 -0
  23. package/plugins/lisa/skills/lisa-automation-status/SKILL.md +11 -3
  24. package/plugins/lisa/skills/lisa-github-validate-issue/SKILL.md +1 -1
  25. package/plugins/lisa/skills/lisa-github-write-issue/SKILL.md +9 -3
  26. package/plugins/lisa/skills/lisa-implement/SKILL.md +6 -2
  27. package/plugins/lisa/skills/lisa-jira-validate-ticket/SKILL.md +1 -1
  28. package/plugins/lisa/skills/lisa-jira-write-ticket/SKILL.md +9 -6
  29. package/plugins/lisa/skills/lisa-linear-validate-issue/SKILL.md +1 -1
  30. package/plugins/lisa/skills/lisa-linear-write-issue/SKILL.md +9 -3
  31. package/plugins/lisa-agy/agents/github-agent.md +2 -2
  32. package/plugins/lisa-agy/agents/jira-agent.md +2 -2
  33. package/plugins/lisa-agy/agents/linear-agent.md +2 -2
  34. package/plugins/lisa-agy/plugin.json +1 -1
  35. package/plugins/lisa-agy/scripts/automation-status-claude-adapter.mjs +64 -8
  36. package/plugins/lisa-agy/scripts/automation-status-codex-adapter.mjs +61 -5
  37. package/plugins/lisa-agy/scripts/automation-status-report.mjs +30 -0
  38. package/plugins/lisa-agy/scripts/automation-status-run-history.mjs +185 -0
  39. package/plugins/lisa-agy/skills/lisa-automation-status/SKILL.md +11 -3
  40. package/plugins/lisa-agy/skills/lisa-github-validate-issue/SKILL.md +1 -1
  41. package/plugins/lisa-agy/skills/lisa-github-write-issue/SKILL.md +9 -3
  42. package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +6 -2
  43. package/plugins/lisa-agy/skills/lisa-jira-validate-ticket/SKILL.md +1 -1
  44. package/plugins/lisa-agy/skills/lisa-jira-write-ticket/SKILL.md +9 -6
  45. package/plugins/lisa-agy/skills/lisa-linear-validate-issue/SKILL.md +1 -1
  46. package/plugins/lisa-agy/skills/lisa-linear-write-issue/SKILL.md +9 -3
  47. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  50. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-copilot/agents/github-agent.agent.md +2 -2
  54. package/plugins/lisa-copilot/agents/jira-agent.agent.md +2 -2
  55. package/plugins/lisa-copilot/agents/linear-agent.agent.md +2 -2
  56. package/plugins/lisa-copilot/rules/eager/config-resolution.md +27 -8
  57. package/plugins/lisa-copilot/rules/eager/pre-flight-autofill.md +21 -7
  58. package/plugins/lisa-copilot/rules/reference/config-resolution.md +55 -16
  59. package/plugins/lisa-copilot/rules/reference/pre-flight-autofill.md +23 -8
  60. package/plugins/lisa-copilot/scripts/automation-status-claude-adapter.mjs +64 -8
  61. package/plugins/lisa-copilot/scripts/automation-status-codex-adapter.mjs +61 -5
  62. package/plugins/lisa-copilot/scripts/automation-status-report.mjs +30 -0
  63. package/plugins/lisa-copilot/scripts/automation-status-run-history.mjs +185 -0
  64. package/plugins/lisa-copilot/skills/lisa-automation-status/SKILL.md +11 -3
  65. package/plugins/lisa-copilot/skills/lisa-github-validate-issue/SKILL.md +1 -1
  66. package/plugins/lisa-copilot/skills/lisa-github-write-issue/SKILL.md +9 -3
  67. package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +6 -2
  68. package/plugins/lisa-copilot/skills/lisa-jira-validate-ticket/SKILL.md +1 -1
  69. package/plugins/lisa-copilot/skills/lisa-jira-write-ticket/SKILL.md +9 -6
  70. package/plugins/lisa-copilot/skills/lisa-linear-validate-issue/SKILL.md +1 -1
  71. package/plugins/lisa-copilot/skills/lisa-linear-write-issue/SKILL.md +9 -3
  72. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-cursor/agents/github-agent.md +2 -2
  74. package/plugins/lisa-cursor/agents/jira-agent.md +2 -2
  75. package/plugins/lisa-cursor/agents/linear-agent.md +2 -2
  76. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +55 -16
  77. package/plugins/lisa-cursor/rules/config-resolution.mdc +27 -8
  78. package/plugins/lisa-cursor/rules/pre-flight-autofill-reference.mdc +23 -8
  79. package/plugins/lisa-cursor/rules/pre-flight-autofill.mdc +21 -7
  80. package/plugins/lisa-cursor/scripts/automation-status-claude-adapter.mjs +64 -8
  81. package/plugins/lisa-cursor/scripts/automation-status-codex-adapter.mjs +61 -5
  82. package/plugins/lisa-cursor/scripts/automation-status-report.mjs +30 -0
  83. package/plugins/lisa-cursor/scripts/automation-status-run-history.mjs +185 -0
  84. package/plugins/lisa-cursor/skills/lisa-automation-status/SKILL.md +11 -3
  85. package/plugins/lisa-cursor/skills/lisa-github-validate-issue/SKILL.md +1 -1
  86. package/plugins/lisa-cursor/skills/lisa-github-write-issue/SKILL.md +9 -3
  87. package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +6 -2
  88. package/plugins/lisa-cursor/skills/lisa-jira-validate-ticket/SKILL.md +1 -1
  89. package/plugins/lisa-cursor/skills/lisa-jira-write-ticket/SKILL.md +9 -6
  90. package/plugins/lisa-cursor/skills/lisa-linear-validate-issue/SKILL.md +1 -1
  91. package/plugins/lisa-cursor/skills/lisa-linear-write-issue/SKILL.md +9 -3
  92. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  94. package/plugins/lisa-expo-agy/plugin.json +1 -1
  95. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  99. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  100. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  104. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  105. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  106. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  109. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  110. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  111. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  113. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  114. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  115. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  116. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  119. package/plugins/lisa-rails-agy/plugin.json +1 -1
  120. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  121. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  123. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  124. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  125. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  126. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  127. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  128. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  129. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  130. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  131. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  132. package/plugins/src/base/agents/github-agent.md +2 -2
  133. package/plugins/src/base/agents/jira-agent.md +2 -2
  134. package/plugins/src/base/agents/linear-agent.md +2 -2
  135. package/plugins/src/base/rules/eager/config-resolution.md +27 -8
  136. package/plugins/src/base/rules/eager/pre-flight-autofill.md +21 -7
  137. package/plugins/src/base/rules/reference/config-resolution.md +55 -16
  138. package/plugins/src/base/rules/reference/pre-flight-autofill.md +23 -8
  139. package/plugins/src/base/scripts/automation-status-claude-adapter.mjs +64 -8
  140. package/plugins/src/base/scripts/automation-status-codex-adapter.mjs +61 -5
  141. package/plugins/src/base/scripts/automation-status-report.mjs +30 -0
  142. package/plugins/src/base/scripts/automation-status-run-history.mjs +185 -0
  143. package/plugins/src/base/skills/lisa-automation-status/SKILL.md +11 -3
  144. package/plugins/src/base/skills/lisa-github-validate-issue/SKILL.md +1 -1
  145. package/plugins/src/base/skills/lisa-github-write-issue/SKILL.md +9 -3
  146. package/plugins/src/base/skills/lisa-implement/SKILL.md +6 -2
  147. package/plugins/src/base/skills/lisa-jira-validate-ticket/SKILL.md +1 -1
  148. package/plugins/src/base/skills/lisa-jira-write-ticket/SKILL.md +9 -6
  149. package/plugins/src/base/skills/lisa-linear-validate-issue/SKILL.md +1 -1
  150. package/plugins/src/base/skills/lisa-linear-write-issue/SKILL.md +9 -3
@@ -53,13 +53,32 @@ empirical evidence only—native Playwright/Cypress/Maestro remains the regressi
53
53
 
54
54
  ## Env → base branch
55
55
 
56
- For implementation work, map the work item's `## Target Backend Environment` to
57
- the PR base branch through `.lisa.config.json` `deploy.branches`. For bugs, a
58
- reported environment extracted from the description/reproduction steps wins over
59
- an autofill default. If a reported environment is absent from `deploy.branches`,
60
- stop and report the missing mapping instead of silently falling back to the
61
- default or integration branch. A non-integration environment bug is fixed,
62
- merged, and verified on that environment branch first, then forward
63
- cherry-picked down to the integration branch via a linked follow-up.
56
+ The `## Target Backend Environment` grammar is durable: human-confirmed values
57
+ are a bare configured key or `Confirmed: <env>`; automation writes either
58
+ `Inferred: <env> evidence: <title|body|reproduction|hostname>` or
59
+ `Assumption: <env> remote default branch <branch>`. When no unique environment
60
+ maps to the default branch, use `Assumption: remote default branch <branch>`.
61
+ Human confirmation replaces an automated annotation with a bare key or
62
+ `Confirmed: <env>`. For legacy bare values, use managed draft markers and current
63
+ ticket content only; provider edit history is not required. A marker proves
64
+ automation and requires re-annotation; otherwise unknown provenance with
65
+ conflicting evidence stops for confirmation.
66
+
67
+ Resolution order is human-confirmed value; validated `Inferred:` evidence; then
68
+ one unambiguous signal from the human-authored title, body, and reproduction
69
+ steps or a URL hostname. Exclude the complete `Target Backend Environment`
70
+ section and other machine-authored metadata/draft blocks from the scan so an
71
+ annotation cannot validate or conflict with itself. Evidence supersedes only an
72
+ `Assumption:`. A signal must be an exact
73
+ `deploy.branches` key as a complete token/hostname label. The sole normalization
74
+ is `prod` ↔ `production` when exactly one is configured; no other aliases exist.
75
+ Conflicting signals stop. Never infer from arbitrary branch text, URL paths or
76
+ query strings, or substrings. With no signals, use the remote default branch and
77
+ record an assumption: include `<env>` only for a unique reverse-map, otherwise
78
+ use the branch-only form without inventing an environment or blocking. Require
79
+ any selected environment mapping and its remote branch to exist.
80
+ A non-integration environment bug is fixed, merged, and verified on that
81
+ environment branch first, then
82
+ forward cherry-picked down to the integration branch via a linked follow-up.
64
83
 
65
84
  Full reference: [reference/config-resolution.md](config-resolution-reference.mdc).
@@ -54,14 +54,29 @@ spec.
54
54
  - **Validation Journey (draft)** — run the vendor `*-add-journey` skill to draft
55
55
  the click-path, markers, and viewports from the reproduction steps. This is a
56
56
  draft for human approval, not the final ratified contract.
57
- - **Target Backend Environment** — for bugs, first parse the reported
58
- environment from the title, description/body, screenshots captions, links, and
59
- reproduction steps. Bare words such as `dev`, `staging`, `prod`, and
60
- `production` count, as do environment-bearing URLs such as
61
- `staging.<domain>`, `gql.staging.*`, and `dev.<domain>`. That reported
62
- environment is authoritative for the draft. Only recommend the repo's
63
- remote default branch when no environment is discoverable anywhere in the
64
- work item, and record the default as an explicit assumption.
57
+ - **Target Backend Environment** — preserve a bare configured key or
58
+ `Confirmed: <env>` as human-confirmed. Automated evidence must write
59
+ `Inferred: <env> evidence: <title|body|reproduction|hostname>`; a generic
60
+ fallback must write `Assumption: <env> remote default branch <branch>`.
61
+ If the default branch has no unique environment reverse-map, instead write
62
+ `Assumption: remote default branch <branch>` without inventing an environment.
63
+ Human confirmation replaces an automated annotation with a bare key or
64
+ `Confirmed: <env>`. For legacy bare values, use managed draft markers and
65
+ current ticket content only; provider edit history is not required. A marker
66
+ proves automation and requires re-annotation; otherwise unknown provenance
67
+ plus conflicting evidence stops for confirmation. A human-confirmed value wins, followed by validated
68
+ `Inferred:` evidence. Otherwise inspect the title, body, and reproduction
69
+ steps or a URL hostname for one unambiguous exact `deploy.branches` key,
70
+ excluding the complete `Target Backend Environment` section and all other
71
+ machine-authored metadata/draft blocks so annotations cannot become evidence.
72
+ Clear evidence supersedes only an `Assumption:` value. The sole normalization
73
+ is `prod` ↔ `production` when exactly one key is configured; no other aliases
74
+ exist. Never infer from arbitrary branch text, URL paths or query strings, or
75
+ substrings. Multiple conflicting signals **stop** autofill. With no
76
+ environment signal, use the remote default and record the applicable
77
+ `Assumption:` form; a missing/ambiguous reverse-map alone never blocks.
78
+ Require any selected environment mapping and remote branch. Never overwrite
79
+ human prose.
65
80
 
66
81
  **Tier B — irreducibly human (cannot invent — but still propose a default):**
67
82
 
@@ -26,13 +26,27 @@ information was already on the ticket; only the structure was missing).
26
26
  Gherkin Acceptance Criteria (one scenario per fix), expected-vs-actual +
27
27
  environment, Repository, Relationship Search (actually run the git+tracker
28
28
  search — don't fabricate "none found"), Validation Journey **draft** (via the
29
- vendor `*-add-journey` skill), Target Backend Environment. For bugs, parse the
30
- reported environment from the item body/reproduction steps before recommending
31
- any default: bare environment names (`dev`, `staging`, `prod`, `production`)
32
- and environment-bearing URLs (for example `staging.<domain>`, `gql.staging.*`,
33
- `dev.<domain>`) are authoritative. Only recommend the repo's remote default branch when
34
- no environment is discoverable anywhere in the work item, and record that as
35
- an assumption.
29
+ vendor `*-add-journey` skill), Target Backend Environment. Preserve a bare
30
+ configured key or `Confirmed: <env>` as human-confirmed. Automation writes
31
+ `Inferred: <env> evidence: <title|body|reproduction|hostname>` or
32
+ `Assumption: <env> remote default branch <branch>`. Without a unique
33
+ reverse-map use `Assumption: remote default branch <branch>`; human confirmation
34
+ replaces the annotation with a bare key or `Confirmed: <env>`. For legacy bare
35
+ values, use managed draft markers and current ticket content only; provider
36
+ edit history is not required. A marker proves automation and requires
37
+ re-annotation; otherwise unknown provenance plus conflicting evidence stops.
38
+ Human-confirmed wins, then validated `Inferred:`
39
+ evidence. Otherwise use one unambiguous exact `deploy.branches` key from the
40
+ title, body, and reproduction steps or a URL hostname, excluding the complete
41
+ `Target Backend Environment` section and other machine-authored metadata/draft
42
+ blocks so annotations cannot become evidence. Evidence supersedes only
43
+ `Assumption:`. Normalize only built-in
44
+ `prod` ↔ `production` when exactly one is configured; no other aliases exist.
45
+ Never infer from arbitrary branch text, URL paths/query strings, or substrings.
46
+ Conflicting signals **stop** autofill. With no environment signal, use the
47
+ remote default and record the applicable `Assumption:` form; a non-unique
48
+ reverse-map alone never blocks. Require any selected mapping and remote branch.
49
+ Never overwrite human prose.
36
50
  - **Tier B — irreducibly human:** real credentials/access that exist nowhere on
37
51
  the item or in repo test-user docs; a genuine product/scoping decision with no
38
52
  defensible default. Still propose a recommended default where one exists; ask
@@ -15,6 +15,10 @@ import {
15
15
  createAutomationGroupBins,
16
16
  renderAutomationGroups,
17
17
  } from "./automation-status-expected-fleet.mjs";
18
+ import {
19
+ resolveAutomationRunDisplay,
20
+ resolveRecoveryEscalation,
21
+ } from "./automation-status-run-history.mjs";
18
22
 
19
23
  const CLAUDE_RUNTIME_LABEL = "Claude /schedule";
20
24
  const CLAUDE_ACTIVE_STATUSES = new Set([
@@ -51,9 +55,10 @@ const NEGATED_FAILURE_PATTERN =
51
55
  * @param {{
52
56
  * readonly expectedFleet: ExpectedFleet
53
57
  * readonly scheduleListing?: string | readonly unknown[] | Record<string, unknown> | null
58
+ * readonly projectRoot?: string
54
59
  * readonly now?: string | Date
55
60
  * }} input
56
- * @returns {{
61
+ * @returns {Promise<{
57
62
  * readonly runtime: string
58
63
  * readonly generatedAt: string
59
64
  * readonly groups: readonly {
@@ -66,20 +71,27 @@ const NEGATED_FAILURE_PATTERN =
66
71
  * readonly expectedCadence?: string
67
72
  * readonly expectedCommand?: string
68
73
  * readonly observed?: string
74
+ * readonly runbook?: string
75
+ * readonly lastOutcome?: { readonly ts: string, readonly outcome: string, readonly summary: string }
76
+ * readonly outcomeHistory?: readonly string[]
77
+ * readonly olderRecordCount?: number
69
78
  * readonly remediation?: string
70
79
  * }[]
71
80
  * }[]
72
81
  * readonly observedAutomations: readonly ObservedClaudeAutomation[]
73
- * }}}
82
+ * }>}
74
83
  */
75
- export function inspectClaudeAutomationFleet(input) {
84
+ export async function inspectClaudeAutomationFleet(input) {
76
85
  const expectedFleet = input.expectedFleet;
77
86
  const now = normalizeDate(input.now);
87
+ const projectRoot = input.projectRoot ?? process.cwd();
78
88
  const observedAutomations = listClaudeAutomations({
79
89
  scheduleListing: input.scheduleListing,
80
90
  automationPrefix: expectedFleet.automationPrefix,
81
91
  });
82
92
 
93
+ const runDisplays = await resolveFleetRunDisplays(projectRoot, expectedFleet);
94
+
83
95
  const expectedGroups = createAutomationGroupBins();
84
96
 
85
97
  const comparisons = compareAutomationFleet({
@@ -96,17 +108,23 @@ export function inspectClaudeAutomationFleet(input) {
96
108
  expected,
97
109
  comparison,
98
110
  now,
111
+ runDisplay: runDisplays.get(expected.id),
99
112
  })
100
113
  );
101
114
  }
102
115
 
103
116
  for (const unsupported of expectedFleet.unsupported) {
117
+ const runDisplay = runDisplays.get(unsupported.id);
104
118
  assignToAutomationGroup(expectedGroups, unsupported.group, {
105
119
  id: unsupported.automationId,
106
120
  status: "UNSUPPORTED",
107
121
  summary: unsupported.reason,
108
122
  expectedCadence: unsupported.expectedCadence,
109
123
  observed: "No automation is expected for this repo/runtime combination.",
124
+ runbook: runDisplay?.runbook,
125
+ lastOutcome: runDisplay?.lastOutcome,
126
+ outcomeHistory: runDisplay?.outcomeHistory,
127
+ olderRecordCount: runDisplay?.olderRecordCount,
110
128
  });
111
129
  }
112
130
 
@@ -195,15 +213,43 @@ function extractClaudeScheduleCadence(command) {
195
213
  );
196
214
  }
197
215
 
216
+ /**
217
+ * Read the read-only run-history display for every expected and unsupported
218
+ * loop up front, keyed by the loop's short id, so the per-entry item builder
219
+ * stays synchronous.
220
+ *
221
+ * @param {string} projectRoot
222
+ * @param {ExpectedFleet} expectedFleet
223
+ * @returns {Promise<Map<string, import("./automation-status-run-history.mjs").AutomationRunDisplay>>}
224
+ */
225
+ async function resolveFleetRunDisplays(projectRoot, expectedFleet) {
226
+ const entries = [...expectedFleet.expected, ...expectedFleet.unsupported];
227
+ const displays = await Promise.all(
228
+ entries.map(entry =>
229
+ resolveAutomationRunDisplay({
230
+ projectRoot,
231
+ loopId: entry.id,
232
+ runbookPath: entry.runbookPath,
233
+ })
234
+ )
235
+ );
236
+ return new Map(entries.map((entry, index) => [entry.id, displays[index]]));
237
+ }
238
+
198
239
  function createObservedStatusItem(input) {
199
240
  const expected = input.expected;
200
241
  const comparison = input.comparison;
201
242
  const observed = comparison.observedAutomation;
243
+ const runDisplay = input.runDisplay;
202
244
  const runSignal = classifyAutomationRunSignal({
203
245
  expected,
204
246
  observedAutomation: observed,
205
247
  now: input.now,
206
248
  });
249
+ // Three or more consecutive recorded recovery-required runs flip the loop to
250
+ // FAILING even when the /schedule entry looks fine — the fleet-health signal
251
+ // the scheduler surface cannot see. It wins over the scheduler run-signal.
252
+ const escalation = resolveRecoveryEscalation(runDisplay);
207
253
 
208
254
  const observedDetails = [comparison.observed];
209
255
  if (observed?.status) {
@@ -223,20 +269,30 @@ function createObservedStatusItem(input) {
223
269
  }
224
270
 
225
271
  const status =
272
+ escalation?.status ??
226
273
  runSignal?.status ??
227
274
  /** @type {"HEALTHY" | "MISSING" | "DRIFTED"} */ (comparison.status);
228
275
 
229
276
  return {
230
277
  id: expected.automationId,
231
278
  status,
232
- summary: composeAutomationSummary({
233
- comparison,
234
- runSignal,
235
- }),
279
+ summary: escalation
280
+ ? escalation.summary
281
+ : composeAutomationSummary({
282
+ comparison,
283
+ runSignal,
284
+ }),
236
285
  expectedCadence: expected.expectedCadence,
237
286
  expectedCommand: expected.expectedCommand,
238
287
  observed: observedDetails.join(" "),
239
- remediation: runSignal?.remediation ?? comparison.remediation,
288
+ runbook: runDisplay?.runbook,
289
+ lastOutcome: runDisplay?.lastOutcome,
290
+ outcomeHistory: runDisplay?.outcomeHistory,
291
+ olderRecordCount: runDisplay?.olderRecordCount,
292
+ remediation:
293
+ escalation?.remediation ??
294
+ runSignal?.remediation ??
295
+ comparison.remediation,
240
296
  };
241
297
  }
242
298
 
@@ -22,6 +22,10 @@ import {
22
22
  createAutomationGroupBins,
23
23
  renderAutomationGroups,
24
24
  } from "./automation-status-expected-fleet.mjs";
25
+ import {
26
+ resolveAutomationRunDisplay,
27
+ resolveRecoveryEscalation,
28
+ } from "./automation-status-run-history.mjs";
25
29
 
26
30
  const CODEx_RUNTIME_LABEL = "Codex automations";
27
31
  const RUN_TIMESTAMP_PATTERN = /20\d{2}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z/;
@@ -132,6 +136,7 @@ async function execGitWithRetry(args, options, attempts = 4) {
132
136
  * @param {{
133
137
  * readonly expectedFleet: ExpectedFleet
134
138
  * readonly automationsDir?: string
139
+ * readonly projectRoot?: string
135
140
  * readonly now?: string | Date
136
141
  * }} input
137
142
  * @returns {Promise<{
@@ -147,6 +152,10 @@ async function execGitWithRetry(args, options, attempts = 4) {
147
152
  * readonly expectedCadence?: string
148
153
  * readonly expectedCommand?: string
149
154
  * readonly observed?: string
155
+ * readonly runbook?: string
156
+ * readonly lastOutcome?: { readonly ts: string, readonly outcome: string, readonly summary: string }
157
+ * readonly outcomeHistory?: readonly string[]
158
+ * readonly olderRecordCount?: number
150
159
  * readonly remediation?: string
151
160
  * }[]
152
161
  * }[]
@@ -156,11 +165,14 @@ async function execGitWithRetry(args, options, attempts = 4) {
156
165
  export async function inspectCodexAutomationFleet(input) {
157
166
  const expectedFleet = input.expectedFleet;
158
167
  const now = normalizeDate(input.now);
168
+ const projectRoot = input.projectRoot ?? process.cwd();
159
169
  const observedAutomations = await listCodexAutomations({
160
170
  automationsDir: input.automationsDir,
161
171
  automationPrefix: expectedFleet.automationPrefix,
162
172
  });
163
173
 
174
+ const runDisplays = await resolveFleetRunDisplays(projectRoot, expectedFleet);
175
+
164
176
  const expectedGroups = createAutomationGroupBins();
165
177
 
166
178
  const comparisons = compareAutomationFleet({
@@ -177,17 +189,23 @@ export async function inspectCodexAutomationFleet(input) {
177
189
  expected,
178
190
  comparison,
179
191
  now,
192
+ runDisplay: runDisplays.get(expected.id),
180
193
  })
181
194
  );
182
195
  }
183
196
 
184
197
  for (const unsupported of expectedFleet.unsupported) {
198
+ const runDisplay = runDisplays.get(unsupported.id);
185
199
  assignToAutomationGroup(expectedGroups, unsupported.group, {
186
200
  id: unsupported.automationId,
187
201
  status: "UNSUPPORTED",
188
202
  summary: unsupported.reason,
189
203
  expectedCadence: unsupported.expectedCadence,
190
204
  observed: "No automation is expected for this repo/runtime combination.",
205
+ runbook: runDisplay?.runbook,
206
+ lastOutcome: runDisplay?.lastOutcome,
207
+ outcomeHistory: runDisplay?.outcomeHistory,
208
+ olderRecordCount: runDisplay?.olderRecordCount,
191
209
  });
192
210
  }
193
211
 
@@ -374,15 +392,43 @@ async function readCodexAutomation(automationDir) {
374
392
  };
375
393
  }
376
394
 
395
+ /**
396
+ * Read the read-only run-history display for every expected and unsupported
397
+ * loop up front, keyed by the loop's short id, so the per-entry item builder
398
+ * stays synchronous.
399
+ *
400
+ * @param {string} projectRoot
401
+ * @param {ExpectedFleet} expectedFleet
402
+ * @returns {Promise<Map<string, import("./automation-status-run-history.mjs").AutomationRunDisplay>>}
403
+ */
404
+ async function resolveFleetRunDisplays(projectRoot, expectedFleet) {
405
+ const entries = [...expectedFleet.expected, ...expectedFleet.unsupported];
406
+ const displays = await Promise.all(
407
+ entries.map(entry =>
408
+ resolveAutomationRunDisplay({
409
+ projectRoot,
410
+ loopId: entry.id,
411
+ runbookPath: entry.runbookPath,
412
+ })
413
+ )
414
+ );
415
+ return new Map(entries.map((entry, index) => [entry.id, displays[index]]));
416
+ }
417
+
377
418
  function createObservedStatusItem(input) {
378
419
  const expected = input.expected;
379
420
  const comparison = input.comparison;
380
421
  const observed = comparison.observedAutomation;
422
+ const runDisplay = input.runDisplay;
381
423
  const runSignal = classifyAutomationRunSignal({
382
424
  expected,
383
425
  observedAutomation: observed,
384
426
  now: input.now,
385
427
  });
428
+ // Three or more consecutive recorded recovery-required runs flip the loop to
429
+ // FAILING even when the scheduler entry looks fine — the fleet-health signal
430
+ // the raw backing store cannot see. It wins over the scheduler run-signal.
431
+ const escalation = resolveRecoveryEscalation(runDisplay);
386
432
 
387
433
  const observedDetails = [comparison.observed];
388
434
  if (observed?.status) {
@@ -401,20 +447,30 @@ function createObservedStatusItem(input) {
401
447
  }
402
448
 
403
449
  const status =
450
+ escalation?.status ??
404
451
  runSignal?.status ??
405
452
  /** @type {"HEALTHY" | "MISSING" | "DRIFTED"} */ (comparison.status);
406
453
 
407
454
  return {
408
455
  id: expected.automationId,
409
456
  status,
410
- summary: composeAutomationSummary({
411
- comparison,
412
- runSignal,
413
- }),
457
+ summary: escalation
458
+ ? escalation.summary
459
+ : composeAutomationSummary({
460
+ comparison,
461
+ runSignal,
462
+ }),
414
463
  expectedCadence: expected.expectedCadence,
415
464
  expectedCommand: expected.expectedCommand,
416
465
  observed: observedDetails.join(" "),
417
- remediation: runSignal?.remediation ?? comparison.remediation,
466
+ runbook: runDisplay?.runbook,
467
+ lastOutcome: runDisplay?.lastOutcome,
468
+ outcomeHistory: runDisplay?.outcomeHistory,
469
+ olderRecordCount: runDisplay?.olderRecordCount,
470
+ remediation:
471
+ escalation?.remediation ??
472
+ runSignal?.remediation ??
473
+ comparison.remediation,
418
474
  };
419
475
  }
420
476
 
@@ -26,12 +26,22 @@ export const AUTOMATION_FLEET_VERDICTS = [
26
26
  * @typedef {"HEALTHY" | "PARTIAL_SUPPORT" | "ATTENTION_NEEDED"} AutomationFleetVerdict
27
27
  *
28
28
  * @typedef {{
29
+ * readonly ts: string
30
+ * readonly outcome: string
31
+ * readonly summary: string
32
+ * }} AutomationLastOutcome
33
+ *
34
+ * @typedef {{
29
35
  * readonly id: string
30
36
  * readonly status: AutomationHealthStatus
31
37
  * readonly summary: string
32
38
  * readonly expectedCadence?: string
33
39
  * readonly expectedCommand?: string
34
40
  * readonly observed?: string
41
+ * readonly runbook?: string
42
+ * readonly lastOutcome?: AutomationLastOutcome
43
+ * readonly outcomeHistory?: readonly string[]
44
+ * readonly olderRecordCount?: number
35
45
  * readonly remediation?: string
36
46
  * }} AutomationStatusItem
37
47
  *
@@ -130,6 +140,26 @@ export function renderAutomationStatusReport(input) {
130
140
  if (item.observed) {
131
141
  lines.push(` Observed: ${item.observed}`);
132
142
  }
143
+ // Contract + run-history block: observable facts about where the runbook
144
+ // lives and how recent runs ended, kept between Observed and Remediation
145
+ // so operators read the state before the fix. Every line degrades
146
+ // explicitly — an absent runbook or empty history is stated, never blank.
147
+ if (item.runbook !== undefined) {
148
+ lines.push(` Runbook: ${item.runbook}`);
149
+ lines.push(
150
+ item.lastOutcome
151
+ ? ` Last run: ${item.lastOutcome.outcome} — ${item.lastOutcome.summary} (${item.lastOutcome.ts})`
152
+ : " Last run: no recorded runs yet"
153
+ );
154
+ if (item.outcomeHistory && item.outcomeHistory.length > 0) {
155
+ const historyLine = ` History: ${item.outcomeHistory.join(", ")} (newest first)`;
156
+ lines.push(
157
+ item.olderRecordCount && item.olderRecordCount > 0
158
+ ? `${historyLine}; … and ${item.olderRecordCount} older records`
159
+ : historyLine
160
+ );
161
+ }
162
+ }
133
163
  if (item.remediation) {
134
164
  lines.push(` Remediation: ${item.remediation}`);
135
165
  }
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared read-only run-history projection for `/lisa:automation-status`.
4
+ *
5
+ * The durable run-outcome substrate (RBC-3, `automation-run-record.mjs`) stores
6
+ * one bounded JSONL file per registered loop under `.lisa/automations/runs/`.
7
+ * This module turns that substrate — plus the checked-in per-loop runbook
8
+ * (`automation-runbook-contract`) — into the per-item display fields both
9
+ * runtime adapters attach to their status rows, so the Codex and Claude
10
+ * adapters never diverge on how the contract, last outcome, and bounded history
11
+ * are rendered.
12
+ *
13
+ * STRICTLY READ-ONLY: it stats the runbook and reads the run-record file. It
14
+ * never creates the runs directory, a runbook, or a run record — a status read
15
+ * must leave the fleet byte-identical.
16
+ */
17
+
18
+ import fs from "node:fs/promises";
19
+ import path from "node:path";
20
+
21
+ import {
22
+ automationRunRecordPath,
23
+ readAutomationRunRecords,
24
+ } from "./automation-run-record.mjs";
25
+
26
+ /**
27
+ * Inline history cap: the newest N outcomes are listed, the remainder is only
28
+ * counted. A bounded view of an already-bounded file, with the trim stated in
29
+ * the output rather than applied silently.
30
+ */
31
+ export const AUTOMATION_RUN_HISTORY_DISPLAY_LIMIT = 5;
32
+
33
+ /** The recorded outcome that signals a loop could not finish its own work. */
34
+ export const RECOVERY_REQUIRED_OUTCOME = "recovery-required";
35
+
36
+ /**
37
+ * How many trailing `recovery-required` runs in a row flip a loop to `FAILING`.
38
+ * The RBC-5 acceptance criteria pin "last three runs" as the escalation
39
+ * trigger; that wins over the ticket prose's looser "five in a row" pattern.
40
+ */
41
+ export const RECOVERY_REQUIRED_ESCALATION_THRESHOLD = 3;
42
+
43
+ const RUNBOOK_NOT_SCAFFOLDED_LINE =
44
+ "not scaffolded — run /lisa:setup-automations";
45
+ const NO_RECORDED_RUNS_LINE = "no recorded runs yet";
46
+
47
+ /**
48
+ * @typedef {{
49
+ * readonly ts: string
50
+ * readonly outcome: string
51
+ * readonly summary: string
52
+ * }} AutomationLastOutcome
53
+ *
54
+ * @typedef {{
55
+ * readonly runbook: string
56
+ * readonly lastOutcome?: AutomationLastOutcome
57
+ * readonly outcomeHistory: readonly string[]
58
+ * readonly olderRecordCount: number
59
+ * readonly recoveryRequiredStreak: number
60
+ * readonly recoverySummaries: readonly string[]
61
+ * readonly skippedCorruptLines: number
62
+ * }} AutomationRunDisplay
63
+ */
64
+
65
+ /**
66
+ * Resolve the read-only run-history display fields for one registered loop.
67
+ *
68
+ * @param {{
69
+ * readonly projectRoot?: string
70
+ * readonly loopId: string
71
+ * readonly runbookPath?: string
72
+ * }} input
73
+ * @returns {Promise<AutomationRunDisplay>}
74
+ */
75
+ export async function resolveAutomationRunDisplay(input) {
76
+ const projectRoot = path.resolve(input.projectRoot ?? process.cwd());
77
+ const runbook = await resolveRunbookLine(projectRoot, input.runbookPath);
78
+ const { records, skippedCorruptLines } = await readAutomationRunRecords(
79
+ automationRunRecordPath(projectRoot, input.loopId)
80
+ );
81
+
82
+ const newestFirst = records.toReversed();
83
+ const latest = records.at(-1);
84
+ const lastOutcome = latest
85
+ ? { ts: latest.ts, outcome: latest.outcome, summary: latest.summary }
86
+ : undefined;
87
+ const outcomeHistory = newestFirst
88
+ .slice(0, AUTOMATION_RUN_HISTORY_DISPLAY_LIMIT)
89
+ .map(record => record.outcome);
90
+ const olderRecordCount = Math.max(
91
+ 0,
92
+ records.length - AUTOMATION_RUN_HISTORY_DISPLAY_LIMIT
93
+ );
94
+ const trailingRecovery = collectTrailingRecoveryRuns(records);
95
+
96
+ return {
97
+ runbook,
98
+ lastOutcome,
99
+ outcomeHistory,
100
+ olderRecordCount,
101
+ recoveryRequiredStreak: trailingRecovery.length,
102
+ recoverySummaries: trailingRecovery.map(record => record.summary),
103
+ skippedCorruptLines,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Force a `FAILING` verdict when a loop has recorded three or more consecutive
109
+ * `recovery-required` runs — the fleet-health signal the scheduler surface
110
+ * cannot see on its own. Returns a run-signal-shaped escalation, or null when
111
+ * the streak is below the threshold.
112
+ *
113
+ * @param {AutomationRunDisplay | undefined} runDisplay
114
+ * @returns {{ readonly status: "FAILING", readonly summary: string, readonly remediation: string } | null}
115
+ */
116
+ export function resolveRecoveryEscalation(runDisplay) {
117
+ if (
118
+ !runDisplay ||
119
+ runDisplay.recoveryRequiredStreak < RECOVERY_REQUIRED_ESCALATION_THRESHOLD
120
+ ) {
121
+ return null;
122
+ }
123
+
124
+ // Number the citations rather than joining on a delimiter: recorded summaries
125
+ // carry their own `;` and `.`, so a `; `-separated list is ambiguous. `(1) …
126
+ // (2) …` stays legible no matter what punctuation a summary contains.
127
+ const citations = runDisplay.recoverySummaries
128
+ .map((summary, index) => `(${index + 1}) ${summary}`)
129
+ .join(" ");
130
+ return {
131
+ status: "FAILING",
132
+ summary: `the last ${runDisplay.recoveryRequiredStreak} recorded runs all required recovery`,
133
+ remediation: `Repeated recovery-required runs — ${citations} Inspect the loop's runbook and the cited runs, fix the recurring failure, then let the next scheduled run confirm recovery.`,
134
+ };
135
+ }
136
+
137
+ /**
138
+ * @param {string} projectRoot
139
+ * @param {string | undefined} runbookPath
140
+ * @returns {Promise<string>}
141
+ */
142
+ async function resolveRunbookLine(projectRoot, runbookPath) {
143
+ if (!runbookPath) {
144
+ return RUNBOOK_NOT_SCAFFOLDED_LINE;
145
+ }
146
+ const scaffolded = await runbookIsScaffolded(
147
+ path.join(projectRoot, runbookPath)
148
+ );
149
+ return scaffolded ? runbookPath : RUNBOOK_NOT_SCAFFOLDED_LINE;
150
+ }
151
+
152
+ /**
153
+ * Read-only existence check for a checked-in runbook. Any stat failure — the
154
+ * file is absent, unreadable, or not a regular file — degrades to "not
155
+ * scaffolded" so the status read never implies health it could not confirm and
156
+ * never throws mid-report.
157
+ *
158
+ * @param {string} runbookAbsolutePath
159
+ * @returns {Promise<boolean>}
160
+ */
161
+ async function runbookIsScaffolded(runbookAbsolutePath) {
162
+ try {
163
+ const stat = await fs.stat(runbookAbsolutePath);
164
+ return stat.isFile();
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * @param {readonly import("./automation-run-record.mjs").AutomationRunRecord[]} records
172
+ * @returns {readonly import("./automation-run-record.mjs").AutomationRunRecord[]} trailing recovery-required runs, newest first
173
+ */
174
+ function collectTrailingRecoveryRuns(records) {
175
+ const trailing = [];
176
+ for (let index = records.length - 1; index >= 0; index -= 1) {
177
+ if (records[index].outcome !== RECOVERY_REQUIRED_OUTCOME) {
178
+ break;
179
+ }
180
+ trailing.push(records[index]);
181
+ }
182
+ return trailing;
183
+ }
184
+
185
+ export { RUNBOOK_NOT_SCAFFOLDED_LINE, NO_RECORDED_RUNS_LINE };