@codyswann/lisa 2.76.0 → 2.77.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.
package/package.json CHANGED
@@ -82,7 +82,7 @@
82
82
  "lodash": ">=4.18.1"
83
83
  },
84
84
  "name": "@codyswann/lisa",
85
- "version": "2.76.0",
85
+ "version": "2.77.1",
86
86
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
87
87
  "main": "dist/index.js",
88
88
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
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": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -398,6 +398,37 @@ For batch skills that consume `source`:
398
398
  2. If `$ARGUMENTS` is the bare token `notion` / `confluence` / `linear` / `github` / `jira`, the source is that vendor; resolve location from the corresponding config section.
399
399
  3. If `$ARGUMENTS` is empty, fall back to `source` from config; if that's also empty, stop and report `"No source specified and no 'source' field in .lisa.config.json."`
400
400
 
401
+ ### Doctor config readiness
402
+
403
+ `/lisa:doctor` reads the same config, but it audits readiness instead of dispatching a write.
404
+ Doctor must validate config in three layers:
405
+
406
+ 1. **Parse and merge**
407
+ - Parse both config files as JSON. Missing or invalid `.lisa.config.json` is a blocking error.
408
+ `.lisa.config.local.json` is optional, but if present and invalid it is also a blocking error.
409
+ - Merge per key with the standard local-overrides-global rule. Doctor reports against the merged
410
+ effective config; it does not treat the local file as a full replacement for the committed
411
+ file.
412
+ 2. **Required-key correctness**
413
+ - Missing `tracker` after merge is a blocking error. Unknown merged `tracker` / `source` values
414
+ are also blocking errors.
415
+ - If the configured tracker/source vendor is missing its required keys after merge, doctor must
416
+ report a blocking readiness failure using the vendor tables above. Examples: `tracker=github`
417
+ requires `github.org` + `github.repo`; `tracker=jira` requires `atlassian.cloudId` +
418
+ `jira.project`; `source=notion` requires `notion.workspaceId` + `notion.prdDatabaseId`.
419
+ 3. **Field locality correctness**
420
+ - `atlassian.email`, `intake.assignee`, and `jira.verified_workflow_hash` are local-only. If
421
+ they appear in committed config, doctor warns that developer-specific state was checked into
422
+ the project file.
423
+ - Project-wide fields that exist only in `.lisa.config.local.json` should warn, not pass
424
+ silently. Current machine works, repository not durably configured for teammates and
425
+ automations. Common examples include `tracker`, `source`, `github.org`, `github.repo`,
426
+ `atlassian.cloudId`, `atlassian.site`, `jira.project`, `linear.workspace`, `linear.teamKey`,
427
+ and `deploy.branches`.
428
+
429
+ Doctor's severity rule is simple: unusable merged config is `FAIL`; locality drift with a still
430
+ usable merged config is `WARN`.
431
+
401
432
  ## Skill mapping
402
433
 
403
434
  The shim → vendor mapping is fixed:
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared doctor report helpers for the base Lisa doctor surface.
4
+ *
5
+ * The first doctor milestone needs a stable grouped output contract before the
6
+ * repo adds real readiness probes. Keep this file dependency-free so future
7
+ * doctor scripts can reuse it from plugin distributions and downstream repos.
8
+ */
9
+
10
+ export const DOCTOR_STATUSES = ["PASS", "WARN", "FAIL", "SKIP"];
11
+ export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
12
+
13
+ /**
14
+ * @typedef {"PASS" | "WARN" | "FAIL" | "SKIP"} DoctorStatus
15
+ * @typedef {"READY" | "READY_WITH_WARNINGS" | "NOT_READY"} DoctorVerdict
16
+ *
17
+ * @typedef {{
18
+ * readonly id: string
19
+ * readonly status: DoctorStatus
20
+ * readonly summary: string
21
+ * readonly observed?: string
22
+ * readonly remediation?: string
23
+ * }} DoctorCheck
24
+ *
25
+ * @typedef {{
26
+ * readonly id: string
27
+ * readonly title: string
28
+ * readonly checks: readonly DoctorCheck[]
29
+ * }} DoctorGroup
30
+ *
31
+ * @typedef {{
32
+ * readonly generatedAt?: string
33
+ * readonly groups: readonly DoctorGroup[]
34
+ * }} DoctorReportInput
35
+ */
36
+
37
+ /**
38
+ * @param {readonly DoctorGroup[]} groups
39
+ * @returns {DoctorVerdict}
40
+ */
41
+ export function computeDoctorVerdict(groups) {
42
+ const checks = groups.flatMap(group => group.checks);
43
+ if (checks.some(check => check.status === "FAIL")) {
44
+ return "NOT_READY";
45
+ }
46
+ if (checks.some(check => check.status === "WARN")) {
47
+ return "READY_WITH_WARNINGS";
48
+ }
49
+ return "READY";
50
+ }
51
+
52
+ /**
53
+ * @param {readonly DoctorGroup[]} groups
54
+ * @returns {Record<DoctorStatus, number>}
55
+ */
56
+ export function countDoctorStatuses(groups) {
57
+ return groups
58
+ .flatMap(group => group.checks)
59
+ .reduce(
60
+ (counts, check) => ({
61
+ ...counts,
62
+ [check.status]: counts[check.status] + 1,
63
+ }),
64
+ { PASS: 0, WARN: 0, FAIL: 0, SKIP: 0 }
65
+ );
66
+ }
67
+
68
+ /**
69
+ * @param {DoctorReportInput} input
70
+ * @returns {{ readonly verdict: DoctorVerdict, readonly counts: Record<DoctorStatus, number>, readonly text: string }}
71
+ */
72
+ export function renderDoctorReport(input) {
73
+ const groups = input.groups.map(normalizeGroup);
74
+ const verdict = computeDoctorVerdict(groups);
75
+ const counts = countDoctorStatuses(groups);
76
+ const lines = [
77
+ `Overall verdict: ${verdict}`,
78
+ `Counts: ${DOCTOR_STATUSES.map(status => `${counts[status]} ${status}`).join(", ")}`,
79
+ ];
80
+
81
+ if (input.generatedAt) {
82
+ lines.push(`Generated at: ${input.generatedAt}`);
83
+ }
84
+
85
+ for (const group of groups) {
86
+ lines.push("", `${group.id}. ${group.title}`);
87
+ if (group.checks.length === 0) {
88
+ lines.push("- SKIP empty-group: no checks registered yet");
89
+ continue;
90
+ }
91
+ for (const check of group.checks) {
92
+ lines.push(`- ${check.status} ${check.id}: ${check.summary}`);
93
+ if (check.observed) {
94
+ lines.push(` Observed: ${check.observed}`);
95
+ }
96
+ if (check.remediation) {
97
+ lines.push(` Remediation: ${check.remediation}`);
98
+ }
99
+ }
100
+ }
101
+
102
+ return {
103
+ verdict,
104
+ counts,
105
+ text: `${lines.join("\n")}\n`,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * @param {DoctorGroup} group
111
+ * @returns {DoctorGroup}
112
+ */
113
+ function normalizeGroup(group) {
114
+ return {
115
+ ...group,
116
+ checks: group.checks.map(normalizeCheck),
117
+ };
118
+ }
119
+
120
+ /**
121
+ * @param {DoctorCheck} check
122
+ * @returns {DoctorCheck}
123
+ */
124
+ function normalizeCheck(check) {
125
+ const normalizedStatus = DOCTOR_STATUSES.includes(check.status)
126
+ ? check.status
127
+ : "FAIL";
128
+ return {
129
+ ...check,
130
+ status: normalizedStatus,
131
+ };
132
+ }
@@ -69,6 +69,41 @@ as applicable to the current repo:
69
69
 
70
70
  If a check family is not applicable to the current repo, report `SKIP` with the reason.
71
71
 
72
+ ### Minimum config-readiness checks
73
+
74
+ The Lisa config group is not just "does a file exist?" Doctor must audit the config contract in
75
+ this order:
76
+
77
+ 1. **Presence + parseability**
78
+ - `FAIL` when `.lisa.config.json` is missing, empty, or invalid JSON.
79
+ - Read `.lisa.config.local.json` only when present; if present but invalid JSON, `FAIL`.
80
+ 2. **Merged effective config**
81
+ - Resolve every key with the same per-key local-overrides-global semantics documented by
82
+ `config-resolution`. Doctor must describe findings against the effective merged value, not by
83
+ pretending one file fully replaces the other.
84
+ 3. **Required top-level dispatch keys**
85
+ - `FAIL` when merged `tracker` is missing or is not one of `jira`, `github`, or `linear`.
86
+ - `FAIL` when merged `source` is present but is not one of `notion`, `confluence`, `linear`,
87
+ `github`, or `jira`.
88
+ 4. **Vendor required-key audit**
89
+ - `FAIL` when the configured tracker/source points at a vendor whose required keys are absent
90
+ after merge. Examples: `tracker=github` requires `github.org` + `github.repo`;
91
+ `tracker=jira` requires `atlassian.cloudId` + `jira.project`; `source=notion` requires
92
+ `notion.workspaceId` + `notion.prdDatabaseId`.
93
+ - Reuse the `config-resolution` vendor tables rather than inventing a second required-key list.
94
+ 5. **Local-vs-committed locality audit**
95
+ - `WARN` when developer-specific fields appear in committed config. At minimum enforce the
96
+ documented local-only examples: `atlassian.email`, `intake.assignee`, and
97
+ `jira.verified_workflow_hash`.
98
+ - `WARN` when project-wide shared fields exist only in `.lisa.config.local.json` and are absent
99
+ from `.lisa.config.json`, because the current machine may work while the repository remains
100
+ under-configured for teammates and automations. Examples include `tracker`, `source`,
101
+ `github.org`, `github.repo`, `atlassian.cloudId`, `atlassian.site`, `jira.project`,
102
+ `linear.workspace`, `linear.teamKey`, and `deploy.branches`.
103
+
104
+ Locality findings are advisory unless the merged config is unusable. Missing shared keys after the
105
+ merge are `FAIL`; shared keys that exist only locally are `WARN`.
106
+
72
107
  ## Output contract
73
108
 
74
109
  The final report must:
@@ -78,6 +113,17 @@ The final report must:
78
113
  - Emit exactly one overall verdict: `READY`, `READY_WITH_WARNINGS`, or `NOT_READY`.
79
114
  - Stay read-only by default.
80
115
 
116
+ Render the report in grouped sections using the shared `scripts/doctor-report.mjs` contract:
117
+
118
+ - Start with `Overall verdict: <VERDICT>` and one `Counts:` line covering `PASS`, `WARN`, `FAIL`,
119
+ and `SKIP`.
120
+ - Then print each group as `<group-id>. <group-title>`.
121
+ - Under each group, print one line per check as `- <STATUS> <check-id>: <summary>`.
122
+ - When available, print `Observed:` and `Remediation:` lines beneath the check so the report keeps
123
+ facts separate from advice.
124
+ - If a group has no applicable checks yet, render it as a grouped `SKIP` with the reason instead of
125
+ silently omitting the section.
126
+
81
127
  The verdict ladder is:
82
128
 
83
129
  - `READY` — no `FAIL` and no `WARN`.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
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.76.0",
3
+ "version": "2.77.1",
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-expo",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "description": "Harper/Fabric-specific Lisa rules for TypeScript component apps.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "description": "NestJS-specific skills and migration write-protection hooks.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -398,6 +398,37 @@ For batch skills that consume `source`:
398
398
  2. If `$ARGUMENTS` is the bare token `notion` / `confluence` / `linear` / `github` / `jira`, the source is that vendor; resolve location from the corresponding config section.
399
399
  3. If `$ARGUMENTS` is empty, fall back to `source` from config; if that's also empty, stop and report `"No source specified and no 'source' field in .lisa.config.json."`
400
400
 
401
+ ### Doctor config readiness
402
+
403
+ `/lisa:doctor` reads the same config, but it audits readiness instead of dispatching a write.
404
+ Doctor must validate config in three layers:
405
+
406
+ 1. **Parse and merge**
407
+ - Parse both config files as JSON. Missing or invalid `.lisa.config.json` is a blocking error.
408
+ `.lisa.config.local.json` is optional, but if present and invalid it is also a blocking error.
409
+ - Merge per key with the standard local-overrides-global rule. Doctor reports against the merged
410
+ effective config; it does not treat the local file as a full replacement for the committed
411
+ file.
412
+ 2. **Required-key correctness**
413
+ - Missing `tracker` after merge is a blocking error. Unknown merged `tracker` / `source` values
414
+ are also blocking errors.
415
+ - If the configured tracker/source vendor is missing its required keys after merge, doctor must
416
+ report a blocking readiness failure using the vendor tables above. Examples: `tracker=github`
417
+ requires `github.org` + `github.repo`; `tracker=jira` requires `atlassian.cloudId` +
418
+ `jira.project`; `source=notion` requires `notion.workspaceId` + `notion.prdDatabaseId`.
419
+ 3. **Field locality correctness**
420
+ - `atlassian.email`, `intake.assignee`, and `jira.verified_workflow_hash` are local-only. If
421
+ they appear in committed config, doctor warns that developer-specific state was checked into
422
+ the project file.
423
+ - Project-wide fields that exist only in `.lisa.config.local.json` should warn, not pass
424
+ silently. Current machine works, repository not durably configured for teammates and
425
+ automations. Common examples include `tracker`, `source`, `github.org`, `github.repo`,
426
+ `atlassian.cloudId`, `atlassian.site`, `jira.project`, `linear.workspace`, `linear.teamKey`,
427
+ and `deploy.branches`.
428
+
429
+ Doctor's severity rule is simple: unusable merged config is `FAIL`; locality drift with a still
430
+ usable merged config is `WARN`.
431
+
401
432
  ## Skill mapping
402
433
 
403
434
  The shim → vendor mapping is fixed:
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared doctor report helpers for the base Lisa doctor surface.
4
+ *
5
+ * The first doctor milestone needs a stable grouped output contract before the
6
+ * repo adds real readiness probes. Keep this file dependency-free so future
7
+ * doctor scripts can reuse it from plugin distributions and downstream repos.
8
+ */
9
+
10
+ export const DOCTOR_STATUSES = ["PASS", "WARN", "FAIL", "SKIP"];
11
+ export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
12
+
13
+ /**
14
+ * @typedef {"PASS" | "WARN" | "FAIL" | "SKIP"} DoctorStatus
15
+ * @typedef {"READY" | "READY_WITH_WARNINGS" | "NOT_READY"} DoctorVerdict
16
+ *
17
+ * @typedef {{
18
+ * readonly id: string
19
+ * readonly status: DoctorStatus
20
+ * readonly summary: string
21
+ * readonly observed?: string
22
+ * readonly remediation?: string
23
+ * }} DoctorCheck
24
+ *
25
+ * @typedef {{
26
+ * readonly id: string
27
+ * readonly title: string
28
+ * readonly checks: readonly DoctorCheck[]
29
+ * }} DoctorGroup
30
+ *
31
+ * @typedef {{
32
+ * readonly generatedAt?: string
33
+ * readonly groups: readonly DoctorGroup[]
34
+ * }} DoctorReportInput
35
+ */
36
+
37
+ /**
38
+ * @param {readonly DoctorGroup[]} groups
39
+ * @returns {DoctorVerdict}
40
+ */
41
+ export function computeDoctorVerdict(groups) {
42
+ const checks = groups.flatMap(group => group.checks);
43
+ if (checks.some(check => check.status === "FAIL")) {
44
+ return "NOT_READY";
45
+ }
46
+ if (checks.some(check => check.status === "WARN")) {
47
+ return "READY_WITH_WARNINGS";
48
+ }
49
+ return "READY";
50
+ }
51
+
52
+ /**
53
+ * @param {readonly DoctorGroup[]} groups
54
+ * @returns {Record<DoctorStatus, number>}
55
+ */
56
+ export function countDoctorStatuses(groups) {
57
+ return groups
58
+ .flatMap(group => group.checks)
59
+ .reduce(
60
+ (counts, check) => ({
61
+ ...counts,
62
+ [check.status]: counts[check.status] + 1,
63
+ }),
64
+ { PASS: 0, WARN: 0, FAIL: 0, SKIP: 0 }
65
+ );
66
+ }
67
+
68
+ /**
69
+ * @param {DoctorReportInput} input
70
+ * @returns {{ readonly verdict: DoctorVerdict, readonly counts: Record<DoctorStatus, number>, readonly text: string }}
71
+ */
72
+ export function renderDoctorReport(input) {
73
+ const groups = input.groups.map(normalizeGroup);
74
+ const verdict = computeDoctorVerdict(groups);
75
+ const counts = countDoctorStatuses(groups);
76
+ const lines = [
77
+ `Overall verdict: ${verdict}`,
78
+ `Counts: ${DOCTOR_STATUSES.map(status => `${counts[status]} ${status}`).join(", ")}`,
79
+ ];
80
+
81
+ if (input.generatedAt) {
82
+ lines.push(`Generated at: ${input.generatedAt}`);
83
+ }
84
+
85
+ for (const group of groups) {
86
+ lines.push("", `${group.id}. ${group.title}`);
87
+ if (group.checks.length === 0) {
88
+ lines.push("- SKIP empty-group: no checks registered yet");
89
+ continue;
90
+ }
91
+ for (const check of group.checks) {
92
+ lines.push(`- ${check.status} ${check.id}: ${check.summary}`);
93
+ if (check.observed) {
94
+ lines.push(` Observed: ${check.observed}`);
95
+ }
96
+ if (check.remediation) {
97
+ lines.push(` Remediation: ${check.remediation}`);
98
+ }
99
+ }
100
+ }
101
+
102
+ return {
103
+ verdict,
104
+ counts,
105
+ text: `${lines.join("\n")}\n`,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * @param {DoctorGroup} group
111
+ * @returns {DoctorGroup}
112
+ */
113
+ function normalizeGroup(group) {
114
+ return {
115
+ ...group,
116
+ checks: group.checks.map(normalizeCheck),
117
+ };
118
+ }
119
+
120
+ /**
121
+ * @param {DoctorCheck} check
122
+ * @returns {DoctorCheck}
123
+ */
124
+ function normalizeCheck(check) {
125
+ const normalizedStatus = DOCTOR_STATUSES.includes(check.status)
126
+ ? check.status
127
+ : "FAIL";
128
+ return {
129
+ ...check,
130
+ status: normalizedStatus,
131
+ };
132
+ }
@@ -69,6 +69,41 @@ as applicable to the current repo:
69
69
 
70
70
  If a check family is not applicable to the current repo, report `SKIP` with the reason.
71
71
 
72
+ ### Minimum config-readiness checks
73
+
74
+ The Lisa config group is not just "does a file exist?" Doctor must audit the config contract in
75
+ this order:
76
+
77
+ 1. **Presence + parseability**
78
+ - `FAIL` when `.lisa.config.json` is missing, empty, or invalid JSON.
79
+ - Read `.lisa.config.local.json` only when present; if present but invalid JSON, `FAIL`.
80
+ 2. **Merged effective config**
81
+ - Resolve every key with the same per-key local-overrides-global semantics documented by
82
+ `config-resolution`. Doctor must describe findings against the effective merged value, not by
83
+ pretending one file fully replaces the other.
84
+ 3. **Required top-level dispatch keys**
85
+ - `FAIL` when merged `tracker` is missing or is not one of `jira`, `github`, or `linear`.
86
+ - `FAIL` when merged `source` is present but is not one of `notion`, `confluence`, `linear`,
87
+ `github`, or `jira`.
88
+ 4. **Vendor required-key audit**
89
+ - `FAIL` when the configured tracker/source points at a vendor whose required keys are absent
90
+ after merge. Examples: `tracker=github` requires `github.org` + `github.repo`;
91
+ `tracker=jira` requires `atlassian.cloudId` + `jira.project`; `source=notion` requires
92
+ `notion.workspaceId` + `notion.prdDatabaseId`.
93
+ - Reuse the `config-resolution` vendor tables rather than inventing a second required-key list.
94
+ 5. **Local-vs-committed locality audit**
95
+ - `WARN` when developer-specific fields appear in committed config. At minimum enforce the
96
+ documented local-only examples: `atlassian.email`, `intake.assignee`, and
97
+ `jira.verified_workflow_hash`.
98
+ - `WARN` when project-wide shared fields exist only in `.lisa.config.local.json` and are absent
99
+ from `.lisa.config.json`, because the current machine may work while the repository remains
100
+ under-configured for teammates and automations. Examples include `tracker`, `source`,
101
+ `github.org`, `github.repo`, `atlassian.cloudId`, `atlassian.site`, `jira.project`,
102
+ `linear.workspace`, `linear.teamKey`, and `deploy.branches`.
103
+
104
+ Locality findings are advisory unless the merged config is unusable. Missing shared keys after the
105
+ merge are `FAIL`; shared keys that exist only locally are `WARN`.
106
+
72
107
  ## Output contract
73
108
 
74
109
  The final report must:
@@ -78,6 +113,17 @@ The final report must:
78
113
  - Emit exactly one overall verdict: `READY`, `READY_WITH_WARNINGS`, or `NOT_READY`.
79
114
  - Stay read-only by default.
80
115
 
116
+ Render the report in grouped sections using the shared `scripts/doctor-report.mjs` contract:
117
+
118
+ - Start with `Overall verdict: <VERDICT>` and one `Counts:` line covering `PASS`, `WARN`, `FAIL`,
119
+ and `SKIP`.
120
+ - Then print each group as `<group-id>. <group-title>`.
121
+ - Under each group, print one line per check as `- <STATUS> <check-id>: <summary>`.
122
+ - When available, print `Observed:` and `Remediation:` lines beneath the check so the report keeps
123
+ facts separate from advice.
124
+ - If a group has no applicable checks yet, render it as a grouped `SKIP` with the reason instead of
125
+ silently omitting the section.
126
+
81
127
  The verdict ladder is:
82
128
 
83
129
  - `READY` — no `FAIL` and no `WARN`.