diff --git a/src/agent/session.ts b/src/agent/session.ts index 8c32a95..e9806ff 100644 --- a/src/agent/session.ts +++ b/src/agent/session.ts @@ -192,6 +192,7 @@ export async function startSession(options: SessionOptions): Promise { "mcp__marvin-governance__get_dashboard_sprint_summary", "mcp__marvin-governance__run_doctor", "mcp__marvin-governance__check_project_health", + "mcp__marvin-governance__get_started", ...pluginTools.map((t) => `mcp__marvin-governance__${t.name}`), ...codeSkillTools.map((t) => `mcp__marvin-governance__${t.name}`), ], diff --git a/src/agent/tools/doctor.ts b/src/agent/tools/doctor.ts index a7a2b62..e8d14d3 100644 --- a/src/agent/tools/doctor.ts +++ b/src/agent/tools/doctor.ts @@ -5,6 +5,7 @@ import type { SourceManifestManager } from "../../sources/manifest.js"; import type { MarvinProjectConfig } 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"; export interface DoctorToolOptions { config?: MarvinProjectConfig; @@ -105,5 +106,59 @@ export function createDoctorTools( }, { annotations: { readOnlyHint: true } }, ), + + tool( + "get_started", + "Get a tailored onboarding guide for the project. Inspects current state (artifacts, sources, config) and returns an ordered checklist of recommended setup steps with completion status. Methodology-aware (SAP AEM vs generic agile).", + {}, + async () => { + if (!options?.config || !options?.marvinDir) { + return { + content: [ + { + type: "text" as const, + text: "Onboarding unavailable: project config or marvinDir not initialized.", + }, + ], + isError: true, + }; + } + + const { config, marvinDir, manifest } = options; + + // Scan manifest before building guide + if (manifest) { + try { + manifest.scan(); + } catch { + // Non-fatal + } + } + + try { + const guide = buildOnboardingGuide({ store, config, manifest, marvinDir }); + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(guide, null, 2), + }, + ], + }; + } catch (err) { + return { + content: [ + { + type: "text" as const, + text: `Onboarding error: ${err instanceof Error ? err.message : String(err)}`, + }, + ], + isError: true, + }; + } + }, + { annotations: { readOnlyHint: true } }, + ), ]; } diff --git a/src/doctor/health/onboarding.ts b/src/doctor/health/onboarding.ts new file mode 100644 index 0000000..ec0e89d --- /dev/null +++ b/src/doctor/health/onboarding.ts @@ -0,0 +1,147 @@ +import type { HealthContext } from "./types.js"; + +export interface OnboardingStep { + order: number; + title: string; + description: string; + tool?: string; + done: boolean; +} + +export interface OnboardingGuide { + projectName: string; + methodology: string; + phase?: string; + status: "empty" | "getting-started" | "in-progress"; + steps: OnboardingStep[]; + summary: string; +} + +/** Inspect project state and produce a tailored onboarding guide. */ +export function buildOnboardingGuide(ctx: HealthContext): OnboardingGuide { + const counts = ctx.store.counts(); + const totalArtifacts = Object.values(counts).reduce((sum, n) => sum + n, 0); + const methodology = ctx.config.methodology ?? "generic-agile"; + const isAem = methodology === "sap-aem"; + const phase = ctx.config.aem?.currentPhase; + + const hasSources = (ctx.manifest?.list()?.length ?? 0) > 0; + const pendingSources = ctx.manifest?.list("pending")?.length ?? 0; + const hasActions = (counts["action"] ?? 0) > 0; + const hasEpics = (counts["epic"] ?? 0) > 0; + const hasSprints = (counts["sprint"] ?? 0) > 0; + const hasFeatures = (counts["feature"] ?? 0) > 0; + const hasUseCases = (counts["use-case"] ?? 0) > 0; + const hasJira = !!ctx.config.jira?.projectKey?.trim(); + + const steps: OnboardingStep[] = []; + let order = 1; + + // Step 1: Ingest source documents + steps.push({ + order: order++, + title: "Ingest source documents", + description: + pendingSources > 0 + ? `${pendingSources} source file(s) in .marvin/sources/ are ready for processing. Set the PO persona and ingest them to extract requirements, use cases, and initial artifacts.` + : hasSources + ? "All source files have been processed." + : "No source files found. Add documents (PDF, Markdown, or text) to .marvin/sources/ to begin processing.", + tool: "set_persona", + done: pendingSources === 0 && hasSources, + }); + + // Step 2: Define scope (methodology-specific) + if (isAem) { + steps.push({ + order: order++, + title: "Define extension use cases", + description: + "As PO, create use cases (UC-xxx) that describe business scenarios requiring SAP BTP extension. Classify each by extension type (in-app, side-by-side, hybrid) and priority.", + tool: "create_use_case", + done: hasUseCases, + }); + } else { + steps.push({ + order: order++, + title: "Define features", + description: + "As PO, create features (F-xxx) that describe the product capabilities to build. Set priorities and acceptance criteria.", + tool: "create_feature", + done: hasFeatures, + }); + } + + // Step 3: Capture decisions and actions + steps.push({ + order: order++, + title: "Capture key decisions and actions", + description: + "Record important decisions (D-xxx) with rationale and create action items (A-xxx) for work that needs to happen. Set owners and due dates on actions.", + tool: "create_decision", + done: (counts["decision"] ?? 0) > 0 && hasActions, + }); + + // Step 4: Break down into epics + steps.push({ + order: order++, + title: "Break work into epics", + description: isAem + ? "As TL, create tech assessments for approved use cases, then break extension designs into implementation epics (E-xxx)." + : "As TL, break approved features into implementation epics (E-xxx) with effort estimates.", + tool: "create_epic", + done: hasEpics, + }); + + // Step 5: Set up Sprint 0 + steps.push({ + order: order++, + title: "Set up Sprint 0", + description: + "As DM, create a Sprint 0 to organize bootstrapping work: infrastructure provisioning, CI/CD setup, backlog refinement, and ceremony scheduling. Sprint 0 is not a regular sprint — it's a variable-duration bootstrapping phase that ensures the team is ready for Sprint 1.", + tool: "create_sprint", + done: hasSprints, + }); + + // Step 6: Configure Jira integration + steps.push({ + order: order++, + title: "Configure Jira integration", + description: + "Add jira.projectKey to .marvin/config.yaml to enable bidirectional sync. This allows pushing artifacts to Jira and pulling sprint progress back.", + done: hasJira, + }); + + // Step 7: Run health check + steps.push({ + order: order, + title: "Run a health check", + description: + "Use check_project_health to verify governance setup is complete. It will flag any missing configuration or unfinished setup steps.", + tool: "check_project_health", + done: false, // Always suggest running this + }); + + const doneCount = steps.filter((s) => s.done).length; + const status: OnboardingGuide["status"] = + totalArtifacts === 0 + ? "empty" + : doneCount < steps.length - 1 + ? "getting-started" + : "in-progress"; + + const pending = steps.filter((s) => !s.done); + const summary = + pending.length === 0 + ? "Project setup is complete. All onboarding steps are done." + : `${doneCount} of ${steps.length} steps complete. Next: ${pending[0].title}.`; + + return { + projectName: ctx.config.name, + methodology, + phase, + status, + steps, + summary, + }; +} diff --git a/src/personas/builtin/delivery-manager.ts b/src/personas/builtin/delivery-manager.ts index 28d0656..9f356b1 100644 --- a/src/personas/builtin/delivery-manager.ts +++ b/src/personas/builtin/delivery-manager.ts @@ -24,6 +24,14 @@ export const deliveryManager: PersonaDefinition = { - Monitor project health and flag risks early - Create meeting notes and ensure action items are assigned +## Sprint 0 +When a project has work items but no sprints, proactively suggest creating a **Sprint 0** — a variable-duration bootstrapping phase (not a regular time-boxed sprint). Sprint 0 should cover: +- **Infrastructure & provisioning**: CI/CD, repositories, cloud services, dev environments +- **Backlog refinement**: Transition from features to epics with acceptance criteria and estimates +- **Ceremony scheduling**: Define cadence for standups, refinement sessions, and reviews +- **Integration setup**: Configure Jira, Confluence, or other tool connections +Sprint 0 ends when the team is ready to start Sprint 1 with a refined backlog and working infrastructure. + ## Communication Style - Process-oriented but pragmatic - Focus on status, risks, and blockers diff --git a/src/plugins/builtin/sap-aem.ts b/src/plugins/builtin/sap-aem.ts index 52b7ebd..1fdac0a 100644 --- a/src/plugins/builtin/sap-aem.ts +++ b/src/plugins/builtin/sap-aem.ts @@ -122,7 +122,14 @@ export const sapAemPlugin: MarvinPlugin = { - Advance phases only when gate conditions are met (soft enforcement — warnings, not blocks). - Generate extension portfolio reports for stakeholder updates. - Generate tech readiness reports to identify BTP service gaps. -- Track risks via actions and questions. Flag unresolved items before phase gates.`, +- Track risks via actions and questions. Flag unresolved items before phase gates. + +**Sprint 0 for AEM Projects:** +When setting up Sprint 0, also include AEM-specific bootstrapping: +- **Phase gate preparation**: Define soft gate checklists with readiness criteria for each AEM phase transition. +- **Tech assessment prerequisites**: Ensure BTP service access, extension point documentation, and sandbox environments are provisioned. +- **Iterative loop definitions**: Map the iterative loops between Assess Use Case ↔ Assess Technology ↔ Define Solution phases. +- **Extension design templates**: Prepare architecture pattern templates for in-app, side-by-side, and hybrid extensions.`, "*": `This project uses the **SAP Application Extension Methodology (AEM)** — a 3-phase approach for building extensions on SAP BTP: diff --git a/test/doctor/health/onboarding.test.ts b/test/doctor/health/onboarding.test.ts new file mode 100644 index 0000000..0b57db6 --- /dev/null +++ b/test/doctor/health/onboarding.test.ts @@ -0,0 +1,186 @@ +import { describe, it, expect, afterEach } from "vitest"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as os from "node:os"; +import * as YAML from "yaml"; +import { DocumentStore } from "../../../src/storage/store.js"; +import { SourceManifestManager } from "../../../src/sources/manifest.js"; +import { buildOnboardingGuide } from "../../../src/doctor/health/onboarding.js"; +import type { HealthContext } from "../../../src/doctor/health/types.js"; +import type { MarvinProjectConfig } from "../../../src/core/config.js"; + +function setup(options?: { methodology?: string; jiraProjectKey?: string; aemPhase?: string }) { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "marvin-onboarding-test-")); + const marvinDir = path.join(tmpDir, ".marvin"); + const docsDir = path.join(marvinDir, "docs"); + const sourcesDir = path.join(marvinDir, "sources"); + fs.mkdirSync(docsDir, { recursive: true }); + fs.mkdirSync(sourcesDir, { recursive: true }); + + const config: MarvinProjectConfig = { + name: "test-project", + methodology: options?.methodology, + jira: options?.jiraProjectKey ? { projectKey: options.jiraProjectKey } : undefined, + aem: options?.aemPhase ? { currentPhase: options.aemPhase } : undefined, + }; + + fs.writeFileSync(path.join(marvinDir, "config.yaml"), YAML.stringify(config), "utf-8"); + + const registrations = [ + { type: "feature", dirName: "features", idPrefix: "F" }, + { type: "epic", dirName: "epics", idPrefix: "E" }, + { type: "sprint", dirName: "sprints", idPrefix: "S" }, + { type: "use-case", dirName: "use-cases", idPrefix: "UC" }, + ]; + + const store = new DocumentStore(marvinDir, registrations); + const manifest = new SourceManifestManager(marvinDir); + + const ctx: HealthContext = { store, config, manifest, marvinDir }; + return { tmpDir, marvinDir, store, manifest, ctx }; +} + +describe("Onboarding Guide", () => { + let tmpDir: string; + + afterEach(() => { + if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("should return empty status for a blank project with full 7-step checklist", () => { + const env = setup(); + tmpDir = env.tmpDir; + + const guide = buildOnboardingGuide(env.ctx); + + expect(guide.status).toBe("empty"); + expect(guide.projectName).toBe("test-project"); + expect(guide.steps).toHaveLength(7); + + const titles = guide.steps.map((s) => s.title); + expect(titles).toEqual([ + "Ingest source documents", + "Define features", + "Capture key decisions and actions", + "Break work into epics", + "Set up Sprint 0", + "Configure Jira integration", + "Run a health check", + ]); + + // All steps should be not-done on a blank project + for (const step of guide.steps) { + expect(step.done).toBe(false); + } + + // Orders should be sequential + expect(guide.steps.map((s) => s.order)).toEqual([1, 2, 3, 4, 5, 6, 7]); + + // Source step should prompt user to add files + expect(guide.steps[0].description).toContain("No source files found"); + }); + + it("should mark source ingestion as pending when sources exist", () => { + const env = setup(); + tmpDir = env.tmpDir; + + fs.writeFileSync(path.join(env.marvinDir, "sources", "spec.pdf"), "fake pdf"); + env.manifest.scan(); + + const guide = buildOnboardingGuide(env.ctx); + + const ingestStep = guide.steps.find((s) => s.title === "Ingest source documents"); + expect(ingestStep).toBeDefined(); + expect(ingestStep!.done).toBe(false); + expect(ingestStep!.description).toContain("1 source file(s)"); + }); + + it("should use use-case language for SAP AEM projects", () => { + const env = setup({ methodology: "sap-aem" }); + tmpDir = env.tmpDir; + + const guide = buildOnboardingGuide(env.ctx); + + expect(guide.methodology).toBe("sap-aem"); + const scopeStep = guide.steps.find((s) => s.title === "Define extension use cases"); + expect(scopeStep).toBeDefined(); + expect(scopeStep!.tool).toBe("create_use_case"); + }); + + it("should use feature language for generic agile projects", () => { + const env = setup({ methodology: "generic-agile" }); + tmpDir = env.tmpDir; + + const guide = buildOnboardingGuide(env.ctx); + + const scopeStep = guide.steps.find((s) => s.title === "Define features"); + expect(scopeStep).toBeDefined(); + expect(scopeStep!.tool).toBe("create_feature"); + }); + + it("should mark Sprint 0 step as done when sprints exist", () => { + const env = setup(); + tmpDir = env.tmpDir; + + env.store.create("sprint", { title: "Sprint 0", status: "active" }); + + const guide = buildOnboardingGuide(env.ctx); + + const sprintStep = guide.steps.find((s) => s.title === "Set up Sprint 0"); + expect(sprintStep).toBeDefined(); + expect(sprintStep!.done).toBe(true); + }); + + it("should mark Jira step as done when configured", () => { + const env = setup({ jiraProjectKey: "TEST" }); + tmpDir = env.tmpDir; + + const guide = buildOnboardingGuide(env.ctx); + + const jiraStep = guide.steps.find((s) => s.title === "Configure Jira integration"); + expect(jiraStep).toBeDefined(); + expect(jiraStep!.done).toBe(true); + }); + + it("should report in-progress when most steps are done", () => { + const env = setup({ jiraProjectKey: "TEST" }); + tmpDir = env.tmpDir; + + // Add and process a source file so the ingest step is done + fs.writeFileSync(path.join(env.marvinDir, "sources", "spec.md"), "# Spec"); + env.manifest.scan(); + env.manifest.markCompleted("spec.md", ["D-001"]); + + env.store.create("decision", { title: "Use React" }); + env.store.create("action", { title: "Set up CI/CD" }); + env.store.create("feature", { title: "Feature 1", status: "approved" }); + env.store.create("epic", { title: "Epic 1" }); + env.store.create("sprint", { title: "Sprint 0", status: "active" }); + + const guide = buildOnboardingGuide(env.ctx); + + expect(guide.status).toBe("in-progress"); + }); + + it("should include summary with next step", () => { + const env = setup(); + tmpDir = env.tmpDir; + + const guide = buildOnboardingGuide(env.ctx); + + expect(guide.summary).toContain("Next:"); + expect(guide.summary).toContain("of"); + }); + + it("should include Sprint 0 description mentioning bootstrapping", () => { + const env = setup(); + tmpDir = env.tmpDir; + + const guide = buildOnboardingGuide(env.ctx); + + const sprintStep = guide.steps.find((s) => s.title === "Set up Sprint 0"); + expect(sprintStep).toBeDefined(); + expect(sprintStep!.description).toContain("bootstrapping"); + expect(sprintStep!.description).toContain("Sprint 1"); + }); +});