@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
@@ -35,7 +35,7 @@ Required fields (stop and ask if missing — do not invent values):
35
35
  | Priority | CREATE | Default to project default if unstated |
36
36
  | Acceptance criteria | Story, Task, Bug, Sub-task, Improvement | Gherkin — see Phase 3 |
37
37
  | Validation Journey | Runtime-behavior changes | Delegate to `/jira-add-journey` |
38
- | Target backend environment | Runtime-behavior changes | `dev` / `staging` / `prod`; recorded in description (Phase 3). Skip only for doc/config/type-only tickets. |
38
+ | Target backend environment | Runtime-behavior changes | For every work type, use an exact `deploy.branches` key when an environment is known. Human: bare key or `Confirmed: <env>`. Automation: `Inferred: <env> — evidence: <title\|body\|reproduction\|hostname>`, `Assumption: <env> remote default branch <branch>` for a unique reverse-map, or `Assumption: remote default branch <branch>` otherwise. Human confirmation replaces an automated annotation with the bare key or `Confirmed: <env>`. |
39
39
  | Sign-in account / credentials | Tickets that touch authenticated surfaces | Name the account (or source — 1Password item, env var, seeded fixture) and role; recorded in description (Phase 3). Omit when sign-in is not required. |
40
40
  | Single-repo scope | Bug, Task, Sub-task, Improvement | These leaf work units MUST cover one repo only. If the work crosses repos, split it before creating. Epic / Spike / Story may span repos. |
41
41
  | Source Requirement | PRD-sourced tickets (`prd_source` provided) | `h2. Source Requirement` with PRD link + verbatim requirement quote(s) — see Phase 3; enforced at every level, sub-tasks included |
@@ -83,11 +83,14 @@ h2. Out of Scope
83
83
  [Explicit list of what this ticket does NOT cover. Forces scope discipline.]
84
84
 
85
85
  h2. Target Backend Environment
86
- [Required when the ticket changes runtime behavior. One of: dev / staging / prod.
87
- This is the environment QA/product reported against and the backend the
88
- implementer points their local stack at during verification before CI/CD.
89
- Backend-only tickets state the deployed env they target. Skip section
90
- entirely for doc-only, config-only, or type-only tickets.]
86
+ [Required when the ticket changes runtime behavior. Use an exact
87
+ `deploy.branches` key. A human-confirmed value is a bare key or
88
+ `Confirmed: <env>`. An automated evidence write is
89
+ `Inferred: <env> evidence: <title|body|reproduction|hostname>`; an automated
90
+ generic default is `Assumption: <env> remote default branch <branch>`.
91
+ Without a unique reverse-map use `Assumption: remote default branch <branch>`.
92
+ Human confirmation replaces the automated annotation with a bare key or
93
+ `Confirmed: <env>`. Skip only for doc/config/type-only tickets.]
91
94
 
92
95
  h2. Sign-in Required
93
96
  [Include this section ONLY if the work touches authenticated surfaces.
@@ -160,7 +160,7 @@ A **build-ready leaf work unit** that is not part of an Epic context stands alon
160
160
 
161
161
  #### S8 — Target Backend Environment
162
162
 
163
- When `runtime_behavior_change = true`, description must contain `## Target Backend Environment` with one of `dev`, `staging`, `prod`. Skipped for doc-only / config-only / type-only / Epic.
163
+ When `runtime_behavior_change = true`, the description must contain `## Target Backend Environment`. Read accepted environments from the exact configured keys of `.lisa.config.json` `deploy.branches`, never from a hardcoded list. Accept a human-confirmed bare exact configured key or `Confirmed: <env>`, automated `Inferred: <env> — evidence: <title|body|reproduction|hostname>`, automated `Assumption: <env> — remote default branch <branch>` for a unique reverse-map, or `Assumption: remote default branch <branch>` when no unique reverse-map exists. Human confirmation replaces an automated annotation with the bare key or `Confirmed: <env>`. For legacy bare values, use managed draft markers and current ticket content only; provider edit history is not required. A marker proves automation and requires re-annotation; otherwise unknown provenance plus conflicting evidence fails for confirmation. Validate the annotation shape/source and remote-default branch; validate `<env>` as an exact configured key whenever present. A valid branch-only assumption must not fail solely because its reverse-map is absent or ambiguous. Normalize built-in `prod` ↔ `production` only when exactly one of those keys is configured. No other aliases are valid. Skipped for doc-only / config-only / type-only / Epic.
164
164
 
165
165
  #### S9 — Sign-in Required
166
166
 
@@ -57,7 +57,7 @@ Required fields (stop and ask if missing — never invent values):
57
57
  | Priority | CREATE | Native Linear priority: 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low |
58
58
  | Acceptance criteria | Story, Task, Bug, Sub-task, Improvement | Gherkin — see Phase 3 |
59
59
  | Validation Journey | Runtime-behavior changes | Delegate to `/linear-add-journey` |
60
- | Target backend environment | Runtime-behavior changes | `dev` / `staging` / `prod`; recorded in description (Phase 3). Skip only for doc/config/type-only items. |
60
+ | Target backend environment | Runtime-behavior changes | For every work type, use an exact `deploy.branches` key when an environment is known. Human: bare key or `Confirmed: <env>`. Automation: `Inferred: <env> — evidence: <title\|body\|reproduction\|hostname>`, `Assumption: <env> remote default branch <branch>` for a unique reverse-map, or `Assumption: remote default branch <branch>` otherwise. Human confirmation replaces an automated annotation with the bare key or `Confirmed: <env>`. |
61
61
  | Sign-in account / credentials | Items that touch authenticated surfaces | Name the account (or source — 1Password item, env var, seeded fixture) and role; recorded in description. Omit when sign-in is not required. |
62
62
  | Single-repo scope | Bug, Task, Sub-task | These types MUST cover one repo only. If the work crosses repos, split it before creating. Epic / Spike / Story may span repos. |
63
63
  | Source Requirement | PRD-sourced Issues (`prd_source` provided) | `## Source Requirement` with PRD link + verbatim requirement quote(s) — see Phase 3; enforced at every level, sub-issues included. |
@@ -103,8 +103,14 @@ Linear descriptions are markdown (NOT Jira wiki markup — no `h2.` headings, us
103
103
  [Explicit list of what this item does NOT cover. Forces scope discipline.]
104
104
 
105
105
  ## Target Backend Environment
106
- [Required when the item changes runtime behavior. One of: dev / staging / prod.
107
- Skip section entirely for doc-only, config-only, or type-only items.]
106
+ [Required when the item changes runtime behavior. Use an exact
107
+ `deploy.branches` key. A human-confirmed value is a bare key or
108
+ `Confirmed: <env>`. An automated evidence write is
109
+ `Inferred: <env> — evidence: <title|body|reproduction|hostname>`; an automated
110
+ generic default is `Assumption: <env> — remote default branch <branch>`.
111
+ Without a unique reverse-map use `Assumption: remote default branch <branch>`.
112
+ Human confirmation replaces the automated annotation with a bare key or
113
+ `Confirmed: <env>`. Skip only for doc/config/type-only items.]
108
114
 
109
115
  ## Sign-in Required
110
116
  [Include this section ONLY if the work touches authenticated surfaces.
@@ -53,7 +53,7 @@ Resolve build labels from `.lisa.config.json` `github.labels.build.*` (defaults:
53
53
 
54
54
  If `github-verify` returns `FAIL` on any of the above, do NOT continue to build. **Draft the missing spec content first, then block for confirmation** — never bounce a raw "go write all this" checklist back to the author:
55
55
 
56
- 1. **Best-effort autofill (before blocking).** Run the **draft-then-block procedure** in the `pre-flight-autofill` rule: draft every *authorable* missing sectionTechnical Approach, Out of Scope, Gherkin acceptance criteria, expected-vs-actual, Repository, Relationship Search (run the git + `gh`/tracker search, don't fabricate it), a Validation Journey **draft** via `github-add-journey`, and Target Backend Environment from the issue's own content (title, body, screenshots, design links, repro steps) plus the codebase. For bugs, parse the reported environment from bare env names and env-bearing URLs before recommending any default; the reported environment wins. Write it into the body via `github-write-issue` as clearly-labeled assumptions/recommendations (never overwrite the author's prose), then re-run `github-verify`. Whatever the agent could author is now structured spec; only genuinely human-only inputs remain.
56
+ 1. **Best-effort autofill (before blocking).** Run `pre-flight-autofill` for every work item. Preserve a human bare configured key or `Confirmed: <env>`; automation writes `Inferred: <env>evidence: <title|body|reproduction|hostname>`, `Assumption: <env> remote default branch <branch>` for a unique reverse-map, or `Assumption: remote default branch <branch>` otherwise. Human confirmation replaces the annotation with the bare key or `Confirmed: <env>`. For a legacy bare value, use managed draft markers and current ticket content only; provider edit history is not required. A marker proves automation and requires re-annotation; otherwise unknown provenance plus conflicting evidence stops for confirmation. Resolve one exact `deploy.branches` key from the human-authored title, body, and reproduction steps or URL hostname; exclude the complete `Target Backend Environment` section and other machine-authored metadata/draft blocks so annotations cannot become evidence. A reported bug environment is an example, not a restriction. Evidence supersedes only `Assumption:`. Normalize `prod` ↔ `production` only when exactly one is configured; no other aliases exist. Conflicts stop; never infer from arbitrary branch text, URL paths/query strings, or substrings. Write through `github-write-issue`, never overwrite human prose, then re-run `github-verify`.
57
57
  2. Relabel: remove the `claimed` label, add the `blocked` label **and** the `human_needed` marker label. Even after the agent drafted what it could, a pre-flight gate failure bounces the issue back to its author because it still needs a human to **confirm the drafted assumptions** or supply something no agent can invent — real missing credentials, access, or an irreducible product/scoping decision — so the marker tells a human scanning the board which blocked issues are waiting on them. The marker is additive to `blocked`, not a replacement. (See the `config-resolution` rule's "Build markers" for when the marker applies and when it must NOT.)
58
58
  ```bash
59
59
  _read_cfg() { local lv gv; lv=$(jq -r "$1 // empty" .lisa.config.local.json 2>/dev/null); gv=$(jq -r "$1 // empty" .lisa.config.json 2>/dev/null); echo "${lv:-${gv}}"; }
@@ -152,4 +152,4 @@ Note: `done` may be a string or an env-keyed map (`{ dev, staging, production }`
152
152
  - Never create or materially edit an issue by calling `gh issue create` / `gh issue edit` directly — always delegate to `github-write-issue` (or, from a vendor-neutral caller, `tracker-write`) so relationships, Gherkin criteria, and metadata gates are enforced.
153
153
  - If sign-in credentials are in the issue body, extract and pass them to the flow. If the issue touches an authenticated surface and credentials are missing, that is a Step 2 failure — block and reassign rather than guessing.
154
154
  - If the issue has a `## Validation Journey` section, pass it to the verifier agent. The Validation Journey's local-verification step must point at the target backend environment named in the body.
155
- - For bug issues, the reported environment named in the issue body or reproduction steps drives the implementation base branch via `deploy.branches`. If no environment can be found anywhere in the ticket, fall back to the configured default branch and record that assumption. If the reported environment is present but missing from `deploy.branches`, or its mapped branch is absent on the remote, stop and report the missing environment/branch mapping instead of defaulting. A non-integration environment fix must be merged and verified on that environment branch, then forward cherry-picked down to the integration branch through a linked follow-up.
155
+ - The environment handoff for every work item uses the same durable grammar: human bare configured key or `Confirmed: <env>`; automated `Inferred: <env> — evidence: <title|body|reproduction|hostname>`, `Assumption: <env> remote default branch <branch>` for a unique reverse-map, or `Assumption: remote default branch <branch>` otherwise. Human confirmation replaces an automated annotation with the bare key or `Confirmed: <env>`. For legacy bare values, managed draft markers and current ticket content decide provenance; provider edit history is not required. A marker proves automation; otherwise unknown provenance plus conflicting evidence stops for confirmation. Human-confirmed wins, then validated `Inferred:`; otherwise one exact `deploy.branches` key from the human-authored title, body, and reproduction steps or URL hostname drives the implementation base branch. Exclude the complete `Target Backend Environment` section and other machine-authored metadata/draft blocks so annotations cannot become evidence. Evidence supersedes only `Assumption:`. Normalize `prod` ↔ `production` only when exactly one is configured; no other aliases exist. Conflicts stop. Never infer from arbitrary branch text, URL paths/query strings, or substrings. With no signal, use the remote default and record the applicable assumption without blocking on a non-unique reverse-map. Require any selected mapping and remote branch. A reported bug environment is an example of the all-work-type rule. Non-integration fixes still require the linked forward cherry-pick.
@@ -48,7 +48,7 @@ Use the `jira-verify` skill to check the ticket against organizational standards
48
48
  **Gating behavior — this is the one place auto-transitioning is allowed:**
49
49
 
50
50
  If `jira-verify` returns `FAIL` on any of the above, do NOT continue to build. **Draft the missing spec content first, then block for confirmation** — never bounce a raw "go write all this" checklist back to the reporter:
51
- 1. **Best-effort autofill (before blocking).** Run the **draft-then-block procedure** in the `pre-flight-autofill` rule: draft every *authorable* missing sectionTechnical Approach, Out of Scope, Gherkin acceptance criteria, expected-vs-actual, Repository, Relationship Search (run the git + JQL search, don't fabricate it), a Validation Journey **draft** via `jira-add-journey`, and Target Backend Environment from the ticket's own content (title, description, screenshots, design links, repro steps) plus the codebase. For bugs, parse the reported environment from bare env names and env-bearing URLs before recommending any default; the reported environment wins. Write it into the description via `jira-write-ticket` as clearly-labeled assumptions/recommendations (never overwrite the reporter's prose), then re-run `jira-verify`. Whatever the agent could author is now structured spec; only genuinely human-only inputs remain.
51
+ 1. **Best-effort autofill (before blocking).** Run `pre-flight-autofill` for every work item. Preserve a human bare configured key or `Confirmed: <env>`; automation writes `Inferred: <env>evidence: <title|body|reproduction|hostname>`, `Assumption: <env> remote default branch <branch>` for a unique reverse-map, or `Assumption: remote default branch <branch>` otherwise. Human confirmation replaces the annotation with the bare key or `Confirmed: <env>`. For a legacy bare value, use managed draft markers and current ticket content only; provider edit history is not required. A marker proves automation and requires re-annotation; otherwise unknown provenance plus conflicting evidence stops for confirmation. Resolve one exact `deploy.branches` key from the human-authored title, body, and reproduction steps or URL hostname; exclude the complete `Target Backend Environment` section and other machine-authored metadata/draft blocks so annotations cannot become evidence. A reported bug environment is an example, not a restriction. Evidence supersedes only `Assumption:`. Normalize `prod` `production` only when exactly one is configured; no other aliases exist. Conflicts stop; never infer from arbitrary branch text, URL paths/query strings, or substrings. Write through `jira-write-ticket`, never overwrite human prose, then re-run `jira-verify`.
52
52
  2. Transition the ticket status to the configured `blocked` status (typically `Blocked` — read from `.lisa.config.json` `jira.workflow.blocked` if present, otherwise the project's standard blocked status). Use `mcp__atlassian__transitionJiraIssue` or equivalent.
53
53
  3. **Add the `human_needed` marker label.** Even after the agent drafted what it could, a pre-flight gate failure bounces the ticket back to its reporter because the ticket still needs a human to **confirm the drafted assumptions** or supply something no agent can invent — real missing credentials, access, or an irreducible product/scoping decision. Add the configured label (`jira.labels.human_needed`, default `Human Needed`) to the ticket's `labels` field via `mcp__atlassian__editJiraIssue` — a lightweight metadata update permitted under this same gate exception. This is additive to (not a replacement for) the `blocked` status, so a human scanning the board sees at a glance which blocked tickets are waiting on them. If the label does not exist in the project, record that and proceed — the marker is best-effort. (See the `config-resolution` rule's "Build markers" for when the marker applies and when it must NOT.)
54
54
  4. Reassign the ticket to the **Reporter** (the human who filed it — not the Creator field, which may be a bot/integration).
@@ -132,4 +132,4 @@ Note: `done` may be a string or an env-keyed map (`{ dev, staging, production }`
132
132
  - Never create or materially edit a ticket by calling MCP write tools directly — always delegate to `jira-write-ticket` so relationships, Gherkin criteria, and metadata gates are enforced
133
133
  - If sign-in credentials are in the ticket, extract and pass them to the flow. If the ticket touches an authenticated surface and credentials are missing, that is a Step 2 failure — block and reassign rather than guessing.
134
134
  - If the ticket has a Validation Journey section, pass it to the verifier agent. The Validation Journey's local-verification step must point at the target backend environment named in the description (for FE work, that's the deployed backend QA reported against).
135
- - For bug tickets, the reported environment named in the description or reproduction steps drives the implementation base branch via `deploy.branches`. If no environment can be found anywhere in the ticket, fall back to the configured default branch and record that assumption. If the reported environment is present but missing from `deploy.branches`, or its mapped branch is absent on the remote, stop and report the missing environment/branch mapping instead of defaulting. A non-integration environment fix must be merged and verified on that environment branch, then forward cherry-picked down to the integration branch through a linked follow-up.
135
+ - The environment handoff for every work item uses the same durable grammar: human bare configured key or `Confirmed: <env>`; automated `Inferred: <env> — evidence: <title|body|reproduction|hostname>`, `Assumption: <env> remote default branch <branch>` for a unique reverse-map, or `Assumption: remote default branch <branch>` otherwise. Human confirmation replaces an automated annotation with the bare key or `Confirmed: <env>`. For legacy bare values, managed draft markers and current ticket content decide provenance; provider edit history is not required. A marker proves automation; otherwise unknown provenance plus conflicting evidence stops for confirmation. Human-confirmed wins, then validated `Inferred:`; otherwise one exact `deploy.branches` key from the human-authored title, body, and reproduction steps or URL hostname drives the implementation base branch. Exclude the complete `Target Backend Environment` section and other machine-authored metadata/draft blocks so annotations cannot become evidence. Evidence supersedes only `Assumption:`. Normalize `prod` ↔ `production` only when exactly one is configured; no other aliases exist. Conflicts stop. Never infer from arbitrary branch text, URL paths/query strings, or substrings. With no signal, use the remote default and record the applicable assumption without blocking on a non-unique reverse-map. Require any selected mapping and remote branch. A reported bug environment is an example of the all-work-type rule. Non-integration fixes still require the linked forward cherry-pick.
@@ -50,7 +50,7 @@ Use the `linear-verify` skill to check the item against organizational standards
50
50
  Resolve build labels from `.lisa.config.json` `linear.labels.build.*` (defaults: `status:ready` / `status:in-progress` / `status:code-review`); resolve the `blocked` label from the same section (`linear.labels.build.blocked`, default `status:blocked`) and the `human_needed` marker label from the same section (`linear.labels.build.human_needed`, default `human-needed`).
51
51
 
52
52
  If `linear-verify` returns `FAIL` on any of the above, do NOT continue to build. **Draft the missing spec content first, then block for confirmation** — never bounce a raw "go write all this" checklist back to the creator:
53
- 1. **Best-effort autofill (before blocking).** Run the **draft-then-block procedure** in the `pre-flight-autofill` rule: draft every *authorable* missing sectionTechnical Approach, Out of Scope, Gherkin acceptance criteria, expected-vs-actual, Repository, Relationship Search (run the git + Linear search, don't fabricate it), a Validation Journey **draft** via `linear-add-journey`, and Target Backend Environment from the item's own content (title, description, screenshots, design links, repro steps) plus the codebase. For bugs, parse the reported environment from bare env names and env-bearing URLs before recommending any default; the reported environment wins. Write it into the description via `linear-write-issue` as clearly-labeled assumptions/recommendations (never overwrite the creator's prose), then re-run `linear-verify`. Whatever the agent could author is now structured spec; only genuinely human-only inputs remain.
53
+ 1. **Best-effort autofill (before blocking).** Run `pre-flight-autofill` for every work item. Preserve a human bare configured key or `Confirmed: <env>`; automation writes `Inferred: <env>evidence: <title|body|reproduction|hostname>`, `Assumption: <env> remote default branch <branch>` for a unique reverse-map, or `Assumption: remote default branch <branch>` otherwise. Human confirmation replaces the annotation with the bare key or `Confirmed: <env>`. For a legacy bare value, use managed draft markers and current ticket content only; provider edit history is not required. A marker proves automation and requires re-annotation; otherwise unknown provenance plus conflicting evidence stops for confirmation. Resolve one exact `deploy.branches` key from the human-authored title, body, and reproduction steps or URL hostname; exclude the complete `Target Backend Environment` section and other machine-authored metadata/draft blocks so annotations cannot become evidence. A reported bug environment is an example, not a restriction. Evidence supersedes only `Assumption:`. Normalize `prod` `production` only when exactly one is configured; no other aliases exist. Conflicts stop; never infer from arbitrary branch text, URL paths/query strings, or substrings. Write through `linear-write-issue`, never overwrite human prose, then re-run `linear-verify`.
54
54
  2. Update labels via `lisa-linear-access operation: save-issue`: remove the current build label, add the configured `blocked` label **and** the configured `human_needed` marker label. (Create either label via `create_issue_label` if needed.) Even after the agent drafted what it could, a pre-flight gate failure bounces the item back to its creator because it still needs a human to **confirm the drafted assumptions** or supply something no agent can invent — real missing credentials, access, or an irreducible product/scoping decision — so the marker tells a human scanning the board which blocked items are waiting on them. The marker is additive to `blocked`, not a replacement. (See the `config-resolution` rule's "Build markers" for when the marker applies and when it must NOT.)
55
55
  3. Reassign the item to the **Issue creator** (the human who filed it — Linear's `creator` field).
56
56
  4. Post the **confirmation comment** from the `pre-flight-autofill` rule via `lisa-linear-access operation: save-comment`, **not** a bare remediation checklist: disclose it is a Claude draft, give one line per drafted section naming the key assumption made, list any remaining human-only item as a specific question with a recommended default, and close with *"review the drafted sections, correct anything wrong, then flip back to Ready and it builds — or reply with corrections."* Prefix with `[{repo}]`.
@@ -135,4 +135,4 @@ The label transitions ARE the canonical signal. The native `state` field stays a
135
135
  - Never create or materially edit an item by calling MCP write tools directly — always delegate to `linear-write-issue` so relationships, Gherkin criteria, and metadata gates are enforced. Two explicit exceptions are permitted: (1) the Step 2 pre-flight failure path (when `linear-verify` returns `FAIL`) may call `lisa-linear-access operation: save-issue` and `lisa-linear-access operation: save-comment` directly to set `status:blocked`, add the configured `human_needed` marker label, and reassign to the creator — this narrow exception is already granted by the rule above; (2) the Step 3 triage path may call `lisa-linear-access operation: save-comment` to post triage findings and `lisa-linear-access operation: save-issue` to add the `claude-triaged-{repo}` label — these are lightweight metadata updates that do not create or materially edit ticket content and therefore do not need to route through `linear-write-issue`.
136
136
  - If sign-in credentials are in the item, extract and pass them to the flow. If the item touches an authenticated surface and credentials are missing, that is a Step 2 failure — block and reassign rather than guessing.
137
137
  - If the item has a Validation Journey section, pass it to the verifier agent. The Validation Journey's local-verification step must point at the target backend environment named in the description.
138
- - For bug items, the reported environment named in the description or reproduction steps drives the implementation base branch via `deploy.branches`. If no environment can be found anywhere in the item, fall back to the configured default branch and record that assumption. If the reported environment is present but missing from `deploy.branches`, or its mapped branch is absent on the remote, stop and report the missing environment/branch mapping instead of defaulting. A non-integration environment fix must be merged and verified on that environment branch, then forward cherry-picked down to the integration branch through a linked follow-up.
138
+ - The environment handoff for every work item uses the same durable grammar: human bare configured key or `Confirmed: <env>`; automated `Inferred: <env> — evidence: <title|body|reproduction|hostname>`, `Assumption: <env> remote default branch <branch>` for a unique reverse-map, or `Assumption: remote default branch <branch>` otherwise. Human confirmation replaces an automated annotation with the bare key or `Confirmed: <env>`. For legacy bare values, managed draft markers and current ticket content decide provenance; provider edit history is not required. A marker proves automation; otherwise unknown provenance plus conflicting evidence stops for confirmation. Human-confirmed wins, then validated `Inferred:`; otherwise one exact `deploy.branches` key from the human-authored title, body, and reproduction steps or URL hostname drives the implementation base branch. Exclude the complete `Target Backend Environment` section and other machine-authored metadata/draft blocks so annotations cannot become evidence. Evidence supersedes only `Assumption:`. Normalize `prod` ↔ `production` only when exactly one is configured; no other aliases exist. Conflicts stop. Never infer from arbitrary branch text, URL paths/query strings, or substrings. With no signal, use the remote default and record the applicable assumption without blocking on a non-unique reverse-map. Require any selected mapping and remote branch. A reported bug environment is an example of the all-work-type rule. Non-integration fixes still require the linked forward cherry-pick.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.255.0",
3
+ "version": "2.256.1",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -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
  }