@a-company/paradigm 3.20.2 → 3.21.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.
@@ -263,22 +263,45 @@ function loadHabitsFresh(rootDir) {
263
263
  for (const seed of SEED_HABITS) {
264
264
  habitsById.set(seed.id, { ...seed });
265
265
  }
266
- const globalHabitsPath = path.join(
267
- process.env.HOME || process.env.USERPROFILE || "~",
268
- ".paradigm",
269
- "habits.yaml"
270
- );
271
- const globalConfig = loadHabitsYaml(globalHabitsPath);
266
+ const home = process.env.HOME || process.env.USERPROFILE || "~";
267
+ const globalConfig = loadHabitsYaml(path.join(home, ".paradigm", "habits.yaml"));
272
268
  if (globalConfig) {
273
269
  mergeHabits(habitsById, globalConfig);
274
270
  }
275
- const projectHabitsPath = path.join(rootDir, ".paradigm", "habits.yaml");
276
- const projectConfig = loadHabitsYaml(projectHabitsPath);
271
+ const globalHabitFiles = loadHabitFiles(path.join(home, ".paradigm", "habits"));
272
+ for (const habit of globalHabitFiles) {
273
+ habitsById.set(habit.id, habit);
274
+ }
275
+ const projectConfig = loadHabitsYaml(path.join(rootDir, ".paradigm", "habits.yaml"));
277
276
  if (projectConfig) {
278
277
  mergeHabits(habitsById, projectConfig);
279
278
  }
279
+ const projectHabitFiles = loadHabitFiles(path.join(rootDir, ".paradigm", "habits"));
280
+ for (const habit of projectHabitFiles) {
281
+ habitsById.set(habit.id, habit);
282
+ }
280
283
  return Array.from(habitsById.values());
281
284
  }
285
+ function loadHabitFiles(dir) {
286
+ if (!fs.existsSync(dir)) return [];
287
+ try {
288
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".habit")).sort();
289
+ const habits = [];
290
+ for (const file of files) {
291
+ try {
292
+ const content = fs.readFileSync(path.join(dir, file), "utf8");
293
+ const habit = yaml.load(content);
294
+ if (habit?.id && habit?.name) {
295
+ habits.push(habit);
296
+ }
297
+ } catch {
298
+ }
299
+ }
300
+ return habits;
301
+ } catch {
302
+ return [];
303
+ }
304
+ }
282
305
  function loadHabitsYaml(filePath) {
283
306
  if (!fs.existsSync(filePath)) {
284
307
  return null;
package/dist/index.js CHANGED
@@ -662,43 +662,43 @@ loreCmd.option("-p, --port <port>", "Port to run on", "3840").option("--no-open"
662
662
  });
663
663
  var habitsCmd = program.command("habits").description("Behavioral habits - practice tracking and compliance");
664
664
  habitsCmd.command("list").alias("ls").description("List all configured habits").option("--trigger <trigger>", "Filter by trigger: preflight, postflight, on-stop, on-commit").option("--category <category>", "Filter by category: discovery, verification, testing, documentation, collaboration, security").option("--json", "Output as JSON").action(async (options) => {
665
- const { habitsListCommand } = await import("./habits-KD4RLIN2.js");
665
+ const { habitsListCommand } = await import("./habits-7BORPC2F.js");
666
666
  await habitsListCommand(options);
667
667
  });
668
668
  habitsCmd.command("status").description("Show practice profile with compliance rates").option("-p, --period <period>", "Time period: 7d, 30d, 90d, all", "30d").option("--json", "Output as JSON").action(async (options) => {
669
- const { habitsStatusCommand } = await import("./habits-KD4RLIN2.js");
669
+ const { habitsStatusCommand } = await import("./habits-7BORPC2F.js");
670
670
  await habitsStatusCommand(options);
671
671
  });
672
672
  habitsCmd.command("init").description("Initialize habits.yaml with seed habits").option("-f, --force", "Overwrite existing file").action(async (options) => {
673
- const { habitsInitCommand } = await import("./habits-KD4RLIN2.js");
673
+ const { habitsInitCommand } = await import("./habits-7BORPC2F.js");
674
674
  await habitsInitCommand(options);
675
675
  });
676
676
  habitsCmd.command("check").description("Evaluate habit compliance for a trigger point").requiredOption("-t, --trigger <trigger>", "Trigger: preflight, postflight, on-stop, on-commit").option("--record", "Record practice events to Sentinel").option("--json", "Output as JSON").option("--files <files>", "Comma-separated files modified (default: git diff)").option("--symbols <symbols>", "Comma-separated symbols touched").action(async (options) => {
677
- const { habitsCheckCommand } = await import("./habits-KD4RLIN2.js");
677
+ const { habitsCheckCommand } = await import("./habits-7BORPC2F.js");
678
678
  await habitsCheckCommand(options);
679
679
  });
680
680
  habitsCmd.command("add").description("Add a custom habit").requiredOption("--id <id>", "Habit ID (kebab-case)").requiredOption("--name <name>", "Human-readable name").requiredOption("--description <desc>", "What this habit enforces").requiredOption("--category <category>", "Category: discovery, verification, testing, documentation, collaboration, security").requiredOption("--trigger <trigger>", "Trigger: preflight, postflight, on-stop, on-commit").option("--severity <severity>", "Severity: advisory, warn, block", "advisory").option("--tools <tools>", "Comma-separated tools to check (for tool-called check type)").option("--check-type <type>", "Check type: tool-called, file-exists, file-modified, lore-recorded, symbols-registered, gates-declared, tests-exist, git-clean", "tool-called").option("--patterns <patterns>", "Comma-separated patterns (for file-exists, file-modified, tests-exist check types)").action(async (options) => {
681
- const { habitsAddCommand } = await import("./habits-KD4RLIN2.js");
681
+ const { habitsAddCommand } = await import("./habits-7BORPC2F.js");
682
682
  await habitsAddCommand({ ...options, checkType: options.checkType });
683
683
  });
684
684
  habitsCmd.command("edit <id>").description("Edit a habit (seed habits: only severity/enabled; custom: all fields)").option("--name <name>", "New name").option("--description <desc>", "New description").option("--category <category>", "New category").option("--trigger <trigger>", "New trigger").option("--severity <severity>", "New severity: advisory, warn, block").option("--enabled <bool>", "Enable or disable: true, false").option("--check-type <type>", "New check type").option("--patterns <patterns>", "Comma-separated patterns").option("--tools <tools>", "Comma-separated tools").action(async (id, options) => {
685
- const { habitsEditCommand } = await import("./habits-KD4RLIN2.js");
685
+ const { habitsEditCommand } = await import("./habits-7BORPC2F.js");
686
686
  await habitsEditCommand(id, { ...options, checkType: options.checkType });
687
687
  });
688
688
  habitsCmd.command("remove <id>").description("Remove a custom habit (seed habits cannot be removed, only disabled)").option("-y, --yes", "Skip confirmation").action(async (id, options) => {
689
- const { habitsRemoveCommand } = await import("./habits-KD4RLIN2.js");
689
+ const { habitsRemoveCommand } = await import("./habits-7BORPC2F.js");
690
690
  await habitsRemoveCommand(id, options);
691
691
  });
692
692
  habitsCmd.command("enable <id>").description("Enable a habit").action(async (id) => {
693
- const { habitsToggleCommand } = await import("./habits-KD4RLIN2.js");
693
+ const { habitsToggleCommand } = await import("./habits-7BORPC2F.js");
694
694
  await habitsToggleCommand(id, "enable");
695
695
  });
696
696
  habitsCmd.command("disable <id>").description("Disable a habit").action(async (id) => {
697
- const { habitsToggleCommand } = await import("./habits-KD4RLIN2.js");
697
+ const { habitsToggleCommand } = await import("./habits-7BORPC2F.js");
698
698
  await habitsToggleCommand(id, "disable");
699
699
  });
700
700
  habitsCmd.action(async () => {
701
- const { habitsListCommand } = await import("./habits-KD4RLIN2.js");
701
+ const { habitsListCommand } = await import("./habits-7BORPC2F.js");
702
702
  await habitsListCommand({});
703
703
  });
704
704
  var sentinelCmd = program.command("sentinel").description("Sentinel \u2014 semantic error monitoring");
package/dist/mcp.js CHANGED
@@ -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
 
@@ -11159,7 +11561,7 @@ async function runChain(rootDir, chainId, options) {
11159
11561
  const start = Date.now();
11160
11562
  const fs22 = await import("fs");
11161
11563
  const path26 = await import("path");
11162
- const yaml13 = await import("js-yaml");
11564
+ const yaml14 = await import("js-yaml");
11163
11565
  const chainPath = path26.join(rootDir, ".paradigm", "personas", "chains", `${chainId}.yaml`);
11164
11566
  if (!fs22.existsSync(chainPath)) {
11165
11567
  return {
@@ -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(fs22.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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a-company/paradigm",
3
- "version": "3.20.2",
3
+ "version": "3.21.0",
4
4
  "description": "Unified CLI for Paradigm developer tools",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",