@codyswann/lisa 2.251.1 → 2.253.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/package.json +1 -1
  2. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  3. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  4. package/plugins/lisa/.codex-plugin/skills/lisa-automation-status/SKILL.md +14 -8
  5. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/SKILL.md +116 -1
  6. package/plugins/lisa/.codex-plugin/skills/lisa-tear-down-automations/SKILL.md +25 -5
  7. package/plugins/lisa/rules/eager/automation-runbook-contract.md +61 -0
  8. package/plugins/lisa/rules/reference/automation-runbook-contract.md +216 -0
  9. package/plugins/lisa/scripts/automation-status-claude-adapter.mjs +36 -18
  10. package/plugins/lisa/scripts/automation-status-codex-adapter.mjs +50 -27
  11. package/plugins/lisa/scripts/automation-status-expected-fleet.mjs +149 -6
  12. package/plugins/lisa/skills/lisa-automation-status/SKILL.md +14 -8
  13. package/plugins/lisa/skills/lisa-setup-automations/SKILL.md +116 -1
  14. package/plugins/lisa/skills/lisa-tear-down-automations/SKILL.md +26 -6
  15. package/plugins/lisa/skills/lisa-tear-down-automations/agents/openai.yaml +2 -2
  16. package/plugins/lisa-agy/plugin.json +1 -1
  17. package/plugins/lisa-agy/scripts/automation-status-claude-adapter.mjs +36 -18
  18. package/plugins/lisa-agy/scripts/automation-status-codex-adapter.mjs +50 -27
  19. package/plugins/lisa-agy/scripts/automation-status-expected-fleet.mjs +149 -6
  20. package/plugins/lisa-agy/skills/lisa-automation-status/SKILL.md +14 -8
  21. package/plugins/lisa-agy/skills/lisa-setup-automations/SKILL.md +116 -1
  22. package/plugins/lisa-agy/skills/lisa-tear-down-automations/SKILL.md +26 -6
  23. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  26. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-copilot/rules/eager/automation-runbook-contract.md +61 -0
  30. package/plugins/lisa-copilot/rules/reference/automation-runbook-contract.md +216 -0
  31. package/plugins/lisa-copilot/scripts/automation-status-claude-adapter.mjs +36 -18
  32. package/plugins/lisa-copilot/scripts/automation-status-codex-adapter.mjs +50 -27
  33. package/plugins/lisa-copilot/scripts/automation-status-expected-fleet.mjs +149 -6
  34. package/plugins/lisa-copilot/skills/lisa-automation-status/SKILL.md +14 -8
  35. package/plugins/lisa-copilot/skills/lisa-setup-automations/SKILL.md +116 -1
  36. package/plugins/lisa-copilot/skills/lisa-tear-down-automations/SKILL.md +26 -6
  37. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-cursor/rules/automation-runbook-contract-reference.mdc +221 -0
  39. package/plugins/lisa-cursor/rules/automation-runbook-contract.mdc +66 -0
  40. package/plugins/lisa-cursor/scripts/automation-status-claude-adapter.mjs +36 -18
  41. package/plugins/lisa-cursor/scripts/automation-status-codex-adapter.mjs +50 -27
  42. package/plugins/lisa-cursor/scripts/automation-status-expected-fleet.mjs +149 -6
  43. package/plugins/lisa-cursor/skills/lisa-automation-status/SKILL.md +14 -8
  44. package/plugins/lisa-cursor/skills/lisa-setup-automations/SKILL.md +116 -1
  45. package/plugins/lisa-cursor/skills/lisa-tear-down-automations/SKILL.md +26 -6
  46. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-expo-agy/plugin.json +1 -1
  49. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  54. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  59. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  64. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  69. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-rails-agy/plugin.json +1 -1
  74. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  79. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  83. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  84. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  86. package/plugins/src/base/rules/eager/automation-runbook-contract.md +61 -0
  87. package/plugins/src/base/rules/reference/automation-runbook-contract.md +216 -0
  88. package/plugins/src/base/scripts/automation-status-claude-adapter.mjs +36 -18
  89. package/plugins/src/base/scripts/automation-status-codex-adapter.mjs +50 -27
  90. package/plugins/src/base/scripts/automation-status-expected-fleet.mjs +149 -6
  91. package/plugins/src/base/skills/lisa-automation-status/SKILL.md +14 -8
  92. package/plugins/src/base/skills/lisa-setup-automations/SKILL.md +116 -1
  93. package/plugins/src/base/skills/lisa-tear-down-automations/SKILL.md +26 -6
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lisa-tear-down-automations
3
- description: "Remove every recurring Lisa automation that /setup-automations created for this project (the lisa-auto-<project>-* set: intake-repair, intake-prd, intake-tickets, exploratory-bugs, exploratory-prds, monitor) using the CURRENT runtime's native scheduler — Codex automations or, on Claude, /schedule. This skill is a declarative specification: it identifies WHICH automations to remove; it does not run teardown scripts. Removes only this project's Lisa automations — never other projects' automations or non-Lisa ones. The inverse of /setup-automations."
3
+ description: "Remove every recurring Lisa automation that /setup-automations registered for this project — whatever is actually registered under the lisa-auto-<project>-* prefix, including the opt-in learnings-audit gardener using the CURRENT runtime's native scheduler — Codex automations or, on Claude, /schedule. This skill is a declarative specification: it identifies WHICH automations to remove; it does not run teardown scripts. Carries no fixed list of loops: the registration set is the roster. Removes only this project's Lisa automations — never other projects' automations or non-Lisa ones. Leaves checked-in runbook files on disk. The inverse of /setup-automations."
4
4
  allowed-tools: ["Skill", "Bash", "Read"]
5
5
  ---
6
6
 
@@ -21,14 +21,34 @@ removes them with its **native** scheduling mechanism.
21
21
 
22
22
  ## Scope (remove only what setup created)
23
23
 
24
- - Remove the six automations `/setup-automations` creates for the current project, matched by the
25
- stable `lisa-auto-<project>-` name prefix: `intake-repair`, `intake-prd`, `intake-tickets`,
26
- `exploratory-bugs`, `exploratory-prds`, `monitor`.
24
+ - Remove **every** automation `/setup-automations` registered for the current project the whole
25
+ set found under the stable `lisa-auto-<project>-` name prefix, whatever it currently contains.
26
+ **Membership is registration, not a roster** (`automation-runbook-contract`): sweep the prefix and
27
+ remove what is there. Do **not** work from a fixed list of loop names — a list drifts the moment a
28
+ loop is added, which is exactly how the opt-in gardener came to be orphaned.
29
+ - This explicitly includes the opt-in **`learnings-audit`** gardener when it is registered:
30
+ `/setup-automations learnings-audit=true` registers it under the same prefix, so teardown removes
31
+ it with the rest. A conditionally-skipped loop (e.g. `exploratory-bugs` on a stack without
32
+ `exploratory-qa`) simply is not in the sweep.
27
33
  - **Never** remove automations for a different project, or any non-Lisa automation (e.g. unrelated
28
34
  crawlers/ingestors). Match strictly on the `lisa-auto-<project>-` prefix for THIS project; when in
29
35
  doubt about an automation's ownership, leave it and report it rather than deleting it.
30
- - **Idempotent** — an automation that is already absent is a no-op, not an error.
36
+ - **Idempotent** — an automation that is already absent is a no-op, not an error. Re-running when
37
+ the prefix sweep finds nothing is a clean, successful no-op.
38
+ - **Leave the runbooks alone.** The checked-in `.lisa/automations/<loop-id>.runbook.md` files that
39
+ `/setup-automations` scaffolded are project knowledge and the historical record of what these
40
+ loops did. Teardown removes scheduler registrations only; it never deletes, edits, or moves a
41
+ runbook file. An operator who wants them gone removes them deliberately, in git.
31
42
 
32
43
  ## Report
33
44
 
34
- List each automation removed, and any in the expected set that were already absent.
45
+ List each automation removed by name. For "already absent", compare against the one source of truth
46
+ — the fleet `scripts/automation-status-expected-fleet.mjs` (`resolveExpectedAutomationFleet`)
47
+ resolves for this project — and name anything it expects that the sweep did not find; that is a
48
+ no-op, not an error. Do not invent an expected set of your own.
49
+
50
+ Then state, in the operator's words, that the runbook files under `.lisa/automations/` were left on
51
+ disk **and why**: they are the written record of what those jobs did, kept on purpose, and if you
52
+ do not want them you delete them yourself in git. Finally, confirm that nothing outside this
53
+ project's `lisa-auto-<project>-` prefix was touched. Write it so a non-technical operator can
54
+ confirm what happened without reading code.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.251.1",
3
+ "version": "2.253.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.251.1",
3
+ "version": "2.253.0",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.251.1",
3
+ "version": "2.253.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.251.1",
3
+ "version": "2.253.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.251.1",
3
+ "version": "2.253.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.251.1",
3
+ "version": "2.253.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -0,0 +1,61 @@
1
+ # Automation Runbook Contract (load-bearing)
2
+
3
+ **Every registered automation loop carries a checked-in runbook, and every run of that loop ends in
4
+ exactly one of six run outcomes** with a one-line, operator-readable summary. A registered loop with
5
+ no runbook, or a run that stops without naming its outcome, is a contract violation.
6
+
7
+ **One vendor-neutral contract, to be cited by (wired in the loop-conformance ticket)
8
+ `lisa-setup-automations`, `lisa-automation-status`, `lisa-tear-down-automations`, and every
9
+ registered loop skill** (the `leaf-only-lifecycle` / `repo-scope-split` precedent: one shared slug,
10
+ never divergent per-loop prose).
11
+
12
+ ## Membership
13
+
14
+ Membership is **registration, not skill-existence**: a loop is under this contract the moment it is
15
+ registered as a scheduled automation, and registering a new one pulls it in automatically. There is
16
+ no hardcoded roster of loops anywhere.
17
+
18
+ ## The six run outcomes
19
+
20
+ Exactly one per run:
21
+ **`nothing-needed | candidate-proposed | change-proved | approval-requested | recovery-required | policy-obsolete`**
22
+
23
+ Health and operator action are **orthogonal** — a healthy run can still need an answer:
24
+
25
+ - `nothing-needed` — the loop ran and found nothing to act on. **Healthy.** Operator action: none.
26
+ - `candidate-proposed` — the loop proposed work (ticket, PRD, recommendation). **Healthy.** Operator
27
+ action: review the proposed item and flip it ready when you want it built.
28
+ - `change-proved` — the loop made a change and proved it with evidence. **Healthy.** Operator
29
+ action: none (informational).
30
+ - `approval-requested` — the loop reached a boundary it may not cross alone. **Healthy.** Operator
31
+ action: answer the approval question.
32
+ - `recovery-required` — the loop itself could not complete (access, tooling, or substrate broken)
33
+ and escalated a decision-ready packet. **Not healthy.** Operator action: restore the named
34
+ capability, then close the escalation item.
35
+ - `policy-obsolete` — the loop's own retirement policy (the retirement condition written in its
36
+ runbook) tripped, so it proposed its own teardown. **Healthy.** Operator action: approve the
37
+ teardown, decline it (close the proposal; the loop keeps running at cadence), or re-cadence it.
38
+
39
+ ## A run outcome is NOT a work-item lifecycle terminal state (CRITICAL)
40
+
41
+ A **run outcome** describes the LOOP ITERATION. A **terminal state** describes a TICKET — Lisa
42
+ already uses that phrase for work-item lifecycles (`lisa-intake`: "`Blocked` is a valid terminal
43
+ state of the downstream lifecycles"). The two vocabularies never merge in an operator-facing report.
44
+ A healthy cycle that routes a work item to `Blocked` with clarifying questions is
45
+ `candidate-proposed` — it produced something — so it is **never `nothing-needed`**, which is
46
+ reserved for runs that found nothing at all, and **never `recovery-required`**, which means the
47
+ loop itself is broken, not that a work item was blocked.
48
+
49
+ ## No silent exit
50
+
51
+ Every run — including a trivial early termination — ends by naming exactly one run outcome plus a
52
+ one-line operator summary of what happened, recorded where the status surface can read it —
53
+ there is no silent exit. Silence and health must never look identical to an operator.
54
+
55
+ ## Never block, always degrade
56
+
57
+ A missing runbook, an unreadable record surface, or an absent optional dependency **degrades** the
58
+ run — say so in the summary and finish with an outcome — it never crashes the loop, never blocks
59
+ other work, and never leaves the run unreported.
60
+
61
+ Full contract (template, outcome definitions, escalation packet, retirement): [reference/automation-runbook-contract.md](../reference/automation-runbook-contract.md).
@@ -0,0 +1,216 @@
1
+ # Automation Runbook Contract
2
+
3
+ Scheduled loops are the busiest machinery in the factory and historically the least explicitly
4
+ contracted: what a loop maintains, what it may do on its own, what it must escalate, and when it
5
+ should stop existing all lived implicitly in each loop's prose. The cost lands on the operator — a
6
+ quiet run and a broken run look identical, and "what is this scheduled thing?" has a different
7
+ answer shape for every loop. This contract writes the answer down once.
8
+
9
+ It is a **single vendor-neutral contract** consumed by `lisa-setup-automations` (which scaffolds a
10
+ runbook per registered loop), `lisa-automation-status` (which reads runbooks and run outcomes back
11
+ to an operator), `lisa-tear-down-automations` (which acts on retirement proposals), and every
12
+ registered loop skill (which conforms its own run to it). Each consumer cites this slug; none of
13
+ them redefines it.
14
+
15
+ **Membership is registration, not skill-existence.** A loop falls under this contract when it is
16
+ registered as a scheduled automation on the host project's runtime scheduler — not when its skill
17
+ file happens to exist in the plugin tree. Registering a new loop pulls it in automatically, and
18
+ un-registering drops it. Nothing in this contract, in the scaffolder, or in the status surface may
19
+ carry a hardcoded roster of loops; the worked example below names one loop as illustration only.
20
+
21
+ ## The runbook template
22
+
23
+ Every registered loop's runbook has these ten sections, in this order. Each is answered in prose a
24
+ non-technical operator can read (`factory-model` rule 5).
25
+
26
+ | Section | What a good answer looks like |
27
+ |---|---|
28
+ | Intent | One sentence naming what this loop maintains, in outcome terms, not mechanism terms. |
29
+ | Sources of truth | The exact surfaces the loop reads, each reachable through its access layer (`integration-access-layer`) — never a direct vendor API call. |
30
+ | Candidate selection | The query or filter that decides what this run acts on, and the bound on how many. |
31
+ | Scope/bounds | What the loop will never touch, and the cap that keeps one run finite. |
32
+ | Proof | What the loop must observe before claiming it changed anything — the evidence it records. |
33
+ | Autonomous-vs-approval boundary | The named line: what it does alone, and the first thing on the other side that requires a human. |
34
+ | Escalation | What it files when it cannot proceed — always the decision-ready packet below. |
35
+ | Recovery | What a human or the next run does to get the loop working again after `recovery-required`. |
36
+ | Next-run state | What the next run can observe from this one, derived from durable surfaces (tracker, records) rather than in-process memory. |
37
+ | Retirement condition | The stateless, tracker-derived condition under which this loop proposes its own teardown. |
38
+
39
+ ### Worked example — the `intake-tickets` loop
40
+
41
+ ```text
42
+ Loop: intake-tickets
43
+
44
+ Intent
45
+ Keeps the build queue moving: work items a human marked ready get built, reviewed, and shipped
46
+ without anyone having to hand them to an agent.
47
+
48
+ Sources of truth
49
+ The configured tracker's build queue (the ready/claimed/done lanes from .lisa.config.json), read
50
+ through the tracker access layer; the item's own comments and linked PRs.
51
+
52
+ Candidate selection
53
+ The oldest eligible leaf work item currently in the ready lane, one per run. Containers with open
54
+ children are repaired out of the queue, never built.
55
+
56
+ Scope/bounds
57
+ Only items in this repo's configured queue. It never edits the ready lane's meaning, never
58
+ invents work items, and never processes more than its per-run cap.
59
+
60
+ Proof
61
+ A merged pull request plus the project's own verification evidence posted back to the work item;
62
+ a claim of "done" without that evidence is not made.
63
+
64
+ Autonomous-vs-approval boundary
65
+ Alone: claim, plan, implement, open the PR, drive it to merge. Requires a human: anything that
66
+ needs a protected deployment approval, and any item whose requirements it cannot resolve — that
67
+ becomes a blocked work item with clarifying questions, not a guess.
68
+
69
+ Escalation
70
+ When the loop itself cannot proceed (tracker credentials revoked, queue unreadable), it files the
71
+ decision-ready packet below, labeled status:blocked + human-needed.
72
+
73
+ Recovery
74
+ A human restores the named access, then flips the escalation item closed; the next scheduled run
75
+ resumes with no manual replay needed.
76
+
77
+ Next-run state
78
+ Nothing in memory. The tracker lanes and the run records are the state: a claimed item that never
79
+ finished is visible as claimed, and the repair path picks it up.
80
+
81
+ Retirement condition
82
+ Propose teardown when BOTH hold: no work item has entered this queue for 30 consecutive days, AND
83
+ this run proposed nothing. It files one teardown-proposal item and keeps running at its normal
84
+ cadence until someone approves, declines, or re-cadences it.
85
+ ```
86
+
87
+ ## The six run outcomes
88
+
89
+ Exactly one per run. The one-line operator summary is not optional — it is what the status surface
90
+ shows and what makes the outcome actionable.
91
+
92
+ Health and operator action are **orthogonal**: several healthy outcomes still want an answer, and
93
+ only `recovery-required` means the machinery itself is broken.
94
+
95
+ | Outcome | Definition | Healthy? | Operator action? | One-line summary must contain |
96
+ |---|---|---|---|---|
97
+ | `nothing-needed` | The loop ran end to end and found nothing to act on. | Healthy — **no operator action** | None. | What was scanned, how much was seen, and "nothing to propose". |
98
+ | `candidate-proposed` | The loop proposed work — a ticket, PRD, or recommendation — for a human or a downstream factory. | Healthy | Review the proposed item and flip it ready when you want it built. | What was proposed and where to find it (item refs). |
99
+ | `change-proved` | The loop made a change and proved it with evidence. | Healthy | None — informational. | What changed and the evidence that proves it. |
100
+ | `approval-requested` | The loop reached its autonomous/approval boundary and asked a human. | Healthy | Answer the approval question. | What is waiting, on whom, and what happens if nobody answers. |
101
+ | `recovery-required` | The loop itself could not complete: access, tooling, or substrate is broken. | **Not healthy** — needs a human | Restore the named capability, then close the escalation item. | What broke, and the escalation item to act on. |
102
+ | `policy-obsolete` | The loop's own retirement policy — the retirement condition written in its runbook — tripped, so it proposed its own teardown. | Healthy | Decide: approve the teardown, decline it (close the proposal; the loop continues at cadence), or re-cadence it. | Why it looks obsolete and the teardown-proposal item. |
103
+
104
+ ### Exemplar one-line summaries
105
+
106
+ These set the register every conforming loop copies — plain, specific, and actionable without
107
+ reading code:
108
+
109
+ ```text
110
+ nothing-needed Scanned 12 ready items; nothing to propose.
111
+ candidate-proposed Proposed #1810 to fix the failing checkout step; awaiting your flip to ready.
112
+ change-proved Merged #1811 raising coverage to 84%; CI green and evidence posted on the item.
113
+ approval-requested Deploy to production is waiting on your approval; nothing ships until you answer.
114
+ recovery-required Lost access to the tracker; filed #1812 with the one decision needed to restore it.
115
+ policy-obsolete No work has reached this queue in 30 days; proposed teardown in #1813.
116
+ ```
117
+
118
+ **No silent exit.** Every run posts its outcome and its one-line summary before stopping —
119
+ including the trivial early terminations (empty queue, nothing eligible, already-claimed) — so
120
+ there is no silent exit for an operator to misread as health. A run that ends without a
121
+ recorded outcome is indistinguishable from a crashed scheduler, which is exactly the ambiguity this
122
+ contract removes. This generalizes `lisa-improve-harness`'s result-record discipline — every
123
+ terminal step posts its result record before stopping, so there is no silent exit — from one flow to
124
+ every registered loop. (The run-record substrate that stores outcomes and summaries is named here
125
+ only; it ships with that ticket (#1797), do not assume its file is present in this branch.)
126
+
127
+ ## A run outcome is not a work-item lifecycle terminal state
128
+
129
+ This is the highest-risk seam in the contract, so it is stated explicitly.
130
+
131
+ - A **run outcome** describes the LOOP ITERATION — how this scheduled execution ended.
132
+ - A **terminal state** describes a TICKET — Lisa already uses that phrase for work-item lifecycles
133
+ (`lisa-intake`: "`Blocked` is a valid terminal state of the downstream lifecycles"; see
134
+ `prd-lifecycle-rollup`).
135
+
136
+ They never merge in an operator-facing report. A perfectly healthy cycle that routes a work item to
137
+ `Blocked` — because the item's requirements are unresolvable and a human must answer — is
138
+ `candidate-proposed`: the run produced something (a blocked item carrying clarifying questions), so
139
+ it is **never `nothing-needed`**, which is reserved for runs that found nothing at all, and **never
140
+ `recovery-required`**, which means the machinery is broken. Conflating a blocked work item with a
141
+ broken loop would paint healthy intake cycles permanently red and train operators to ignore the
142
+ fleet verdict.
143
+
144
+ ## Escalation — the decision-ready packet
145
+
146
+ A loop that cannot proceed escalates by filing a tracker item (through `lisa-tracker-write`, per
147
+ `tracked-work` and `integration-access-layer`) labeled **`status:blocked`** and **`human-needed`**,
148
+ containing exactly these fields:
149
+
150
+ | Field | Content |
151
+ |---|---|
152
+ | Current state | Where things stand right now, in plain terms — what is and is not working. |
153
+ | Work already attempted | What the loop tried on its own before escalating, so nobody repeats it. |
154
+ | Evidence | Links to the runs, items, logs, or records that show the problem (the evidence packet from the evidence PRD is the carrier; this contract names it and defines no evidence formats). |
155
+ | Risk of inaction | What degrades or stops if nobody acts, and how soon. |
156
+ | Smallest unresolved choice | The single smallest decision a human must make — one question, not a list — stated with **named options** (or as a yes/no), and what the loop will do with each answer. |
157
+ | How to answer | The mechanical response path: comment on the item, flip a label, or close it — so the operator never has to guess how to reply. |
158
+
159
+ The wording requirement is binding: a **non-technical operator must be able to act on the packet
160
+ without reading code** (`factory-model` rule 5). A stack trace may be attached as evidence, but the
161
+ packet must stand without it.
162
+
163
+ ## Retirement condition
164
+
165
+ A loop that no longer earns its schedule should say so. The condition is **stateless — derived from
166
+ the tracker, never from a counter or a state file** (there is no durable home for a run counter, and
167
+ a tracker-derived condition is headless- and concurrent-safe by construction). This generalizes the
168
+ *Retirement condition* section of `lisa-learnings-audit`, which already retires itself this way; its
169
+ current wording still uses the older three-value "terminal states" vocabulary, and the
170
+ loop-conformance ticket conforms it to the six run outcomes above. A loop proposes retirement when
171
+ BOTH hold:
172
+
173
+ 1. **Quiet trailing window** — a date-filtered tracker search finds nothing this loop produced
174
+ within its trailing quiet window (sized to a small multiple of its cadence).
175
+ 2. **This run proposes nothing** — the current run yields zero candidates (it is otherwise ending
176
+ `nothing-needed`).
177
+
178
+ When both hold, the run ends `policy-obsolete` and files **exactly ONE marker-deduped**
179
+ teardown-proposal item — matched on the marker, never the title — carrying the date-filtered search
180
+ result and this run's summary as evidence, and proposing either a longer cadence or
181
+ `lisa-tear-down-automations`. The loop **keeps running at its normal cadence** until an operator
182
+ flips that proposal one of three ways: **approve** it (`lisa-tear-down-automations` runs and the
183
+ registration goes away), **decline** it (close the proposal — the loop simply continues), or
184
+ **re-cadence** it (register it at the longer cadence instead of tearing it down). Retirement is a
185
+ recommendation like any other, **never a self-executed exit**: a loop never deletes its own
186
+ registration, and a proposal nobody answers changes nothing.
187
+
188
+ ## Never block, always degrade
189
+
190
+ A loop degrades rather than blocking. Three triggers are expected and must never crash a run:
191
+
192
+ - **A missing runbook** — the loop runs on the contract's defaults and says so.
193
+ - **An unreadable record surface** — the loop still reports its outcome and summary in its own run
194
+ output, so the operator is never left with silence.
195
+ - **An absent optional dependency** — a skill or surface that ships with a sibling ticket is not
196
+ yet installed; the loop skips that enrichment and continues.
197
+
198
+ A degraded run still **reports its actual outcome from the same six values** — degradation does not
199
+ mint a seventh token and, on its own, is not `recovery-required`. Its one-line summary **leads with
200
+ the degradation**, then states the outcome:
201
+
202
+ ```text
203
+ Runbook missing — ran on defaults; nothing to propose.
204
+ ```
205
+
206
+ Degradation becomes `recovery-required` only when it prevents the loop from doing its actual job
207
+ (the queue itself is unreadable, credentials are revoked) — and then it escalates the decision-ready
208
+ packet above. A degraded run never blocks other work and never leaves the run unreported.
209
+
210
+ ## Citing adjacent work
211
+
212
+ Cite these by name; each ships with its own ticket, do not assume its file is present in this
213
+ branch: the runbook scaffolding that instantiates this template (#1796), the run-record substrate
214
+ that stores outcomes and summaries (#1797), and the evidence packet defined by the evidence PRD
215
+ (#1738) — named here, with no evidence format defined by this contract. Rejection memory
216
+ extends `rejection-detection`; this contract neither restates nor overrides it.
@@ -10,6 +10,11 @@
10
10
  */
11
11
 
12
12
  import { compareAutomationFleet } from "./automation-status-contract-drift.mjs";
13
+ import {
14
+ assignToAutomationGroup,
15
+ createAutomationGroupBins,
16
+ renderAutomationGroups,
17
+ } from "./automation-status-expected-fleet.mjs";
13
18
 
14
19
  const CLAUDE_RUNTIME_LABEL = "Claude /schedule";
15
20
  const CLAUDE_ACTIVE_STATUSES = new Set([
@@ -75,10 +80,7 @@ export function inspectClaudeAutomationFleet(input) {
75
80
  automationPrefix: expectedFleet.automationPrefix,
76
81
  });
77
82
 
78
- const expectedGroups = new Map([
79
- ["core", []],
80
- ["exploratory", []],
81
- ]);
83
+ const expectedGroups = createAutomationGroupBins();
82
84
 
83
85
  const comparisons = compareAutomationFleet({
84
86
  expectedAutomations: expectedFleet.expected,
@@ -87,7 +89,9 @@ export function inspectClaudeAutomationFleet(input) {
87
89
 
88
90
  for (const [index, expected] of expectedFleet.expected.entries()) {
89
91
  const comparison = comparisons[index];
90
- expectedGroups.get(expected.group)?.push(
92
+ assignToAutomationGroup(
93
+ expectedGroups,
94
+ expected.group,
91
95
  createObservedStatusItem({
92
96
  expected,
93
97
  comparison,
@@ -97,7 +101,7 @@ export function inspectClaudeAutomationFleet(input) {
97
101
  }
98
102
 
99
103
  for (const unsupported of expectedFleet.unsupported) {
100
- expectedGroups.get(unsupported.group)?.push({
104
+ assignToAutomationGroup(expectedGroups, unsupported.group, {
101
105
  id: unsupported.automationId,
102
106
  status: "UNSUPPORTED",
103
107
  summary: unsupported.reason,
@@ -109,18 +113,7 @@ export function inspectClaudeAutomationFleet(input) {
109
113
  return {
110
114
  runtime: `${CLAUDE_RUNTIME_LABEL} listing`,
111
115
  generatedAt: now.toISOString(),
112
- groups: [
113
- {
114
- id: "1",
115
- title: "Core automations",
116
- items: expectedGroups.get("core") ?? [],
117
- },
118
- {
119
- id: "2",
120
- title: "Exploratory automations",
121
- items: expectedGroups.get("exploratory") ?? [],
122
- },
123
- ],
116
+ groups: renderAutomationGroups(expectedGroups),
124
117
  observedAutomations,
125
118
  };
126
119
  }
@@ -487,6 +480,13 @@ function normalizeClaudeRRule(value) {
487
480
  ) {
488
481
  return "FREQ=DAILY;INTERVAL=1";
489
482
  }
483
+ if (
484
+ cadence === "once a week" ||
485
+ cadence === "every week" ||
486
+ cadence === "weekly"
487
+ ) {
488
+ return "FREQ=WEEKLY;INTERVAL=1";
489
+ }
490
490
 
491
491
  const everyMinutes = cadence.match(/every (\d+) minutes?/);
492
492
  if (everyMinutes?.[1]) {
@@ -503,6 +503,11 @@ function normalizeClaudeRRule(value) {
503
503
  return `FREQ=DAILY;INTERVAL=${everyDays[1]}`;
504
504
  }
505
505
 
506
+ const everyWeeks = cadence.match(/every (\d+) weeks?/);
507
+ if (everyWeeks?.[1]) {
508
+ return `FREQ=WEEKLY;INTERVAL=${everyWeeks[1]}`;
509
+ }
510
+
506
511
  return undefined;
507
512
  }
508
513
 
@@ -527,6 +532,9 @@ function humanizeClaudeCadence(value) {
527
532
  if (normalized === "every day") {
528
533
  return "once a day";
529
534
  }
535
+ if (normalized === "weekly" || normalized === "every week") {
536
+ return "once a week";
537
+ }
530
538
  return normalized;
531
539
  }
532
540
 
@@ -556,6 +564,11 @@ function humanizeRRule(rrule) {
556
564
  return Number(daily[1]) === 1 ? "once a day" : `every ${daily[1]} days`;
557
565
  }
558
566
 
567
+ const weekly = rrule.match(/^FREQ=WEEKLY;INTERVAL=(\d+)$/);
568
+ if (weekly?.[1]) {
569
+ return Number(weekly[1]) === 1 ? "once a week" : `every ${weekly[1]} weeks`;
570
+ }
571
+
559
572
  return rrule;
560
573
  }
561
574
 
@@ -574,6 +587,11 @@ function cadenceLabelToIntervalMs(label) {
574
587
  return 24 * 60 * 60_000;
575
588
  }
576
589
 
590
+ const oncePerWeek = new Set(["once a week", "weekly"]);
591
+ if (oncePerWeek.has(label)) {
592
+ return 7 * 24 * 60 * 60_000;
593
+ }
594
+
577
595
  return null;
578
596
  }
579
597
 
@@ -17,6 +17,11 @@ import { execFile } from "node:child_process";
17
17
  import { promisify } from "node:util";
18
18
 
19
19
  import { compareAutomationFleet } from "./automation-status-contract-drift.mjs";
20
+ import {
21
+ assignToAutomationGroup,
22
+ createAutomationGroupBins,
23
+ renderAutomationGroups,
24
+ } from "./automation-status-expected-fleet.mjs";
20
25
 
21
26
  const CODEx_RUNTIME_LABEL = "Codex automations";
22
27
  const RUN_TIMESTAMP_PATTERN = /20\d{2}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z/;
@@ -156,10 +161,7 @@ export async function inspectCodexAutomationFleet(input) {
156
161
  automationPrefix: expectedFleet.automationPrefix,
157
162
  });
158
163
 
159
- const expectedGroups = new Map([
160
- ["core", []],
161
- ["exploratory", []],
162
- ]);
164
+ const expectedGroups = createAutomationGroupBins();
163
165
 
164
166
  const comparisons = compareAutomationFleet({
165
167
  expectedAutomations: expectedFleet.expected,
@@ -168,7 +170,9 @@ export async function inspectCodexAutomationFleet(input) {
168
170
 
169
171
  for (const [index, expected] of expectedFleet.expected.entries()) {
170
172
  const comparison = comparisons[index];
171
- expectedGroups.get(expected.group)?.push(
173
+ assignToAutomationGroup(
174
+ expectedGroups,
175
+ expected.group,
172
176
  createObservedStatusItem({
173
177
  expected,
174
178
  comparison,
@@ -178,7 +182,7 @@ export async function inspectCodexAutomationFleet(input) {
178
182
  }
179
183
 
180
184
  for (const unsupported of expectedFleet.unsupported) {
181
- expectedGroups.get(unsupported.group)?.push({
185
+ assignToAutomationGroup(expectedGroups, unsupported.group, {
182
186
  id: unsupported.automationId,
183
187
  status: "UNSUPPORTED",
184
188
  summary: unsupported.reason,
@@ -190,18 +194,7 @@ export async function inspectCodexAutomationFleet(input) {
190
194
  return {
191
195
  runtime: `${CODEx_RUNTIME_LABEL} (backing-store metadata)`,
192
196
  generatedAt: now.toISOString(),
193
- groups: [
194
- {
195
- id: "1",
196
- title: "Core automations",
197
- items: expectedGroups.get("core") ?? [],
198
- },
199
- {
200
- id: "2",
201
- title: "Exploratory automations",
202
- items: expectedGroups.get("exploratory") ?? [],
203
- },
204
- ],
197
+ groups: renderAutomationGroups(expectedGroups),
205
198
  observedAutomations,
206
199
  };
207
200
  }
@@ -249,21 +242,35 @@ export function deriveCodexObservedCommand(prompt) {
249
242
  return undefined;
250
243
  }
251
244
 
245
+ // The "with arguments" clause is optional: no-argument loops (monitor, the
246
+ // gardener) register without one. Requiring it made every such loop derive an
247
+ // undefined command and report DRIFTED forever, un-fixable by re-running setup.
252
248
  const lisaSkillMatch = prompt.match(
253
- /Use the Lisa ([a-z0-9:-]+) skill with arguments `([^`]+)`/i
249
+ /Use the Lisa ([a-z0-9:-]+) skill(?: with arguments `([^`]*)`)?/i
254
250
  );
255
- if (lisaSkillMatch?.[1] && lisaSkillMatch[2]) {
256
- return `/lisa:${lisaSkillMatch[1]} ${lisaSkillMatch[2]}`.trim();
251
+ if (lisaSkillMatch?.[1]) {
252
+ return `/lisa:${lisaSkillMatch[1]} ${lisaSkillMatch[2] ?? ""}`.trim();
257
253
  }
258
254
 
259
255
  const aliasSkillMatch = prompt.match(
260
- /Use the `\$([a-z0-9:-]+)` skill with arguments `([^`]+)`/i
256
+ /Use the `\$([a-z0-9:-]+)` skill(?: with arguments `([^`]*)`)?/i
261
257
  );
262
- if (aliasSkillMatch?.[1] && aliasSkillMatch[2]) {
263
- return `${canonicalizeCodexSkillAlias(aliasSkillMatch[1])} ${aliasSkillMatch[2]}`.trim();
264
- }
265
-
266
- return undefined;
258
+ if (aliasSkillMatch?.[1]) {
259
+ return `${canonicalizeCodexSkillAlias(aliasSkillMatch[1])} ${aliasSkillMatch[2] ?? ""}`.trim();
260
+ }
261
+
262
+ // A registration whose prompt carries the literal Lisa command on its own
263
+ // line — the shape `/lisa:setup-automations` bakes so a command that is not a
264
+ // plain skill name (`/lisa:learnings:audit`) round-trips exactly. Mirrors the
265
+ // Claude adapter, which has always accepted a bare `/lisa:` command. Codex
266
+ // stores prompts as single-line TOML, so a line break arrives either real or
267
+ // as the two-character escape `\n`; both delimit a line here.
268
+ const literalCommand = prompt
269
+ .split(/\r?\n|\\n/)
270
+ .map(segment => segment.trim())
271
+ .find(segment => /^\/lisa[:-]\S/.test(segment));
272
+
273
+ return literalCommand;
267
274
  }
268
275
 
269
276
  function canonicalizeCodexSkillAlias(alias) {
@@ -583,6 +590,13 @@ function humanizeAutomationCadence(rrule) {
583
590
  : `every ${everyDays[1]} days`;
584
591
  }
585
592
 
593
+ const everyWeeks = rrule.match(/^FREQ=WEEKLY;INTERVAL=(\d+)$/);
594
+ if (everyWeeks?.[1]) {
595
+ return Number(everyWeeks[1]) === 1
596
+ ? "once a week"
597
+ : `every ${everyWeeks[1]} weeks`;
598
+ }
599
+
586
600
  return rrule;
587
601
  }
588
602
 
@@ -606,6 +620,11 @@ function rruleToIntervalMs(rrule) {
606
620
  return Number(daily[1]) * 24 * 60 * 60_000;
607
621
  }
608
622
 
623
+ const weekly = rrule.match(/^FREQ=WEEKLY;INTERVAL=(\d+)$/);
624
+ if (weekly?.[1]) {
625
+ return Number(weekly[1]) * 7 * 24 * 60 * 60_000;
626
+ }
627
+
609
628
  return null;
610
629
  }
611
630
 
@@ -623,6 +642,10 @@ function cadenceLabelToIntervalMs(label) {
623
642
  return 24 * 60 * 60_000;
624
643
  }
625
644
 
645
+ if (label === "once a week") {
646
+ return 7 * 24 * 60 * 60_000;
647
+ }
648
+
626
649
  return null;
627
650
  }
628
651