@a-company/paradigm 3.20.2 → 3.22.0

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/mcp.js CHANGED
@@ -2026,7 +2026,7 @@ function registerResources(server, getContext2) {
2026
2026
 
2027
2027
  // ../paradigm-mcp/src/tools/index.ts
2028
2028
  import * as os3 from "os";
2029
- import * as path25 from "path";
2029
+ import * as path26 from "path";
2030
2030
  import {
2031
2031
  ListToolsRequestSchema,
2032
2032
  CallToolRequestSchema
@@ -2790,7 +2790,7 @@ function navigateExplore(config, target, rootDir) {
2790
2790
  }
2791
2791
  if (result.paths.length === 0) {
2792
2792
  const areaSymbols = Object.entries(config.symbols).filter(
2793
- ([sym, path26]) => sym.toLowerCase().includes(targetLower) || path26.toLowerCase().includes(targetLower)
2793
+ ([sym, path27]) => sym.toLowerCase().includes(targetLower) || path27.toLowerCase().includes(targetLower)
2794
2794
  ).slice(0, 10);
2795
2795
  result.paths = [...new Set(areaSymbols.map(([, p]) => p))];
2796
2796
  result.symbols = areaSymbols.map(([s]) => s);
@@ -7543,8 +7543,16 @@ function loadHabitsFresh(rootDir) {
7543
7543
  const home = process.env.HOME || process.env.USERPROFILE || "~";
7544
7544
  const globalConfig = loadHabitsYaml(path18.join(home, ".paradigm", "habits.yaml"));
7545
7545
  if (globalConfig) mergeHabits(habitsById, globalConfig);
7546
+ const globalHabitFiles = loadHabitFiles(path18.join(home, ".paradigm", "habits"));
7547
+ for (const habit of globalHabitFiles) {
7548
+ habitsById.set(habit.id, habit);
7549
+ }
7546
7550
  const projectConfig = loadHabitsYaml(path18.join(rootDir, ".paradigm", "habits.yaml"));
7547
7551
  if (projectConfig) mergeHabits(habitsById, projectConfig);
7552
+ const projectHabitFiles = loadHabitFiles(path18.join(rootDir, ".paradigm", "habits"));
7553
+ for (const habit of projectHabitFiles) {
7554
+ habitsById.set(habit.id, habit);
7555
+ }
7548
7556
  return Array.from(habitsById.values());
7549
7557
  }
7550
7558
  function loadHabitsYaml(filePath) {
@@ -7575,6 +7583,141 @@ function mergeHabits(habitsById, config) {
7575
7583
  function getHabitsByTrigger(habits, trigger) {
7576
7584
  return habits.filter((h) => h.enabled && h.trigger === trigger);
7577
7585
  }
7586
+ function invalidateHabitsCache(rootDir) {
7587
+ habitsCache.delete(path18.resolve(rootDir));
7588
+ }
7589
+ function loadHabitFiles(dir) {
7590
+ if (!fs17.existsSync(dir)) return [];
7591
+ try {
7592
+ const files = fs17.readdirSync(dir).filter((f) => f.endsWith(".habit")).sort();
7593
+ const habits = [];
7594
+ for (const file of files) {
7595
+ try {
7596
+ const content = fs17.readFileSync(path18.join(dir, file), "utf8");
7597
+ const habit = yaml11.load(content);
7598
+ if (habit?.id && habit?.name) {
7599
+ habits.push(habit);
7600
+ }
7601
+ } catch {
7602
+ }
7603
+ }
7604
+ return habits;
7605
+ } catch {
7606
+ return [];
7607
+ }
7608
+ }
7609
+ var VALID_CATEGORIES = [
7610
+ "discovery",
7611
+ "verification",
7612
+ "testing",
7613
+ "documentation",
7614
+ "collaboration",
7615
+ "security"
7616
+ ];
7617
+ var VALID_TRIGGERS = [
7618
+ "preflight",
7619
+ "postflight",
7620
+ "on-commit",
7621
+ "on-stop"
7622
+ ];
7623
+ var VALID_SEVERITIES = ["advisory", "warn", "block"];
7624
+ var VALID_CHECK_TYPES = [
7625
+ "tool-called",
7626
+ "file-exists",
7627
+ "file-modified",
7628
+ "lore-recorded",
7629
+ "symbols-registered",
7630
+ "gates-declared",
7631
+ "tests-exist",
7632
+ "git-clean",
7633
+ "commit-message-format",
7634
+ "flow-coverage",
7635
+ "context-checked",
7636
+ "aspect-anchored"
7637
+ ];
7638
+ var KEBAB_CASE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
7639
+ function validateHabitDefinition(habit) {
7640
+ const errors = [];
7641
+ if (!habit.id) errors.push("Missing required field: id");
7642
+ if (!habit.name) errors.push("Missing required field: name");
7643
+ if (!habit.description) errors.push("Missing required field: description");
7644
+ if (!habit.category) errors.push("Missing required field: category");
7645
+ if (!habit.trigger) errors.push("Missing required field: trigger");
7646
+ if (!habit.severity) errors.push("Missing required field: severity");
7647
+ if (!habit.check) errors.push("Missing required field: check");
7648
+ if (habit.enabled === void 0 || habit.enabled === null) errors.push("Missing required field: enabled");
7649
+ if (habit.id && !KEBAB_CASE_RE.test(habit.id)) {
7650
+ errors.push(`Invalid id format: "${habit.id}" \u2014 must be kebab-case (lowercase, hyphens only)`);
7651
+ }
7652
+ if (habit.category && !VALID_CATEGORIES.includes(habit.category)) {
7653
+ errors.push(`Invalid category: "${habit.category}" \u2014 must be one of: ${VALID_CATEGORIES.join(", ")}`);
7654
+ }
7655
+ if (habit.trigger && !VALID_TRIGGERS.includes(habit.trigger)) {
7656
+ errors.push(`Invalid trigger: "${habit.trigger}" \u2014 must be one of: ${VALID_TRIGGERS.join(", ")}`);
7657
+ }
7658
+ if (habit.severity && !VALID_SEVERITIES.includes(habit.severity)) {
7659
+ errors.push(`Invalid severity: "${habit.severity}" \u2014 must be one of: ${VALID_SEVERITIES.join(", ")}`);
7660
+ }
7661
+ if (habit.check) {
7662
+ if (!VALID_CHECK_TYPES.includes(habit.check.type)) {
7663
+ errors.push(`Invalid check.type: "${habit.check.type}" \u2014 must be one of: ${VALID_CHECK_TYPES.join(", ")}`);
7664
+ }
7665
+ const params = habit.check.params || {};
7666
+ switch (habit.check.type) {
7667
+ case "tool-called":
7668
+ if (!params.tools || !Array.isArray(params.tools) || params.tools.length === 0) {
7669
+ errors.push('check.type "tool-called" requires check.params.tools[] (non-empty array)');
7670
+ }
7671
+ break;
7672
+ case "file-exists":
7673
+ case "file-modified":
7674
+ if (!params.patterns || !Array.isArray(params.patterns) || params.patterns.length === 0) {
7675
+ errors.push(`check.type "${habit.check.type}" requires check.params.patterns[] (non-empty array)`);
7676
+ }
7677
+ break;
7678
+ case "commit-message-format":
7679
+ if (!params.messagePatterns || !Array.isArray(params.messagePatterns) || params.messagePatterns.length === 0) {
7680
+ errors.push('check.type "commit-message-format" requires check.params.messagePatterns[] (non-empty array)');
7681
+ }
7682
+ break;
7683
+ }
7684
+ }
7685
+ return { valid: errors.length === 0, errors };
7686
+ }
7687
+ var SEED_HABIT_IDS = new Set(SEED_HABITS.map((h) => h.id));
7688
+ function isSeedHabit(id) {
7689
+ return SEED_HABIT_IDS.has(id);
7690
+ }
7691
+ function saveHabit(rootDir, habit, scope = "project") {
7692
+ const baseDir = scope === "global" ? path18.join(process.env.HOME || process.env.USERPROFILE || "~", ".paradigm", "habits") : path18.join(rootDir, ".paradigm", "habits");
7693
+ if (!fs17.existsSync(baseDir)) {
7694
+ fs17.mkdirSync(baseDir, { recursive: true });
7695
+ }
7696
+ const filePath = path18.join(baseDir, `${habit.id}.habit`);
7697
+ const content = yaml11.dump(habit, { lineWidth: 120, noRefs: true });
7698
+ fs17.writeFileSync(filePath, content, "utf8");
7699
+ invalidateHabitsCache(rootDir);
7700
+ return filePath;
7701
+ }
7702
+ function removeHabit(rootDir, id) {
7703
+ if (isSeedHabit(id)) {
7704
+ return { removed: false, reason: `"${id}" is a seed habit and cannot be removed. Use overrides in habits.yaml to disable it.` };
7705
+ }
7706
+ const projectPath = path18.join(rootDir, ".paradigm", "habits", `${id}.habit`);
7707
+ if (fs17.existsSync(projectPath)) {
7708
+ fs17.unlinkSync(projectPath);
7709
+ invalidateHabitsCache(rootDir);
7710
+ return { removed: true };
7711
+ }
7712
+ const home = process.env.HOME || process.env.USERPROFILE || "~";
7713
+ const globalPath = path18.join(home, ".paradigm", "habits", `${id}.habit`);
7714
+ if (fs17.existsSync(globalPath)) {
7715
+ fs17.unlinkSync(globalPath);
7716
+ invalidateHabitsCache(rootDir);
7717
+ return { removed: true };
7718
+ }
7719
+ return { removed: false, reason: `No .habit file found for "${id}". It may be defined in habits.yaml \u2014 edit that file directly.` };
7720
+ }
7578
7721
  function evaluateHabits(habits, trigger, context2, platform2) {
7579
7722
  let activeHabits = getHabitsByTrigger(habits, trigger);
7580
7723
  if (platform2) {
@@ -8786,6 +8929,7 @@ function summarizeEntry(entry) {
8786
8929
  // ../paradigm-mcp/src/tools/habits.ts
8787
8930
  import * as fs19 from "fs";
8788
8931
  import * as path21 from "path";
8932
+ import * as yaml12 from "js-yaml";
8789
8933
  import { execSync as execSync3 } from "child_process";
8790
8934
  function getHabitsToolsList() {
8791
8935
  return [
@@ -8897,6 +9041,128 @@ function getHabitsToolsList() {
8897
9041
  readOnlyHint: true,
8898
9042
  destructiveHint: false
8899
9043
  }
9044
+ },
9045
+ {
9046
+ name: "paradigm_habits_add",
9047
+ description: 'Create a new custom habit as an individual .habit file. Validates all fields and checks for ID collisions with seed habits. Use scope "global" for ~/.paradigm/habits/ or "project" (default) for .paradigm/habits/. ~150 tokens.',
9048
+ inputSchema: {
9049
+ type: "object",
9050
+ properties: {
9051
+ id: { type: "string", description: 'Unique habit ID in kebab-case (e.g. "check-changelog")' },
9052
+ name: { type: "string", description: "Human-readable habit name" },
9053
+ description: { type: "string", description: "What this habit checks and why" },
9054
+ category: {
9055
+ type: "string",
9056
+ enum: ["discovery", "verification", "testing", "documentation", "collaboration", "security"],
9057
+ description: "Habit category"
9058
+ },
9059
+ trigger: {
9060
+ type: "string",
9061
+ enum: ["preflight", "postflight", "on-commit", "on-stop"],
9062
+ description: "When the habit is evaluated"
9063
+ },
9064
+ severity: {
9065
+ type: "string",
9066
+ enum: ["advisory", "warn", "block"],
9067
+ description: "How strictly to enforce (block prevents session completion)"
9068
+ },
9069
+ check: {
9070
+ type: "object",
9071
+ description: "Check definition with type and params",
9072
+ properties: {
9073
+ type: {
9074
+ type: "string",
9075
+ enum: [
9076
+ "tool-called",
9077
+ "file-exists",
9078
+ "file-modified",
9079
+ "lore-recorded",
9080
+ "symbols-registered",
9081
+ "gates-declared",
9082
+ "tests-exist",
9083
+ "git-clean",
9084
+ "commit-message-format",
9085
+ "flow-coverage",
9086
+ "context-checked",
9087
+ "aspect-anchored"
9088
+ ]
9089
+ },
9090
+ params: { type: "object", description: "Check-specific parameters (tools[], patterns[], etc.)" }
9091
+ },
9092
+ required: ["type", "params"]
9093
+ },
9094
+ enabled: { type: "boolean", description: "Whether the habit is active (default: true)" },
9095
+ platforms: {
9096
+ type: "array",
9097
+ items: { type: "string" },
9098
+ description: 'Platforms this habit applies to (e.g. ["claude", "cursor"]). Omit for all.'
9099
+ },
9100
+ scope: {
9101
+ type: "string",
9102
+ enum: ["project", "global"],
9103
+ description: 'Where to save: "project" (default) or "global" (~/.paradigm/habits/)'
9104
+ }
9105
+ },
9106
+ required: ["id", "name", "description", "category", "trigger", "severity", "check"]
9107
+ },
9108
+ annotations: {
9109
+ readOnlyHint: false,
9110
+ destructiveHint: false
9111
+ }
9112
+ },
9113
+ {
9114
+ name: "paradigm_habits_edit",
9115
+ description: "Update fields on an existing custom .habit file. Cannot edit seed habits \u2014 use overrides in habits.yaml instead. Merges provided fields with existing definition and re-validates. ~150 tokens.",
9116
+ inputSchema: {
9117
+ type: "object",
9118
+ properties: {
9119
+ id: { type: "string", description: "ID of the habit to edit" },
9120
+ name: { type: "string", description: "Updated name" },
9121
+ description: { type: "string", description: "Updated description" },
9122
+ category: {
9123
+ type: "string",
9124
+ enum: ["discovery", "verification", "testing", "documentation", "collaboration", "security"]
9125
+ },
9126
+ trigger: {
9127
+ type: "string",
9128
+ enum: ["preflight", "postflight", "on-commit", "on-stop"]
9129
+ },
9130
+ severity: {
9131
+ type: "string",
9132
+ enum: ["advisory", "warn", "block"]
9133
+ },
9134
+ check: {
9135
+ type: "object",
9136
+ properties: {
9137
+ type: { type: "string" },
9138
+ params: { type: "object" }
9139
+ },
9140
+ required: ["type", "params"]
9141
+ },
9142
+ enabled: { type: "boolean" },
9143
+ platforms: { type: "array", items: { type: "string" } }
9144
+ },
9145
+ required: ["id"]
9146
+ },
9147
+ annotations: {
9148
+ readOnlyHint: false,
9149
+ destructiveHint: false
9150
+ }
9151
+ },
9152
+ {
9153
+ name: "paradigm_habits_remove",
9154
+ description: "Delete a custom .habit file. Cannot remove seed habits \u2014 use overrides to disable them instead. Searches both project and global habit directories. ~100 tokens.",
9155
+ inputSchema: {
9156
+ type: "object",
9157
+ properties: {
9158
+ id: { type: "string", description: "ID of the habit to remove" }
9159
+ },
9160
+ required: ["id"]
9161
+ },
9162
+ annotations: {
9163
+ readOnlyHint: false,
9164
+ destructiveHint: true
9165
+ }
8900
9166
  }
8901
9167
  ];
8902
9168
  }
@@ -8922,6 +9188,21 @@ async function handleHabitsTool(name, args, ctx) {
8922
9188
  trackToolCall(result.length, name);
8923
9189
  return { text: result, handled: true };
8924
9190
  }
9191
+ case "paradigm_habits_add": {
9192
+ const result = handleHabitsAdd(args, ctx);
9193
+ trackToolCall(result.length, name);
9194
+ return { text: result, handled: true };
9195
+ }
9196
+ case "paradigm_habits_edit": {
9197
+ const result = handleHabitsEdit(args, ctx);
9198
+ trackToolCall(result.length, name);
9199
+ return { text: result, handled: true };
9200
+ }
9201
+ case "paradigm_habits_remove": {
9202
+ const result = handleHabitsRemove(args, ctx);
9203
+ trackToolCall(result.length, name);
9204
+ return { text: result, handled: true };
9205
+ }
8925
9206
  default:
8926
9207
  return { text: "", handled: false };
8927
9208
  }
@@ -9111,6 +9392,127 @@ function buildRecommendations(evaluation) {
9111
9392
  }
9112
9393
  return [...new Set(recs)];
9113
9394
  }
9395
+ function handleHabitsAdd(args, ctx) {
9396
+ const id = args.id;
9397
+ const scope = args.scope || "project";
9398
+ if (isSeedHabit(id)) {
9399
+ return JSON.stringify({
9400
+ error: true,
9401
+ message: `Cannot create habit "${id}" \u2014 it collides with a seed habit. Choose a different ID or use overrides in habits.yaml to customize the seed habit.`
9402
+ }, null, 2);
9403
+ }
9404
+ const existingHabits = loadHabits(ctx.rootDir);
9405
+ const existing = existingHabits.find((h) => h.id === id);
9406
+ if (existing) {
9407
+ return JSON.stringify({
9408
+ error: true,
9409
+ message: `Habit "${id}" already exists. Use paradigm_habits_edit to update it, or choose a different ID.`
9410
+ }, null, 2);
9411
+ }
9412
+ const habit = {
9413
+ id,
9414
+ name: args.name,
9415
+ description: args.description,
9416
+ category: args.category,
9417
+ trigger: args.trigger,
9418
+ severity: args.severity,
9419
+ check: args.check,
9420
+ enabled: args.enabled !== void 0 ? args.enabled : true
9421
+ };
9422
+ if (args.platforms) habit.platforms = args.platforms;
9423
+ const validation = validateHabitDefinition(habit);
9424
+ if (!validation.valid) {
9425
+ return JSON.stringify({
9426
+ error: true,
9427
+ message: "Validation failed",
9428
+ errors: validation.errors
9429
+ }, null, 2);
9430
+ }
9431
+ const filePath = saveHabit(ctx.rootDir, habit, scope);
9432
+ return JSON.stringify({
9433
+ created: true,
9434
+ id: habit.id,
9435
+ filePath,
9436
+ scope,
9437
+ message: `Habit "${habit.name}" created at ${filePath}`
9438
+ }, null, 2);
9439
+ }
9440
+ function handleHabitsEdit(args, ctx) {
9441
+ const id = args.id;
9442
+ if (isSeedHabit(id)) {
9443
+ return JSON.stringify({
9444
+ error: true,
9445
+ message: `Cannot edit "${id}" \u2014 it is a seed habit. To customize it, add an override in .paradigm/habits.yaml under the "overrides:" key.`
9446
+ }, null, 2);
9447
+ }
9448
+ const projectPath = path21.join(ctx.rootDir, ".paradigm", "habits", `${id}.habit`);
9449
+ const home = process.env.HOME || process.env.USERPROFILE || "~";
9450
+ const globalPath = path21.join(home, ".paradigm", "habits", `${id}.habit`);
9451
+ let filePath = null;
9452
+ let scope = "project";
9453
+ if (fs19.existsSync(projectPath)) {
9454
+ filePath = projectPath;
9455
+ scope = "project";
9456
+ } else if (fs19.existsSync(globalPath)) {
9457
+ filePath = globalPath;
9458
+ scope = "global";
9459
+ }
9460
+ if (!filePath) {
9461
+ return JSON.stringify({
9462
+ error: true,
9463
+ message: `No .habit file found for "${id}". It may be defined in habits.yaml \u2014 edit that file directly.`
9464
+ }, null, 2);
9465
+ }
9466
+ let existing;
9467
+ try {
9468
+ const content = fs19.readFileSync(filePath, "utf8");
9469
+ existing = yaml12.load(content);
9470
+ } catch {
9471
+ return JSON.stringify({
9472
+ error: true,
9473
+ message: `Failed to read ${filePath}`
9474
+ }, null, 2);
9475
+ }
9476
+ const updated = { ...existing };
9477
+ if (args.name !== void 0) updated.name = args.name;
9478
+ if (args.description !== void 0) updated.description = args.description;
9479
+ if (args.category !== void 0) updated.category = args.category;
9480
+ if (args.trigger !== void 0) updated.trigger = args.trigger;
9481
+ if (args.severity !== void 0) updated.severity = args.severity;
9482
+ if (args.check !== void 0) updated.check = args.check;
9483
+ if (args.enabled !== void 0) updated.enabled = args.enabled;
9484
+ if (args.platforms !== void 0) updated.platforms = args.platforms;
9485
+ const validation = validateHabitDefinition(updated);
9486
+ if (!validation.valid) {
9487
+ return JSON.stringify({
9488
+ error: true,
9489
+ message: "Validation failed after merge",
9490
+ errors: validation.errors
9491
+ }, null, 2);
9492
+ }
9493
+ saveHabit(ctx.rootDir, updated, scope);
9494
+ return JSON.stringify({
9495
+ updated: true,
9496
+ id: updated.id,
9497
+ filePath,
9498
+ message: `Habit "${updated.name}" updated`
9499
+ }, null, 2);
9500
+ }
9501
+ function handleHabitsRemove(args, ctx) {
9502
+ const id = args.id;
9503
+ const result = removeHabit(ctx.rootDir, id);
9504
+ if (result.removed) {
9505
+ return JSON.stringify({
9506
+ removed: true,
9507
+ id,
9508
+ message: `Habit "${id}" removed`
9509
+ }, null, 2);
9510
+ }
9511
+ return JSON.stringify({
9512
+ error: true,
9513
+ message: result.reason
9514
+ }, null, 2);
9515
+ }
9114
9516
  async function handleHabitsStatus(args, ctx) {
9115
9517
  const engineer = args.engineer;
9116
9518
  const period = args.period || "30d";
@@ -10366,7 +10768,7 @@ async function handleTasksTool(name, args, ctx) {
10366
10768
  // ../paradigm-mcp/src/utils/assessment-loader.ts
10367
10769
  import * as fs21 from "fs";
10368
10770
  import * as path23 from "path";
10369
- import * as yaml12 from "js-yaml";
10771
+ import * as yaml13 from "js-yaml";
10370
10772
  var ASSESSMENTS_DIR = ".paradigm/assessments";
10371
10773
  var ARCS_DIR = "arcs";
10372
10774
  var INDEX_FILE = "index.yaml";
@@ -10379,7 +10781,7 @@ function computeArcStats(rootDir, arc) {
10379
10781
  const files = fs21.readdirSync(entriesPath).filter((f) => f.endsWith(".yaml"));
10380
10782
  for (const file of files) {
10381
10783
  try {
10382
- const entry = yaml12.load(fs21.readFileSync(path23.join(entriesPath, file), "utf8"));
10784
+ const entry = yaml13.load(fs21.readFileSync(path23.join(entriesPath, file), "utf8"));
10383
10785
  entryCount++;
10384
10786
  if (entry.symbols) entry.symbols.forEach((s) => symbolSet.add(s));
10385
10787
  if (!latestDate || entry.date > latestDate) latestDate = entry.date;
@@ -10404,7 +10806,7 @@ async function loadArcs(rootDir, status) {
10404
10806
  const arcFile = path23.join(arcsPath, arcDir, "arc.yaml");
10405
10807
  if (!fs21.existsSync(arcFile)) continue;
10406
10808
  try {
10407
- const arc = yaml12.load(fs21.readFileSync(arcFile, "utf8"));
10809
+ const arc = yaml13.load(fs21.readFileSync(arcFile, "utf8"));
10408
10810
  if (status && status !== "all" && arc.status !== status) continue;
10409
10811
  arcs.push(computeArcStats(rootDir, arc));
10410
10812
  } catch {
@@ -10421,7 +10823,7 @@ async function loadArc(rootDir, arcId) {
10421
10823
  const arcFile = path23.join(rootDir, ASSESSMENTS_DIR, ARCS_DIR, arcId, "arc.yaml");
10422
10824
  if (!fs21.existsSync(arcFile)) return null;
10423
10825
  try {
10424
- const arc = yaml12.load(fs21.readFileSync(arcFile, "utf8"));
10826
+ const arc = yaml13.load(fs21.readFileSync(arcFile, "utf8"));
10425
10827
  return computeArcStats(rootDir, arc);
10426
10828
  } catch {
10427
10829
  return null;
@@ -10431,9 +10833,9 @@ async function closeArc(rootDir, arcId, status) {
10431
10833
  const arcFile = path23.join(rootDir, ASSESSMENTS_DIR, ARCS_DIR, arcId, "arc.yaml");
10432
10834
  if (!fs21.existsSync(arcFile)) return false;
10433
10835
  try {
10434
- const arc = yaml12.load(fs21.readFileSync(arcFile, "utf8"));
10836
+ const arc = yaml13.load(fs21.readFileSync(arcFile, "utf8"));
10435
10837
  arc.status = status;
10436
- fs21.writeFileSync(arcFile, yaml12.dump(arc, { lineWidth: -1, noRefs: true }));
10838
+ fs21.writeFileSync(arcFile, yaml13.dump(arc, { lineWidth: -1, noRefs: true }));
10437
10839
  await rebuildAssessmentIndex(rootDir);
10438
10840
  return true;
10439
10841
  } catch {
@@ -10447,7 +10849,7 @@ async function loadEntries(rootDir, arcId) {
10447
10849
  const files = fs21.readdirSync(entriesPath).filter((f) => f.endsWith(".yaml")).sort();
10448
10850
  for (const file of files) {
10449
10851
  try {
10450
- const entry = yaml12.load(fs21.readFileSync(path23.join(entriesPath, file), "utf8"));
10852
+ const entry = yaml13.load(fs21.readFileSync(path23.join(entriesPath, file), "utf8"));
10451
10853
  entries.push(entry);
10452
10854
  } catch {
10453
10855
  }
@@ -10469,9 +10871,9 @@ async function loadEntry(rootDir, entryId) {
10469
10871
  const entryFile = path23.join(arcsPath, arcDir, "entries", `${entryId}.yaml`);
10470
10872
  if (fs21.existsSync(entryFile)) {
10471
10873
  try {
10472
- const entry = yaml12.load(fs21.readFileSync(entryFile, "utf8"));
10874
+ const entry = yaml13.load(fs21.readFileSync(entryFile, "utf8"));
10473
10875
  const arcFile = path23.join(arcsPath, arcDir, "arc.yaml");
10474
- const arc = yaml12.load(fs21.readFileSync(arcFile, "utf8"));
10876
+ const arc = yaml13.load(fs21.readFileSync(arcFile, "utf8"));
10475
10877
  return { entry, arc };
10476
10878
  } catch {
10477
10879
  return null;
@@ -10498,7 +10900,7 @@ async function searchEntries(rootDir, filter) {
10498
10900
  const files = fs21.readdirSync(entriesPath).filter((f) => f.endsWith(".yaml"));
10499
10901
  for (const file of files) {
10500
10902
  try {
10501
- const entry = yaml12.load(fs21.readFileSync(path23.join(entriesPath, file), "utf8"));
10903
+ const entry = yaml13.load(fs21.readFileSync(path23.join(entriesPath, file), "utf8"));
10502
10904
  if (filter.symbol && !(entry.symbols || []).includes(filter.symbol)) continue;
10503
10905
  if (filter.tag && !(entry.tags || []).includes(filter.tag)) continue;
10504
10906
  if (filter.type && entry.type !== filter.type) continue;
@@ -10529,7 +10931,7 @@ async function rebuildAssessmentIndex(rootDir) {
10529
10931
  const arcFile = path23.join(arcsPath, arcDir, "arc.yaml");
10530
10932
  if (!fs21.existsSync(arcFile)) continue;
10531
10933
  try {
10532
- const arc = yaml12.load(fs21.readFileSync(arcFile, "utf8"));
10934
+ const arc = yaml13.load(fs21.readFileSync(arcFile, "utf8"));
10533
10935
  const entriesPath = path23.join(arcsPath, arcDir, "entries");
10534
10936
  const entryCount = fs21.existsSync(entriesPath) ? fs21.readdirSync(entriesPath).filter((f) => f.endsWith(".yaml")).length : 0;
10535
10937
  totalArcs++;
@@ -10549,7 +10951,7 @@ async function rebuildAssessmentIndex(rootDir) {
10549
10951
  arcs: arcSummaries
10550
10952
  };
10551
10953
  fs21.mkdirSync(assessmentsPath, { recursive: true });
10552
- fs21.writeFileSync(path23.join(assessmentsPath, INDEX_FILE), yaml12.dump(index, { lineWidth: -1, noRefs: true }));
10954
+ fs21.writeFileSync(path23.join(assessmentsPath, INDEX_FILE), yaml13.dump(index, { lineWidth: -1, noRefs: true }));
10553
10955
  return index;
10554
10956
  }
10555
10957
 
@@ -10886,8 +11288,8 @@ function generateRunId() {
10886
11288
  var TEMPLATE_REGEX = /\{\{([^}]+)\}\}/g;
10887
11289
  function interpolate(value, scope) {
10888
11290
  if (typeof value === "string") {
10889
- return value.replace(TEMPLATE_REGEX, (_match, path26) => {
10890
- const resolved = resolvePath(path26.trim(), scope);
11291
+ return value.replace(TEMPLATE_REGEX, (_match, path27) => {
11292
+ const resolved = resolvePath(path27.trim(), scope);
10891
11293
  return resolved !== void 0 ? String(resolved) : _match;
10892
11294
  });
10893
11295
  }
@@ -10920,8 +11322,8 @@ function resolvePath(dotPath, scope) {
10920
11322
  return void 0;
10921
11323
  }
10922
11324
  }
10923
- function deepGet(obj, path26) {
10924
- const parts = path26.split(/[.\[\]]+/).filter(Boolean);
11325
+ function deepGet(obj, path27) {
11326
+ const parts = path27.split(/[.\[\]]+/).filter(Boolean);
10925
11327
  let current = obj;
10926
11328
  for (const part of parts) {
10927
11329
  if (current == null || typeof current !== "object") return void 0;
@@ -11157,11 +11559,11 @@ async function runPersonaObject(rootDir, persona, options) {
11157
11559
  }
11158
11560
  async function runChain(rootDir, chainId, options) {
11159
11561
  const start = Date.now();
11160
- const fs22 = await import("fs");
11161
- const path26 = await import("path");
11162
- const yaml13 = await import("js-yaml");
11163
- const chainPath = path26.join(rootDir, ".paradigm", "personas", "chains", `${chainId}.yaml`);
11164
- if (!fs22.existsSync(chainPath)) {
11562
+ const fs23 = await import("fs");
11563
+ const path27 = await import("path");
11564
+ const yaml14 = await import("js-yaml");
11565
+ const chainPath = path27.join(rootDir, ".paradigm", "personas", "chains", `${chainId}.yaml`);
11566
+ if (!fs23.existsSync(chainPath)) {
11165
11567
  return {
11166
11568
  chain_id: chainId,
11167
11569
  status: "error",
@@ -11170,7 +11572,7 @@ async function runChain(rootDir, chainId, options) {
11170
11572
  duration_ms: Date.now() - start
11171
11573
  };
11172
11574
  }
11173
- const chain = yaml13.load(fs22.readFileSync(chainPath, "utf8"));
11575
+ const chain = yaml14.load(fs23.readFileSync(chainPath, "utf8"));
11174
11576
  let permutation;
11175
11577
  if (options.permutation && chain.permutations) {
11176
11578
  permutation = chain.permutations.find((p) => p.id === options.permutation);
@@ -11274,8 +11676,8 @@ function validateInterpolation(persona) {
11274
11676
  const serialized = JSON.stringify(step);
11275
11677
  const templates = serialized.match(TEMPLATE_REGEX) || [];
11276
11678
  for (const template of templates) {
11277
- const path26 = template.replace("{{", "").replace("}}", "").trim();
11278
- const [namespace, ...rest] = path26.split(".");
11679
+ const path27 = template.replace("{{", "").replace("}}", "").trim();
11680
+ const [namespace, ...rest] = path27.split(".");
11279
11681
  const key = rest.join(".");
11280
11682
  switch (namespace) {
11281
11683
  case "fixtures":
@@ -12287,8 +12689,241 @@ function summarizeStep(step) {
12287
12689
  return result;
12288
12690
  }
12289
12691
 
12290
- // ../paradigm-mcp/src/tools/fallback-grep.ts
12692
+ // ../paradigm-mcp/src/tools/graph.ts
12693
+ import * as fs22 from "fs";
12291
12694
  import * as path24 from "path";
12695
+ var CATEGORY_PREFIXES = {
12696
+ component: "#",
12697
+ flow: "$",
12698
+ gate: "^",
12699
+ signal: "!",
12700
+ aspect: "~"
12701
+ };
12702
+ var NODE_WIDTH = 200;
12703
+ var NODE_HEIGHT = 60;
12704
+ var NODE_GAP = 20;
12705
+ var GROUP_PADDING = 40;
12706
+ var GROUP_HEADER = 50;
12707
+ var GROUP_GAP = 60;
12708
+ function getGraphToolsList() {
12709
+ return [
12710
+ {
12711
+ name: "paradigm_graph_generate",
12712
+ description: "Generate a GraphState JSON document for the Paradigm Symbol Graph UI. Accepts optional symbols (filter), groups (clustering), and links (edges between groups). Returns valid GraphState ready to load. ~200 tokens.",
12713
+ inputSchema: {
12714
+ type: "object",
12715
+ properties: {
12716
+ symbols: {
12717
+ type: "array",
12718
+ items: { type: "string" },
12719
+ description: 'Symbol names to include (e.g. ["#auth-middleware", "^authenticated"]). Omit to include all from scan-index.'
12720
+ },
12721
+ groups: {
12722
+ type: "array",
12723
+ items: {
12724
+ type: "object",
12725
+ properties: {
12726
+ label: { type: "string", description: "Group display label" },
12727
+ symbols: {
12728
+ type: "array",
12729
+ items: { type: "string" },
12730
+ description: "Symbol names belonging to this group"
12731
+ }
12732
+ },
12733
+ required: ["label", "symbols"]
12734
+ },
12735
+ description: "Optional groupings of symbols."
12736
+ },
12737
+ links: {
12738
+ type: "array",
12739
+ items: {
12740
+ type: "object",
12741
+ properties: {
12742
+ source: { type: "string", description: "Source group label" },
12743
+ target: { type: "string", description: "Target group label" },
12744
+ label: { type: "string", description: "Edge label" }
12745
+ },
12746
+ required: ["source", "target"]
12747
+ },
12748
+ description: "Edges between groups (by label name)."
12749
+ },
12750
+ name: {
12751
+ type: "string",
12752
+ description: 'Graph name (default: "Generated Graph").'
12753
+ }
12754
+ }
12755
+ },
12756
+ annotations: {
12757
+ readOnlyHint: true,
12758
+ destructiveHint: false
12759
+ }
12760
+ }
12761
+ ];
12762
+ }
12763
+ async function handleGraphTool(name, args, ctx) {
12764
+ if (name !== "paradigm_graph_generate") {
12765
+ return { handled: false, text: "" };
12766
+ }
12767
+ try {
12768
+ const result = buildGraphState(
12769
+ ctx.rootDir,
12770
+ args.symbols,
12771
+ args.groups,
12772
+ args.links,
12773
+ args.name || "Generated Graph"
12774
+ );
12775
+ const text = JSON.stringify(result, null, 2);
12776
+ trackToolCall(text.length, name);
12777
+ return { handled: true, text };
12778
+ } catch (err2) {
12779
+ const text = JSON.stringify({ error: err2.message }, null, 2);
12780
+ trackToolCall(text.length, name);
12781
+ return { handled: true, text };
12782
+ }
12783
+ }
12784
+ var SCAN_CATEGORY_MAP = {
12785
+ components: "component",
12786
+ flows: "flow",
12787
+ gates: "gate",
12788
+ signals: "signal",
12789
+ aspects: "aspect"
12790
+ };
12791
+ function loadScanIndex(rootDir) {
12792
+ const indexPath = path24.join(rootDir, ".paradigm", "scan-index.json");
12793
+ if (!fs22.existsSync(indexPath)) return [];
12794
+ const raw = JSON.parse(fs22.readFileSync(indexPath, "utf8"));
12795
+ const symbols = [];
12796
+ for (const [sectionKey, categoryName] of Object.entries(SCAN_CATEGORY_MAP)) {
12797
+ const section = raw[sectionKey];
12798
+ if (!section || typeof section !== "object") continue;
12799
+ for (const [id, sym] of Object.entries(section)) {
12800
+ const s = sym;
12801
+ symbols.push({
12802
+ id,
12803
+ name: id,
12804
+ category: categoryName,
12805
+ prefix: CATEGORY_PREFIXES[categoryName] || "#",
12806
+ description: s.description,
12807
+ path: s.path,
12808
+ tags: s.tags
12809
+ });
12810
+ }
12811
+ }
12812
+ return symbols;
12813
+ }
12814
+ function resolveSymbol(name, allSymbols) {
12815
+ const stripped = name.replace(/^[#$^!~]/, "");
12816
+ return allSymbols.find(
12817
+ (s) => s.id === stripped || s.name === stripped || s.id === name || s.name === name
12818
+ );
12819
+ }
12820
+ function buildGraphState(rootDir, symbolFilter, groups, links, graphName = "Generated Graph") {
12821
+ const allSymbols = loadScanIndex(rootDir);
12822
+ let included;
12823
+ if (symbolFilter && symbolFilter.length > 0) {
12824
+ included = symbolFilter.map((name) => resolveSymbol(name, allSymbols)).filter(Boolean);
12825
+ } else {
12826
+ included = allSymbols;
12827
+ }
12828
+ const nodes = [];
12829
+ const edges = [];
12830
+ const groupIdMap = /* @__PURE__ */ new Map();
12831
+ const assignedSymbols = /* @__PURE__ */ new Set();
12832
+ let nextGroupX = 0;
12833
+ if (groups && groups.length > 0) {
12834
+ for (const group of groups) {
12835
+ const groupId = `group-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
12836
+ groupIdMap.set(group.label, groupId);
12837
+ const memberSymbols = group.symbols.map((name) => resolveSymbol(name, included)).filter(Boolean);
12838
+ const cols = Math.ceil(Math.sqrt(memberSymbols.length));
12839
+ const rows = Math.ceil(memberSymbols.length / cols);
12840
+ for (let i = 0; i < memberSymbols.length; i++) {
12841
+ const sym = memberSymbols[i];
12842
+ const col = i % cols;
12843
+ const row = Math.floor(i / cols);
12844
+ const prefix = CATEGORY_PREFIXES[sym.category] || "#";
12845
+ nodes.push({
12846
+ id: `sym-${sym.id}`,
12847
+ type: "symbolNode",
12848
+ position: {
12849
+ x: GROUP_PADDING + col * (NODE_WIDTH + NODE_GAP),
12850
+ y: GROUP_HEADER + GROUP_PADDING + row * (NODE_HEIGHT + NODE_GAP)
12851
+ },
12852
+ parentId: groupId,
12853
+ data: {
12854
+ type: "symbol",
12855
+ symbol: sym,
12856
+ label: `${prefix}${sym.name}`
12857
+ }
12858
+ });
12859
+ assignedSymbols.add(sym.id);
12860
+ }
12861
+ const cols2 = Math.max(cols, 1);
12862
+ const rows2 = Math.max(rows, 1);
12863
+ const groupWidth = GROUP_PADDING * 2 + cols2 * NODE_WIDTH + (cols2 - 1) * NODE_GAP;
12864
+ const groupHeight = GROUP_HEADER + GROUP_PADDING * 2 + rows2 * NODE_HEIGHT + (rows2 - 1) * NODE_GAP;
12865
+ nodes.unshift({
12866
+ id: groupId,
12867
+ type: "groupNode",
12868
+ position: { x: nextGroupX, y: 0 },
12869
+ style: { width: groupWidth, height: groupHeight },
12870
+ data: { type: "group", label: group.label }
12871
+ });
12872
+ nextGroupX += groupWidth + GROUP_GAP;
12873
+ }
12874
+ }
12875
+ const ungrouped = included.filter((s) => !assignedSymbols.has(s.id));
12876
+ if (ungrouped.length > 0) {
12877
+ const startY = groups && groups.length > 0 ? 400 : 0;
12878
+ const cols = Math.ceil(Math.sqrt(ungrouped.length));
12879
+ for (let i = 0; i < ungrouped.length; i++) {
12880
+ const sym = ungrouped[i];
12881
+ const col = i % cols;
12882
+ const row = Math.floor(i / cols);
12883
+ const prefix = CATEGORY_PREFIXES[sym.category] || "#";
12884
+ nodes.push({
12885
+ id: `sym-${sym.id}`,
12886
+ type: "symbolNode",
12887
+ position: {
12888
+ x: col * (NODE_WIDTH + NODE_GAP),
12889
+ y: startY + row * (NODE_HEIGHT + NODE_GAP)
12890
+ },
12891
+ data: {
12892
+ type: "symbol",
12893
+ symbol: sym,
12894
+ label: `${prefix}${sym.name}`
12895
+ }
12896
+ });
12897
+ }
12898
+ }
12899
+ if (links && links.length > 0) {
12900
+ for (const link of links) {
12901
+ const sourceId = groupIdMap.get(link.source);
12902
+ const targetId = groupIdMap.get(link.target);
12903
+ if (sourceId && targetId) {
12904
+ edges.push({
12905
+ id: `e-${sourceId}-${targetId}`,
12906
+ source: sourceId,
12907
+ target: targetId,
12908
+ type: "default",
12909
+ label: link.label,
12910
+ data: { label: link.label }
12911
+ });
12912
+ }
12913
+ }
12914
+ }
12915
+ return {
12916
+ version: "1.0",
12917
+ name: graphName,
12918
+ projectId: path24.basename(rootDir),
12919
+ lastModified: (/* @__PURE__ */ new Date()).toISOString(),
12920
+ nodes,
12921
+ edges
12922
+ };
12923
+ }
12924
+
12925
+ // ../paradigm-mcp/src/tools/fallback-grep.ts
12926
+ import * as path25 from "path";
12292
12927
  import { execSync as execSync5 } from "child_process";
12293
12928
  function grepForReferences(rootDir, symbol, options = {}) {
12294
12929
  const { maxResults = 20 } = options;
@@ -12317,7 +12952,7 @@ function grepForReferences(rootDir, symbol, options = {}) {
12317
12952
  const match = line.match(/^(.+?):(\d+):(.*)$/);
12318
12953
  if (match) {
12319
12954
  const [, filePath, lineNum, content] = match;
12320
- const relativePath = path24.relative(rootDir, filePath);
12955
+ const relativePath = path25.relative(rootDir, filePath);
12321
12956
  let context2 = "unknown";
12322
12957
  if (relativePath.includes(".purpose") || relativePath.includes("portal.yaml")) {
12323
12958
  context2 = "purpose";
@@ -12580,6 +13215,8 @@ function registerTools(server, getContext2, reloadContext2) {
12580
13215
  ...getPersonaToolsList(),
12581
13216
  // Protocol tools
12582
13217
  ...getProtocolsToolsList(),
13218
+ // Graph generation tool
13219
+ ...getGraphToolsList(),
12583
13220
  // Plugin update check
12584
13221
  {
12585
13222
  name: "paradigm_plugin_check",
@@ -13213,7 +13850,7 @@ Update command:
13213
13850
  const { rebuildStaticFiles: rebuildStaticFiles2 } = await import("./reindex-CMZARW5K.js");
13214
13851
  const memberResults = [];
13215
13852
  for (const member of ctx.workspace.config.members) {
13216
- const memberAbsPath = path25.resolve(path25.dirname(ctx.workspace.workspacePath), member.path);
13853
+ const memberAbsPath = path26.resolve(path26.dirname(ctx.workspace.workspacePath), member.path);
13217
13854
  try {
13218
13855
  const result = await rebuildStaticFiles2(memberAbsPath);
13219
13856
  memberResults.push({
@@ -13402,6 +14039,15 @@ Update command:
13402
14039
  };
13403
14040
  }
13404
14041
  }
14042
+ if (name === "paradigm_graph_generate") {
14043
+ const result = await handleGraphTool(name, args, ctx);
14044
+ if (result.handled) {
14045
+ trackToolCall(result.text.length, name);
14046
+ return {
14047
+ content: [{ type: "text", text: result.text }]
14048
+ };
14049
+ }
14050
+ }
13405
14051
  if (name === "paradigm_reindex") {
13406
14052
  const reload = reloadContext2 || (async () => {
13407
14053
  });