Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/guides/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,25 @@ marvin serve

See the [MCP Server guide](mcp-server.md) for configuration details.

## Onboarding assistance

If you're unsure what to do next, Marvin can guide you. Use the `get_started` tool in an MCP session (Claude Desktop or Claude Code) to get a tailored checklist based on your project's current state:

```
> get_started
{
"status": "empty",
"steps": [
{ "title": "Ingest source documents", "done": false },
{ "title": "Define features", "done": false },
...
],
"summary": "0 of 7 steps complete. Next: Ingest source documents."
}
```

You can also run `check_project_health` at any time to get recommendations about missing governance setup (sprints, Jira integration, phase readiness).

## Next steps

- **[Personas](personas.md)** — learn what each persona does and when to use it
Expand Down
35 changes: 35 additions & 0 deletions docs/guides/jira.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,41 @@ jira:
Done: done
```

Alternatively, set credentials via environment variables: `JIRA_HOST`, `JIRA_EMAIL`, `JIRA_API_TOKEN`.

### Project-scoped credentials

If you work with multiple Jira instances, you can override the host and email per project in `.marvin/config.yaml`:

```yaml
jira:
host: project-specific.atlassian.net
email: project-team@example.com
projectKey: PROJ
```

The resolution order is: **project config → user config → environment variables**. The API token is never stored in project config — use the user config or environment variables.

### Checking configuration

Use the `check_integrations` MCP tool to verify your Jira setup without exposing secrets:

```json
// > check_integrations
{
"jira": {
"configured": true,
"host": "your-instance.atlassian.net",
"hostSource": "user",
"emailConfigured": true,
"emailSource": "user",
"apiTokenConfigured": true,
"apiTokenSource": "env",
"projectKey": "PROJ"
}
}
```

3. Assign the Jira skill to the relevant personas:

```bash
Expand Down
9 changes: 9 additions & 0 deletions docs/guides/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,13 @@ Add Marvin to your Claude Desktop MCP config:

The MCP server exposes all governance tools: creating and managing decisions, actions, questions, features, epics, sprints, tasks, and more. It also provides persona management tools (`set_persona`, `get_persona_guidance`) that let Claude switch between personas within a session.

### Diagnostic tools

| Tool | Description |
|------|-------------|
| `run_doctor` | Scan documents for structural issues (orphaned refs, status alignment) with optional auto-repair. |
| `check_project_health` | Project-level governance health checks — flags missing sprints, unprocessed sources, Jira config, and AEM phase readiness. |
| `get_started` | Tailored onboarding guide with a step-by-step checklist that adapts to methodology and tracks completion. |
| `check_integrations` | Check integration status (Jira, Confluence) without exposing credentials. Reports which credentials are configured and their source (project/user/env). |

See the [CLI Reference](../reference/cli.md) for the full list of operations available through MCP tools.
4 changes: 3 additions & 1 deletion docs/guides/personas.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The Product Owner focuses on product vision, stakeholder needs, backlog prioriti
- Make decisions about scope, priority, and trade-offs
- Accept or reject work results based on acceptance criteria

**Document types:** decisions, questions, actions, features
**Document types:** decisions, questions, actions, features, use-cases

**Contribution types:** stakeholder-feedback, acceptance-result, priority-change, market-insight

Expand Down Expand Up @@ -44,6 +44,8 @@ The Delivery Manager focuses on project delivery, risk management, team coordina

**Contribution types:** risk-finding, blocker-report, dependency-update, status-assessment

**Sprint 0 support:** The DM persona understands Sprint 0 as a variable-duration bootstrapping phase (not a regular time-boxed sprint). When a project has work items but no sprints, the DM will proactively suggest creating Sprint 0 to cover infrastructure provisioning, backlog refinement, ceremony scheduling, and integration setup.

**When to use:** When you need to manage how things get built — planning sprints, tracking progress, running meetings, managing risks, or generating status reports.

```bash
Expand Down
26 changes: 26 additions & 0 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ git:

# Jira project settings (optional)
jira:
host: project-specific.atlassian.net # overrides user config and env var
email: project-team@example.com # overrides user config and env var
projectKey: PROJ
statusMap:
To Do: open
Expand All @@ -87,6 +89,10 @@ jira:
Blocked:
default: blocked
inSprint: in-progress

# AEM phase tracking (sap-aem methodology only)
aem:
currentPhase: assess-use-case
```

### Fields
Expand All @@ -99,8 +105,11 @@ jira:
| `personas.<id>.extraInstructions` | string | no | Additional system prompt text for the persona. |
| `skills.<personaId>` | string[] | no | List of skill IDs assigned to a persona. |
| `git.remote` | string | no | Remote URL for governance data sync. |
| `jira.host` | string | no | Project-specific Jira host. Overrides user config and `JIRA_HOST` env var. |
| `jira.email` | string | no | Project-specific Jira email. Overrides user config and `JIRA_EMAIL` env var. |
| `jira.projectKey` | string | no | Jira project key for integration. |
| `jira.statusMap` | object | no | Mapping of Jira statuses to Marvin statuses. See below. |
| `aem.currentPhase` | string | no | Current AEM phase (`assess-use-case`, `assess-technology`, `define-solution`). Managed by the `advance_phase` tool. |

### Jira status mapping

Expand Down Expand Up @@ -133,8 +142,25 @@ Methodologies are plugins that define additional document types, tools, and pers

**sap-aem** — Extends generic-agile with SAP-specific artifacts: use cases (UC), tech assessments (TA), and extension designs (XD). Includes phase management and SAP BTP guidance.

## Jira credential resolution

Jira credentials are resolved in priority order: **project config → user config → environment variables**. This allows per-project overrides for teams working across multiple Jira instances.

| Credential | Project config | User config | Environment variable |
|------------|---------------|-------------|---------------------|
| Host | `jira.host` | `jira.host` | `JIRA_HOST` |
| Email | `jira.email` | `jira.email` | `JIRA_EMAIL` |
| API Token | — (not supported) | `jira.apiToken` | `JIRA_API_TOKEN` |

The API token is intentionally excluded from project config to avoid committing secrets. Use the user config or environment variables for the token.

Use the `check_integrations` MCP tool to verify which credentials are configured and their source — it reports status without exposing secret values.

## Environment variables

| Variable | Description |
|----------|-------------|
| `ANTHROPIC_API_KEY` | Anthropic API key (overrides user config). |
| `JIRA_HOST` | Jira Cloud instance hostname (fallback when not in user/project config). |
| `JIRA_EMAIL` | Jira account email (fallback when not in user/project config). |
| `JIRA_API_TOKEN` | Jira API token (fallback when not in user config). |
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mrvn-cli",
"version": "0.6.1",
"version": "0.6.2",
"description": "AI-powered software product development assistant with Product Owner, Delivery Manager, and Technical Lead personas",
"type": "module",
"bin": {
Expand Down
1 change: 1 addition & 0 deletions src/agent/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export async function startSession(options: SessionOptions): Promise<void> {
"mcp__marvin-governance__get_dashboard_sprint_summary",
"mcp__marvin-governance__run_doctor",
"mcp__marvin-governance__check_project_health",
"mcp__marvin-governance__check_integrations",
"mcp__marvin-governance__get_started",
...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`),
...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`),
Expand Down
57 changes: 57 additions & 0 deletions src/agent/tools/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { tool, type SdkMcpToolDefinition } from "@anthropic-ai/claude-agent-sdk"
import type { DocumentStore } from "../../storage/store.js";
import type { SourceManifestManager } from "../../sources/manifest.js";
import type { MarvinProjectConfig } from "../../core/config.js";
import { loadUserConfig } from "../../core/config.js";
import { runDoctorScan, runDoctorFix } from "../../doctor/engine.js";
import { runHealthCheck } from "../../doctor/health/engine.js";
import { buildOnboardingGuide } from "../../doctor/health/onboarding.js";
import { resolveJiraStatus } from "../../skills/builtin/jira/client.js";

export interface DoctorToolOptions {
config?: MarvinProjectConfig;
Expand Down Expand Up @@ -160,5 +162,60 @@ export function createDoctorTools(
},
{ annotations: { readOnlyHint: true } },
),

tool(
"check_integrations",
"Check which integrations (Jira, Confluence) are configured and their credential status. Returns presence/source info without exposing secrets. Use this instead of reading config files directly.",
{},
async () => {
try {
const jiraSources = {
project: options?.config?.jira
? { host: options.config.jira.host, email: options.config.jira.email }
: undefined,
user: loadUserConfig().jira,
};

const jira = resolveJiraStatus(jiraSources);

const result = {
jira: {
configured: jira.host.configured && jira.email.configured && jira.apiToken.configured,
host: jira.host.value ?? null,
hostSource: jira.host.source ?? null,
emailConfigured: jira.email.configured,
emailSource: jira.email.source ?? null,
apiTokenConfigured: jira.apiToken.configured,
apiTokenSource: jira.apiToken.source ?? null,
projectKey: options?.config?.jira?.projectKey?.trim() || null,
},
confluence: {
configured: jira.host.configured && jira.email.configured && jira.apiToken.configured,
note: "Confluence uses the same Jira/Atlassian credentials.",
},
};

return {
content: [
{
type: "text" as const,
text: JSON.stringify(result, null, 2),
},
],
};
} catch (err) {
return {
content: [
{
type: "text" as const,
text: `Integration check error: ${err instanceof Error ? err.message : String(err)}`,
},
],
isError: true,
};
}
},
{ annotations: { readOnlyHint: true } },
),
];
}
2 changes: 2 additions & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export interface LegacyJiraStatusMap {
}

export interface JiraProjectConfig {
host?: string;
email?: string;
projectKey?: string;
statusMap?:
| FlatJiraStatusMap
Expand Down
92 changes: 82 additions & 10 deletions src/skills/builtin/jira/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,19 +266,91 @@ export interface ResolvedJiraConfig {
host: string;
}

export function createJiraClient(jiraUserConfig?: {
host?: string;
email?: string;
apiToken?: string;
}): ResolvedJiraConfig | null {
const host = (jiraUserConfig?.host?.trim() ?? process.env.JIRA_HOST?.trim()) || undefined;
const email = (jiraUserConfig?.email?.trim() ?? process.env.JIRA_EMAIL?.trim()) || undefined;
export interface JiraConfigSources {
/** Project-level config (.marvin/config.yaml jira section) */
project?: { host?: string; email?: string };
/** User-level config (~/.config/marvin/config.yaml jira section) */
user?: { host?: string; email?: string; apiToken?: string };
}

/**
* Resolve Jira credentials from project config → user config → env vars.
* Returns null if any required credential is missing.
*/
export function createJiraClient(
userConfigOrSources?: JiraConfigSources["user"] | JiraConfigSources,
): ResolvedJiraConfig | null {
// Support both legacy (user-only) and new (project+user) signatures
const sources = isConfigSources(userConfigOrSources)
? userConfigOrSources
: { user: userConfigOrSources };

const host =
sources.project?.host?.trim() ||
sources.user?.host?.trim() ||
process.env.JIRA_HOST?.trim() ||
undefined;
const email =
sources.project?.email?.trim() ||
sources.user?.email?.trim() ||
process.env.JIRA_EMAIL?.trim() ||
undefined;
const apiToken =
(jiraUserConfig?.apiToken?.trim() ?? process.env.JIRA_API_TOKEN?.trim()) || undefined;
sources.user?.apiToken?.trim() || process.env.JIRA_API_TOKEN?.trim() || undefined;

if (!host || !email || !apiToken) return null;

// Normalize host for consistent jiraUrl generation
const normalizedHost = host.replace(/^https?:\/\//, "").replace(/\/+$/, "");
return { client: new JiraClient({ host, email, apiToken }), host: normalizedHost };
return {
client: new JiraClient({ host: normalizedHost, email, apiToken }),
host: normalizedHost,
};
}

export type CredentialSource = "project" | "user" | "env";

export interface JiraIntegrationStatus {
host: { configured: boolean; value?: string; source?: CredentialSource };
email: { configured: boolean; source?: CredentialSource };
apiToken: { configured: boolean; source?: CredentialSource };
}

/** Check which Jira credentials are present without exposing values. */
export function resolveJiraStatus(sources?: JiraConfigSources): JiraIntegrationStatus {
const projectHost = sources?.project?.host?.trim();
const userHost = sources?.user?.host?.trim();
const envHost = process.env.JIRA_HOST?.trim();
const host = projectHost || userHost || envHost;

const projectEmail = sources?.project?.email?.trim();
const userEmail = sources?.user?.email?.trim();
const envEmail = process.env.JIRA_EMAIL?.trim();

const userToken = sources?.user?.apiToken?.trim();
const envToken = process.env.JIRA_API_TOKEN?.trim();

return {
host: {
configured: !!host,
value: host ? host.replace(/^https?:\/\//, "").replace(/\/+$/, "") : undefined,
source: projectHost ? "project" : userHost ? "user" : envHost ? "env" : undefined,
},
email: {
configured: !!(projectEmail || userEmail || envEmail),
source: projectEmail ? "project" : userEmail ? "user" : envEmail ? "env" : undefined,
},
apiToken: {
configured: !!(userToken || envToken),
source: userToken ? "user" : envToken ? "env" : undefined,
},
};
}

function isConfigSources(
value: JiraConfigSources["user"] | JiraConfigSources | undefined,
): value is JiraConfigSources {
if (!value) return false;
// Reject legacy objects that have top-level credentials (host/email/apiToken)
if ("host" in value || "email" in value || "apiToken" in value) return false;
return "project" in value || "user" in value;
}
Loading
Loading