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/marvin.js CHANGED
@@ -14101,6 +14101,128 @@ function createMeetingTools(store) {
14101
14101
 
14102
14102
  // src/plugins/builtin/tools/reports.ts
14103
14103
  import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
14104
+
14105
+ // src/reports/gar/collector.ts
14106
+ function collectGarMetrics(store) {
14107
+ const allActions = store.list({ type: "action" });
14108
+ const openActions = allActions.filter((d) => d.frontmatter.status === "open");
14109
+ const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
14110
+ const allDocs = store.list();
14111
+ const blockedItems = allDocs.filter(
14112
+ (d) => d.frontmatter.tags?.includes("blocked")
14113
+ );
14114
+ const overdueItems = allDocs.filter(
14115
+ (d) => d.frontmatter.tags?.includes("overdue")
14116
+ );
14117
+ const openQuestions = store.list({ type: "question", status: "open" });
14118
+ const riskItems = allDocs.filter(
14119
+ (d) => d.frontmatter.tags?.includes("risk")
14120
+ );
14121
+ const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
14122
+ const total = allActions.length;
14123
+ const done = doneActions.length;
14124
+ const completionPct = total > 0 ? Math.round(done / total * 100) : 100;
14125
+ const scheduleItems = [
14126
+ ...blockedItems,
14127
+ ...overdueItems
14128
+ ].filter(
14129
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
14130
+ ).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
14131
+ const qualityItems = [
14132
+ ...riskItems,
14133
+ ...openQuestions
14134
+ ].filter(
14135
+ (d, i, arr) => arr.findIndex((x) => x.frontmatter.id === d.frontmatter.id) === i
14136
+ ).map((d) => ({ id: d.frontmatter.id, title: d.frontmatter.title }));
14137
+ const resourceItems = unownedActions.map((d) => ({
14138
+ id: d.frontmatter.id,
14139
+ title: d.frontmatter.title
14140
+ }));
14141
+ return {
14142
+ scope: {
14143
+ total,
14144
+ open: openActions.length,
14145
+ done,
14146
+ completionPct
14147
+ },
14148
+ schedule: {
14149
+ blocked: blockedItems.length,
14150
+ overdue: overdueItems.length,
14151
+ items: scheduleItems
14152
+ },
14153
+ quality: {
14154
+ risks: riskItems.length,
14155
+ openQuestions: openQuestions.length,
14156
+ items: qualityItems
14157
+ },
14158
+ resources: {
14159
+ unowned: unownedActions.length,
14160
+ items: resourceItems
14161
+ }
14162
+ };
14163
+ }
14164
+
14165
+ // src/reports/gar/evaluator.ts
14166
+ function worstStatus(statuses) {
14167
+ if (statuses.includes("red")) return "red";
14168
+ if (statuses.includes("amber")) return "amber";
14169
+ return "green";
14170
+ }
14171
+ function evaluateGar(projectName, metrics) {
14172
+ const areas = [];
14173
+ const scopePct = metrics.scope.completionPct;
14174
+ const scopeStatus = scopePct >= 70 ? "green" : scopePct >= 40 ? "amber" : "red";
14175
+ areas.push({
14176
+ name: "Scope",
14177
+ status: scopeStatus,
14178
+ summary: `${scopePct}% complete (${metrics.scope.done}/${metrics.scope.total})`,
14179
+ items: []
14180
+ });
14181
+ const scheduleCount = metrics.schedule.blocked + metrics.schedule.overdue;
14182
+ const scheduleStatus = scheduleCount === 0 ? "green" : scheduleCount <= 2 ? "amber" : "red";
14183
+ const scheduleParts = [];
14184
+ if (metrics.schedule.blocked > 0)
14185
+ scheduleParts.push(`${metrics.schedule.blocked} blocked`);
14186
+ if (metrics.schedule.overdue > 0)
14187
+ scheduleParts.push(`${metrics.schedule.overdue} overdue`);
14188
+ areas.push({
14189
+ name: "Schedule",
14190
+ status: scheduleStatus,
14191
+ summary: scheduleParts.length > 0 ? scheduleParts.join(", ") : "on track",
14192
+ items: metrics.schedule.items
14193
+ });
14194
+ const qualityCount = metrics.quality.risks + metrics.quality.openQuestions;
14195
+ const qualityStatus = qualityCount === 0 ? "green" : qualityCount <= 2 ? "amber" : "red";
14196
+ const qualityParts = [];
14197
+ if (metrics.quality.risks > 0)
14198
+ qualityParts.push(`${metrics.quality.risks} risk(s)`);
14199
+ if (metrics.quality.openQuestions > 0)
14200
+ qualityParts.push(`${metrics.quality.openQuestions} open question(s)`);
14201
+ areas.push({
14202
+ name: "Quality",
14203
+ status: qualityStatus,
14204
+ summary: qualityParts.length > 0 ? qualityParts.join(", ") : "no issues",
14205
+ items: metrics.quality.items
14206
+ });
14207
+ const resourceCount = metrics.resources.unowned;
14208
+ const resourceStatus = resourceCount === 0 ? "green" : resourceCount <= 2 ? "amber" : "red";
14209
+ areas.push({
14210
+ name: "Resources",
14211
+ status: resourceStatus,
14212
+ summary: resourceCount > 0 ? `${resourceCount} unowned action(s)` : "all assigned",
14213
+ items: metrics.resources.items
14214
+ });
14215
+ const overall = worstStatus(areas.map((a) => a.status));
14216
+ return {
14217
+ projectName,
14218
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
14219
+ overall,
14220
+ areas,
14221
+ metrics
14222
+ };
14223
+ }
14224
+
14225
+ // src/plugins/builtin/tools/reports.ts
14104
14226
  function createReportTools(store) {
14105
14227
  return [
14106
14228
  tool2(
@@ -14189,41 +14311,10 @@ function createReportTools(store) {
14189
14311
  "Generate a Green-Amber-Red report with metrics across scope, schedule, quality, and resources",
14190
14312
  {},
14191
14313
  async () => {
14192
- const allActions = store.list({ type: "action" });
14193
- const openActions = allActions.filter((d) => d.frontmatter.status === "open");
14194
- const doneActions = allActions.filter((d) => d.frontmatter.status === "done");
14195
- const allDocs = store.list();
14196
- const blockedItems = allDocs.filter(
14197
- (d) => d.frontmatter.tags?.includes("blocked")
14198
- );
14199
- const overdueItems = allDocs.filter(
14200
- (d) => d.frontmatter.tags?.includes("overdue")
14201
- );
14202
- const openQuestions = store.list({ type: "question", status: "open" });
14203
- const riskItems = allDocs.filter(
14204
- (d) => d.frontmatter.tags?.includes("risk")
14205
- );
14206
- const unownedActions = openActions.filter((d) => !d.frontmatter.owner);
14207
- const areas = {
14208
- scope: {
14209
- total: allActions.length,
14210
- open: openActions.length,
14211
- done: doneActions.length
14212
- },
14213
- schedule: {
14214
- blocked: blockedItems.length,
14215
- overdue: overdueItems.length
14216
- },
14217
- quality: {
14218
- openQuestions: openQuestions.length,
14219
- risks: riskItems.length
14220
- },
14221
- resources: {
14222
- unowned: unownedActions.length
14223
- }
14224
- };
14314
+ const metrics = collectGarMetrics(store);
14315
+ const report = evaluateGar("project", metrics);
14225
14316
  return {
14226
- content: [{ type: "text", text: JSON.stringify({ areas }, null, 2) }]
14317
+ content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
14227
14318
  };
14228
14319
  },
14229
14320
  { annotations: { readOnly: true } }
@@ -15003,6 +15094,188 @@ function createSprintTools(store) {
15003
15094
  ];
15004
15095
  }
15005
15096
 
15097
+ // src/plugins/builtin/tools/sprint-planning.ts
15098
+ import { tool as tool7 } from "@anthropic-ai/claude-agent-sdk";
15099
+ var PRIORITY_ORDER = {
15100
+ critical: 0,
15101
+ high: 1,
15102
+ medium: 2,
15103
+ low: 3
15104
+ };
15105
+ function priorityRank(p) {
15106
+ return PRIORITY_ORDER[p ?? ""] ?? 99;
15107
+ }
15108
+ function createSprintPlanningTools(store) {
15109
+ return [
15110
+ tool7(
15111
+ "gather_sprint_planning_context",
15112
+ "Aggregate all planning-relevant data for proposing the next sprint: approved features, backlog epics, active sprint, velocity reference, blockers, and summary stats",
15113
+ {
15114
+ focusFeature: external_exports.string().optional().describe("Filter backlog to epics of a specific feature ID (e.g. 'F-001')"),
15115
+ sprintDurationDays: external_exports.number().optional().describe("Expected sprint duration in days \u2014 passed through for capacity reasoning")
15116
+ },
15117
+ async (args) => {
15118
+ const features = store.list({ type: "feature" });
15119
+ const epics = store.list({ type: "epic" });
15120
+ const sprints = store.list({ type: "sprint" });
15121
+ const questions = store.list({ type: "question", status: "open" });
15122
+ const contributions = store.list({ type: "contribution" });
15123
+ const approvedFeatures = features.filter((f) => f.frontmatter.status === "approved").sort((a, b) => priorityRank(a.frontmatter.priority) - priorityRank(b.frontmatter.priority)).map((f) => {
15124
+ const linkedEpics = epics.filter((e) => e.frontmatter.linkedFeature === f.frontmatter.id);
15125
+ const epicsByStatus = {};
15126
+ for (const e of linkedEpics) {
15127
+ epicsByStatus[e.frontmatter.status] = (epicsByStatus[e.frontmatter.status] ?? 0) + 1;
15128
+ }
15129
+ return {
15130
+ id: f.frontmatter.id,
15131
+ title: f.frontmatter.title,
15132
+ priority: f.frontmatter.priority,
15133
+ owner: f.frontmatter.owner,
15134
+ epicCount: linkedEpics.length,
15135
+ epicsByStatus
15136
+ };
15137
+ });
15138
+ const assignedEpicIds = /* @__PURE__ */ new Set();
15139
+ for (const sp of sprints) {
15140
+ const linked = sp.frontmatter.linkedEpics ?? [];
15141
+ for (const id of linked) assignedEpicIds.add(id);
15142
+ }
15143
+ const featureMap = new Map(features.map((f) => [f.frontmatter.id, f]));
15144
+ let backlogEpics = epics.filter(
15145
+ (e) => !assignedEpicIds.has(e.frontmatter.id) && e.frontmatter.status !== "done"
15146
+ );
15147
+ if (args.focusFeature) {
15148
+ backlogEpics = backlogEpics.filter(
15149
+ (e) => e.frontmatter.linkedFeature === args.focusFeature
15150
+ );
15151
+ }
15152
+ const backlog = backlogEpics.sort((a, b) => {
15153
+ const fa = featureMap.get(a.frontmatter.linkedFeature);
15154
+ const fb = featureMap.get(b.frontmatter.linkedFeature);
15155
+ return priorityRank(fa?.frontmatter.priority) - priorityRank(fb?.frontmatter.priority);
15156
+ }).map((e) => {
15157
+ const parent = featureMap.get(e.frontmatter.linkedFeature);
15158
+ return {
15159
+ id: e.frontmatter.id,
15160
+ title: e.frontmatter.title,
15161
+ status: e.frontmatter.status,
15162
+ linkedFeature: e.frontmatter.linkedFeature,
15163
+ featureTitle: parent?.frontmatter.title ?? null,
15164
+ featurePriority: parent?.frontmatter.priority ?? null,
15165
+ estimatedEffort: e.frontmatter.estimatedEffort ?? null,
15166
+ targetDate: e.frontmatter.targetDate ?? null
15167
+ };
15168
+ });
15169
+ const activeSprintDoc = sprints.find((s) => s.frontmatter.status === "active") ?? null;
15170
+ let activeSprint = null;
15171
+ if (activeSprintDoc) {
15172
+ const linkedEpicIds = activeSprintDoc.frontmatter.linkedEpics ?? [];
15173
+ const linkedEpics = linkedEpicIds.map((epicId) => {
15174
+ const epic = store.get(epicId);
15175
+ return epic ? { id: epicId, title: epic.frontmatter.title, status: epic.frontmatter.status } : { id: epicId, title: "(not found)", status: "unknown" };
15176
+ });
15177
+ const allDocs = store.list();
15178
+ const sprintTag = `sprint:${activeSprintDoc.frontmatter.id}`;
15179
+ const workItems = allDocs.filter(
15180
+ (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(sprintTag)
15181
+ );
15182
+ const doneCount = workItems.filter(
15183
+ (d) => d.frontmatter.status === "done" || d.frontmatter.status === "resolved" || d.frontmatter.status === "closed"
15184
+ ).length;
15185
+ const completionPct = workItems.length > 0 ? Math.round(doneCount / workItems.length * 100) : 0;
15186
+ activeSprint = {
15187
+ id: activeSprintDoc.frontmatter.id,
15188
+ title: activeSprintDoc.frontmatter.title,
15189
+ goal: activeSprintDoc.frontmatter.goal,
15190
+ startDate: activeSprintDoc.frontmatter.startDate,
15191
+ endDate: activeSprintDoc.frontmatter.endDate,
15192
+ linkedEpics,
15193
+ workItems: { total: workItems.length, done: doneCount, completionPct }
15194
+ };
15195
+ }
15196
+ const completedSprints = sprints.filter((s) => s.frontmatter.status === "completed").sort((a, b) => {
15197
+ const da = a.frontmatter.endDate ?? "";
15198
+ const db = b.frontmatter.endDate ?? "";
15199
+ return da < db ? 1 : da > db ? -1 : 0;
15200
+ }).slice(0, 2);
15201
+ const velocityReference = completedSprints.map((sp) => {
15202
+ const linkedEpicIds = sp.frontmatter.linkedEpics ?? [];
15203
+ const efforts = [];
15204
+ for (const epicId of linkedEpicIds) {
15205
+ const epic = store.get(epicId);
15206
+ if (epic?.frontmatter.estimatedEffort) {
15207
+ efforts.push(String(epic.frontmatter.estimatedEffort));
15208
+ }
15209
+ }
15210
+ const allDocs = store.list();
15211
+ const sprintTag = `sprint:${sp.frontmatter.id}`;
15212
+ const workItems = allDocs.filter(
15213
+ (d) => d.frontmatter.type !== "sprint" && d.frontmatter.type !== "epic" && d.frontmatter.tags?.includes(sprintTag)
15214
+ );
15215
+ return {
15216
+ id: sp.frontmatter.id,
15217
+ title: sp.frontmatter.title,
15218
+ startDate: sp.frontmatter.startDate,
15219
+ endDate: sp.frontmatter.endDate,
15220
+ epicCount: linkedEpicIds.length,
15221
+ efforts,
15222
+ workItemCount: workItems.length
15223
+ };
15224
+ });
15225
+ const openBlockerContributions = contributions.filter(
15226
+ (c) => c.frontmatter.status === "open" && (c.frontmatter.contributionType === "risk-finding" || c.frontmatter.contributionType === "blocker-report")
15227
+ );
15228
+ const blockers = {
15229
+ openQuestions: questions.map((q) => ({
15230
+ id: q.frontmatter.id,
15231
+ title: q.frontmatter.title
15232
+ })),
15233
+ openRiskAndBlockerContributions: openBlockerContributions.map((c) => ({
15234
+ id: c.frontmatter.id,
15235
+ title: c.frontmatter.title,
15236
+ contributionType: c.frontmatter.contributionType
15237
+ }))
15238
+ };
15239
+ const totalBacklogEfforts = backlog.filter((e) => e.estimatedEffort !== null).map((e) => String(e.estimatedEffort));
15240
+ const approvedFeaturesWithNoEpics = approvedFeatures.filter((f) => f.epicCount === 0).map((f) => ({ id: f.id, title: f.title }));
15241
+ const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15242
+ const epicsAtRisk = epics.filter((e) => {
15243
+ if (e.frontmatter.status === "done") return false;
15244
+ if (e.frontmatter.targetDate && e.frontmatter.targetDate < now) return true;
15245
+ const parent = featureMap.get(e.frontmatter.linkedFeature);
15246
+ if (parent?.frontmatter.status === "deferred") return true;
15247
+ return false;
15248
+ }).map((e) => ({
15249
+ id: e.frontmatter.id,
15250
+ title: e.frontmatter.title,
15251
+ reason: e.frontmatter.targetDate && e.frontmatter.targetDate < now ? "past-target-date" : "deferred-feature"
15252
+ }));
15253
+ const plannedSprintCount = sprints.filter((s) => s.frontmatter.status === "planned").length;
15254
+ const summary = {
15255
+ totalBacklogEpics: backlog.length,
15256
+ totalBacklogEfforts,
15257
+ approvedFeaturesWithNoEpics,
15258
+ epicsAtRisk,
15259
+ plannedSprintCount
15260
+ };
15261
+ const result = {
15262
+ approvedFeatures,
15263
+ backlog,
15264
+ activeSprint,
15265
+ velocityReference,
15266
+ blockers,
15267
+ summary,
15268
+ ...args.sprintDurationDays !== void 0 ? { sprintDurationDays: args.sprintDurationDays } : {}
15269
+ };
15270
+ return {
15271
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
15272
+ };
15273
+ },
15274
+ { annotations: { readOnly: true } }
15275
+ )
15276
+ ];
15277
+ }
15278
+
15006
15279
  // src/plugins/common.ts
15007
15280
  var COMMON_REGISTRATIONS = [
15008
15281
  { type: "meeting", dirName: "meetings", idPrefix: "M" },
@@ -15019,7 +15292,8 @@ function createCommonTools(store) {
15019
15292
  ...createFeatureTools(store),
15020
15293
  ...createEpicTools(store),
15021
15294
  ...createContributionTools(store),
15022
- ...createSprintTools(store)
15295
+ ...createSprintTools(store),
15296
+ ...createSprintPlanningTools(store)
15023
15297
  ];
15024
15298
  }
15025
15299
 
@@ -15092,7 +15366,12 @@ var genericAgilePlugin = {
15092
15366
  - **list_sprints** / **get_sprint**: View sprints to understand iteration scope and delivery dates.
15093
15367
  - **update_sprint**: Assign epics to sprints by updating linkedEpics when breaking features into work.
15094
15368
  - Tag technical actions and decisions with \`sprint:SP-xxx\` to associate them with a sprint.
15095
- - Use **generate_sprint_progress** to track technical work completion within an iteration.`,
15369
+ - Use **generate_sprint_progress** to track technical work completion within an iteration.
15370
+
15371
+ **Sprint Planning:**
15372
+ - When asked to plan or propose a sprint, ALWAYS call **gather_sprint_planning_context** first.
15373
+ - Focus on: technical readiness of each epic, open technical questions or spikes, effort balance across the sprint, and feature coverage.
15374
+ - Present a structured sprint proposal with technical rationale for each selected epic, known technical risks, and any prerequisite work that should be completed first.`,
15096
15375
  "delivery-manager": `You track delivery across features and epics, manage schedules, and report on progress.
15097
15376
 
15098
15377
  **Report Tools:**
@@ -15140,7 +15419,13 @@ var genericAgilePlugin = {
15140
15419
  - Assign epics to sprints via linkedEpics.
15141
15420
  - Tag work items (actions, decisions, questions) with \`sprint:SP-xxx\` for sprint scoping.
15142
15421
  - Track delivery dates and flag at-risk sprints.
15143
- - Register past/completed sprints for historical tracking.`,
15422
+ - Register past/completed sprints for historical tracking.
15423
+
15424
+ **Sprint Planning:**
15425
+ - 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.
15426
+ - Reason through: priority (critical/high features first), capacity (compare backlog effort to velocity reference), dependencies and blockers, balance across features, and risk.
15427
+ - Present a structured sprint proposal: title, goal, suggested dates, selected epics with rationale for each, excluded epics with reason, and identified risks.
15428
+ - After user confirmation, use **create_sprint** with the agreed epics to persist the sprint.`,
15144
15429
  "*": `You have access to feature, epic, sprint, and meeting tools for project coordination:
15145
15430
 
15146
15431
  **Features** (F-xxx): Product capabilities defined by the Product Owner. Features progress through draft \u2192 approved \u2192 done.
@@ -15163,10 +15448,10 @@ var genericAgilePlugin = {
15163
15448
  };
15164
15449
 
15165
15450
  // src/plugins/builtin/tools/use-cases.ts
15166
- import { tool as tool7 } from "@anthropic-ai/claude-agent-sdk";
15451
+ import { tool as tool8 } from "@anthropic-ai/claude-agent-sdk";
15167
15452
  function createUseCaseTools(store) {
15168
15453
  return [
15169
- tool7(
15454
+ tool8(
15170
15455
  "list_use_cases",
15171
15456
  "List all extension use cases, optionally filtered by status or extension type",
15172
15457
  {
@@ -15196,7 +15481,7 @@ function createUseCaseTools(store) {
15196
15481
  },
15197
15482
  { annotations: { readOnly: true } }
15198
15483
  ),
15199
- tool7(
15484
+ tool8(
15200
15485
  "get_use_case",
15201
15486
  "Get the full content of a specific use case by ID",
15202
15487
  { id: external_exports.string().describe("Use case ID (e.g. 'UC-001')") },
@@ -15223,7 +15508,7 @@ function createUseCaseTools(store) {
15223
15508
  },
15224
15509
  { annotations: { readOnly: true } }
15225
15510
  ),
15226
- tool7(
15511
+ tool8(
15227
15512
  "create_use_case",
15228
15513
  "Create a new extension use case definition (Phase 1: Assess Extension Use Case)",
15229
15514
  {
@@ -15257,7 +15542,7 @@ function createUseCaseTools(store) {
15257
15542
  };
15258
15543
  }
15259
15544
  ),
15260
- tool7(
15545
+ tool8(
15261
15546
  "update_use_case",
15262
15547
  "Update an existing extension use case",
15263
15548
  {
@@ -15287,10 +15572,10 @@ function createUseCaseTools(store) {
15287
15572
  }
15288
15573
 
15289
15574
  // src/plugins/builtin/tools/tech-assessments.ts
15290
- import { tool as tool8 } from "@anthropic-ai/claude-agent-sdk";
15575
+ import { tool as tool9 } from "@anthropic-ai/claude-agent-sdk";
15291
15576
  function createTechAssessmentTools(store) {
15292
15577
  return [
15293
- tool8(
15578
+ tool9(
15294
15579
  "list_tech_assessments",
15295
15580
  "List all technology assessments, optionally filtered by status",
15296
15581
  {
@@ -15321,7 +15606,7 @@ function createTechAssessmentTools(store) {
15321
15606
  },
15322
15607
  { annotations: { readOnly: true } }
15323
15608
  ),
15324
- tool8(
15609
+ tool9(
15325
15610
  "get_tech_assessment",
15326
15611
  "Get the full content of a specific technology assessment by ID",
15327
15612
  { id: external_exports.string().describe("Tech assessment ID (e.g. 'TA-001')") },
@@ -15348,7 +15633,7 @@ function createTechAssessmentTools(store) {
15348
15633
  },
15349
15634
  { annotations: { readOnly: true } }
15350
15635
  ),
15351
- tool8(
15636
+ tool9(
15352
15637
  "create_tech_assessment",
15353
15638
  "Create a new technology assessment linked to an assessed/approved use case (Phase 2: Assess Extension Technology)",
15354
15639
  {
@@ -15419,7 +15704,7 @@ function createTechAssessmentTools(store) {
15419
15704
  };
15420
15705
  }
15421
15706
  ),
15422
- tool8(
15707
+ tool9(
15423
15708
  "update_tech_assessment",
15424
15709
  "Update an existing technology assessment. The linked use case cannot be changed.",
15425
15710
  {
@@ -15449,10 +15734,10 @@ function createTechAssessmentTools(store) {
15449
15734
  }
15450
15735
 
15451
15736
  // src/plugins/builtin/tools/extension-designs.ts
15452
- import { tool as tool9 } from "@anthropic-ai/claude-agent-sdk";
15737
+ import { tool as tool10 } from "@anthropic-ai/claude-agent-sdk";
15453
15738
  function createExtensionDesignTools(store) {
15454
15739
  return [
15455
- tool9(
15740
+ tool10(
15456
15741
  "list_extension_designs",
15457
15742
  "List all extension designs, optionally filtered by status",
15458
15743
  {
@@ -15482,7 +15767,7 @@ function createExtensionDesignTools(store) {
15482
15767
  },
15483
15768
  { annotations: { readOnly: true } }
15484
15769
  ),
15485
- tool9(
15770
+ tool10(
15486
15771
  "get_extension_design",
15487
15772
  "Get the full content of a specific extension design by ID",
15488
15773
  { id: external_exports.string().describe("Extension design ID (e.g. 'XD-001')") },
@@ -15509,7 +15794,7 @@ function createExtensionDesignTools(store) {
15509
15794
  },
15510
15795
  { annotations: { readOnly: true } }
15511
15796
  ),
15512
- tool9(
15797
+ tool10(
15513
15798
  "create_extension_design",
15514
15799
  "Create a new extension design linked to a recommended tech assessment (Phase 3: Define Extension Target Solution)",
15515
15800
  {
@@ -15577,7 +15862,7 @@ function createExtensionDesignTools(store) {
15577
15862
  };
15578
15863
  }
15579
15864
  ),
15580
- tool9(
15865
+ tool10(
15581
15866
  "update_extension_design",
15582
15867
  "Update an existing extension design. The linked tech assessment cannot be changed.",
15583
15868
  {
@@ -15606,10 +15891,10 @@ function createExtensionDesignTools(store) {
15606
15891
  }
15607
15892
 
15608
15893
  // src/plugins/builtin/tools/aem-reports.ts
15609
- import { tool as tool10 } from "@anthropic-ai/claude-agent-sdk";
15894
+ import { tool as tool11 } from "@anthropic-ai/claude-agent-sdk";
15610
15895
  function createAemReportTools(store) {
15611
15896
  return [
15612
- tool10(
15897
+ tool11(
15613
15898
  "generate_extension_portfolio",
15614
15899
  "Generate a portfolio view of all use cases with their linked tech assessments and extension designs",
15615
15900
  {},
@@ -15661,7 +15946,7 @@ function createAemReportTools(store) {
15661
15946
  },
15662
15947
  { annotations: { readOnly: true } }
15663
15948
  ),
15664
- tool10(
15949
+ tool11(
15665
15950
  "generate_tech_readiness",
15666
15951
  "Generate a BTP technology readiness report showing service coverage and gaps across assessments",
15667
15952
  {},
@@ -15713,7 +15998,7 @@ function createAemReportTools(store) {
15713
15998
  },
15714
15999
  { annotations: { readOnly: true } }
15715
16000
  ),
15716
- tool10(
16001
+ tool11(
15717
16002
  "generate_phase_status",
15718
16003
  "Generate a phase progress report showing artifact counts and readiness per AEM phase",
15719
16004
  {},
@@ -15775,11 +16060,11 @@ function createAemReportTools(store) {
15775
16060
  import * as fs3 from "fs";
15776
16061
  import * as path3 from "path";
15777
16062
  import * as YAML2 from "yaml";
15778
- import { tool as tool11 } from "@anthropic-ai/claude-agent-sdk";
16063
+ import { tool as tool12 } from "@anthropic-ai/claude-agent-sdk";
15779
16064
  var PHASES = ["assess-use-case", "assess-technology", "define-solution"];
15780
16065
  function createAemPhaseTools(store, marvinDir) {
15781
16066
  return [
15782
- tool11(
16067
+ tool12(
15783
16068
  "get_current_phase",
15784
16069
  "Get the current AEM phase from project configuration",
15785
16070
  {},
@@ -15800,7 +16085,7 @@ function createAemPhaseTools(store, marvinDir) {
15800
16085
  },
15801
16086
  { annotations: { readOnly: true } }
15802
16087
  ),
15803
- tool11(
16088
+ tool12(
15804
16089
  "advance_phase",
15805
16090
  "Advance to the next AEM phase. Performs soft gate checks and warns if artifacts are incomplete, but does not block.",
15806
16091
  {
@@ -16668,10 +16953,10 @@ import {
16668
16953
  } from "@anthropic-ai/claude-agent-sdk";
16669
16954
 
16670
16955
  // src/agent/tools/decisions.ts
16671
- import { tool as tool12 } from "@anthropic-ai/claude-agent-sdk";
16956
+ import { tool as tool13 } from "@anthropic-ai/claude-agent-sdk";
16672
16957
  function createDecisionTools(store) {
16673
16958
  return [
16674
- tool12(
16959
+ tool13(
16675
16960
  "list_decisions",
16676
16961
  "List all decisions in the project, optionally filtered by status",
16677
16962
  { status: external_exports.string().optional().describe("Filter by status (e.g. 'open', 'decided', 'superseded')") },
@@ -16689,7 +16974,7 @@ function createDecisionTools(store) {
16689
16974
  },
16690
16975
  { annotations: { readOnly: true } }
16691
16976
  ),
16692
- tool12(
16977
+ tool13(
16693
16978
  "get_decision",
16694
16979
  "Get the full content of a specific decision by ID",
16695
16980
  { id: external_exports.string().describe("Decision ID (e.g. 'D-001')") },
@@ -16716,7 +17001,7 @@ function createDecisionTools(store) {
16716
17001
  },
16717
17002
  { annotations: { readOnly: true } }
16718
17003
  ),
16719
- tool12(
17004
+ tool13(
16720
17005
  "create_decision",
16721
17006
  "Create a new decision record",
16722
17007
  {
@@ -16747,7 +17032,7 @@ function createDecisionTools(store) {
16747
17032
  };
16748
17033
  }
16749
17034
  ),
16750
- tool12(
17035
+ tool13(
16751
17036
  "update_decision",
16752
17037
  "Update an existing decision",
16753
17038
  {
@@ -16774,10 +17059,10 @@ function createDecisionTools(store) {
16774
17059
  }
16775
17060
 
16776
17061
  // src/agent/tools/actions.ts
16777
- import { tool as tool13 } from "@anthropic-ai/claude-agent-sdk";
17062
+ import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
16778
17063
  function createActionTools(store) {
16779
17064
  return [
16780
- tool13(
17065
+ tool14(
16781
17066
  "list_actions",
16782
17067
  "List all action items in the project, optionally filtered by status or owner",
16783
17068
  {
@@ -16804,7 +17089,7 @@ function createActionTools(store) {
16804
17089
  },
16805
17090
  { annotations: { readOnly: true } }
16806
17091
  ),
16807
- tool13(
17092
+ tool14(
16808
17093
  "get_action",
16809
17094
  "Get the full content of a specific action item by ID",
16810
17095
  { id: external_exports.string().describe("Action ID (e.g. 'A-001')") },
@@ -16831,7 +17116,7 @@ function createActionTools(store) {
16831
17116
  },
16832
17117
  { annotations: { readOnly: true } }
16833
17118
  ),
16834
- tool13(
17119
+ tool14(
16835
17120
  "create_action",
16836
17121
  "Create a new action item",
16837
17122
  {
@@ -16864,7 +17149,7 @@ function createActionTools(store) {
16864
17149
  };
16865
17150
  }
16866
17151
  ),
16867
- tool13(
17152
+ tool14(
16868
17153
  "update_action",
16869
17154
  "Update an existing action item",
16870
17155
  {
@@ -16892,10 +17177,10 @@ function createActionTools(store) {
16892
17177
  }
16893
17178
 
16894
17179
  // src/agent/tools/questions.ts
16895
- import { tool as tool14 } from "@anthropic-ai/claude-agent-sdk";
17180
+ import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
16896
17181
  function createQuestionTools(store) {
16897
17182
  return [
16898
- tool14(
17183
+ tool15(
16899
17184
  "list_questions",
16900
17185
  "List all questions in the project, optionally filtered by status",
16901
17186
  {
@@ -16916,7 +17201,7 @@ function createQuestionTools(store) {
16916
17201
  },
16917
17202
  { annotations: { readOnly: true } }
16918
17203
  ),
16919
- tool14(
17204
+ tool15(
16920
17205
  "get_question",
16921
17206
  "Get the full content of a specific question by ID",
16922
17207
  { id: external_exports.string().describe("Question ID (e.g. 'Q-001')") },
@@ -16943,7 +17228,7 @@ function createQuestionTools(store) {
16943
17228
  },
16944
17229
  { annotations: { readOnly: true } }
16945
17230
  ),
16946
- tool14(
17231
+ tool15(
16947
17232
  "create_question",
16948
17233
  "Create a new question that needs to be answered",
16949
17234
  {
@@ -16974,7 +17259,7 @@ function createQuestionTools(store) {
16974
17259
  };
16975
17260
  }
16976
17261
  ),
16977
- tool14(
17262
+ tool15(
16978
17263
  "update_question",
16979
17264
  "Update an existing question",
16980
17265
  {
@@ -17001,10 +17286,10 @@ function createQuestionTools(store) {
17001
17286
  }
17002
17287
 
17003
17288
  // src/agent/tools/documents.ts
17004
- import { tool as tool15 } from "@anthropic-ai/claude-agent-sdk";
17289
+ import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
17005
17290
  function createDocumentTools(store) {
17006
17291
  return [
17007
- tool15(
17292
+ tool16(
17008
17293
  "search_documents",
17009
17294
  "Search all project documents, optionally filtered by type, status, or tag",
17010
17295
  {
@@ -17036,7 +17321,7 @@ function createDocumentTools(store) {
17036
17321
  },
17037
17322
  { annotations: { readOnly: true } }
17038
17323
  ),
17039
- tool15(
17324
+ tool16(
17040
17325
  "read_document",
17041
17326
  "Read the full content of any project document by ID",
17042
17327
  { id: external_exports.string().describe("Document ID (e.g. 'D-001', 'A-003', 'Q-002')") },
@@ -17063,7 +17348,7 @@ function createDocumentTools(store) {
17063
17348
  },
17064
17349
  { annotations: { readOnly: true } }
17065
17350
  ),
17066
- tool15(
17351
+ tool16(
17067
17352
  "project_summary",
17068
17353
  "Get a summary of all project documents and their counts",
17069
17354
  {},
@@ -17095,10 +17380,10 @@ function createDocumentTools(store) {
17095
17380
  }
17096
17381
 
17097
17382
  // src/agent/tools/sources.ts
17098
- import { tool as tool16 } from "@anthropic-ai/claude-agent-sdk";
17383
+ import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
17099
17384
  function createSourceTools(manifest) {
17100
17385
  return [
17101
- tool16(
17386
+ tool17(
17102
17387
  "list_sources",
17103
17388
  "List all source documents and their processing status",
17104
17389
  {
@@ -17128,7 +17413,7 @@ function createSourceTools(manifest) {
17128
17413
  },
17129
17414
  { annotations: { readOnly: true } }
17130
17415
  ),
17131
- tool16(
17416
+ tool17(
17132
17417
  "get_source_info",
17133
17418
  "Get detailed information about a specific source document",
17134
17419
  {
@@ -17166,10 +17451,10 @@ function createSourceTools(manifest) {
17166
17451
  }
17167
17452
 
17168
17453
  // src/agent/tools/sessions.ts
17169
- import { tool as tool17 } from "@anthropic-ai/claude-agent-sdk";
17454
+ import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
17170
17455
  function createSessionTools(store) {
17171
17456
  return [
17172
- tool17(
17457
+ tool18(
17173
17458
  "list_sessions",
17174
17459
  "List all saved chat sessions, sorted by most recently used",
17175
17460
  {
@@ -17193,7 +17478,7 @@ function createSessionTools(store) {
17193
17478
  },
17194
17479
  { annotations: { readOnly: true } }
17195
17480
  ),
17196
- tool17(
17481
+ tool18(
17197
17482
  "get_session",
17198
17483
  "Get details of a specific saved session by name",
17199
17484
  { name: external_exports.string().describe("Session name (e.g. 'jwt-auth-decision')") },
@@ -17421,9 +17706,495 @@ Be thorough but concise. Focus on actionable insights.`,
17421
17706
  ]
17422
17707
  };
17423
17708
 
17709
+ // src/skills/builtin/jira/tools.ts
17710
+ import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
17711
+
17712
+ // src/skills/builtin/jira/client.ts
17713
+ var JiraClient = class {
17714
+ baseUrl;
17715
+ authHeader;
17716
+ constructor(config2) {
17717
+ this.baseUrl = `https://${config2.host}/rest/api/2`;
17718
+ this.authHeader = "Basic " + Buffer.from(`${config2.email}:${config2.apiToken}`).toString("base64");
17719
+ }
17720
+ async request(path18, method = "GET", body) {
17721
+ const url2 = `${this.baseUrl}${path18}`;
17722
+ const headers = {
17723
+ Authorization: this.authHeader,
17724
+ "Content-Type": "application/json",
17725
+ Accept: "application/json"
17726
+ };
17727
+ const response = await fetch(url2, {
17728
+ method,
17729
+ headers,
17730
+ body: body ? JSON.stringify(body) : void 0
17731
+ });
17732
+ if (!response.ok) {
17733
+ const text = await response.text().catch(() => "");
17734
+ throw new Error(
17735
+ `Jira API error ${response.status} ${method} ${path18}: ${text}`
17736
+ );
17737
+ }
17738
+ if (response.status === 204) return void 0;
17739
+ return response.json();
17740
+ }
17741
+ async searchIssues(jql, maxResults = 50) {
17742
+ const params = new URLSearchParams({
17743
+ jql,
17744
+ maxResults: String(maxResults)
17745
+ });
17746
+ return this.request(`/search?${params}`);
17747
+ }
17748
+ async getIssue(key) {
17749
+ return this.request(`/issue/${encodeURIComponent(key)}`);
17750
+ }
17751
+ async createIssue(fields) {
17752
+ return this.request("/issue", "POST", { fields });
17753
+ }
17754
+ async updateIssue(key, fields) {
17755
+ await this.request(
17756
+ `/issue/${encodeURIComponent(key)}`,
17757
+ "PUT",
17758
+ { fields }
17759
+ );
17760
+ }
17761
+ async addComment(key, body) {
17762
+ await this.request(
17763
+ `/issue/${encodeURIComponent(key)}/comment`,
17764
+ "POST",
17765
+ { body }
17766
+ );
17767
+ }
17768
+ };
17769
+ function createJiraClient() {
17770
+ const host = process.env.JIRA_HOST;
17771
+ const email3 = process.env.JIRA_EMAIL;
17772
+ const apiToken = process.env.JIRA_API_TOKEN;
17773
+ if (!host || !email3 || !apiToken) return null;
17774
+ return new JiraClient({ host, email: email3, apiToken });
17775
+ }
17776
+
17777
+ // src/skills/builtin/jira/tools.ts
17778
+ var JIRA_TYPE = "jira-issue";
17779
+ function jiraNotConfiguredError() {
17780
+ return {
17781
+ content: [
17782
+ {
17783
+ type: "text",
17784
+ text: "Jira is not configured. Set JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN environment variables."
17785
+ }
17786
+ ],
17787
+ isError: true
17788
+ };
17789
+ }
17790
+ function mapJiraStatus(jiraStatus) {
17791
+ const lower = jiraStatus.toLowerCase();
17792
+ if (lower === "done" || lower === "closed" || lower === "resolved") return "done";
17793
+ if (lower === "in progress" || lower === "in review") return "in-progress";
17794
+ return "open";
17795
+ }
17796
+ function jiraIssueToFrontmatter(issue2, host, linkedArtifacts) {
17797
+ return {
17798
+ title: issue2.fields.summary,
17799
+ status: mapJiraStatus(issue2.fields.status.name),
17800
+ jiraKey: issue2.key,
17801
+ jiraUrl: `https://${host}/browse/${issue2.key}`,
17802
+ issueType: issue2.fields.issuetype.name,
17803
+ priority: issue2.fields.priority?.name ?? "None",
17804
+ assignee: issue2.fields.assignee?.displayName ?? "",
17805
+ labels: issue2.fields.labels ?? [],
17806
+ linkedArtifacts: linkedArtifacts ?? [],
17807
+ tags: [`jira:${issue2.key}`],
17808
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
17809
+ };
17810
+ }
17811
+ function findByJiraKey(store, jiraKey) {
17812
+ const docs = store.list({ type: JIRA_TYPE });
17813
+ return docs.find((d) => d.frontmatter.jiraKey === jiraKey);
17814
+ }
17815
+ function createJiraTools(store) {
17816
+ return [
17817
+ // --- Local read tools ---
17818
+ tool19(
17819
+ "list_jira_issues",
17820
+ "List locally synced Jira issues (JI-xxx documents), optionally filtered by status or Jira key",
17821
+ {
17822
+ status: external_exports.enum(["open", "in-progress", "done"]).optional().describe("Filter by local status"),
17823
+ jiraKey: external_exports.string().optional().describe("Filter by Jira issue key (e.g. 'PROJ-123')")
17824
+ },
17825
+ async (args) => {
17826
+ let docs = store.list({ type: JIRA_TYPE, status: args.status });
17827
+ if (args.jiraKey) {
17828
+ docs = docs.filter((d) => d.frontmatter.jiraKey === args.jiraKey);
17829
+ }
17830
+ const summary = docs.map((d) => ({
17831
+ id: d.frontmatter.id,
17832
+ title: d.frontmatter.title,
17833
+ status: d.frontmatter.status,
17834
+ jiraKey: d.frontmatter.jiraKey,
17835
+ issueType: d.frontmatter.issueType,
17836
+ priority: d.frontmatter.priority,
17837
+ assignee: d.frontmatter.assignee,
17838
+ linkedArtifacts: d.frontmatter.linkedArtifacts
17839
+ }));
17840
+ return {
17841
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
17842
+ };
17843
+ },
17844
+ { annotations: { readOnly: true } }
17845
+ ),
17846
+ tool19(
17847
+ "get_jira_issue",
17848
+ "Get the full content of a locally synced Jira issue by local ID (JI-xxx) or Jira key (PROJ-123)",
17849
+ {
17850
+ id: external_exports.string().describe("Local ID (e.g. 'JI-001') or Jira key (e.g. 'PROJ-123')")
17851
+ },
17852
+ async (args) => {
17853
+ let doc = store.get(args.id);
17854
+ if (!doc) {
17855
+ doc = findByJiraKey(store, args.id);
17856
+ }
17857
+ if (!doc) {
17858
+ return {
17859
+ content: [{ type: "text", text: `Jira issue ${args.id} not found locally` }],
17860
+ isError: true
17861
+ };
17862
+ }
17863
+ return {
17864
+ content: [
17865
+ {
17866
+ type: "text",
17867
+ text: JSON.stringify(
17868
+ { ...doc.frontmatter, content: doc.content },
17869
+ null,
17870
+ 2
17871
+ )
17872
+ }
17873
+ ]
17874
+ };
17875
+ },
17876
+ { annotations: { readOnly: true } }
17877
+ ),
17878
+ // --- Jira → Local tools ---
17879
+ tool19(
17880
+ "pull_jira_issue",
17881
+ "Fetch a single Jira issue by key and create/update a local JI-xxx document",
17882
+ {
17883
+ key: external_exports.string().describe("Jira issue key (e.g. 'PROJ-123')")
17884
+ },
17885
+ async (args) => {
17886
+ const client = createJiraClient();
17887
+ if (!client) return jiraNotConfiguredError();
17888
+ const issue2 = await client.getIssue(args.key);
17889
+ const host = process.env.JIRA_HOST;
17890
+ const existing = findByJiraKey(store, args.key);
17891
+ if (existing) {
17892
+ const fm2 = jiraIssueToFrontmatter(
17893
+ issue2,
17894
+ host,
17895
+ existing.frontmatter.linkedArtifacts
17896
+ );
17897
+ const doc2 = store.update(
17898
+ existing.frontmatter.id,
17899
+ fm2,
17900
+ issue2.fields.description ?? ""
17901
+ );
17902
+ return {
17903
+ content: [
17904
+ {
17905
+ type: "text",
17906
+ text: `Updated ${doc2.frontmatter.id} from Jira ${args.key}`
17907
+ }
17908
+ ]
17909
+ };
17910
+ }
17911
+ const fm = jiraIssueToFrontmatter(issue2, host);
17912
+ const doc = store.create(
17913
+ JIRA_TYPE,
17914
+ fm,
17915
+ issue2.fields.description ?? ""
17916
+ );
17917
+ return {
17918
+ content: [
17919
+ {
17920
+ type: "text",
17921
+ text: `Created ${doc.frontmatter.id} from Jira ${args.key}`
17922
+ }
17923
+ ]
17924
+ };
17925
+ }
17926
+ ),
17927
+ tool19(
17928
+ "pull_jira_issues_jql",
17929
+ "Bulk fetch Jira issues via JQL query and create/update local JI-xxx documents",
17930
+ {
17931
+ jql: external_exports.string().describe(`JQL query (e.g. 'project = PROJ AND status = "In Progress"')`),
17932
+ maxResults: external_exports.number().optional().describe("Max issues to fetch (default 50)")
17933
+ },
17934
+ async (args) => {
17935
+ const client = createJiraClient();
17936
+ if (!client) return jiraNotConfiguredError();
17937
+ const result = await client.searchIssues(args.jql, args.maxResults);
17938
+ const host = process.env.JIRA_HOST;
17939
+ const created = [];
17940
+ const updated = [];
17941
+ for (const issue2 of result.issues) {
17942
+ const existing = findByJiraKey(store, issue2.key);
17943
+ if (existing) {
17944
+ const fm = jiraIssueToFrontmatter(
17945
+ issue2,
17946
+ host,
17947
+ existing.frontmatter.linkedArtifacts
17948
+ );
17949
+ store.update(
17950
+ existing.frontmatter.id,
17951
+ fm,
17952
+ issue2.fields.description ?? ""
17953
+ );
17954
+ updated.push(`${existing.frontmatter.id} (${issue2.key})`);
17955
+ } else {
17956
+ const fm = jiraIssueToFrontmatter(issue2, host);
17957
+ const doc = store.create(
17958
+ JIRA_TYPE,
17959
+ fm,
17960
+ issue2.fields.description ?? ""
17961
+ );
17962
+ created.push(`${doc.frontmatter.id} (${issue2.key})`);
17963
+ }
17964
+ }
17965
+ const parts = [
17966
+ `Fetched ${result.issues.length} of ${result.total} matching issues.`
17967
+ ];
17968
+ if (created.length > 0) parts.push(`Created: ${created.join(", ")}`);
17969
+ if (updated.length > 0) parts.push(`Updated: ${updated.join(", ")}`);
17970
+ return {
17971
+ content: [{ type: "text", text: parts.join("\n") }]
17972
+ };
17973
+ }
17974
+ ),
17975
+ // --- Local → Jira tools ---
17976
+ tool19(
17977
+ "push_artifact_to_jira",
17978
+ "Create a Jira issue from any Marvin artifact (D/A/Q/F/E) and create a tracking JI-xxx document",
17979
+ {
17980
+ artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'D-001', 'F-003', 'E-002')"),
17981
+ projectKey: external_exports.string().describe("Jira project key (e.g. 'PROJ')"),
17982
+ issueType: external_exports.enum(["Story", "Task", "Bug", "Epic"]).optional().describe("Jira issue type (default: 'Task')")
17983
+ },
17984
+ async (args) => {
17985
+ const client = createJiraClient();
17986
+ if (!client) return jiraNotConfiguredError();
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 description = [
17997
+ artifact.content,
17998
+ "",
17999
+ `---`,
18000
+ `Marvin artifact: ${artifact.frontmatter.id} (${artifact.frontmatter.type})`,
18001
+ `Status: ${artifact.frontmatter.status}`
18002
+ ].join("\n");
18003
+ const jiraResult = await client.createIssue({
18004
+ project: { key: args.projectKey },
18005
+ summary: artifact.frontmatter.title,
18006
+ description,
18007
+ issuetype: { name: args.issueType ?? "Task" }
18008
+ });
18009
+ const host = process.env.JIRA_HOST;
18010
+ const jiDoc = store.create(
18011
+ JIRA_TYPE,
18012
+ {
18013
+ title: artifact.frontmatter.title,
18014
+ status: "open",
18015
+ jiraKey: jiraResult.key,
18016
+ jiraUrl: `https://${host}/browse/${jiraResult.key}`,
18017
+ issueType: args.issueType ?? "Task",
18018
+ priority: "Medium",
18019
+ assignee: "",
18020
+ labels: [],
18021
+ linkedArtifacts: [args.artifactId],
18022
+ tags: [`jira:${jiraResult.key}`],
18023
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
18024
+ },
18025
+ ""
18026
+ );
18027
+ return {
18028
+ content: [
18029
+ {
18030
+ type: "text",
18031
+ text: `Created Jira ${jiraResult.key} from ${args.artifactId}. Tracking locally as ${jiDoc.frontmatter.id}.`
18032
+ }
18033
+ ]
18034
+ };
18035
+ }
18036
+ ),
18037
+ // --- Bidirectional sync ---
18038
+ tool19(
18039
+ "sync_jira_issue",
18040
+ "Bidirectional sync: push local title/description to Jira, pull latest status/assignee/labels back",
18041
+ {
18042
+ id: external_exports.string().describe("Local JI-xxx ID")
18043
+ },
18044
+ async (args) => {
18045
+ const client = createJiraClient();
18046
+ if (!client) return jiraNotConfiguredError();
18047
+ const doc = store.get(args.id);
18048
+ if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
18049
+ return {
18050
+ content: [
18051
+ { type: "text", text: `Jira issue ${args.id} not found locally` }
18052
+ ],
18053
+ isError: true
18054
+ };
18055
+ }
18056
+ const jiraKey = doc.frontmatter.jiraKey;
18057
+ await client.updateIssue(jiraKey, {
18058
+ summary: doc.frontmatter.title,
18059
+ description: doc.content || void 0
18060
+ });
18061
+ const issue2 = await client.getIssue(jiraKey);
18062
+ const host = process.env.JIRA_HOST;
18063
+ const fm = jiraIssueToFrontmatter(
18064
+ issue2,
18065
+ host,
18066
+ doc.frontmatter.linkedArtifacts
18067
+ );
18068
+ store.update(args.id, fm, issue2.fields.description ?? "");
18069
+ return {
18070
+ content: [
18071
+ {
18072
+ type: "text",
18073
+ text: `Synced ${args.id} \u2194 ${jiraKey}. Status: ${fm.status}, Assignee: ${fm.assignee || "unassigned"}`
18074
+ }
18075
+ ]
18076
+ };
18077
+ }
18078
+ ),
18079
+ // --- Local link tool ---
18080
+ tool19(
18081
+ "link_artifact_to_jira",
18082
+ "Add a Marvin artifact ID to a JI-xxx document's linkedArtifacts field",
18083
+ {
18084
+ jiraIssueId: external_exports.string().describe("Local JI-xxx ID"),
18085
+ artifactId: external_exports.string().describe("Marvin artifact ID to link (e.g. 'D-001', 'F-003')")
18086
+ },
18087
+ async (args) => {
18088
+ const doc = store.get(args.jiraIssueId);
18089
+ if (!doc || doc.frontmatter.type !== JIRA_TYPE) {
18090
+ return {
18091
+ content: [
18092
+ {
18093
+ type: "text",
18094
+ text: `Jira issue ${args.jiraIssueId} not found locally`
18095
+ }
18096
+ ],
18097
+ isError: true
18098
+ };
18099
+ }
18100
+ const artifact = store.get(args.artifactId);
18101
+ if (!artifact) {
18102
+ return {
18103
+ content: [
18104
+ { type: "text", text: `Artifact ${args.artifactId} not found` }
18105
+ ],
18106
+ isError: true
18107
+ };
18108
+ }
18109
+ const linked = doc.frontmatter.linkedArtifacts ?? [];
18110
+ if (linked.includes(args.artifactId)) {
18111
+ return {
18112
+ content: [
18113
+ {
18114
+ type: "text",
18115
+ text: `${args.artifactId} is already linked to ${args.jiraIssueId}`
18116
+ }
18117
+ ]
18118
+ };
18119
+ }
18120
+ store.update(args.jiraIssueId, {
18121
+ linkedArtifacts: [...linked, args.artifactId]
18122
+ });
18123
+ return {
18124
+ content: [
18125
+ {
18126
+ type: "text",
18127
+ text: `Linked ${args.artifactId} to ${args.jiraIssueId}`
18128
+ }
18129
+ ]
18130
+ };
18131
+ }
18132
+ )
18133
+ ];
18134
+ }
18135
+
18136
+ // src/skills/builtin/jira/index.ts
18137
+ var jiraSkill = {
18138
+ id: "jira",
18139
+ name: "Jira Integration",
18140
+ description: "Bidirectional sync between Marvin artifacts and Jira issues",
18141
+ version: "1.0.0",
18142
+ format: "builtin-ts",
18143
+ // No default persona affinity — opt-in via config.yaml skills section
18144
+ documentTypeRegistrations: [
18145
+ { type: "jira-issue", dirName: "jira-issues", idPrefix: "JI" }
18146
+ ],
18147
+ tools: (store) => createJiraTools(store),
18148
+ promptFragments: {
18149
+ "product-owner": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
18150
+
18151
+ **Available tools:**
18152
+ - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18153
+ - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18154
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, feature, etc.)
18155
+ - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18156
+ - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18157
+
18158
+ **As Product Owner, use Jira integration to:**
18159
+ - Pull stakeholder-reported issues for triage and prioritization
18160
+ - Push approved features as Stories for development tracking
18161
+ - Link decisions to Jira issues for audit trail and traceability
18162
+ - Use JQL queries to review backlog status (e.g. \`project = PROJ AND status = "To Do"\`)`,
18163
+ "tech-lead": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
18164
+
18165
+ **Available tools:**
18166
+ - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18167
+ - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18168
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, epic, etc.)
18169
+ - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18170
+ - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18171
+
18172
+ **As Tech Lead, use Jira integration to:**
18173
+ - Pull technical issues and bugs for sprint planning and estimation
18174
+ - Push epics and technical decisions to Jira for cross-team visibility
18175
+ - Bidirectional sync to keep local governance and Jira in alignment
18176
+ - Use JQL queries to track technical debt (e.g. \`labels = "tech-debt" AND status != "Done"\`)`,
18177
+ "delivery-manager": `You have the **Jira Integration** skill. You can pull issues from Jira and push Marvin artifacts to Jira.
18178
+
18179
+ **Available tools:**
18180
+ - \`list_jira_issues\` / \`get_jira_issue\` \u2014 browse locally synced Jira issues
18181
+ - \`pull_jira_issue\` / \`pull_jira_issues_jql\` \u2014 import issues from Jira by key or JQL query
18182
+ - \`push_artifact_to_jira\` \u2014 create a Jira issue from a Marvin artifact (decision, action, etc.)
18183
+ - \`sync_jira_issue\` \u2014 bidirectional sync of a local JI-xxx with Jira
18184
+ - \`link_artifact_to_jira\` \u2014 link a Marvin artifact to an existing JI-xxx
18185
+
18186
+ **As Delivery Manager, use Jira integration to:**
18187
+ - Pull sprint issues for tracking progress and blockers
18188
+ - Push actions and decisions to Jira for stakeholder visibility
18189
+ - Use JQL queries for reporting (e.g. \`sprint in openSprints() AND assignee = currentUser()\`)
18190
+ - Sync status between Marvin governance items and Jira issues`
18191
+ }
18192
+ };
18193
+
17424
18194
  // src/skills/registry.ts
17425
18195
  var BUILTIN_SKILLS = {
17426
- "governance-review": governanceReviewSkill
18196
+ "governance-review": governanceReviewSkill,
18197
+ "jira": jiraSkill
17427
18198
  };
17428
18199
  var GOVERNANCE_TOOL_NAMES = [
17429
18200
  "mcp__marvin-governance__list_decisions",
@@ -17572,6 +18343,16 @@ function resolveSkillsForPersona(personaId, skillsConfig, allSkills) {
17572
18343
  }
17573
18344
  return result;
17574
18345
  }
18346
+ function collectSkillRegistrations(skillIds, allSkills) {
18347
+ const registrations = [];
18348
+ for (const id of skillIds) {
18349
+ const skill = allSkills.get(id);
18350
+ if (skill?.documentTypeRegistrations) {
18351
+ registrations.push(...skill.documentTypeRegistrations);
18352
+ }
18353
+ }
18354
+ return registrations;
18355
+ }
17575
18356
  function getSkillTools(skillIds, allSkills, store) {
17576
18357
  const tools = [];
17577
18358
  for (const id of skillIds) {
@@ -17683,16 +18464,17 @@ ${wildcardPrompt}
17683
18464
  async function startSession(options) {
17684
18465
  const { persona, config: config2, marvinDir, projectRoot } = options;
17685
18466
  const plugin = resolvePlugin(config2.project.methodology);
17686
- const registrations = plugin?.documentTypeRegistrations ?? [];
17687
- const store = new DocumentStore(marvinDir, registrations);
18467
+ const pluginRegistrations = plugin?.documentTypeRegistrations ?? [];
18468
+ const allSkills = loadAllSkills(marvinDir);
18469
+ const skillIds = resolveSkillsForPersona(persona.id, config2.project.skills, allSkills);
18470
+ const skillRegistrations = collectSkillRegistrations(skillIds, allSkills);
18471
+ const store = new DocumentStore(marvinDir, [...pluginRegistrations, ...skillRegistrations]);
17688
18472
  const sessionStore = new SessionStore(marvinDir);
17689
18473
  const sourcesDir = path9.join(marvinDir, "sources");
17690
18474
  const hasSourcesDir = fs9.existsSync(sourcesDir);
17691
18475
  const manifest = hasSourcesDir ? new SourceManifestManager(marvinDir) : void 0;
17692
18476
  const pluginTools = plugin ? getPluginTools(plugin, store, marvinDir) : [];
17693
18477
  const pluginPromptFragment = plugin ? getPluginPromptFragment(plugin, persona.id) : void 0;
17694
- const allSkills = loadAllSkills(marvinDir);
17695
- const skillIds = resolveSkillsForPersona(persona.id, config2.project.skills, allSkills);
17696
18478
  const codeSkillTools = getSkillTools(skillIds, allSkills, store);
17697
18479
  const skillAgents = getSkillAgentDefinitions(skillIds, allSkills);
17698
18480
  const skillPromptFragment = getSkillPromptFragment(skillIds, allSkills, persona.id);
@@ -18782,7 +19564,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18782
19564
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18783
19565
 
18784
19566
  // src/skills/action-tools.ts
18785
- import { tool as tool18 } from "@anthropic-ai/claude-agent-sdk";
19567
+ import { tool as tool20 } from "@anthropic-ai/claude-agent-sdk";
18786
19568
 
18787
19569
  // src/skills/action-runner.ts
18788
19570
  import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
@@ -18848,7 +19630,7 @@ function createSkillActionTools(skills, context) {
18848
19630
  if (!skill.actions) continue;
18849
19631
  for (const action of skill.actions) {
18850
19632
  tools.push(
18851
- tool18(
19633
+ tool20(
18852
19634
  `${skill.id}__${action.id}`,
18853
19635
  action.description,
18854
19636
  {
@@ -18940,10 +19722,10 @@ ${lines.join("\n\n")}`;
18940
19722
  }
18941
19723
 
18942
19724
  // src/mcp/persona-tools.ts
18943
- import { tool as tool19 } from "@anthropic-ai/claude-agent-sdk";
19725
+ import { tool as tool21 } from "@anthropic-ai/claude-agent-sdk";
18944
19726
  function createPersonaTools(ctx, marvinDir) {
18945
19727
  return [
18946
- tool19(
19728
+ tool21(
18947
19729
  "set_persona",
18948
19730
  "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.",
18949
19731
  {
@@ -18973,7 +19755,7 @@ ${summaries}`
18973
19755
  };
18974
19756
  }
18975
19757
  ),
18976
- tool19(
19758
+ tool21(
18977
19759
  "get_persona_guidance",
18978
19760
  "Get guidance for a persona without changing the active persona. If no persona is specified, lists all available personas with summaries.",
18979
19761
  {
@@ -19204,20 +19986,23 @@ async function skillsInstallCommand(skillId, options) {
19204
19986
  console.log(chalk10.red("Please specify a persona with --as <persona>."));
19205
19987
  return;
19206
19988
  }
19989
+ const targets = persona === "all" ? listPersonas().map((p) => p.id) : [persona];
19207
19990
  const config2 = loadProjectConfig(project.marvinDir);
19208
19991
  if (!config2.skills) {
19209
19992
  config2.skills = {};
19210
19993
  }
19211
- if (!config2.skills[persona]) {
19212
- config2.skills[persona] = [];
19213
- }
19214
- if (config2.skills[persona].includes(skillId)) {
19215
- console.log(chalk10.yellow(`Skill "${skillId}" is already assigned to ${persona}.`));
19216
- return;
19994
+ for (const target of targets) {
19995
+ if (!config2.skills[target]) {
19996
+ config2.skills[target] = [];
19997
+ }
19998
+ if (config2.skills[target].includes(skillId)) {
19999
+ console.log(chalk10.yellow(`Skill "${skillId}" is already assigned to ${target}.`));
20000
+ continue;
20001
+ }
20002
+ config2.skills[target].push(skillId);
20003
+ console.log(chalk10.green(`Assigned skill "${skillId}" to ${target}.`));
19217
20004
  }
19218
- config2.skills[persona].push(skillId);
19219
20005
  saveProjectConfig(project.marvinDir, config2);
19220
- console.log(chalk10.green(`Assigned skill "${skillId}" to ${persona}.`));
19221
20006
  }
19222
20007
  async function skillsRemoveCommand(skillId, options) {
19223
20008
  const project = loadProject();
@@ -19226,25 +20011,28 @@ async function skillsRemoveCommand(skillId, options) {
19226
20011
  console.log(chalk10.red("Please specify a persona with --as <persona>."));
19227
20012
  return;
19228
20013
  }
20014
+ const targets = persona === "all" ? listPersonas().map((p) => p.id) : [persona];
19229
20015
  const config2 = loadProjectConfig(project.marvinDir);
19230
- if (!config2.skills?.[persona]) {
19231
- console.log(chalk10.yellow(`No skills configured for ${persona}.`));
19232
- return;
19233
- }
19234
- const idx = config2.skills[persona].indexOf(skillId);
19235
- if (idx === -1) {
19236
- console.log(chalk10.yellow(`Skill "${skillId}" is not assigned to ${persona}.`));
19237
- return;
19238
- }
19239
- config2.skills[persona].splice(idx, 1);
19240
- if (config2.skills[persona].length === 0) {
19241
- delete config2.skills[persona];
20016
+ for (const target of targets) {
20017
+ if (!config2.skills?.[target]) {
20018
+ console.log(chalk10.yellow(`No skills configured for ${target}.`));
20019
+ continue;
20020
+ }
20021
+ const idx = config2.skills[target].indexOf(skillId);
20022
+ if (idx === -1) {
20023
+ console.log(chalk10.yellow(`Skill "${skillId}" is not assigned to ${target}.`));
20024
+ continue;
20025
+ }
20026
+ config2.skills[target].splice(idx, 1);
20027
+ if (config2.skills[target].length === 0) {
20028
+ delete config2.skills[target];
20029
+ }
20030
+ console.log(chalk10.green(`Removed skill "${skillId}" from ${target}.`));
19242
20031
  }
19243
- if (Object.keys(config2.skills).length === 0) {
20032
+ if (config2.skills && Object.keys(config2.skills).length === 0) {
19244
20033
  delete config2.skills;
19245
20034
  }
19246
20035
  saveProjectConfig(project.marvinDir, config2);
19247
- console.log(chalk10.green(`Removed skill "${skillId}" from ${persona}.`));
19248
20036
  }
19249
20037
  async function skillsCreateCommand(name) {
19250
20038
  const project = loadProject();
@@ -20491,12 +21279,94 @@ Contribution: ${options.type}`));
20491
21279
  });
20492
21280
  }
20493
21281
 
21282
+ // src/reports/gar/render-ascii.ts
21283
+ import chalk16 from "chalk";
21284
+ var STATUS_DOT = {
21285
+ green: chalk16.green("\u25CF"),
21286
+ amber: chalk16.yellow("\u25CF"),
21287
+ red: chalk16.red("\u25CF")
21288
+ };
21289
+ var STATUS_LABEL = {
21290
+ green: chalk16.green.bold("GREEN"),
21291
+ amber: chalk16.yellow.bold("AMBER"),
21292
+ red: chalk16.red.bold("RED")
21293
+ };
21294
+ var SEPARATOR = chalk16.dim("\u2500".repeat(60));
21295
+ function renderAscii(report) {
21296
+ const lines = [];
21297
+ lines.push("");
21298
+ lines.push(chalk16.bold(` GAR Report \xB7 ${report.projectName}`));
21299
+ lines.push(chalk16.dim(` ${report.generatedAt}`));
21300
+ lines.push("");
21301
+ lines.push(` Overall: ${STATUS_LABEL[report.overall]}`);
21302
+ lines.push("");
21303
+ lines.push(` ${SEPARATOR}`);
21304
+ for (const area of report.areas) {
21305
+ lines.push(` ${STATUS_DOT[area.status]} ${chalk16.bold(area.name.padEnd(12))} ${area.summary}`);
21306
+ for (const item of area.items) {
21307
+ lines.push(` ${chalk16.dim("\u2514")} ${item.id} ${item.title}`);
21308
+ }
21309
+ }
21310
+ lines.push(` ${SEPARATOR}`);
21311
+ lines.push("");
21312
+ return lines.join("\n");
21313
+ }
21314
+
21315
+ // src/reports/gar/render-confluence.ts
21316
+ var EMOJI = {
21317
+ green: ":green_circle:",
21318
+ amber: ":yellow_circle:",
21319
+ red: ":red_circle:"
21320
+ };
21321
+ function renderConfluence(report) {
21322
+ const lines = [];
21323
+ lines.push(`# GAR Report \u2014 ${report.projectName}`);
21324
+ lines.push("");
21325
+ lines.push(`**Date:** ${report.generatedAt}`);
21326
+ lines.push(`**Overall:** ${EMOJI[report.overall]} ${report.overall.toUpperCase()}`);
21327
+ lines.push("");
21328
+ lines.push("| Area | Status | Summary |");
21329
+ lines.push("|------|--------|---------|");
21330
+ for (const area of report.areas) {
21331
+ lines.push(
21332
+ `| ${area.name} | ${EMOJI[area.status]} ${area.status.toUpperCase()} | ${area.summary} |`
21333
+ );
21334
+ }
21335
+ lines.push("");
21336
+ for (const area of report.areas) {
21337
+ if (area.items.length === 0) continue;
21338
+ lines.push(`## ${area.name}`);
21339
+ lines.push("");
21340
+ for (const item of area.items) {
21341
+ lines.push(`- **${item.id}** ${item.title}`);
21342
+ }
21343
+ lines.push("");
21344
+ }
21345
+ return lines.join("\n");
21346
+ }
21347
+
21348
+ // src/cli/commands/report.ts
21349
+ async function garReportCommand(options) {
21350
+ const project = loadProject();
21351
+ const plugin = resolvePlugin(project.config.methodology);
21352
+ const registrations = plugin?.documentTypeRegistrations ?? [];
21353
+ const store = new DocumentStore(project.marvinDir, registrations);
21354
+ const metrics = collectGarMetrics(store);
21355
+ const report = evaluateGar(project.config.name, metrics);
21356
+ const format = options.format ?? "ascii";
21357
+ if (format === "confluence") {
21358
+ console.log(renderConfluence(report));
21359
+ } else {
21360
+ console.log(renderAscii(report));
21361
+ }
21362
+ }
21363
+
20494
21364
  // src/cli/program.ts
20495
21365
  function createProgram() {
20496
21366
  const program2 = new Command();
20497
21367
  program2.name("marvin").description(
20498
21368
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
20499
- ).version("0.2.5");
21369
+ ).version("0.2.8");
20500
21370
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
20501
21371
  await initCommand();
20502
21372
  });
@@ -20566,6 +21436,13 @@ function createProgram() {
20566
21436
  skillsCmd.command("migrate").description("Migrate YAML skill files to SKILL.md directory format").action(async () => {
20567
21437
  await skillsMigrateCommand();
20568
21438
  });
21439
+ const reportCmd = program2.command("report").description("Generate project reports");
21440
+ reportCmd.command("gar").description("Generate a Green/Amber/Red status report").option(
21441
+ "--format <format>",
21442
+ "Output format: ascii or confluence (default: ascii)"
21443
+ ).action(async (options) => {
21444
+ await garReportCommand(options);
21445
+ });
20569
21446
  return program2;
20570
21447
  }
20571
21448