mrvn-cli 0.2.5 → 0.2.8

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/dist/index.js CHANGED
@@ -15302,6 +15302,128 @@ function createMeetingTools(store) {
15302
15302
 
15303
15303
  // src/plugins/builtin/tools/reports.ts
15304
15304
  import { tool as tool8 } from "@anthropic-ai/claude-agent-sdk";
15305
+
15306
+ // src/reports/gar/collector.ts
15307
+ function collectGarMetrics(store) {
15308
+ const allActions = store.list({ type: "action" });
15309
+ const openActions = allActions.filter((d) => d.frontmatter.status === "open");
15310
+ const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
15311
+ const allDocs = store.list();
15312
+ const blockedItems = allDocs.filter(
15313
+ (d) => d.frontmatter.tags?.includes("blocked")
15314
+ );
15315
+ const overdueItems = allDocs.filter(
15316
+ (d) => d.frontmatter.tags?.includes("overdue")
15317
+ );
15318
+ const openQuestions = store.list({ type: "question", status: "open" });
15319
+ const riskItems = allDocs.filter(
15320
+ (d) => d.frontmatter.tags?.includes("risk")
15321
+ );
15322
+ const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
15323
+ const total = allActions.length;
15324
+ const done = doneActions.length;
15325
+ const completionPct = total > 0 ? Math.round(done / total * 100) : 100;
15326
+ const scheduleItems = [
15327
+ ...blockedItems,
15328
+ ...overdueItems
15329
+ ].filter(
15330
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
15331
+ ).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
15332
+ const qualityItems = [
15333
+ ...riskItems,
15334
+ ...openQuestions
15335
+ ].filter(
15336
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
15337
+ ).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
15338
+ const resourceItems = unownedActions.map((d) => ({
15339
+ id: d.frontmatter.id,
15340
+ title: d.frontmatter.title
15341
+ }));
15342
+ return {
15343
+ scope: {
15344
+ total,
15345
+ open: openActions.length,
15346
+ done,
15347
+ completionPct
15348
+ },
15349
+ schedule: {
15350
+ blocked: blockedItems.length,
15351
+ overdue: overdueItems.length,
15352
+ items: scheduleItems
15353
+ },
15354
+ quality: {
15355
+ risks: riskItems.length,
15356
+ openQuestions: openQuestions.length,
15357
+ items: qualityItems
15358
+ },
15359
+ resources: {
15360
+ unowned: unownedActions.length,
15361
+ items: resourceItems
15362
+ }
15363
+ };
15364
+ }
15365
+
15366
+ // src/reports/gar/evaluator.ts
15367
+ function worstStatus(statuses) {
15368
+ if (statuses.includes("red")) return "red";
15369
+ if (statuses.includes("amber")) return "amber";
15370
+ return "green";
15371
+ }
15372
+ function evaluateGar(projectName, metrics) {
15373
+ const areas = [];
15374
+ const scopePct = metrics.scope.completionPct;
15375
+ const scopeStatus = scopePct >= 70 ? "green" : scopePct >= 40 ? "amber" : "red";
15376
+ areas.push({
15377
+ name: "Scope",
15378
+ status: scopeStatus,
15379
+ summary: `${scopePct}% complete (${metrics.scope.done}/${metrics.scope.total})`,
15380
+ items: []
15381
+ });
15382
+ const scheduleCount = metrics.schedule.blocked + metrics.schedule.overdue;
15383
+ const scheduleStatus = scheduleCount === 0 ? "green" : scheduleCount <= 2 ? "amber" : "red";
15384
+ const scheduleParts = [];
15385
+ if (metrics.schedule.blocked > 0)
15386
+ scheduleParts.push(`${metrics.schedule.blocked} blocked`);
15387
+ if (metrics.schedule.overdue > 0)
15388
+ scheduleParts.push(`${metrics.schedule.overdue} overdue`);
15389
+ areas.push({
15390
+ name: "Schedule",
15391
+ status: scheduleStatus,
15392
+ summary: scheduleParts.length > 0 ? scheduleParts.join(", ") : "on track",
15393
+ items: metrics.schedule.items
15394
+ });
15395
+ const qualityCount = metrics.quality.risks + metrics.quality.openQuestions;
15396
+ const qualityStatus = qualityCount === 0 ? "green" : qualityCount <= 2 ? "amber" : "red";
15397
+ const qualityParts = [];
15398
+ if (metrics.quality.risks > 0)
15399
+ qualityParts.push(`${metrics.quality.risks} risk(s)`);
15400
+ if (metrics.quality.openQuestions > 0)
15401
+ qualityParts.push(`${metrics.quality.openQuestions} open question(s)`);
15402
+ areas.push({
15403
+ name: "Quality",
15404
+ status: qualityStatus,
15405
+ summary: qualityParts.length > 0 ? qualityParts.join(", ") : "no issues",
15406
+ items: metrics.quality.items
15407
+ });
15408
+ const resourceCount = metrics.resources.unowned;
15409
+ const resourceStatus = resourceCount === 0 ? "green" : resourceCount <= 2 ? "amber" : "red";
15410
+ areas.push({
15411
+ name: "Resources",
15412
+ status: resourceStatus,
15413
+ summary: resourceCount > 0 ? `${resourceCount} unowned action(s)` : "all assigned",
15414
+ items: metrics.resources.items
15415
+ });
15416
+ const overall = worstStatus(areas.map((a) => a.status));
15417
+ return {
15418
+ projectName,
15419
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
15420
+ overall,
15421
+ areas,
15422
+ metrics
15423
+ };
15424
+ }
15425
+
15426
+ // src/plugins/builtin/tools/reports.ts
15305
15427
  function createReportTools(store) {
15306
15428
  return [
15307
15429
  tool8(
@@ -15390,41 +15512,10 @@ function createReportTools(store) {
15390
15512
  "Generate a Green-Amber-Red report with metrics across scope, schedule, quality, and resources",
15391
15513
  {},
15392
15514
  async () => {
15393
- const allActions = store.list({ type: "action" });
15394
- const openActions = allActions.filter((d) => d.frontmatter.status === "open");
15395
- const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
15396
- const allDocs = store.list();
15397
- const blockedItems = allDocs.filter(
15398
- (d) => d.frontmatter.tags?.includes("blocked")
15399
- );
15400
- const overdueItems = allDocs.filter(
15401
- (d) => d.frontmatter.tags?.includes("overdue")
15402
- );
15403
- const openQuestions = store.list({ type: "question", status: "open" });
15404
- const riskItems = allDocs.filter(
15405
- (d) => d.frontmatter.tags?.includes("risk")
15406
- );
15407
- const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
15408
- const areas = {
15409
- scope: {
15410
- total: allActions.length,
15411
- open: openActions.length,
15412
- done: doneActions.length
15413
- },
15414
- schedule: {
15415
- blocked: blockedItems.length,
15416
- overdue: overdueItems.length
15417
- },
15418
- quality: {
15419
- openQuestions: openQuestions.length,
15420
- risks: riskItems.length
15421
- },
15422
- resources: {
15423
- unowned: unownedActions.length
15424
- }
15425
- };
15515
+ const metrics = collectGarMetrics(store);
15516
+ const report = evaluateGar("project", metrics);
15426
15517
  return {
15427
- content: [{ type: "text", text: JSON.stringify({ areas }, null, 2) }]
15518
+ content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
15428
15519
  };
15429
15520
  },
15430
15521
  { annotations: { readOnly: true } }
@@ -16204,6 +16295,188 @@ function createSprintTools(store) {
16204
16295
  ];
16205
16296
  }
16206
16297
 
16298
+ // src/plugins/builtin/tools/sprint-planning.ts
16299
+ import { tool as tool13 } from "@anthropic-ai/claude-agent-sdk";
16300
+ var PRIORITY_ORDER = {
16301
+ critical: 0,
16302
+ high: 1,
16303
+ medium: 2,
16304
+ low: 3
16305
+ };
16306
+ function priorityRank(p) {
16307
+ return PRIORITY_ORDER[p ?? ""] ?? 99;
16308
+ }
16309
+ function createSprintPlanningTools(store) {
16310
+ return [
16311
+ tool13(
16312
+ "gather_sprint_planning_context",
16313
+ "Aggregate all planning-relevant data for proposing the next sprint: approved features, backlog epics, active sprint, velocity reference, blockers, and summary stats",
16314
+ {
16315
+ focusFeature: external_exports.string().optional().describe("Filter backlog to epics of a specific feature ID (e.g. 'F-001')"),
16316
+ sprintDurationDays: external_exports.number().optional().describe("Expected sprint duration in days \u2014 passed through for capacity reasoning")
16317
+ },
16318
+ async (args) => {
16319
+ const features = store.list({ type: "feature" });
16320
+ const epics = store.list({ type: "epic" });
16321
+ const sprints = store.list({ type: "sprint" });
16322
+ const questions = store.list({ type: "question", status: "open" });
16323
+ const contributions = store.list({ type: "contribution" });
16324
+ const approvedFeatures = features.filter((f) => f.frontmatter.status === "approved").sort((a, b) => priorityRank(a.frontmatter.priority) - priorityRank(b.frontmatter.priority)).map((f) => {
16325
+ const linkedEpics = epics.filter((e) => e.frontmatter.linkedFeature === f.frontmatter.id);
16326
+ const epicsByStatus = {};
16327
+ for (const e of linkedEpics) {
16328
+ epicsByStatus[e.frontmatter.status] = (epicsByStatus[e.frontmatter.status] ?? 0) + 1;
16329
+ }
16330
+ return {
16331
+ id: f.frontmatter.id,
16332
+ title: f.frontmatter.title,
16333
+ priority: f.frontmatter.priority,
16334
+ owner: f.frontmatter.owner,
16335
+ epicCount: linkedEpics.length,
16336
+ epicsByStatus
16337
+ };
16338
+ });
16339
+ const assignedEpicIds = /* @__PURE__ */ new Set();
16340
+ for (const sp of sprints) {
16341
+ const linked = sp.frontmatter.linkedEpics ?? [];
16342
+ for (const id of linked) assignedEpicIds.add(id);
16343
+ }
16344
+ const featureMap = new Map(features.map((f) => [f.frontmatter.id, f]));
16345
+ let backlogEpics = epics.filter(
16346
+ (e) => !assignedEpicIds.has(e.frontmatter.id) && e.frontmatter.status !== "done"
16347
+ );
16348
+ if (args.focusFeature) {
16349
+ backlogEpics = backlogEpics.filter(
16350
+ (e) => e.frontmatter.linkedFeature === args.focusFeature
16351
+ );
16352
+ }
16353
+ const backlog = backlogEpics.sort((a, b) => {
16354
+ const fa = featureMap.get(a.frontmatter.linkedFeature);
16355
+ const fb = featureMap.get(b.frontmatter.linkedFeature);
16356
+ return priorityRank(fa?.frontmatter.priority) - priorityRank(fb?.frontmatter.priority);
16357
+ }).map((e) => {
16358
+ const parent = featureMap.get(e.frontmatter.linkedFeature);
16359
+ return {
16360
+ id: e.frontmatter.id,
16361
+ title: e.frontmatter.title,
16362
+ status: e.frontmatter.status,
16363
+ linkedFeature: e.frontmatter.linkedFeature,
16364
+ featureTitle: parent?.frontmatter.title ?? null,
16365
+ featurePriority: parent?.frontmatter.priority ?? null,
16366
+ estimatedEffort: e.frontmatter.estimatedEffort ?? null,
16367
+ targetDate: e.frontmatter.targetDate ?? null
16368
+ };
16369
+ });
16370
+ const activeSprintDoc = sprints.find((s) => s.frontmatter.status === "active") ?? null;
16371
+ let activeSprint = null;
16372
+ if (activeSprintDoc) {
16373
+ const linkedEpicIds = activeSprintDoc.frontmatter.linkedEpics ?? [];
16374
+ const linkedEpics = linkedEpicIds.map((epicId) => {
16375
+ const epic = store.get(epicId);
16376
+ return epic ? { id: epicId, title: epic.frontmatter.title, status: epic.frontmatter.status } : { id: epicId, title: "(not found)", status: "unknown" };
16377
+ });
16378
+ const allDocs = store.list();
16379
+ const sprintTag = `sprint:${activeSprintDoc.frontmatter.id}`;
16380
+ const workItems = allDocs.filter(
16381
+ (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(sprintTag)
16382
+ );
16383
+ const doneCount = workItems.filter(
16384
+ (d) => d.frontmatter.status === "done" || d.frontmatter.status === "resolved" || d.frontmatter.status === "closed"
16385
+ ).length;
16386
+ const completionPct = workItems.length > 0 ? Math.round(doneCount / workItems.length * 100) : 0;
16387
+ activeSprint = {
16388
+ id: activeSprintDoc.frontmatter.id,
16389
+ title: activeSprintDoc.frontmatter.title,
16390
+ goal: activeSprintDoc.frontmatter.goal,
16391
+ startDate: activeSprintDoc.frontmatter.startDate,
16392
+ endDate: activeSprintDoc.frontmatter.endDate,
16393
+ linkedEpics,
16394
+ workItems: { total: workItems.length, done: doneCount, completionPct }
16395
+ };
16396
+ }
16397
+ const completedSprints = sprints.filter((s) => s.frontmatter.status === "completed").sort((a, b) => {
16398
+ const da = a.frontmatter.endDate ?? "";
16399
+ const db = b.frontmatter.endDate ?? "";
16400
+ return da < db ? 1 : da > db ? -1 : 0;
16401
+ }).slice(0, 2);
16402
+ const velocityReference = completedSprints.map((sp) => {
16403
+ const linkedEpicIds = sp.frontmatter.linkedEpics ?? [];
16404
+ const efforts = [];
16405
+ for (const epicId of linkedEpicIds) {
16406
+ const epic = store.get(epicId);
16407
+ if (epic?.frontmatter.estimatedEffort) {
16408
+ efforts.push(String(epic.frontmatter.estimatedEffort));
16409
+ }
16410
+ }
16411
+ const allDocs = store.list();
16412
+ const sprintTag = `sprint:${sp.frontmatter.id}`;
16413
+ const workItems = allDocs.filter(
16414
+ (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(sprintTag)
16415
+ );
16416
+ return {
16417
+ id: sp.frontmatter.id,
16418
+ title: sp.frontmatter.title,
16419
+ startDate: sp.frontmatter.startDate,
16420
+ endDate: sp.frontmatter.endDate,
16421
+ epicCount: linkedEpicIds.length,
16422
+ efforts,
16423
+ workItemCount: workItems.length
16424
+ };
16425
+ });
16426
+ const openBlockerContributions = contributions.filter(
16427
+ (c) => c.frontmatter.status === "open" && (c.frontmatter.contributionType === "risk-finding" || c.frontmatter.contributionType === "blocker-report")
16428
+ );
16429
+ const blockers = {
16430
+ openQuestions: questions.map((q) => ({
16431
+ id: q.frontmatter.id,
16432
+ title: q.frontmatter.title
16433
+ })),
16434
+ openRiskAndBlockerContributions: openBlockerContributions.map((c) => ({
16435
+ id: c.frontmatter.id,
16436
+ title: c.frontmatter.title,
16437
+ contributionType: c.frontmatter.contributionType
16438
+ }))
16439
+ };
16440
+ const totalBacklogEfforts = backlog.filter((e) => e.estimatedEffort !== null).map((e) => String(e.estimatedEffort));
16441
+ const approvedFeaturesWithNoEpics = approvedFeatures.filter((f) => f.epicCount === 0).map((f) => ({ id: f.id, title: f.title }));
16442
+ const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
16443
+ const epicsAtRisk = epics.filter((e) => {
16444
+ if (e.frontmatter.status === "done") return false;
16445
+ if (e.frontmatter.targetDate && e.frontmatter.targetDate < now) return true;
16446
+ const parent = featureMap.get(e.frontmatter.linkedFeature);
16447
+ if (parent?.frontmatter.status === "deferred") return true;
16448
+ return false;
16449
+ }).map((e) => ({
16450
+ id: e.frontmatter.id,
16451
+ title: e.frontmatter.title,
16452
+ reason: e.frontmatter.targetDate && e.frontmatter.targetDate < now ? "past-target-date" : "deferred-feature"
16453
+ }));
16454
+ const plannedSprintCount = sprints.filter((s) => s.frontmatter.status === "planned").length;
16455
+ const summary = {
16456
+ totalBacklogEpics: backlog.length,
16457
+ totalBacklogEfforts,
16458
+ approvedFeaturesWithNoEpics,
16459
+ epicsAtRisk,
16460
+ plannedSprintCount
16461
+ };
16462
+ const result = {
16463
+ approvedFeatures,
16464
+ backlog,
16465
+ activeSprint,
16466
+ velocityReference,
16467
+ blockers,
16468
+ summary,
16469
+ ...args.sprintDurationDays !== void 0 ? { sprintDurationDays: args.sprintDurationDays } : {}
16470
+ };
16471
+ return {
16472
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
16473
+ };
16474
+ },
16475
+ { annotations: { readOnly: true } }
16476
+ )
16477
+ ];
16478
+ }
16479
+
16207
16480
  // src/plugins/common.ts
16208
16481
  var COMMON_REGISTRATIONS = [
16209
16482
  { type: "meeting", dirName: "meetings", idPrefix: "M" },
@@ -16220,7 +16493,8 @@ function createCommonTools(store) {
16220
16493
  ...createFeatureTools(store),
16221
16494
  ...createEpicTools(store),
16222
16495
  ...createContributionTools(store),
16223
- ...createSprintTools(store)
16496
+ ...createSprintTools(store),
16497
+ ...createSprintPlanningTools(store)
16224
16498
  ];
16225
16499
  }
16226
16500
 
@@ -16293,7 +16567,12 @@ var genericAgilePlugin = {
16293
16567
  - **list_sprints** / **get_sprint**: View sprints to understand iteration scope and delivery dates.
16294
16568
  - **update_sprint**: Assign epics to sprints by updating linkedEpics when breaking features into work.
16295
16569
  - Tag technical actions and decisions with \`sprint:SP-xxx\` to associate them with a sprint.
16296
- - Use **generate_sprint_progress** to track technical work completion within an iteration.`,
16570
+ - Use **generate_sprint_progress** to track technical work completion within an iteration.
16571
+
16572
+ **Sprint Planning:**
16573
+ - When asked to plan or propose a sprint, ALWAYS call **gather_sprint_planning_context** first.
16574
+ - Focus on: technical readiness of each epic, open technical questions or spikes, effort balance across the sprint, and feature coverage.
16575
+ - Present a structured sprint proposal with technical rationale for each selected epic, known technical risks, and any prerequisite work that should be completed first.`,
16297
16576
  "delivery-manager": `You track delivery across features and epics, manage schedules, and report on progress.
16298
16577
 
16299
16578
  **Report Tools:**
@@ -16341,7 +16620,13 @@ var genericAgilePlugin = {
16341
16620
  - Assign epics to sprints via linkedEpics.
16342
16621
  - Tag work items (actions, decisions, questions) with \`sprint:SP-xxx\` for sprint scoping.
16343
16622
  - Track delivery dates and flag at-risk sprints.
16344
- - Register past/completed sprints for historical tracking.`,
16623
+ - Register past/completed sprints for historical tracking.
16624
+
16625
+ **Sprint Planning:**
16626
+ - When asked to plan or propose a sprint, ALWAYS call **gather_sprint_planning_context** first. It aggregates approved features, backlog epics, active sprint status, velocity from recent sprints, blockers, and summary stats in one call.
16627
+ - Reason through: priority (critical/high features first), capacity (compare backlog effort to velocity reference), dependencies and blockers, balance across features, and risk.
16628
+ - Present a structured sprint proposal: title, goal, suggested dates, selected epics with rationale for each, excluded epics with reason, and identified risks.
16629
+ - After user confirmation, use **create_sprint** with the agreed epics to persist the sprint.`,
16345
16630
  "*": `You have access to feature, epic, sprint, and meeting tools for project coordination:
16346
16631
 
16347
16632
  **Features** (F-xxx): Product capabilities defined by the Product Owner. Features progress through draft \u2192 approved \u2192 done.
@@ -16364,10 +16649,10 @@ var genericAgilePlugin = {
16364
16649
  };
16365
16650
 
16366
16651
  // src/plugins/builtin/tools/use-cases.ts
16367
- import { tool as tool13 } from "@anthropic-ai/claude-agent-sdk";
16652
+ import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
16368
16653
  function createUseCaseTools(store) {
16369
16654
  return [
16370
- tool13(
16655
+ tool14(
16371
16656
  "list_use_cases",
16372
16657
  "List all extension use cases, optionally filtered by status or extension type",
16373
16658
  {
@@ -16397,7 +16682,7 @@ function createUseCaseTools(store) {
16397
16682
  },
16398
16683
  { annotations: { readOnly: true } }
16399
16684
  ),
16400
- tool13(
16685
+ tool14(
16401
16686
  "get_use_case",
16402
16687
  "Get the full content of a specific use case by ID",
16403
16688
  { id: external_exports.string().describe("Use case ID (e.g. 'UC-001')") },
@@ -16424,7 +16709,7 @@ function createUseCaseTools(store) {
16424
16709
  },
16425
16710
  { annotations: { readOnly: true } }
16426
16711
  ),
16427
- tool13(
16712
+ tool14(
16428
16713
  "create_use_case",
16429
16714
  "Create a new extension use case definition (Phase 1: Assess Extension Use Case)",
16430
16715
  {
@@ -16458,7 +16743,7 @@ function createUseCaseTools(store) {
16458
16743
  };
16459
16744
  }
16460
16745
  ),
16461
- tool13(
16746
+ tool14(
16462
16747
  "update_use_case",
16463
16748
  "Update an existing extension use case",
16464
16749
  {
@@ -16488,10 +16773,10 @@ function createUseCaseTools(store) {
16488
16773
  }
16489
16774
 
16490
16775
  // src/plugins/builtin/tools/tech-assessments.ts
16491
- import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
16776
+ import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
16492
16777
  function createTechAssessmentTools(store) {
16493
16778
  return [
16494
- tool14(
16779
+ tool15(
16495
16780
  "list_tech_assessments",
16496
16781
  "List all technology assessments, optionally filtered by status",
16497
16782
  {
@@ -16522,7 +16807,7 @@ function createTechAssessmentTools(store) {
16522
16807
  },
16523
16808
  { annotations: { readOnly: true } }
16524
16809
  ),
16525
- tool14(
16810
+ tool15(
16526
16811
  "get_tech_assessment",
16527
16812
  "Get the full content of a specific technology assessment by ID",
16528
16813
  { id: external_exports.string().describe("Tech assessment ID (e.g. 'TA-001')") },
@@ -16549,7 +16834,7 @@ function createTechAssessmentTools(store) {
16549
16834
  },
16550
16835
  { annotations: { readOnly: true } }
16551
16836
  ),
16552
- tool14(
16837
+ tool15(
16553
16838
  "create_tech_assessment",
16554
16839
  "Create a new technology assessment linked to an assessed/approved use case (Phase 2: Assess Extension Technology)",
16555
16840
  {
@@ -16620,7 +16905,7 @@ function createTechAssessmentTools(store) {
16620
16905
  };
16621
16906
  }
16622
16907
  ),
16623
- tool14(
16908
+ tool15(
16624
16909
  "update_tech_assessment",
16625
16910
  "Update an existing technology assessment. The linked use case cannot be changed.",
16626
16911
  {
@@ -16650,10 +16935,10 @@ function createTechAssessmentTools(store) {
16650
16935
  }
16651
16936
 
16652
16937
  // src/plugins/builtin/tools/extension-designs.ts
16653
- import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
16938
+ import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
16654
16939
  function createExtensionDesignTools(store) {
16655
16940
  return [
16656
- tool15(
16941
+ tool16(
16657
16942
  "list_extension_designs",
16658
16943
  "List all extension designs, optionally filtered by status",
16659
16944
  {
@@ -16683,7 +16968,7 @@ function createExtensionDesignTools(store) {
16683
16968
  },
16684
16969
  { annotations: { readOnly: true } }
16685
16970
  ),
16686
- tool15(
16971
+ tool16(
16687
16972
  "get_extension_design",
16688
16973
  "Get the full content of a specific extension design by ID",
16689
16974
  { id: external_exports.string().describe("Extension design ID (e.g. 'XD-001')") },
@@ -16710,7 +16995,7 @@ function createExtensionDesignTools(store) {
16710
16995
  },
16711
16996
  { annotations: { readOnly: true } }
16712
16997
  ),
16713
- tool15(
16998
+ tool16(
16714
16999
  "create_extension_design",
16715
17000
  "Create a new extension design linked to a recommended tech assessment (Phase 3: Define Extension Target Solution)",
16716
17001
  {
@@ -16778,7 +17063,7 @@ function createExtensionDesignTools(store) {
16778
17063
  };
16779
17064
  }
16780
17065
  ),
16781
- tool15(
17066
+ tool16(
16782
17067
  "update_extension_design",
16783
17068
  "Update an existing extension design. The linked tech assessment cannot be changed.",
16784
17069
  {
@@ -16807,10 +17092,10 @@ function createExtensionDesignTools(store) {
16807
17092
  }
16808
17093
 
16809
17094
  // src/plugins/builtin/tools/aem-reports.ts
16810
- import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
17095
+ import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
16811
17096
  function createAemReportTools(store) {
16812
17097
  return [
16813
- tool16(
17098
+ tool17(
16814
17099
  "generate_extension_portfolio",
16815
17100
  "Generate a portfolio view of all use cases with their linked tech assessments and extension designs",
16816
17101
  {},
@@ -16862,7 +17147,7 @@ function createAemReportTools(store) {
16862
17147
  },
16863
17148
  { annotations: { readOnly: true } }
16864
17149
  ),
16865
- tool16(
17150
+ tool17(
16866
17151
  "generate_tech_readiness",
16867
17152
  "Generate a BTP technology readiness report showing service coverage and gaps across assessments",
16868
17153
  {},
@@ -16914,7 +17199,7 @@ function createAemReportTools(store) {
16914
17199
  },
16915
17200
  { annotations: { readOnly: true } }
16916
17201
  ),
16917
- tool16(
17202
+ tool17(
16918
17203
  "generate_phase_status",
16919
17204
  "Generate a phase progress report showing artifact counts and readiness per AEM phase",
16920
17205
  {},
@@ -16976,11 +17261,11 @@ function createAemReportTools(store) {
16976
17261
  import * as fs6 from "fs";
16977
17262
  import * as path6 from "path";
16978
17263
  import * as YAML4 from "yaml";
16979
- import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
17264
+ import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
16980
17265
  var PHASES = ["assess-use-case", "assess-technology", "define-solution"];
16981
17266
  function createAemPhaseTools(store, marvinDir) {
16982
17267
  return [
16983
- tool17(
17268
+ tool18(
16984
17269
  "get_current_phase",
16985
17270
  "Get the current AEM phase from project configuration",
16986
17271
  {},
@@ -17001,7 +17286,7 @@ function createAemPhaseTools(store, marvinDir) {
17001
17286
  },
17002
17287
  { annotations: { readOnly: true } }
17003
17288
  ),
17004
- tool17(
17289
+ tool18(
17005
17290
  "advance_phase",
17006
17291
  "Advance to the next AEM phase. Performs soft gate checks and warns if artifacts are incomplete, but does not block.",
17007
17292
  {
@@ -17308,9 +17593,495 @@ Be thorough but concise. Focus on actionable insights.`,
17308
17593
  ]
17309
17594
  };
17310
17595
 
17596
+ // src/skills/builtin/jira/tools.ts
17597
+ import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
17598
+
17599
+ // src/skills/builtin/jira/client.ts
17600
+ var JiraClient = class {
17601
+ baseUrl;
17602
+ authHeader;
17603
+ constructor(config2) {
17604
+ this.baseUrl = `https://${config2.host}/rest/api/2`;
17605
+ this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
17606
+ }
17607
+ async request(path18, method = "GET", body) {
17608
+ const url2 = `${this.baseUrl}${path18}`;
17609
+ const headers = {
17610
+ Authorization: this.authHeader,
17611
+ "Content-Type": "application/json",
17612
+ Accept: "application/json"
17613
+ };
17614
+ const response = await fetch(url2, {
17615
+ method,
17616
+ headers,
17617
+ body: body ? JSON.stringify(body) : void 0
17618
+ });
17619
+ if (!response.ok) {
17620
+ const text = await response.text().catch(() => "");
17621
+ throw new Error(
17622
+ `Jira API error ${response.status} ${method} ${path18}: ${text}`
17623
+ );
17624
+ }
17625
+ if (response.status === 204) return void 0;
17626
+ return response.json();
17627
+ }
17628
+ async searchIssues(jql, maxResults = 50) {
17629
+ const params = new URLSearchParams({
17630
+ jql,
17631
+ maxResults: String(maxResults)
17632
+ });
17633
+ return this.request(`/search?${params}`);
17634
+ }
17635
+ async getIssue(key) {
17636
+ return this.request(`/issue/${encodeURIComponent(key)}`);
17637
+ }
17638
+ async createIssue(fields) {
17639
+ return this.request("/issue", "POST", { fields });
17640
+ }
17641
+ async updateIssue(key, fields) {
17642
+ await this.request(
17643
+ `/issue/${encodeURIComponent(key)}`,
17644
+ "PUT",
17645
+ { fields }
17646
+ );
17647
+ }
17648
+ async addComment(key, body) {
17649
+ await this.request(
17650
+ `/issue/${encodeURIComponent(key)}/comment`,
17651
+ "POST",
17652
+ { body }
17653
+ );
17654
+ }
17655
+ };
17656
+ function createJiraClient() {
17657
+ const host = process.env.JIRA_HOST;
17658
+ const email3 = process.env.JIRA_EMAIL;
17659
+ const apiToken = process.env.JIRA_API_TOKEN;
17660
+ if (!host || !email3 || !apiToken) return null;
17661
+ return new JiraClient({ host, email: email3, apiToken });
17662
+ }
17663
+
17664
+ // src/skills/builtin/jira/tools.ts
17665
+ var JIRA_TYPE = "jira-issue";
17666
+ function jiraNotConfiguredError() {
17667
+ return {
17668
+ content: [
17669
+ {
17670
+ type: "text",
17671
+ text: "Jira is not configured. Set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables."
17672
+ }
17673
+ ],
17674
+ isError: true
17675
+ };
17676
+ }
17677
+ function mapJiraStatus(jiraStatus) {
17678
+ const lower = jiraStatus.toLowerCase();
17679
+ if (lower === "done" || lower === "closed" || lower === "resolved") return "done";
17680
+ if (lower === "in progress" || lower === "in review") return "in-progress";
17681
+ return "open";
17682
+ }
17683
+ function jiraIssueToFrontmatter(issue2, host, linkedArtifacts) {
17684
+ return {
17685
+ title: issue2.fields.summary,
17686
+ status: mapJiraStatus(issue2.fields.status.name),
17687
+ jiraKey: issue2.key,
17688
+ jiraUrl: `https://${host}/browse/${issue2.key}`,
17689
+ issueType: issue2.fields.issuetype.name,
17690
+ priority: issue2.fields.priority?.name ?? "None",
17691
+ assignee: issue2.fields.assignee?.displayName ?? "",
17692
+ labels: issue2.fields.labels ?? [],
17693
+ linkedArtifacts: linkedArtifacts ?? [],
17694
+ tags: [`jira:${issue2.key}`],
17695
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
17696
+ };
17697
+ }
17698
+ function findByJiraKey(store, jiraKey) {
17699
+ const docs = store.list({ type: JIRA_TYPE });
17700
+ return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
17701
+ }
17702
+ function createJiraTools(store) {
17703
+ return [
17704
+ // --- Local read tools ---
17705
+ tool19(
17706
+ "list_jira_issues",
17707
+ "List locally synced Jira issues (JI-xxx documents), optionally filtered by status or Jira key",
17708
+ {
17709
+ status: external_exports.enum(["open", "in-progress", "done"]).optional().describe("Filter by local status"),
17710
+ jiraKey: external_exports.string().optional().describe("Filter by Jira issue key (e.g. 'PROJ-123')")
17711
+ },
17712
+ async (args) => {
17713
+ let docs = store.list({ type: JIRA_TYPE, status: args.status });
17714
+ if (args.jiraKey) {
17715
+ docs = docs.filter((d) => d.frontmatter.jiraKey === args.jiraKey);
17716
+ }
17717
+ const summary = docs.map((d) => ({
17718
+ id: d.frontmatter.id,
17719
+ title: d.frontmatter.title,
17720
+ status: d.frontmatter.status,
17721
+ jiraKey: d.frontmatter.jiraKey,
17722
+ issueType: d.frontmatter.issueType,
17723
+ priority: d.frontmatter.priority,
17724
+ assignee: d.frontmatter.assignee,
17725
+ linkedArtifacts: d.frontmatter.linkedArtifacts
17726
+ }));
17727
+ return {
17728
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17729
+ };
17730
+ },
17731
+ { annotations: { readOnly: true } }
17732
+ ),
17733
+ tool19(
17734
+ "get_jira_issue",
17735
+ "Get the full content of a locally synced Jira issue by local ID (JI-xxx) or Jira key (PROJ-123)",
17736
+ {
17737
+ id: external_exports.string().describe("Local ID (e.g. 'JI-001') or Jira key (e.g. 'PROJ-123')")
17738
+ },
17739
+ async (args) => {
17740
+ let doc = store.get(args.id);
17741
+ if (!doc) {
17742
+ doc = findByJiraKey(store, args.id);
17743
+ }
17744
+ if (!doc) {
17745
+ return {
17746
+ content: [{ type: "text", text: `Jira issue ${args.id} not found locally` }],
17747
+ isError: true
17748
+ };
17749
+ }
17750
+ return {
17751
+ content: [
17752
+ {
17753
+ type: "text",
17754
+ text: JSON.stringify(
17755
+ { ...doc.frontmatter, content: doc.content },
17756
+ null,
17757
+ 2
17758
+ )
17759
+ }
17760
+ ]
17761
+ };
17762
+ },
17763
+ { annotations: { readOnly: true } }
17764
+ ),
17765
+ // --- Jira → Local tools ---
17766
+ tool19(
17767
+ "pull_jira_issue",
17768
+ "Fetch a single Jira issue by key and create/update a local JI-xxx document",
17769
+ {
17770
+ key: external_exports.string().describe("Jira issue key (e.g. 'PROJ-123')")
17771
+ },
17772
+ async (args) => {
17773
+ const client = createJiraClient();
17774
+ if (!client) return jiraNotConfiguredError();
17775
+ const issue2 = await client.getIssue(args.key);
17776
+ const host = process.env.JIRA_HOST;
17777
+ const existing = findByJiraKey(store, args.key);
17778
+ if (existing) {
17779
+ const fm2 = jiraIssueToFrontmatter(
17780
+ issue2,
17781
+ host,
17782
+ existing.frontmatter.linkedArtifacts
17783
+ );
17784
+ const doc2 = store.update(
17785
+ existing.frontmatter.id,
17786
+ fm2,
17787
+ issue2.fields.description ?? ""
17788
+ );
17789
+ return {
17790
+ content: [
17791
+ {
17792
+ type: "text",
17793
+ text: `Updated ${doc2.frontmatter.id} from Jira ${args.key}`
17794
+ }
17795
+ ]
17796
+ };
17797
+ }
17798
+ const fm = jiraIssueToFrontmatter(issue2, host);
17799
+ const doc = store.create(
17800
+ JIRA_TYPE,
17801
+ fm,
17802
+ issue2.fields.description ?? ""
17803
+ );
17804
+ return {
17805
+ content: [
17806
+ {
17807
+ type: "text",
17808
+ text: `Created ${doc.frontmatter.id} from Jira ${args.key}`
17809
+ }
17810
+ ]
17811
+ };
17812
+ }
17813
+ ),
17814
+ tool19(
17815
+ "pull_jira_issues_jql",
17816
+ "Bulk fetch Jira issues via JQL query and create/update local JI-xxx documents",
17817
+ {
17818
+ jql: external_exports.string().describe(`JQL query (e.g. 'project = PROJ AND status = "In Progress"')`),
17819
+ maxResults: external_exports.number().optional().describe("Max issues to fetch (default 50)")
17820
+ },
17821
+ async (args) => {
17822
+ const client = createJiraClient();
17823
+ if (!client) return jiraNotConfiguredError();
17824
+ const result = await client.searchIssues(args.jql, args.maxResults);
17825
+ const host = process.env.JIRA_HOST;
17826
+ const created = [];
17827
+ const updated = [];
17828
+ for (const issue2 of result.issues) {
17829
+ const existing = findByJiraKey(store, issue2.key);
17830
+ if (existing) {
17831
+ const fm = jiraIssueToFrontmatter(
17832
+ issue2,
17833
+ host,
17834
+ existing.frontmatter.linkedArtifacts
17835
+ );
17836
+ store.update(
17837
+ existing.frontmatter.id,
17838
+ fm,
17839
+ issue2.fields.description ?? ""
17840
+ );
17841
+ updated.push(`${existing.frontmatter.id} (${issue2.key})`);
17842
+ } else {
17843
+ const fm = jiraIssueToFrontmatter(issue2, host);
17844
+ const doc = store.create(
17845
+ JIRA_TYPE,
17846
+ fm,
17847
+ issue2.fields.description ?? ""
17848
+ );
17849
+ created.push(`${doc.frontmatter.id} (${issue2.key})`);
17850
+ }
17851
+ }
17852
+ const parts = [
17853
+ `Fetched ${result.issues.length} of ${result.total} matching issues.`
17854
+ ];
17855
+ if (created.length > 0) parts.push(`Created: ${created.join(", ")}`);
17856
+ if (updated.length > 0) parts.push(`Updated: ${updated.join(", ")}`);
17857
+ return {
17858
+ content: [{ type: "text", text: parts.join("\n") }]
17859
+ };
17860
+ }
17861
+ ),
17862
+ // --- Local → Jira tools ---
17863
+ tool19(
17864
+ "push_artifact_to_jira",
17865
+ "Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
17866
+ {
17867
+ artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
17868
+ projectKey: external_exports.string().describe("Jira project key (e.g. 'PROJ')"),
17869
+ issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
17870
+ },
17871
+ async (args) => {
17872
+ const client = createJiraClient();
17873
+ if (!client) return jiraNotConfiguredError();
17874
+ const artifact = store.get(args.artifactId);
17875
+ if (!artifact) {
17876
+ return {
17877
+ content: [
17878
+ { type: "text", text: `Artifact ${args.artifactId} not found` }
17879
+ ],
17880
+ isError: true
17881
+ };
17882
+ }
17883
+ const description = [
17884
+ artifact.content,
17885
+ "",
17886
+ `---`,
17887
+ `Marvin artifact: ${artifact.frontmatter.id} (${artifact.frontmatter.type})`,
17888
+ `Status: ${artifact.frontmatter.status}`
17889
+ ].join("\n");
17890
+ const jiraResult = await client.createIssue({
17891
+ project: { key: args.projectKey },
17892
+ summary: artifact.frontmatter.title,
17893
+ description,
17894
+ issuetype: { name: args.issueType ?? "Task" }
17895
+ });
17896
+ const host = process.env.JIRA_HOST;
17897
+ const jiDoc = store.create(
17898
+ JIRA_TYPE,
17899
+ {
17900
+ title: artifact.frontmatter.title,
17901
+ status: "open",
17902
+ jiraKey: jiraResult.key,
17903
+ jiraUrl: `https://${host}/browse/${jiraResult.key}`,
17904
+ issueType: args.issueType ?? "Task",
17905
+ priority: "Medium",
17906
+ assignee: "",
17907
+ labels: [],
17908
+ linkedArtifacts: [args.artifactId],
17909
+ tags: [`jira:${jiraResult.key}`],
17910
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
17911
+ },
17912
+ ""
17913
+ );
17914
+ return {
17915
+ content: [
17916
+ {
17917
+ type: "text",
17918
+ text: `Created Jira ${jiraResult.key} from ${args.artifactId}. Tracking locally as ${jiDoc.frontmatter.id}.`
17919
+ }
17920
+ ]
17921
+ };
17922
+ }
17923
+ ),
17924
+ // --- Bidirectional sync ---
17925
+ tool19(
17926
+ "sync_jira_issue",
17927
+ "Bidirectional sync: push local title/description to Jira, pull latest status/assignee/labels back",
17928
+ {
17929
+ id: external_exports.string().describe("Local JI-xxx ID")
17930
+ },
17931
+ async (args) => {
17932
+ const client = createJiraClient();
17933
+ if (!client) return jiraNotConfiguredError();
17934
+ const doc = store.get(args.id);
17935
+ if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
17936
+ return {
17937
+ content: [
17938
+ { type: "text", text: `Jira issue ${args.id} not found locally` }
17939
+ ],
17940
+ isError: true
17941
+ };
17942
+ }
17943
+ const jiraKey = doc.frontmatter.jiraKey;
17944
+ await client.updateIssue(jiraKey, {
17945
+ summary: doc.frontmatter.title,
17946
+ description: doc.content || void 0
17947
+ });
17948
+ const issue2 = await client.getIssue(jiraKey);
17949
+ const host = process.env.JIRA_HOST;
17950
+ const fm = jiraIssueToFrontmatter(
17951
+ issue2,
17952
+ host,
17953
+ doc.frontmatter.linkedArtifacts
17954
+ );
17955
+ store.update(args.id, fm, issue2.fields.description ?? "");
17956
+ return {
17957
+ content: [
17958
+ {
17959
+ type: "text",
17960
+ text: `Synced ${args.id} \u2194 ${jiraKey}. Status: ${fm.status}, Assignee: ${fm.assignee || "unassigned"}`
17961
+ }
17962
+ ]
17963
+ };
17964
+ }
17965
+ ),
17966
+ // --- Local link tool ---
17967
+ tool19(
17968
+ "link_artifact_to_jira",
17969
+ "Add a Marvin artifact ID to a JI-xxx document's linkedArtifacts field",
17970
+ {
17971
+ jiraIssueId: external_exports.string().describe("Local JI-xxx ID"),
17972
+ artifactId: external_exports.string().describe("Marvin artifact ID to link (e.g. 'D-001', 'F-003')")
17973
+ },
17974
+ async (args) => {
17975
+ const doc = store.get(args.jiraIssueId);
17976
+ if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
17977
+ return {
17978
+ content: [
17979
+ {
17980
+ type: "text",
17981
+ text: `Jira issue ${args.jiraIssueId} not found locally`
17982
+ }
17983
+ ],
17984
+ isError: true
17985
+ };
17986
+ }
17987
+ const artifact = store.get(args.artifactId);
17988
+ if (!artifact) {
17989
+ return {
17990
+ content: [
17991
+ { type: "text", text: `Artifact ${args.artifactId} not found` }
17992
+ ],
17993
+ isError: true
17994
+ };
17995
+ }
17996
+ const linked = doc.frontmatter.linkedArtifacts ?? [];
17997
+ if (linked.includes(args.artifactId)) {
17998
+ return {
17999
+ content: [
18000
+ {
18001
+ type: "text",
18002
+ text: `${args.artifactId} is already linked to ${args.jiraIssueId}`
18003
+ }
18004
+ ]
18005
+ };
18006
+ }
18007
+ store.update(args.jiraIssueId, {
18008
+ linkedArtifacts: [...linked, args.artifactId]
18009
+ });
18010
+ return {
18011
+ content: [
18012
+ {
18013
+ type: "text",
18014
+ text: `Linked ${args.artifactId} to ${args.jiraIssueId}`
18015
+ }
18016
+ ]
18017
+ };
18018
+ }
18019
+ )
18020
+ ];
18021
+ }
18022
+
18023
+ // src/skills/builtin/jira/index.ts
18024
+ var jiraSkill = {
18025
+ id: "jira",
18026
+ name: "Jira Integration",
18027
+ description: "Bidirectional sync between Marvin artifacts and Jira issues",
18028
+ version: "1.0.0",
18029
+ format: "builtin-ts",
18030
+ // No default persona affinity — opt-in via config.yaml skills section
18031
+ documentTypeRegistrations: [
18032
+ { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
18033
+ ],
18034
+ tools: (store) => createJiraTools(store),
18035
+ promptFragments: {
18036
+ "product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
18037
+
18038
+ **Available tools:**
18039
+ - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18040
+ - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18041
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.)
18042
+ - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18043
+ - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18044
+
18045
+ **As Product Owner, use Jira integration to:**
18046
+ - Pull stakeholder-reported issues for triage and prioritization
18047
+ - Push approved features as Stories for development tracking
18048
+ - Link decisions to Jira issues for audit trail and traceability
18049
+ - Use JQL queries to review backlog status (e.g. \`project = PROJ AND status = "To Do"\`)`,
18050
+ "tech-lead": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
18051
+
18052
+ **Available tools:**
18053
+ - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18054
+ - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18055
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, etc.)
18056
+ - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18057
+ - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18058
+
18059
+ **As Tech Lead, use Jira integration to:**
18060
+ - Pull technical issues and bugs for sprint planning and estimation
18061
+ - Push epics and technical decisions to Jira for cross-team visibility
18062
+ - Bidirectional sync to keep local governance and Jira in alignment
18063
+ - Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
18064
+ "delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
18065
+
18066
+ **Available tools:**
18067
+ - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18068
+ - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18069
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.)
18070
+ - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18071
+ - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18072
+
18073
+ **As Delivery Manager, use Jira integration to:**
18074
+ - Pull sprint issues for tracking progress and blockers
18075
+ - Push actions and decisions to Jira for stakeholder visibility
18076
+ - Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
18077
+ - Sync status between Marvin governance items and Jira issues`
18078
+ }
18079
+ };
18080
+
17311
18081
  // src/skills/registry.ts
17312
18082
  var BUILTIN_SKILLS = {
17313
- "governance-review": governanceReviewSkill
18083
+ "governance-review": governanceReviewSkill,
18084
+ "jira": jiraSkill
17314
18085
  };
17315
18086
  var GOVERNANCE_TOOL_NAMES = [
17316
18087
  "mcp__marvin-governance__list_decisions",
@@ -17459,6 +18230,16 @@ function resolveSkillsForPersona(personaId, skillsConfig, allSkills) {
17459
18230
  }
17460
18231
  return result;
17461
18232
  }
18233
+ function collectSkillRegistrations(skillIds, allSkills) {
18234
+ const registrations = [];
18235
+ for (const id of skillIds) {
18236
+ const skill = allSkills.get(id);
18237
+ if (skill?.documentTypeRegistrations) {
18238
+ registrations.push(...skill.documentTypeRegistrations);
18239
+ }
18240
+ }
18241
+ return registrations;
18242
+ }
17462
18243
  function getSkillTools(skillIds, allSkills, store) {
17463
18244
  const tools = [];
17464
18245
  for (const id of skillIds) {
@@ -17570,16 +18351,17 @@ ${wildcardPrompt}
17570
18351
  async function startSession(options) {
17571
18352
  const { persona, config: config2, marvinDir, projectRoot } = options;
17572
18353
  const plugin = resolvePlugin(config2.project.methodology);
17573
- const registrations = plugin?.documentTypeRegistrations ?? [];
17574
- const store = new DocumentStore(marvinDir, registrations);
18354
+ const pluginRegistrations = plugin?.documentTypeRegistrations ?? [];
18355
+ const allSkills = loadAllSkills(marvinDir);
18356
+ const skillIds = resolveSkillsForPersona(persona.id, config2.project.skills, allSkills);
18357
+ const skillRegistrations = collectSkillRegistrations(skillIds, allSkills);
18358
+ const store = new DocumentStore(marvinDir, [...pluginRegistrations, ...skillRegistrations]);
17575
18359
  const sessionStore = new SessionStore(marvinDir);
17576
18360
  const sourcesDir = path8.join(marvinDir, "sources");
17577
18361
  const hasSourcesDir = fs8.existsSync(sourcesDir);
17578
18362
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
17579
18363
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
17580
18364
  const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
17581
- const allSkills = loadAllSkills(marvinDir);
17582
- const skillIds = resolveSkillsForPersona(persona.id, config2.project.skills, allSkills);
17583
18365
  const codeSkillTools = getSkillTools(skillIds, allSkills, store);
17584
18366
  const skillAgents = getSkillAgentDefinitions(skillIds, allSkills);
17585
18367
  const skillPromptFragment = getSkillPromptFragment(skillIds, allSkills, persona.id);
@@ -17776,7 +18558,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17776
18558
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17777
18559
 
17778
18560
  // src/skills/action-tools.ts
17779
- import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
18561
+ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
17780
18562
 
17781
18563
  // src/skills/action-runner.ts
17782
18564
  import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
@@ -17842,7 +18624,7 @@ function createSkillActionTools(skills, context) {
17842
18624
  if (!skill.actions) continue;
17843
18625
  for (const action of skill.actions) {
17844
18626
  tools.push(
17845
- tool18(
18627
+ tool20(
17846
18628
  `${skill.id}__${action.id}`,
17847
18629
  action.description,
17848
18630
  {
@@ -17934,10 +18716,10 @@ ${lines.join("\n\n")}`;
17934
18716
  }
17935
18717
 
17936
18718
  // src/mcp/persona-tools.ts
17937
- import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
18719
+ import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
17938
18720
  function createPersonaTools(ctx, marvinDir) {
17939
18721
  return [
17940
- tool19(
18722
+ tool21(
17941
18723
  "set_persona",
17942
18724
  "Set the active persona for this session. Returns full guidance for the selected persona including behavioral rules, allowed document types, and scope. Call this before working to ensure persona-appropriate behavior.",
17943
18725
  {
@@ -17967,7 +18749,7 @@ ${summaries}`
17967
18749
  };
17968
18750
  }
17969
18751
  ),
17970
- tool19(
18752
+ tool21(
17971
18753
  "get_persona_guidance",
17972
18754
  "Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
17973
18755
  {
@@ -19210,20 +19992,23 @@ async function skillsInstallCommand(skillId, options) {
19210
19992
  console.log(chalk10.red("Please specify a persona with --as <persona>."));
19211
19993
  return;
19212
19994
  }
19995
+ const targets = persona === "all" ? listPersonas().map((p) => p.id) : [persona];
19213
19996
  const config2 = loadProjectConfig(project.marvinDir);
19214
19997
  if (!config2.skills) {
19215
19998
  config2.skills = {};
19216
19999
  }
19217
- if (!config2.skills[persona]) {
19218
- config2.skills[persona] = [];
19219
- }
19220
- if (config2.skills[persona].includes(skillId)) {
19221
- console.log(chalk10.yellow(`Skill "${skillId}" is already assigned to ${persona}.`));
19222
- return;
20000
+ for (const target of targets) {
20001
+ if (!config2.skills[target]) {
20002
+ config2.skills[target] = [];
20003
+ }
20004
+ if (config2.skills[target].includes(skillId)) {
20005
+ console.log(chalk10.yellow(`Skill "${skillId}" is already assigned to ${target}.`));
20006
+ continue;
20007
+ }
20008
+ config2.skills[target].push(skillId);
20009
+ console.log(chalk10.green(`Assigned skill "${skillId}" to ${target}.`));
19223
20010
  }
19224
- config2.skills[persona].push(skillId);
19225
20011
  saveProjectConfig(project.marvinDir, config2);
19226
- console.log(chalk10.green(`Assigned skill "${skillId}" to ${persona}.`));
19227
20012
  }
19228
20013
  async function skillsRemoveCommand(skillId, options) {
19229
20014
  const project = loadProject();
@@ -19232,25 +20017,28 @@ async function skillsRemoveCommand(skillId, options) {
19232
20017
  console.log(chalk10.red("Please specify a persona with --as <persona>."));
19233
20018
  return;
19234
20019
  }
20020
+ const targets = persona === "all" ? listPersonas().map((p) => p.id) : [persona];
19235
20021
  const config2 = loadProjectConfig(project.marvinDir);
19236
- if (!config2.skills?.[persona]) {
19237
- console.log(chalk10.yellow(`No skills configured for ${persona}.`));
19238
- return;
19239
- }
19240
- const idx = config2.skills[persona].indexOf(skillId);
19241
- if (idx === -1) {
19242
- console.log(chalk10.yellow(`Skill "${skillId}" is not assigned to ${persona}.`));
19243
- return;
19244
- }
19245
- config2.skills[persona].splice(idx, 1);
19246
- if (config2.skills[persona].length === 0) {
19247
- delete config2.skills[persona];
20022
+ for (const target of targets) {
20023
+ if (!config2.skills?.[target]) {
20024
+ console.log(chalk10.yellow(`No skills configured for ${target}.`));
20025
+ continue;
20026
+ }
20027
+ const idx = config2.skills[target].indexOf(skillId);
20028
+ if (idx === -1) {
20029
+ console.log(chalk10.yellow(`Skill "${skillId}" is not assigned to ${target}.`));
20030
+ continue;
20031
+ }
20032
+ config2.skills[target].splice(idx, 1);
20033
+ if (config2.skills[target].length === 0) {
20034
+ delete config2.skills[target];
20035
+ }
20036
+ console.log(chalk10.green(`Removed skill "${skillId}" from ${target}.`));
19248
20037
  }
19249
- if (Object.keys(config2.skills).length === 0) {
20038
+ if (config2.skills && Object.keys(config2.skills).length === 0) {
19250
20039
  delete config2.skills;
19251
20040
  }
19252
20041
  saveProjectConfig(project.marvinDir, config2);
19253
- console.log(chalk10.green(`Removed skill "${skillId}" from ${persona}.`));
19254
20042
  }
19255
20043
  async function skillsCreateCommand(name) {
19256
20044
  const project = loadProject();
@@ -20497,12 +21285,94 @@ Contribution: ${options.type}`));
20497
21285
  });
20498
21286
  }
20499
21287
 
21288
+ // src/reports/gar/render-ascii.ts
21289
+ import chalk16 from "chalk";
21290
+ var STATUS_DOT = {
21291
+ green: chalk16.green("\u25CF"),
21292
+ amber: chalk16.yellow("\u25CF"),
21293
+ red: chalk16.red("\u25CF")
21294
+ };
21295
+ var STATUS_LABEL = {
21296
+ green: chalk16.green.bold("GREEN"),
21297
+ amber: chalk16.yellow.bold("AMBER"),
21298
+ red: chalk16.red.bold("RED")
21299
+ };
21300
+ var SEPARATOR = chalk16.dim("\u2500".repeat(60));
21301
+ function renderAscii(report) {
21302
+ const lines = [];
21303
+ lines.push("");
21304
+ lines.push(chalk16.bold(` GAR Report \xB7 ${report.projectName}`));
21305
+ lines.push(chalk16.dim(` ${report.generatedAt}`));
21306
+ lines.push("");
21307
+ lines.push(` Overall: ${STATUS_LABEL[report.overall]}`);
21308
+ lines.push("");
21309
+ lines.push(` ${SEPARATOR}`);
21310
+ for (const area of report.areas) {
21311
+ lines.push(` ${STATUS_DOT[area.status]} ${chalk16.bold(area.name.padEnd(12))} ${area.summary}`);
21312
+ for (const item of area.items) {
21313
+ lines.push(` ${chalk16.dim("\u2514")} ${item.id} ${item.title}`);
21314
+ }
21315
+ }
21316
+ lines.push(` ${SEPARATOR}`);
21317
+ lines.push("");
21318
+ return lines.join("\n");
21319
+ }
21320
+
21321
+ // src/reports/gar/render-confluence.ts
21322
+ var EMOJI = {
21323
+ green: ":green_circle:",
21324
+ amber: ":yellow_circle:",
21325
+ red: ":red_circle:"
21326
+ };
21327
+ function renderConfluence(report) {
21328
+ const lines = [];
21329
+ lines.push(`# GAR Report \u2014 ${report.projectName}`);
21330
+ lines.push("");
21331
+ lines.push(`**Date:** ${report.generatedAt}`);
21332
+ lines.push(`**Overall:** ${EMOJI[report.overall]} ${report.overall.toUpperCase()}`);
21333
+ lines.push("");
21334
+ lines.push("| Area | Status | Summary |");
21335
+ lines.push("|------|--------|---------|");
21336
+ for (const area of report.areas) {
21337
+ lines.push(
21338
+ `| ${area.name} | ${EMOJI[area.status]} ${area.status.toUpperCase()} | ${area.summary} |`
21339
+ );
21340
+ }
21341
+ lines.push("");
21342
+ for (const area of report.areas) {
21343
+ if (area.items.length === 0) continue;
21344
+ lines.push(`## ${area.name}`);
21345
+ lines.push("");
21346
+ for (const item of area.items) {
21347
+ lines.push(`- **${item.id}** ${item.title}`);
21348
+ }
21349
+ lines.push("");
21350
+ }
21351
+ return lines.join("\n");
21352
+ }
21353
+
21354
+ // src/cli/commands/report.ts
21355
+ async function garReportCommand(options) {
21356
+ const project = loadProject();
21357
+ const plugin = resolvePlugin(project.config.methodology);
21358
+ const registrations = plugin?.documentTypeRegistrations ?? [];
21359
+ const store = new DocumentStore(project.marvinDir, registrations);
21360
+ const metrics = collectGarMetrics(store);
21361
+ const report = evaluateGar(project.config.name, metrics);
21362
+ const format = options.format ?? "ascii";
21363
+ if (format === "confluence") {
21364
+ console.log(renderConfluence(report));
21365
+ } else {
21366
+ console.log(renderAscii(report));
21367
+ }
21368
+ }
21369
+
20500
21370
  // src/cli/program.ts
20501
21371
  function createProgram() {
20502
21372
  const program = new Command();
20503
21373
  program.name("marvin").description(
20504
21374
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
20505
- ).version("0.2.5");
21375
+ ).version("0.2.8");
20506
21376
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
20507
21377
  await initCommand();
20508
21378
  });
@@ -20572,6 +21442,13 @@ function createProgram() {
20572
21442
  skillsCmd.command("migrate").description("Migrate YAML skill files to SKILL.md directory format").action(async () => {
20573
21443
  await skillsMigrateCommand();
20574
21444
  });
21445
+ const reportCmd = program.command("report").description("Generate project reports");
21446
+ reportCmd.command("gar").description("Generate a Green/Amber/Red status report").option(
21447
+ "--format <format>",
21448
+ "Output format: ascii or confluence (default: ascii)"
21449
+ ).action(async (options) => {
21450
+ await garReportCommand(options);
21451
+ });
20575
21452
  return program;
20576
21453
  }
20577
21454
  export {