@wrongstack/core 0.305.1 → 0.306.2

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.
Files changed (60) hide show
  1. package/dist/chronicle/index.js +6 -1
  2. package/dist/chronicle/project-server.js +13 -3
  3. package/dist/coordination/agents/index.js +3 -2
  4. package/dist/coordination/agents/types.d.ts +1 -1
  5. package/dist/coordination/index.d.ts +1 -0
  6. package/dist/coordination/index.js +165 -59
  7. package/dist/coordination/mailbox-codecs.d.ts +29 -10
  8. package/dist/coordination/mailbox-constants.d.ts +30 -16
  9. package/dist/coordination/mailbox-health.d.ts +16 -0
  10. package/dist/coordination/mailbox-http-validation.d.ts +2 -1
  11. package/dist/coordination/mailbox-parse-state.d.ts +28 -10
  12. package/dist/coordination/mailbox-project-server.js +98 -4
  13. package/dist/coordination/mailbox-types.d.ts +44 -6
  14. package/dist/coordination/package-outdated-watcher.d.ts +15 -1
  15. package/dist/coordination/sqlite-mailbox-credentials.d.ts +26 -0
  16. package/dist/coordination/sqlite-mailbox.d.ts +25 -0
  17. package/dist/coordination/techstack-mailbox-consumer.d.ts +17 -0
  18. package/dist/core/index.d.ts +2 -1
  19. package/dist/core/index.js +2793 -2634
  20. package/dist/core/system-prompt-blocks.d.ts +1 -1
  21. package/dist/core/system-prompt-builder.d.ts +7 -1
  22. package/dist/core/system-prompt-glossary.d.ts +0 -23
  23. package/dist/defaults/index.js +9 -41
  24. package/dist/execution/index.js +9 -3
  25. package/dist/hq/index.js +6 -39
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +1053 -626
  28. package/dist/infrastructure/index.js +6 -39
  29. package/dist/observability/index.js +7 -3
  30. package/dist/plugin/index.d.ts +4 -3
  31. package/dist/plugin/index.js +595 -145
  32. package/dist/plugins/auto-review-plugin.d.ts +14 -7
  33. package/dist/plugins/chimera-plugin.d.ts +15 -1
  34. package/dist/plugins/review-finding-integration.d.ts +15 -3
  35. package/dist/plugins/review-finding-parser.d.ts +36 -0
  36. package/dist/plugins/review-finding-types.d.ts +46 -0
  37. package/dist/plugins/review-finding-verification.d.ts +53 -0
  38. package/dist/plugins/review-report-integration.d.ts +1 -0
  39. package/dist/plugins/review-report-store.d.ts +7 -0
  40. package/dist/plugins/review-report-types.d.ts +14 -0
  41. package/dist/plugins/review-types.d.ts +74 -0
  42. package/dist/replay/replay-provider-runner.d.ts +5 -4
  43. package/dist/security/file-permissions.d.ts +12 -35
  44. package/dist/security/index.js +6 -50
  45. package/dist/session-catalog/project-server.js +6 -39
  46. package/dist/storage/index.js +6 -1
  47. package/dist/tools/fallback-manage-tool-options.d.ts +9 -0
  48. package/dist/tools/index.js +91 -38
  49. package/dist/tools/one-shot-llm-tool.d.ts +6 -0
  50. package/dist/types/blocks.d.ts +10 -0
  51. package/dist/utils/index.d.ts +1 -0
  52. package/dist/utils/index.js +30 -9
  53. package/dist/utils/memory-evidence-fence.d.ts +47 -0
  54. package/instructions/agents/browser.md +1 -0
  55. package/instructions/agents/e2e.md +2 -0
  56. package/instructions/llm/chimera-review.md +52 -1
  57. package/instructions/system-lite.md +17 -6
  58. package/instructions/system-pro.md +25 -20
  59. package/instructions/system.md +25 -12
  60. package/package.json +3 -3
@@ -180,11 +180,15 @@ var init_errors = __esm({
180
180
  });
181
181
 
182
182
  // src/security/file-permissions.ts
183
- var SECRET_FILE_MODE;
183
+ import {
184
+ restrictDirPermissions,
185
+ restrictFilePermissions,
186
+ SECRET_DIR_MODE,
187
+ SECRET_FILE_MODE
188
+ } from "@wrongstack/persistence";
184
189
  var init_file_permissions = __esm({
185
190
  "src/security/file-permissions.ts"() {
186
191
  "use strict";
187
- SECRET_FILE_MODE = 384;
188
192
  }
189
193
  });
190
194
 
@@ -671,7 +675,9 @@ var init_review_report_store = __esm({
671
675
  unparseableCount: input.unparseableCount,
672
676
  durationSeconds: input.durationSeconds ?? existing.durationSeconds,
673
677
  rawText: input.rawText || existing.rawText,
674
- files: input.files.length > 0 ? input.files : existing.files
678
+ files: input.files.length > 0 ? input.files : existing.files,
679
+ ...input.evidenceStatus !== void 0 ? { evidenceStatus: input.evidenceStatus } : {},
680
+ ...input.evidenceChecks !== void 0 ? { evidenceChecks: input.evidenceChecks } : {}
675
681
  };
676
682
  await fsp4.appendFile(this.filePath, JSON.stringify({ __report: 1, data: updated }) + NL2, {
677
683
  encoding: "utf8",
@@ -694,7 +700,9 @@ var init_review_report_store = __esm({
694
700
  unparseableCount: input.unparseableCount,
695
701
  durationSeconds: input.durationSeconds,
696
702
  rawText: input.rawText,
697
- ...input.cascadeDepth !== void 0 ? { cascadeDepth: input.cascadeDepth } : {}
703
+ ...input.cascadeDepth !== void 0 ? { cascadeDepth: input.cascadeDepth } : {},
704
+ ...input.evidenceStatus !== void 0 ? { evidenceStatus: input.evidenceStatus } : {},
705
+ ...input.evidenceChecks !== void 0 ? { evidenceChecks: input.evidenceChecks } : {}
698
706
  };
699
707
  const createdEvent = {
700
708
  id: randomUUID6(),
@@ -738,6 +746,24 @@ var init_review_report_store = __esm({
738
746
  return { ...entry.report };
739
747
  });
740
748
  }
749
+ async updateEvidence(reportId, status, checks) {
750
+ return withFileLock(this.filePath, async () => {
751
+ const all = await this._readAll();
752
+ const entry = all.find((candidate) => candidate.report.id === reportId);
753
+ if (!entry) throw new Error(`Review report not found: ${reportId}`);
754
+ const updated = {
755
+ ...this._materialize(entry),
756
+ evidenceStatus: status,
757
+ evidenceChecks: checks
758
+ };
759
+ await fsp4.appendFile(
760
+ this.filePath,
761
+ `${JSON.stringify({ __report: 1, data: updated })}${NL2}`,
762
+ { encoding: "utf8", mode: SECRET_FILE_MODE }
763
+ );
764
+ return updated;
765
+ });
766
+ }
741
767
  async addNote(reportId, actor, note) {
742
768
  return withFileLock(this.filePath, async () => {
743
769
  const all = await this._readAll();
@@ -1141,10 +1167,10 @@ function validateAgainstSchema(value, schema) {
1141
1167
  return { ok: errors.length === 0, errors };
1142
1168
  }
1143
1169
  var MAX_SCHEMA_DEPTH = 64;
1144
- function walk(value, schema, path32, errors, depth) {
1170
+ function walk(value, schema, path33, errors, depth) {
1145
1171
  if (depth > MAX_SCHEMA_DEPTH) {
1146
1172
  errors.push({
1147
- path: path32 || "<root>",
1173
+ path: path33 || "<root>",
1148
1174
  message: `schema nesting exceeds maximum depth (${MAX_SCHEMA_DEPTH})`
1149
1175
  });
1150
1176
  return;
@@ -1152,7 +1178,7 @@ function walk(value, schema, path32, errors, depth) {
1152
1178
  if (schema.enum !== void 0) {
1153
1179
  if (!enumIncludes(schema.enum, value)) {
1154
1180
  errors.push({
1155
- path: path32 || "<root>",
1181
+ path: path33 || "<root>",
1156
1182
  message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
1157
1183
  });
1158
1184
  return;
@@ -1161,7 +1187,7 @@ function walk(value, schema, path32, errors, depth) {
1161
1187
  if (typeof schema.type === "string") {
1162
1188
  if (!checkType(value, schema.type)) {
1163
1189
  errors.push({
1164
- path: path32 || "<root>",
1190
+ path: path33 || "<root>",
1165
1191
  message: `expected ${schema.type}, got ${describeType(value)} (${previewValue(value)})`
1166
1192
  });
1167
1193
  return;
@@ -1173,7 +1199,7 @@ function walk(value, schema, path32, errors, depth) {
1173
1199
  if (!(req in obj)) {
1174
1200
  const expected = schema.properties?.[req]?.type;
1175
1201
  errors.push({
1176
- path: joinPath(path32, req),
1202
+ path: joinPath(path33, req),
1177
1203
  message: `required property missing${typeof expected === "string" ? ` (expected ${expected})` : ""}`
1178
1204
  });
1179
1205
  }
@@ -1181,14 +1207,14 @@ function walk(value, schema, path32, errors, depth) {
1181
1207
  if (schema.properties) {
1182
1208
  for (const [key, subSchema] of Object.entries(schema.properties)) {
1183
1209
  if (key in obj) {
1184
- walk(obj[key], subSchema, joinPath(path32, key), errors, depth + 1);
1210
+ walk(obj[key], subSchema, joinPath(path33, key), errors, depth + 1);
1185
1211
  }
1186
1212
  }
1187
1213
  }
1188
1214
  }
1189
1215
  if (schema.type === "array" && Array.isArray(value) && schema.items) {
1190
1216
  for (let i = 0; i < value.length; i++) {
1191
- walk(value[i], schema.items, `${path32}[${i}]`, errors, depth + 1);
1217
+ walk(value[i], schema.items, `${path33}[${i}]`, errors, depth + 1);
1192
1218
  }
1193
1219
  }
1194
1220
  }
@@ -1902,7 +1928,10 @@ var DefaultPluginAPI = class {
1902
1928
  }
1903
1929
  };
1904
1930
  this.tools = {
1905
- register: (t) => tr.register(t, owner),
1931
+ register: (t) => {
1932
+ tr.register(t, owner);
1933
+ tr.exposeToProvider(t.name);
1934
+ },
1906
1935
  unregister: (name) => {
1907
1936
  assertCanMutateTool(name, "unregister");
1908
1937
  return tr.unregister(name);
@@ -7956,6 +7985,15 @@ function resolveAutoReviewConfig(cfg, sessionConfig) {
7956
7985
  maxCascadeDepth: cfg.maxCascadeDepth ?? DEFAULT_MAX_CASCADE_DEPTH
7957
7986
  };
7958
7987
  }
7988
+ function severitiesFromFindings(findings) {
7989
+ const severities = { critical: 0, high: 0, medium: 0 };
7990
+ for (const finding of findings) {
7991
+ if (finding.severity === "critical") severities.critical++;
7992
+ else if (finding.severity === "high") severities.high++;
7993
+ else if (finding.severity === "medium") severities.medium++;
7994
+ }
7995
+ return severities;
7996
+ }
7959
7997
  function parseReviewSeverity(text) {
7960
7998
  const result = { critical: 0, high: 0, medium: 0 };
7961
7999
  if (!text) return result;
@@ -7972,9 +8010,20 @@ function parseReviewSeverity(text) {
7972
8010
  }
7973
8011
  return result;
7974
8012
  }
7975
- function decideCascadeAgents(text, severities) {
8013
+ function decideCascadeAgents(text, severities, findings) {
7976
8014
  const agents = /* @__PURE__ */ new Set();
7977
8015
  if (severities.critical > 0 || severities.high > 0) agents.add("bug-hunter");
8016
+ if (findings && findings.length > 0) {
8017
+ const highPlus = findings.filter(
8018
+ (finding) => finding.severity === "critical" || finding.severity === "high"
8019
+ );
8020
+ if (highPlus.some((finding) => finding.category === "security")) {
8021
+ agents.add("security-scanner");
8022
+ }
8023
+ if (highPlus.every((finding) => finding.category !== void 0)) {
8024
+ return [...agents];
8025
+ }
8026
+ }
7978
8027
  const securityKeywords = [
7979
8028
  "injection",
7980
8029
  "xss",
@@ -8347,7 +8396,9 @@ function createAutoReviewPlugin() {
8347
8396
  maxFiles: cfg.maxFilesPerBatch,
8348
8397
  autoFix: "off",
8349
8398
  cascadeOn: "off",
8350
- maxCascadeDepth: 0
8399
+ maxCascadeDepth: 0,
8400
+ fallbackModels: [...cfg.fallbackModels],
8401
+ fallbackProfile: void 0
8351
8402
  },
8352
8403
  files: filesWithContent,
8353
8404
  activeTodos: ctxTodos,
@@ -8452,7 +8503,9 @@ function createAutoReviewPlugin() {
8452
8503
  maxFiles: cfg.maxFilesPerBatch,
8453
8504
  autoFix: "off",
8454
8505
  cascadeOn: "off",
8455
- maxCascadeDepth: 0
8506
+ maxCascadeDepth: 0,
8507
+ fallbackModels: [...cfg.fallbackModels],
8508
+ fallbackProfile: void 0
8456
8509
  },
8457
8510
  files: filesWithContent,
8458
8511
  cascadeOn: cfg.cascadeOn,
@@ -8494,23 +8547,31 @@ function createAutoReviewPlugin() {
8494
8547
  if (!p.reviewText) return;
8495
8548
  const cascadeOn = p.bundle.cascadeOn ?? "off";
8496
8549
  if (cascadeOn === "off") return;
8497
- const severities = parseReviewSeverity(p.reviewText);
8550
+ const parsed = p.parsedReport;
8551
+ const verifiedFindings = parsed?.findings.filter((f) => f.verification?.status === "verified") ?? [];
8552
+ const severities = parsed ? severitiesFromFindings(verifiedFindings) : parseReviewSeverity(p.reviewText);
8498
8553
  const threshold = shouldCascade(cascadeOn, severities);
8499
8554
  if (!threshold) return;
8500
- const agents = decideCascadeAgents(p.reviewText, severities);
8555
+ const agents = decideCascadeAgents(
8556
+ p.reviewText,
8557
+ severities,
8558
+ parsed ? verifiedFindings : void 0
8559
+ );
8501
8560
  if (agents.length === 0) {
8502
8561
  return;
8503
8562
  }
8504
8563
  const cascadePayload = {
8505
8564
  bundle: p.bundle,
8565
+ ...p.reportId ? { reportId: p.reportId } : {},
8506
8566
  reviewText: p.reviewText,
8507
8567
  severities,
8508
8568
  threshold,
8509
- agents
8569
+ agents,
8570
+ ...parsed ? { verifiedFindings } : {}
8510
8571
  };
8511
8572
  api.emitCustom("chimera.cascade_needed", cascadePayload);
8512
8573
  api.log.info(
8513
- `[auto-review] cascade_needed emitted \u2014 ${severities.critical} critical, ${severities.high} high, ${severities.medium} medium; agents: ${agents.join(", ")}`
8574
+ `[auto-review] cascade_needed emitted \u2014 ${severities.critical} critical, ${severities.high} high, ${severities.medium} medium; agents: ${agents.join(", ")}${parsed ? ` (gated on ${verifiedFindings.length} verified finding(s))` : ""}`
8514
8575
  );
8515
8576
  } catch (err) {
8516
8577
  api.log.warn(
@@ -8542,12 +8603,88 @@ init_review_finding_store();
8542
8603
  // src/plugins/review-finding-parser.ts
8543
8604
  init_review_finding_types();
8544
8605
  import { randomUUID as randomUUID5 } from "node:crypto";
8606
+ var SEVERITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
8607
+ var CATEGORIES = /* @__PURE__ */ new Set([
8608
+ "bug",
8609
+ "security",
8610
+ "performance",
8611
+ "type",
8612
+ "contract",
8613
+ "test",
8614
+ "other"
8615
+ ]);
8616
+ var CONFIDENCES = /* @__PURE__ */ new Set(["high", "medium", "low"]);
8617
+ var FENCED_BLOCK = /```json[ \t]*\r?\n([\s\S]*?)\r?\n```/gi;
8618
+ function extractStructuredFindingsBlock(reportText) {
8619
+ if (!reportText) return null;
8620
+ let best = null;
8621
+ for (const match of reportText.matchAll(FENCED_BLOCK)) {
8622
+ const body = match[1];
8623
+ if (!body?.trim()) continue;
8624
+ let parsed;
8625
+ try {
8626
+ parsed = JSON.parse(body);
8627
+ } catch {
8628
+ continue;
8629
+ }
8630
+ if (typeof parsed !== "object" || parsed === null) continue;
8631
+ const findings = parsed.findings;
8632
+ if (!Array.isArray(findings)) continue;
8633
+ const items = [];
8634
+ for (const raw of findings) {
8635
+ const item = normalizeStructuredItem(raw);
8636
+ if (item) items.push(item);
8637
+ }
8638
+ if (findings.length > 0 && items.length === 0) continue;
8639
+ const durationRaw = parsed.durationSeconds;
8640
+ const durationSeconds = typeof durationRaw === "number" && Number.isFinite(durationRaw) && durationRaw > 0 ? Math.floor(durationRaw) : void 0;
8641
+ best = { findings: items, ...durationSeconds !== void 0 ? { durationSeconds } : {} };
8642
+ }
8643
+ return best;
8644
+ }
8645
+ function normalizeStructuredItem(raw) {
8646
+ if (typeof raw !== "object" || raw === null) return null;
8647
+ const item = raw;
8648
+ const severity = typeof item.severity === "string" ? item.severity.toLowerCase() : "";
8649
+ if (!SEVERITIES.has(severity)) return null;
8650
+ const title = typeof item.title === "string" ? item.title.trim() : "";
8651
+ if (title.length === 0) return null;
8652
+ const file = typeof item.file === "string" && item.file.trim().length > 0 ? item.file.trim() : void 0;
8653
+ const line = typeof item.line === "number" && Number.isInteger(item.line) && item.line >= 1 ? item.line : void 0;
8654
+ const categoryRaw = typeof item.category === "string" ? item.category.toLowerCase() : "";
8655
+ const category = CATEGORIES.has(categoryRaw) ? categoryRaw : void 0;
8656
+ const confidenceRaw = typeof item.confidence === "string" ? item.confidence.toLowerCase() : "";
8657
+ const confidence = CONFIDENCES.has(confidenceRaw) ? confidenceRaw : void 0;
8658
+ return {
8659
+ severity,
8660
+ ...file ? { file } : {},
8661
+ ...line !== void 0 ? { line } : {},
8662
+ ...category ? { category } : {},
8663
+ ...confidence ? { confidence } : {},
8664
+ title,
8665
+ ...typeof item.description === "string" && item.description.trim().length > 0 ? { description: item.description.trim() } : {},
8666
+ ...typeof item.suggestedFix === "string" && item.suggestedFix.trim().length > 0 ? { suggestedFix: item.suggestedFix.trim() } : {}
8667
+ };
8668
+ }
8545
8669
  var SUGGEST_LINE = /^\s*(?:→|->|=>)\s*(.+)$/;
8546
8670
  var DURATION_LINE = /^Duration:\s*(\d+)s\s*$/im;
8547
8671
  function parseChimeraReviewReport(reportText, context = {}) {
8548
8672
  if (!reportText || reportText.trim().length === 0) {
8549
8673
  return { findings: [], unparseableCount: 0 };
8550
8674
  }
8675
+ const structured = extractStructuredFindingsBlock(reportText);
8676
+ if (structured) {
8677
+ const reportId2 = context.reportId ?? randomUUID5();
8678
+ const findings2 = structured.findings.map(
8679
+ (item) => buildFindingFromStructuredItem(item, { ...context, reportId: reportId2 })
8680
+ );
8681
+ return {
8682
+ findings: findings2,
8683
+ unparseableCount: 0,
8684
+ ...structured.durationSeconds !== void 0 ? { durationSeconds: structured.durationSeconds } : {},
8685
+ structured: true
8686
+ };
8687
+ }
8551
8688
  const findings = [];
8552
8689
  const reportId = context.reportId ?? randomUUID5();
8553
8690
  let unparseableCount = 0;
@@ -8679,14 +8816,99 @@ function normalizeFindingSource(reviewType) {
8679
8816
  return "chimera";
8680
8817
  }
8681
8818
  }
8819
+ function buildFindingFromStructuredItem(item, context) {
8820
+ const file = item.file;
8821
+ const line = item.line;
8822
+ const title = item.title;
8823
+ const description = item.description ?? title;
8824
+ return {
8825
+ id: randomUUID5(),
8826
+ fingerprint: computeFindingFingerprint(file ?? "", line ?? null, title),
8827
+ severity: item.severity,
8828
+ source: normalizeFindingSource(context.reviewType),
8829
+ ...file ? { location: { file, ...line !== void 0 ? { line } : {} } } : {},
8830
+ ...item.category ? { category: item.category } : {},
8831
+ ...item.confidence ? { confidence: item.confidence } : {},
8832
+ title,
8833
+ description,
8834
+ ...item.suggestedFix ? { suggestedFix: item.suggestedFix } : {},
8835
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8836
+ status: "active",
8837
+ originReport: {
8838
+ reportId: context.reportId ?? randomUUID5(),
8839
+ sessionId: context.sessionId ?? "",
8840
+ agentId: context.agentId ?? "",
8841
+ reviewerModel: context.reviewerModel ?? ""
8842
+ }
8843
+ };
8844
+ }
8682
8845
 
8683
8846
  // src/plugins/review-report-integration.ts
8684
8847
  init_review_finding_store();
8848
+
8849
+ // src/plugins/review-finding-integration.ts
8850
+ init_review_finding_store();
8851
+ function classifyChimeraReviewSource(bundle) {
8852
+ const cascadeDepth = bundle.cascadeDepth ?? 0;
8853
+ if (cascadeDepth > 0) return "cascade";
8854
+ const cascadeOn = bundle.cascadeOn;
8855
+ if (cascadeOn !== void 0 && cascadeOn !== "off") return "auto";
8856
+ return "chimera";
8857
+ }
8858
+ async function integrateFindings(payload, projectDir, reportId) {
8859
+ if ((!payload.reviewText || payload.reviewText.trim().length === 0) && !payload.parsedReport) {
8860
+ return { created: 0, relinked: 0, reopened: 0, totalFindings: 0, unparseableCount: 0 };
8861
+ }
8862
+ const store = new JsonlFindingStore(projectDir);
8863
+ const source = classifyChimeraReviewSource(payload.bundle);
8864
+ const agentId = payload.bundle.fileProvenance?.find((entry) => entry.agentId)?.agentId ?? "chimera-review";
8865
+ const sessionId = payload.sessionId ?? payload.cwd;
8866
+ const model = payload.bundle.config.model;
8867
+ const parsed = payload.parsedReport ?? parseChimeraReviewReport(payload.reviewText, {
8868
+ sessionId,
8869
+ agentId,
8870
+ reviewerModel: model,
8871
+ reviewType: source,
8872
+ reportId
8873
+ });
8874
+ if (parsed.findings.length === 0) {
8875
+ return {
8876
+ created: 0,
8877
+ relinked: 0,
8878
+ reopened: 0,
8879
+ totalFindings: 0,
8880
+ unparseableCount: parsed.unparseableCount
8881
+ };
8882
+ }
8883
+ const result = await store.upsert(parsed.findings, {
8884
+ sessionId,
8885
+ reportId,
8886
+ agentId,
8887
+ model
8888
+ });
8889
+ return {
8890
+ created: result.created,
8891
+ relinked: result.relinked,
8892
+ reopened: result.reopened,
8893
+ reportId,
8894
+ totalFindings: parsed.findings.length,
8895
+ unparseableCount: parsed.unparseableCount
8896
+ };
8897
+ }
8898
+
8899
+ // src/plugins/review-report-integration.ts
8685
8900
  init_review_report_store();
8901
+ async function updateReviewReportEvidence(reportId, projectDir, evidenceStatus, evidenceChecks) {
8902
+ await new JsonlReportStore(projectDir).updateEvidence(
8903
+ reportId,
8904
+ evidenceStatus,
8905
+ evidenceChecks
8906
+ );
8907
+ }
8686
8908
  async function persistReviewReport(payload, reportId, projectDir) {
8687
8909
  const store = new JsonlReportStore(projectDir);
8688
8910
  const existed = await store.get(reportId);
8689
- const source = classifySource(payload);
8911
+ const source = classifyChimeraReviewSource(payload.bundle);
8690
8912
  const agentId = payload.bundle.fileProvenance?.find((entry) => entry.agentId)?.agentId ?? "chimera-review";
8691
8913
  const sessionId = payload.sessionId ?? payload.cwd;
8692
8914
  const model = payload.bundle.config.model;
@@ -8696,7 +8918,13 @@ async function persistReviewReport(payload, reportId, projectDir) {
8696
8918
  status: f.status
8697
8919
  }));
8698
8920
  const reviewStatus = payload.status === "success" ? "success" : "failed";
8699
- const parsed = reviewStatus === "success" ? parseChimeraReviewReport(payload.reviewText, { reportId }) : { findings: [], unparseableCount: 0, durationSeconds: void 0 };
8921
+ const parsed = reviewStatus === "success" ? payload.parsedReport ?? parseChimeraReviewReport(payload.reviewText, {
8922
+ sessionId,
8923
+ agentId,
8924
+ reviewerModel: model,
8925
+ reviewType: source,
8926
+ reportId
8927
+ }) : { findings: [], unparseableCount: 0, durationSeconds: void 0 };
8700
8928
  const counts = { critical: 0, high: 0, medium: 0, low: 0 };
8701
8929
  for (const finding of parsed.findings) {
8702
8930
  counts[finding.severity]++;
@@ -8714,7 +8942,12 @@ async function persistReviewReport(payload, reportId, projectDir) {
8714
8942
  unparseableCount: parsed.unparseableCount,
8715
8943
  durationSeconds: parsed.durationSeconds,
8716
8944
  rawText: payload.reviewText,
8717
- ...cascadeDepth !== void 0 ? { cascadeDepth } : {}
8945
+ ...cascadeDepth !== void 0 ? { cascadeDepth } : {},
8946
+ // P0-3: carry the cascade evidence verification (status + per-check
8947
+ // comparisons) so the persisted report is auditable. Absent on initial
8948
+ // reviews — no cascade step produced evidence yet.
8949
+ ...payload.bundle.evidenceStatus !== void 0 ? { evidenceStatus: payload.bundle.evidenceStatus } : {},
8950
+ ...payload.bundle.evidenceChecks !== void 0 ? { evidenceChecks: payload.bundle.evidenceChecks } : {}
8718
8951
  };
8719
8952
  await store.persist(input);
8720
8953
  if (reviewStatus === "success" && parsed.findings.length === 0 && parsed.unparseableCount === 0 && isExplicitAllClearReview(payload.reviewText)) {
@@ -8777,13 +9010,6 @@ async function syncReportReopen(reportId, projectDir, actor, reason) {
8777
9010
  });
8778
9011
  return { reportId, reopened: true, previousLifecycle: report.lifecycle };
8779
9012
  }
8780
- function classifySource(payload) {
8781
- const cascadeDepth = payload.bundle.cascadeDepth ?? 0;
8782
- if (cascadeDepth > 0) return "cascade";
8783
- const cascadeOn = payload.bundle.cascadeOn;
8784
- if (cascadeOn !== void 0 && cascadeOn !== "off") return "auto";
8785
- return "chimera";
8786
- }
8787
9013
 
8788
9014
  // src/plugins/review-finding-commands.ts
8789
9015
  async function executeFindingCommand(args, ctx) {
@@ -9113,6 +9339,18 @@ async function showReport(id, ctx) {
9113
9339
  `**Review status:** ${report.reviewStatus}`,
9114
9340
  ...report.cascadeDepth !== void 0 ? [`**Cascade depth:** ${report.cascadeDepth}`] : [],
9115
9341
  ...report.durationSeconds !== void 0 ? [`**Duration:** ${report.durationSeconds}s`] : [],
9342
+ ...report.evidenceStatus !== void 0 ? [
9343
+ `**Evidence:** ${report.evidenceStatus === "verified" ? "\u2705 verified" : report.evidenceStatus === "failed" ? "\u274C failed" : "\u26A0\uFE0F missing"}`,
9344
+ ...report.evidenceChecks && report.evidenceChecks.length > 0 ? [
9345
+ "",
9346
+ ...report.evidenceChecks.map((check) => {
9347
+ const mark = check.ok ? "\u2713" : "\u2717";
9348
+ const claimed = check.claimedExitCode ?? "\u2014";
9349
+ const actual = check.actualExitCode ?? "\u2014";
9350
+ return ` ${mark} \`${check.name}\` \u2014 \`${check.command}\` (claimed ${claimed}, observed ${actual})`;
9351
+ })
9352
+ ] : []
9353
+ ] : [],
9116
9354
  "",
9117
9355
  "**Severity counts:**",
9118
9356
  ` \u{1F534} Critical: ${report.counts.critical}`,
@@ -9221,7 +9459,9 @@ function resolveChimeraConfig(cfg, sessionProvider, sessionModel) {
9221
9459
  maxFiles: cfg.maxFiles ?? DEFAULT_MAX_FILES,
9222
9460
  autoFix: cfg.autoFix ?? "off",
9223
9461
  cascadeOn: cfg.cascadeOn ?? DEFAULT_CASCADE_ON,
9224
- maxCascadeDepth: cfg.maxCascadeDepth ?? DEFAULT_MAX_CASCADE_DEPTH2
9462
+ maxCascadeDepth: cfg.maxCascadeDepth ?? DEFAULT_MAX_CASCADE_DEPTH2,
9463
+ fallbackModels: cfg.fallbackModels ? [...cfg.fallbackModels] : [],
9464
+ fallbackProfile: cfg.fallbackProfile
9225
9465
  };
9226
9466
  }
9227
9467
  var CHIMERA_REVIEW_PROMPT = readBundledInstructionText("llm/chimera-review.md");
@@ -10468,55 +10708,262 @@ function dim(s) {
10468
10708
  return `\x1B[2m${s}\x1B[0m`;
10469
10709
  }
10470
10710
 
10471
- // src/plugins/review-finding-integration.ts
10472
- init_review_finding_store();
10473
- async function integrateFindings(payload, projectDir, reportId) {
10474
- if (!payload.reviewText || payload.reviewText.trim().length === 0) {
10475
- return { created: 0, relinked: 0, reopened: 0, totalFindings: 0, unparseableCount: 0 };
10476
- }
10477
- const store = new JsonlFindingStore(projectDir);
10478
- const source = (payload.bundle.cascadeDepth ?? 0) > 0 ? "cascade" : payload.bundle.cascadeOn !== void 0 && payload.bundle.cascadeOn !== "off" ? "auto" : "chimera";
10479
- const agentId = payload.bundle.fileProvenance?.find((entry) => entry.agentId)?.agentId ?? "chimera-review";
10480
- const sessionId = payload.sessionId ?? payload.cwd;
10481
- const model = payload.bundle.config.model;
10482
- const parsed = parseChimeraReviewReport(payload.reviewText, {
10483
- sessionId,
10484
- agentId,
10485
- reviewerModel: model,
10486
- reviewType: source,
10487
- reportId
10488
- });
10489
- if (parsed.findings.length === 0) {
10490
- return {
10491
- created: 0,
10492
- relinked: 0,
10493
- reopened: 0,
10494
- totalFindings: 0,
10495
- unparseableCount: parsed.unparseableCount
10496
- };
10497
- }
10498
- const result = await store.upsert(parsed.findings, {
10499
- sessionId,
10500
- reportId,
10501
- agentId,
10502
- model
10503
- });
10504
- return {
10505
- created: result.created,
10506
- relinked: result.relinked,
10507
- reopened: result.reopened,
10508
- reportId,
10509
- totalFindings: parsed.findings.length,
10510
- unparseableCount: parsed.unparseableCount
10711
+ // src/plugins/review-finding-verification.ts
10712
+ import * as fsp6 from "node:fs/promises";
10713
+ import * as path22 from "node:path";
10714
+ var DEFAULT_ANCHOR_WINDOW = 2;
10715
+ var STOPWORDS = /* @__PURE__ */ new Set([
10716
+ "a",
10717
+ "about",
10718
+ "above",
10719
+ "after",
10720
+ "again",
10721
+ "against",
10722
+ "all",
10723
+ "an",
10724
+ "and",
10725
+ "any",
10726
+ "are",
10727
+ "as",
10728
+ "at",
10729
+ "be",
10730
+ "been",
10731
+ "before",
10732
+ "below",
10733
+ "between",
10734
+ "both",
10735
+ "but",
10736
+ "by",
10737
+ "can",
10738
+ "could",
10739
+ "did",
10740
+ "do",
10741
+ "does",
10742
+ "each",
10743
+ "few",
10744
+ "for",
10745
+ "from",
10746
+ "further",
10747
+ "get",
10748
+ "had",
10749
+ "has",
10750
+ "have",
10751
+ "he",
10752
+ "her",
10753
+ "here",
10754
+ "him",
10755
+ "his",
10756
+ "how",
10757
+ "if",
10758
+ "in",
10759
+ "into",
10760
+ "is",
10761
+ "it",
10762
+ "its",
10763
+ "just",
10764
+ "may",
10765
+ "me",
10766
+ "might",
10767
+ "more",
10768
+ "most",
10769
+ "much",
10770
+ "must",
10771
+ "my",
10772
+ "new",
10773
+ "no",
10774
+ "nor",
10775
+ "not",
10776
+ "now",
10777
+ "of",
10778
+ "off",
10779
+ "on",
10780
+ "once",
10781
+ "only",
10782
+ "or",
10783
+ "other",
10784
+ "our",
10785
+ "out",
10786
+ "own",
10787
+ "same",
10788
+ "she",
10789
+ "should",
10790
+ "so",
10791
+ "some",
10792
+ "such",
10793
+ "than",
10794
+ "that",
10795
+ "the",
10796
+ "their",
10797
+ "them",
10798
+ "then",
10799
+ "there",
10800
+ "these",
10801
+ "they",
10802
+ "this",
10803
+ "those",
10804
+ "through",
10805
+ "to",
10806
+ "too",
10807
+ "under",
10808
+ "until",
10809
+ "up",
10810
+ "us",
10811
+ "very",
10812
+ "was",
10813
+ "we",
10814
+ "were",
10815
+ "what",
10816
+ "when",
10817
+ "where",
10818
+ "which",
10819
+ "while",
10820
+ "who",
10821
+ "whom",
10822
+ "why",
10823
+ "will",
10824
+ "with",
10825
+ "would",
10826
+ "you",
10827
+ "your",
10828
+ // Finding-prose generics: common English nouns that are terrible anchors
10829
+ // because they match prose everywhere. Kept deliberately small.
10830
+ "area",
10831
+ "bug",
10832
+ "check",
10833
+ "code",
10834
+ "data",
10835
+ "endpoint",
10836
+ "error",
10837
+ "file",
10838
+ "fix",
10839
+ "issue",
10840
+ "line",
10841
+ "make",
10842
+ "may",
10843
+ "potential",
10844
+ "request",
10845
+ "response",
10846
+ "result",
10847
+ "use",
10848
+ "used",
10849
+ "user",
10850
+ "using",
10851
+ "value"
10852
+ ]);
10853
+ function extractFindingAnchor(title, description) {
10854
+ const text = `${title ?? ""} ${description ?? ""}`;
10855
+ const tokens = text.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) ?? [];
10856
+ if (tokens.length === 0) return void 0;
10857
+ const meaningful = tokens.filter(
10858
+ (t) => t.length >= 3 && !STOPWORDS.has(t.toLowerCase())
10859
+ );
10860
+ if (meaningful.length === 0) return void 0;
10861
+ const codeLike = meaningful.filter((t) => /[a-z][A-Z]/.test(t) || t.includes("_") || t.endsWith("$"));
10862
+ if (codeLike.length > 0) {
10863
+ codeLike.sort((a, b) => b.length - a.length);
10864
+ return codeLike[0];
10865
+ }
10866
+ meaningful.sort((a, b) => b.length - a.length);
10867
+ return meaningful[0];
10868
+ }
10869
+ function resolveFindingPath(raw, cwd) {
10870
+ const pathMod = process.platform === "win32" ? path22.win32 : path22;
10871
+ const forward = raw.replace(/\\/g, "/").replace(/^\.\//, "");
10872
+ const isAbsolute3 = pathMod.isAbsolute(forward) || /^[a-zA-Z]:\//.test(forward);
10873
+ const relative5 = isAbsolute3 ? pathMod.relative(cwd, forward).replace(/\\/g, "/") : forward;
10874
+ if (relative5 === ".." || relative5.startsWith("../") || pathMod.isAbsolute(relative5)) return null;
10875
+ const resolved = pathMod.resolve(cwd, relative5);
10876
+ const rel = pathMod.relative(cwd, resolved);
10877
+ if (rel === ".." || rel.startsWith("..") || pathMod.isAbsolute(rel)) return null;
10878
+ return resolved;
10879
+ }
10880
+ async function verifyFindingsAgainstDisk(findings, opts) {
10881
+ const window = opts.anchorWindow ?? DEFAULT_ANCHOR_WINDOW;
10882
+ const cache = /* @__PURE__ */ new Map();
10883
+ const readFile20 = async (abs) => {
10884
+ const cached = cache.get(abs);
10885
+ if (cached) return cached;
10886
+ let result;
10887
+ try {
10888
+ const content = await fsp6.readFile(abs, "utf8");
10889
+ result = { lines: content.split("\n") };
10890
+ } catch (err) {
10891
+ result = { error: err.code === "ENOENT" ? "missing" : "unreadable" };
10892
+ }
10893
+ cache.set(abs, result);
10894
+ return result;
10511
10895
  };
10896
+ return Promise.all(
10897
+ findings.map(async (finding) => {
10898
+ if (!finding.location?.file) return finding;
10899
+ const abs = resolveFindingPath(finding.location.file, opts.cwd);
10900
+ if (abs === null) {
10901
+ return { ...finding, verification: { status: "failed", reason: "outside_workspace" } };
10902
+ }
10903
+ const file = await readFile20(abs);
10904
+ if ("error" in file) {
10905
+ return {
10906
+ ...finding,
10907
+ verification: {
10908
+ status: "failed",
10909
+ reason: file.error === "missing" ? "file_missing" : "unreadable"
10910
+ }
10911
+ };
10912
+ }
10913
+ const anchor = extractFindingAnchor(finding.title, finding.description);
10914
+ if (finding.location.line === void 0) {
10915
+ if (anchor && /[a-z][A-Z]/.test(anchor)) {
10916
+ const hit = file.lines.findIndex((l) => l.includes(anchor));
10917
+ if (hit !== -1) {
10918
+ return {
10919
+ ...finding,
10920
+ verification: {
10921
+ status: "verified",
10922
+ reason: "anchor_found",
10923
+ evidence: trimEvidence(file.lines[hit] ?? "")
10924
+ }
10925
+ };
10926
+ }
10927
+ }
10928
+ return { ...finding, verification: { status: "unverified", reason: "no_line" } };
10929
+ }
10930
+ const line = finding.location.line;
10931
+ if (line < 1 || line > file.lines.length) {
10932
+ return { ...finding, verification: { status: "failed", reason: "line_out_of_range" } };
10933
+ }
10934
+ if (!anchor) {
10935
+ return { ...finding, verification: { status: "unverified", reason: "no_anchor" } };
10936
+ }
10937
+ const from = Math.max(0, line - 1 - window);
10938
+ const to = Math.min(file.lines.length, line - 1 + window + 1);
10939
+ for (let i = from; i < to; i++) {
10940
+ const sourceLine = file.lines[i];
10941
+ if (sourceLine.includes(anchor)) {
10942
+ return {
10943
+ ...finding,
10944
+ verification: {
10945
+ status: "verified",
10946
+ reason: "anchor_found",
10947
+ evidence: trimEvidence(sourceLine)
10948
+ }
10949
+ };
10950
+ }
10951
+ }
10952
+ return { ...finding, verification: { status: "unverified", reason: "anchor_not_found" } };
10953
+ })
10954
+ );
10955
+ }
10956
+ function trimEvidence(line) {
10957
+ const trimmed = line.trim();
10958
+ return trimmed.length > 200 ? `${trimmed.slice(0, 197)}...` : trimmed;
10512
10959
  }
10513
10960
 
10514
10961
  // src/plugins/review-store-maintenance.ts
10515
10962
  init_atomic_write();
10516
10963
  init_review_finding_store();
10517
10964
  init_review_report_store();
10518
- import * as fsp6 from "node:fs/promises";
10519
- import * as path22 from "node:path";
10965
+ import * as fsp7 from "node:fs/promises";
10966
+ import * as path23 from "node:path";
10520
10967
  var REVIEW_STORE_COMPACTION_THRESHOLD_BYTES = 8 * 1024 * 1024;
10521
10968
  var REVIEW_STORE_COMPACTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
10522
10969
  var REVIEW_STORE_MAINTENANCE_FILE = ".review-store-maintenance.json";
@@ -10524,7 +10971,7 @@ async function maybeCompactReviewStores(projectDir, opts = {}) {
10524
10971
  const thresholdBytes = opts.thresholdBytes ?? REVIEW_STORE_COMPACTION_THRESHOLD_BYTES;
10525
10972
  const intervalMs = opts.intervalMs ?? REVIEW_STORE_COMPACTION_INTERVAL_MS;
10526
10973
  const now = opts.now ?? Date.now();
10527
- const maintenancePath = path22.join(projectDir, REVIEW_STORE_MAINTENANCE_FILE);
10974
+ const maintenancePath = path23.join(projectDir, REVIEW_STORE_MAINTENANCE_FILE);
10528
10975
  return withFileLock(maintenancePath, async () => {
10529
10976
  const totalBytes = await fileSize(resolveReportStorePath(projectDir)) + await fileSize(resolveFindingStorePath(projectDir));
10530
10977
  if (totalBytes < thresholdBytes) {
@@ -10564,7 +11011,7 @@ function emptyResult(reason, totalBytes) {
10564
11011
  }
10565
11012
  async function fileSize(filePath) {
10566
11013
  try {
10567
- return (await fsp6.stat(filePath)).size;
11014
+ return (await fsp7.stat(filePath)).size;
10568
11015
  } catch (error) {
10569
11016
  if (error.code === "ENOENT") return 0;
10570
11017
  throw error;
@@ -10572,7 +11019,7 @@ async function fileSize(filePath) {
10572
11019
  }
10573
11020
  async function readMaintenanceState(filePath) {
10574
11021
  try {
10575
- const parsed = JSON.parse(await fsp6.readFile(filePath, "utf8"));
11022
+ const parsed = JSON.parse(await fsp7.readFile(filePath, "utf8"));
10576
11023
  return typeof parsed.compactedAt === "string" && Number.isFinite(parsed.totalBytesBefore) ? parsed : null;
10577
11024
  } catch (error) {
10578
11025
  if (error.code === "ENOENT" || error instanceof SyntaxError) {
@@ -10584,7 +11031,7 @@ async function readMaintenanceState(filePath) {
10584
11031
 
10585
11032
  // src/plugins/skills-plugin.ts
10586
11033
  import * as os7 from "node:os";
10587
- import * as path27 from "node:path";
11034
+ import * as path28 from "node:path";
10588
11035
 
10589
11036
  // src/skills/foreign-sources.ts
10590
11037
  var FOREIGN_SKILL_TOOLS = [
@@ -10767,7 +11214,7 @@ function numField(rec, key) {
10767
11214
  init_errors();
10768
11215
  import { spawn as spawn5 } from "node:child_process";
10769
11216
  import * as fs14 from "node:fs/promises";
10770
- import * as path23 from "node:path";
11217
+ import * as path24 from "node:path";
10771
11218
  async function validateSkillNameAvailable(name, loader) {
10772
11219
  const formatViolations = validateSkillName(name);
10773
11220
  const conflicts = loader ? (await loader.listEntries()).filter((e) => e.name === name) : [];
@@ -10845,7 +11292,7 @@ function extractSkillFromPrompt(prompt) {
10845
11292
  const description = paragraphs[0]?.replace(/^#{1,6}\s+/m, "").trim() ?? titleSource;
10846
11293
  const body = paragraphs.slice(1).join("\n\n");
10847
11294
  const quoted = [...text.matchAll(/"([^"]{2,40})"/g)].map((m) => m[1]).filter((q) => typeof q === "string").map((q) => q.toLowerCase());
10848
- const titleWords = titleSource.split(/\W+/).map((w) => w.toLowerCase()).filter((w) => w.length > 3 && !STOPWORDS.has(w));
11295
+ const titleWords = titleSource.split(/\W+/).map((w) => w.toLowerCase()).filter((w) => w.length > 3 && !STOPWORDS2.has(w));
10849
11296
  const triggerKeywords = dedupe([...quoted, ...titleWords]).slice(0, 8);
10850
11297
  return { suggestedName, description, body, triggerKeywords };
10851
11298
  }
@@ -10891,8 +11338,8 @@ async function writeSkeletonSkill(skillsDir, body, opts = {}) {
10891
11338
  subsystem: "general"
10892
11339
  });
10893
11340
  }
10894
- const skillDir = path23.join(skillsDir, name);
10895
- const skillFile = path23.join(skillDir, "SKILL.md");
11341
+ const skillDir = path24.join(skillsDir, name);
11342
+ const skillFile = path24.join(skillDir, "SKILL.md");
10896
11343
  if (!opts.overwrite) {
10897
11344
  try {
10898
11345
  await fs14.access(skillFile);
@@ -10914,7 +11361,7 @@ function bodyLineAdvisory(body) {
10914
11361
  const lines = body.split("\n").length;
10915
11362
  return { lines, over: lines > SKILL_LIMITS.SKILL_BODY_LINE_LIMIT };
10916
11363
  }
10917
- var STOPWORDS = /* @__PURE__ */ new Set([
11364
+ var STOPWORDS2 = /* @__PURE__ */ new Set([
10918
11365
  "the",
10919
11366
  "and",
10920
11367
  "for",
@@ -10951,14 +11398,14 @@ function defaultEditor() {
10951
11398
  init_errors();
10952
11399
  init_error();
10953
11400
  import * as fs17 from "node:fs/promises";
10954
- import * as path26 from "node:path";
11401
+ import * as path27 from "node:path";
10955
11402
 
10956
11403
  // src/skills/github-fetcher.ts
10957
11404
  init_errors();
10958
11405
  import { createWriteStream } from "node:fs";
10959
11406
  import * as fs15 from "node:fs/promises";
10960
11407
  import * as os6 from "node:os";
10961
- import * as path24 from "node:path";
11408
+ import * as path25 from "node:path";
10962
11409
  import { Readable } from "node:stream";
10963
11410
  import { pipeline } from "node:stream/promises";
10964
11411
  import { createGunzip } from "node:zlib";
@@ -11100,7 +11547,7 @@ async function downloadGitHubTarball(parsed) {
11100
11547
  }
11101
11548
  });
11102
11549
  }
11103
- const tempDir = await fs15.mkdtemp(path24.join(os6.tmpdir(), "wskill-"));
11550
+ const tempDir = await fs15.mkdtemp(path25.join(os6.tmpdir(), "wskill-"));
11104
11551
  try {
11105
11552
  if (!response.body) {
11106
11553
  throw new WrongStackError({
@@ -11111,7 +11558,7 @@ async function downloadGitHubTarball(parsed) {
11111
11558
  });
11112
11559
  }
11113
11560
  const nodeStream = Readable.fromWeb(response.body);
11114
- const tarPath = path24.join(tempDir, ".wrongstack-download.tar");
11561
+ const tarPath = path25.join(tempDir, ".wrongstack-download.tar");
11115
11562
  await writeBoundedGzipStream(nodeStream, tarPath, SKILL_LIMITS.MAX_UNCOMPRESSED_TARBALL_SIZE);
11116
11563
  const tarBuf = await fs15.readFile(tarPath);
11117
11564
  try {
@@ -11176,10 +11623,10 @@ async function extractTar(buf, destDir) {
11176
11623
  const fullPath = prefix ? `${prefix}/${name}` : name;
11177
11624
  const relPath = stripTopDir(fullPath);
11178
11625
  if (relPath && relPath !== "." && relPath !== "..") {
11179
- const destPath = path24.join(destDir, relPath);
11180
- const resolvedDest = path24.resolve(destPath);
11181
- const resolvedRoot = path24.resolve(destDir);
11182
- if (resolvedDest !== resolvedRoot && !resolvedDest.startsWith(resolvedRoot + path24.sep)) {
11626
+ const destPath = path25.join(destDir, relPath);
11627
+ const resolvedDest = path25.resolve(destPath);
11628
+ const resolvedRoot = path25.resolve(destDir);
11629
+ if (resolvedDest !== resolvedRoot && !resolvedDest.startsWith(resolvedRoot + path25.sep)) {
11183
11630
  offset += 512 + Math.ceil(size / 512) * 512;
11184
11631
  continue;
11185
11632
  }
@@ -11189,7 +11636,7 @@ async function extractTar(buf, destDir) {
11189
11636
  }
11190
11637
  }
11191
11638
  if ((typeflag === 48 || typeflag === 0 || typeflag === 0) && size > 0) {
11192
- const dir = path24.dirname(destPath);
11639
+ const dir = path25.dirname(destPath);
11193
11640
  await fs15.mkdir(dir, { recursive: true });
11194
11641
  const dataStart = offset + 512;
11195
11642
  const dataEnd = dataStart + size;
@@ -11204,7 +11651,7 @@ async function extractTar(buf, destDir) {
11204
11651
  // src/skills/manifest-store.ts
11205
11652
  init_atomic_write();
11206
11653
  import * as fs16 from "node:fs/promises";
11207
- import * as path25 from "node:path";
11654
+ import * as path26 from "node:path";
11208
11655
  var SkillManifestStore = class {
11209
11656
  manifestPath;
11210
11657
  cache;
@@ -11227,7 +11674,7 @@ var SkillManifestStore = class {
11227
11674
  return this.cache;
11228
11675
  }
11229
11676
  async write(data) {
11230
- const dir = path25.dirname(this.manifestPath);
11677
+ const dir = path26.dirname(this.manifestPath);
11231
11678
  await fs16.mkdir(dir, { recursive: true });
11232
11679
  await atomicWrite(this.manifestPath, JSON.stringify(data, null, 2) + "\n");
11233
11680
  this.cache = data;
@@ -11266,8 +11713,8 @@ var SkillManifestStore = class {
11266
11713
 
11267
11714
  // src/skills/skill-installer.ts
11268
11715
  function isInside(resolved, destDir) {
11269
- const root = path26.resolve(destDir);
11270
- return resolved === root || resolved.startsWith(root + path26.sep);
11716
+ const root = path27.resolve(destDir);
11717
+ return resolved === root || resolved.startsWith(root + path27.sep);
11271
11718
  }
11272
11719
  var MAX_SKILL_FILE_SIZE = SKILL_LIMITS.MAX_SKILL_FILE_SIZE;
11273
11720
  var SkillInstaller = class {
@@ -11322,13 +11769,13 @@ var SkillInstaller = class {
11322
11769
  this.opts.log?.(`Overwriting existing skill "${skill.name}" (${scope})...`);
11323
11770
  await this.removeSkillFiles(skill.name, scope);
11324
11771
  }
11325
- const destDir = path26.join(targetDir, skill.name);
11772
+ const destDir = path27.join(targetDir, skill.name);
11326
11773
  await fs17.mkdir(destDir, { recursive: true });
11327
11774
  const copiedFiles = [];
11328
11775
  for (const file of skill.files) {
11329
- const srcPath = path26.join(skill.baseDir, file);
11330
- const destPath = path26.join(destDir, file);
11331
- const resolved2 = path26.resolve(destPath);
11776
+ const srcPath = path27.join(skill.baseDir, file);
11777
+ const destPath = path27.join(destDir, file);
11778
+ const resolved2 = path27.resolve(destPath);
11332
11779
  if (!isInside(resolved2, destDir)) {
11333
11780
  throw new FsError({
11334
11781
  message: `Path traversal detected in skill file: ${file}`,
@@ -11346,7 +11793,7 @@ var SkillInstaller = class {
11346
11793
  context: { skillName: skill.name, fileSize: stat8.size, maxSize: MAX_SKILL_FILE_SIZE }
11347
11794
  });
11348
11795
  }
11349
- await fs17.mkdir(path26.dirname(destPath), { recursive: true });
11796
+ await fs17.mkdir(path27.dirname(destPath), { recursive: true });
11350
11797
  await fs17.copyFile(srcPath, destPath);
11351
11798
  copiedFiles.push(file);
11352
11799
  }
@@ -11404,7 +11851,7 @@ var SkillInstaller = class {
11404
11851
  const results = [];
11405
11852
  for (const e of entries) {
11406
11853
  if (!await entryIsDirectory(srcDir, e)) continue;
11407
- const skillMdPath = path26.join(srcDir, e.name, "SKILL.md");
11854
+ const skillMdPath = path27.join(srcDir, e.name, "SKILL.md");
11408
11855
  let content;
11409
11856
  try {
11410
11857
  content = await fs17.readFile(skillMdPath, "utf8");
@@ -11417,15 +11864,15 @@ var SkillInstaller = class {
11417
11864
  if (existing.find((x) => x.scope === scope)) {
11418
11865
  await this.removeSkillFiles(fm.name, scope);
11419
11866
  }
11420
- const destDir = path26.join(targetDir, fm.name);
11867
+ const destDir = path27.join(targetDir, fm.name);
11421
11868
  await fs17.mkdir(destDir, { recursive: true });
11422
- const srcSkillDir = path26.join(srcDir, e.name);
11869
+ const srcSkillDir = path27.join(srcDir, e.name);
11423
11870
  const files = await collectFiles(srcSkillDir, srcSkillDir);
11424
11871
  const copiedFiles = [];
11425
11872
  for (const file of files) {
11426
- const srcPath = path26.join(srcSkillDir, file);
11427
- const destPath = path26.join(destDir, file);
11428
- const resolved = path26.resolve(destPath);
11873
+ const srcPath = path27.join(srcSkillDir, file);
11874
+ const destPath = path27.join(destDir, file);
11875
+ const resolved = path27.resolve(destPath);
11429
11876
  if (!isInside(resolved, destDir)) {
11430
11877
  throw new FsError({
11431
11878
  message: `Path traversal detected in skill file: ${file}`,
@@ -11434,7 +11881,7 @@ var SkillInstaller = class {
11434
11881
  context: { reason: "path_traversal", skillName: fm.name }
11435
11882
  });
11436
11883
  }
11437
- await fs17.mkdir(path26.dirname(destPath), { recursive: true });
11884
+ await fs17.mkdir(path27.dirname(destPath), { recursive: true });
11438
11885
  if (opts?.link) {
11439
11886
  try {
11440
11887
  await fs17.symlink(srcPath, destPath);
@@ -11597,7 +12044,7 @@ var SkillInstaller = class {
11597
12044
  */
11598
12045
  async detectSkills(baseDir) {
11599
12046
  const results = [];
11600
- const rootSkillMd = path26.join(baseDir, "SKILL.md");
12047
+ const rootSkillMd = path27.join(baseDir, "SKILL.md");
11601
12048
  try {
11602
12049
  await fs17.access(rootSkillMd);
11603
12050
  const content = await fs17.readFile(rootSkillMd, "utf8");
@@ -11612,17 +12059,17 @@ var SkillInstaller = class {
11612
12059
  }
11613
12060
  } catch {
11614
12061
  }
11615
- const skillsDir = path26.join(baseDir, "skills");
12062
+ const skillsDir = path27.join(baseDir, "skills");
11616
12063
  try {
11617
12064
  const entries = await fs17.readdir(skillsDir, { withFileTypes: true });
11618
12065
  for (const entry of entries) {
11619
12066
  if (!entry.isDirectory()) continue;
11620
- const skillFile = path26.join(skillsDir, entry.name, "SKILL.md");
12067
+ const skillFile = path27.join(skillsDir, entry.name, "SKILL.md");
11621
12068
  try {
11622
12069
  const content = await fs17.readFile(skillFile, "utf8");
11623
12070
  const fm = parseSkillFrontmatter(content);
11624
12071
  if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
11625
- const skillDir = path26.join(skillsDir, entry.name);
12072
+ const skillDir = path27.join(skillsDir, entry.name);
11626
12073
  const files = await collectFiles(skillDir, skillDir);
11627
12074
  results.push({
11628
12075
  name: fm.name,
@@ -11655,8 +12102,8 @@ var SkillInstaller = class {
11655
12102
  */
11656
12103
  async removeSkillFiles(name, scope) {
11657
12104
  const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
11658
- const root = path26.resolve(targetDir);
11659
- const skillDir = path26.resolve(path26.join(targetDir, name));
12105
+ const root = path27.resolve(targetDir);
12106
+ const skillDir = path27.resolve(path27.join(targetDir, name));
11660
12107
  if (skillDir === root || !isInside(skillDir, root)) {
11661
12108
  throw new FsError({
11662
12109
  message: `Refusing to delete skill files outside the skills directory: ${name}`,
@@ -11700,7 +12147,7 @@ async function entryIsDirectory(dir, entry) {
11700
12147
  if (entry.isDirectory()) return true;
11701
12148
  if (entry.isSymbolicLink()) {
11702
12149
  try {
11703
- return (await fs17.stat(path26.join(dir, entry.name))).isDirectory();
12150
+ return (await fs17.stat(path27.join(dir, entry.name))).isDirectory();
11704
12151
  } catch {
11705
12152
  return false;
11706
12153
  }
@@ -11711,8 +12158,8 @@ async function collectFiles(dir, baseDir) {
11711
12158
  const results = [];
11712
12159
  const entries = await fs17.readdir(dir, { withFileTypes: true });
11713
12160
  for (const entry of entries) {
11714
- const fullPath = path26.join(dir, entry.name);
11715
- const relPath = path26.relative(baseDir, fullPath);
12161
+ const fullPath = path27.join(dir, entry.name);
12162
+ const relPath = path27.relative(baseDir, fullPath);
11716
12163
  if (entry.isDirectory()) {
11717
12164
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
11718
12165
  results.push(...await collectFiles(fullPath, baseDir));
@@ -11791,7 +12238,7 @@ function createSkillsPlugin(opts) {
11791
12238
  function makeInstaller(skillLoader, projectRoot, registryAdapters) {
11792
12239
  const paths = resolveWstackPaths({ projectRoot });
11793
12240
  return new SkillInstaller({
11794
- manifestPath: path27.join(paths.configDir, "installed-skills.json"),
12241
+ manifestPath: path28.join(paths.configDir, "installed-skills.json"),
11795
12242
  projectSkillsDir: paths.inProjectSkills,
11796
12243
  globalSkillsDir: paths.globalSkills,
11797
12244
  projectHash: paths.projectHash,
@@ -12170,7 +12617,7 @@ function resolveImportSourceDir(tool, opts) {
12170
12617
  const entry = IMPORT_SOURCE_TOOLS.find((t) => t.id === tool);
12171
12618
  if (!entry) return void 0;
12172
12619
  const base = opts.global ? opts.homeDir ?? os7.homedir() : opts.projectRoot;
12173
- return path27.join(base, "." + entry.id, entry.subdir);
12620
+ return path28.join(base, "." + entry.id, entry.subdir);
12174
12621
  }
12175
12622
  function buildSkillImportCommand(skillLoader) {
12176
12623
  return {
@@ -12211,7 +12658,7 @@ function buildSkillImportCommand(skillLoader) {
12211
12658
  };
12212
12659
  }
12213
12660
  } else if (positional) {
12214
- srcDir = path27.resolve(ctx.projectRoot, positional);
12661
+ srcDir = path28.resolve(ctx.projectRoot, positional);
12215
12662
  } else {
12216
12663
  return {
12217
12664
  message: "Usage: /skill-import <src-dir> | --from <tool> | --from-claude [--global] [--link]"
@@ -12338,14 +12785,14 @@ function truncate(s, max) {
12338
12785
  }
12339
12786
 
12340
12787
  // src/plugins/sync-plugin.ts
12341
- import * as path29 from "node:path";
12788
+ import * as path30 from "node:path";
12342
12789
  init_error();
12343
12790
 
12344
12791
  // src/storage/cloud-sync.ts
12345
12792
  init_atomic_write();
12346
12793
  init_errors();
12347
12794
  import * as fs18 from "node:fs/promises";
12348
- import * as path28 from "node:path";
12795
+ import * as path29 from "node:path";
12349
12796
  import { createHash as createHash9 } from "node:crypto";
12350
12797
  var ALL_SYNC_CATEGORIES = ["settings", "skills", "prompts", "memory", "history"];
12351
12798
  var CloudSync = class {
@@ -12353,8 +12800,8 @@ var CloudSync = class {
12353
12800
  this.paths = paths;
12354
12801
  this.getConfig = getConfig;
12355
12802
  this.setConfig = setConfig;
12356
- this.statePath = path28.join(paths.configDir, "sync-state.json");
12357
- this.getSettingsConfigPath = getSettingsConfigPath ?? (() => path28.join(this.paths.configDir, "config.json"));
12803
+ this.statePath = path29.join(paths.configDir, "sync-state.json");
12804
+ this.getSettingsConfigPath = getSettingsConfigPath ?? (() => path29.join(this.paths.configDir, "config.json"));
12358
12805
  }
12359
12806
  paths;
12360
12807
  getConfig;
@@ -12640,7 +13087,7 @@ var CloudSync = class {
12640
13087
  const files = await this.walkDir(localPath, localPath);
12641
13088
  for (const file of files) {
12642
13089
  const content = await fs18.readFile(file, "utf8");
12643
- const rel = path28.relative(localPath, file).replace(/\\/g, "/");
13090
+ const rel = path29.relative(localPath, file).replace(/\\/g, "/");
12644
13091
  entries.push({ path: `data/${cat}/${rel}`, content, mode: "100644" });
12645
13092
  hashes.push(`${cat}/${rel}\0${content}`);
12646
13093
  }
@@ -12667,7 +13114,7 @@ var CloudSync = class {
12667
13114
  const files = await this.walkDir(localPath, localPath);
12668
13115
  for (const file of files) {
12669
13116
  const content = await fs18.readFile(file, "utf8");
12670
- const rel = path28.relative(localPath, file).replace(/\\/g, "/");
13117
+ const rel = path29.relative(localPath, file).replace(/\\/g, "/");
12671
13118
  hashes.push(`${cat}/${rel}\0${content}`);
12672
13119
  }
12673
13120
  } else {
@@ -12701,7 +13148,7 @@ var CloudSync = class {
12701
13148
  const entries = await fs18.readdir(dir, { withFileTypes: true });
12702
13149
  entries.sort((a, b) => a.name.localeCompare(b.name));
12703
13150
  for (const entry of entries) {
12704
- const full = path28.join(dir, entry.name);
13151
+ const full = path29.join(dir, entry.name);
12705
13152
  if (entry.isSymbolicLink()) continue;
12706
13153
  if (entry.isDirectory()) {
12707
13154
  results.push(...await this.walkDir(full, base));
@@ -12714,17 +13161,17 @@ var CloudSync = class {
12714
13161
  };
12715
13162
  async function preparePulledDestination(cat, localPath, destPath, remotePath) {
12716
13163
  const directoryBacked = cat === "skills" || cat === "prompts";
12717
- const rootPath = directoryBacked ? localPath : path28.dirname(localPath);
13164
+ const rootPath = directoryBacked ? localPath : path29.dirname(localPath);
12718
13165
  const rootStat = await lstatIfExists(rootPath);
12719
13166
  if (rootStat?.isSymbolicLink()) {
12720
13167
  throw unsafePulledSymlinkError(remotePath, rootPath);
12721
13168
  }
12722
13169
  if (directoryBacked) {
12723
- const relativeParent = path28.relative(rootPath, path28.dirname(destPath));
13170
+ const relativeParent = path29.relative(rootPath, path29.dirname(destPath));
12724
13171
  let cursor = rootPath;
12725
13172
  if (relativeParent) {
12726
- for (const segment of relativeParent.split(path28.sep)) {
12727
- cursor = path28.join(cursor, segment);
13173
+ for (const segment of relativeParent.split(path29.sep)) {
13174
+ cursor = path29.join(cursor, segment);
12728
13175
  const stat8 = await lstatIfExists(cursor);
12729
13176
  if (stat8?.isSymbolicLink()) throw unsafePulledSymlinkError(remotePath, cursor);
12730
13177
  }
@@ -12732,7 +13179,7 @@ async function preparePulledDestination(cat, localPath, destPath, remotePath) {
12732
13179
  }
12733
13180
  const destStat = await lstatIfExists(destPath);
12734
13181
  if (destStat?.isSymbolicLink()) throw unsafePulledSymlinkError(remotePath, destPath);
12735
- await fs18.mkdir(path28.dirname(destPath), { recursive: true });
13182
+ await fs18.mkdir(path29.dirname(destPath), { recursive: true });
12736
13183
  }
12737
13184
  async function lstatIfExists(filePath) {
12738
13185
  try {
@@ -12762,9 +13209,9 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
12762
13209
  return localPath;
12763
13210
  }
12764
13211
  if (!rel) return localPath;
12765
- const normalizedRel = path28.normalize(rel);
12766
- const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path28.sep}`);
12767
- if (path28.isAbsolute(normalizedRel) || traversesUp) {
13212
+ const normalizedRel = path29.normalize(rel);
13213
+ const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path29.sep}`);
13214
+ if (path29.isAbsolute(normalizedRel) || traversesUp) {
12768
13215
  throw new FsError({
12769
13216
  message: `Refusing CloudSync path traversal: ${remotePath}`,
12770
13217
  code: ERROR_CODES.FS_DELETE_FAILED,
@@ -12772,10 +13219,10 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
12772
13219
  context: { reason: "path_traversal", normalizedRel }
12773
13220
  });
12774
13221
  }
12775
- const dest = path28.resolve(localPath, normalizedRel);
12776
- const root = path28.resolve(localPath);
12777
- const relative5 = path28.relative(root, dest);
12778
- if (relative5.startsWith("..") || path28.isAbsolute(relative5)) {
13222
+ const dest = path29.resolve(localPath, normalizedRel);
13223
+ const root = path29.resolve(localPath);
13224
+ const relative5 = path29.relative(root, dest);
13225
+ if (relative5.startsWith("..") || path29.isAbsolute(relative5)) {
12779
13226
  throw new FsError({
12780
13227
  message: `Refusing CloudSync path outside category root: ${remotePath}`,
12781
13228
  code: ERROR_CODES.FS_DELETE_FAILED,
@@ -12818,7 +13265,7 @@ function createSyncPlugin(opts) {
12818
13265
  api.log.warn("[sync] paths, configStore, or configDir not available \u2014 /sync disabled");
12819
13266
  return;
12820
13267
  }
12821
- const syncConfigPath = paths.syncConfig ?? path29.join(paths.configDir, "sync.json");
13268
+ const syncConfigPath = paths.syncConfig ?? path30.join(paths.configDir, "sync.json");
12822
13269
  cloud = new CloudSync(
12823
13270
  paths,
12824
13271
  () => {
@@ -12829,7 +13276,7 @@ function createSyncPlugin(opts) {
12829
13276
  await persistSyncConfig(syncConfigPath, cfg, vault);
12830
13277
  configStore?.update({ sync: cfg });
12831
13278
  },
12832
- () => path29.join(paths.configDir, "config.json")
13279
+ () => path30.join(paths.configDir, "config.json")
12833
13280
  );
12834
13281
  void cloud.loadState();
12835
13282
  api.slashCommands.register(buildSyncCommand(cloud, configStore, vault, syncConfigPath));
@@ -13021,7 +13468,7 @@ Warning: sync completed, but metadata persistence failed: ${toErrorMessage(err)}
13021
13468
  }
13022
13469
 
13023
13470
  // src/plugins/cloud-config-sync-plugin.ts
13024
- import * as path31 from "node:path";
13471
+ import * as path32 from "node:path";
13025
13472
 
13026
13473
  // src/security/config-secrets.ts
13027
13474
  function decryptConfigSecrets(cfg, vault, opts) {
@@ -13070,7 +13517,7 @@ function isSecretField(name) {
13070
13517
  init_atomic_write();
13071
13518
  import { createHash as createHash10, randomUUID as nodeRandomUUID } from "node:crypto";
13072
13519
  import * as fs19 from "node:fs/promises";
13073
- import * as path30 from "node:path";
13520
+ import * as path31 from "node:path";
13074
13521
 
13075
13522
  // src/storage/cloud-config-sync/sanitize.ts
13076
13523
  var SAGE_TREE = {
@@ -13645,7 +14092,7 @@ var CloudConfigSync = class {
13645
14092
  }
13646
14093
  async saveState(state) {
13647
14094
  this.state = state;
13648
- await fs19.mkdir(path30.dirname(this.deps.statePath), { recursive: true });
14095
+ await fs19.mkdir(path31.dirname(this.deps.statePath), { recursive: true });
13649
14096
  await atomicWrite(this.deps.statePath, `${JSON.stringify(state, null, 2)}
13650
14097
  `, {
13651
14098
  mode: 384
@@ -13692,8 +14139,8 @@ function createCloudConfigSyncPlugin(opts) {
13692
14139
  );
13693
14140
  return;
13694
14141
  }
13695
- const profileConfigPath = path31.join(paths.configDir, "config.json");
13696
- const statePath = path31.join(paths.configDir, "cloud-sync-state.json");
14142
+ const profileConfigPath = path32.join(paths.configDir, "config.json");
14143
+ const statePath = path32.join(paths.configDir, "cloud-sync-state.json");
13697
14144
  const warn = (msg) => api.log.warn(msg);
13698
14145
  const readLocalConfig = async () => {
13699
14146
  const raw = await readJsonObjectFile(profileConfigPath).catch(() => ({}));
@@ -13870,6 +14317,7 @@ export {
13870
14317
  DefaultPluginAPI,
13871
14318
  KERNEL_API_VERSION,
13872
14319
  buildReviewerModelPool,
14320
+ classifyChimeraReviewSource,
13873
14321
  createAutoReviewPlugin,
13874
14322
  createChimeraPlugin,
13875
14323
  createCloudConfigSyncPlugin,
@@ -13894,6 +14342,8 @@ export {
13894
14342
  resolvePluginManifestConfig,
13895
14343
  selectRoundRobinReviewerAssignment,
13896
14344
  unloadPlugins,
13897
- validatePluginConfigMetadata
14345
+ updateReviewReportEvidence,
14346
+ validatePluginConfigMetadata,
14347
+ verifyFindingsAgainstDisk
13898
14348
  };
13899
14349
  //# sourceMappingURL=index.js.map