Skip to content

feat: add check_project_health tool for governance health checks - #74

Merged
ablanrob merged 3 commits into
mainfrom
feat/health-check
Apr 22, 2026
Merged

feat: add check_project_health tool for governance health checks#74
ablanrob merged 3 commits into
mainfrom
feat/health-check

Conversation

@ablanrob

Copy link
Copy Markdown
Owner

Summary

Implements feedback item #3 (Governance Health Check / Active Guardrails) from the eam-rpt-forecast PoC report.

Adds a new check_project_health MCP tool that inspects project state and returns soft recommendations about missing governance setup. This complements the existing run_doctor tool which focuses on document-level structural issues.

Six health checks:

  • empty-project — detects new projects with no artifacts, suggests onboarding steps (sources present? ingest them)
  • unprocessed-sources — flags pending or errored source files in .marvin/sources/
  • no-sprints — work items exist (actions/epics) but no sprints defined, suggests Sprint 0
  • unassigned-actions — open actions not assigned to any sprint (only fires when sprints exist)
  • no-jira-project — no Jira project key configured when artifacts exist
  • phase-readiness — AEM phase gate prerequisites incomplete (Phase 1: no use cases, Phase 2: no tech assessments, Phase 3: no extension designs)

Also fixes: run_doctor was missing from the agent session's allowedTools list — now included alongside check_project_health.

Architecture: follows the existing doctor pattern — rule-based checks under src/doctor/health/checks/, an engine that runs them all, and a tool that exposes the results via MCP. Uses HealthFinding with recommendation/observation severities instead of DoctorIssue with error/warning.

Test plan

  • 828 tests pass (812 existing + 16 new)
  • test/doctor/health/engine.test.ts — 8 tests covering all 6 checks
  • test/doctor/health/phase-readiness.test.ts — 7 tests for AEM phase logic
  • Manual: run check_project_health on an empty project with source files
  • Manual: run check_project_health on a project with actions but no sprints
  • Manual: run check_project_health on a project with SAP AEM methodology

Introduces project-level health checks that surface soft recommendations
about missing setup — sprints, Jira config, source processing, and AEM
phase readiness. Complements the existing run_doctor tool which focuses
on document-level structural issues.

Six checks implemented:
- empty-project: detects new projects, suggests onboarding steps
- unprocessed-sources: flags pending/errored source files
- no-sprints: work exists but no sprints defined
- unassigned-actions: open actions without sprint assignment
- no-jira-project: Jira project key not configured
- phase-readiness: AEM phase gate prerequisites incomplete

Also adds run_doctor to the agent session allowed tools list (was
previously missing).
@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f56be62e-8a82-4278-96dc-1a64716e5ab9

📥 Commits

Reviewing files that changed from the base of the PR and between e9f0f32 and 9cd9b3b.

📒 Files selected for processing (8)
  • src/agent/tools/doctor.ts
  • src/core/config.ts
  • src/doctor/health/checks/empty-project.ts
  • src/doctor/health/checks/no-jira-project.ts
  • src/doctor/health/checks/phase-readiness.ts
  • src/doctor/health/checks/unprocessed-sources.ts
  • src/doctor/health/engine.ts
  • test/doctor/health/phase-readiness.test.ts
✅ Files skipped from review due to trivial changes (2)
  • src/core/config.ts
  • test/doctor/health/phase-readiness.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/doctor/health/checks/unprocessed-sources.ts
  • src/doctor/health/checks/phase-readiness.ts
  • src/doctor/health/checks/no-jira-project.ts
  • src/doctor/health/engine.ts
  • src/agent/tools/doctor.ts
  • src/doctor/health/checks/empty-project.ts

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Introduced a comprehensive project health checking system that diagnoses configuration and setup issues including empty projects, unprocessed sources, missing sprints, unassigned actions, and unconfigured Jira integration
    • Added support for SAP AEM methodology phase tracking in project configuration

Walkthrough

Adds a health-check subsystem and MCP tool that inspects Marvin projects (artifacts, sources, sprints, actions, Jira, and AEM phase readiness), aggregates findings into a HealthReport, and exposes it via a new check_project_health doctor tool integrated into the MCP/server tooling.

Changes

Cohort / File(s) Summary
Types & Engine
src/doctor/health/types.ts, src/doctor/health/engine.ts
Introduce health-check types and a runHealthCheck(ctx) engine that executes all checks, aggregates findings by severity and checkId, and returns a HealthReport.
Health Check Implementations
src/doctor/health/checks/index.ts, src/doctor/health/checks/empty-project.ts, src/doctor/health/checks/unprocessed-sources.ts, src/doctor/health/checks/no-sprints.ts, src/doctor/health/checks/unassigned-actions.ts, src/doctor/health/checks/no-jira-project.ts, src/doctor/health/checks/phase-readiness.ts
Add six health checks (empty-project, unprocessed-sources, no-sprints, unassigned-actions, no-jira-project, phase-readiness) and an index exporting them in priority order.
Doctor Tool & Agent Integration
src/agent/tools/doctor.ts, src/agent/mcp-server.ts, src/agent/session.ts, src/mcp/stdio-server.ts
Make createDoctorTools accept DoctorToolOptions (config, manifest, marvinDir); add check_project_health MCP tool that calls runHealthCheck; propagate project config and marvinDir through MCP server/session and stdio-server tool collection; add governance tools to allowed list.
Config Extension
src/core/config.ts
Extend MarvinProjectConfig with new optional aem?: AemConfig and AemConfig.currentPhase?: string.
Tests
test/doctor/health/engine.test.ts, test/doctor/health/phase-readiness.test.ts
Add Vitest suites covering engine aggregation, empty/pending/error sources, sprints/unassigned actions, Jira config, and AEM phase-readiness scenarios.

Sequence Diagram

sequenceDiagram
    actor User
    participant MCP as "MCP Tool"
    participant Engine as "Health Engine"
    participant Checks as "Health Checks"
    participant Store as "Document Store"
    participant Manifest as "Source Manifest"

    User->>MCP: call check_project_health
    MCP->>Engine: runHealthCheck(ctx with store, config, manifest, marvinDir)
    Engine->>Manifest: optional scan()
    loop for each check in allHealthChecks
        Engine->>Checks: check.run(ctx)
        Checks->>Store: query counts / list entities
        Checks->>Manifest: list pending/error (if present)
        Store-->>Checks: entity data
        Manifest-->>Checks: manifest entries
        Checks-->>Engine: HealthFinding[]
    end
    Engine->>Engine: aggregate findings by severity & checkId
    Engine-->>MCP: HealthReport (JSON)
    MCP-->>User: report
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Soft paws on config and file,

I hop the repo, checking a while.
Sprints and sources, Jira too,
I sniff the gaps and give a view.
Hooray — health reports, fresh and new!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main feature: adding a new check_project_health tool for governance health checks. It is concise, specific, and reflects the primary change in the changeset.
Description check ✅ Passed The description is comprehensive and mostly complete. It includes a clear summary of what the PR does, lists all six health checks implemented, mentions the fix for run_doctor, explains the architecture, and provides detailed test coverage information. The testing section is partially filled but the summary and changes sections fully explain the work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/health-check

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ablanrob

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/agent/tools/doctor.ts (2)

65-78: Minor: narrow options before non-null assertions.

options.config! / options.marvinDir! work given the outer guard, but destructuring once into local const { config, marvinDir, manifest } = options; after a narrowing check avoids the ! assertions and keeps the closure typing clean.

Proposed tweak
-  if (options?.config && options?.marvinDir) {
+  if (options?.config && options?.marvinDir) {
+    const { config, marvinDir, manifest } = options;
     tools.push(
       tool(
         "check_project_health",
         "...",
         {},
         async () => {
           try {
-            const report = runHealthCheck({
-              store,
-              config: options.config!,
-              manifest: options.manifest,
-              marvinDir: options.marvinDir!,
-            });
+            const report = runHealthCheck({ store, config, manifest, marvinDir });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/tools/doctor.ts` around lines 65 - 78, After checking options is
truthy in the if (options?.config && options?.marvinDir) guard, destructure
const { config, marvinDir, manifest } = options and use those local variables
when calling runHealthCheck inside the check_project_health tool closure (and
when passing marvinDir/config to runHealthCheck) instead of using
options.config! and options.marvinDir!; this removes the non-null assertions and
keeps closure typing clean while leaving the surrounding guard and tool(...)
call intact.

65-103: Conditional tool registration creates brittle mismatch with hardcoded allowedTools.

The check_project_health tool is registered conditionally when both config and marvinDir are provided, but allowedTools in src/agent/session.ts (line 194) unconditionally includes it. While current code paths always provide both parameters, this creates a fragile pattern: if createDoctorTools is called without these options, the tool won't exist but the allowedTools list still references it, leaving callers with no signal about unavailability.

Either always register the tool and return a structured error response ("project not initialized") when config/marvinDir are absent, or conditionally build allowedTools to match actual tool registration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/tools/doctor.ts` around lines 65 - 103, The conditional
registration of the "check_project_health" tool creates a mismatch with
allowedTools; change createDoctorTools so the tool("check_project_health", ...)
is always pushed regardless of options, and inside its async handler check
options.config and options.marvinDir; if either is missing return a structured
error response (e.g., content with type "text" and message like "Project not
initialized: missing config or marvinDir", plus isError: true) instead of
attempting runHealthCheck, otherwise call runHealthCheck as before; this ensures
allowedTools (referenced in src/agent/session.ts) always points to a registered
tool and callers get a clear, consistent error when the project is not
initialized.
src/doctor/health/checks/unprocessed-sources.ts (1)

25-25: Consider truncating file name lists in messages.

pending.map((f) => f.name).join(", ") can produce very long messages if a project has many pending/errored sources, making the health report hard to read in tool output. Consider capping at ~10 names with an "…and N more" suffix.

Also applies to: 37-37

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/doctor/health/checks/unprocessed-sources.ts` at line 25, The generated
names string (const names = pending.map((f) => f.name).join(", ")) can become
very long; change the logic that builds the names string (and the similar
mapping at the other occurrence) to limit output to a fixed max (e.g., 10) by
taking the first N pending.map(f => f.name), joining those with ", ", and if
pending.length > N append a suffix like "…and X more" where X = pending.length -
N; update the variable(s) (e.g., names) and any usage sites to use this
truncated string.
src/doctor/health/checks/empty-project.ts (1)

21-27: Duplicate manifest.scan() across checks.

Both emptyProjectCheck and unprocessedSourcesCheck call ctx.manifest.scan(), causing redundant filesystem I/O per health run. Consider scanning once in runHealthCheck (engine) before invoking checks, or memoizing on the HealthContext, so individual checks only consume manifest.list(...).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/doctor/health/checks/empty-project.ts` around lines 21 - 27, Both
emptyProjectCheck and unprocessedSourcesCheck redundantly call
ctx.manifest.scan(); remove those scan calls from the checks and ensure the
manifest is scanned only once prior to running checks by either invoking
ctx.manifest.scan() inside runHealthCheck (engine) or adding a memoized scan
method/property on HealthContext (e.g., HealthContext.ensureManifestScanned or a
cached ctx.manifest.scanned true flag). Update emptyProjectCheck and
unprocessedSourcesCheck to only call ctx.manifest.list(...) and rely on the
single pre-scan, and add defensive no-op protection if scan has already run.
src/doctor/health/checks/phase-readiness.ts (1)

1-3: Add aem?: { currentPhase?: string } to MarvinProjectConfig and use ctx.config.aem?.currentPhase instead of re-reading YAML.

readPhase duplicates I/O and YAML parsing on every health run despite ctx.config already containing the parsed configuration. This creates two sources of truth—methodology comes from ctx.config while the phase is read from disk, allowing them to diverge if the config is mutated. Additionally, all errors (including malformed YAML) are silently swallowed with no observation emitted. Reading the phase from ctx.config directly also eliminates the unused node:fs and yaml imports from this check.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/doctor/health/checks/phase-readiness.ts` around lines 1 - 3, Add an
optional aem field to the MarvinProjectConfig type (aem?: { currentPhase?:
string }) and update the readPhase logic to use ctx.config.aem?.currentPhase
instead of re-reading and parsing the YAML on disk (replace any calls to
readPhase with direct access to ctx.config.aem?.currentPhase); remove the unused
node:fs and yaml/YAML imports and any try/catch that previously swallowed
file/YAML errors, and ensure you handle a missing/undefined phase value
explicitly (emit an observation or fallback behavior where the code previously
relied on the disk-read result).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/doctor/health/checks/no-jira-project.ts`:
- Line 17: The check treating ctx.config.jira?.projectKey as configured should
trim whitespace first because a whitespace-only string is truthy but invalid;
replace the direct truthiness test of ctx.config.jira?.projectKey with a
trimmed-value check (e.g., compute trimmed = ctx.config.jira?.projectKey?.trim()
and use that) so only non-empty, non-whitespace project keys bypass the finding
and maintain the rest of the logic in the same function where the current if
(ctx.config.jira?.projectKey) return [];.

In `@src/doctor/health/checks/phase-readiness.ts`:
- Around line 50-54: The variable approvedUCs currently includes both "assessed"
and "approved" statuses but its name and surrounding message claim "approved";
change the filter to only include uc.frontmatter.status === "approved" (keep the
approvedUCs name) so the selection matches the AEM lifecycle in
src/plugins/builtin/sap-aem.ts, and verify any message text that references
approved use case(s) remains accurate after this change; locate the array
creation via ctx.store.list({ type: "use-case" }) and the approvedUCs variable
in phase-readiness.ts to make the edit.

---

Nitpick comments:
In `@src/agent/tools/doctor.ts`:
- Around line 65-78: After checking options is truthy in the if (options?.config
&& options?.marvinDir) guard, destructure const { config, marvinDir, manifest }
= options and use those local variables when calling runHealthCheck inside the
check_project_health tool closure (and when passing marvinDir/config to
runHealthCheck) instead of using options.config! and options.marvinDir!; this
removes the non-null assertions and keeps closure typing clean while leaving the
surrounding guard and tool(...) call intact.
- Around line 65-103: The conditional registration of the "check_project_health"
tool creates a mismatch with allowedTools; change createDoctorTools so the
tool("check_project_health", ...) is always pushed regardless of options, and
inside its async handler check options.config and options.marvinDir; if either
is missing return a structured error response (e.g., content with type "text"
and message like "Project not initialized: missing config or marvinDir", plus
isError: true) instead of attempting runHealthCheck, otherwise call
runHealthCheck as before; this ensures allowedTools (referenced in
src/agent/session.ts) always points to a registered tool and callers get a
clear, consistent error when the project is not initialized.

In `@src/doctor/health/checks/empty-project.ts`:
- Around line 21-27: Both emptyProjectCheck and unprocessedSourcesCheck
redundantly call ctx.manifest.scan(); remove those scan calls from the checks
and ensure the manifest is scanned only once prior to running checks by either
invoking ctx.manifest.scan() inside runHealthCheck (engine) or adding a memoized
scan method/property on HealthContext (e.g., HealthContext.ensureManifestScanned
or a cached ctx.manifest.scanned true flag). Update emptyProjectCheck and
unprocessedSourcesCheck to only call ctx.manifest.list(...) and rely on the
single pre-scan, and add defensive no-op protection if scan has already run.

In `@src/doctor/health/checks/phase-readiness.ts`:
- Around line 1-3: Add an optional aem field to the MarvinProjectConfig type
(aem?: { currentPhase?: string }) and update the readPhase logic to use
ctx.config.aem?.currentPhase instead of re-reading and parsing the YAML on disk
(replace any calls to readPhase with direct access to
ctx.config.aem?.currentPhase); remove the unused node:fs and yaml/YAML imports
and any try/catch that previously swallowed file/YAML errors, and ensure you
handle a missing/undefined phase value explicitly (emit an observation or
fallback behavior where the code previously relied on the disk-read result).

In `@src/doctor/health/checks/unprocessed-sources.ts`:
- Line 25: The generated names string (const names = pending.map((f) =>
f.name).join(", ")) can become very long; change the logic that builds the names
string (and the similar mapping at the other occurrence) to limit output to a
fixed max (e.g., 10) by taking the first N pending.map(f => f.name), joining
those with ", ", and if pending.length > N append a suffix like "…and X more"
where X = pending.length - N; update the variable(s) (e.g., names) and any usage
sites to use this truncated string.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0ba93c7-8c43-43ea-bfea-1a6e308af842

📥 Commits

Reviewing files that changed from the base of the PR and between dec334d and e9f0f32.

📒 Files selected for processing (15)
  • src/agent/mcp-server.ts
  • src/agent/session.ts
  • src/agent/tools/doctor.ts
  • src/doctor/health/checks/empty-project.ts
  • src/doctor/health/checks/index.ts
  • src/doctor/health/checks/no-jira-project.ts
  • src/doctor/health/checks/no-sprints.ts
  • src/doctor/health/checks/phase-readiness.ts
  • src/doctor/health/checks/unassigned-actions.ts
  • src/doctor/health/checks/unprocessed-sources.ts
  • src/doctor/health/engine.ts
  • src/doctor/health/types.ts
  • src/mcp/stdio-server.ts
  • test/doctor/health/engine.test.ts
  • test/doctor/health/phase-readiness.test.ts

Comment thread src/doctor/health/checks/no-jira-project.ts Outdated
Comment thread src/doctor/health/checks/phase-readiness.ts Outdated
A whitespace-only projectKey string is truthy but invalid — trim
before the check so it correctly flags as unconfigured.
- Always register check_project_health tool; return structured error
  when config/marvinDir unavailable instead of conditionally omitting
- Destructure options to remove non-null assertions in closure
- Scan manifest once in engine before running checks, not per-check
- Read AEM phase from config.aem.currentPhase instead of re-parsing
  YAML from disk; add AemConfig interface to MarvinProjectConfig
- Filter only "approved" use cases in Phase 2 check (was including
  "assessed" which is a pre-approval status)
- Truncate file name lists to 10 entries in unprocessed-sources
@ablanrob
ablanrob merged commit a177143 into main Apr 22, 2026
2 checks passed
@ablanrob
ablanrob deleted the feat/health-check branch April 22, 2026 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant