@anthropologies/claudestory 0.1.58 → 0.1.59

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
@@ -3689,8 +3689,48 @@ var init_session_types = __esm({
3689
3689
  maxTicketsPerSession: z9.number().min(0).default(5),
3690
3690
  handoverInterval: z9.number().min(0).default(3),
3691
3691
  compactThreshold: z9.string().default("high"),
3692
- reviewBackends: z9.array(z9.string()).default(["codex", "agent"])
3692
+ reviewBackends: z9.array(z9.string()).default(["codex", "agent"]),
3693
+ // T-181: Multi-lens review config
3694
+ lensConfig: z9.object({
3695
+ lenses: z9.union([z9.literal("auto"), z9.array(z9.string())]).default("auto"),
3696
+ maxLenses: z9.number().min(1).max(8).default(8),
3697
+ lensTimeout: z9.union([
3698
+ z9.number(),
3699
+ z9.object({ default: z9.number(), opus: z9.number() })
3700
+ ]).default({ default: 60, opus: 120 }),
3701
+ findingBudget: z9.number().min(1).default(10),
3702
+ confidenceFloor: z9.number().min(0).max(1).default(0.6),
3703
+ tokenBudgetPerLens: z9.number().min(1e3).default(32e3),
3704
+ hotPaths: z9.array(z9.string()).default([]),
3705
+ lensModels: z9.record(z9.string()).default({ default: "sonnet", security: "opus", concurrency: "opus" })
3706
+ }).optional(),
3707
+ blockingPolicy: z9.object({
3708
+ neverBlock: z9.array(z9.string()).default([]),
3709
+ alwaysBlock: z9.array(z9.string()).default(["injection", "auth-bypass", "hardcoded-secrets"]),
3710
+ planReviewBlockingLenses: z9.array(z9.string()).default(["security", "error-handling"])
3711
+ }).optional(),
3712
+ requireSecretsGate: z9.boolean().default(false),
3713
+ requireAccessibility: z9.boolean().default(false),
3714
+ testMapping: z9.object({
3715
+ strategy: z9.literal("convention"),
3716
+ patterns: z9.array(z9.object({
3717
+ source: z9.string(),
3718
+ test: z9.string()
3719
+ }))
3720
+ }).optional()
3693
3721
  }).default({ maxTicketsPerSession: 5, compactThreshold: "high", reviewBackends: ["codex", "agent"], handoverInterval: 3 }),
3722
+ // T-181: Lens review findings history (for lessons feedback loop)
3723
+ lensReviewHistory: z9.array(z9.object({
3724
+ ticketId: z9.string(),
3725
+ stage: z9.enum(["CODE_REVIEW", "PLAN_REVIEW"]),
3726
+ lens: z9.string(),
3727
+ category: z9.string(),
3728
+ severity: z9.string(),
3729
+ disposition: z9.enum(["open", "addressed", "contested", "deferred"]),
3730
+ description: z9.string(),
3731
+ dismissReason: z9.string().optional(),
3732
+ timestamp: z9.string()
3733
+ })).default([]),
3694
3734
  // T-123: Issue sweep tracking
3695
3735
  issueSweepState: z9.object({
3696
3736
  remaining: z9.array(z9.string()),
@@ -3719,7 +3759,8 @@ var init_session_types = __esm({
3719
3759
  resolvedDefaults: z9.object({
3720
3760
  maxTicketsPerSession: z9.number(),
3721
3761
  compactThreshold: z9.string(),
3722
- reviewBackends: z9.array(z9.string())
3762
+ reviewBackends: z9.array(z9.string()),
3763
+ handoverInterval: z9.number().optional()
3723
3764
  }).optional()
3724
3765
  }).passthrough();
3725
3766
  }
@@ -4096,8 +4137,8 @@ var init_session = __esm({
4096
4137
 
4097
4138
  // src/mcp/index.ts
4098
4139
  init_esm_shims();
4099
- import { realpathSync as realpathSync2, existsSync as existsSync11 } from "fs";
4100
- import { resolve as resolve8, join as join19, isAbsolute } from "path";
4140
+ import { realpathSync as realpathSync2, existsSync as existsSync12 } from "fs";
4141
+ import { resolve as resolve8, join as join20, isAbsolute } from "path";
4101
4142
  import { z as z11 } from "zod";
4102
4143
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4103
4144
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -4150,7 +4191,7 @@ init_errors();
4150
4191
  init_helpers();
4151
4192
  init_types();
4152
4193
  import { z as z10 } from "zod";
4153
- import { join as join17 } from "path";
4194
+ import { join as join18 } from "path";
4154
4195
 
4155
4196
  // src/cli/commands/status.ts
4156
4197
  init_esm_shims();
@@ -5444,8 +5485,8 @@ init_handover();
5444
5485
  init_esm_shims();
5445
5486
  init_session_types();
5446
5487
  init_session();
5447
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, readdirSync as readdirSync4 } from "fs";
5448
- import { join as join14 } from "path";
5488
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync4, readdirSync as readdirSync4 } from "fs";
5489
+ import { join as join15 } from "path";
5449
5490
 
5450
5491
  // src/autonomous/state-machine.ts
5451
5492
  init_esm_shims();
@@ -5984,6 +6025,22 @@ function djb2Hash(content) {
5984
6025
  }
5985
6026
  return hash.toString(36);
5986
6027
  }
6028
+ function buildLensHistoryUpdate(findings, existing, ticketId, stage) {
6029
+ const existingKeys = new Set(
6030
+ existing.map((e) => `${e.ticketId}:${e.stage}:${e.lens}:${e.category}`)
6031
+ );
6032
+ const newEntries = findings.map((f) => ({
6033
+ ticketId,
6034
+ stage,
6035
+ lens: typeof f.lens === "string" && f.lens !== "" ? f.lens : "unknown",
6036
+ category: f.category,
6037
+ severity: f.severity,
6038
+ disposition: f.disposition ?? "open",
6039
+ description: f.description,
6040
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6041
+ })).filter((e) => !existingKeys.has(`${e.ticketId}:${e.stage}:${e.lens}:${e.category}`));
6042
+ return newEntries.length > 0 ? [...existing, ...newEntries] : null;
6043
+ }
5987
6044
 
5988
6045
  // src/autonomous/stages/index.ts
5989
6046
  init_esm_shims();
@@ -6254,6 +6311,31 @@ var PlanReviewStage = class {
6254
6311
  const reviewer = nextReviewer(existingReviews, backends);
6255
6312
  const risk = ctx.state.ticket?.risk ?? "low";
6256
6313
  const minRounds = requiredRounds(risk);
6314
+ if (reviewer === "lenses") {
6315
+ return {
6316
+ instruction: [
6317
+ `# Multi-Lens Plan Review \u2014 Round ${roundNum} of ${Math.max(minRounds, roundNum)} minimum`,
6318
+ "",
6319
+ "This round uses the **multi-lens review orchestrator** for plan review. It fans out to specialized review agents (Clean Code, Security, Error Handling, and more) in parallel to evaluate the plan from multiple perspectives.",
6320
+ "",
6321
+ "1. Read the plan file",
6322
+ "2. Call `prepareLensReview()` with the plan text (stage: PLAN_REVIEW)",
6323
+ "3. Spawn all lens subagents in parallel",
6324
+ "4. Collect results and pass through the merger and judge pipeline",
6325
+ "5. Report the final SynthesisResult verdict and findings",
6326
+ "",
6327
+ "When done, call `claudestory_autonomous_guide` with:",
6328
+ "```json",
6329
+ `{ "sessionId": "${ctx.state.sessionId}", "action": "report", "report": { "completedAction": "plan_review_round", "verdict": "<approve|revise|reject>", "findings": [...] } }`,
6330
+ "```"
6331
+ ].join("\n"),
6332
+ reminders: [
6333
+ "Report the exact verdict and findings from the synthesizer.",
6334
+ "Lens subagents run in parallel with read-only tools (Read, Grep, Glob)."
6335
+ ],
6336
+ transitionedFrom: ctx.state.previousState ?? void 0
6337
+ };
6338
+ }
6257
6339
  return {
6258
6340
  instruction: [
6259
6341
  `# Plan Review \u2014 Round ${roundNum} of ${Math.max(minRounds, roundNum)} minimum`,
@@ -6315,9 +6397,19 @@ var PlanReviewStage = class {
6315
6397
  nextAction = "PLAN_REVIEW";
6316
6398
  }
6317
6399
  const reviewsForWrite = isReject ? { ...ctx.state.reviews, plan: [] } : { ...ctx.state.reviews, plan: planReviews };
6318
- ctx.writeState({
6400
+ const stateUpdate = {
6319
6401
  reviews: reviewsForWrite
6320
- });
6402
+ };
6403
+ if (reviewerBackend === "lenses" && findings.length > 0) {
6404
+ const updated = buildLensHistoryUpdate(
6405
+ findings,
6406
+ ctx.state.lensReviewHistory ?? [],
6407
+ ctx.state.ticket?.id ?? "unknown",
6408
+ "PLAN_REVIEW"
6409
+ );
6410
+ if (updated) stateUpdate.lensReviewHistory = updated;
6411
+ }
6412
+ ctx.writeState(stateUpdate);
6321
6413
  ctx.appendEvent("plan_review", {
6322
6414
  round: roundNum,
6323
6415
  verdict,
@@ -6661,6 +6753,25 @@ var TestStage = class {
6661
6753
 
6662
6754
  // src/autonomous/stages/code-review.ts
6663
6755
  init_esm_shims();
6756
+
6757
+ // src/autonomous/review-lenses/cache.ts
6758
+ init_esm_shims();
6759
+ import { createHash } from "crypto";
6760
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync9, rmSync as rmSync2, renameSync as renameSync2 } from "fs";
6761
+ import { join as join12 } from "path";
6762
+
6763
+ // src/autonomous/review-lenses/schema-validator.ts
6764
+ init_esm_shims();
6765
+
6766
+ // src/autonomous/review-lenses/cache.ts
6767
+ var CACHE_DIR = "lens-cache";
6768
+ function clearCache(sessionDir2) {
6769
+ const dir = join12(sessionDir2, CACHE_DIR);
6770
+ if (!existsSync9(dir)) return;
6771
+ rmSync2(dir, { recursive: true, force: true });
6772
+ }
6773
+
6774
+ // src/autonomous/stages/code-review.ts
6664
6775
  var CodeReviewStage = class {
6665
6776
  id = "CODE_REVIEW";
6666
6777
  async enter(ctx) {
@@ -6673,6 +6784,32 @@ var CodeReviewStage = class {
6673
6784
  const mergeBase = ctx.state.git.mergeBase;
6674
6785
  const diffCommand = mergeBase ? `\`git diff ${mergeBase}\`` : `\`git diff HEAD\` AND \`git ls-files --others --exclude-standard\``;
6675
6786
  const diffReminder = mergeBase ? `Run: git diff ${mergeBase} \u2014 pass FULL output to reviewer.` : "Run: git diff HEAD + git ls-files --others --exclude-standard \u2014 pass FULL output to reviewer.";
6787
+ if (reviewer === "lenses") {
6788
+ return {
6789
+ instruction: [
6790
+ `# Multi-Lens Code Review \u2014 Round ${roundNum} of ${rounds} minimum`,
6791
+ "",
6792
+ `Capture the diff with: ${diffCommand}`,
6793
+ "",
6794
+ "This round uses the **multi-lens review orchestrator**. It fans out to specialized review agents (Clean Code, Security, Error Handling, and more) in parallel, then synthesizes findings into a single verdict.",
6795
+ "",
6796
+ "1. Capture the full diff",
6797
+ "2. Call `prepareLensReview()` with the diff and changed file list",
6798
+ "3. Spawn all lens subagents in parallel (each prompt is provided by the orchestrator)",
6799
+ "4. Collect results and pass through the merger and judge pipeline",
6800
+ "5. Report the final SynthesisResult verdict and findings",
6801
+ "",
6802
+ "When done, report verdict and findings."
6803
+ ].join("\n"),
6804
+ reminders: [
6805
+ diffReminder,
6806
+ "Do NOT compress or summarize the diff.",
6807
+ "Lens subagents run in parallel with read-only tools (Read, Grep, Glob).",
6808
+ "If the reviewer flags pre-existing issues unrelated to your changes, file them as issues using claudestory_issue_create with severity and impact. Do not fix them in this ticket."
6809
+ ],
6810
+ transitionedFrom: ctx.state.previousState ?? void 0
6811
+ };
6812
+ }
6676
6813
  return {
6677
6814
  instruction: [
6678
6815
  `# Code Review \u2014 Round ${roundNum} of ${rounds} minimum`,
@@ -6738,8 +6875,10 @@ var CodeReviewStage = class {
6738
6875
  nextAction = "CODE_REVIEW";
6739
6876
  }
6740
6877
  if (nextAction === "PLAN") {
6878
+ clearCache(ctx.dir);
6741
6879
  ctx.writeState({
6742
6880
  reviews: { plan: [], code: [] },
6881
+ lensReviewHistory: [],
6743
6882
  ticket: ctx.state.ticket ? { ...ctx.state.ticket, realizedRisk: void 0 } : ctx.state.ticket
6744
6883
  });
6745
6884
  ctx.appendEvent("code_review", {
@@ -6751,9 +6890,19 @@ var CodeReviewStage = class {
6751
6890
  await ctx.fileDeferredFindings(findings, "code");
6752
6891
  return { action: "back", target: "PLAN", reason: "plan_redirect" };
6753
6892
  }
6754
- ctx.writeState({
6893
+ const stateUpdate = {
6755
6894
  reviews: { ...ctx.state.reviews, code: codeReviews }
6756
- });
6895
+ };
6896
+ if (reviewerBackend === "lenses" && findings.length > 0) {
6897
+ const updated = buildLensHistoryUpdate(
6898
+ findings,
6899
+ ctx.state.lensReviewHistory ?? [],
6900
+ ctx.state.ticket?.id ?? "unknown",
6901
+ "CODE_REVIEW"
6902
+ );
6903
+ if (updated) stateUpdate.lensReviewHistory = updated;
6904
+ }
6905
+ ctx.writeState(stateUpdate);
6757
6906
  ctx.appendEvent("code_review", {
6758
6907
  round: roundNum,
6759
6908
  verdict,
@@ -7482,6 +7631,44 @@ var LessonCaptureStage = class {
7482
7631
  planFindings,
7483
7632
  codeFindings
7484
7633
  });
7634
+ const lensHistory = ctx.state.lensReviewHistory ?? [];
7635
+ const dismissed = lensHistory.filter(
7636
+ (f) => f.disposition === "contested"
7637
+ );
7638
+ const tupleCounts = /* @__PURE__ */ new Map();
7639
+ for (const f of lensHistory) {
7640
+ const key = `${f.lens}:${f.category}`;
7641
+ const entry = tupleCounts.get(key) ?? { total: 0, dismissed: 0 };
7642
+ entry.total++;
7643
+ if (f.disposition === "contested") {
7644
+ entry.dismissed++;
7645
+ }
7646
+ tupleCounts.set(key, entry);
7647
+ }
7648
+ const falsePositivePatterns = [];
7649
+ for (const [key, counts] of tupleCounts) {
7650
+ if (counts.total >= 5 && counts.dismissed / counts.total >= 0.6) {
7651
+ falsePositivePatterns.push(
7652
+ `- **${key}**: ${counts.dismissed}/${counts.total} dismissed (${Math.round(counts.dismissed / counts.total * 100)}%)`
7653
+ );
7654
+ }
7655
+ }
7656
+ const lensSection = falsePositivePatterns.length > 0 ? [
7657
+ "",
7658
+ "## Lens False Positive Patterns",
7659
+ "",
7660
+ "These (lens, category) tuples have a >60% dismissal rate over 5+ reviews. Create a lesson for each:",
7661
+ ...falsePositivePatterns,
7662
+ "",
7663
+ 'For each pattern above, create a lesson: "Lens X tends to flag category Y, but this project considers it acceptable because [reason from dismiss history]."',
7664
+ 'Tag with `source: "lens-feedback"` and `tags: ["lens", lens-name]`.'
7665
+ ].join("\n") : "";
7666
+ const dismissedSection = dismissed.length > 0 && falsePositivePatterns.length === 0 ? [
7667
+ "",
7668
+ `## Dismissed Lens Findings (${dismissed.length})`,
7669
+ "",
7670
+ "Some lens findings were dismissed this session. Not enough data for automatic patterns yet (need 5+ reviews per tuple at >60% dismissal rate)."
7671
+ ].join("\n") : "";
7485
7672
  return {
7486
7673
  instruction: [
7487
7674
  "# Capture Lessons from Review Findings",
@@ -7497,15 +7684,18 @@ var LessonCaptureStage = class {
7497
7684
  " - If it matches an existing lesson \u2192 call `claudestory_lesson_reinforce`",
7498
7685
  ' - If it\'s a new pattern \u2192 call `claudestory_lesson_create` with `source: "review"`',
7499
7686
  "3. Skip patterns that are one-off or already well-covered",
7687
+ lensSection,
7688
+ dismissedSection,
7500
7689
  `4. Call \`claudestory_autonomous_guide\` with completedAction: "lessons_captured"`,
7501
7690
  "",
7502
7691
  "```json",
7503
7692
  `{ "sessionId": "${ctx.state.sessionId}", "action": "report", "report": { "completedAction": "lessons_captured" } }`,
7504
7693
  "```"
7505
- ].join("\n"),
7694
+ ].filter(Boolean).join("\n"),
7506
7695
  reminders: [
7507
7696
  "Check existing lessons first \u2014 reinforce before creating duplicates.",
7508
- "Only capture patterns worth remembering across sessions."
7697
+ "Only capture patterns worth remembering across sessions.",
7698
+ ...falsePositivePatterns.length > 0 ? ["Lens false positive patterns MUST be captured as lessons \u2014 they improve future reviews."] : []
7509
7699
  ]
7510
7700
  };
7511
7701
  }
@@ -7692,8 +7882,8 @@ Impact: ${nextIssue.impact}` : ""}` : `Fix issue ${next}.`,
7692
7882
  // src/autonomous/stages/handover.ts
7693
7883
  init_esm_shims();
7694
7884
  init_handover();
7695
- import { writeFileSync as writeFileSync2 } from "fs";
7696
- import { join as join12 } from "path";
7885
+ import { writeFileSync as writeFileSync3 } from "fs";
7886
+ import { join as join13 } from "path";
7697
7887
  var HandoverStage = class {
7698
7888
  id = "HANDOVER";
7699
7889
  async enter(ctx) {
@@ -7724,8 +7914,8 @@ var HandoverStage = class {
7724
7914
  } catch {
7725
7915
  handoverFailed = true;
7726
7916
  try {
7727
- const fallbackPath = join12(ctx.dir, "handover-fallback.md");
7728
- writeFileSync2(fallbackPath, content, "utf-8");
7917
+ const fallbackPath = join13(ctx.dir, "handover-fallback.md");
7918
+ writeFileSync3(fallbackPath, content, "utf-8");
7729
7919
  } catch {
7730
7920
  }
7731
7921
  }
@@ -7797,8 +7987,8 @@ init_queries();
7797
7987
 
7798
7988
  // src/autonomous/version-check.ts
7799
7989
  init_esm_shims();
7800
- import { readFileSync as readFileSync6 } from "fs";
7801
- import { join as join13, dirname as dirname4 } from "path";
7990
+ import { readFileSync as readFileSync7 } from "fs";
7991
+ import { join as join14, dirname as dirname4 } from "path";
7802
7992
  import { fileURLToPath as fileURLToPath3 } from "url";
7803
7993
  function checkVersionMismatch(runningVersion, installedVersion) {
7804
7994
  if (!installedVersion) return null;
@@ -7810,12 +8000,12 @@ function getInstalledVersion() {
7810
8000
  try {
7811
8001
  const thisFile = fileURLToPath3(import.meta.url);
7812
8002
  const candidates = [
7813
- join13(dirname4(thisFile), "..", "..", "package.json"),
7814
- join13(dirname4(thisFile), "..", "package.json")
8003
+ join14(dirname4(thisFile), "..", "..", "package.json"),
8004
+ join14(dirname4(thisFile), "..", "package.json")
7815
8005
  ];
7816
8006
  for (const candidate of candidates) {
7817
8007
  try {
7818
- const raw = readFileSync6(candidate, "utf-8");
8008
+ const raw = readFileSync7(candidate, "utf-8");
7819
8009
  const pkg = JSON.parse(raw);
7820
8010
  if (pkg.version) return pkg.version;
7821
8011
  } catch {
@@ -7827,7 +8017,7 @@ function getInstalledVersion() {
7827
8017
  }
7828
8018
  }
7829
8019
  function getRunningVersion() {
7830
- return "0.1.58";
8020
+ return "0.1.59";
7831
8021
  }
7832
8022
 
7833
8023
  // src/autonomous/guide.ts
@@ -7852,18 +8042,18 @@ var RECOVERY_MAPPING = {
7852
8042
  function buildGuideRecommendOptions(root) {
7853
8043
  const opts = {};
7854
8044
  try {
7855
- const handoversDir = join14(root, ".story", "handovers");
8045
+ const handoversDir = join15(root, ".story", "handovers");
7856
8046
  const files = readdirSync4(handoversDir, "utf-8").filter((f) => f.endsWith(".md")).sort();
7857
8047
  if (files.length > 0) {
7858
- opts.latestHandoverContent = readFileSync7(join14(handoversDir, files[files.length - 1]), "utf-8");
8048
+ opts.latestHandoverContent = readFileSync8(join15(handoversDir, files[files.length - 1]), "utf-8");
7859
8049
  }
7860
8050
  } catch {
7861
8051
  }
7862
8052
  try {
7863
- const snapshotsDir = join14(root, ".story", "snapshots");
8053
+ const snapshotsDir = join15(root, ".story", "snapshots");
7864
8054
  const snapFiles = readdirSync4(snapshotsDir, "utf-8").filter((f) => f.endsWith(".json")).sort();
7865
8055
  if (snapFiles.length > 0) {
7866
- const raw = readFileSync7(join14(snapshotsDir, snapFiles[snapFiles.length - 1]), "utf-8");
8056
+ const raw = readFileSync8(join15(snapshotsDir, snapFiles[snapFiles.length - 1]), "utf-8");
7867
8057
  const snap = JSON.parse(raw);
7868
8058
  if (snap.issues) {
7869
8059
  opts.previousOpenIssueCount = snap.issues.filter((i) => i.status !== "resolved").length;
@@ -8251,7 +8441,7 @@ Staged: ${stagedResult.data.join(", ")}`
8251
8441
  }
8252
8442
  }
8253
8443
  const { state: projectState, warnings } = await loadProject(root);
8254
- const handoversDir = join14(root, ".story", "handovers");
8444
+ const handoversDir = join15(root, ".story", "handovers");
8255
8445
  const ctx = { state: projectState, warnings, root, handoversDir, format: "md" };
8256
8446
  let handoverText = "";
8257
8447
  try {
@@ -8268,7 +8458,7 @@ Staged: ${stagedResult.data.join(", ")}`
8268
8458
  }
8269
8459
  } catch {
8270
8460
  }
8271
- const rulesText = readFileSafe2(join14(root, "RULES.md"));
8461
+ const rulesText = readFileSafe2(join15(root, "RULES.md"));
8272
8462
  const lessonDigest = buildLessonDigest(projectState.lessons);
8273
8463
  const digestParts = [
8274
8464
  handoverText ? `## Recent Handovers
@@ -8284,7 +8474,7 @@ ${rulesText}` : "",
8284
8474
  ].filter(Boolean);
8285
8475
  const digest = digestParts.join("\n\n---\n\n");
8286
8476
  try {
8287
- writeFileSync3(join14(dir, "context-digest.md"), digest, "utf-8");
8477
+ writeFileSync4(join15(dir, "context-digest.md"), digest, "utf-8");
8288
8478
  } catch {
8289
8479
  }
8290
8480
  if (mode !== "auto" && args.ticketId) {
@@ -9037,7 +9227,7 @@ function guideError(err) {
9037
9227
  }
9038
9228
  function readFileSafe2(path2) {
9039
9229
  try {
9040
- return readFileSync7(path2, "utf-8");
9230
+ return readFileSync8(path2, "utf-8");
9041
9231
  } catch {
9042
9232
  return "";
9043
9233
  }
@@ -9047,8 +9237,8 @@ function readFileSafe2(path2) {
9047
9237
  init_esm_shims();
9048
9238
  init_session();
9049
9239
  init_session_types();
9050
- import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
9051
- import { join as join15 } from "path";
9240
+ import { readFileSync as readFileSync9, existsSync as existsSync11 } from "fs";
9241
+ import { join as join16 } from "path";
9052
9242
 
9053
9243
  // src/core/session-report-formatter.ts
9054
9244
  init_esm_shims();
@@ -9254,7 +9444,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
9254
9444
  };
9255
9445
  }
9256
9446
  const dir = sessionDir(root, sessionId);
9257
- if (!existsSync10(dir)) {
9447
+ if (!existsSync11(dir)) {
9258
9448
  return {
9259
9449
  output: `Error: Session ${sessionId} not found.`,
9260
9450
  exitCode: 1,
@@ -9262,8 +9452,8 @@ async function handleSessionReport(sessionId, root, format = "md") {
9262
9452
  isError: true
9263
9453
  };
9264
9454
  }
9265
- const statePath2 = join15(dir, "state.json");
9266
- if (!existsSync10(statePath2)) {
9455
+ const statePath2 = join16(dir, "state.json");
9456
+ if (!existsSync11(statePath2)) {
9267
9457
  return {
9268
9458
  output: `Error: Session ${sessionId} corrupt \u2014 state.json missing.`,
9269
9459
  exitCode: 1,
@@ -9272,7 +9462,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
9272
9462
  };
9273
9463
  }
9274
9464
  try {
9275
- const rawJson = JSON.parse(readFileSync8(statePath2, "utf-8"));
9465
+ const rawJson = JSON.parse(readFileSync9(statePath2, "utf-8"));
9276
9466
  if (rawJson && typeof rawJson === "object" && "schemaVersion" in rawJson && rawJson.schemaVersion !== CURRENT_SESSION_SCHEMA_VERSION) {
9277
9467
  return {
9278
9468
  output: `Error: Session ${sessionId} \u2014 unsupported session schema version ${rawJson.schemaVersion}.`,
@@ -9301,7 +9491,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
9301
9491
  const events = readEvents(dir);
9302
9492
  let planContent = null;
9303
9493
  try {
9304
- planContent = readFileSync8(join15(dir, "plan.md"), "utf-8");
9494
+ planContent = readFileSync9(join16(dir, "plan.md"), "utf-8");
9305
9495
  } catch {
9306
9496
  }
9307
9497
  let gitLog = null;
@@ -9326,7 +9516,7 @@ init_issue();
9326
9516
  init_roadmap();
9327
9517
  init_output_formatter();
9328
9518
  init_helpers();
9329
- import { join as join16, resolve as resolve6 } from "path";
9519
+ import { join as join17, resolve as resolve6 } from "path";
9330
9520
  var PHASE_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
9331
9521
  var PHASE_ID_MAX_LENGTH = 40;
9332
9522
  function validatePhaseId(id) {
@@ -9435,7 +9625,7 @@ function formatMcpError(code, message) {
9435
9625
  async function runMcpReadTool(pinnedRoot, handler) {
9436
9626
  try {
9437
9627
  const { state, warnings } = await loadProject(pinnedRoot);
9438
- const handoversDir = join17(pinnedRoot, ".story", "handovers");
9628
+ const handoversDir = join18(pinnedRoot, ".story", "handovers");
9439
9629
  const ctx = { state, warnings, root: pinnedRoot, handoversDir, format: "md" };
9440
9630
  const result = await handler(ctx);
9441
9631
  if (result.errorCode && INFRASTRUCTURE_ERROR_CODES.includes(result.errorCode)) {
@@ -9993,10 +10183,10 @@ init_esm_shims();
9993
10183
  init_project_loader();
9994
10184
  init_errors();
9995
10185
  import { mkdir as mkdir4, stat as stat2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
9996
- import { join as join18, resolve as resolve7 } from "path";
10186
+ import { join as join19, resolve as resolve7 } from "path";
9997
10187
  async function initProject(root, options) {
9998
10188
  const absRoot = resolve7(root);
9999
- const wrapDir = join18(absRoot, ".story");
10189
+ const wrapDir = join19(absRoot, ".story");
10000
10190
  let exists = false;
10001
10191
  try {
10002
10192
  const s = await stat2(wrapDir);
@@ -10016,11 +10206,11 @@ async function initProject(root, options) {
10016
10206
  ".story/ already exists. Use --force to overwrite config and roadmap."
10017
10207
  );
10018
10208
  }
10019
- await mkdir4(join18(wrapDir, "tickets"), { recursive: true });
10020
- await mkdir4(join18(wrapDir, "issues"), { recursive: true });
10021
- await mkdir4(join18(wrapDir, "handovers"), { recursive: true });
10022
- await mkdir4(join18(wrapDir, "notes"), { recursive: true });
10023
- await mkdir4(join18(wrapDir, "lessons"), { recursive: true });
10209
+ await mkdir4(join19(wrapDir, "tickets"), { recursive: true });
10210
+ await mkdir4(join19(wrapDir, "issues"), { recursive: true });
10211
+ await mkdir4(join19(wrapDir, "handovers"), { recursive: true });
10212
+ await mkdir4(join19(wrapDir, "notes"), { recursive: true });
10213
+ await mkdir4(join19(wrapDir, "lessons"), { recursive: true });
10024
10214
  const created = [
10025
10215
  ".story/config.json",
10026
10216
  ".story/roadmap.json",
@@ -10060,7 +10250,7 @@ async function initProject(root, options) {
10060
10250
  };
10061
10251
  await writeConfig(config, absRoot);
10062
10252
  await writeRoadmap(roadmap, absRoot);
10063
- const gitignorePath = join18(wrapDir, ".gitignore");
10253
+ const gitignorePath = join19(wrapDir, ".gitignore");
10064
10254
  await ensureGitignoreEntries(gitignorePath, STORY_GITIGNORE_ENTRIES);
10065
10255
  const warnings = [];
10066
10256
  if (options.force && exists) {
@@ -10099,7 +10289,7 @@ async function ensureGitignoreEntries(gitignorePath, entries) {
10099
10289
  // src/mcp/index.ts
10100
10290
  var ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
10101
10291
  var CONFIG_PATH2 = ".story/config.json";
10102
- var version = "0.1.58";
10292
+ var version = "0.1.59";
10103
10293
  function tryDiscoverRoot() {
10104
10294
  const envRoot = process.env[ENV_VAR2];
10105
10295
  if (envRoot) {
@@ -10111,7 +10301,7 @@ function tryDiscoverRoot() {
10111
10301
  const resolved = resolve8(envRoot);
10112
10302
  try {
10113
10303
  const canonical = realpathSync2(resolved);
10114
- if (existsSync11(join19(canonical, CONFIG_PATH2))) {
10304
+ if (existsSync12(join20(canonical, CONFIG_PATH2))) {
10115
10305
  return canonical;
10116
10306
  }
10117
10307
  process.stderr.write(`Warning: No .story/config.json at ${canonical}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anthropologies/claudestory",
3
- "version": "0.1.58",
3
+ "version": "0.1.59",
4
4
  "license": "PolyForm-Noncommercial-1.0.0",
5
5
  "description": "An agentic development framework. Track tickets, issues, and progress for your project so every session builds on the last.",
6
6
  "homepage": "https://claudestory.com",
@@ -23,6 +23,7 @@ claudestory tracks tickets, issues, roadmap, and handovers in a `.story/` direct
23
23
  - `/story settings` -> manage project settings (see Settings section below)
24
24
  - `/story design` -> evaluate frontend design (read `design/design.md` in the same directory as this skill file; if not found, tell user to run `claudestory setup-skill`)
25
25
  - `/story design <platform>` -> evaluate for specific platform: web, ios, macos, android (read `design/design.md` in the same directory as this skill file)
26
+ - `/story review-lenses` -> run multi-lens review on current diff (read `review-lenses/review-lenses.md` in the same directory as this skill file; if not found, tell user to run `claudestory setup-skill`). Note: the autonomous guide invokes lenses automatically when `reviewBackends` includes `"lenses"` -- this command is for manual/debug use.
26
27
  - `/story help` -> show all capabilities (read `reference.md` in the same directory as this skill file; if not found, tell user to run `claudestory setup-skill`)
27
28
 
28
29
  If the user's intent doesn't match any of these, use the full context load.
@@ -354,7 +355,24 @@ Do NOT search source code for this. The full config.json schema is shown below.
354
355
  },
355
356
  "LESSON_CAPTURE": { "enabled": "boolean" },
356
357
  "ISSUE_SWEEP": { "enabled": "boolean" }
357
- }
358
+ },
359
+ "lensConfig": {
360
+ "lenses": "\"auto\" | string[] (default: \"auto\")",
361
+ "maxLenses": "number (1-8, default: 8)",
362
+ "lensTimeout": "number | { default: number, opus: number } (default: { default: 60, opus: 120 })",
363
+ "findingBudget": "number (default: 10)",
364
+ "confidenceFloor": "number 0-1 (default: 0.6)",
365
+ "tokenBudgetPerLens": "number (default: 32000)",
366
+ "hotPaths": "string[] (glob patterns for Performance lens, default: [])",
367
+ "lensModels": "Record<string, string> (default: { default: sonnet, security: opus, concurrency: opus })"
368
+ },
369
+ "blockingPolicy": {
370
+ "neverBlock": "string[] (lens names that never produce blocking findings, default: [])",
371
+ "alwaysBlock": "string[] (categories that always block, default: [injection, auth-bypass, hardcoded-secrets])",
372
+ "planReviewBlockingLenses": "string[] (default: [security, error-handling])"
373
+ },
374
+ "requireSecretsGate": "boolean (default: false, require detect-secrets for lens reviews)",
375
+ "requireAccessibility": "boolean (default: false, make accessibility findings blocking)"
358
376
  }
359
377
  }
360
378
  ```
@@ -367,3 +385,4 @@ Additional skill documentation, loaded on demand:
367
385
  - **`autonomous-mode.md`** -- Autonomous mode, review, plan, and guided execution tiers
368
386
  - **`reference.md`** -- Full CLI command and MCP tool reference
369
387
  - **`design/design.md`** -- Frontend design evaluation and implementation guidance, with platform references in `design/references/`
388
+ - **`review-lenses/review-lenses.md`** -- Multi-lens review orchestrator (8 specialized parallel reviewers), with lens prompts in `review-lenses/references/`
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: judge
3
+ version: v1
4
+ model: sonnet
5
+ ---
6
+
7
+ # Judge
8
+
9
+ Synthesis step 2. Receives deduplicated findings and tensions from the Merger. Performs severity calibration, stage-aware verdict generation, and completeness assessment.
10
+
11
+ Verdict rules:
12
+ - reject: critical + confidence >= 0.8 + blocking (plan review: only security/integrity)
13
+ - revise: major + blocking, or any blocking tension
14
+ - approve: only minor/suggestion/non-blocking remain
15
+
16
+ Partial review (required lens failed): never approves, maximum is revise.
17
+
18
+ See `src/autonomous/review-lenses/judge.ts` for the full prompt.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: accessibility
3
+ version: v1
4
+ model: sonnet
5
+ type: surface-activated
6
+ maxSeverity: major
7
+ scope: web-first
8
+ activation: ".tsx, .jsx, .html, .vue, .svelte, .css, .scss"
9
+ ---
10
+
11
+ # Accessibility Lens
12
+
13
+ Finds WCAG compliance issues preventing users with disabilities from using the application. Web-first scope. Checks: missing alt text, non-semantic HTML, missing ARIA labels, no keyboard navigation, color contrast, missing focus management, skip-to-content, form labels, ARIA landmarks, auto-playing media, missing live regions, CSS focus removal, hidden-but-focusable elements.
14
+
15
+ Native mobile/desktop accessibility is out of scope for v1.
16
+
17
+ See `src/autonomous/review-lenses/lenses/accessibility.ts` for the full prompt.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: api-design
3
+ version: v1
4
+ model: sonnet
5
+ type: surface-activated
6
+ maxSeverity: critical
7
+ activation: "**/api/**", route handlers, controllers, GraphQL resolvers
8
+ ---
9
+
10
+ # API Design Lens
11
+
12
+ Focuses on REST/GraphQL API quality -- consistency, correctness, backward compatibility, consumer experience. Checks: breaking changes, inconsistent error format, wrong HTTP status codes, non-RESTful patterns, missing pagination, naming inconsistency, missing Content-Type, overfetching/underfetching, missing idempotency, auth inconsistency.
13
+
14
+ See `src/autonomous/review-lenses/lenses/api-design.ts` for the full prompt.