@codyswann/lisa 3.18.2 → 3.19.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 (81) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +18 -1
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/dist/health/drift-tickets.d.ts +88 -0
  5. package/dist/health/drift-tickets.d.ts.map +1 -0
  6. package/dist/health/drift-tickets.js +112 -0
  7. package/dist/health/drift-tickets.js.map +1 -0
  8. package/dist/health/index.d.ts +1 -0
  9. package/dist/health/index.d.ts.map +1 -1
  10. package/dist/health/index.js +1 -0
  11. package/dist/health/index.js.map +1 -1
  12. package/package.json +1 -1
  13. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa/.codex-plugin/skills/lisa-health-drift-cron/SKILL.md +80 -0
  16. package/plugins/lisa/.codex-plugin/skills/lisa-health-drift-cron/agents/openai.yaml +4 -0
  17. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/SKILL.md +10 -0
  18. package/plugins/lisa/commands/health-drift-cron.md +9 -0
  19. package/plugins/lisa/skills/lisa-health-drift-cron/SKILL.md +80 -0
  20. package/plugins/lisa/skills/lisa-health-drift-cron/agents/openai.yaml +4 -0
  21. package/plugins/lisa/skills/lisa-setup-automations/SKILL.md +10 -0
  22. package/plugins/lisa-agy/commands/lisa/health-drift-cron.md +9 -0
  23. package/plugins/lisa-agy/plugin.json +1 -1
  24. package/plugins/lisa-agy/skills/lisa-health-drift-cron/SKILL.md +80 -0
  25. package/plugins/lisa-agy/skills/lisa-setup-automations/SKILL.md +10 -0
  26. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  29. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-copilot/commands/lisa/health-drift-cron.md +9 -0
  33. package/plugins/lisa-copilot/skills/lisa-health-drift-cron/SKILL.md +80 -0
  34. package/plugins/lisa-copilot/skills/lisa-setup-automations/SKILL.md +10 -0
  35. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-cursor/commands/lisa/health-drift-cron.md +9 -0
  37. package/plugins/lisa-cursor/skills/lisa-health-drift-cron/SKILL.md +80 -0
  38. package/plugins/lisa-cursor/skills/lisa-setup-automations/SKILL.md +10 -0
  39. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-expo-agy/plugin.json +1 -1
  42. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  52. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  57. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  62. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  66. package/plugins/lisa-rails-agy/plugin.json +1 -1
  67. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  71. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  72. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  76. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  77. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  79. package/plugins/src/base/commands/health-drift-cron.md +9 -0
  80. package/plugins/src/base/skills/lisa-health-drift-cron/SKILL.md +80 -0
  81. package/plugins/src/base/skills/lisa-setup-automations/SKILL.md +10 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Decide which health drift becomes a ticket, and which is already tracked.
3
+ *
4
+ * The scheduled health consumer's whole difficulty is idempotency: a nightly
5
+ * cron that refiles the same drift every night is worse than no cron at all,
6
+ * because it trains people to ignore the tickets it files. Everything here
7
+ * exists to make "the same drift" a decidable question.
8
+ *
9
+ * ## Dedupe is per CHECK, not per drift set
10
+ *
11
+ * The marker fingerprints the check id. Fingerprinting the whole finding set
12
+ * would mean one added finding produces a brand-new ticket while the old one
13
+ * still stands, so a slowly-degrading project accumulates near-duplicates —
14
+ * the same "worse than no cron" outcome by a slightly different route. Per
15
+ * check also makes partial repair behave: fix one drifting check, its ticket
16
+ * closes, the others stay open on their own merits.
17
+ *
18
+ * ## A closed ticket does not suppress live drift
19
+ *
20
+ * Enforced by construction rather than by a conditional: this planner is given
21
+ * only the OPEN tickets, so a closed one cannot suppress anything. It is not
22
+ * that closed tickets are checked and then ignored — they are not in evidence.
23
+ *
24
+ * The alternative, treating closure as permanent suppression, means closing a
25
+ * ticket without fixing anything makes that drift invisible forever, which is
26
+ * the silent decay this consumer exists to prevent. Suppression should be a
27
+ * configured declaration visible in a diff, not a side effect of somebody
28
+ * tidying a backlog. Turning the check off in config is the sanctioned way.
29
+ * @module health/drift-tickets
30
+ */
31
+ import type { HealthFinding } from "./contract.js";
32
+ /** Marker prefix written into a filed ticket so the next run can find it. */
33
+ export declare const DRIFT_MARKER_PREFIX = "lisa-health-drift";
34
+ /**
35
+ * The dedupe marker for one check.
36
+ *
37
+ * An HTML comment so it is invisible in rendered ticket bodies but present in
38
+ * the raw text every tracker returns. Trackers differ in what they preserve of
39
+ * a body; they do not differ in preserving its characters.
40
+ * @param check - The health check id
41
+ * @returns The marker to embed in a filed ticket
42
+ */
43
+ export declare function driftMarker(check: string): string;
44
+ /** A ticket already open in the tracker, reduced to what dedupe needs. */
45
+ export interface OpenTicket {
46
+ readonly id: string;
47
+ readonly body: string;
48
+ }
49
+ /** One ticket the consumer should file. */
50
+ export interface DriftTicket {
51
+ readonly check: string;
52
+ readonly title: string;
53
+ readonly body: string;
54
+ readonly marker: string;
55
+ }
56
+ /** Drift already covered by an open ticket, reported rather than dropped. */
57
+ export interface TrackedDrift {
58
+ readonly check: string;
59
+ readonly ticketId: string;
60
+ }
61
+ /** What a scheduled run should do about the drift it found. */
62
+ export interface DriftTicketPlan {
63
+ readonly file: readonly DriftTicket[];
64
+ readonly alreadyTracked: readonly TrackedDrift[];
65
+ }
66
+ /**
67
+ * Inputs to a planning pass.
68
+ *
69
+ * `openTickets` is deliberately named for its own precondition. A caller that
70
+ * passes closed tickets defeats the ruling above, and a parameter called
71
+ * `tickets` would not have told them so.
72
+ */
73
+ export interface DriftTicketInput {
74
+ readonly findings: readonly HealthFinding[];
75
+ readonly openTickets: readonly OpenTicket[];
76
+ }
77
+ /**
78
+ * Decide what to file for the drift a run found.
79
+ *
80
+ * Total and pure: every unique drifting check lands in exactly one of `file` or
81
+ * `alreadyTracked` after duplicate findings for that check collapse, so a caller
82
+ * reporting "nothing to do" is asserting it looked at all checks rather than
83
+ * that its filter happened to be empty.
84
+ * @param input - Findings from the run and the tracker's OPEN tickets
85
+ * @returns Tickets to file, and drift already covered
86
+ */
87
+ export declare function planDriftTickets(input: DriftTicketInput): DriftTicketPlan;
88
+ //# sourceMappingURL=drift-tickets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drift-tickets.d.ts","sourceRoot":"","sources":["../../src/health/drift-tickets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAKnD,6EAA6E;AAC7E,eAAO,MAAM,mBAAmB,sBAAsB,CAAC;AAEvD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,0EAA0E;AAC1E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,2CAA2C;AAC3C,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,+DAA+D;AAC/D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,WAAW,EAAE,CAAC;IACtC,QAAQ,CAAC,cAAc,EAAE,SAAS,YAAY,EAAE,CAAC;CAClD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAC5C,QAAQ,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,CAAC;CAC7C;AA0BD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,eAAe,CA0CzE"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Decide which health drift becomes a ticket, and which is already tracked.
3
+ *
4
+ * The scheduled health consumer's whole difficulty is idempotency: a nightly
5
+ * cron that refiles the same drift every night is worse than no cron at all,
6
+ * because it trains people to ignore the tickets it files. Everything here
7
+ * exists to make "the same drift" a decidable question.
8
+ *
9
+ * ## Dedupe is per CHECK, not per drift set
10
+ *
11
+ * The marker fingerprints the check id. Fingerprinting the whole finding set
12
+ * would mean one added finding produces a brand-new ticket while the old one
13
+ * still stands, so a slowly-degrading project accumulates near-duplicates —
14
+ * the same "worse than no cron" outcome by a slightly different route. Per
15
+ * check also makes partial repair behave: fix one drifting check, its ticket
16
+ * closes, the others stay open on their own merits.
17
+ *
18
+ * ## A closed ticket does not suppress live drift
19
+ *
20
+ * Enforced by construction rather than by a conditional: this planner is given
21
+ * only the OPEN tickets, so a closed one cannot suppress anything. It is not
22
+ * that closed tickets are checked and then ignored — they are not in evidence.
23
+ *
24
+ * The alternative, treating closure as permanent suppression, means closing a
25
+ * ticket without fixing anything makes that drift invisible forever, which is
26
+ * the silent decay this consumer exists to prevent. Suppression should be a
27
+ * configured declaration visible in a diff, not a side effect of somebody
28
+ * tidying a backlog. Turning the check off in config is the sanctioned way.
29
+ * @module health/drift-tickets
30
+ */
31
+ /** Statuses that count as drift worth tracking. `pass` is not drift. */
32
+ const DRIFT_STATUSES = new Set(["warn", "fail"]);
33
+ /** Marker prefix written into a filed ticket so the next run can find it. */
34
+ export const DRIFT_MARKER_PREFIX = "lisa-health-drift";
35
+ /**
36
+ * The dedupe marker for one check.
37
+ *
38
+ * An HTML comment so it is invisible in rendered ticket bodies but present in
39
+ * the raw text every tracker returns. Trackers differ in what they preserve of
40
+ * a body; they do not differ in preserving its characters.
41
+ * @param check - The health check id
42
+ * @returns The marker to embed in a filed ticket
43
+ */
44
+ export function driftMarker(check) {
45
+ return `<!-- ${DRIFT_MARKER_PREFIX}: ${check} -->`;
46
+ }
47
+ /**
48
+ * Compose the ticket body for one drifting check.
49
+ * @param finding - The drifting finding
50
+ * @returns Operator-readable body carrying the dedupe marker
51
+ */
52
+ function bodyFor(finding) {
53
+ return [
54
+ `The scheduled health check found drift in \`${finding.check}\`.`,
55
+ "",
56
+ `**Status:** ${finding.status}`,
57
+ `**Layer:** ${finding.layer}`,
58
+ `**Reason:** ${finding.reason}`,
59
+ "",
60
+ "Filed automatically by the scheduled health consumer. It files; it never",
61
+ "closes or edits. Repairing the drift is separate work.",
62
+ "",
63
+ "If this drift is accepted rather than fixed, turn the check off in config",
64
+ "rather than closing this ticket — a closed ticket does not suppress live",
65
+ "drift, so it would simply be filed again on the next run.",
66
+ "",
67
+ driftMarker(finding.check),
68
+ ].join("\n");
69
+ }
70
+ /**
71
+ * Decide what to file for the drift a run found.
72
+ *
73
+ * Total and pure: every unique drifting check lands in exactly one of `file` or
74
+ * `alreadyTracked` after duplicate findings for that check collapse, so a caller
75
+ * reporting "nothing to do" is asserting it looked at all checks rather than
76
+ * that its filter happened to be empty.
77
+ * @param input - Findings from the run and the tracker's OPEN tickets
78
+ * @returns Tickets to file, and drift already covered
79
+ */
80
+ export function planDriftTickets(input) {
81
+ const drifting = input.findings.filter(finding => DRIFT_STATUSES.has(finding.status));
82
+ // One entry per check even when a run reports the same check twice: the
83
+ // marker cannot distinguish them, so filing two would self-duplicate on the
84
+ // very first run — the defect this module exists to prevent, arriving before
85
+ // any second run happens. First occurrence wins, so the reported reason is
86
+ // the one a reader would see first in the run output.
87
+ const unique = drifting.filter((finding, index) => drifting.findIndex(other => other.check === finding.check) === index);
88
+ const decided = unique.map(finding => {
89
+ const marker = driftMarker(finding.check);
90
+ return {
91
+ finding,
92
+ marker,
93
+ existing: input.openTickets.find(ticket => ticket.body.includes(marker)),
94
+ };
95
+ });
96
+ return {
97
+ file: decided.flatMap(entry => entry.existing
98
+ ? []
99
+ : [
100
+ {
101
+ check: entry.finding.check,
102
+ title: `health drift: ${entry.finding.check}`,
103
+ body: bodyFor(entry.finding),
104
+ marker: entry.marker,
105
+ },
106
+ ]),
107
+ alreadyTracked: decided.flatMap(entry => entry.existing
108
+ ? [{ check: entry.finding.check, ticketId: entry.existing.id }]
109
+ : []),
110
+ };
111
+ }
112
+ //# sourceMappingURL=drift-tickets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drift-tickets.js","sourceRoot":"","sources":["../../src/health/drift-tickets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH,wEAAwE;AACxE,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAEjD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,mBAAmB,GAAG,mBAAmB,CAAC;AAEvD;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,QAAQ,mBAAmB,KAAK,KAAK,MAAM,CAAC;AACrD,CAAC;AAwCD;;;;GAIG;AACH,SAAS,OAAO,CAAC,OAAsB;IACrC,OAAO;QACL,+CAA+C,OAAO,CAAC,KAAK,KAAK;QACjE,EAAE;QACF,eAAe,OAAO,CAAC,MAAM,EAAE;QAC/B,cAAc,OAAO,CAAC,KAAK,EAAE;QAC7B,eAAe,OAAO,CAAC,MAAM,EAAE;QAC/B,EAAE;QACF,0EAA0E;QAC1E,wDAAwD;QACxD,EAAE;QACF,2EAA2E;QAC3E,0EAA0E;QAC1E,2DAA2D;QAC3D,EAAE;QACF,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;KAC3B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAuB;IACtD,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAC/C,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CACnC,CAAC;IACF,wEAAwE;IACxE,4EAA4E;IAC5E,6EAA6E;IAC7E,2EAA2E;IAC3E,sDAAsD;IACtD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAC5B,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CACjB,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,CACvE,CAAC;IAEF,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE;QACnC,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC1C,OAAO;YACL,OAAO;YACP,MAAM;YACN,QAAQ,EAAE,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;SACzE,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAC5B,KAAK,CAAC,QAAQ;YACZ,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC;gBACE;oBACE,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK;oBAC1B,KAAK,EAAE,iBAAiB,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE;oBAC7C,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC;oBAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;iBACrB;aACF,CACN;QACD,cAAc,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CACtC,KAAK,CAAC,QAAQ;YACZ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YAC/D,CAAC,CAAC,EAAE,CACP;KACF,CAAC;AACJ,CAAC"}
@@ -5,4 +5,5 @@ export * from "./storage.js";
5
5
  export * from "./evaluation-protocol.js";
6
6
  export * from "./prepare.js";
7
7
  export * from "./consumer.js";
8
+ export * from "./drift-tickets.js";
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/health/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/health/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC"}
@@ -5,4 +5,5 @@ export * from "./storage.js";
5
5
  export * from "./evaluation-protocol.js";
6
6
  export * from "./prepare.js";
7
7
  export * from "./consumer.js";
8
+ export * from "./drift-tickets.js";
8
9
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/health/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/health/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC"}
package/package.json CHANGED
@@ -129,7 +129,7 @@
129
129
  }
130
130
  },
131
131
  "name": "@codyswann/lisa",
132
- "version": "3.18.2",
132
+ "version": "3.19.0",
133
133
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
134
134
  "main": "dist/index.js",
135
135
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.18.2",
3
+ "version": "3.19.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.18.2",
3
+ "version": "3.19.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,80 @@
1
+ ---
2
+ name: lisa-health-drift-cron
3
+ description: "Scheduled health consumer"
4
+ allowed-tools: ["Skill", "Bash", "Read"]
5
+ ---
6
+
7
+ # Lisa Health Drift Cron: $ARGUMENTS
8
+
9
+ Consumer #3 of the health layer: the cron. A health check you have to remember to run is a health check that goes stale, so this one runs on a schedule and turns drift into **tracked work** rather than silent decay.
10
+
11
+ It **files**. It never closes, edits, or repairs. Disposal belongs to humans and the Implement factory.
12
+
13
+ ## Why idempotency is the whole job
14
+
15
+ A nightly cron that refiles the same drift every night is worse than no cron at all, because it teaches everyone to ignore the tickets it files. Every rule below exists to make *"the same drift"* a decidable question, and the decision is not made in this prose — it is made by `planDriftTickets` in `src/health/drift-tickets.ts`, which is unit-tested against each of these cases.
16
+
17
+ **Do not reimplement the dedupe here.** Call the planner and act on its answer. A second implementation in prose is a second implementation to drift.
18
+
19
+ ## Phase 1 — Run the health check headless
20
+
21
+ Invoke **`/lisa-health`** and use the JSON it emits. Do not call the CLI directly and do not invent flags for it: `lisa health` takes `[path]`, `--prepare-agentic`, and `--agentic-evaluation`, and nothing else. There is no `--json` — the persisted result IS the command's stdout, and an unknown option makes commander exit non-zero, so a guessed flag turns the first phase of a scheduled run into a failure nobody is watching.
22
+
23
+ Routing through the skill also means the bounded harness-review step and its cleanup stay in one place rather than being re-described here and drifting.
24
+
25
+ Do not reconstruct, merge, or summarize findings; the result is passed to Phase 3 verbatim.
26
+
27
+ If the run itself fails, that is a **recovery-required** outcome. Report it and stop. Do not file a drift ticket about a health check that did not complete — a run that could not measure has not found drift, and saying otherwise is the same defect the health layer exists to catch.
28
+
29
+ ## Phase 2 — Read the OPEN tickets only
30
+
31
+ Fetch the tracker's **open** items carrying the drift marker prefix `lisa-health-drift`.
32
+
33
+ **Open only, and this is load-bearing.** A closed ticket must not suppress live drift: if it did, closing a ticket without fixing anything would make that drift invisible forever, which is exactly the silent decay this consumer exists to prevent. Suppression should be a configured declaration visible in a diff — turn the check off in config — not a side effect of somebody tidying a backlog.
34
+
35
+ The planner enforces this by construction: it accepts an `openTickets` list and has no notion of a closed one. Passing closed tickets defeats the design, and the parameter is named to say so.
36
+
37
+ ## Phase 3 — Plan
38
+
39
+ Feed the findings and the open tickets to `planDriftTickets`. It returns:
40
+
41
+ - `file` — one entry per drifting check with no open ticket, carrying `title`, `body`, and `marker`
42
+ - `alreadyTracked` — drift that an open ticket already covers, with the ticket id
43
+
44
+ Every unique drifting check lands in exactly one of the two after duplicate findings for the same check are collapsed, so a run reporting "nothing to do" is asserting it looked at all of them.
45
+
46
+ The collapse is not tidiness. The marker cannot tell two findings for one check apart, so filing both would self-duplicate on the very FIRST run — before any second run exists to blame. `file.length + alreadyTracked.length` therefore counts distinct drifting checks, which is fewer than the drifting findings whenever a run reports one check twice.
47
+
48
+ Dedupe is per **check**, not per drift set. Fingerprinting the whole finding set would mean one added finding produces a fresh ticket while the old one still stands, so a slowly-degrading project accumulates near-duplicates — the same "worse than no cron" outcome by another route.
49
+
50
+ ## Phase 4 — File
51
+
52
+ For each entry in `file`, invoke **`/lisa-tracker-write`** with its title and body. **Never call a vendor writer (`lisa-github-write-issue` / `lisa-jira-write-ticket` / `lisa-linear-write-issue`) directly** — routing through the shim is what makes the tracker switchable per project.
53
+
54
+ The body already contains the marker. Do not strip it, and do not add a second one: the next run finds the ticket by that exact string.
55
+
56
+ ## Run outcome
57
+
58
+ Per the automation runbook contract, end with exactly one outcome and a one-line operator-readable summary:
59
+
60
+ - **no-change** — the project is in band, or every drifting check is already tracked. Say which: `no-change — in band, nothing filed.` versus `no-change — 2 drifting checks, both already tracked (#41, #42).`
61
+ - **change-proved** — tickets were filed. `change-proved — filed 1 drift ticket: coverage-floor (#57).`
62
+ - **recovery-required** — the health run failed, or a write failed. Name what a human must do.
63
+
64
+ The distinction between the two `no-change` shapes matters. "Nothing filed" and "nothing wrong" are different facts, and collapsing them hides a project whose drift is real and simply already on somebody's list.
65
+
66
+ ## Registration
67
+
68
+ Registered as `lisa-auto-<project>-health-drift` when `health.schedule` is set to `daily` or `weekly` in `.lisa.config.json`. `off` (the default) registers nothing. Torn down with the rest of the `lisa-auto-<project>-*` set.
69
+
70
+ Register at most **one** per project.
71
+
72
+ ### What the dedupe does and does not guarantee
73
+
74
+ It guarantees **convergence, not mutual exclusion**, and the difference is worth stating plainly rather than leaving to be discovered.
75
+
76
+ Two runs that overlap — a manual invocation alongside the scheduled one, or a run still going when the next fires — can both read an empty open-ticket set and both file for the same check. A single registration makes that rare; it does not make it impossible, and no amount of care in this document would.
77
+
78
+ What the design does guarantee is that it stops there. The next run sees both open tickets carrying the marker, matches, and files nothing further, so duplicates do not compound. The transient duplicate persists until a human closes one, and closing it is safe: if the drift is still live, the remaining ticket still tracks it.
79
+
80
+ This is the same stance `lisa-learnings-audit` takes for the same reason, and the same advice applies — **a manual run should first confirm the cron is not due or running.** A project-scoped lease would buy true exclusion at the cost of a lock to acquire, hold, and expire correctly on a path that only runs daily; the failure it prevents is cosmetic and self-limiting, and the failure a stuck lease causes is a health check that silently stops running.
@@ -0,0 +1,4 @@
1
+ display_name: "Health Drift Cron"
2
+ short_description: "Scheduled health consumer"
3
+ default_prompt:
4
+ - "Use $lisa-health-drift-cron: Scheduled health consumer."
@@ -197,6 +197,16 @@ closed by the next run's dedupe or the human), not mutual exclusion; manual
197
197
  runs should first confirm the cron is not due or running. Tear-down removes
198
198
  it with the rest of the `lisa-auto-<project>-*` set.
199
199
 
200
+ **Optional automation — the health cron.** When `health.schedule` in
201
+ `.lisa.config.json` is `daily` or `weekly` (default **off**, which registers
202
+ nothing), additionally create `lisa-auto-<project>-health-drift` running
203
+ `/lisa:health-drift-cron` at that cadence. It runs Lisa Health headless and,
204
+ for each check that has drifted, files exactly one ticket through
205
+ `lisa-tracker-write`. A project in band files nothing. Register at most ONE per
206
+ project: the per-check marker dedupe converges under a single scheduled runner,
207
+ and two concurrent runs can both observe an empty open-ticket set and both
208
+ file. Tear-down removes it with the rest of the `lisa-auto-<project>-*` set.
209
+
200
210
  **Exploratory PRD pressure gate.** `auto-start-prds=true` means "create PRDs in the ready PRD
201
211
  lifecycle when the PRD queue has capacity," not "always create a new ready PRD." The
202
212
  `exploratory-prds` automation uses the same PRD source queue and pressure roles reported by
@@ -0,0 +1,9 @@
1
+ ---
2
+ description: "Run the scheduled health consumer: check health headless, and file one deduped ticket per drifting check."
3
+ argument-hint: "[project-path]"
4
+ ---
5
+
6
+ Use the /lisa-health-drift-cron skill to run Lisa Health for the optional project path and turn any
7
+ drift into tracked work. File exactly one ticket per drifting check through `lisa-tracker-write`,
8
+ deduped by the per-check marker across OPEN tickets only, and file nothing for a project in band.
9
+ This flow files; it never closes, edits, or repairs. $ARGUMENTS
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: lisa-health-drift-cron
3
+ description: "Scheduled health consumer. Runs Lisa Health headless, and for each check that has drifted files exactly one tracker ticket through lisa-tracker-write, deduped by a per-check marker across OPEN tickets only. Files nothing for a project in band. Use for the lisa-auto-<project>-health-drift automation registered when health.schedule is set."
4
+ allowed-tools: ["Skill", "Bash", "Read"]
5
+ ---
6
+
7
+ # Lisa Health Drift Cron: $ARGUMENTS
8
+
9
+ Consumer #3 of the health layer: the cron. A health check you have to remember to run is a health check that goes stale, so this one runs on a schedule and turns drift into **tracked work** rather than silent decay.
10
+
11
+ It **files**. It never closes, edits, or repairs. Disposal belongs to humans and the Implement factory.
12
+
13
+ ## Why idempotency is the whole job
14
+
15
+ A nightly cron that refiles the same drift every night is worse than no cron at all, because it teaches everyone to ignore the tickets it files. Every rule below exists to make *"the same drift"* a decidable question, and the decision is not made in this prose — it is made by `planDriftTickets` in `src/health/drift-tickets.ts`, which is unit-tested against each of these cases.
16
+
17
+ **Do not reimplement the dedupe here.** Call the planner and act on its answer. A second implementation in prose is a second implementation to drift.
18
+
19
+ ## Phase 1 — Run the health check headless
20
+
21
+ Invoke **`/lisa-health`** and use the JSON it emits. Do not call the CLI directly and do not invent flags for it: `lisa health` takes `[path]`, `--prepare-agentic`, and `--agentic-evaluation`, and nothing else. There is no `--json` — the persisted result IS the command's stdout, and an unknown option makes commander exit non-zero, so a guessed flag turns the first phase of a scheduled run into a failure nobody is watching.
22
+
23
+ Routing through the skill also means the bounded harness-review step and its cleanup stay in one place rather than being re-described here and drifting.
24
+
25
+ Do not reconstruct, merge, or summarize findings; the result is passed to Phase 3 verbatim.
26
+
27
+ If the run itself fails, that is a **recovery-required** outcome. Report it and stop. Do not file a drift ticket about a health check that did not complete — a run that could not measure has not found drift, and saying otherwise is the same defect the health layer exists to catch.
28
+
29
+ ## Phase 2 — Read the OPEN tickets only
30
+
31
+ Fetch the tracker's **open** items carrying the drift marker prefix `lisa-health-drift`.
32
+
33
+ **Open only, and this is load-bearing.** A closed ticket must not suppress live drift: if it did, closing a ticket without fixing anything would make that drift invisible forever, which is exactly the silent decay this consumer exists to prevent. Suppression should be a configured declaration visible in a diff — turn the check off in config — not a side effect of somebody tidying a backlog.
34
+
35
+ The planner enforces this by construction: it accepts an `openTickets` list and has no notion of a closed one. Passing closed tickets defeats the design, and the parameter is named to say so.
36
+
37
+ ## Phase 3 — Plan
38
+
39
+ Feed the findings and the open tickets to `planDriftTickets`. It returns:
40
+
41
+ - `file` — one entry per drifting check with no open ticket, carrying `title`, `body`, and `marker`
42
+ - `alreadyTracked` — drift that an open ticket already covers, with the ticket id
43
+
44
+ Every unique drifting check lands in exactly one of the two after duplicate findings for the same check are collapsed, so a run reporting "nothing to do" is asserting it looked at all of them.
45
+
46
+ The collapse is not tidiness. The marker cannot tell two findings for one check apart, so filing both would self-duplicate on the very FIRST run — before any second run exists to blame. `file.length + alreadyTracked.length` therefore counts distinct drifting checks, which is fewer than the drifting findings whenever a run reports one check twice.
47
+
48
+ Dedupe is per **check**, not per drift set. Fingerprinting the whole finding set would mean one added finding produces a fresh ticket while the old one still stands, so a slowly-degrading project accumulates near-duplicates — the same "worse than no cron" outcome by another route.
49
+
50
+ ## Phase 4 — File
51
+
52
+ For each entry in `file`, invoke **`/lisa-tracker-write`** with its title and body. **Never call a vendor writer (`lisa-github-write-issue` / `lisa-jira-write-ticket` / `lisa-linear-write-issue`) directly** — routing through the shim is what makes the tracker switchable per project.
53
+
54
+ The body already contains the marker. Do not strip it, and do not add a second one: the next run finds the ticket by that exact string.
55
+
56
+ ## Run outcome
57
+
58
+ Per the automation runbook contract, end with exactly one outcome and a one-line operator-readable summary:
59
+
60
+ - **no-change** — the project is in band, or every drifting check is already tracked. Say which: `no-change — in band, nothing filed.` versus `no-change — 2 drifting checks, both already tracked (#41, #42).`
61
+ - **change-proved** — tickets were filed. `change-proved — filed 1 drift ticket: coverage-floor (#57).`
62
+ - **recovery-required** — the health run failed, or a write failed. Name what a human must do.
63
+
64
+ The distinction between the two `no-change` shapes matters. "Nothing filed" and "nothing wrong" are different facts, and collapsing them hides a project whose drift is real and simply already on somebody's list.
65
+
66
+ ## Registration
67
+
68
+ Registered as `lisa-auto-<project>-health-drift` when `health.schedule` is set to `daily` or `weekly` in `.lisa.config.json`. `off` (the default) registers nothing. Torn down with the rest of the `lisa-auto-<project>-*` set.
69
+
70
+ Register at most **one** per project.
71
+
72
+ ### What the dedupe does and does not guarantee
73
+
74
+ It guarantees **convergence, not mutual exclusion**, and the difference is worth stating plainly rather than leaving to be discovered.
75
+
76
+ Two runs that overlap — a manual invocation alongside the scheduled one, or a run still going when the next fires — can both read an empty open-ticket set and both file for the same check. A single registration makes that rare; it does not make it impossible, and no amount of care in this document would.
77
+
78
+ What the design does guarantee is that it stops there. The next run sees both open tickets carrying the marker, matches, and files nothing further, so duplicates do not compound. The transient duplicate persists until a human closes one, and closing it is safe: if the drift is still live, the remaining ticket still tracks it.
79
+
80
+ This is the same stance `lisa-learnings-audit` takes for the same reason, and the same advice applies — **a manual run should first confirm the cron is not due or running.** A project-scoped lease would buy true exclusion at the cost of a lock to acquire, hold, and expire correctly on a path that only runs daily; the failure it prevents is cosmetic and self-limiting, and the failure a stuck lease causes is a health check that silently stops running.
@@ -0,0 +1,4 @@
1
+ display_name: "Health Drift Cron"
2
+ short_description: "Scheduled health consumer"
3
+ default_prompt:
4
+ - "Use $lisa-health-drift-cron: Scheduled health consumer."
@@ -197,6 +197,16 @@ closed by the next run's dedupe or the human), not mutual exclusion; manual
197
197
  runs should first confirm the cron is not due or running. Tear-down removes
198
198
  it with the rest of the `lisa-auto-<project>-*` set.
199
199
 
200
+ **Optional automation — the health cron.** When `health.schedule` in
201
+ `.lisa.config.json` is `daily` or `weekly` (default **off**, which registers
202
+ nothing), additionally create `lisa-auto-<project>-health-drift` running
203
+ `/lisa:health-drift-cron` at that cadence. It runs Lisa Health headless and,
204
+ for each check that has drifted, files exactly one ticket through
205
+ `lisa-tracker-write`. A project in band files nothing. Register at most ONE per
206
+ project: the per-check marker dedupe converges under a single scheduled runner,
207
+ and two concurrent runs can both observe an empty open-ticket set and both
208
+ file. Tear-down removes it with the rest of the `lisa-auto-<project>-*` set.
209
+
200
210
  **Exploratory PRD pressure gate.** `auto-start-prds=true` means "create PRDs in the ready PRD
201
211
  lifecycle when the PRD queue has capacity," not "always create a new ready PRD." The
202
212
  `exploratory-prds` automation uses the same PRD source queue and pressure roles reported by
@@ -0,0 +1,9 @@
1
+ ---
2
+ description: "Run the scheduled health consumer: check health headless, and file one deduped ticket per drifting check."
3
+ argument-hint: "[project-path]"
4
+ ---
5
+
6
+ Use the /lisa-health-drift-cron skill to run Lisa Health for the optional project path and turn any
7
+ drift into tracked work. File exactly one ticket per drifting check through `lisa-tracker-write`,
8
+ deduped by the per-check marker across OPEN tickets only, and file nothing for a project in band.
9
+ This flow files; it never closes, edits, or repairs. $ARGUMENTS
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.18.2",
3
+ "version": "3.19.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,80 @@
1
+ ---
2
+ name: lisa-health-drift-cron
3
+ description: "Scheduled health consumer. Runs Lisa Health headless, and for each check that has drifted files exactly one tracker ticket through lisa-tracker-write, deduped by a per-check marker across OPEN tickets only. Files nothing for a project in band. Use for the lisa-auto-<project>-health-drift automation registered when health.schedule is set."
4
+ allowed-tools: ["Skill", "Bash", "Read"]
5
+ ---
6
+
7
+ # Lisa Health Drift Cron: $ARGUMENTS
8
+
9
+ Consumer #3 of the health layer: the cron. A health check you have to remember to run is a health check that goes stale, so this one runs on a schedule and turns drift into **tracked work** rather than silent decay.
10
+
11
+ It **files**. It never closes, edits, or repairs. Disposal belongs to humans and the Implement factory.
12
+
13
+ ## Why idempotency is the whole job
14
+
15
+ A nightly cron that refiles the same drift every night is worse than no cron at all, because it teaches everyone to ignore the tickets it files. Every rule below exists to make *"the same drift"* a decidable question, and the decision is not made in this prose — it is made by `planDriftTickets` in `src/health/drift-tickets.ts`, which is unit-tested against each of these cases.
16
+
17
+ **Do not reimplement the dedupe here.** Call the planner and act on its answer. A second implementation in prose is a second implementation to drift.
18
+
19
+ ## Phase 1 — Run the health check headless
20
+
21
+ Invoke **`/lisa-health`** and use the JSON it emits. Do not call the CLI directly and do not invent flags for it: `lisa health` takes `[path]`, `--prepare-agentic`, and `--agentic-evaluation`, and nothing else. There is no `--json` — the persisted result IS the command's stdout, and an unknown option makes commander exit non-zero, so a guessed flag turns the first phase of a scheduled run into a failure nobody is watching.
22
+
23
+ Routing through the skill also means the bounded harness-review step and its cleanup stay in one place rather than being re-described here and drifting.
24
+
25
+ Do not reconstruct, merge, or summarize findings; the result is passed to Phase 3 verbatim.
26
+
27
+ If the run itself fails, that is a **recovery-required** outcome. Report it and stop. Do not file a drift ticket about a health check that did not complete — a run that could not measure has not found drift, and saying otherwise is the same defect the health layer exists to catch.
28
+
29
+ ## Phase 2 — Read the OPEN tickets only
30
+
31
+ Fetch the tracker's **open** items carrying the drift marker prefix `lisa-health-drift`.
32
+
33
+ **Open only, and this is load-bearing.** A closed ticket must not suppress live drift: if it did, closing a ticket without fixing anything would make that drift invisible forever, which is exactly the silent decay this consumer exists to prevent. Suppression should be a configured declaration visible in a diff — turn the check off in config — not a side effect of somebody tidying a backlog.
34
+
35
+ The planner enforces this by construction: it accepts an `openTickets` list and has no notion of a closed one. Passing closed tickets defeats the design, and the parameter is named to say so.
36
+
37
+ ## Phase 3 — Plan
38
+
39
+ Feed the findings and the open tickets to `planDriftTickets`. It returns:
40
+
41
+ - `file` — one entry per drifting check with no open ticket, carrying `title`, `body`, and `marker`
42
+ - `alreadyTracked` — drift that an open ticket already covers, with the ticket id
43
+
44
+ Every unique drifting check lands in exactly one of the two after duplicate findings for the same check are collapsed, so a run reporting "nothing to do" is asserting it looked at all of them.
45
+
46
+ The collapse is not tidiness. The marker cannot tell two findings for one check apart, so filing both would self-duplicate on the very FIRST run — before any second run exists to blame. `file.length + alreadyTracked.length` therefore counts distinct drifting checks, which is fewer than the drifting findings whenever a run reports one check twice.
47
+
48
+ Dedupe is per **check**, not per drift set. Fingerprinting the whole finding set would mean one added finding produces a fresh ticket while the old one still stands, so a slowly-degrading project accumulates near-duplicates — the same "worse than no cron" outcome by another route.
49
+
50
+ ## Phase 4 — File
51
+
52
+ For each entry in `file`, invoke **`/lisa-tracker-write`** with its title and body. **Never call a vendor writer (`lisa-github-write-issue` / `lisa-jira-write-ticket` / `lisa-linear-write-issue`) directly** — routing through the shim is what makes the tracker switchable per project.
53
+
54
+ The body already contains the marker. Do not strip it, and do not add a second one: the next run finds the ticket by that exact string.
55
+
56
+ ## Run outcome
57
+
58
+ Per the automation runbook contract, end with exactly one outcome and a one-line operator-readable summary:
59
+
60
+ - **no-change** — the project is in band, or every drifting check is already tracked. Say which: `no-change — in band, nothing filed.` versus `no-change — 2 drifting checks, both already tracked (#41, #42).`
61
+ - **change-proved** — tickets were filed. `change-proved — filed 1 drift ticket: coverage-floor (#57).`
62
+ - **recovery-required** — the health run failed, or a write failed. Name what a human must do.
63
+
64
+ The distinction between the two `no-change` shapes matters. "Nothing filed" and "nothing wrong" are different facts, and collapsing them hides a project whose drift is real and simply already on somebody's list.
65
+
66
+ ## Registration
67
+
68
+ Registered as `lisa-auto-<project>-health-drift` when `health.schedule` is set to `daily` or `weekly` in `.lisa.config.json`. `off` (the default) registers nothing. Torn down with the rest of the `lisa-auto-<project>-*` set.
69
+
70
+ Register at most **one** per project.
71
+
72
+ ### What the dedupe does and does not guarantee
73
+
74
+ It guarantees **convergence, not mutual exclusion**, and the difference is worth stating plainly rather than leaving to be discovered.
75
+
76
+ Two runs that overlap — a manual invocation alongside the scheduled one, or a run still going when the next fires — can both read an empty open-ticket set and both file for the same check. A single registration makes that rare; it does not make it impossible, and no amount of care in this document would.
77
+
78
+ What the design does guarantee is that it stops there. The next run sees both open tickets carrying the marker, matches, and files nothing further, so duplicates do not compound. The transient duplicate persists until a human closes one, and closing it is safe: if the drift is still live, the remaining ticket still tracks it.
79
+
80
+ This is the same stance `lisa-learnings-audit` takes for the same reason, and the same advice applies — **a manual run should first confirm the cron is not due or running.** A project-scoped lease would buy true exclusion at the cost of a lock to acquire, hold, and expire correctly on a path that only runs daily; the failure it prevents is cosmetic and self-limiting, and the failure a stuck lease causes is a health check that silently stops running.
@@ -197,6 +197,16 @@ closed by the next run's dedupe or the human), not mutual exclusion; manual
197
197
  runs should first confirm the cron is not due or running. Tear-down removes
198
198
  it with the rest of the `lisa-auto-<project>-*` set.
199
199
 
200
+ **Optional automation — the health cron.** When `health.schedule` in
201
+ `.lisa.config.json` is `daily` or `weekly` (default **off**, which registers
202
+ nothing), additionally create `lisa-auto-<project>-health-drift` running
203
+ `/lisa:health-drift-cron` at that cadence. It runs Lisa Health headless and,
204
+ for each check that has drifted, files exactly one ticket through
205
+ `lisa-tracker-write`. A project in band files nothing. Register at most ONE per
206
+ project: the per-check marker dedupe converges under a single scheduled runner,
207
+ and two concurrent runs can both observe an empty open-ticket set and both
208
+ file. Tear-down removes it with the rest of the `lisa-auto-<project>-*` set.
209
+
200
210
  **Exploratory PRD pressure gate.** `auto-start-prds=true` means "create PRDs in the ready PRD
201
211
  lifecycle when the PRD queue has capacity," not "always create a new ready PRD." The
202
212
  `exploratory-prds` automation uses the same PRD source queue and pressure roles reported by
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "3.18.2",
3
+ "version": "3.19.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": "3.18.2",
3
+ "version": "3.19.0",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"