@wrongstack/core 0.299.0 → 0.301.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/dist/chronicle/index.d.ts +2 -0
  2. package/dist/chronicle/index.js +297 -10
  3. package/dist/chronicle/project-server.js +253 -15
  4. package/dist/chronicle/sqlite-compaction.d.ts +20 -0
  5. package/dist/chronicle/sqlite-journal.d.ts +57 -0
  6. package/dist/coordination/agents/capability-manifest.d.ts +7 -0
  7. package/dist/coordination/agents/index.d.ts +3 -2
  8. package/dist/coordination/agents/index.js +818 -570
  9. package/dist/coordination/agents/project-agent-identity.d.ts +6 -6
  10. package/dist/coordination/agents/role-skills.d.ts +1 -1
  11. package/dist/coordination/agents/types.d.ts +6 -0
  12. package/dist/coordination/director.d.ts +10 -2
  13. package/dist/coordination/fleet-manager.d.ts +48 -3
  14. package/dist/coordination/ifleet-manager.d.ts +2 -0
  15. package/dist/coordination/index.js +6880 -6440
  16. package/dist/coordination/mail-tools.d.ts +3 -3
  17. package/dist/coordination/mailbox-project-server.js +3 -4
  18. package/dist/coordination/mailbox-types.d.ts +6 -0
  19. package/dist/coordination/multi-agent-coordinator.d.ts +1 -0
  20. package/dist/core/agent-response.d.ts +5 -1
  21. package/dist/core/agent-tools.d.ts +3 -0
  22. package/dist/core/context.d.ts +20 -0
  23. package/dist/core/fallback-model.d.ts +48 -0
  24. package/dist/core/index.d.ts +11 -9
  25. package/dist/core/index.js +1119 -310
  26. package/dist/core/instruction-template.d.ts +83 -0
  27. package/dist/core/next-steps-slot.d.ts +88 -0
  28. package/dist/core/system-prompt-blocks.d.ts +10 -1
  29. package/dist/core/system-prompt-builder.d.ts +40 -4
  30. package/dist/core/system-prompt-memory-skills.d.ts +1 -0
  31. package/dist/core/system-prompt-skill-bodies.d.ts +3 -3
  32. package/dist/defaults/index.js +9805 -8767
  33. package/dist/design/index.js +11 -3
  34. package/dist/execution/auto-compaction-middleware.d.ts +17 -0
  35. package/dist/execution/autonomy-brain.d.ts +15 -2
  36. package/dist/execution/brain-runtime.d.ts +3 -1
  37. package/dist/execution/compaction-core.d.ts +41 -2
  38. package/dist/execution/council-brain.d.ts +47 -1
  39. package/dist/execution/council-orchestrator.d.ts +25 -6
  40. package/dist/execution/council-prompts.d.ts +12 -1
  41. package/dist/execution/index.d.ts +11 -10
  42. package/dist/execution/index.js +10507 -9189
  43. package/dist/execution/tool-executor.d.ts +4 -1
  44. package/dist/execution/topic-shift-advisor.d.ts +53 -0
  45. package/dist/extension/index.js +8 -2
  46. package/dist/extension/registry.d.ts +8 -1
  47. package/dist/fleet-notifier.d.ts +9 -2
  48. package/dist/goal/index.js +11 -3
  49. package/dist/hooks/index.js +50 -13
  50. package/dist/hooks/runner.d.ts +12 -1
  51. package/dist/hq/index.js +59 -21
  52. package/dist/hq/protocol/fleet.d.ts +20 -0
  53. package/dist/hq/protocol.js +10 -0
  54. package/dist/hq/publisher.d.ts +21 -3
  55. package/dist/index.d.ts +22 -20
  56. package/dist/index.js +12211 -8572
  57. package/dist/infrastructure/index.js +108 -21
  58. package/dist/kernel/events/brain-events.d.ts +9 -0
  59. package/dist/kernel/events/provider-events.d.ts +42 -1
  60. package/dist/kernel/events/session-events.d.ts +13 -0
  61. package/dist/kernel/events/tool-events.d.ts +3 -3
  62. package/dist/models/index.d.ts +1 -0
  63. package/dist/models/index.js +18 -1
  64. package/dist/models/provider-credentials.d.ts +54 -0
  65. package/dist/plugin/api.d.ts +6 -0
  66. package/dist/plugin/config.d.ts +55 -0
  67. package/dist/plugin/index.d.ts +2 -1
  68. package/dist/plugin/index.js +2179 -544
  69. package/dist/plugins/auto-review-plugin.d.ts +3 -0
  70. package/dist/plugins/cloud-config-sync-plugin.d.ts +22 -0
  71. package/dist/plugins/review-claim-registry.d.ts +23 -8
  72. package/dist/plugins/review-types.d.ts +2 -0
  73. package/dist/registry/index.js +109 -74
  74. package/dist/registry/tool-registry.d.ts +15 -0
  75. package/dist/security/capabilities.d.ts +2 -0
  76. package/dist/security/index.d.ts +2 -2
  77. package/dist/security/index.js +166 -44
  78. package/dist/security/permission-helpers.d.ts +23 -6
  79. package/dist/security/permission-policy.d.ts +16 -0
  80. package/dist/security/readonly-permission-policy.d.ts +20 -0
  81. package/dist/security/totp.d.ts +14 -0
  82. package/dist/session-registry-atomic-file.d.ts +32 -5
  83. package/dist/skills/frontmatter.d.ts +6 -0
  84. package/dist/skills/index.js +22 -5
  85. package/dist/storage/cloud-config-sync/sanitize.d.ts +54 -0
  86. package/dist/storage/cloud-config-sync.d.ts +87 -0
  87. package/dist/storage/director-state.d.ts +7 -0
  88. package/dist/storage/index.d.ts +1 -0
  89. package/dist/storage/index.js +887 -74
  90. package/dist/tools/fallback-system-config-view-tool.d.ts +1 -1
  91. package/dist/tools/index.d.ts +1 -1
  92. package/dist/tools/index.js +898 -335
  93. package/dist/tools/one-shot-llm-tool.d.ts +1 -0
  94. package/dist/tools/plugin-manager.d.ts +27 -0
  95. package/dist/types/config/mcp-features.d.ts +18 -11
  96. package/dist/types/config/root.d.ts +8 -1
  97. package/dist/types/config/runtime.d.ts +20 -6
  98. package/dist/types/config/skills-fleet-brain.d.ts +12 -4
  99. package/dist/types/config/tools.d.ts +16 -0
  100. package/dist/types/context-window.d.ts +1 -0
  101. package/dist/types/council.d.ts +11 -0
  102. package/dist/types/hooks.d.ts +14 -0
  103. package/dist/types/index.d.ts +3 -3
  104. package/dist/types/index.js +1 -0
  105. package/dist/types/multi-agent.d.ts +12 -0
  106. package/dist/types/one-shot-llm.d.ts +25 -0
  107. package/dist/types/plugin.d.ts +28 -0
  108. package/dist/types/provider.d.ts +16 -0
  109. package/dist/types/runtime-capability-manifest.d.ts +168 -0
  110. package/dist/types/skill.d.ts +5 -0
  111. package/dist/types/system-prompt.d.ts +3 -1
  112. package/dist/types/tool-executor.d.ts +8 -1
  113. package/dist/utils/context-breakdown.d.ts +8 -2
  114. package/dist/utils/index.d.ts +1 -1
  115. package/dist/utils/index.js +141 -31
  116. package/dist/utils/regex-guard.d.ts +16 -2
  117. package/dist/utils/sage-output-block.d.ts +7 -10
  118. package/dist/utils/wstack-paths.d.ts +11 -3
  119. package/dist/worktree/index.js +4 -4
  120. package/instructions/agents/browser.md +1 -4
  121. package/instructions/agents/e2e.md +17 -15
  122. package/instructions/agents/ios.md +3 -3
  123. package/instructions/agents/search.md +1 -1
  124. package/instructions/autonomy/goal-preamble.md +4 -4
  125. package/instructions/coordination/director-preamble.md +2 -3
  126. package/instructions/coordination/subagent-baseline.md +3 -1
  127. package/instructions/leader-after-task.md +12 -0
  128. package/instructions/llm/chimera-review.md +1 -1
  129. package/instructions/modes/audit-lite.md +1 -1
  130. package/instructions/sections/tool/common-patterns.md +18 -2
  131. package/instructions/sections/tool/mailbox-compact.md +1 -1
  132. package/instructions/sections/tool/mailbox-full.md +1 -1
  133. package/instructions/system-lite.md +91 -3
  134. package/instructions/system-pro.md +296 -102
  135. package/instructions/system.md +248 -92
  136. package/package.json +3 -3
  137. package/skills/api-design/SKILL.md +3 -0
  138. package/skills/audit-log/SKILL.md +2 -0
  139. package/skills/auto-review/SKILL.md +6 -1
  140. package/skills/bug-hunter/SKILL.md +4 -1
  141. package/skills/chimera/SKILL.md +3 -0
  142. package/skills/data-governance/SKILL.md +3 -0
  143. package/skills/design-system/SKILL.md +5 -2
  144. package/skills/docker-deploy/SKILL.md +2 -0
  145. package/skills/git-flow/SKILL.md +2 -0
  146. package/skills/mailbox-bridge/SKILL.md +3 -0
  147. package/skills/mnemosyne/SKILL.md +2 -0
  148. package/skills/multi-agent/SKILL.md +4 -1
  149. package/skills/node-modern/SKILL.md +4 -1
  150. package/skills/observability/SKILL.md +3 -0
  151. package/skills/output-standards/SKILL.md +2 -0
  152. package/skills/plugin-author/SKILL.md +4 -1
  153. package/skills/prompt-engineering/SKILL.md +3 -1
  154. package/skills/react-modern/SKILL.md +6 -3
  155. package/skills/refactor-planner/SKILL.md +2 -0
  156. package/skills/research-web/SKILL.md +3 -0
  157. package/skills/sdd/SKILL.md +3 -1
  158. package/skills/security-scanner/SKILL.md +3 -0
  159. package/skills/skill-creator/SKILL.md +2 -0
  160. package/skills/tech-stack/SKILL.md +3 -0
  161. package/skills/testing/SKILL.md +4 -1
  162. package/skills/typescript-strict/SKILL.md +4 -1
  163. package/skills/wrongstack-kanban/SKILL.md +6 -3
  164. package/skills/wrongstack-mailbox/SKILL.md +4 -1
  165. package/skills/wrongstack-mailbox-mcp/SKILL.md +10 -8
@@ -277,11 +277,11 @@ __export(review_finding_store_exports, {
277
277
  JsonlFindingStore: () => JsonlFindingStore,
278
278
  resolveFindingStorePath: () => resolveFindingStorePath
279
279
  });
280
- import { randomUUID as randomUUID3 } from "node:crypto";
281
- import * as fsp2 from "node:fs/promises";
282
- import * as path15 from "node:path";
280
+ import { randomUUID as randomUUID4 } from "node:crypto";
281
+ import * as fsp3 from "node:fs/promises";
282
+ import * as path16 from "node:path";
283
283
  function resolveFindingStorePath(projectDir) {
284
- return path15.join(projectDir, FINDING_STORE_FILE);
284
+ return path16.join(projectDir, FINDING_STORE_FILE);
285
285
  }
286
286
  var FINDING_RESOLVED_RETENTION_MS, FINDING_IGNORED_RETENTION_MS, FINDING_DEFAULT_PAGE_SIZE, NL, LINE_SEPARATOR, FINDING_STORE_FILE, JsonlFindingStore;
287
287
  var init_review_finding_store = __esm({
@@ -325,7 +325,7 @@ var init_review_finding_store = __esm({
325
325
  match.finding.status = "active";
326
326
  match.finding.resolution = void 0;
327
327
  const updatedRecord = { __finding: 1, data: match.finding };
328
- await fsp2.appendFile(
328
+ await fsp3.appendFile(
329
329
  this.filePath,
330
330
  JSON.stringify(updatedRecord) + LINE_SEPARATOR + JSON.stringify({ __findingEvent: 1, data: reopenEvent }) + LINE_SEPARATOR,
331
331
  { encoding: "utf8", mode: SECRET_FILE_MODE }
@@ -339,7 +339,7 @@ var init_review_finding_store = __esm({
339
339
  match.finding.status,
340
340
  context
341
341
  );
342
- await fsp2.appendFile(
342
+ await fsp3.appendFile(
343
343
  this.filePath,
344
344
  JSON.stringify({ __findingEvent: 1, data: relinkEvent }) + LINE_SEPARATOR,
345
345
  { encoding: "utf8", mode: SECRET_FILE_MODE }
@@ -350,7 +350,7 @@ var init_review_finding_store = __esm({
350
350
  const record = { __finding: 1, data: finding };
351
351
  const createdEvent = this._makeEvent(finding.id, "created", null, "active", context);
352
352
  const lines = JSON.stringify(record) + LINE_SEPARATOR + JSON.stringify({ __findingEvent: 1, data: createdEvent }) + LINE_SEPARATOR;
353
- await fsp2.appendFile(this.filePath, lines, { encoding: "utf8", mode: SECRET_FILE_MODE });
353
+ await fsp3.appendFile(this.filePath, lines, { encoding: "utf8", mode: SECRET_FILE_MODE });
354
354
  result.created++;
355
355
  }
356
356
  }
@@ -366,7 +366,7 @@ var init_review_finding_store = __esm({
366
366
  validateTransition(from, to);
367
367
  validateResolution(to, opts?.outcome);
368
368
  const event = {
369
- id: randomUUID3(),
369
+ id: randomUUID4(),
370
370
  findingId,
371
371
  eventType: to === "resolved" ? "resolved" : to === "ignored" ? "ignored" : this._eventTypeFor(to),
372
372
  fromStatus: from,
@@ -386,7 +386,7 @@ var init_review_finding_store = __esm({
386
386
  };
387
387
  }
388
388
  const updatedRecord = { __finding: 1, data: entry.finding };
389
- await fsp2.appendFile(
389
+ await fsp3.appendFile(
390
390
  this.filePath,
391
391
  JSON.stringify(updatedRecord) + LINE_SEPARATOR + JSON.stringify({ __findingEvent: 1, data: event }) + LINE_SEPARATOR,
392
392
  { encoding: "utf8", mode: SECRET_FILE_MODE }
@@ -503,7 +503,7 @@ var init_review_finding_store = __esm({
503
503
  // ── Private helpers ──────────────────────────────────────────────
504
504
  async _readAllLines() {
505
505
  try {
506
- const raw = await fsp2.readFile(this.filePath, "utf8");
506
+ const raw = await fsp3.readFile(this.filePath, "utf8");
507
507
  return raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
508
508
  } catch (err) {
509
509
  if (err.code === "ENOENT") return [];
@@ -550,7 +550,7 @@ var init_review_finding_store = __esm({
550
550
  }
551
551
  _makeEvent(findingId, eventType, fromStatus, toStatus, context) {
552
552
  return {
553
- id: randomUUID3(),
553
+ id: randomUUID4(),
554
554
  findingId,
555
555
  eventType,
556
556
  fromStatus,
@@ -634,11 +634,11 @@ __export(review_report_store_exports, {
634
634
  REPORT_STORE_FILE: () => REPORT_STORE_FILE,
635
635
  resolveReportStorePath: () => resolveReportStorePath
636
636
  });
637
- import { randomUUID as randomUUID5 } from "node:crypto";
638
- import * as fsp3 from "node:fs/promises";
639
- import * as path16 from "node:path";
637
+ import { randomUUID as randomUUID6 } from "node:crypto";
638
+ import * as fsp4 from "node:fs/promises";
639
+ import * as path17 from "node:path";
640
640
  function resolveReportStorePath(projectDir) {
641
- return path16.join(projectDir, REPORT_STORE_FILE);
641
+ return path17.join(projectDir, REPORT_STORE_FILE);
642
642
  }
643
643
  var REPORT_RETENTION_MS, REPORT_DEFAULT_PAGE_SIZE, NL2, REPORT_STORE_FILE, JsonlReportStore;
644
644
  var init_review_report_store = __esm({
@@ -673,7 +673,7 @@ var init_review_report_store = __esm({
673
673
  rawText: input.rawText || existing.rawText,
674
674
  files: input.files.length > 0 ? input.files : existing.files
675
675
  };
676
- await fsp3.appendFile(this.filePath, JSON.stringify({ __report: 1, data: updated }) + NL2, {
676
+ await fsp4.appendFile(this.filePath, JSON.stringify({ __report: 1, data: updated }) + NL2, {
677
677
  encoding: "utf8",
678
678
  mode: SECRET_FILE_MODE
679
679
  });
@@ -697,7 +697,7 @@ var init_review_report_store = __esm({
697
697
  ...input.cascadeDepth !== void 0 ? { cascadeDepth: input.cascadeDepth } : {}
698
698
  };
699
699
  const createdEvent = {
700
- id: randomUUID5(),
700
+ id: randomUUID6(),
701
701
  reportId: report.id,
702
702
  eventType: "created",
703
703
  fromLifecycle: null,
@@ -707,7 +707,7 @@ var init_review_report_store = __esm({
707
707
  timestamp: report.reviewedAt
708
708
  };
709
709
  const lines = JSON.stringify({ __report: 1, data: report }) + NL2 + JSON.stringify({ __reportEvent: 1, data: createdEvent }) + NL2;
710
- await fsp3.appendFile(this.filePath, lines, { encoding: "utf8", mode: SECRET_FILE_MODE });
710
+ await fsp4.appendFile(this.filePath, lines, { encoding: "utf8", mode: SECRET_FILE_MODE });
711
711
  return report;
712
712
  });
713
713
  }
@@ -720,7 +720,7 @@ var init_review_report_store = __esm({
720
720
  const from = this._materialize(entry).lifecycle;
721
721
  validateReportTransition(from, to);
722
722
  const event = {
723
- id: randomUUID5(),
723
+ id: randomUUID6(),
724
724
  reportId,
725
725
  eventType: reportEventTypeFor(to),
726
726
  fromLifecycle: from,
@@ -731,7 +731,7 @@ var init_review_report_store = __esm({
731
731
  reason: opts?.reason
732
732
  };
733
733
  entry.report.lifecycle = to;
734
- await fsp3.appendFile(this.filePath, JSON.stringify({ __reportEvent: 1, data: event }) + NL2, {
734
+ await fsp4.appendFile(this.filePath, JSON.stringify({ __reportEvent: 1, data: event }) + NL2, {
735
735
  encoding: "utf8",
736
736
  mode: SECRET_FILE_MODE
737
737
  });
@@ -745,7 +745,7 @@ var init_review_report_store = __esm({
745
745
  if (!entry) throw new Error(`Review report not found: ${reportId}`);
746
746
  const materialized = this._materialize(entry);
747
747
  const event = {
748
- id: randomUUID5(),
748
+ id: randomUUID6(),
749
749
  reportId,
750
750
  eventType: "note_added",
751
751
  fromLifecycle: materialized.lifecycle,
@@ -755,7 +755,7 @@ var init_review_report_store = __esm({
755
755
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
756
756
  reason: note
757
757
  };
758
- await fsp3.appendFile(this.filePath, JSON.stringify({ __reportEvent: 1, data: event }) + NL2, {
758
+ await fsp4.appendFile(this.filePath, JSON.stringify({ __reportEvent: 1, data: event }) + NL2, {
759
759
  encoding: "utf8",
760
760
  mode: SECRET_FILE_MODE
761
761
  });
@@ -850,7 +850,7 @@ var init_review_report_store = __esm({
850
850
  // ── Private helpers ──────────────────────────────────────────────
851
851
  async _readAllLines() {
852
852
  try {
853
- const raw = await fsp3.readFile(this.filePath, "utf8");
853
+ const raw = await fsp4.readFile(this.filePath, "utf8");
854
854
  return raw.split(NL2).filter((l) => l.trim().length > 0);
855
855
  } catch (err) {
856
856
  if (err.code === "ENOENT") return [];
@@ -1078,9 +1078,15 @@ var ExtensionRegistry = class {
1078
1078
  * Build a composed provider runner. Extensions with `wrapProviderRunner`
1079
1079
  * form a middleware-style chain: the innermost extension wraps the
1080
1080
  * default runner, each subsequent wrapper wraps the previous.
1081
+ *
1082
+ * Utility calls may exclude agent-loop-only middleware (notably
1083
+ * `fallback-model`) while retaining request-safety wrappers such as the
1084
+ * prompt firewall. One-shot orchestration owns its provider and fallback
1085
+ * route, so an agent-loop fallback wrapper cannot safely run against it.
1081
1086
  */
1082
- wrapProviderRunner(inner) {
1083
- const wrappers = this.extensions.filter((e) => e.wrapProviderRunner).map((e) => ({ name: e.name, wrap: expectDefined(e.wrapProviderRunner) }));
1087
+ wrapProviderRunner(inner, options) {
1088
+ const excluded = new Set(options?.exclude);
1089
+ const wrappers = this.extensions.filter((e) => e.wrapProviderRunner && !excluded.has(e.name)).map((e) => ({ name: e.name, wrap: expectDefined(e.wrapProviderRunner) }));
1084
1090
  if (wrappers.length === 0) return inner;
1085
1091
  let composed = inner;
1086
1092
  for (let i = wrappers.length - 1; i >= 0; i--) {
@@ -1135,10 +1141,10 @@ function validateAgainstSchema(value, schema) {
1135
1141
  return { ok: errors.length === 0, errors };
1136
1142
  }
1137
1143
  var MAX_SCHEMA_DEPTH = 64;
1138
- function walk(value, schema, path29, errors, depth) {
1144
+ function walk(value, schema, path32, errors, depth) {
1139
1145
  if (depth > MAX_SCHEMA_DEPTH) {
1140
1146
  errors.push({
1141
- path: path29 || "<root>",
1147
+ path: path32 || "<root>",
1142
1148
  message: `schema nesting exceeds maximum depth (${MAX_SCHEMA_DEPTH})`
1143
1149
  });
1144
1150
  return;
@@ -1146,7 +1152,7 @@ function walk(value, schema, path29, errors, depth) {
1146
1152
  if (schema.enum !== void 0) {
1147
1153
  if (!enumIncludes(schema.enum, value)) {
1148
1154
  errors.push({
1149
- path: path29 || "<root>",
1155
+ path: path32 || "<root>",
1150
1156
  message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
1151
1157
  });
1152
1158
  return;
@@ -1155,7 +1161,7 @@ function walk(value, schema, path29, errors, depth) {
1155
1161
  if (typeof schema.type === "string") {
1156
1162
  if (!checkType(value, schema.type)) {
1157
1163
  errors.push({
1158
- path: path29 || "<root>",
1164
+ path: path32 || "<root>",
1159
1165
  message: `expected ${schema.type}, got ${describeType(value)} (${previewValue(value)})`
1160
1166
  });
1161
1167
  return;
@@ -1167,7 +1173,7 @@ function walk(value, schema, path29, errors, depth) {
1167
1173
  if (!(req in obj)) {
1168
1174
  const expected = schema.properties?.[req]?.type;
1169
1175
  errors.push({
1170
- path: joinPath(path29, req),
1176
+ path: joinPath(path32, req),
1171
1177
  message: `required property missing${typeof expected === "string" ? ` (expected ${expected})` : ""}`
1172
1178
  });
1173
1179
  }
@@ -1175,14 +1181,14 @@ function walk(value, schema, path29, errors, depth) {
1175
1181
  if (schema.properties) {
1176
1182
  for (const [key, subSchema] of Object.entries(schema.properties)) {
1177
1183
  if (key in obj) {
1178
- walk(obj[key], subSchema, joinPath(path29, key), errors, depth + 1);
1184
+ walk(obj[key], subSchema, joinPath(path32, key), errors, depth + 1);
1179
1185
  }
1180
1186
  }
1181
1187
  }
1182
1188
  }
1183
1189
  if (schema.type === "array" && Array.isArray(value) && schema.items) {
1184
1190
  for (let i = 0; i < value.length; i++) {
1185
- walk(value[i], schema.items, `${path29}[${i}]`, errors, depth + 1);
1191
+ walk(value[i], schema.items, `${path32}[${i}]`, errors, depth + 1);
1186
1192
  }
1187
1193
  }
1188
1194
  }
@@ -1275,6 +1281,36 @@ function resolvePluginConfig(input) {
1275
1281
  merge(input.explicitOptions, "explicit-options");
1276
1282
  return { options, configured, sources };
1277
1283
  }
1284
+ function pluginEntryMatchesName(configuredName, name, aliases = []) {
1285
+ for (const candidate of [name, ...aliases]) {
1286
+ if (configuredName === candidate) return true;
1287
+ if (configuredName === `@wrongstack/plugins/${candidate}`) return true;
1288
+ }
1289
+ return false;
1290
+ }
1291
+ function resolvePluginEnablement(input) {
1292
+ if (input.config?.features?.plugins === false) {
1293
+ return { enabled: false, source: "feature-flag" };
1294
+ }
1295
+ const names = [.../* @__PURE__ */ new Set([input.name, ...input.aliases ?? []])];
1296
+ const matches2 = input.matches ?? ((configuredName) => pluginEntryMatchesName(configuredName, input.name, input.aliases ?? []));
1297
+ const plugins = input.config?.plugins;
1298
+ if (Array.isArray(plugins)) {
1299
+ for (const candidate of plugins) {
1300
+ if (typeof candidate === "string") {
1301
+ if (matches2(candidate)) return { enabled: true, source: "plugin-entry" };
1302
+ continue;
1303
+ }
1304
+ if (!isPluginEntry(candidate) || !matches2(candidate.name)) continue;
1305
+ return { enabled: candidate.enabled !== false, source: "plugin-entry" };
1306
+ }
1307
+ }
1308
+ for (const name of names) {
1309
+ const enabled = input.config?.extensions?.[name]?.["enabled"];
1310
+ if (typeof enabled === "boolean") return { enabled, source: "extension" };
1311
+ }
1312
+ return { enabled: input.defaultState === "active", source: "default" };
1313
+ }
1278
1314
  function resolvePluginManifestConfig(plugin, config, explicitOptions) {
1279
1315
  return resolvePluginConfig({
1280
1316
  name: plugin.name,
@@ -1740,11 +1776,12 @@ function wrapApiForCapabilityCheck(plugin, api, log, enforce = false) {
1740
1776
  });
1741
1777
  const wrappedLlm = caps.llm !== false || !api.llm ? api.llm : new Proxy(api.llm, {
1742
1778
  get(target, prop, receiver) {
1743
- if (prop === "complete") {
1779
+ if (prop === "complete" || prop === "council") {
1744
1780
  return (prompt, options) => {
1745
- violate("llm", `complete(${prompt.length} chars)`);
1746
- const complete = target.complete;
1747
- return options === void 0 ? complete(prompt) : complete(prompt, options);
1781
+ violate("llm", `${String(prop)}(${prompt.length} chars)`);
1782
+ const method = target[prop];
1783
+ if (!method) return void 0;
1784
+ return options === void 0 ? method.call(target, prompt) : method.call(target, prompt, options);
1748
1785
  };
1749
1786
  }
1750
1787
  return Reflect.get(target, prop, receiver);
@@ -1973,6 +2010,8 @@ function makePluginLLM(owner, hostLLM, providerRegistry, config, getLiveConfig,
1973
2010
  const currentModel = () => hostLLM.getModel?.() ?? hostLLM.model;
1974
2011
  const DEFAULT_MAX_TOKENS = 2048;
1975
2012
  const HARD_MAX_TOKENS = 32768;
2013
+ const DEFAULT_TIMEOUT_MS = 3e4;
2014
+ const HARD_TIMEOUT_MS = 12e4;
1976
2015
  const pluginDefaults = () => {
1977
2016
  const extensions = currentConfig().extensions;
1978
2017
  const raw = extensions?.[owner]?.["llm"];
@@ -1982,7 +2021,11 @@ function makePluginLLM(owner, hostLLM, providerRegistry, config, getLiveConfig,
1982
2021
  ...typeof r["provider"] === "string" && r["provider"] ? { provider: r["provider"] } : {},
1983
2022
  ...typeof r["model"] === "string" && r["model"] ? { model: r["model"] } : {},
1984
2023
  ...typeof r["maxTokens"] === "number" ? { maxTokens: r["maxTokens"] } : {},
1985
- ...typeof r["temperature"] === "number" ? { temperature: r["temperature"] } : {}
2024
+ ...typeof r["temperature"] === "number" ? { temperature: r["temperature"] } : {},
2025
+ ...typeof r["role"] === "string" && r["role"] ? { role: r["role"] } : {},
2026
+ ...Array.isArray(r["fallbackModels"]) && r["fallbackModels"].every((v) => typeof v === "string") ? { fallbackModels: [...r["fallbackModels"]] } : {},
2027
+ ...typeof r["timeoutMs"] === "number" ? { timeoutMs: r["timeoutMs"] } : {},
2028
+ ...typeof r["councilProfile"] === "string" && r["councilProfile"] ? { councilProfile: r["councilProfile"] } : {}
1986
2029
  };
1987
2030
  };
1988
2031
  const resolveProvider = (name, model) => {
@@ -2019,7 +2062,7 @@ function makePluginLLM(owner, hostLLM, providerRegistry, config, getLiveConfig,
2019
2062
  providerCache.set(cacheKey, created);
2020
2063
  return { provider: created, providerName };
2021
2064
  };
2022
- return {
2065
+ const pluginLlm = {
2023
2066
  defaults() {
2024
2067
  const d = pluginDefaults();
2025
2068
  return {
@@ -2030,12 +2073,49 @@ function makePluginLLM(owner, hostLLM, providerRegistry, config, getLiveConfig,
2030
2073
  async complete(prompt, opts) {
2031
2074
  const defaults = pluginDefaults();
2032
2075
  const model = opts?.model ?? defaults.model ?? currentModel();
2033
- const { provider, providerName } = resolveProvider(opts?.provider, model);
2076
+ const providerName = opts?.provider ?? defaults.provider ?? currentProvider().id;
2034
2077
  const maxTokens = Math.min(
2035
2078
  HARD_MAX_TOKENS,
2036
2079
  opts?.maxTokens ?? defaults.maxTokens ?? DEFAULT_MAX_TOKENS
2037
2080
  );
2038
2081
  const temperature = opts?.temperature ?? defaults.temperature;
2082
+ const timeoutMs = Math.min(
2083
+ HARD_TIMEOUT_MS,
2084
+ Math.max(1, opts?.timeoutMs ?? defaults.timeoutMs ?? DEFAULT_TIMEOUT_MS)
2085
+ );
2086
+ if (hostLLM.oneShot) {
2087
+ metrics.counter("llm.calls", 1, { provider: providerName, model, engine: "one-shot" });
2088
+ const result = await hostLLM.oneShot({
2089
+ userPrompt: prompt,
2090
+ providerId: providerName,
2091
+ model,
2092
+ maxTokens,
2093
+ timeoutMs,
2094
+ ...temperature !== void 0 ? { temperature } : {},
2095
+ ...opts?.system ? { system: opts.system } : {},
2096
+ ...opts?.responseFormat === "json" ? { responseFormat: { type: "json_object" } } : {},
2097
+ ...opts?.signal ? { signal: opts.signal } : {},
2098
+ ...opts?.role ?? defaults.role ? { role: opts?.role ?? defaults.role } : {},
2099
+ ...opts?.fallbackModels ?? defaults.fallbackModels ? { fallbackModels: [...opts?.fallbackModels ?? defaults.fallbackModels ?? []] } : {}
2100
+ });
2101
+ if (result.error) {
2102
+ metrics.counter("llm.errors", 1, { provider: result.provider, model: result.model });
2103
+ throw new Error(result.error);
2104
+ }
2105
+ metrics.counter("llm.tokens_in", result.tokens.input);
2106
+ metrics.counter("llm.tokens_out", result.tokens.output);
2107
+ return {
2108
+ text: result.text,
2109
+ model: result.model,
2110
+ provider: result.provider,
2111
+ usage: { input: result.tokens.input, output: result.tokens.output },
2112
+ stopReason: result.stopReason ?? "end_turn",
2113
+ fromFallback: result.fromFallback,
2114
+ attempts: result.attempts,
2115
+ durationMs: result.durationMs
2116
+ };
2117
+ }
2118
+ const { provider } = resolveProvider(providerName, model);
2039
2119
  const request = {
2040
2120
  model,
2041
2121
  messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
@@ -2044,7 +2124,8 @@ function makePluginLLM(owner, hostLLM, providerRegistry, config, getLiveConfig,
2044
2124
  ...opts?.system ? { system: [{ type: "text", text: opts.system }] } : {},
2045
2125
  ...opts?.responseFormat === "json" ? { responseFormat: { type: "json_object" } } : {}
2046
2126
  };
2047
- const signal = opts?.signal ?? new AbortController().signal;
2127
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
2128
+ const signal = opts?.signal ? AbortSignal.any([opts.signal, timeoutSignal]) : timeoutSignal;
2048
2129
  metrics.counter("llm.calls", 1, { provider: providerName, model });
2049
2130
  try {
2050
2131
  const response = await provider.complete(request, { signal });
@@ -2069,6 +2150,33 @@ function makePluginLLM(owner, hostLLM, providerRegistry, config, getLiveConfig,
2069
2150
  }
2070
2151
  }
2071
2152
  };
2153
+ const council = hostLLM.council;
2154
+ if (council) {
2155
+ pluginLlm.council = async (question, opts) => {
2156
+ const trimmed = question.trim();
2157
+ if (!trimmed) throw new Error("Plugin Council question must not be empty.");
2158
+ if (trimmed.length > 2e4) {
2159
+ throw new Error("Plugin Council question must not exceed 20000 characters.");
2160
+ }
2161
+ if ((opts?.context?.length ?? 0) > 8e4) {
2162
+ throw new Error("Plugin Council context must not exceed 80000 characters.");
2163
+ }
2164
+ const defaults = pluginDefaults();
2165
+ metrics.counter("llm.council.calls", 1, { plugin: owner });
2166
+ const result = await council({
2167
+ question: trimmed,
2168
+ ...opts?.context ? { context: opts.context } : {},
2169
+ ...opts?.options ? { options: opts.options } : {},
2170
+ ...opts?.profile ?? defaults.councilProfile ? { profile: opts?.profile ?? defaults.councilProfile } : {},
2171
+ ...opts?.signal ? { signal: opts.signal } : {}
2172
+ });
2173
+ metrics.counter("llm.tokens_in", result.usage.inputTokens);
2174
+ metrics.counter("llm.tokens_out", result.usage.outputTokens);
2175
+ if (result.status === "failed") metrics.counter("llm.errors", 1, { provider: "council" });
2176
+ return result;
2177
+ };
2178
+ }
2179
+ return pluginLlm;
2072
2180
  }
2073
2181
  function scopedMetrics(sink, pluginName) {
2074
2182
  const prefix = `plugin.${pluginName}.`;
@@ -2107,8 +2215,8 @@ function definePlugin(metadata, factory) {
2107
2215
  // src/plugins/auto-review-plugin.ts
2108
2216
  import { spawn as spawn3 } from "node:child_process";
2109
2217
  import { createHash as createHash7 } from "node:crypto";
2110
- import * as fsp from "node:fs/promises";
2111
- import * as path14 from "node:path";
2218
+ import * as fsp2 from "node:fs/promises";
2219
+ import * as path15 from "node:path";
2112
2220
 
2113
2221
  // src/core/model-availability-calendar.ts
2114
2222
  function logicalCalendarTarget(providerId, model) {
@@ -2470,14 +2578,327 @@ var FREEZER_EMPTY = Object.freeze([]);
2470
2578
  init_error();
2471
2579
 
2472
2580
  // src/plugins/review-claim-registry.ts
2473
- import { createHash } from "node:crypto";
2581
+ import { createHash, randomUUID } from "node:crypto";
2582
+ import * as fsp from "node:fs/promises";
2583
+ import { hostname } from "node:os";
2584
+ import * as path from "node:path";
2585
+
2586
+ // src/utils/pid.ts
2587
+ function isPidAlive(pid) {
2588
+ if (!Number.isInteger(pid) || pid <= 0) return false;
2589
+ if (pid === process.pid) return true;
2590
+ try {
2591
+ process.kill(pid, 0);
2592
+ return true;
2593
+ } catch (err) {
2594
+ const code = err.code;
2595
+ if (code === "EPERM") return true;
2596
+ return false;
2597
+ }
2598
+ }
2599
+
2600
+ // src/plugins/review-claim-registry.ts
2601
+ var CLAIMS_FILE = "review-claims.jsonl";
2602
+ var DEFAULT_CLAIM_TTL_MS = 30 * 60 * 1e3;
2603
+ var LOCK_WAIT_MS = 5e3;
2604
+ var LOCK_RETRY_MAX_MS = 50;
2605
+ var LOCK_STALE_MS = 3e4;
2606
+ var SAME_HOST_STALE_MS = 2 * LOCK_STALE_MS;
2607
+ var LOCK_RETRY_BUDGET_MS = SAME_HOST_STALE_MS + 5e3;
2608
+ var CLAIM_STORE_LOCK_TIMEOUT = "CLAIM_STORE_LOCK_TIMEOUT";
2609
+ var HOST_NAME = hostname();
2610
+ function lockOwnerStamp() {
2611
+ return `${HOST_NAME}:${process.pid}`;
2612
+ }
2613
+ var lastFallbackWarnAt = 0;
2614
+ var FALLBACK_WARN_INTERVAL_MS = 6e4;
2615
+ function warnFallbackOnce(message) {
2616
+ const now = Date.now();
2617
+ if (now - lastFallbackWarnAt < FALLBACK_WARN_INTERVAL_MS) return;
2618
+ lastFallbackWarnAt = now;
2619
+ console.warn(message);
2620
+ }
2621
+ async function breakStaleLockVerified(lockPath, verify) {
2622
+ if (!await verify()) return false;
2623
+ return await breakLockAtomically(lockPath) === true;
2624
+ }
2625
+ async function breakStaleLock(lockPath) {
2626
+ try {
2627
+ const ownerRaw = await fsp.readFile(lockPath, "utf8").catch(() => null);
2628
+ if (ownerRaw !== null) {
2629
+ const [ownerHost, pidRaw] = ownerRaw.trim().split(":", 2);
2630
+ const ownerPid = Number.parseInt(pidRaw ?? "", 10);
2631
+ if (ownerHost === HOST_NAME && Number.isInteger(ownerPid) && ownerPid > 0) {
2632
+ if (isPidAlive(ownerPid)) {
2633
+ const st2 = await fsp.stat(lockPath);
2634
+ if (Date.now() - st2.mtimeMs > SAME_HOST_STALE_MS) {
2635
+ return await breakStaleLockVerified(lockPath, async () => {
2636
+ const st22 = await fsp.stat(lockPath);
2637
+ return Date.now() - st22.mtimeMs > SAME_HOST_STALE_MS;
2638
+ });
2639
+ }
2640
+ return false;
2641
+ }
2642
+ return await breakStaleLockVerified(lockPath, async () => {
2643
+ const content = await fsp.readFile(lockPath, "utf8").catch(() => "");
2644
+ return content.trim() === ownerRaw.trim();
2645
+ });
2646
+ }
2647
+ }
2648
+ const st = await fsp.stat(lockPath);
2649
+ if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
2650
+ return await breakStaleLockVerified(lockPath, async () => {
2651
+ const st2 = await fsp.stat(lockPath);
2652
+ return Date.now() - st2.mtimeMs > LOCK_STALE_MS;
2653
+ });
2654
+ }
2655
+ } catch {
2656
+ }
2657
+ return false;
2658
+ }
2659
+ async function breakLockAtomically(lockPath) {
2660
+ const tombstone = `${lockPath}.stale-${randomUUID()}.tmp`;
2661
+ try {
2662
+ await fsp.rename(lockPath, tombstone);
2663
+ } catch {
2664
+ return false;
2665
+ }
2666
+ void fsp.unlink(tombstone).catch(() => void 0);
2667
+ return true;
2668
+ }
2669
+ async function withStoreLock(storeDir, fn, waitMs = LOCK_WAIT_MS) {
2670
+ const lockPath = lockFilePath(storeDir);
2671
+ await fsp.mkdir(storeDir, { recursive: true });
2672
+ const deadline = Date.now() + waitMs;
2673
+ let attempt = 0;
2674
+ for (; ; ) {
2675
+ let handle = await fsp.open(lockPath, "wx").catch(() => null);
2676
+ if (!handle && await breakStaleLock(lockPath)) {
2677
+ handle = await fsp.open(lockPath, "wx").catch(() => null);
2678
+ }
2679
+ if (handle) {
2680
+ let stampFailed = false;
2681
+ try {
2682
+ let stamped = false;
2683
+ for (let stampAttempt = 0; stampAttempt < 3 && !stamped; stampAttempt++) {
2684
+ try {
2685
+ await handle.writeFile(lockOwnerStamp());
2686
+ stamped = true;
2687
+ } catch {
2688
+ if (stampAttempt < 2) await new Promise((resolve16) => setTimeout(resolve16, 5));
2689
+ }
2690
+ }
2691
+ if (!stamped) {
2692
+ stampFailed = true;
2693
+ throw new Error("failed to stamp claim lock owner");
2694
+ }
2695
+ return await fn();
2696
+ } finally {
2697
+ await handle.close().catch(() => void 0);
2698
+ if (stampFailed) {
2699
+ await fsp.unlink(lockPath).catch(() => void 0);
2700
+ } else {
2701
+ const owner = await fsp.readFile(lockPath, "utf8").catch(() => null);
2702
+ if (owner !== null && owner.trim() === lockOwnerStamp()) {
2703
+ await fsp.unlink(lockPath).catch(() => void 0);
2704
+ }
2705
+ }
2706
+ }
2707
+ }
2708
+ const remaining = deadline - Date.now();
2709
+ if (remaining <= 0) {
2710
+ throw Object.assign(new Error("review claim store lock timeout"), {
2711
+ code: CLAIM_STORE_LOCK_TIMEOUT
2712
+ });
2713
+ }
2714
+ const delay2 = Math.min(LOCK_RETRY_MAX_MS * (attempt + 1), remaining);
2715
+ await new Promise((resolve16) => setTimeout(resolve16, delay2));
2716
+ attempt += 1;
2717
+ }
2718
+ }
2719
+ var MAX_LEDGER_LINES = 1e4;
2720
+ var HOST_SID = randomUUID();
2474
2721
  var claimsByEventBus = /* @__PURE__ */ new WeakMap();
2475
2722
  var startedReviews = /* @__PURE__ */ new WeakMap();
2723
+ var pendingStartedReviews = /* @__PURE__ */ new WeakMap();
2476
2724
  function fingerprint(content) {
2477
2725
  return createHash("sha256").update(content).digest("hex");
2478
2726
  }
2727
+ function normalizeKeyPart(p) {
2728
+ const forward = p.replace(/\\/g, "/").replace(/\/+$/, "");
2729
+ return process.platform === "win32" ? forward.toLowerCase() : forward;
2730
+ }
2479
2731
  function claimKey(cwd, filePath) {
2480
- return `${cwd}\0${filePath}`;
2732
+ return `${normalizeKeyPart(cwd)}\0${normalizeKeyPart(filePath)}`;
2733
+ }
2734
+ function resolveStore(opts, cwd) {
2735
+ return {
2736
+ storeDir: opts?.storeDir ?? path.join(cwd, ".wrongstack"),
2737
+ ttlMs: opts?.ttlMs ?? DEFAULT_CLAIM_TTL_MS,
2738
+ maxLedgerLines: opts?.maxLedgerLines ?? MAX_LEDGER_LINES
2739
+ };
2740
+ }
2741
+ function claimsFilePath(storeDir) {
2742
+ return path.join(storeDir, CLAIMS_FILE);
2743
+ }
2744
+ function lockFilePath(storeDir) {
2745
+ return path.join(storeDir, `${CLAIMS_FILE}.lock`);
2746
+ }
2747
+ async function readLedger(storeDir, now) {
2748
+ const active = /* @__PURE__ */ new Map();
2749
+ const raw = await fsp.readFile(claimsFilePath(storeDir), "utf8").catch((err) => {
2750
+ const code = err.code;
2751
+ if (code === "ENOENT") return "";
2752
+ throw err;
2753
+ });
2754
+ for (const line of raw.split("\n")) {
2755
+ if (!line.trim()) continue;
2756
+ let entry;
2757
+ try {
2758
+ const parsed = JSON.parse(line);
2759
+ if (parsed && typeof parsed === "object" && (parsed.op === "claim" || parsed.op === "release") && typeof parsed.key === "string" && typeof parsed.fp === "string" && typeof parsed.sid === "string" && typeof parsed.ts === "number" && (typeof parsed.exp === "undefined" || typeof parsed.exp === "number")) {
2760
+ entry = parsed;
2761
+ }
2762
+ } catch {
2763
+ }
2764
+ if (!entry) continue;
2765
+ let bucket = active.get(entry.key);
2766
+ if (!bucket) {
2767
+ bucket = /* @__PURE__ */ new Map();
2768
+ active.set(entry.key, bucket);
2769
+ }
2770
+ if (entry.op === "claim") {
2771
+ const exp = entry.exp ?? entry.ts + DEFAULT_CLAIM_TTL_MS;
2772
+ if (now > exp) continue;
2773
+ bucket.set(entry.fp, { sid: entry.sid, ts: entry.ts, exp });
2774
+ } else {
2775
+ const existing = bucket.get(entry.fp);
2776
+ if (existing?.sid === entry.sid && existing.ts === entry.ts) bucket.delete(entry.fp);
2777
+ }
2778
+ }
2779
+ return active;
2780
+ }
2781
+ async function ledgerLineCount(storeDir) {
2782
+ const raw = await fsp.readFile(claimsFilePath(storeDir), "utf8").catch(() => "");
2783
+ return raw.split("\n").filter((l) => l.trim()).length;
2784
+ }
2785
+ async function compactLedger(storeDir, active) {
2786
+ const lines = [];
2787
+ for (const [key, fps] of active) {
2788
+ for (const [fp, claim] of fps) {
2789
+ lines.push(
2790
+ JSON.stringify({
2791
+ op: "claim",
2792
+ key,
2793
+ fp,
2794
+ sid: claim.sid,
2795
+ ts: claim.ts,
2796
+ exp: claim.exp
2797
+ })
2798
+ );
2799
+ }
2800
+ }
2801
+ const tmp = `${claimsFilePath(storeDir)}.tmp-${randomUUID()}`;
2802
+ await fsp.writeFile(tmp, lines.length > 0 ? `${lines.join("\n")}
2803
+ ` : "", "utf8");
2804
+ let replaced = false;
2805
+ for (let attempt = 0; attempt < 3 && !replaced; attempt++) {
2806
+ try {
2807
+ await fsp.rename(tmp, claimsFilePath(storeDir));
2808
+ replaced = true;
2809
+ } catch {
2810
+ if (attempt === 2) {
2811
+ await fsp.copyFile(tmp, claimsFilePath(storeDir)).catch(async (copyErr) => {
2812
+ throw copyErr;
2813
+ });
2814
+ await fsp.unlink(tmp).catch(() => void 0);
2815
+ replaced = true;
2816
+ } else {
2817
+ await new Promise((resolve16) => setTimeout(resolve16, 10 * (attempt + 1)));
2818
+ }
2819
+ }
2820
+ }
2821
+ }
2822
+ async function acquireClaims(storeDir, ttlMs, maxLedgerLines, cwd, files, waitMs = LOCK_WAIT_MS) {
2823
+ return withStoreLock(
2824
+ storeDir,
2825
+ async () => {
2826
+ const now = Date.now();
2827
+ const active = await readLedger(storeDir, now);
2828
+ const claims = [];
2829
+ const lines = [];
2830
+ for (const file of files) {
2831
+ const key = claimKey(cwd, file.path);
2832
+ const fp = fingerprint(file.content);
2833
+ let bucket = active.get(key);
2834
+ if (bucket?.has(fp)) continue;
2835
+ if (!bucket) {
2836
+ bucket = /* @__PURE__ */ new Map();
2837
+ active.set(key, bucket);
2838
+ }
2839
+ bucket.set(fp, { sid: HOST_SID, ts: now, exp: now + ttlMs });
2840
+ claims.push({ key, fingerprint: fp, sid: HOST_SID, ts: now });
2841
+ lines.push(
2842
+ JSON.stringify({
2843
+ op: "claim",
2844
+ key,
2845
+ fp,
2846
+ sid: HOST_SID,
2847
+ ts: now,
2848
+ exp: now + ttlMs
2849
+ })
2850
+ );
2851
+ }
2852
+ if (lines.length > 0) {
2853
+ await fsp.appendFile(claimsFilePath(storeDir), `${lines.join("\n")}
2854
+ `, "utf8");
2855
+ try {
2856
+ if (await ledgerLineCount(storeDir) > maxLedgerLines) {
2857
+ await compactLedger(storeDir, active);
2858
+ }
2859
+ } catch (err) {
2860
+ warnFallbackOnce(
2861
+ `[review-claim-registry] ledger compaction failed (${err instanceof Error ? err.message : String(err)}); will retry on a future acquire`
2862
+ );
2863
+ }
2864
+ }
2865
+ return claims;
2866
+ },
2867
+ waitMs
2868
+ );
2869
+ }
2870
+ async function releaseSharedClaims(storeDir, claims, waitMs = LOCK_WAIT_MS) {
2871
+ if (claims.length === 0) return;
2872
+ await withStoreLock(
2873
+ storeDir,
2874
+ async () => {
2875
+ const now = Date.now();
2876
+ const active = await readLedger(storeDir, now);
2877
+ const lines = [];
2878
+ for (const claim of claims) {
2879
+ const bucket = active.get(claim.key);
2880
+ const live = bucket?.get(claim.fingerprint);
2881
+ if (live && live.sid === claim.sid && live.ts === claim.ts) {
2882
+ lines.push(
2883
+ JSON.stringify({
2884
+ op: "release",
2885
+ key: claim.key,
2886
+ fp: claim.fingerprint,
2887
+ sid: claim.sid,
2888
+ // Target the ORIGINAL claim time so a late release only ever
2889
+ // removes the claim it owns (see readLedger release matching).
2890
+ ts: claim.ts
2891
+ })
2892
+ );
2893
+ }
2894
+ }
2895
+ if (lines.length > 0) {
2896
+ await fsp.appendFile(claimsFilePath(storeDir), `${lines.join("\n")}
2897
+ `, "utf8");
2898
+ }
2899
+ },
2900
+ waitMs
2901
+ );
2481
2902
  }
2482
2903
  function ledgerFor(events) {
2483
2904
  let ledger = claimsByEventBus.get(events);
@@ -2487,68 +2908,162 @@ function ledgerFor(events) {
2487
2908
  }
2488
2909
  return ledger;
2489
2910
  }
2490
- function claimFiles(events, cwd, files) {
2911
+ function claimFilesInMemory(events, cwd, files, ttlMs) {
2491
2912
  const ledger = ledgerFor(events);
2913
+ const now = Date.now();
2492
2914
  const claims = [];
2493
2915
  for (const file of files) {
2494
2916
  const key = claimKey(cwd, file.path);
2495
2917
  const fileFingerprint = fingerprint(file.content);
2496
2918
  let activeFingerprints = ledger.get(key);
2919
+ if (activeFingerprints) {
2920
+ for (const [fp, entry] of activeFingerprints) {
2921
+ if (now > entry.exp) activeFingerprints.delete(fp);
2922
+ }
2923
+ if (activeFingerprints.size === 0) {
2924
+ ledger.delete(key);
2925
+ activeFingerprints = void 0;
2926
+ }
2927
+ }
2497
2928
  if (!activeFingerprints) {
2498
- activeFingerprints = /* @__PURE__ */ new Set();
2929
+ activeFingerprints = /* @__PURE__ */ new Map();
2499
2930
  ledger.set(key, activeFingerprints);
2500
2931
  }
2501
2932
  if (activeFingerprints.has(fileFingerprint)) continue;
2502
- activeFingerprints.add(fileFingerprint);
2503
- claims.push({ key, fingerprint: fileFingerprint });
2933
+ activeFingerprints.set(fileFingerprint, { ts: now, exp: now + ttlMs });
2934
+ claims.push({ key, fingerprint: fileFingerprint, sid: HOST_SID, ts: now });
2504
2935
  }
2505
2936
  return claims;
2506
2937
  }
2507
- function releaseClaims(events, claims) {
2938
+ function releaseClaimsInMemory(events, claims) {
2508
2939
  const ledger = ledgerFor(events);
2509
2940
  for (const claim of claims) {
2510
2941
  const activeFingerprints = ledger.get(claim.key);
2511
2942
  if (!activeFingerprints) continue;
2512
- activeFingerprints.delete(claim.fingerprint);
2943
+ if (activeFingerprints.get(claim.fingerprint)?.ts === claim.ts) {
2944
+ activeFingerprints.delete(claim.fingerprint);
2945
+ }
2513
2946
  if (activeFingerprints.size === 0) ledger.delete(claim.key);
2514
2947
  }
2515
2948
  }
2516
- function recordStartedReview(events, payload) {
2949
+ async function claimFiles(events, cwd, files, opts) {
2950
+ const { storeDir, ttlMs, maxLedgerLines } = resolveStore(opts, cwd);
2951
+ try {
2952
+ const claims = await acquireClaims(storeDir, ttlMs, maxLedgerLines, cwd, files);
2953
+ return { claims, storeDir };
2954
+ } catch (err) {
2955
+ const code = err.code;
2956
+ if (code === CLAIM_STORE_LOCK_TIMEOUT) {
2957
+ try {
2958
+ const claims = await acquireClaims(
2959
+ storeDir,
2960
+ ttlMs,
2961
+ maxLedgerLines,
2962
+ cwd,
2963
+ files,
2964
+ LOCK_RETRY_BUDGET_MS
2965
+ );
2966
+ return { claims, storeDir };
2967
+ } catch (err2) {
2968
+ warnFallbackOnce(
2969
+ `[review-claim-registry] claim lock still contended after ${LOCK_WAIT_MS + LOCK_RETRY_BUDGET_MS}ms (${err2 instanceof Error ? err2.message : String(err2)}); falling back to in-memory dedup`
2970
+ );
2971
+ return { claims: claimFilesInMemory(events, cwd, files, ttlMs), storeDir: null };
2972
+ }
2973
+ }
2974
+ warnFallbackOnce(
2975
+ `[review-claim-registry] shared claim store unavailable (${err instanceof Error ? err.message : String(err)}); falling back to in-memory dedup`
2976
+ );
2977
+ return { claims: claimFilesInMemory(events, cwd, files, ttlMs), storeDir: null };
2978
+ }
2979
+ }
2980
+ async function releaseClaims(events, claims, storeDir) {
2981
+ if (storeDir === null) {
2982
+ releaseClaimsInMemory(events, claims);
2983
+ return;
2984
+ }
2985
+ try {
2986
+ await releaseSharedClaims(storeDir, claims);
2987
+ } catch (err) {
2988
+ if (err.code === CLAIM_STORE_LOCK_TIMEOUT) {
2989
+ try {
2990
+ await releaseSharedClaims(storeDir, claims, LOCK_RETRY_BUDGET_MS);
2991
+ return;
2992
+ } catch {
2993
+ }
2994
+ }
2995
+ warnFallbackOnce(
2996
+ `[review-claim-registry] shared claim release failed (${err instanceof Error ? err.message : String(err)}); claim will expire via its stored TTL`
2997
+ );
2998
+ }
2999
+ }
3000
+ async function recordStartedReview(events, payload, opts) {
2517
3001
  if (!payload || typeof payload !== "object") return;
3002
+ const bundle = payload;
3003
+ if (startedReviews.has(bundle) || pendingStartedReviews.has(bundle)) return;
2518
3004
  const candidate = payload;
2519
3005
  if (typeof candidate.cwd !== "string" || !Array.isArray(candidate.files)) return;
3006
+ const cwd = candidate.cwd;
2520
3007
  const files = candidate.files.filter(
2521
3008
  (file) => Boolean(file) && typeof file === "object" && typeof file.path === "string" && typeof file.content === "string"
2522
3009
  );
2523
- const claims = claimFiles(events, candidate.cwd, files);
2524
- if (claims.length > 0) {
2525
- startedReviews.set(payload, { events, claims });
3010
+ const pending = (async () => {
3011
+ try {
3012
+ const { claims, storeDir } = await claimFiles(events, cwd, files, opts);
3013
+ if (claims.length === 0) return null;
3014
+ const entry = { events, storeDir, claims };
3015
+ startedReviews.set(bundle, entry);
3016
+ return entry;
3017
+ } catch {
3018
+ return null;
3019
+ }
3020
+ })();
3021
+ pendingStartedReviews.set(bundle, pending);
3022
+ try {
3023
+ await pending;
3024
+ } finally {
3025
+ if (pendingStartedReviews.get(bundle) === pending) {
3026
+ pendingStartedReviews.delete(bundle);
3027
+ }
2526
3028
  }
2527
3029
  }
2528
- function recordCompletedReview(events, payload) {
3030
+ async function recordCompletedReview(events, payload) {
2529
3031
  if (!payload || typeof payload !== "object") return;
2530
3032
  const bundle = payload.bundle;
2531
3033
  if (!bundle || typeof bundle !== "object") return;
2532
- const startedReview = startedReviews.get(bundle);
3034
+ let startedReview = startedReviews.get(bundle);
3035
+ if (!startedReview) {
3036
+ const pending = pendingStartedReviews.get(bundle);
3037
+ if (pending) startedReview = await pending ?? void 0;
3038
+ }
2533
3039
  if (!startedReview || startedReview.events !== events) return;
2534
- releaseClaims(events, startedReview.claims);
3040
+ await releaseClaims(events, startedReview.claims, startedReview.storeDir);
2535
3041
  startedReviews.delete(bundle);
2536
3042
  }
2537
- function emitReviewIfChanged(api, bundle) {
2538
- const claims = claimFiles(api.events, bundle.cwd, bundle.files);
3043
+ async function emitReviewIfChanged(api, bundle, opts) {
3044
+ const { claims, storeDir } = await claimFiles(api.events, bundle.cwd, bundle.files, opts);
2539
3045
  if (claims.length === 0) return null;
2540
- const claimedKeys = new Set(claims.map((claim) => claim.key));
3046
+ const claimed = new Set(claims.map((claim) => `${claim.key}\0${claim.fingerprint}`));
2541
3047
  const claimedBundle = {
2542
3048
  ...bundle,
2543
- files: bundle.files.filter((file) => claimedKeys.has(claimKey(bundle.cwd, file.path)))
3049
+ // Filter on key+FINGERPRINT pairs, not just key: two entries for the same
3050
+ // path in one batch can differ in content, and a fingerprint claimed by
3051
+ // another session must not ride along in the emitted bundle.
3052
+ files: bundle.files.filter(
3053
+ (file) => claimed.has(`${claimKey(bundle.cwd, file.path)}\0${fingerprint(file.content)}`)
3054
+ )
2544
3055
  };
2545
- startedReviews.set(claimedBundle, { events: api.events, claims });
3056
+ startedReviews.set(claimedBundle, {
3057
+ events: api.events,
3058
+ storeDir,
3059
+ claims
3060
+ });
2546
3061
  try {
2547
3062
  api.emitCustom("chimera.review_needed", claimedBundle);
2548
3063
  return claimedBundle;
2549
3064
  } catch (err) {
2550
3065
  startedReviews.delete(claimedBundle);
2551
- releaseClaims(api.events, claims);
3066
+ await releaseClaims(api.events, claims, storeDir);
2552
3067
  throw err;
2553
3068
  }
2554
3069
  }
@@ -2559,27 +3074,30 @@ import { spawn as spawn2 } from "node:child_process";
2559
3074
  // src/chronicle/project-access.ts
2560
3075
  import * as fsPromises from "node:fs/promises";
2561
3076
  import * as os4 from "node:os";
2562
- import * as path13 from "node:path";
3077
+ import * as path14 from "node:path";
2563
3078
 
2564
3079
  // src/utils/wstack-paths.ts
2565
3080
  import { createHash as createHash2 } from "node:crypto";
2566
3081
  import * as fs from "node:fs";
2567
3082
  import * as os from "node:os";
2568
- import * as path from "node:path";
3083
+ import * as path2 from "node:path";
2569
3084
  function canonicalProjectRoot(absRoot) {
2570
- const checkoutRoot = path.resolve(absRoot);
2571
- const dotGit = path.join(checkoutRoot, ".git");
3085
+ const checkoutRoot = path2.resolve(absRoot);
3086
+ const dotGit = path2.join(checkoutRoot, ".git");
2572
3087
  try {
2573
3088
  if (!fs.statSync(dotGit).isFile()) return checkoutRoot;
2574
3089
  const gitDirLine = fs.readFileSync(dotGit, "utf8").trim();
2575
3090
  const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
2576
3091
  if (!match?.[1]) return checkoutRoot;
2577
- const gitDir = path.resolve(checkoutRoot, match[1].trim());
2578
- const commonDirFile = path.join(gitDir, "commondir");
3092
+ const gitDir = path2.resolve(checkoutRoot, match[1].trim());
3093
+ const commonDirFile = path2.join(gitDir, "commondir");
2579
3094
  if (!fs.statSync(commonDirFile).isFile()) return checkoutRoot;
2580
- const commonDir = path.resolve(gitDir, fs.readFileSync(commonDirFile, "utf8").trim());
2581
- if (path.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
2582
- return path.dirname(commonDir);
3095
+ const commonDir = path2.resolve(gitDir, fs.readFileSync(commonDirFile, "utf8").trim());
3096
+ const worktreesDir = path2.dirname(gitDir);
3097
+ if (path2.basename(worktreesDir).toLowerCase() !== "worktrees") return checkoutRoot;
3098
+ if (path2.resolve(worktreesDir, "..") !== commonDir) return checkoutRoot;
3099
+ if (path2.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
3100
+ return path2.dirname(commonDir);
2583
3101
  } catch {
2584
3102
  return checkoutRoot;
2585
3103
  }
@@ -2589,7 +3107,7 @@ function projectHash(absRoot) {
2589
3107
  }
2590
3108
  function projectSlug(absRoot) {
2591
3109
  const identityRoot = canonicalProjectRoot(absRoot);
2592
- const base = slugify(path.basename(identityRoot));
3110
+ const base = slugify(path2.basename(identityRoot));
2593
3111
  const hash = createHash2("sha256").update(identityRoot).digest("hex").slice(0, 6);
2594
3112
  return `${base}-${hash}`;
2595
3113
  }
@@ -2600,9 +3118,9 @@ function safeProfileName(name) {
2600
3118
  const safe = (name ?? "").replace(/[/\\:]/g, "_").replace(/\.\./g, "_").trim();
2601
3119
  return safe || "default";
2602
3120
  }
2603
- function activeProfileName(globalRoot) {
3121
+ function bootstrapProfileName(globalRoot) {
2604
3122
  try {
2605
- const parsed = JSON.parse(fs.readFileSync(path.join(globalRoot, "config.json"), "utf8"));
3123
+ const parsed = JSON.parse(fs.readFileSync(path2.join(globalRoot, "config.json"), "utf8"));
2606
3124
  return safeProfileName(
2607
3125
  typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0
2608
3126
  );
@@ -2612,17 +3130,17 @@ function activeProfileName(globalRoot) {
2612
3130
  }
2613
3131
  function wstackGlobalRoot() {
2614
3132
  const fromEnv = process.env["WRONGSTACK_HOME"];
2615
- if (fromEnv && fromEnv.trim().length > 0) return path.resolve(fromEnv);
2616
- return path.join(os.homedir(), ".wrongstack");
3133
+ if (fromEnv && fromEnv.trim().length > 0) return path2.resolve(fromEnv);
3134
+ return path2.join(os.homedir(), ".wrongstack");
2617
3135
  }
2618
3136
  function resolveWstackPaths(opts) {
2619
- const globalRoot = opts.globalRoot ?? (opts.userHome ? path.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
3137
+ const globalRoot = opts.globalRoot ?? (opts.userHome ? path2.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
2620
3138
  const homeDir = opts.userHome ?? os.homedir();
2621
- const profileName = safeProfileName(opts.profileName ?? activeProfileName(globalRoot));
2622
- const profileDir = path.join(globalRoot, "profiles", profileName);
3139
+ const profileName = safeProfileName(opts.profileName ?? bootstrapProfileName(globalRoot));
3140
+ const profileDir = path2.join(globalRoot, "profiles", profileName);
2623
3141
  const hash = projectHash(opts.projectRoot);
2624
3142
  const slug = projectSlug(opts.projectRoot);
2625
- const projectDir = path.join(globalRoot, "projects", slug);
3143
+ const projectDir = path2.join(globalRoot, "projects", slug);
2626
3144
  return {
2627
3145
  globalRoot,
2628
3146
  profileName,
@@ -2630,79 +3148,84 @@ function resolveWstackPaths(opts) {
2630
3148
  projectRoot: opts.projectRoot,
2631
3149
  homeDir,
2632
3150
  configDir: profileDir,
2633
- globalConfig: path.join(globalRoot, "config.json"),
2634
- profilesDir: path.join(globalRoot, "profiles"),
3151
+ globalConfig: path2.join(globalRoot, "config.json"),
3152
+ profilesDir: path2.join(globalRoot, "profiles"),
2635
3153
  profileConfig: (name) => {
2636
- return path.join(globalRoot, "profiles", safeProfileName(name), "config.json");
3154
+ return path2.join(globalRoot, "profiles", safeProfileName(name), "config.json");
2637
3155
  },
2638
3156
  profileStatuslineConfig: (name) => {
2639
- return path.join(globalRoot, "profiles", safeProfileName(name), "statusline.json");
3157
+ return path2.join(globalRoot, "profiles", safeProfileName(name), "statusline.json");
2640
3158
  },
2641
3159
  profileModeConfig: (name) => {
2642
- return path.join(globalRoot, "profiles", safeProfileName(name), "mode.json");
3160
+ return path2.join(globalRoot, "profiles", safeProfileName(name), "mode.json");
2643
3161
  },
2644
3162
  profileProviderStatus: (name) => {
2645
- return path.join(globalRoot, "profiles", safeProfileName(name), "provider-status.json");
3163
+ return path2.join(globalRoot, "profiles", safeProfileName(name), "provider-status.json");
2646
3164
  },
2647
3165
  profileUpdateCache: (name) => {
2648
- return path.join(globalRoot, "profiles", safeProfileName(name), "update-cache.json");
3166
+ return path2.join(globalRoot, "profiles", safeProfileName(name), "update-cache.json");
2649
3167
  },
2650
- secretsKey: path.join(globalRoot, ".key"),
2651
- globalMemory: path.join(profileDir, "memory.md"),
2652
- globalSkills: path.join(profileDir, "skills"),
2653
- globalClaudeSkills: path.join(homeDir, ".claude", "skills"),
2654
- globalDesignKits: path.join(profileDir, "design-kits"),
2655
- globalPrompts: path.join(profileDir, "prompts"),
2656
- globalInstructions: path.join(profileDir, "instructions"),
2657
- promptUsage: path.join(profileDir, "prompt-usage.json"),
2658
- cacheDir: path.join(globalRoot, "cache"),
2659
- modelsCache: path.join(globalRoot, "cache", "models.dev.json"),
2660
- modelsOverlayCache: path.join(globalRoot, "cache", "models-overlay.json"),
2661
- historyFile: path.join(profileDir, "history"),
2662
- logFile: path.join(globalRoot, "logs", "wrongstack.log"),
3168
+ secretsKey: path2.join(globalRoot, ".key"),
3169
+ globalMemory: path2.join(profileDir, "memory.md"),
3170
+ globalSkills: path2.join(profileDir, "skills"),
3171
+ globalClaudeSkills: path2.join(homeDir, ".claude", "skills"),
3172
+ globalDesignKits: path2.join(profileDir, "design-kits"),
3173
+ globalPrompts: path2.join(profileDir, "prompts"),
3174
+ globalInstructions: path2.join(profileDir, "instructions"),
3175
+ promptUsage: path2.join(profileDir, "prompt-usage.json"),
3176
+ cacheDir: path2.join(globalRoot, "cache"),
3177
+ modelsCache: path2.join(globalRoot, "cache", "models.dev.json"),
3178
+ modelsOverlayCache: path2.join(globalRoot, "cache", "models-overlay.json"),
3179
+ historyFile: path2.join(profileDir, "history"),
3180
+ logFile: path2.join(globalRoot, "logs", "wrongstack.log"),
2663
3181
  projectDir,
2664
- projectCodebaseIndex: path.join(projectDir, "codebase-index"),
2665
- projectMemory: path.join(projectDir, "memory.md"),
2666
- projectSessions: path.join(projectDir, "sessions"),
2667
- projectTrust: path.join(projectDir, "trust.json"),
2668
- projectMeta: path.join(projectDir, "meta.json"),
2669
- projectLocalConfig: path.join(projectDir, "config.local.json"),
2670
- inProjectConfig: path.join(opts.projectRoot, ".wrongstack", "config.json"),
2671
- inProjectAgentsFile: path.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
2672
- inProjectSkills: path.join(opts.projectRoot, ".wrongstack", "skills"),
2673
- inProjectClaudeSkills: path.join(opts.projectRoot, ".claude", "skills"),
2674
- inProjectPrompts: path.join(opts.projectRoot, ".wrongstack", "prompts"),
2675
- inProjectInstructions: path.join(opts.projectRoot, ".wrongstack", "instructions"),
2676
- inProjectDesignKits: path.join(opts.projectRoot, ".wrongstack", "design-kits"),
2677
- inProjectWorktrees: path.join(opts.projectRoot, ".wrongstack", "worktrees"),
3182
+ projectCodebaseIndex: path2.join(projectDir, "codebase-index"),
3183
+ projectMemory: path2.join(projectDir, "memory.md"),
3184
+ projectSessions: path2.join(projectDir, "sessions"),
3185
+ projectTrust: path2.join(projectDir, "trust.json"),
3186
+ projectMeta: path2.join(projectDir, "meta.json"),
3187
+ projectLocalConfig: path2.join(projectDir, "config.local.json"),
3188
+ inProjectConfig: path2.join(opts.projectRoot, ".wrongstack", "config.json"),
3189
+ inProjectAgentsFile: path2.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
3190
+ inProjectSkills: path2.join(opts.projectRoot, ".wrongstack", "skills"),
3191
+ inProjectClaudeSkills: path2.join(opts.projectRoot, ".claude", "skills"),
3192
+ inProjectPrompts: path2.join(opts.projectRoot, ".wrongstack", "prompts"),
3193
+ inProjectInstructions: path2.join(opts.projectRoot, ".wrongstack", "instructions"),
3194
+ inProjectDesignKits: path2.join(opts.projectRoot, ".wrongstack", "design-kits"),
3195
+ inProjectWorktrees: path2.join(opts.projectRoot, ".wrongstack", "worktrees"),
2678
3196
  projectHash: hash,
2679
3197
  projectSlug: slug,
2680
- projectGoal: path.join(projectDir, "goal.json"),
2681
- projectInputHistory: path.join(projectDir, "input-history.json"),
2682
- projectSpecs: path.join(projectDir, "specs"),
2683
- projectTaskGraphs: path.join(projectDir, "task-graphs"),
2684
- projectSddSession: path.join(projectDir, "sdd-session.json"),
2685
- projectPlan: path.join(projectDir, "plan.json"),
2686
- projectAutophase: path.join(projectDir, "autophase"),
2687
- projectSddBoards: path.join(projectDir, "sdd-boards"),
2688
- projectRequirementIntakes: path.join(projectDir, "requirement-intakes"),
2689
- syncConfig: path.join(profileDir, "sync.json"),
2690
- configHistoryDir: path.join(globalRoot, "config-history"),
2691
- projectStatus: (projectHash2) => path.join(globalRoot, "projects", projectHash2, "status.json")
3198
+ projectGoal: path2.join(projectDir, "goal.json"),
3199
+ projectInputHistory: path2.join(projectDir, "input-history.json"),
3200
+ projectSpecs: path2.join(projectDir, "specs"),
3201
+ projectTaskGraphs: path2.join(projectDir, "task-graphs"),
3202
+ projectSddSession: path2.join(projectDir, "sdd-session.json"),
3203
+ projectPlan: path2.join(projectDir, "plan.json"),
3204
+ projectAutophase: path2.join(projectDir, "autophase"),
3205
+ projectSddBoards: path2.join(projectDir, "sdd-boards"),
3206
+ projectRequirementIntakes: path2.join(projectDir, "requirement-intakes"),
3207
+ syncConfig: path2.join(profileDir, "sync.json"),
3208
+ configHistoryDir: path2.join(globalRoot, "config-history"),
3209
+ projectStatus: (statusProjectSlug) => {
3210
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,39})-[a-f0-9]{6}$/.test(statusProjectSlug)) {
3211
+ throw new Error(`Invalid project slug: ${statusProjectSlug}`);
3212
+ }
3213
+ return path2.join(globalRoot, "projects", statusProjectSlug, "status.json");
3214
+ }
2692
3215
  };
2693
3216
  }
2694
3217
 
2695
3218
  // src/chronicle/identity.ts
2696
3219
  import { createHash as createHash3 } from "node:crypto";
2697
3220
  import * as os2 from "node:os";
2698
- import * as path2 from "node:path";
3221
+ import * as path3 from "node:path";
2699
3222
  function resolveChronicleRuntimeLocation(input) {
2700
3223
  const day = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2701
3224
  return {
2702
- installationId: stableId("installation", path2.resolve(input.globalRoot)),
3225
+ installationId: stableId("installation", path3.resolve(input.globalRoot)),
2703
3226
  machineId: stableId("machine", `${os2.hostname()}\0${os2.platform()}\0${os2.arch()}`),
2704
3227
  projectId: input.projectId,
2705
- chronicleDirectory: path2.join(input.projectDir, "chronicle"),
3228
+ chronicleDirectory: path3.join(input.projectDir, "chronicle"),
2706
3229
  day
2707
3230
  };
2708
3231
  }
@@ -2713,10 +3236,10 @@ function stableId(prefix, value) {
2713
3236
  // src/chronicle/journal.ts
2714
3237
  init_file_permissions();
2715
3238
  init_atomic_write();
2716
- import { randomUUID } from "node:crypto";
3239
+ import { randomUUID as randomUUID2 } from "node:crypto";
2717
3240
  import { createReadStream } from "node:fs";
2718
3241
  import * as fs2 from "node:fs/promises";
2719
- import * as path4 from "node:path";
3242
+ import * as path5 from "node:path";
2720
3243
  import { createInterface } from "node:readline";
2721
3244
 
2722
3245
  // src/chronicle/event-hash.ts
@@ -2739,14 +3262,14 @@ function chronicleEventHash(event) {
2739
3262
  }
2740
3263
 
2741
3264
  // src/chronicle/partition-filename.ts
2742
- import * as path3 from "node:path";
3265
+ import * as path4 from "node:path";
2743
3266
  var PARTITION_FILE_PATTERN = /^(.*\.events)(?:\.(\d{5}))?\.jsonl$/;
2744
3267
  function partitionFamily(filePath) {
2745
- const match = PARTITION_FILE_PATTERN.exec(path3.basename(filePath));
2746
- return path3.join(path3.dirname(filePath), match?.[1] ?? path3.basename(filePath));
3268
+ const match = PARTITION_FILE_PATTERN.exec(path4.basename(filePath));
3269
+ return path4.join(path4.dirname(filePath), match?.[1] ?? path4.basename(filePath));
2747
3270
  }
2748
3271
  function partitionSequence(filePath) {
2749
- const match = PARTITION_FILE_PATTERN.exec(path3.basename(filePath));
3272
+ const match = PARTITION_FILE_PATTERN.exec(path4.basename(filePath));
2750
3273
  return Number(match?.[2] ?? 0);
2751
3274
  }
2752
3275
  function comparePartitionPaths(left, right) {
@@ -2795,10 +3318,10 @@ var ChronicleJournal = class {
2795
3318
  lastHash = GENESIS_HASH;
2796
3319
  lastAutoPurgeAt = 0;
2797
3320
  constructor(options) {
2798
- this.basePath = path4.resolve(options.filePath);
3321
+ this.basePath = path5.resolve(options.filePath);
2799
3322
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
2800
3323
  this.monotonicNow = options.monotonicNow ?? (() => process.hrtime.bigint());
2801
- this.idFactory = options.idFactory ?? randomUUID;
3324
+ this.idFactory = options.idFactory ?? randomUUID2;
2802
3325
  this.maxPending = Math.max(1, options.maxPending ?? 1e5);
2803
3326
  this.batchWindowMs = Math.max(0, options.batchWindowMs ?? 5);
2804
3327
  this.maxPartitionSizeBytes = options.maxPartitionSizeBytes ?? DEFAULT_MAX_PARTITION_BYTES;
@@ -2868,14 +3391,14 @@ var ChronicleJournal = class {
2868
3391
  }
2869
3392
  await this.refreshStateFromDisk();
2870
3393
  const cutoff = Date.now() - options.retentionDays * 864e5;
2871
- const activePath = path4.resolve(this.path);
3394
+ const activePath = path5.resolve(this.path);
2872
3395
  const errors = [];
2873
3396
  let dc = 0, db = 0, sc = 0;
2874
3397
  const suppliedFiles = options.files === void 0 ? await collectJournalPartitions(this.basePath) : [...options.files];
2875
3398
  const eligible = /* @__PURE__ */ new Set();
2876
3399
  for (const suppliedPath of new Set(suppliedFiles)) {
2877
- const file = path4.resolve(suppliedPath);
2878
- if (!isJournalPartition(file, path4.dirname(this.basePath), this.basePath)) {
3400
+ const file = path5.resolve(suppliedPath);
3401
+ if (!isJournalPartition(file, path5.dirname(this.basePath), this.basePath)) {
2879
3402
  errors.push({
2880
3403
  file: suppliedPath,
2881
3404
  reason: "not a Chronicle journal partition in this journal directory"
@@ -3096,34 +3619,34 @@ var ChronicleJournal = class {
3096
3619
  }
3097
3620
  };
3098
3621
  function rotatedPath(basePath, index) {
3099
- const dir = path4.dirname(basePath);
3100
- const ext = path4.extname(basePath);
3101
- const base = path4.basename(basePath, ext);
3102
- return path4.join(dir, `${base}.${String(index).padStart(5, "0")}${ext}`);
3622
+ const dir = path5.dirname(basePath);
3623
+ const ext = path5.extname(basePath);
3624
+ const base = path5.basename(basePath, ext);
3625
+ return path5.join(dir, `${base}.${String(index).padStart(5, "0")}${ext}`);
3103
3626
  }
3104
3627
  async function collectPartitions(basePath) {
3105
- const dir = path4.dirname(basePath);
3106
- const ext = path4.extname(basePath);
3107
- const base = path4.basename(basePath, ext);
3628
+ const dir = path5.dirname(basePath);
3629
+ const ext = path5.extname(basePath);
3630
+ const base = path5.basename(basePath, ext);
3108
3631
  const pattern = new RegExp(`^${escapeRegex(base)}(?:\\.\\d{5})?${escapeRegex(ext)}$`);
3109
3632
  const result = [];
3110
3633
  try {
3111
3634
  const entries = await fs2.readdir(dir, { withFileTypes: true });
3112
3635
  for (const entry of entries)
3113
- if (entry.isFile() && pattern.test(entry.name)) result.push(path4.join(dir, entry.name));
3636
+ if (entry.isFile() && pattern.test(entry.name)) result.push(path5.join(dir, entry.name));
3114
3637
  } catch {
3115
3638
  }
3116
- const baseFile = path4.join(dir, base + ext);
3639
+ const baseFile = path5.join(dir, base + ext);
3117
3640
  const rotated = result.filter((file) => file !== baseFile).sort((left, right) => parseIndex(left, base, ext) - parseIndex(right, base, ext));
3118
3641
  return [baseFile, ...rotated];
3119
3642
  }
3120
3643
  async function collectJournalPartitions(basePath) {
3121
- const directory = path4.dirname(basePath);
3644
+ const directory = path5.dirname(basePath);
3122
3645
  const result = [];
3123
3646
  try {
3124
3647
  const entries = await fs2.readdir(directory, { withFileTypes: true });
3125
3648
  for (const entry of entries) {
3126
- const file = path4.join(directory, entry.name);
3649
+ const file = path5.join(directory, entry.name);
3127
3650
  if (entry.isFile() && isJournalPartition(file, directory, basePath)) result.push(file);
3128
3651
  }
3129
3652
  } catch {
@@ -3131,13 +3654,13 @@ async function collectJournalPartitions(basePath) {
3131
3654
  return result.sort(comparePartitionPaths);
3132
3655
  }
3133
3656
  function isJournalPartition(filePath, directory, basePath) {
3134
- if (path4.dirname(path4.resolve(filePath)) !== path4.resolve(directory)) return false;
3135
- const fileName = path4.basename(filePath);
3657
+ if (path5.dirname(path5.resolve(filePath)) !== path5.resolve(directory)) return false;
3658
+ const fileName = path5.basename(filePath);
3136
3659
  if (!basePath) return false;
3137
- const baseName = path4.basename(basePath);
3660
+ const baseName = path5.basename(basePath);
3138
3661
  const dailyFamily = /^\d{4}-\d{2}-\d{2}\.events(?:\.\d{5})?\.jsonl$/;
3139
3662
  if (/^\d{4}-\d{2}-\d{2}\.events\.jsonl$/.test(baseName)) return dailyFamily.test(fileName);
3140
- return partitionFamilyBase(path4.resolve(filePath)) === partitionFamilyBase(path4.resolve(basePath));
3663
+ return partitionFamilyBase(path5.resolve(filePath)) === partitionFamilyBase(path5.resolve(basePath));
3141
3664
  }
3142
3665
  function groupPartitionsByFamily(files) {
3143
3666
  const groups = /* @__PURE__ */ new Map();
@@ -3261,12 +3784,12 @@ function isHash(value) {
3261
3784
  return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
3262
3785
  }
3263
3786
  function parseIndex(filePath, base, ext) {
3264
- const suffix = path4.basename(filePath).slice(base.length + 1, -ext.length);
3787
+ const suffix = path5.basename(filePath).slice(base.length + 1, -ext.length);
3265
3788
  return suffix ? parseInt(suffix, 10) : 0;
3266
3789
  }
3267
3790
  function partitionIndex(filePath, basePath) {
3268
- const ext = path4.extname(basePath);
3269
- return parseIndex(filePath, path4.basename(basePath, ext), ext);
3791
+ const ext = path5.extname(basePath);
3792
+ return parseIndex(filePath, path5.basename(basePath, ext), ext);
3270
3793
  }
3271
3794
  function escapeRegex(text) {
3272
3795
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -3280,8 +3803,8 @@ async function readLastEntryState(filePath) {
3280
3803
  throw error;
3281
3804
  }
3282
3805
  try {
3283
- const stat7 = await handle.stat();
3284
- const size = stat7.size;
3806
+ const stat8 = await handle.stat();
3807
+ const size = stat8.size;
3285
3808
  let position = size, suffix = "";
3286
3809
  while (position > 0) {
3287
3810
  const length = Math.min(65536, position);
@@ -3298,14 +3821,14 @@ async function readLastEntryState(filePath) {
3298
3821
  return {
3299
3822
  entry: JSON.parse(trimmed),
3300
3823
  size,
3301
- birthtimeMs: stat7.birthtimeMs
3824
+ birthtimeMs: stat8.birthtimeMs
3302
3825
  };
3303
3826
  } catch {
3304
3827
  }
3305
3828
  }
3306
3829
  suffix = lines[0] ?? "";
3307
3830
  }
3308
- return { size, birthtimeMs: stat7.birthtimeMs };
3831
+ return { size, birthtimeMs: stat8.birthtimeMs };
3309
3832
  } finally {
3310
3833
  await handle.close();
3311
3834
  }
@@ -3323,7 +3846,7 @@ async function* streamEntriesStrict(filePath) {
3323
3846
  try {
3324
3847
  entry = JSON.parse(trimmed);
3325
3848
  } catch {
3326
- throw new Error(`invalid JSON at line ${lineNumber} in ${path4.basename(filePath)}`);
3849
+ throw new Error(`invalid JSON at line ${lineNumber} in ${path5.basename(filePath)}`);
3327
3850
  }
3328
3851
  yield entry;
3329
3852
  }
@@ -3348,7 +3871,7 @@ function errorMessage(error) {
3348
3871
  init_atomic_write();
3349
3872
  import * as fs5 from "node:fs/promises";
3350
3873
  import { createRequire } from "node:module";
3351
- import * as path7 from "node:path";
3874
+ import * as path8 from "node:path";
3352
3875
 
3353
3876
  // src/utils/sqlite-warning.ts
3354
3877
  var SQLITE_EXPERIMENTAL_WARNING_RE = /sqlite is an experimental feature/i;
@@ -3377,13 +3900,13 @@ function withSqliteExperimentalWarningSuppressed(run) {
3377
3900
  import { createHash as createHash5 } from "node:crypto";
3378
3901
  import { createReadStream as createReadStream2 } from "node:fs";
3379
3902
  import * as fs4 from "node:fs/promises";
3380
- import * as path6 from "node:path";
3903
+ import * as path7 from "node:path";
3381
3904
  import { createInterface as createInterface2 } from "node:readline";
3382
3905
 
3383
3906
  // src/chronicle/partition-range-cache.ts
3384
3907
  init_atomic_write();
3385
3908
  import * as fs3 from "node:fs/promises";
3386
- import * as path5 from "node:path";
3909
+ import * as path6 from "node:path";
3387
3910
  var CACHE_VERSION = 1;
3388
3911
  function isRange(value) {
3389
3912
  if (!value || typeof value !== "object") return false;
@@ -3393,14 +3916,14 @@ function isRange(value) {
3393
3916
  var ChroniclePartitionRangeCache = class {
3394
3917
  constructor(directory) {
3395
3918
  this.directory = directory;
3396
- this.cachePath = path5.join(directory, "range-cache.json");
3919
+ this.cachePath = path6.join(directory, "range-cache.json");
3397
3920
  }
3398
3921
  directory;
3399
3922
  cachePath;
3400
3923
  entries;
3401
3924
  dirty = false;
3402
3925
  toKey(filePath) {
3403
- return path5.relative(this.directory, filePath).split(path5.sep).join("/");
3926
+ return path6.relative(this.directory, filePath).split(path6.sep).join("/");
3404
3927
  }
3405
3928
  async load() {
3406
3929
  if (this.entries) return this.entries;
@@ -3501,7 +4024,7 @@ var ChronicleQueryEngine = class _ChronicleQueryEngine {
3501
4024
  /** The one file (if any) that could still be actively appended to — never cache-checked or skipped. */
3502
4025
  activeFile;
3503
4026
  static async fromDirectory(directory, options) {
3504
- const resolved = path6.resolve(directory);
4027
+ const resolved = path7.resolve(directory);
3505
4028
  const files = await findPartitions(resolved);
3506
4029
  const rangeCache = options?.rangeCache ?? new ChroniclePartitionRangeCache(resolved);
3507
4030
  return new _ChronicleQueryEngine(
@@ -3898,7 +4421,7 @@ async function findPartitions(root) {
3898
4421
  return;
3899
4422
  }
3900
4423
  for (const entry of entries) {
3901
- const full = path6.join(directory, entry.name);
4424
+ const full = path7.join(directory, entry.name);
3902
4425
  if (entry.isDirectory()) await visit(full);
3903
4426
  else if (entry.isFile() && PARTITION_FILE_PATTERN.test(entry.name)) result.push(full);
3904
4427
  }
@@ -4085,7 +4608,7 @@ async function resolveSnapshotFiles(files, snapshot) {
4085
4608
  }));
4086
4609
  }
4087
4610
  function fileId(file) {
4088
- return createHash5("sha256").update(path6.resolve(file), "utf8").digest("base64url");
4611
+ return createHash5("sha256").update(path7.resolve(file), "utf8").digest("base64url");
4089
4612
  }
4090
4613
  function stableStringify2(value) {
4091
4614
  if (value === null || typeof value !== "object") return JSON.stringify(value);
@@ -4197,8 +4720,8 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
4197
4720
  directory;
4198
4721
  dbPath;
4199
4722
  constructor(directory) {
4200
- this.directory = path7.resolve(directory);
4201
- this.dbPath = path7.join(this.directory, "metrics.db");
4723
+ this.directory = path8.resolve(directory);
4724
+ this.dbPath = path8.join(this.directory, "metrics.db");
4202
4725
  const Database = loadDatabaseSync();
4203
4726
  this.db = new Database(this.dbPath);
4204
4727
  this.db.exec("PRAGMA journal_mode = WAL");
@@ -4223,7 +4746,7 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
4223
4746
  const files = await findChroniclePartitions(this.directory);
4224
4747
  const offsets = this.loadOffsets();
4225
4748
  for (const file of files) {
4226
- const key = normalizeKey(path7.relative(this.directory, file));
4749
+ const key = normalizeKey(path8.relative(this.directory, file));
4227
4750
  const consumed = offsets.get(key) ?? 0;
4228
4751
  const ingested = await this.ingestFile(file, key, consumed, result);
4229
4752
  if (ingested) result.sourceFiles++;
@@ -4593,7 +5116,7 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
4593
5116
  }
4594
5117
  pruneOffsets(existingFiles) {
4595
5118
  const keep = new Set(
4596
- existingFiles.map((file) => normalizeKey(path7.relative(this.directory, file)))
5119
+ existingFiles.map((file) => normalizeKey(path8.relative(this.directory, file)))
4597
5120
  );
4598
5121
  for (const row of this.db.prepare("SELECT file FROM ingest_state").all()) {
4599
5122
  if (!keep.has(row.file))
@@ -4890,7 +5413,7 @@ function normalizePathKey(value) {
4890
5413
 
4891
5414
  // src/chronicle/legacy-journal-import.ts
4892
5415
  import * as fs6 from "node:fs/promises";
4893
- import * as path8 from "node:path";
5416
+ import * as path9 from "node:path";
4894
5417
  var ChronicleImportError = class extends Error {
4895
5418
  constructor(message, day, sequence) {
4896
5419
  super(message);
@@ -4933,7 +5456,7 @@ async function importLegacyChronicleJournal(journal, directory) {
4933
5456
  let importedFamilies = 0;
4934
5457
  for (const familyBase of families) {
4935
5458
  const day = dayOfFamily(familyBase);
4936
- const basePath = path8.join(directory, `${familyBase}.jsonl`);
5459
+ const basePath = path9.join(directory, `${familyBase}.jsonl`);
4937
5460
  let familyEvents = 0;
4938
5461
  if (journal.hasImportedDay(day)) continue;
4939
5462
  try {
@@ -4996,9 +5519,10 @@ async function importLegacyChronicleJournal(journal, directory) {
4996
5519
  }
4997
5520
 
4998
5521
  // src/chronicle/sqlite-journal.ts
4999
- import { randomUUID as randomUUID2 } from "node:crypto";
5522
+ import { randomUUID as randomUUID3 } from "node:crypto";
5523
+ import * as fs8 from "node:fs";
5000
5524
  import { createRequire as createRequire2 } from "node:module";
5001
- import * as path10 from "node:path";
5525
+ import * as path11 from "node:path";
5002
5526
 
5003
5527
  // src/utils/index.ts
5004
5528
  init_atomic_write();
@@ -5027,7 +5551,7 @@ function envFlag(value) {
5027
5551
  return !/^(0|false|no|off)$/i.test(value.trim());
5028
5552
  }
5029
5553
  var COLOR = isColorTty();
5030
- var wrap = (open4, close) => (s) => COLOR ? `\x1B[${open4}m${s}\x1B[${close}m` : s;
5554
+ var wrap = (open5, close) => (s) => COLOR ? `\x1B[${open5}m${s}\x1B[${close}m` : s;
5031
5555
  var color = {
5032
5556
  reset: wrap("0", "0"),
5033
5557
  bold: wrap("1", "22"),
@@ -5047,6 +5571,20 @@ var color = {
5047
5571
  bgGreen: wrap("42", "49")
5048
5572
  };
5049
5573
 
5574
+ // src/utils/config-json.ts
5575
+ import * as fs7 from "node:fs/promises";
5576
+ async function readJsonObjectFile(filePath) {
5577
+ try {
5578
+ const parsed = JSON.parse(await fs7.readFile(filePath, "utf8"));
5579
+ return isJsonObject(parsed) ? parsed : {};
5580
+ } catch {
5581
+ return {};
5582
+ }
5583
+ }
5584
+ function isJsonObject(value) {
5585
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5586
+ }
5587
+
5050
5588
  // src/skills/limits.ts
5051
5589
  var SKILL_LIMITS = {
5052
5590
  /**
@@ -5147,7 +5685,14 @@ var SKILL_LIMITS = {
5147
5685
  };
5148
5686
 
5149
5687
  // src/skills/frontmatter.ts
5150
- var SCALAR_KEYS = /* @__PURE__ */ new Set(["name", "description", "trigger", "version", "license", "compatibility"]);
5688
+ var SCALAR_KEYS = /* @__PURE__ */ new Set([
5689
+ "name",
5690
+ "description",
5691
+ "trigger",
5692
+ "version",
5693
+ "license",
5694
+ "compatibility"
5695
+ ]);
5151
5696
  function parseSkillFrontmatter(raw) {
5152
5697
  const text = normalizeLineEndings(raw);
5153
5698
  if (!text.startsWith("---")) return {};
@@ -5206,8 +5751,9 @@ function parseFrontmatterBlock(block) {
5206
5751
  out[normalizeKey2(key)] = collected.join("\n").trim();
5207
5752
  continue;
5208
5753
  }
5209
- if (key === "allowed-tools" || key === "allowedTools") {
5210
- out.allowedTools = rest.split(/[\s,]+/).filter(Boolean);
5754
+ if (key === "allowed-tools" || key === "allowedTools" || key === "required-capabilities" || key === "requiredCapabilities" || key === "required-tools" || key === "requiredTools" || key === "optional-capabilities" || key === "optionalCapabilities") {
5755
+ const values = rest.replace(/^\[/, "").replace(/\]$/, "").split(/[\s,]+/).map(unquote).filter(Boolean);
5756
+ out[normalizeKey2(key)] = values;
5211
5757
  i++;
5212
5758
  continue;
5213
5759
  }
@@ -5221,7 +5767,11 @@ function parseFrontmatterBlock(block) {
5221
5767
  return out;
5222
5768
  }
5223
5769
  function normalizeKey2(key) {
5224
- return key === "allowed-tools" ? "allowedTools" : key;
5770
+ if (key === "allowed-tools") return "allowedTools";
5771
+ if (key === "required-capabilities") return "requiredCapabilities";
5772
+ if (key === "required-tools") return "requiredTools";
5773
+ if (key === "optional-capabilities") return "optionalCapabilities";
5774
+ return key;
5225
5775
  }
5226
5776
  function unquote(s) {
5227
5777
  if (s.length >= 2 && (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'"))) {
@@ -5262,7 +5812,7 @@ function capSkillBody(body) {
5262
5812
 
5263
5813
  // src/utils/instruction-file.ts
5264
5814
  import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
5265
- import * as path9 from "node:path";
5815
+ import * as path10 from "node:path";
5266
5816
  import { fileURLToPath } from "node:url";
5267
5817
  var textCache = /* @__PURE__ */ new Map();
5268
5818
  var rootCandidates;
@@ -5272,7 +5822,7 @@ function readBundledInstructionText(relativePath) {
5272
5822
  let resolved = "";
5273
5823
  for (const root of instructionRootCandidates()) {
5274
5824
  try {
5275
- resolved = readFileSync2(path9.join(root, relativePath), "utf8").trimEnd();
5825
+ resolved = readFileSync2(path10.join(root, relativePath), "utf8").trimEnd();
5276
5826
  break;
5277
5827
  } catch {
5278
5828
  }
@@ -5288,11 +5838,11 @@ function renderInstructionTemplate(template, values) {
5288
5838
  }
5289
5839
  function instructionRootCandidates() {
5290
5840
  if (rootCandidates !== void 0) return rootCandidates;
5291
- const here = path9.dirname(fileURLToPath(import.meta.url));
5841
+ const here = path10.dirname(fileURLToPath(import.meta.url));
5292
5842
  const candidates = [
5293
- path9.resolve(here, "../../instructions"),
5294
- path9.resolve(here, "../instructions"),
5295
- path9.resolve(here, "instructions")
5843
+ path10.resolve(here, "../../instructions"),
5844
+ path10.resolve(here, "../instructions"),
5845
+ path10.resolve(here, "instructions")
5296
5846
  ];
5297
5847
  rootCandidates = candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
5298
5848
  return rootCandidates;
@@ -5406,7 +5956,7 @@ var ChronicleSqliteQueryEngine = class {
5406
5956
  const comparison = order === "asc" ? ">" : "<";
5407
5957
  const keyset = cursor ? ` AND (day, sequence) ${comparison} (?, ?)` : "";
5408
5958
  const sql = `SELECT day, sequence, payload FROM events WHERE ${pushed.clause}${keyset} ORDER BY day ${direction}, sequence ${direction} LIMIT ? OFFSET ?`;
5409
- const summary = createSummaryAccumulator();
5959
+ const summary2 = createSummaryAccumulator();
5410
5960
  const page = [];
5411
5961
  let total = 0;
5412
5962
  let scannedEvents = 0;
@@ -5430,7 +5980,7 @@ var ChronicleSqliteQueryEngine = class {
5430
5980
  }
5431
5981
  if (!matches(event, query)) continue;
5432
5982
  total++;
5433
- updateSummary(summary, event);
5983
+ updateSummary(summary2, event);
5434
5984
  if (page.length < limit) {
5435
5985
  page.push(event);
5436
5986
  last = { day: row.day, sequence: row.sequence };
@@ -5445,7 +5995,7 @@ var ChronicleSqliteQueryEngine = class {
5445
5995
  scannedEvents,
5446
5996
  sourceFiles: this.diagnostics.sourceFiles,
5447
5997
  invalidLines: this.diagnostics.invalidLines,
5448
- summary: finalizeSummary(summary)
5998
+ summary: finalizeSummary(summary2)
5449
5999
  };
5450
6000
  if (total > page.length && last) {
5451
6001
  return { ...result, nextCursor: encodeCursor2(last) };
@@ -5580,6 +6130,25 @@ var CHRONICLE_SQLITE_FILE = "chronicle.sqlite";
5580
6130
  var LEGACY_JSONL_MIGRATION_KEY = "legacy-jsonl-v1";
5581
6131
  var LEGACY_JSONL_QUARANTINE_KEY = "legacy-jsonl-v1:quarantine";
5582
6132
  var SCHEMA_VERSION2 = 1;
6133
+ var SQLITE_FIXED_OVERHEAD_BYTES = 16 * 1024 * 1024;
6134
+ var MIN_SQLITE_PAGE_BUDGET_BYTES = 64 * 1024;
6135
+ var MAX_ROLLBACK_JOURNAL_RESERVE_BYTES = 64 * 1024 * 1024;
6136
+ var ChronicleStorageQuotaError = class extends Error {
6137
+ currentBytes;
6138
+ batchBytes;
6139
+ maxBytes;
6140
+ path;
6141
+ constructor(details) {
6142
+ super(
6143
+ `Chronicle SQLite quota exceeded at ${details.path}: ${details.currentBytes} live bytes + ${details.batchBytes} batch bytes exceeds ${details.maxBytes}; lower chronicle retentionDays/maxEvents to shed data (run chronicle compact to return the freed pages to the filesystem)`
6144
+ );
6145
+ this.name = "ChronicleStorageQuotaError";
6146
+ this.currentBytes = details.currentBytes;
6147
+ this.batchBytes = details.batchBytes;
6148
+ this.maxBytes = details.maxBytes;
6149
+ this.path = details.path;
6150
+ }
6151
+ };
5583
6152
  var Ctor2;
5584
6153
  function loadDatabaseSync2() {
5585
6154
  if (Ctor2) return Ctor2;
@@ -5602,6 +6171,14 @@ var ChronicleSqliteJournal = class {
5602
6171
  now;
5603
6172
  monotonicNow;
5604
6173
  idFactory;
6174
+ retentionDays;
6175
+ maxEvents;
6176
+ maxBytes;
6177
+ retentionCheckIntervalMs;
6178
+ nextRetentionCheckAt = 0;
6179
+ retainedEventCount = 0;
6180
+ /** Resolved lazily by `pageSizeBytes()`; constant for an open database. */
6181
+ cachedPageSize;
5605
6182
  /**
5606
6183
  * Cached chain head per day. Cleared wholesale on any write failure so the
5607
6184
  * next append rebuilds from the database rather than trusting a counter that
@@ -5619,14 +6196,40 @@ var ChronicleSqliteJournal = class {
5619
6196
  };
5620
6197
  lastBatchDurationMs;
5621
6198
  constructor(options) {
5622
- this.dbPath = path10.join(path10.resolve(options.directory), CHRONICLE_SQLITE_FILE);
6199
+ this.dbPath = path11.join(path11.resolve(options.directory), CHRONICLE_SQLITE_FILE);
5623
6200
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
5624
6201
  this.monotonicNow = options.monotonicNow ?? (() => process.hrtime.bigint());
5625
- this.idFactory = options.idFactory ?? (() => randomUUID2());
6202
+ this.idFactory = options.idFactory ?? (() => randomUUID3());
6203
+ if (options.retentionDays !== void 0 && (!Number.isFinite(options.retentionDays) || options.retentionDays <= 0)) {
6204
+ throw new RangeError("retentionDays must be a positive finite number");
6205
+ }
6206
+ if (options.retentionCheckIntervalMs !== void 0 && (!Number.isFinite(options.retentionCheckIntervalMs) || options.retentionCheckIntervalMs <= 0)) {
6207
+ throw new RangeError("retentionCheckIntervalMs must be a positive finite number");
6208
+ }
6209
+ if (options.maxEvents !== void 0 && (!Number.isInteger(options.maxEvents) || options.maxEvents < 1)) {
6210
+ throw new RangeError("maxEvents must be a positive integer");
6211
+ }
6212
+ if (options.maxBytes !== void 0 && (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1)) {
6213
+ throw new RangeError("maxBytes must be a positive safe integer");
6214
+ }
6215
+ const minimumQuotaBytes = SQLITE_FIXED_OVERHEAD_BYTES + 2 * MIN_SQLITE_PAGE_BUDGET_BYTES;
6216
+ if (options.maxBytes !== void 0 && options.maxBytes < minimumQuotaBytes) {
6217
+ throw new RangeError(`maxBytes must be at least ${minimumQuotaBytes}`);
6218
+ }
6219
+ this.retentionDays = options.retentionDays;
6220
+ this.maxEvents = options.maxEvents;
6221
+ this.maxBytes = options.maxBytes;
6222
+ this.retentionCheckIntervalMs = options.retentionCheckIntervalMs ?? 60 * 60 * 1e3;
5626
6223
  const Database = loadDatabaseSync2();
5627
6224
  this.db = new Database(this.dbPath);
5628
- this.db.exec("PRAGMA journal_mode = WAL");
6225
+ this.db.exec(`PRAGMA journal_mode = ${this.maxBytes === void 0 ? "WAL" : "DELETE"}`);
5629
6226
  this.ensureSchema();
6227
+ this.configureByteQuota();
6228
+ if (this.maxEvents !== void 0) {
6229
+ const row = this.db.prepare("SELECT COUNT(*) AS count FROM events").get();
6230
+ this.retainedEventCount = row.count;
6231
+ this.enforceEventLimitAtStartup();
6232
+ }
5630
6233
  }
5631
6234
  close() {
5632
6235
  this.db.close();
@@ -5685,8 +6288,10 @@ var ChronicleSqliteJournal = class {
5685
6288
  events.push(event);
5686
6289
  previous = { sequence: event.sequence, hash: event.hash };
5687
6290
  }
6291
+ let retainedCountAfterCommit = this.retainedEventCount;
5688
6292
  try {
5689
6293
  this.db.exec("BEGIN IMMEDIATE");
6294
+ this.assertWithinByteQuota(events);
5690
6295
  const insert = this.db.prepare(
5691
6296
  `INSERT INTO events (
5692
6297
  day, sequence, event_id, hash, previous_hash, occurred_at, event_type, outcome,
@@ -5718,6 +6323,8 @@ var ChronicleSqliteJournal = class {
5718
6323
  JSON.stringify(event)
5719
6324
  );
5720
6325
  }
6326
+ retainedCountAfterCommit = this.enforceEventLimitWithinTransaction();
6327
+ this.assertActualAllocationWithinQuota();
5721
6328
  this.db.exec("COMMIT");
5722
6329
  } catch (error) {
5723
6330
  try {
@@ -5727,11 +6334,13 @@ var ChronicleSqliteJournal = class {
5727
6334
  this.anchors.clear();
5728
6335
  this.counters.failedEvents += inputs.length;
5729
6336
  this.lastBatchDurationMs = performance.now() - started;
5730
- throw error;
6337
+ throw this.normalizeQuotaError(error);
5731
6338
  }
5732
6339
  this.anchors.set(day, previous);
5733
6340
  this.counters.persistedEvents += events.length;
6341
+ this.retainedEventCount = retainedCountAfterCommit;
5734
6342
  this.lastBatchDurationMs = performance.now() - started;
6343
+ await this.enforceRetentionIfDue();
5735
6344
  return events;
5736
6345
  }
5737
6346
  async readAll() {
@@ -5792,31 +6401,170 @@ var ChronicleSqliteJournal = class {
5792
6401
  }
5793
6402
  return { ok: true, entries, lastSequence, lastHash };
5794
6403
  }
6404
+ async enforceRetentionIfDue() {
6405
+ if (this.retentionDays === void 0) return;
6406
+ const now = this.now();
6407
+ if (now.getTime() < this.nextRetentionCheckAt) return;
6408
+ this.nextRetentionCheckAt = now.getTime() + this.retentionCheckIntervalMs;
6409
+ try {
6410
+ await this.purge({ retentionDays: this.retentionDays });
6411
+ } catch {
6412
+ }
6413
+ }
6414
+ configureByteQuota() {
6415
+ if (this.maxBytes === void 0) {
6416
+ this.db.exec("PRAGMA max_page_count = 2147483646");
6417
+ return;
6418
+ }
6419
+ const halfSplit = Math.floor((this.maxBytes - SQLITE_FIXED_OVERHEAD_BYTES) / 2);
6420
+ const journalReserve = Math.min(MAX_ROLLBACK_JOURNAL_RESERVE_BYTES, halfSplit);
6421
+ const mainBudget = this.maxBytes - SQLITE_FIXED_OVERHEAD_BYTES - journalReserve;
6422
+ if (mainBudget < MIN_SQLITE_PAGE_BUDGET_BYTES) {
6423
+ throw new Error(
6424
+ `maxBytes must be at least ${SQLITE_FIXED_OVERHEAD_BYTES + 2 * MIN_SQLITE_PAGE_BUDGET_BYTES}`
6425
+ );
6426
+ }
6427
+ const maxPages = Math.max(1, Math.floor(mainBudget / this.pageSizeBytes()));
6428
+ this.db.exec(`PRAGMA max_page_count = ${maxPages}`);
6429
+ }
5795
6430
  /**
5796
- * Drop events older than the retention window.
6431
+ * Bytes the journal actually occupies: live pages in the main database plus
6432
+ * whatever the rollback/WAL sidecars currently hold.
5797
6433
  *
5798
- * Retention is day-granular and chains are day-scoped, so a purge removes
5799
- * whole chains rather than truncating one. That is why nothing needs to be
5800
- * checkpointed here: there is no surviving suffix left dangling without an
5801
- * anchor. Any checkpoint imported from a partially-purged legacy day family
5802
- * is dropped alongside its events.
6434
+ * The quota MUST be measured this way rather than by `statSync` on the main
6435
+ * database file. SQLite never returns freed pages to the filesystem it
6436
+ * parks them on the freelist and reuses them so a database that once grew
6437
+ * large keeps that size forever, even after retention and `maxEvents` have
6438
+ * evicted almost everything. Measuring the file instead wedged the journal
6439
+ * permanently: a 3.9 GB file whose live data was 243 MB failed a 512 MB
6440
+ * quota on EVERY append, and the only escape (`chronicle compact`) refuses
6441
+ * to run while the daemon is up — and the daemon respawns on demand. Live
6442
+ * pages shrink when retention deletes rows, so the quota can recover on its
6443
+ * own; allocated size cannot.
6444
+ *
6445
+ * The sidecars keep their on-disk size: they are transient and genuinely
6446
+ * bounded by `configureByteQuota`'s half-of-budget split.
5803
6447
  */
5804
- async purge(options) {
5805
- const empty = {
5806
- deletedCount: 0,
5807
- deletedBytes: 0,
5808
- skippedCount: 0,
5809
- errors: []
5810
- };
5811
- if (!Number.isFinite(options.retentionDays) || options.retentionDays <= 0) return empty;
5812
- const cutoff = new Date(this.now().getTime() - options.retentionDays * 864e5).toISOString().slice(0, 10);
5813
- const count = this.db.prepare("SELECT COUNT(*) AS n FROM events WHERE day < ?").get(cutoff).n;
5814
- if (count === 0) return empty;
5815
- if (options.dryRun) {
5816
- const days = this.db.prepare("SELECT DISTINCT day FROM events WHERE day < ? ORDER BY day").all(cutoff);
5817
- return { ...empty, deletedCount: count, candidates: days.map((row) => row.day) };
6448
+ aggregateLiveBytes() {
6449
+ let total = 0;
6450
+ for (const file of [`${this.dbPath}-journal`, `${this.dbPath}-wal`, `${this.dbPath}-shm`]) {
6451
+ try {
6452
+ total += fs8.statSync(file).size;
6453
+ } catch (error) {
6454
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
6455
+ }
5818
6456
  }
5819
- try {
6457
+ return total + this.mainDatabaseLiveBytes();
6458
+ }
6459
+ /** Live (non-freelist) pages of the main database, in bytes. */
6460
+ mainDatabaseLiveBytes() {
6461
+ const pageCount = Number(
6462
+ this.db.prepare("PRAGMA page_count").get().page_count
6463
+ );
6464
+ const freelist = Number(
6465
+ this.db.prepare("PRAGMA freelist_count").get().freelist_count
6466
+ );
6467
+ return Math.max(0, pageCount - freelist) * this.pageSizeBytes();
6468
+ }
6469
+ /** Page size never changes for an open database; resolve it once. */
6470
+ pageSizeBytes() {
6471
+ if (this.cachedPageSize === void 0) {
6472
+ this.cachedPageSize = Number(
6473
+ this.db.prepare("PRAGMA page_size").get().page_size
6474
+ );
6475
+ }
6476
+ return this.cachedPageSize;
6477
+ }
6478
+ normalizeQuotaError(error) {
6479
+ if (this.maxBytes === void 0 || !(error instanceof Error)) return error;
6480
+ const sqliteError = error;
6481
+ const quotaExhausted = sqliteError.code === "SQLITE_FULL" || sqliteError.code === 13 || sqliteError.errcode === 13 || /database or disk is full/i.test(sqliteError.message);
6482
+ if (!quotaExhausted) return error;
6483
+ return new ChronicleStorageQuotaError({
6484
+ currentBytes: this.aggregateLiveBytes(),
6485
+ batchBytes: 0,
6486
+ maxBytes: this.maxBytes,
6487
+ path: this.dbPath
6488
+ });
6489
+ }
6490
+ assertActualAllocationWithinQuota() {
6491
+ if (this.maxBytes === void 0) return;
6492
+ const currentBytes = this.aggregateLiveBytes();
6493
+ if (currentBytes <= this.maxBytes) return;
6494
+ throw new ChronicleStorageQuotaError({
6495
+ currentBytes,
6496
+ batchBytes: 0,
6497
+ maxBytes: this.maxBytes,
6498
+ path: this.dbPath
6499
+ });
6500
+ }
6501
+ assertWithinByteQuota(events) {
6502
+ if (this.maxBytes === void 0) return;
6503
+ const currentBytes = this.aggregateLiveBytes();
6504
+ const batchBytes = Buffer.byteLength(JSON.stringify(events), "utf8");
6505
+ if (currentBytes + batchBytes <= this.maxBytes) return;
6506
+ throw new ChronicleStorageQuotaError({
6507
+ currentBytes,
6508
+ batchBytes,
6509
+ maxBytes: this.maxBytes,
6510
+ path: this.dbPath
6511
+ });
6512
+ }
6513
+ enforceEventLimitWithinTransaction() {
6514
+ const row = this.db.prepare("SELECT COUNT(*) AS count FROM events").get();
6515
+ if (this.maxEvents === void 0 || row.count <= this.maxEvents) return row.count;
6516
+ const excess = row.count - this.maxEvents;
6517
+ const boundary = this.db.prepare("SELECT day, sequence, hash FROM events ORDER BY day, sequence LIMIT 1 OFFSET ?").get(excess - 1);
6518
+ if (!boundary) return row.count;
6519
+ this.db.prepare(
6520
+ `INSERT INTO chain_checkpoint (day, sequence, hash) VALUES (?, ?, ?)
6521
+ ON CONFLICT(day) DO UPDATE SET sequence = excluded.sequence, hash = excluded.hash`
6522
+ ).run(boundary.day, boundary.sequence, boundary.hash);
6523
+ this.db.prepare("DELETE FROM events WHERE day < ? OR (day = ? AND sequence <= ?)").run(boundary.day, boundary.day, boundary.sequence);
6524
+ this.db.prepare("DELETE FROM chain_checkpoint WHERE day < ?").run(boundary.day);
6525
+ this.anchors.clear();
6526
+ return this.maxEvents;
6527
+ }
6528
+ enforceEventLimitAtStartup() {
6529
+ if (this.maxEvents === void 0 || this.retainedEventCount <= this.maxEvents) return;
6530
+ try {
6531
+ this.db.exec("BEGIN IMMEDIATE");
6532
+ const retainedCount = this.enforceEventLimitWithinTransaction();
6533
+ this.db.exec("COMMIT");
6534
+ this.retainedEventCount = retainedCount;
6535
+ } catch (error) {
6536
+ try {
6537
+ this.db.exec("ROLLBACK");
6538
+ } catch {
6539
+ }
6540
+ throw error;
6541
+ }
6542
+ }
6543
+ /**
6544
+ * Drop events older than the retention window.
6545
+ *
6546
+ * Retention is day-granular and chains are day-scoped, so a purge removes
6547
+ * whole chains rather than truncating one. That is why nothing needs to be
6548
+ * checkpointed here: there is no surviving suffix left dangling without an
6549
+ * anchor. Any checkpoint imported from a partially-purged legacy day family
6550
+ * is dropped alongside its events.
6551
+ */
6552
+ async purge(options) {
6553
+ const empty = {
6554
+ deletedCount: 0,
6555
+ deletedBytes: 0,
6556
+ skippedCount: 0,
6557
+ errors: []
6558
+ };
6559
+ if (!Number.isFinite(options.retentionDays) || options.retentionDays <= 0) return empty;
6560
+ const cutoff = new Date(this.now().getTime() - options.retentionDays * 864e5).toISOString().slice(0, 10);
6561
+ const count = this.db.prepare("SELECT COUNT(*) AS n FROM events WHERE day < ?").get(cutoff).n;
6562
+ if (count === 0) return empty;
6563
+ if (options.dryRun) {
6564
+ const days = this.db.prepare("SELECT DISTINCT day FROM events WHERE day < ? ORDER BY day").all(cutoff);
6565
+ return { ...empty, deletedCount: count, candidates: days.map((row) => row.day) };
6566
+ }
6567
+ try {
5820
6568
  this.db.exec("BEGIN IMMEDIATE");
5821
6569
  this.db.prepare("DELETE FROM events WHERE day < ?").run(cutoff);
5822
6570
  this.db.prepare("DELETE FROM chain_checkpoint WHERE day < ?").run(cutoff);
@@ -5837,6 +6585,7 @@ var ChronicleSqliteJournal = class {
5837
6585
  };
5838
6586
  }
5839
6587
  this.anchors.clear();
6588
+ this.retainedEventCount = Math.max(0, this.retainedEventCount - count);
5840
6589
  return { ...empty, deletedCount: count };
5841
6590
  }
5842
6591
  /**
@@ -5868,7 +6617,9 @@ var ChronicleSqliteJournal = class {
5868
6617
  ON CONFLICT(day) DO UPDATE SET sequence = excluded.sequence, hash = excluded.hash`
5869
6618
  );
5870
6619
  this.db.exec("BEGIN IMMEDIATE");
6620
+ let retainedCountAfterCommit = this.retainedEventCount;
5871
6621
  try {
6622
+ this.assertWithinByteQuota([]);
5872
6623
  await load({
5873
6624
  insert: (day, event) => {
5874
6625
  const row = projectEvent(event);
@@ -5898,16 +6649,19 @@ var ChronicleSqliteJournal = class {
5898
6649
  checkpoint.run(day, sequence, hash);
5899
6650
  }
5900
6651
  });
6652
+ retainedCountAfterCommit = this.enforceEventLimitWithinTransaction();
6653
+ this.assertActualAllocationWithinQuota();
5901
6654
  this.db.exec("COMMIT");
5902
6655
  } catch (error) {
5903
6656
  try {
5904
6657
  this.db.exec("ROLLBACK");
5905
6658
  } catch {
5906
6659
  }
5907
- throw error;
6660
+ throw this.normalizeQuotaError(error);
5908
6661
  } finally {
5909
6662
  this.anchors.clear();
5910
6663
  }
6664
+ this.retainedEventCount = retainedCountAfterCommit;
5911
6665
  }
5912
6666
  /**
5913
6667
  * A read engine over this journal's own connection.
@@ -6063,15 +6817,15 @@ function projectEvent(event) {
6063
6817
 
6064
6818
  // src/chronicle/project-server-client.ts
6065
6819
  import { spawn } from "node:child_process";
6066
- import * as fs7 from "node:fs";
6820
+ import * as fs9 from "node:fs";
6067
6821
  import * as net from "node:net";
6068
- import * as path12 from "node:path";
6822
+ import * as path13 from "node:path";
6069
6823
  import { fileURLToPath as fileURLToPath2 } from "node:url";
6070
6824
 
6071
6825
  // src/chronicle/project-server-endpoint.ts
6072
6826
  import { createHash as createHash6 } from "node:crypto";
6073
6827
  import * as os3 from "node:os";
6074
- import * as path11 from "node:path";
6828
+ import * as path12 from "node:path";
6075
6829
 
6076
6830
  // src/chronicle/project-server-protocol.ts
6077
6831
  var CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION = 2;
@@ -6084,39 +6838,39 @@ function encodeChronicleProjectServerMessage(message) {
6084
6838
  // src/chronicle/project-server-endpoint.ts
6085
6839
  var CHRONICLE_PROJECT_SERVER_METADATA_FILE = "server.json";
6086
6840
  function normalizedPath(value) {
6087
- const resolved = path11.resolve(value);
6841
+ const resolved = path12.resolve(value);
6088
6842
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
6089
6843
  }
6090
6844
  function chronicleProjectServerKey(projectDir) {
6091
- return createHash6("sha256").update(normalizedPath(path11.join(projectDir, "chronicle"))).digest("hex").slice(0, 24);
6845
+ return createHash6("sha256").update(normalizedPath(path12.join(projectDir, "chronicle"))).digest("hex").slice(0, 24);
6092
6846
  }
6093
6847
  function chronicleProjectServerEndpoint(projectDir) {
6094
6848
  const key = chronicleProjectServerKey(projectDir);
6095
6849
  if (process.platform === "win32") {
6096
6850
  return `\\\\.\\pipe\\wrongstack-chronicle-v${CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION}-${key}`;
6097
6851
  }
6098
- return path11.join(
6852
+ return path12.join(
6099
6853
  os3.tmpdir(),
6100
6854
  `wsch-v${CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION}`,
6101
6855
  `${key}.sock`
6102
6856
  );
6103
6857
  }
6104
6858
  function chronicleProjectServerMetadataPath(projectDir) {
6105
- return path11.join(projectDir, "chronicle", CHRONICLE_PROJECT_SERVER_METADATA_FILE);
6859
+ return path12.join(projectDir, "chronicle", CHRONICLE_PROJECT_SERVER_METADATA_FILE);
6106
6860
  }
6107
6861
 
6108
6862
  // src/chronicle/project-server-client.ts
6109
6863
  var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
6110
6864
  var SERVER_START_TIMEOUT_MS = 1e4;
6111
6865
  var DEFAULT_CALL_TIMEOUT_MS = 3e4;
6112
- function resolveChronicleDaemonAvailability(moduleUrl = import.meta.url, exists = fs7.existsSync) {
6866
+ function resolveChronicleDaemonAvailability(moduleUrl = import.meta.url, exists = fs9.existsSync) {
6113
6867
  if (process.env["WRONGSTACK_CHRONICLE_INLINE"] || process.env["WRONGSTACK_CHRONICLE_SERVER"] === "0") {
6114
6868
  return { kind: "inline-requested" };
6115
6869
  }
6116
6870
  const url = locateChronicleProjectServer(moduleUrl, exists);
6117
6871
  return url ? { kind: "available", url } : { kind: "missing-build" };
6118
6872
  }
6119
- function resolveChronicleProjectServerUrl(moduleUrl = import.meta.url, exists = fs7.existsSync) {
6873
+ function resolveChronicleProjectServerUrl(moduleUrl = import.meta.url, exists = fs9.existsSync) {
6120
6874
  const availability = resolveChronicleDaemonAvailability(moduleUrl, exists);
6121
6875
  return availability.kind === "available" ? availability.url : null;
6122
6876
  }
@@ -6285,7 +7039,7 @@ var ChronicleProjectServerClient = class {
6285
7039
  currentAuthToken() {
6286
7040
  if (this.authToken === void 0) {
6287
7041
  try {
6288
- const raw = fs7.readFileSync(
7042
+ const raw = fs9.readFileSync(
6289
7043
  chronicleProjectServerMetadataPath(this.options.projectDir),
6290
7044
  "utf8"
6291
7045
  );
@@ -6423,7 +7177,7 @@ var ChronicleProjectServerClient = class {
6423
7177
  }
6424
7178
  };
6425
7179
  function normalizePath(value) {
6426
- const resolved = path12.resolve(value);
7180
+ const resolved = path13.resolve(value);
6427
7181
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
6428
7182
  }
6429
7183
 
@@ -6431,7 +7185,7 @@ function normalizePath(value) {
6431
7185
  function resolveChronicleProjectServerOptions(options) {
6432
7186
  if (options.projectPaths) {
6433
7187
  return {
6434
- projectRoot: path13.resolve(options.projectRoot),
7188
+ projectRoot: path14.resolve(options.projectRoot),
6435
7189
  ...options.projectPaths,
6436
7190
  ...options.retentionDays !== void 0 ? { retentionDays: options.retentionDays } : {}
6437
7191
  };
@@ -6441,7 +7195,7 @@ function resolveChronicleProjectServerOptions(options) {
6441
7195
  userHome: options.userHome ?? os4.homedir()
6442
7196
  });
6443
7197
  return {
6444
- projectRoot: path13.resolve(options.projectRoot),
7198
+ projectRoot: path14.resolve(options.projectRoot),
6445
7199
  globalRoot: paths.globalRoot,
6446
7200
  projectId: paths.projectHash,
6447
7201
  projectDir: paths.projectDir,
@@ -6469,7 +7223,7 @@ function assertInlineWasRequested(caller) {
6469
7223
  );
6470
7224
  }
6471
7225
  function legacyPartitionPath(location) {
6472
- return path13.join(location.chronicleDirectory, `${location.day}.events.jsonl`);
7226
+ return path14.join(location.chronicleDirectory, `${location.day}.events.jsonl`);
6473
7227
  }
6474
7228
  function useSqliteStore() {
6475
7229
  return process.env["WRONGSTACK_CHRONICLE_STORE"] !== "jsonl";
@@ -6575,7 +7329,7 @@ var InlineChronicleProjectAccess = class {
6575
7329
  }
6576
7330
  }
6577
7331
  get chronicleDirectory() {
6578
- return path13.join(this.options.projectDir, "chronicle");
7332
+ return path14.join(this.options.projectDir, "chronicle");
6579
7333
  }
6580
7334
  journalForToday() {
6581
7335
  const now = /* @__PURE__ */ new Date();
@@ -6784,16 +7538,16 @@ async function findActiveKanbanCard(projectRoot) {
6784
7538
  }
6785
7539
  async function findFileProvenance(projectRoot, filePaths) {
6786
7540
  if (filePaths.length === 0) return [];
6787
- let access6;
7541
+ let access7;
6788
7542
  try {
6789
- access6 = createChronicleProjectAccess({ projectRoot });
7543
+ access7 = createChronicleProjectAccess({ projectRoot });
6790
7544
  } catch {
6791
7545
  return [];
6792
7546
  }
6793
7547
  const provenance = [];
6794
7548
  try {
6795
7549
  const uniquePaths = [...new Set(filePaths)];
6796
- const result = await access6.call("metrics", {
7550
+ const result = await access7.call("metrics", {
6797
7551
  view: "files",
6798
7552
  refresh: false,
6799
7553
  files: {
@@ -6818,7 +7572,7 @@ async function findFileProvenance(projectRoot, filePaths) {
6818
7572
  });
6819
7573
  }
6820
7574
  } finally {
6821
- await access6.close();
7575
+ await access7.close();
6822
7576
  }
6823
7577
  return provenance;
6824
7578
  }
@@ -6915,6 +7669,7 @@ function resolveAutoReviewConfig(cfg, sessionConfig) {
6915
7669
  provider: resolvedProvider,
6916
7670
  model: resolvedModel,
6917
7671
  fallbackModels,
7672
+ modelSelection: cfg.modelSelection === "random" ? "random" : "round-robin",
6918
7673
  debounceMs: cfg.debounceMs ?? DEFAULT_DEBOUNCE_MS,
6919
7674
  maxFilesPerBatch: cfg.maxFilesPerBatch ?? DEFAULT_MAX_FILES_PER_BATCH,
6920
7675
  maxConcurrentReviews: cfg.maxConcurrentReviews ?? DEFAULT_MAX_CONCURRENT_REVIEWS,
@@ -7035,9 +7790,9 @@ async function snapshotChangedFiles(cwd) {
7035
7790
  if (file.path.startsWith(".wrongstack/")) continue;
7036
7791
  if (budget <= 0) break;
7037
7792
  try {
7038
- const stat7 = await fsp.stat(path14.join(cwd, file.path));
7039
- if (!stat7.isFile() || stat7.size > MAX_SNAPSHOT_FILE_BYTES) continue;
7040
- const content = await fsp.readFile(path14.join(cwd, file.path), "utf8");
7793
+ const stat8 = await fsp2.stat(path15.join(cwd, file.path));
7794
+ if (!stat8.isFile() || stat8.size > MAX_SNAPSHOT_FILE_BYTES) continue;
7795
+ const content = await fsp2.readFile(path15.join(cwd, file.path), "utf8");
7041
7796
  budget -= content.length;
7042
7797
  snapshots.push({
7043
7798
  ...file,
@@ -7059,7 +7814,7 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
7059
7814
  "",
7060
7815
  "Continuous code review that fires on every change during a session.",
7061
7816
  "Detects git-tracked file edits and dispatches review subagents",
7062
- "with configurable provider/model/fallback.",
7817
+ "with configurable provider/model/fallback and profile selection.",
7063
7818
  "",
7064
7819
  "Reports are persisted and shown as passive notifications.",
7065
7820
  "They never wake the leader or spawn mutating follow-up agents.",
@@ -7074,6 +7829,7 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
7074
7829
  " provider provider id for review agents",
7075
7830
  " model model id for review agents",
7076
7831
  " fallbackProfile named profile from config.fallbackProfiles",
7832
+ " modelSelection round-robin | random (default round-robin)",
7077
7833
  " debounceMs debounce window (default 15000)",
7078
7834
  " maxFilesPerBatch max files per review (default 15)",
7079
7835
  " maxConcurrentReviews max parallel reviews (default 2)",
@@ -7100,7 +7856,8 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
7100
7856
  "",
7101
7857
  ` Provider: ${cfg.provider}`,
7102
7858
  ` Model: ${cfg.model}`,
7103
- ` Fallback: ${cfg.fallbackModels.length > 0 ? cfg.fallbackModels.join(", ") : "(none)"}`,
7859
+ ` Fallback / RR: ${cfg.fallbackModels.length > 0 ? cfg.fallbackModels.join(", ") : "(none)"}`,
7860
+ ` Selection: ${cfg.modelSelection}`,
7104
7861
  ` Debounce: ${cfg.debounceMs} ms`,
7105
7862
  ` Max files: ${cfg.maxFilesPerBatch}`,
7106
7863
  ` Max parallel: ${cfg.maxConcurrentReviews}`,
@@ -7319,6 +8076,7 @@ function createAutoReviewPlugin() {
7319
8076
  maxCascadeDepth: cfg.maxCascadeDepth
7320
8077
  });
7321
8078
  bundle.reviewFallbackModels = [...cfg.fallbackModels];
8079
+ bundle.reviewModelSelection = cfg.modelSelection;
7322
8080
  const trackedChangedPaths = new Set(
7323
8081
  snapshots.filter((file) => !file.path.startsWith(".wrongstack/")).map((file) => file.path)
7324
8082
  );
@@ -7329,7 +8087,7 @@ function createAutoReviewPlugin() {
7329
8087
  api.log.info(
7330
8088
  `[auto-review] #${reviewCounter} emitting review_needed (${filesWithContent.length} files, provider=${cfg.provider} model=${cfg.model})`
7331
8089
  );
7332
- const emittedBundle = emitReviewIfChanged(api, bundle);
8090
+ const emittedBundle = await emitReviewIfChanged(api, bundle);
7333
8091
  if (!emittedBundle) {
7334
8092
  api.log.info(
7335
8093
  `[auto-review] #${reviewCounter} skipped \u2014 file content already has a review in progress`
@@ -7362,89 +8120,94 @@ function createAutoReviewPlugin() {
7362
8120
  api.onEvent("iteration.completed", async (payload) => {
7363
8121
  await handleIterationCompleted?.(payload);
7364
8122
  });
7365
- api.onEvent("session.ended", async () => {
7366
- sessionEnded = true;
7367
- const handedOffFiles = pendingFiles.size;
7368
- cancelPendingReviewTimer();
7369
- if (handedOffFiles > 0) {
7370
- api.log.info(
7371
- `[auto-review] session ended \u2014 handed ${handedOffFiles} pending mid-session file(s) to post-session review`
7372
- );
7373
- }
7374
- await pendingReviewWork;
7375
- try {
7376
- const cfg = resolved;
7377
- if (!cfg.enabled) return;
7378
- const cwd = api.config.cwd ?? process.cwd();
7379
- if (!await isGitRepo(cwd)) return;
7380
- const allChanged = await getChangedFiles(cwd);
7381
- const existing = [];
7382
- for (const f of allChanged) {
7383
- if (f.path.startsWith(".wrongstack/")) continue;
7384
- try {
7385
- await fsp.access(path14.join(cwd, f.path));
7386
- existing.push(f);
7387
- } catch {
7388
- }
7389
- }
7390
- if (existing.length === 0) return;
7391
- const toReview = existing.slice(0, cfg.maxFilesPerBatch);
7392
- const filesWithContent = [];
7393
- for (const f of toReview) {
7394
- try {
7395
- const absPath = path14.join(cwd, f.path);
7396
- const content = await fsp.readFile(absPath, "utf8");
7397
- filesWithContent.push({ path: f.path, status: f.status, content });
7398
- } catch {
7399
- }
7400
- }
7401
- if (filesWithContent.length === 0) return;
7402
- if (inFlight.length >= cfg.maxConcurrentReviews) {
8123
+ api.onEvent("session.ended", (event) => {
8124
+ const work = (async () => {
8125
+ sessionEnded = true;
8126
+ const handedOffFiles = pendingFiles.size;
8127
+ cancelPendingReviewTimer();
8128
+ if (handedOffFiles > 0) {
7403
8129
  api.log.info(
7404
- `[auto-review] session end \u2014 at max concurrent (${cfg.maxConcurrentReviews}), skipping final review`
8130
+ `[auto-review] session ended \u2014 handed ${handedOffFiles} pending mid-session file(s) to post-session review`
7405
8131
  );
7406
- return;
7407
8132
  }
7408
- const bundle = await buildReviewContext({
7409
- cwd,
7410
- config: {
7411
- enabled: true,
7412
- provider: cfg.provider,
7413
- model: cfg.model,
7414
- maxFiles: cfg.maxFilesPerBatch,
7415
- autoFix: "off",
7416
- cascadeOn: "off",
7417
- maxCascadeDepth: 0
7418
- },
7419
- files: filesWithContent,
7420
- cascadeOn: cfg.cascadeOn,
7421
- maxCascadeDepth: cfg.maxCascadeDepth
7422
- });
7423
- bundle.reviewFallbackModels = [...cfg.fallbackModels];
7424
- const trackedChangedPaths = new Set(existing.map((file) => file.path));
7425
- bundle.allChangedFiles = bundle.allChangedFiles?.filter(
7426
- (file) => trackedChangedPaths.has(file.path)
7427
- );
7428
- const emittedBundle = emitReviewIfChanged(api, bundle);
7429
- if (!emittedBundle) {
8133
+ await pendingReviewWork;
8134
+ try {
8135
+ const cfg = resolved;
8136
+ if (!cfg.enabled) return;
8137
+ const cwd = api.config.cwd ?? process.cwd();
8138
+ if (!await isGitRepo(cwd)) return;
8139
+ const allChanged = await getChangedFiles(cwd);
8140
+ const existing = [];
8141
+ for (const f of allChanged) {
8142
+ if (f.path.startsWith(".wrongstack/")) continue;
8143
+ try {
8144
+ await fsp2.access(path15.join(cwd, f.path));
8145
+ existing.push(f);
8146
+ } catch {
8147
+ }
8148
+ }
8149
+ if (existing.length === 0) return;
8150
+ const toReview = existing.slice(0, cfg.maxFilesPerBatch);
8151
+ const filesWithContent = [];
8152
+ for (const f of toReview) {
8153
+ try {
8154
+ const absPath = path15.join(cwd, f.path);
8155
+ const content = await fsp2.readFile(absPath, "utf8");
8156
+ filesWithContent.push({ path: f.path, status: f.status, content });
8157
+ } catch {
8158
+ }
8159
+ }
8160
+ if (filesWithContent.length === 0) return;
8161
+ if (inFlight.length >= cfg.maxConcurrentReviews) {
8162
+ api.log.info(
8163
+ `[auto-review] session end \u2014 at max concurrent (${cfg.maxConcurrentReviews}), skipping final review`
8164
+ );
8165
+ return;
8166
+ }
8167
+ const bundle = await buildReviewContext({
8168
+ cwd,
8169
+ config: {
8170
+ enabled: true,
8171
+ provider: cfg.provider,
8172
+ model: cfg.model,
8173
+ maxFiles: cfg.maxFilesPerBatch,
8174
+ autoFix: "off",
8175
+ cascadeOn: "off",
8176
+ maxCascadeDepth: 0
8177
+ },
8178
+ files: filesWithContent,
8179
+ cascadeOn: cfg.cascadeOn,
8180
+ maxCascadeDepth: cfg.maxCascadeDepth
8181
+ });
8182
+ bundle.reviewFallbackModels = [...cfg.fallbackModels];
8183
+ bundle.reviewModelSelection = cfg.modelSelection;
8184
+ const trackedChangedPaths = new Set(existing.map((file) => file.path));
8185
+ bundle.allChangedFiles = bundle.allChangedFiles?.filter(
8186
+ (file) => trackedChangedPaths.has(file.path)
8187
+ );
8188
+ const emittedBundle = await emitReviewIfChanged(api, bundle);
8189
+ if (!emittedBundle) {
8190
+ api.log.info(
8191
+ "[auto-review] session end \u2014 unchanged files already have reviews in progress"
8192
+ );
8193
+ return;
8194
+ }
8195
+ const inflightEntry = {
8196
+ files: emittedBundle.files.map((file) => file.path),
8197
+ startedAt: Date.now(),
8198
+ subagentType: "review"
8199
+ };
8200
+ inFlight.push(inflightEntry);
8201
+ expireInFlight(inflightEntry);
7430
8202
  api.log.info(
7431
- "[auto-review] session end \u2014 unchanged files already have reviews in progress"
8203
+ `[auto-review] session end \u2014 final review (${emittedBundle.files.length} files)`
7432
8204
  );
7433
- return;
8205
+ } catch (err) {
8206
+ api.log.warn(`[auto-review] session.ended handler failed: ${toErrorMessage(err)}`);
7434
8207
  }
7435
- const inflightEntry = {
7436
- files: emittedBundle.files.map((file) => file.path),
7437
- startedAt: Date.now(),
7438
- subagentType: "review"
7439
- };
7440
- inFlight.push(inflightEntry);
7441
- expireInFlight(inflightEntry);
7442
- api.log.info(
7443
- `[auto-review] session end \u2014 final review (${emittedBundle.files.length} files)`
7444
- );
7445
- } catch (err) {
7446
- api.log.warn(`[auto-review] session.ended handler failed: ${toErrorMessage(err)}`);
7447
- }
8208
+ })();
8209
+ event?.waitUntil?.(work);
8210
+ return work;
7448
8211
  });
7449
8212
  api.onPattern("chimera.review_complete", (_event, payload) => {
7450
8213
  try {
@@ -7490,16 +8253,16 @@ function createAutoReviewPlugin() {
7490
8253
  // src/plugins/chimera-plugin.ts
7491
8254
  init_error();
7492
8255
  import { spawn as spawn4 } from "node:child_process";
7493
- import * as fsp4 from "node:fs/promises";
8256
+ import * as fsp5 from "node:fs/promises";
7494
8257
  import * as os5 from "node:os";
7495
- import * as path17 from "node:path";
8258
+ import * as path18 from "node:path";
7496
8259
 
7497
8260
  // src/plugins/review-finding-commands.ts
7498
8261
  init_review_finding_store();
7499
8262
 
7500
8263
  // src/plugins/review-finding-parser.ts
7501
8264
  init_review_finding_types();
7502
- import { randomUUID as randomUUID4 } from "node:crypto";
8265
+ import { randomUUID as randomUUID5 } from "node:crypto";
7503
8266
  var SUGGEST_LINE = /^\s*(?:→|->|=>)\s*(.+)$/;
7504
8267
  var DURATION_LINE = /^Duration:\s*(\d+)s\s*$/im;
7505
8268
  function parseChimeraReviewReport(reportText, context = {}) {
@@ -7507,7 +8270,7 @@ function parseChimeraReviewReport(reportText, context = {}) {
7507
8270
  return { findings: [], unparseableCount: 0 };
7508
8271
  }
7509
8272
  const findings = [];
7510
- const reportId = context.reportId ?? randomUUID4();
8273
+ const reportId = context.reportId ?? randomUUID5();
7511
8274
  let unparseableCount = 0;
7512
8275
  let durationSeconds;
7513
8276
  let currentSeverity = null;
@@ -7608,7 +8371,7 @@ function parseFindingSegment(segment, severity, context) {
7608
8371
  const fullDesc = suggestions.length > 0 ? cleanDesc + "\n" + suggestions.map((s) => " \u2192 " + s).join("\n") : cleanDesc;
7609
8372
  const suggestedFix = suggestions.length > 0 ? suggestions.join("\n") : void 0;
7610
8373
  return {
7611
- id: randomUUID4(),
8374
+ id: randomUUID5(),
7612
8375
  fingerprint: computeFindingFingerprint(file ?? "", line ?? null, title),
7613
8376
  severity,
7614
8377
  source: normalizeFindingSource(context.reviewType),
@@ -7619,7 +8382,7 @@ function parseFindingSegment(segment, severity, context) {
7619
8382
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7620
8383
  status: "active",
7621
8384
  originReport: {
7622
- reportId: context.reportId ?? randomUUID4(),
8385
+ reportId: context.reportId ?? randomUUID5(),
7623
8386
  sessionId: context.sessionId ?? "",
7624
8387
  agentId: context.agentId ?? "",
7625
8388
  reviewerModel: context.reviewerModel ?? ""
@@ -8273,7 +9036,10 @@ function buildChimeraCommand(getConfig, events) {
8273
9036
  ].join("\n")
8274
9037
  };
8275
9038
  }
8276
- events.emit("chimera.set_autofix", { mode });
9039
+ events.emit(
9040
+ "chimera.set_autofix",
9041
+ { mode }
9042
+ );
8277
9043
  return {
8278
9044
  message: `\u2705 Chimera autoFix set to "${mode}" for this session (config file unchanged).`
8279
9045
  };
@@ -8312,8 +9078,8 @@ function buildReviewCommand(_getConfig) {
8312
9078
  async run(args) {
8313
9079
  const cwd = process.cwd();
8314
9080
  const slug = projectSlug(cwd);
8315
- const globalRoot = path17.join(os5.homedir(), ".wrongstack");
8316
- const projectDir = path17.join(globalRoot, "projects", slug);
9081
+ const globalRoot = path18.join(os5.homedir(), ".wrongstack");
9082
+ const projectDir = path18.join(globalRoot, "projects", slug);
8317
9083
  const trimmed = (args ?? "").trim();
8318
9084
  const parts = trimmed.split(/\s+/).filter(Boolean);
8319
9085
  return {
@@ -8346,10 +9112,10 @@ function createChimeraPlugin() {
8346
9112
  }
8347
9113
  });
8348
9114
  api.onPattern("chimera.review_needed", (_event, payload) => {
8349
- recordStartedReview(api.events, payload);
9115
+ void recordStartedReview(api.events, payload).catch(() => void 0);
8350
9116
  });
8351
9117
  api.onPattern("chimera.review_complete", (_event, payload) => {
8352
- recordCompletedReview(api.events, payload);
9118
+ void recordCompletedReview(api.events, payload).catch(() => void 0);
8353
9119
  });
8354
9120
  if (!resolved.enabled) {
8355
9121
  api.log.info("[chimera] disabled by config");
@@ -8360,64 +9126,70 @@ function createChimeraPlugin() {
8360
9126
  );
8361
9127
  api.slashCommands.register(buildChimeraCommand(() => resolved, api.events));
8362
9128
  api.slashCommands.register(buildReviewCommand(() => resolved));
8363
- api.onEvent("session.ended", async () => {
8364
- try {
8365
- const cfg = resolved;
8366
- if (!cfg.enabled) return;
8367
- const cwd = api.config.cwd ?? process.cwd();
8368
- if (!await isGitRepo2(cwd)) {
8369
- api.log.info("[chimera] skipped \u2014 not a git repo");
8370
- return;
8371
- }
8372
- const allChanged = await getChangedFiles2(cwd);
8373
- const existing = [];
8374
- for (const f of allChanged) {
8375
- if (f.path.startsWith(".wrongstack/")) continue;
8376
- try {
8377
- await fsp4.access(path17.join(cwd, f.path));
8378
- existing.push(f);
8379
- } catch {
9129
+ api.onEvent("session.ended", (event) => {
9130
+ const work = (async () => {
9131
+ try {
9132
+ const cfg = resolved;
9133
+ if (!cfg.enabled) return;
9134
+ const cwd = api.config.cwd ?? process.cwd();
9135
+ if (!await isGitRepo2(cwd)) {
9136
+ api.log.info("[chimera] skipped \u2014 not a git repo");
9137
+ return;
8380
9138
  }
8381
- }
8382
- if (existing.length === 0) {
8383
- api.log.info("[chimera] no changed files to review");
8384
- return;
8385
- }
8386
- const toReview = existing.slice(0, cfg.maxFiles);
8387
- if (existing.length > cfg.maxFiles) {
8388
- api.log.info(`[chimera] capping review at ${cfg.maxFiles} of ${existing.length} files`);
8389
- }
8390
- const filesWithContent = [];
8391
- for (const f of toReview) {
8392
- try {
8393
- const absPath = path17.join(cwd, f.path);
8394
- const content = await fsp4.readFile(absPath, "utf8");
8395
- filesWithContent.push({ path: f.path, status: f.status, content });
8396
- } catch {
9139
+ const allChanged = await getChangedFiles2(cwd);
9140
+ const existing = [];
9141
+ for (const f of allChanged) {
9142
+ if (f.path.startsWith(".wrongstack/")) continue;
9143
+ try {
9144
+ await fsp5.access(path18.join(cwd, f.path));
9145
+ existing.push(f);
9146
+ } catch {
9147
+ }
8397
9148
  }
9149
+ if (existing.length === 0) {
9150
+ api.log.info("[chimera] no changed files to review");
9151
+ return;
9152
+ }
9153
+ const toReview = existing.slice(0, cfg.maxFiles);
9154
+ if (existing.length > cfg.maxFiles) {
9155
+ api.log.info(
9156
+ `[chimera] capping review at ${cfg.maxFiles} of ${existing.length} files`
9157
+ );
9158
+ }
9159
+ const filesWithContent = [];
9160
+ for (const f of toReview) {
9161
+ try {
9162
+ const absPath = path18.join(cwd, f.path);
9163
+ const content = await fsp5.readFile(absPath, "utf8");
9164
+ filesWithContent.push({ path: f.path, status: f.status, content });
9165
+ } catch {
9166
+ }
9167
+ }
9168
+ if (filesWithContent.length === 0) {
9169
+ api.log.info("[chimera] could not read changed files");
9170
+ return;
9171
+ }
9172
+ const bundle = await buildReviewContext({
9173
+ cwd,
9174
+ config: cfg,
9175
+ files: filesWithContent,
9176
+ cascadeOn: cfg.cascadeOn,
9177
+ maxCascadeDepth: cfg.maxCascadeDepth
9178
+ });
9179
+ const emittedBundle = await emitReviewIfChanged(api, bundle);
9180
+ if (!emittedBundle) {
9181
+ api.log.info("[chimera] skipped \u2014 changed files already have reviews in progress");
9182
+ return;
9183
+ }
9184
+ api.log.info(
9185
+ `[chimera] emitted review_needed event (${emittedBundle.files.length} files)`
9186
+ );
9187
+ } catch (err) {
9188
+ api.log.warn(`[chimera] session.ended handler failed: ${toErrorMessage(err)}`);
8398
9189
  }
8399
- if (filesWithContent.length === 0) {
8400
- api.log.info("[chimera] could not read changed files");
8401
- return;
8402
- }
8403
- const bundle = await buildReviewContext({
8404
- cwd,
8405
- config: cfg,
8406
- files: filesWithContent,
8407
- cascadeOn: cfg.cascadeOn,
8408
- maxCascadeDepth: cfg.maxCascadeDepth
8409
- });
8410
- const emittedBundle = emitReviewIfChanged(api, bundle);
8411
- if (!emittedBundle) {
8412
- api.log.info("[chimera] skipped \u2014 changed files already have reviews in progress");
8413
- return;
8414
- }
8415
- api.log.info(
8416
- `[chimera] emitted review_needed event (${emittedBundle.files.length} files)`
8417
- );
8418
- } catch (err) {
8419
- api.log.warn(`[chimera] session.ended handler failed: ${toErrorMessage(err)}`);
8420
- }
9190
+ })();
9191
+ event?.waitUntil?.(work);
9192
+ return work;
8421
9193
  });
8422
9194
  },
8423
9195
  teardown(api) {
@@ -8432,17 +9204,17 @@ function createChimeraPlugin() {
8432
9204
  }
8433
9205
 
8434
9206
  // src/plugins/prompts-plugin.ts
8435
- import * as fs11 from "node:fs/promises";
8436
- import * as path20 from "node:path";
9207
+ import * as fs13 from "node:fs/promises";
9208
+ import * as path21 from "node:path";
8437
9209
 
8438
9210
  // src/execution/prompt-loader.ts
8439
- import * as fs9 from "node:fs/promises";
8440
- import * as path19 from "node:path";
9211
+ import * as fs11 from "node:fs/promises";
9212
+ import * as path20 from "node:path";
8441
9213
 
8442
9214
  // src/storage/prompt-store.ts
8443
9215
  init_atomic_write();
8444
- import * as fs8 from "node:fs/promises";
8445
- import * as path18 from "node:path";
9216
+ import * as fs10 from "node:fs/promises";
9217
+ import * as path19 from "node:path";
8446
9218
  var SCHEMA_VERSION3 = 2;
8447
9219
  function migratePromptEntry(raw) {
8448
9220
  if (!raw || typeof raw !== "object") return null;
@@ -8503,12 +9275,12 @@ var DefaultPromptStore = class {
8503
9275
  await ensureDir(this.dir);
8504
9276
  const entries = [];
8505
9277
  try {
8506
- const files = await fs8.readdir(this.dir);
9278
+ const files = await fs10.readdir(this.dir);
8507
9279
  for (const file of files) {
8508
9280
  if (!file.endsWith(".json")) continue;
8509
9281
  try {
8510
9282
  const raw = JSON.parse(
8511
- await fs8.readFile(path18.join(this.dir, file), "utf8")
9283
+ await fs10.readFile(path19.join(this.dir, file), "utf8")
8512
9284
  );
8513
9285
  const migrated = migratePromptEntry(raw.entry);
8514
9286
  if (migrated) entries.push(migrated);
@@ -8522,9 +9294,9 @@ var DefaultPromptStore = class {
8522
9294
  );
8523
9295
  }
8524
9296
  async get(id) {
8525
- const file = path18.join(this.dir, `${id}.json`);
9297
+ const file = path19.join(this.dir, `${id}.json`);
8526
9298
  try {
8527
- const raw = JSON.parse(await fs8.readFile(file, "utf8"));
9299
+ const raw = JSON.parse(await fs10.readFile(file, "utf8"));
8528
9300
  return migratePromptEntry(raw.entry);
8529
9301
  } catch {
8530
9302
  return null;
@@ -8532,14 +9304,14 @@ var DefaultPromptStore = class {
8532
9304
  }
8533
9305
  async save(entry) {
8534
9306
  await ensureDir(this.dir);
8535
- const file = path18.join(this.dir, `${entry.id}.json`);
9307
+ const file = path19.join(this.dir, `${entry.id}.json`);
8536
9308
  const raw = { version: SCHEMA_VERSION3, entry };
8537
9309
  await atomicWrite(file, JSON.stringify(raw, null, 2));
8538
9310
  }
8539
9311
  async delete(id) {
8540
- const file = path18.join(this.dir, `${id}.json`);
9312
+ const file = path19.join(this.dir, `${id}.json`);
8541
9313
  try {
8542
- await fs8.unlink(file);
9314
+ await fs10.unlink(file);
8543
9315
  return true;
8544
9316
  } catch {
8545
9317
  return false;
@@ -8626,7 +9398,7 @@ var DefaultPromptLoader = class {
8626
9398
  constructor(opts) {
8627
9399
  this.projectStore = typeof opts.paths.inProjectPrompts === "string" ? new DefaultPromptStore(opts.paths.inProjectPrompts) : void 0;
8628
9400
  this.userStore = typeof opts.paths.globalPrompts === "string" ? new DefaultPromptStore(opts.paths.globalPrompts) : void 0;
8629
- this.builtinDir = opts.bundledDir ? path19.join(opts.bundledDir, "prompts") : void 0;
9401
+ this.builtinDir = opts.bundledDir ? path20.join(opts.bundledDir, "prompts") : void 0;
8630
9402
  }
8631
9403
  async list() {
8632
9404
  if (this.cache) return this.cache;
@@ -8739,7 +9511,7 @@ var DefaultPromptLoader = class {
8739
9511
  const files = await walkJson(dir);
8740
9512
  for (const file of files) {
8741
9513
  try {
8742
- const parsed = JSON.parse(await fs9.readFile(file, "utf8"));
9514
+ const parsed = JSON.parse(await fs11.readFile(file, "utf8"));
8743
9515
  const migrated = migratePromptEntry(parsed);
8744
9516
  if (migrated) out.push({ ...migrated, source: "builtin" });
8745
9517
  } catch {
@@ -8753,12 +9525,12 @@ async function walkJson(dir) {
8753
9525
  const out = [];
8754
9526
  let entries;
8755
9527
  try {
8756
- entries = await fs9.readdir(dir, { withFileTypes: true });
9528
+ entries = await fs11.readdir(dir, { withFileTypes: true });
8757
9529
  } catch {
8758
9530
  return out;
8759
9531
  }
8760
9532
  for (const e of entries) {
8761
- const full = path19.join(dir, e.name);
9533
+ const full = path20.join(dir, e.name);
8762
9534
  if (e.isDirectory()) {
8763
9535
  out.push(...await walkJson(full));
8764
9536
  } else if (e.name.endsWith(".json") && e.name !== "index.json" && e.name !== "schema.json") {
@@ -8833,7 +9605,7 @@ function escapeRegExp(s) {
8833
9605
 
8834
9606
  // src/storage/prompt-usage-store.ts
8835
9607
  init_atomic_write();
8836
- import * as fs10 from "node:fs/promises";
9608
+ import * as fs12 from "node:fs/promises";
8837
9609
  var PromptUsageStore = class {
8838
9610
  constructor(file) {
8839
9611
  this.file = file;
@@ -8853,7 +9625,7 @@ var PromptUsageStore = class {
8853
9625
  return this.cachedUsage;
8854
9626
  }
8855
9627
  try {
8856
- const raw = JSON.parse(await fs10.readFile(this.file, "utf8"));
9628
+ const raw = JSON.parse(await fs12.readFile(this.file, "utf8"));
8857
9629
  if (raw && typeof raw === "object" && raw.usage && typeof raw.usage === "object") {
8858
9630
  this.cachedUsage = raw.usage;
8859
9631
  this.cachedSignature = signature;
@@ -8933,8 +9705,8 @@ var PromptUsageStore = class {
8933
9705
  }
8934
9706
  async fileSignature() {
8935
9707
  try {
8936
- const stat7 = await fs10.stat(this.file);
8937
- return { size: stat7.size, mtimeMs: stat7.mtimeMs, ctimeMs: stat7.ctimeMs };
9708
+ const stat8 = await fs12.stat(this.file);
9709
+ return { size: stat8.size, mtimeMs: stat8.mtimeMs, ctimeMs: stat8.ctimeMs };
8938
9710
  } catch {
8939
9711
  return null;
8940
9712
  }
@@ -8958,7 +9730,7 @@ function createPromptsPlugin(opts) {
8958
9730
  version: "1.0.0",
8959
9731
  description: "Prompt library with 100+ builtin prompts, search, and AI authoring",
8960
9732
  apiVersion: "^0.1",
8961
- capabilities: { slashCommands: true },
9733
+ capabilities: { slashCommands: true, llm: true },
8962
9734
  defaultConfig: {},
8963
9735
  setup(api) {
8964
9736
  const rawConfig = api.config;
@@ -8969,7 +9741,7 @@ function createPromptsPlugin(opts) {
8969
9741
  bundledDir: rawConfig["bundledPromptsDir"]
8970
9742
  }) : null);
8971
9743
  usage = opts?.usage ?? (paths ? new PromptUsageStore(paths.promptUsage) : null);
8972
- api.slashCommands.register(buildPromptsCommand(() => store, () => loader));
9744
+ api.slashCommands.register(buildPromptsCommand(() => store, () => loader, () => api.llm));
8973
9745
  api.slashCommands.register(buildPromptSearchCommand(() => loader, () => usage));
8974
9746
  api.slashCommands.register(buildPromptGenCommand(() => loader));
8975
9747
  api.log.info("[prompts] loaded \u2014 /prompts, /prompt, /prompt-gen available");
@@ -8985,7 +9757,7 @@ function createPromptsPlugin(opts) {
8985
9757
  }
8986
9758
  };
8987
9759
  }
8988
- function buildPromptsCommand(getStore, getLoader) {
9760
+ function buildPromptsCommand(getStore, getLoader, getLlm) {
8989
9761
  return {
8990
9762
  name: "prompts",
8991
9763
  description: "Manage your prompt library: /prompts [list|view|add|edit|delete|favorite|extend]",
@@ -9080,16 +9852,25 @@ ${lines.join("\n")}
9080
9852
  const matches2 = await store.find(parsed.title);
9081
9853
  if (matches2.length === 0) return { message: `No prompt matching "${parsed.title}".` };
9082
9854
  const exact = matches2.find((m) => m.title.toLowerCase() === parsed.title?.toLowerCase()) ?? expectDefined(matches2[0]);
9083
- const prov = ctx.provider;
9084
- if (!prov?.complete) return { message: "LLM not available. Configure a provider first." };
9085
- const enhanced = await prov.complete(
9086
- ctx.model,
9087
- renderInstructionTemplate(readBundledInstructionText("llm/prompt-extend.md"), {
9088
- existingPrompt: exact.content,
9089
- additionalInstructions: parsed.content
9090
- })
9091
- );
9092
- exact.content = enhanced.trim();
9855
+ const llm = getLlm();
9856
+ if (!llm) return { message: "LLM not available. Configure a provider first." };
9857
+ let enhanced;
9858
+ try {
9859
+ enhanced = await llm.complete(
9860
+ renderInstructionTemplate(readBundledInstructionText("llm/prompt-extend.md"), {
9861
+ existingPrompt: exact.content,
9862
+ additionalInstructions: parsed.content
9863
+ }),
9864
+ {
9865
+ system: "You improve reusable prompts while preserving their intent and variables.",
9866
+ role: "prompt-refiner",
9867
+ maxTokens: 2048
9868
+ }
9869
+ );
9870
+ } catch {
9871
+ return { message: "LLM enhancement failed. The saved prompt was not changed." };
9872
+ }
9873
+ exact.content = enhanced.text.trim();
9093
9874
  exact.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9094
9875
  await store.save(exact);
9095
9876
  getLoader()?.invalidateCache();
@@ -9111,7 +9892,7 @@ ${exact.content}` };
9111
9892
  2
9112
9893
  );
9113
9894
  try {
9114
- await fs11.writeFile(target, payload, "utf8");
9895
+ await fs13.writeFile(target, payload, "utf8");
9115
9896
  return { message: `Exported ${own.length} prompt(s) \u2192 ${target}` };
9116
9897
  } catch (err) {
9117
9898
  return { message: `Export failed: ${err instanceof Error ? err.message : String(err)}` };
@@ -9123,7 +9904,7 @@ ${exact.content}` };
9123
9904
  const src = resolveIoPath(restJoined, ctx);
9124
9905
  let raw;
9125
9906
  try {
9126
- raw = JSON.parse(await fs11.readFile(src, "utf8"));
9907
+ raw = JSON.parse(await fs13.readFile(src, "utf8"));
9127
9908
  } catch (err) {
9128
9909
  return { message: `Import failed: ${err instanceof Error ? err.message : String(err)}` };
9129
9910
  }
@@ -9329,9 +10110,9 @@ ${dim(`slug: ${entry.slug} | category: ${entry.category} | source: ${entry.sourc
9329
10110
  }
9330
10111
  function resolveIoPath(p, ctx) {
9331
10112
  const cleaned = p.trim().replace(/^["']|["']$/g, "");
9332
- if (path20.isAbsolute(cleaned)) return cleaned;
10113
+ if (path21.isAbsolute(cleaned)) return cleaned;
9333
10114
  const base = ctx.projectRoot ?? process.cwd();
9334
- return path20.resolve(base, cleaned);
10115
+ return path21.resolve(base, cleaned);
9335
10116
  }
9336
10117
  function sourceGlyph(e) {
9337
10118
  return e.source === "project" ? "\u{1F4C1}" : e.source === "user" ? "\u{1F464}" : e.source === "synced" ? "\u2601" : "\u{1F4E6}";
@@ -9455,8 +10236,8 @@ async function integrateFindings(payload, projectDir, reportId) {
9455
10236
  init_atomic_write();
9456
10237
  init_review_finding_store();
9457
10238
  init_review_report_store();
9458
- import * as fsp5 from "node:fs/promises";
9459
- import * as path21 from "node:path";
10239
+ import * as fsp6 from "node:fs/promises";
10240
+ import * as path22 from "node:path";
9460
10241
  var REVIEW_STORE_COMPACTION_THRESHOLD_BYTES = 8 * 1024 * 1024;
9461
10242
  var REVIEW_STORE_COMPACTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
9462
10243
  var REVIEW_STORE_MAINTENANCE_FILE = ".review-store-maintenance.json";
@@ -9464,7 +10245,7 @@ async function maybeCompactReviewStores(projectDir, opts = {}) {
9464
10245
  const thresholdBytes = opts.thresholdBytes ?? REVIEW_STORE_COMPACTION_THRESHOLD_BYTES;
9465
10246
  const intervalMs = opts.intervalMs ?? REVIEW_STORE_COMPACTION_INTERVAL_MS;
9466
10247
  const now = opts.now ?? Date.now();
9467
- const maintenancePath = path21.join(projectDir, REVIEW_STORE_MAINTENANCE_FILE);
10248
+ const maintenancePath = path22.join(projectDir, REVIEW_STORE_MAINTENANCE_FILE);
9468
10249
  return withFileLock(maintenancePath, async () => {
9469
10250
  const totalBytes = await fileSize(resolveReportStorePath(projectDir)) + await fileSize(resolveFindingStorePath(projectDir));
9470
10251
  if (totalBytes < thresholdBytes) {
@@ -9504,7 +10285,7 @@ function emptyResult(reason, totalBytes) {
9504
10285
  }
9505
10286
  async function fileSize(filePath) {
9506
10287
  try {
9507
- return (await fsp5.stat(filePath)).size;
10288
+ return (await fsp6.stat(filePath)).size;
9508
10289
  } catch (error) {
9509
10290
  if (error.code === "ENOENT") return 0;
9510
10291
  throw error;
@@ -9512,7 +10293,7 @@ async function fileSize(filePath) {
9512
10293
  }
9513
10294
  async function readMaintenanceState(filePath) {
9514
10295
  try {
9515
- const parsed = JSON.parse(await fsp5.readFile(filePath, "utf8"));
10296
+ const parsed = JSON.parse(await fsp6.readFile(filePath, "utf8"));
9516
10297
  return typeof parsed.compactedAt === "string" && Number.isFinite(parsed.totalBytesBefore) ? parsed : null;
9517
10298
  } catch (error) {
9518
10299
  if (error.code === "ENOENT" || error instanceof SyntaxError) {
@@ -9524,7 +10305,7 @@ async function readMaintenanceState(filePath) {
9524
10305
 
9525
10306
  // src/plugins/skills-plugin.ts
9526
10307
  import * as os7 from "node:os";
9527
- import * as path26 from "node:path";
10308
+ import * as path27 from "node:path";
9528
10309
 
9529
10310
  // src/skills/foreign-sources.ts
9530
10311
  var FOREIGN_SKILL_TOOLS = [
@@ -9706,8 +10487,8 @@ function numField(rec, key) {
9706
10487
  // src/skills/skill-generator.ts
9707
10488
  init_errors();
9708
10489
  import { spawn as spawn5 } from "node:child_process";
9709
- import * as fs12 from "node:fs/promises";
9710
- import * as path22 from "node:path";
10490
+ import * as fs14 from "node:fs/promises";
10491
+ import * as path23 from "node:path";
9711
10492
  async function validateSkillNameAvailable(name, loader) {
9712
10493
  const formatViolations = validateSkillName(name);
9713
10494
  const conflicts = loader ? (await loader.listEntries()).filter((e) => e.name === name) : [];
@@ -9812,7 +10593,12 @@ async function openInEditor(filePath, env = process.env) {
9812
10593
  const child = spawn5(parts[0], [...parts.slice(1), filePath], {
9813
10594
  stdio: "ignore",
9814
10595
  detached: true,
9815
- shell
10596
+ shell,
10597
+ // `shell` routes through cmd.exe on win32, which flashes a console window
10598
+ // before the editor appears. A GUI editor is unaffected by the flag; the
10599
+ // shell wrapper is what it suppresses. Repo convention — see
10600
+ // `core/tests/architecture/spawn-convention.test.ts`.
10601
+ windowsHide: true
9816
10602
  });
9817
10603
  child.unref();
9818
10604
  }
@@ -9826,11 +10612,11 @@ async function writeSkeletonSkill(skillsDir, body, opts = {}) {
9826
10612
  subsystem: "general"
9827
10613
  });
9828
10614
  }
9829
- const skillDir = path22.join(skillsDir, name);
9830
- const skillFile = path22.join(skillDir, "SKILL.md");
10615
+ const skillDir = path23.join(skillsDir, name);
10616
+ const skillFile = path23.join(skillDir, "SKILL.md");
9831
10617
  if (!opts.overwrite) {
9832
10618
  try {
9833
- await fs12.access(skillFile);
10619
+ await fs14.access(skillFile);
9834
10620
  throw new WrongStackError({
9835
10621
  message: `A skill already exists at ${skillFile}. Use --force to overwrite.`,
9836
10622
  code: ERROR_CODES.VALIDATION_ERROR,
@@ -9841,8 +10627,8 @@ async function writeSkeletonSkill(skillsDir, body, opts = {}) {
9841
10627
  if (err instanceof WrongStackError) throw err;
9842
10628
  }
9843
10629
  }
9844
- await fs12.mkdir(skillDir, { recursive: true });
9845
- await fs12.writeFile(skillFile, body, "utf8");
10630
+ await fs14.mkdir(skillDir, { recursive: true });
10631
+ await fs14.writeFile(skillFile, body, "utf8");
9846
10632
  return skillFile;
9847
10633
  }
9848
10634
  function bodyLineAdvisory(body) {
@@ -9885,15 +10671,15 @@ function defaultEditor() {
9885
10671
  // src/skills/skill-installer.ts
9886
10672
  init_errors();
9887
10673
  init_error();
9888
- import * as fs15 from "node:fs/promises";
9889
- import * as path25 from "node:path";
10674
+ import * as fs17 from "node:fs/promises";
10675
+ import * as path26 from "node:path";
9890
10676
 
9891
10677
  // src/skills/github-fetcher.ts
9892
10678
  init_errors();
9893
10679
  import { createWriteStream } from "node:fs";
9894
- import * as fs13 from "node:fs/promises";
10680
+ import * as fs15 from "node:fs/promises";
9895
10681
  import * as os6 from "node:os";
9896
- import * as path23 from "node:path";
10682
+ import * as path24 from "node:path";
9897
10683
  import { Readable } from "node:stream";
9898
10684
  import { pipeline } from "node:stream/promises";
9899
10685
  import { createGunzip } from "node:zlib";
@@ -10035,7 +10821,7 @@ async function downloadGitHubTarball(parsed) {
10035
10821
  }
10036
10822
  });
10037
10823
  }
10038
- const tempDir = await fs13.mkdtemp(path23.join(os6.tmpdir(), "wskill-"));
10824
+ const tempDir = await fs15.mkdtemp(path24.join(os6.tmpdir(), "wskill-"));
10039
10825
  try {
10040
10826
  if (!response.body) {
10041
10827
  throw new WrongStackError({
@@ -10046,17 +10832,17 @@ async function downloadGitHubTarball(parsed) {
10046
10832
  });
10047
10833
  }
10048
10834
  const nodeStream = Readable.fromWeb(response.body);
10049
- const tarPath = path23.join(tempDir, ".wrongstack-download.tar");
10835
+ const tarPath = path24.join(tempDir, ".wrongstack-download.tar");
10050
10836
  await writeBoundedGzipStream(nodeStream, tarPath, SKILL_LIMITS.MAX_UNCOMPRESSED_TARBALL_SIZE);
10051
- const tarBuf = await fs13.readFile(tarPath);
10837
+ const tarBuf = await fs15.readFile(tarPath);
10052
10838
  try {
10053
10839
  await extractTar(tarBuf, tempDir);
10054
10840
  } finally {
10055
- await fs13.rm(tarPath, { force: true });
10841
+ await fs15.rm(tarPath, { force: true });
10056
10842
  }
10057
10843
  return { tempDir };
10058
10844
  } catch (err) {
10059
- await fs13.rm(tempDir, { recursive: true, force: true }).catch(() => {
10845
+ await fs15.rm(tempDir, { recursive: true, force: true }).catch(() => {
10060
10846
  });
10061
10847
  throw err;
10062
10848
  }
@@ -10111,25 +10897,25 @@ async function extractTar(buf, destDir) {
10111
10897
  const fullPath = prefix ? `${prefix}/${name}` : name;
10112
10898
  const relPath = stripTopDir(fullPath);
10113
10899
  if (relPath && relPath !== "." && relPath !== "..") {
10114
- const destPath = path23.join(destDir, relPath);
10115
- const resolvedDest = path23.resolve(destPath);
10116
- const resolvedRoot = path23.resolve(destDir);
10117
- if (resolvedDest !== resolvedRoot && !resolvedDest.startsWith(resolvedRoot + path23.sep)) {
10900
+ const destPath = path24.join(destDir, relPath);
10901
+ const resolvedDest = path24.resolve(destPath);
10902
+ const resolvedRoot = path24.resolve(destDir);
10903
+ if (resolvedDest !== resolvedRoot && !resolvedDest.startsWith(resolvedRoot + path24.sep)) {
10118
10904
  offset += 512 + Math.ceil(size / 512) * 512;
10119
10905
  continue;
10120
10906
  }
10121
10907
  if (typeflag === 53 || typeflag === 0) {
10122
10908
  if (relPath.endsWith("/") || typeflag === 53) {
10123
- await fs13.mkdir(destPath, { recursive: true });
10909
+ await fs15.mkdir(destPath, { recursive: true });
10124
10910
  }
10125
10911
  }
10126
10912
  if ((typeflag === 48 || typeflag === 0 || typeflag === 0) && size > 0) {
10127
- const dir = path23.dirname(destPath);
10128
- await fs13.mkdir(dir, { recursive: true });
10913
+ const dir = path24.dirname(destPath);
10914
+ await fs15.mkdir(dir, { recursive: true });
10129
10915
  const dataStart = offset + 512;
10130
10916
  const dataEnd = dataStart + size;
10131
10917
  if (dataEnd > buf.length) break;
10132
- await fs13.writeFile(destPath, buf.subarray(dataStart, dataEnd));
10918
+ await fs15.writeFile(destPath, buf.subarray(dataStart, dataEnd));
10133
10919
  }
10134
10920
  }
10135
10921
  offset += 512 + Math.ceil(size / 512) * 512;
@@ -10138,8 +10924,8 @@ async function extractTar(buf, destDir) {
10138
10924
 
10139
10925
  // src/skills/manifest-store.ts
10140
10926
  init_atomic_write();
10141
- import * as fs14 from "node:fs/promises";
10142
- import * as path24 from "node:path";
10927
+ import * as fs16 from "node:fs/promises";
10928
+ import * as path25 from "node:path";
10143
10929
  var SkillManifestStore = class {
10144
10930
  manifestPath;
10145
10931
  cache;
@@ -10149,7 +10935,7 @@ var SkillManifestStore = class {
10149
10935
  async read() {
10150
10936
  if (this.cache) return this.cache;
10151
10937
  try {
10152
- const raw = await fs14.readFile(this.manifestPath, "utf8");
10938
+ const raw = await fs16.readFile(this.manifestPath, "utf8");
10153
10939
  const data = JSON.parse(raw);
10154
10940
  if (!Array.isArray(data.skills)) {
10155
10941
  this.cache = { skills: [] };
@@ -10162,8 +10948,8 @@ var SkillManifestStore = class {
10162
10948
  return this.cache;
10163
10949
  }
10164
10950
  async write(data) {
10165
- const dir = path24.dirname(this.manifestPath);
10166
- await fs14.mkdir(dir, { recursive: true });
10951
+ const dir = path25.dirname(this.manifestPath);
10952
+ await fs16.mkdir(dir, { recursive: true });
10167
10953
  await atomicWrite(this.manifestPath, JSON.stringify(data, null, 2) + "\n");
10168
10954
  this.cache = data;
10169
10955
  }
@@ -10201,8 +10987,8 @@ var SkillManifestStore = class {
10201
10987
 
10202
10988
  // src/skills/skill-installer.ts
10203
10989
  function isInside(resolved, destDir) {
10204
- const root = path25.resolve(destDir);
10205
- return resolved === root || resolved.startsWith(root + path25.sep);
10990
+ const root = path26.resolve(destDir);
10991
+ return resolved === root || resolved.startsWith(root + path26.sep);
10206
10992
  }
10207
10993
  var MAX_SKILL_FILE_SIZE = SKILL_LIMITS.MAX_SKILL_FILE_SIZE;
10208
10994
  var SkillInstaller = class {
@@ -10257,13 +11043,13 @@ var SkillInstaller = class {
10257
11043
  this.opts.log?.(`Overwriting existing skill "${skill.name}" (${scope})...`);
10258
11044
  await this.removeSkillFiles(skill.name, scope);
10259
11045
  }
10260
- const destDir = path25.join(targetDir, skill.name);
10261
- await fs15.mkdir(destDir, { recursive: true });
11046
+ const destDir = path26.join(targetDir, skill.name);
11047
+ await fs17.mkdir(destDir, { recursive: true });
10262
11048
  const copiedFiles = [];
10263
11049
  for (const file of skill.files) {
10264
- const srcPath = path25.join(skill.baseDir, file);
10265
- const destPath = path25.join(destDir, file);
10266
- const resolved2 = path25.resolve(destPath);
11050
+ const srcPath = path26.join(skill.baseDir, file);
11051
+ const destPath = path26.join(destDir, file);
11052
+ const resolved2 = path26.resolve(destPath);
10267
11053
  if (!isInside(resolved2, destDir)) {
10268
11054
  throw new FsError({
10269
11055
  message: `Path traversal detected in skill file: ${file}`,
@@ -10272,17 +11058,17 @@ var SkillInstaller = class {
10272
11058
  context: { reason: "path_traversal", skillName: skill.name }
10273
11059
  });
10274
11060
  }
10275
- const stat7 = await fs15.stat(srcPath);
10276
- if (stat7.size > MAX_SKILL_FILE_SIZE) {
11061
+ const stat8 = await fs17.stat(srcPath);
11062
+ if (stat8.size > MAX_SKILL_FILE_SIZE) {
10277
11063
  throw new FsError({
10278
- message: `Skill file "${file}" is too large (${(stat7.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`,
11064
+ message: `Skill file "${file}" is too large (${(stat8.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`,
10279
11065
  code: ERROR_CODES.FS_WRITE_FAILED,
10280
11066
  path: srcPath,
10281
- context: { skillName: skill.name, fileSize: stat7.size, maxSize: MAX_SKILL_FILE_SIZE }
11067
+ context: { skillName: skill.name, fileSize: stat8.size, maxSize: MAX_SKILL_FILE_SIZE }
10282
11068
  });
10283
11069
  }
10284
- await fs15.mkdir(path25.dirname(destPath), { recursive: true });
10285
- await fs15.copyFile(srcPath, destPath);
11070
+ await fs17.mkdir(path26.dirname(destPath), { recursive: true });
11071
+ await fs17.copyFile(srcPath, destPath);
10286
11072
  copiedFiles.push(file);
10287
11073
  }
10288
11074
  const entry = {
@@ -10312,7 +11098,7 @@ var SkillInstaller = class {
10312
11098
  this.invalidateLoaderCache();
10313
11099
  return results;
10314
11100
  } finally {
10315
- await fs15.rm(tempDir, { recursive: true, force: true }).catch(() => {
11101
+ await fs17.rm(tempDir, { recursive: true, force: true }).catch(() => {
10316
11102
  });
10317
11103
  }
10318
11104
  }
@@ -10327,7 +11113,7 @@ var SkillInstaller = class {
10327
11113
  const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
10328
11114
  let entries;
10329
11115
  try {
10330
- entries = await fs15.readdir(srcDir, { withFileTypes: true });
11116
+ entries = await fs17.readdir(srcDir, { withFileTypes: true });
10331
11117
  } catch {
10332
11118
  throw new WrongStackError({
10333
11119
  message: `Source directory not found or not readable: ${srcDir}`,
@@ -10339,10 +11125,10 @@ var SkillInstaller = class {
10339
11125
  const results = [];
10340
11126
  for (const e of entries) {
10341
11127
  if (!await entryIsDirectory(srcDir, e)) continue;
10342
- const skillMdPath = path25.join(srcDir, e.name, "SKILL.md");
11128
+ const skillMdPath = path26.join(srcDir, e.name, "SKILL.md");
10343
11129
  let content;
10344
11130
  try {
10345
- content = await fs15.readFile(skillMdPath, "utf8");
11131
+ content = await fs17.readFile(skillMdPath, "utf8");
10346
11132
  } catch {
10347
11133
  continue;
10348
11134
  }
@@ -10352,15 +11138,15 @@ var SkillInstaller = class {
10352
11138
  if (existing.find((x) => x.scope === scope)) {
10353
11139
  await this.removeSkillFiles(fm.name, scope);
10354
11140
  }
10355
- const destDir = path25.join(targetDir, fm.name);
10356
- await fs15.mkdir(destDir, { recursive: true });
10357
- const srcSkillDir = path25.join(srcDir, e.name);
11141
+ const destDir = path26.join(targetDir, fm.name);
11142
+ await fs17.mkdir(destDir, { recursive: true });
11143
+ const srcSkillDir = path26.join(srcDir, e.name);
10358
11144
  const files = await collectFiles(srcSkillDir, srcSkillDir);
10359
11145
  const copiedFiles = [];
10360
11146
  for (const file of files) {
10361
- const srcPath = path25.join(srcSkillDir, file);
10362
- const destPath = path25.join(destDir, file);
10363
- const resolved = path25.resolve(destPath);
11147
+ const srcPath = path26.join(srcSkillDir, file);
11148
+ const destPath = path26.join(destDir, file);
11149
+ const resolved = path26.resolve(destPath);
10364
11150
  if (!isInside(resolved, destDir)) {
10365
11151
  throw new FsError({
10366
11152
  message: `Path traversal detected in skill file: ${file}`,
@@ -10369,16 +11155,16 @@ var SkillInstaller = class {
10369
11155
  context: { reason: "path_traversal", skillName: fm.name }
10370
11156
  });
10371
11157
  }
10372
- await fs15.mkdir(path25.dirname(destPath), { recursive: true });
11158
+ await fs17.mkdir(path26.dirname(destPath), { recursive: true });
10373
11159
  if (opts?.link) {
10374
11160
  try {
10375
- await fs15.symlink(srcPath, destPath);
11161
+ await fs17.symlink(srcPath, destPath);
10376
11162
  } catch (err) {
10377
- await fs15.copyFile(srcPath, destPath);
11163
+ await fs17.copyFile(srcPath, destPath);
10378
11164
  this.opts.log?.(`symlink failed for ${file} (${toErrorMessage(err)}); copied instead`);
10379
11165
  }
10380
11166
  } else {
10381
- await fs15.copyFile(srcPath, destPath);
11167
+ await fs17.copyFile(srcPath, destPath);
10382
11168
  }
10383
11169
  copiedFiles.push(file);
10384
11170
  }
@@ -10532,10 +11318,10 @@ var SkillInstaller = class {
10532
11318
  */
10533
11319
  async detectSkills(baseDir) {
10534
11320
  const results = [];
10535
- const rootSkillMd = path25.join(baseDir, "SKILL.md");
11321
+ const rootSkillMd = path26.join(baseDir, "SKILL.md");
10536
11322
  try {
10537
- await fs15.access(rootSkillMd);
10538
- const content = await fs15.readFile(rootSkillMd, "utf8");
11323
+ await fs17.access(rootSkillMd);
11324
+ const content = await fs17.readFile(rootSkillMd, "utf8");
10539
11325
  const fm = parseSkillFrontmatter(content);
10540
11326
  if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
10541
11327
  results.push({
@@ -10547,17 +11333,17 @@ var SkillInstaller = class {
10547
11333
  }
10548
11334
  } catch {
10549
11335
  }
10550
- const skillsDir = path25.join(baseDir, "skills");
11336
+ const skillsDir = path26.join(baseDir, "skills");
10551
11337
  try {
10552
- const entries = await fs15.readdir(skillsDir, { withFileTypes: true });
11338
+ const entries = await fs17.readdir(skillsDir, { withFileTypes: true });
10553
11339
  for (const entry of entries) {
10554
11340
  if (!entry.isDirectory()) continue;
10555
- const skillFile = path25.join(skillsDir, entry.name, "SKILL.md");
11341
+ const skillFile = path26.join(skillsDir, entry.name, "SKILL.md");
10556
11342
  try {
10557
- const content = await fs15.readFile(skillFile, "utf8");
11343
+ const content = await fs17.readFile(skillFile, "utf8");
10558
11344
  const fm = parseSkillFrontmatter(content);
10559
11345
  if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
10560
- const skillDir = path25.join(skillsDir, entry.name);
11346
+ const skillDir = path26.join(skillsDir, entry.name);
10561
11347
  const files = await collectFiles(skillDir, skillDir);
10562
11348
  results.push({
10563
11349
  name: fm.name,
@@ -10590,8 +11376,8 @@ var SkillInstaller = class {
10590
11376
  */
10591
11377
  async removeSkillFiles(name, scope) {
10592
11378
  const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
10593
- const root = path25.resolve(targetDir);
10594
- const skillDir = path25.resolve(path25.join(targetDir, name));
11379
+ const root = path26.resolve(targetDir);
11380
+ const skillDir = path26.resolve(path26.join(targetDir, name));
10595
11381
  if (skillDir === root || !isInside(skillDir, root)) {
10596
11382
  throw new FsError({
10597
11383
  message: `Refusing to delete skill files outside the skills directory: ${name}`,
@@ -10600,7 +11386,7 @@ var SkillInstaller = class {
10600
11386
  context: { reason: "path_traversal", skillName: name, scope }
10601
11387
  });
10602
11388
  }
10603
- await fs15.rm(skillDir, { recursive: true, force: true });
11389
+ await fs17.rm(skillDir, { recursive: true, force: true });
10604
11390
  }
10605
11391
  /**
10606
11392
  * Invalidate the skill loader's cache so newly installed skills appear.
@@ -10635,7 +11421,7 @@ async function entryIsDirectory(dir, entry) {
10635
11421
  if (entry.isDirectory()) return true;
10636
11422
  if (entry.isSymbolicLink()) {
10637
11423
  try {
10638
- return (await fs15.stat(path25.join(dir, entry.name))).isDirectory();
11424
+ return (await fs17.stat(path26.join(dir, entry.name))).isDirectory();
10639
11425
  } catch {
10640
11426
  return false;
10641
11427
  }
@@ -10644,10 +11430,10 @@ async function entryIsDirectory(dir, entry) {
10644
11430
  }
10645
11431
  async function collectFiles(dir, baseDir) {
10646
11432
  const results = [];
10647
- const entries = await fs15.readdir(dir, { withFileTypes: true });
11433
+ const entries = await fs17.readdir(dir, { withFileTypes: true });
10648
11434
  for (const entry of entries) {
10649
- const fullPath = path25.join(dir, entry.name);
10650
- const relPath = path25.relative(baseDir, fullPath);
11435
+ const fullPath = path26.join(dir, entry.name);
11436
+ const relPath = path26.relative(baseDir, fullPath);
10651
11437
  if (entry.isDirectory()) {
10652
11438
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
10653
11439
  results.push(...await collectFiles(fullPath, baseDir));
@@ -10726,7 +11512,7 @@ function createSkillsPlugin(opts) {
10726
11512
  function makeInstaller(skillLoader, projectRoot, registryAdapters) {
10727
11513
  const paths = resolveWstackPaths({ projectRoot });
10728
11514
  return new SkillInstaller({
10729
- manifestPath: path26.join(paths.configDir, "installed-skills.json"),
11515
+ manifestPath: path27.join(paths.configDir, "installed-skills.json"),
10730
11516
  projectSkillsDir: paths.inProjectSkills,
10731
11517
  globalSkillsDir: paths.globalSkills,
10732
11518
  projectHash: paths.projectHash,
@@ -11105,7 +11891,7 @@ function resolveImportSourceDir(tool, opts) {
11105
11891
  const entry = IMPORT_SOURCE_TOOLS.find((t) => t.id === tool);
11106
11892
  if (!entry) return void 0;
11107
11893
  const base = opts.global ? opts.homeDir ?? os7.homedir() : opts.projectRoot;
11108
- return path26.join(base, "." + entry.id, entry.subdir);
11894
+ return path27.join(base, "." + entry.id, entry.subdir);
11109
11895
  }
11110
11896
  function buildSkillImportCommand(skillLoader) {
11111
11897
  return {
@@ -11146,7 +11932,7 @@ function buildSkillImportCommand(skillLoader) {
11146
11932
  };
11147
11933
  }
11148
11934
  } else if (positional) {
11149
- srcDir = path26.resolve(ctx.projectRoot, positional);
11935
+ srcDir = path27.resolve(ctx.projectRoot, positional);
11150
11936
  } else {
11151
11937
  return {
11152
11938
  message: "Usage: /skill-import <src-dir> | --from <tool> | --from-claude [--global] [--link]"
@@ -11273,14 +12059,14 @@ function truncate(s, max) {
11273
12059
  }
11274
12060
 
11275
12061
  // src/plugins/sync-plugin.ts
11276
- import * as path28 from "node:path";
12062
+ import * as path29 from "node:path";
11277
12063
  init_error();
11278
12064
 
11279
12065
  // src/storage/cloud-sync.ts
11280
12066
  init_atomic_write();
11281
12067
  init_errors();
11282
- import * as fs16 from "node:fs/promises";
11283
- import * as path27 from "node:path";
12068
+ import * as fs18 from "node:fs/promises";
12069
+ import * as path28 from "node:path";
11284
12070
  import { createHash as createHash9 } from "node:crypto";
11285
12071
  var ALL_SYNC_CATEGORIES = ["settings", "skills", "prompts", "memory", "history"];
11286
12072
  var CloudSync = class {
@@ -11288,8 +12074,8 @@ var CloudSync = class {
11288
12074
  this.paths = paths;
11289
12075
  this.getConfig = getConfig;
11290
12076
  this.setConfig = setConfig;
11291
- this.statePath = path27.join(paths.configDir, "sync-state.json");
11292
- this.getSettingsConfigPath = getSettingsConfigPath ?? (() => path27.join(this.paths.configDir, "config.json"));
12077
+ this.statePath = path28.join(paths.configDir, "sync-state.json");
12078
+ this.getSettingsConfigPath = getSettingsConfigPath ?? (() => path28.join(this.paths.configDir, "config.json"));
11293
12079
  }
11294
12080
  paths;
11295
12081
  getConfig;
@@ -11444,7 +12230,7 @@ var CloudSync = class {
11444
12230
  }
11445
12231
  async loadState() {
11446
12232
  try {
11447
- const raw = await fs16.readFile(this.statePath, "utf8");
12233
+ const raw = await fs18.readFile(this.statePath, "utf8");
11448
12234
  this.state = JSON.parse(raw);
11449
12235
  } catch {
11450
12236
  this.state = null;
@@ -11569,18 +12355,18 @@ var CloudSync = class {
11569
12355
  const localPath = this.categoryToPath(cat);
11570
12356
  if (!localPath) continue;
11571
12357
  try {
11572
- const stat7 = await fs16.lstat(localPath);
11573
- if (stat7.isSymbolicLink()) continue;
11574
- if (stat7.isDirectory()) {
12358
+ const stat8 = await fs18.lstat(localPath);
12359
+ if (stat8.isSymbolicLink()) continue;
12360
+ if (stat8.isDirectory()) {
11575
12361
  const files = await this.walkDir(localPath, localPath);
11576
12362
  for (const file of files) {
11577
- const content = await fs16.readFile(file, "utf8");
11578
- const rel = path27.relative(localPath, file).replace(/\\/g, "/");
12363
+ const content = await fs18.readFile(file, "utf8");
12364
+ const rel = path28.relative(localPath, file).replace(/\\/g, "/");
11579
12365
  entries.push({ path: `data/${cat}/${rel}`, content, mode: "100644" });
11580
12366
  hashes.push(`${cat}/${rel}\0${content}`);
11581
12367
  }
11582
12368
  } else {
11583
- const content = await fs16.readFile(localPath, "utf8");
12369
+ const content = await fs18.readFile(localPath, "utf8");
11584
12370
  entries.push({ path: `data/${cat}`, content, mode: "100644" });
11585
12371
  hashes.push(`${cat}\0${content}`);
11586
12372
  }
@@ -11596,17 +12382,17 @@ var CloudSync = class {
11596
12382
  const localPath = this.categoryToPath(cat);
11597
12383
  if (!localPath) continue;
11598
12384
  try {
11599
- const stat7 = await fs16.lstat(localPath);
11600
- if (stat7.isSymbolicLink()) continue;
11601
- if (stat7.isDirectory()) {
12385
+ const stat8 = await fs18.lstat(localPath);
12386
+ if (stat8.isSymbolicLink()) continue;
12387
+ if (stat8.isDirectory()) {
11602
12388
  const files = await this.walkDir(localPath, localPath);
11603
12389
  for (const file of files) {
11604
- const content = await fs16.readFile(file, "utf8");
11605
- const rel = path27.relative(localPath, file).replace(/\\/g, "/");
12390
+ const content = await fs18.readFile(file, "utf8");
12391
+ const rel = path28.relative(localPath, file).replace(/\\/g, "/");
11606
12392
  hashes.push(`${cat}/${rel}\0${content}`);
11607
12393
  }
11608
12394
  } else {
11609
- const content = await fs16.readFile(localPath, "utf8");
12395
+ const content = await fs18.readFile(localPath, "utf8");
11610
12396
  hashes.push(`${cat}\0${content}`);
11611
12397
  }
11612
12398
  } catch {
@@ -11633,10 +12419,10 @@ var CloudSync = class {
11633
12419
  }
11634
12420
  async walkDir(dir, base) {
11635
12421
  const results = [];
11636
- const entries = await fs16.readdir(dir, { withFileTypes: true });
12422
+ const entries = await fs18.readdir(dir, { withFileTypes: true });
11637
12423
  entries.sort((a, b) => a.name.localeCompare(b.name));
11638
12424
  for (const entry of entries) {
11639
- const full = path27.join(dir, entry.name);
12425
+ const full = path28.join(dir, entry.name);
11640
12426
  if (entry.isSymbolicLink()) continue;
11641
12427
  if (entry.isDirectory()) {
11642
12428
  results.push(...await this.walkDir(full, base));
@@ -11649,29 +12435,29 @@ var CloudSync = class {
11649
12435
  };
11650
12436
  async function preparePulledDestination(cat, localPath, destPath, remotePath) {
11651
12437
  const directoryBacked = cat === "skills" || cat === "prompts";
11652
- const rootPath = directoryBacked ? localPath : path27.dirname(localPath);
12438
+ const rootPath = directoryBacked ? localPath : path28.dirname(localPath);
11653
12439
  const rootStat = await lstatIfExists(rootPath);
11654
12440
  if (rootStat?.isSymbolicLink()) {
11655
12441
  throw unsafePulledSymlinkError(remotePath, rootPath);
11656
12442
  }
11657
12443
  if (directoryBacked) {
11658
- const relativeParent = path27.relative(rootPath, path27.dirname(destPath));
12444
+ const relativeParent = path28.relative(rootPath, path28.dirname(destPath));
11659
12445
  let cursor = rootPath;
11660
12446
  if (relativeParent) {
11661
- for (const segment of relativeParent.split(path27.sep)) {
11662
- cursor = path27.join(cursor, segment);
11663
- const stat7 = await lstatIfExists(cursor);
11664
- if (stat7?.isSymbolicLink()) throw unsafePulledSymlinkError(remotePath, cursor);
12447
+ for (const segment of relativeParent.split(path28.sep)) {
12448
+ cursor = path28.join(cursor, segment);
12449
+ const stat8 = await lstatIfExists(cursor);
12450
+ if (stat8?.isSymbolicLink()) throw unsafePulledSymlinkError(remotePath, cursor);
11665
12451
  }
11666
12452
  }
11667
12453
  }
11668
12454
  const destStat = await lstatIfExists(destPath);
11669
12455
  if (destStat?.isSymbolicLink()) throw unsafePulledSymlinkError(remotePath, destPath);
11670
- await fs16.mkdir(path27.dirname(destPath), { recursive: true });
12456
+ await fs18.mkdir(path28.dirname(destPath), { recursive: true });
11671
12457
  }
11672
12458
  async function lstatIfExists(filePath) {
11673
12459
  try {
11674
- return await fs16.lstat(filePath);
12460
+ return await fs18.lstat(filePath);
11675
12461
  } catch (err) {
11676
12462
  if (err.code === "ENOENT") return null;
11677
12463
  throw err;
@@ -11697,9 +12483,9 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
11697
12483
  return localPath;
11698
12484
  }
11699
12485
  if (!rel) return localPath;
11700
- const normalizedRel = path27.normalize(rel);
11701
- const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path27.sep}`);
11702
- if (path27.isAbsolute(normalizedRel) || traversesUp) {
12486
+ const normalizedRel = path28.normalize(rel);
12487
+ const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path28.sep}`);
12488
+ if (path28.isAbsolute(normalizedRel) || traversesUp) {
11703
12489
  throw new FsError({
11704
12490
  message: `Refusing CloudSync path traversal: ${remotePath}`,
11705
12491
  code: ERROR_CODES.FS_DELETE_FAILED,
@@ -11707,10 +12493,10 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
11707
12493
  context: { reason: "path_traversal", normalizedRel }
11708
12494
  });
11709
12495
  }
11710
- const dest = path27.resolve(localPath, normalizedRel);
11711
- const root = path27.resolve(localPath);
11712
- const relative5 = path27.relative(root, dest);
11713
- if (relative5.startsWith("..") || path27.isAbsolute(relative5)) {
12496
+ const dest = path28.resolve(localPath, normalizedRel);
12497
+ const root = path28.resolve(localPath);
12498
+ const relative5 = path28.relative(root, dest);
12499
+ if (relative5.startsWith("..") || path28.isAbsolute(relative5)) {
11714
12500
  throw new FsError({
11715
12501
  message: `Refusing CloudSync path outside category root: ${remotePath}`,
11716
12502
  code: ERROR_CODES.FS_DELETE_FAILED,
@@ -11753,7 +12539,7 @@ function createSyncPlugin(opts) {
11753
12539
  api.log.warn("[sync] paths, configStore, or configDir not available \u2014 /sync disabled");
11754
12540
  return;
11755
12541
  }
11756
- const syncConfigPath = paths.syncConfig ?? path28.join(paths.configDir, "sync.json");
12542
+ const syncConfigPath = paths.syncConfig ?? path29.join(paths.configDir, "sync.json");
11757
12543
  cloud = new CloudSync(
11758
12544
  paths,
11759
12545
  () => {
@@ -11764,7 +12550,7 @@ function createSyncPlugin(opts) {
11764
12550
  await persistSyncConfig(syncConfigPath, cfg, vault);
11765
12551
  configStore?.update({ sync: cfg });
11766
12552
  },
11767
- () => path28.join(paths.configDir, "config.json")
12553
+ () => path29.join(paths.configDir, "config.json")
11768
12554
  );
11769
12555
  void cloud.loadState();
11770
12556
  api.slashCommands.register(buildSyncCommand(cloud, configStore, vault, syncConfigPath));
@@ -11954,6 +12740,852 @@ Warning: sync completed, but metadata persistence failed: ${toErrorMessage(err)}
11954
12740
  }
11955
12741
  };
11956
12742
  }
12743
+
12744
+ // src/plugins/cloud-config-sync-plugin.ts
12745
+ import * as path31 from "node:path";
12746
+
12747
+ // src/security/config-secrets.ts
12748
+ function decryptConfigSecrets(cfg, vault, opts) {
12749
+ const warn = opts?.warn ?? ((msg) => console.warn(msg));
12750
+ return walk2(cfg, vault, (v, key) => {
12751
+ try {
12752
+ return vault.decrypt(v);
12753
+ } catch (err) {
12754
+ warn(
12755
+ `[secret-vault] Failed to decrypt "${key}": ${err instanceof Error ? err.message : err}`
12756
+ );
12757
+ return "";
12758
+ }
12759
+ });
12760
+ }
12761
+ function encryptConfigSecrets(cfg, vault, _opts) {
12762
+ return walk2(cfg, vault, (v) => vault.encrypt(v));
12763
+ }
12764
+ function walk2(node, vault, transform) {
12765
+ if (node === null || node === void 0) return node;
12766
+ if (typeof node !== "object") return node;
12767
+ if (Array.isArray(node)) {
12768
+ return node.map((item) => walk2(item, vault, transform));
12769
+ }
12770
+ const out = /* @__PURE__ */ Object.create(null);
12771
+ for (const [k, v] of Object.entries(node)) {
12772
+ if (typeof v === "string" && isSecretField(k)) {
12773
+ out[k] = transform(v, k);
12774
+ } else if (typeof v === "object" && v !== null) {
12775
+ out[k] = walk2(v, vault, transform);
12776
+ } else {
12777
+ out[k] = v;
12778
+ }
12779
+ }
12780
+ return out;
12781
+ }
12782
+ var SECRET_KEY_PATTERN = /(?:apikey|api_key|authtoken|auth_token|bearer|secret|password|passwd|pwd|refreshtoken|refresh_token|sessionkey|session_key|access[_-]?token|private[_-]?key|token\b)/i;
12783
+ var NON_SECRET_OVERRIDES = /* @__PURE__ */ new Set(["publickey", "public_key"]);
12784
+ function isSecretField(name) {
12785
+ const lc = name.toLowerCase();
12786
+ if (NON_SECRET_OVERRIDES.has(lc)) return false;
12787
+ return SECRET_KEY_PATTERN.test(lc);
12788
+ }
12789
+
12790
+ // src/storage/cloud-config-sync.ts
12791
+ init_atomic_write();
12792
+ import { createHash as createHash10, randomUUID as nodeRandomUUID } from "node:crypto";
12793
+ import * as fs19 from "node:fs/promises";
12794
+ import * as path30 from "node:path";
12795
+
12796
+ // src/storage/cloud-config-sync/sanitize.ts
12797
+ var SAGE_TREE = {
12798
+ enabled: true,
12799
+ storage: { projectLocal: true, directory: true },
12800
+ inject: {
12801
+ turnContext: true,
12802
+ toolResults: true,
12803
+ taskAware: true,
12804
+ maxHintsPerTool: true,
12805
+ maxCharsPerTool: true,
12806
+ minScore: true,
12807
+ repeatCooldownMs: true,
12808
+ maxTurnMemories: true,
12809
+ maxCharsPerTurn: true,
12810
+ minImportance: true,
12811
+ relationFloor: true
12812
+ },
12813
+ hygiene: {
12814
+ autoAfterSession: true,
12815
+ autoOnFileChange: true,
12816
+ retentionDays: true,
12817
+ archiveLowConfidenceAfterDays: true
12818
+ },
12819
+ embeddings: { enabled: true }
12820
+ };
12821
+ var CORE_RUNTIME_TREE = {
12822
+ maxConcurrent: true,
12823
+ context: {
12824
+ mode: true,
12825
+ autoCompact: true,
12826
+ strategy: true,
12827
+ warnThreshold: true,
12828
+ softThreshold: true,
12829
+ hardThreshold: true,
12830
+ preserveK: true,
12831
+ eliseThreshold: true,
12832
+ targetLoad: true
12833
+ },
12834
+ tools: {
12835
+ defaultExecutionStrategy: true,
12836
+ maxIterations: true,
12837
+ iterationTimeoutMs: true,
12838
+ sessionTimeoutMs: true,
12839
+ perIterationOutputCapBytes: true,
12840
+ autoExtendLimit: true,
12841
+ restrictToProjectRoot: true,
12842
+ descriptionMode: true,
12843
+ disabledTools: true,
12844
+ maxToolTimeoutMs: true,
12845
+ loopDetection: {
12846
+ mode: true,
12847
+ steerThreshold: true,
12848
+ cutThreshold: true,
12849
+ windowSize: true,
12850
+ callRepeatThreshold: true
12851
+ }
12852
+ },
12853
+ log: { level: true, format: true },
12854
+ features: {
12855
+ mcp: true,
12856
+ plugins: true,
12857
+ memory: true,
12858
+ modelsRegistry: true,
12859
+ skills: true,
12860
+ prompts: true,
12861
+ tokenSavingMode: true,
12862
+ allowOutsideProjectRoot: true,
12863
+ developerMode: true
12864
+ },
12865
+ session: { auditLevel: true, sampling: { toolProgress: { sampleRate: true } } },
12866
+ indexing: { onSessionStart: true, onEdit: true, watchExternal: true, debounceMs: true },
12867
+ circuitBreaker: { enabled: true, autoKillResetMs: true },
12868
+ modelRuntime: {
12869
+ reasoning: { mode: true, effort: true, preserve: true },
12870
+ cache: { ttl: true }
12871
+ },
12872
+ systemPrompt: { variant: true },
12873
+ skills: { readClaudeSkills: true },
12874
+ Sage: SAGE_TREE,
12875
+ fallbackAuto: true,
12876
+ yolo: true,
12877
+ nextPrediction: true,
12878
+ hints: true,
12879
+ debugStream: true
12880
+ };
12881
+ var UI_PREFERENCES_TREE = {
12882
+ autonomy: {
12883
+ defaultMode: true,
12884
+ autoProceedDelayMs: true,
12885
+ autoProceedMaxIterations: true,
12886
+ autonomyNextPrompt: true,
12887
+ terminalTitleAnimation: true,
12888
+ yolo: true,
12889
+ streamFleet: true,
12890
+ chime: true,
12891
+ confirmExit: true,
12892
+ mouseMode: true,
12893
+ enhance: true,
12894
+ enhanceDelayMs: true,
12895
+ enhanceCountdownMs: true,
12896
+ enhanceLanguage: true,
12897
+ statuslineMode: true,
12898
+ thinkingWord: true,
12899
+ midRunSendPicker: true,
12900
+ animationStyle: true,
12901
+ fleetChatVerbosity: true,
12902
+ refinerFallbackProfile: true,
12903
+ refinerProvider: true,
12904
+ refinerModel: true,
12905
+ showModelReasoning: true,
12906
+ shellBangWarningDontShowAgain: true,
12907
+ showAgentSwarmPanel: true,
12908
+ showSageMemoryInject: true,
12909
+ readAdvancedMode: true,
12910
+ panelPositions: true
12911
+ },
12912
+ launch: { mode: true, autonomy: true },
12913
+ uiLocale: true
12914
+ };
12915
+ var MODELS_ROUTING_TREE = {
12916
+ provider: true,
12917
+ model: true,
12918
+ favoriteModels: true,
12919
+ favoriteModelsOnly: true,
12920
+ fallbackModels: true,
12921
+ fallbackProfiles: true,
12922
+ modelMatrix: { "*": { fallbackProfile: true } },
12923
+ brain: {
12924
+ mode: true,
12925
+ maxAutoRisk: true,
12926
+ models: true,
12927
+ strategy: true,
12928
+ decisionTimeoutMs: true,
12929
+ humanTimeoutMs: true,
12930
+ council: {
12931
+ enabled: true,
12932
+ minRisk: true,
12933
+ voters: true
12934
+ },
12935
+ ledger: { enabled: true, autoDenyAfterFailures: true },
12936
+ trace: { enabled: true }
12937
+ }
12938
+ };
12939
+ var PROVIDERS_CATALOG_TREE = {
12940
+ providers: {
12941
+ "*": {
12942
+ type: true,
12943
+ family: true,
12944
+ baseUrl: true,
12945
+ envVars: true,
12946
+ models: true,
12947
+ activeKey: true
12948
+ }
12949
+ }
12950
+ };
12951
+ var MCP_SERVERS_TREE = {
12952
+ mcpServers: {
12953
+ "*": {
12954
+ name: true,
12955
+ transport: true,
12956
+ command: true,
12957
+ args: true,
12958
+ url: true,
12959
+ description: true,
12960
+ permission: true,
12961
+ enabled: true,
12962
+ lazy: true,
12963
+ envVars: true
12964
+ }
12965
+ }
12966
+ };
12967
+ var EXTENSIONS_PLUGINS_TREE = {
12968
+ plugins: true,
12969
+ extensions: true
12970
+ };
12971
+ var CLOUD_SYNC_CONTRACT = {
12972
+ "core.runtime": CORE_RUNTIME_TREE,
12973
+ "ui.preferences": UI_PREFERENCES_TREE,
12974
+ "models.routing": MODELS_ROUTING_TREE,
12975
+ "providers.catalog": PROVIDERS_CATALOG_TREE,
12976
+ "mcp.servers": MCP_SERVERS_TREE,
12977
+ "extensions.plugins": EXTENSIONS_PLUGINS_TREE
12978
+ };
12979
+ var CLOUD_SYNC_NAMESPACES = Object.keys(CLOUD_SYNC_CONTRACT);
12980
+ var NAMESPACE_SCHEMA_VERSIONS = {
12981
+ "core.runtime": 1,
12982
+ "ui.preferences": 1,
12983
+ "models.routing": 1,
12984
+ "providers.catalog": 1,
12985
+ "mcp.servers": 2,
12986
+ "extensions.plugins": 1
12987
+ };
12988
+ var ENCRYPTED_VALUE = /enc:v\d+:/;
12989
+ var SECRET_KEY = /^(api[-_]?key|apikeys|.*token|secret.*|.*secret|password|passwd|credential.*|private[-_]?key|authorization)$/i;
12990
+ function isPlainObject2(value) {
12991
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12992
+ }
12993
+ function stripSecretMaterial(value) {
12994
+ if (typeof value === "string") {
12995
+ return ENCRYPTED_VALUE.test(value) ? void 0 : value;
12996
+ }
12997
+ if (Array.isArray(value)) {
12998
+ const cleaned = value.map((entry) => stripSecretMaterial(entry));
12999
+ return cleaned.filter((entry) => entry !== void 0);
13000
+ }
13001
+ if (isPlainObject2(value)) {
13002
+ const out = {};
13003
+ for (const [key, child] of Object.entries(value)) {
13004
+ if (SECRET_KEY.test(key)) continue;
13005
+ const cleaned = stripSecretMaterial(child);
13006
+ if (cleaned !== void 0) out[key] = cleaned;
13007
+ }
13008
+ return out;
13009
+ }
13010
+ return value;
13011
+ }
13012
+ function project(value, tree) {
13013
+ if (tree === true) return value;
13014
+ if (!isPlainObject2(value)) return void 0;
13015
+ const wildcard = tree["*"];
13016
+ const out = {};
13017
+ for (const [key, child] of Object.entries(value)) {
13018
+ const node = Object.hasOwn(tree, key) && key !== "*" ? tree[key] : wildcard;
13019
+ if (node === void 0) continue;
13020
+ const projected = project(child, node);
13021
+ if (projected !== void 0) out[key] = projected;
13022
+ }
13023
+ return out;
13024
+ }
13025
+ function buildNamespacePayloads(config) {
13026
+ const payloads = {};
13027
+ for (const [namespace, tree] of Object.entries(CLOUD_SYNC_CONTRACT)) {
13028
+ if (tree === true) continue;
13029
+ const payload = {};
13030
+ for (const [key, node] of Object.entries(tree)) {
13031
+ if (!(key in config) || config[key] === void 0) continue;
13032
+ const projected = project(config[key], node);
13033
+ if (projected === void 0) continue;
13034
+ const cleaned = stripSecretMaterial(projected);
13035
+ if (cleaned === void 0) continue;
13036
+ if (isPlainObject2(cleaned) && Object.keys(cleaned).length === 0 && node !== true) continue;
13037
+ payload[key] = cleaned;
13038
+ }
13039
+ if (Object.keys(payload).length > 0) payloads[namespace] = payload;
13040
+ }
13041
+ return payloads;
13042
+ }
13043
+ function reinjectLocalSecrets(local, incoming) {
13044
+ if (!isPlainObject2(local) || !isPlainObject2(incoming)) return incoming;
13045
+ const out = { ...incoming };
13046
+ for (const [key, value] of Object.entries(local)) {
13047
+ const secretish = SECRET_KEY.test(key) || typeof value === "string" && ENCRYPTED_VALUE.test(value);
13048
+ if (secretish && !(key in incoming)) {
13049
+ out[key] = value;
13050
+ continue;
13051
+ }
13052
+ if (isPlainObject2(value) && isPlainObject2(incoming[key])) {
13053
+ out[key] = reinjectLocalSecrets(value, incoming[key]);
13054
+ }
13055
+ }
13056
+ return out;
13057
+ }
13058
+ function mergeAtContract(local, incoming, tree) {
13059
+ if (tree === true) return reinjectLocalSecrets(local, incoming);
13060
+ if (!isPlainObject2(incoming)) return local;
13061
+ const base = isPlainObject2(local) ? { ...local } : {};
13062
+ const wildcard = tree["*"];
13063
+ for (const [key, child] of Object.entries(incoming)) {
13064
+ const node = Object.hasOwn(tree, key) && key !== "*" ? tree[key] : wildcard;
13065
+ if (node === void 0) continue;
13066
+ base[key] = mergeAtContract(base[key], child, node);
13067
+ }
13068
+ return base;
13069
+ }
13070
+ function applyNamespacePayload(config, namespace, payload) {
13071
+ const tree = CLOUD_SYNC_CONTRACT[namespace];
13072
+ if (!tree || tree === true) return config;
13073
+ const next = { ...config };
13074
+ for (const [key, incoming] of Object.entries(payload)) {
13075
+ const node = Object.hasOwn(tree, key) ? tree[key] : void 0;
13076
+ if (node === void 0) continue;
13077
+ next[key] = mergeAtContract(next[key], incoming, node);
13078
+ }
13079
+ return next;
13080
+ }
13081
+
13082
+ // src/storage/cloud-config-sync.ts
13083
+ var EMPTY_STATE = {
13084
+ version: 1,
13085
+ cursor: 0,
13086
+ namespaceRevisions: {},
13087
+ syncedHashes: {},
13088
+ syncedPayloads: {},
13089
+ pendingMutations: {},
13090
+ rejectedHashes: {}
13091
+ };
13092
+ function stableStringify3(value) {
13093
+ if (Array.isArray(value)) {
13094
+ return `[${value.map((entry) => stableStringify3(entry)).join(",")}]`;
13095
+ }
13096
+ if (typeof value === "object" && value !== null) {
13097
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify3(v)}`);
13098
+ return `{${entries.join(",")}}`;
13099
+ }
13100
+ return JSON.stringify(value) ?? "null";
13101
+ }
13102
+ function hashPayload(payload) {
13103
+ return createHash10("sha256").update(stableStringify3(payload)).digest("hex");
13104
+ }
13105
+ function deepEquals(a, b) {
13106
+ return stableStringify3(a) === stableStringify3(b);
13107
+ }
13108
+ var CloudConfigSync = class {
13109
+ constructor(deps) {
13110
+ this.deps = deps;
13111
+ this.fetchImpl = deps.fetchImpl ?? fetch;
13112
+ this.uuid = deps.randomUUID ?? (() => nodeRandomUUID());
13113
+ this.timeoutMs = deps.requestTimeoutMs ?? 15e3;
13114
+ }
13115
+ deps;
13116
+ state = null;
13117
+ fetchImpl;
13118
+ uuid;
13119
+ timeoutMs;
13120
+ /** One full pull-then-push-then-heartbeat pass. Never throws; failures are summarized. */
13121
+ async syncOnce() {
13122
+ const settings = this.deps.getSettings();
13123
+ if (!settings?.enabled || !settings.url || !settings.token) {
13124
+ return summary({ skipped: true, message: "Cloud sync is not configured." });
13125
+ }
13126
+ const state = await this.loadState();
13127
+ const pulled = [];
13128
+ const pushed = [];
13129
+ const conflicted = [];
13130
+ const rejected = [];
13131
+ try {
13132
+ await this.pullChanges(settings, state, pulled);
13133
+ await this.pushDirty(settings, state, { pushed, conflicted, rejected });
13134
+ await this.heartbeat(settings, state);
13135
+ state.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
13136
+ await this.saveState(state);
13137
+ return summary({
13138
+ ok: true,
13139
+ pulled,
13140
+ pushed,
13141
+ conflicted,
13142
+ rejected,
13143
+ message: pulled.length + pushed.length === 0 ? "Already in sync." : `Pulled ${pulled.length}, pushed ${pushed.length} namespace(s).`
13144
+ });
13145
+ } catch (error) {
13146
+ await this.saveState(state).catch(() => {
13147
+ });
13148
+ const message = error instanceof Error ? error.message : String(error);
13149
+ this.deps.logger?.warn(`cloud-config-sync: pass failed: ${message}`);
13150
+ return summary({ pulled, pushed, conflicted, rejected, message });
13151
+ }
13152
+ }
13153
+ /** Human-readable status for the slash command. */
13154
+ async statusText() {
13155
+ const settings = this.deps.getSettings();
13156
+ if (!settings?.enabled) {
13157
+ return "Cloud config sync: disabled. Run `/cloudsync on` after setting url + token.";
13158
+ }
13159
+ const state = await this.loadState();
13160
+ const dirty = await this.dirtyNamespaces(state);
13161
+ const rejectedList = Object.keys(state.rejectedHashes);
13162
+ const lines = [
13163
+ "Cloud config sync: enabled",
13164
+ ` url: ${settings.url}`,
13165
+ ` cursor: ${state.cursor}`,
13166
+ ` last sync: ${state.lastSyncedAt ?? "never"}`,
13167
+ ` dirty: ${dirty.length > 0 ? dirty.join(", ") : "none"}`
13168
+ ];
13169
+ if (rejectedList.length > 0) {
13170
+ lines.push(` rejected: ${rejectedList.join(", ")} (kept local; edit to retry)`);
13171
+ }
13172
+ return lines.join("\n");
13173
+ }
13174
+ // ── Pull ────────────────────────────────────────────────────────────────
13175
+ async pullChanges(settings, state, pulled) {
13176
+ const delta = (await this.request(settings, "GET", `/api/sync/v1/config?since=${state.cursor}`)).json;
13177
+ const toRead = delta.full ? CLOUD_SYNC_NAMESPACES.map((namespace) => ({ namespace })) : delta.changed.filter(
13178
+ (entry) => CLOUD_SYNC_NAMESPACES.includes(entry.namespace) && entry.revision > (state.namespaceRevisions[entry.namespace] ?? 0)
13179
+ );
13180
+ const dirtyBefore = new Set(await this.dirtyNamespaces(state));
13181
+ for (const entry of toRead) {
13182
+ const snapshot = (await this.request(
13183
+ settings,
13184
+ "GET",
13185
+ `/api/sync/v1/config/${encodeURIComponent(entry.namespace)}`
13186
+ )).json;
13187
+ if ((state.namespaceRevisions[entry.namespace] ?? 0) >= snapshot.revision) continue;
13188
+ if (snapshot.revision === 0) {
13189
+ state.namespaceRevisions[entry.namespace] = 0;
13190
+ continue;
13191
+ }
13192
+ if (dirtyBefore.has(entry.namespace)) {
13193
+ const local = await this.deps.readLocalConfig();
13194
+ const localPayload = buildNamespacePayloads(local)[entry.namespace] ?? {};
13195
+ const base = state.syncedPayloads[entry.namespace] ?? {};
13196
+ const merged = { ...snapshot.payload };
13197
+ for (const [key, localValue] of Object.entries(localPayload)) {
13198
+ if (!deepEquals(localValue, base[key])) merged[key] = localValue;
13199
+ }
13200
+ await this.deps.writeLocalConfig(
13201
+ (config) => applyNamespacePayload(config, entry.namespace, merged)
13202
+ );
13203
+ state.namespaceRevisions[entry.namespace] = snapshot.revision;
13204
+ state.syncedPayloads[entry.namespace] = snapshot.payload;
13205
+ } else {
13206
+ await this.applySnapshot(entry.namespace, snapshot, state);
13207
+ }
13208
+ pulled.push(entry.namespace);
13209
+ }
13210
+ state.cursor = Math.max(state.cursor, delta.cursor);
13211
+ }
13212
+ async applySnapshot(namespace, snapshot, state) {
13213
+ await this.deps.writeLocalConfig(
13214
+ (config) => applyNamespacePayload(config, namespace, snapshot.payload)
13215
+ );
13216
+ state.namespaceRevisions[namespace] = snapshot.revision;
13217
+ state.syncedPayloads[namespace] = snapshot.payload;
13218
+ const local = await this.deps.readLocalConfig();
13219
+ const projected = buildNamespacePayloads(local)[namespace] ?? {};
13220
+ state.syncedHashes[namespace] = hashPayload(projected);
13221
+ delete state.pendingMutations[namespace];
13222
+ delete state.rejectedHashes[namespace];
13223
+ }
13224
+ // ── Push ────────────────────────────────────────────────────────────────
13225
+ async dirtyNamespaces(state) {
13226
+ const local = await this.deps.readLocalConfig();
13227
+ const payloads = buildNamespacePayloads(local);
13228
+ return Object.entries(payloads).filter(([namespace, payload]) => hashPayload(payload) !== state.syncedHashes[namespace]).map(([namespace]) => namespace);
13229
+ }
13230
+ async pushDirty(settings, state, out) {
13231
+ const local = await this.deps.readLocalConfig();
13232
+ const payloads = buildNamespacePayloads(local);
13233
+ for (const [namespace, payload] of Object.entries(payloads)) {
13234
+ const hash = hashPayload(payload);
13235
+ if (hash === state.syncedHashes[namespace]) continue;
13236
+ if (state.rejectedHashes[namespace] === hash) continue;
13237
+ const pending = state.pendingMutations[namespace];
13238
+ const mutationId = pending?.hash === hash ? pending.mutationId : this.uuid();
13239
+ state.pendingMutations[namespace] = { mutationId, hash };
13240
+ await this.saveState(state);
13241
+ const result = await this.putNamespace(settings, state, namespace, payload, mutationId);
13242
+ if (result.kind === "applied") {
13243
+ out.pushed.push(namespace);
13244
+ } else if (result.kind === "conflict") {
13245
+ out.conflicted.push(namespace);
13246
+ await this.resolveConflict(settings, state, namespace, payload, result.server, out);
13247
+ } else {
13248
+ state.rejectedHashes[namespace] = hash;
13249
+ delete state.pendingMutations[namespace];
13250
+ out.rejected.push(namespace);
13251
+ this.deps.logger?.warn(
13252
+ `cloud-config-sync: server rejected ${namespace} (${result.code}); keeping it local until the config changes.`
13253
+ );
13254
+ }
13255
+ }
13256
+ }
13257
+ async putNamespace(settings, state, namespace, payload, mutationId) {
13258
+ const response = await this.request(
13259
+ settings,
13260
+ "PUT",
13261
+ `/api/sync/v1/config/${encodeURIComponent(namespace)}`,
13262
+ {
13263
+ baseRevision: state.namespaceRevisions[namespace] ?? 0,
13264
+ mutationId,
13265
+ schemaVersion: NAMESPACE_SCHEMA_VERSIONS[namespace] ?? 1,
13266
+ payload
13267
+ },
13268
+ // 409 (conflict) and 422 (contract mismatch) are protocol answers, not failures.
13269
+ [409, 422]
13270
+ );
13271
+ if (response.status === 422) {
13272
+ const code = response.json.error?.code;
13273
+ return { kind: "rejected", code: code ?? "invalid_payload" };
13274
+ }
13275
+ const body = response.json;
13276
+ if (response.status === 409) {
13277
+ return { kind: "conflict", server: body };
13278
+ }
13279
+ state.namespaceRevisions[namespace] = body.revision;
13280
+ state.syncedHashes[namespace] = hashPayload(payload);
13281
+ state.syncedPayloads[namespace] = payload;
13282
+ if (typeof body.cursor === "number") state.cursor = Math.max(state.cursor, body.cursor);
13283
+ delete state.pendingMutations[namespace];
13284
+ delete state.rejectedHashes[namespace];
13285
+ return { kind: "applied" };
13286
+ }
13287
+ /**
13288
+ * Three-way merge at owned-top-level-key granularity: keys unchanged locally since the
13289
+ * last sync adopt the server value; locally-changed keys win and are re-pushed on top
13290
+ * of the server revision with a fresh mutation id.
13291
+ */
13292
+ async resolveConflict(settings, state, namespace, localPayload, server, out) {
13293
+ const base = state.syncedPayloads[namespace] ?? {};
13294
+ const merged = { ...server.payload };
13295
+ for (const [key, localValue] of Object.entries(localPayload)) {
13296
+ if (!deepEquals(localValue, base[key])) merged[key] = localValue;
13297
+ }
13298
+ await this.deps.writeLocalConfig((config) => applyNamespacePayload(config, namespace, merged));
13299
+ state.namespaceRevisions[namespace] = server.revision;
13300
+ const mutationId = this.uuid();
13301
+ const hash = hashPayload(merged);
13302
+ state.pendingMutations[namespace] = { mutationId, hash };
13303
+ await this.saveState(state);
13304
+ const result = await this.putNamespace(settings, state, namespace, merged, mutationId);
13305
+ if (result.kind === "applied") {
13306
+ out.pushed.push(namespace);
13307
+ } else {
13308
+ this.deps.logger?.warn(
13309
+ `cloud-config-sync: conflict resolution for ${namespace} did not converge (${result.kind}); will retry next pass.`
13310
+ );
13311
+ }
13312
+ }
13313
+ // ── Heartbeat ───────────────────────────────────────────────────────────
13314
+ async heartbeat(settings, state) {
13315
+ await this.request(settings, "POST", "/api/sync/v1/heartbeat", {
13316
+ transport: "poll",
13317
+ syncCursor: state.cursor,
13318
+ namespaceRevisions: state.namespaceRevisions
13319
+ }).catch((error) => {
13320
+ const message = error instanceof Error ? error.message : String(error);
13321
+ this.deps.logger?.warn(`cloud-config-sync: heartbeat failed: ${message}`);
13322
+ });
13323
+ }
13324
+ // ── HTTP + state plumbing ───────────────────────────────────────────────
13325
+ async request(settings, method, pathname, body, acceptStatuses = []) {
13326
+ const url = `${settings.url.replace(/\/+$/, "")}${pathname}`;
13327
+ const headers = {
13328
+ Authorization: `Bearer ${settings.token}`,
13329
+ "X-WrongStack-Version": this.deps.appVersion ?? "unknown"
13330
+ };
13331
+ if (body !== void 0) headers["Content-Type"] = "application/json";
13332
+ const init = {
13333
+ method,
13334
+ headers,
13335
+ signal: AbortSignal.timeout(this.timeoutMs)
13336
+ };
13337
+ if (body !== void 0) init.body = JSON.stringify(body);
13338
+ const response = await this.fetchImpl(url, init);
13339
+ const ok = response.ok || acceptStatuses.includes(response.status);
13340
+ if (!ok) {
13341
+ if (response.status === 401) {
13342
+ throw new Error("unauthorized: the machine token is invalid, expired, or revoked.");
13343
+ }
13344
+ if (response.status === 429) {
13345
+ throw new Error("rate limited by the portal; backing off until the next pass.");
13346
+ }
13347
+ throw new Error(`${method} ${pathname} failed with HTTP ${response.status}.`);
13348
+ }
13349
+ const json = response.status === 204 ? null : await response.json().catch(() => null);
13350
+ return { status: response.status, json };
13351
+ }
13352
+ async loadState() {
13353
+ if (this.state) return this.state;
13354
+ try {
13355
+ const raw = await fs19.readFile(this.deps.statePath, "utf8");
13356
+ const parsed = JSON.parse(raw);
13357
+ this.state = {
13358
+ ...structuredClone(EMPTY_STATE),
13359
+ ...parsed,
13360
+ version: 1
13361
+ };
13362
+ } catch {
13363
+ this.state = structuredClone(EMPTY_STATE);
13364
+ }
13365
+ return this.state;
13366
+ }
13367
+ async saveState(state) {
13368
+ this.state = state;
13369
+ await fs19.mkdir(path30.dirname(this.deps.statePath), { recursive: true });
13370
+ await atomicWrite(this.deps.statePath, `${JSON.stringify(state, null, 2)}
13371
+ `, {
13372
+ mode: 384
13373
+ });
13374
+ }
13375
+ };
13376
+ function summary(partial) {
13377
+ return {
13378
+ ok: partial.ok ?? false,
13379
+ skipped: partial.skipped ?? false,
13380
+ pulled: partial.pulled ?? [],
13381
+ pushed: partial.pushed ?? [],
13382
+ conflicted: partial.conflicted ?? [],
13383
+ rejected: partial.rejected ?? [],
13384
+ message: partial.message
13385
+ };
13386
+ }
13387
+
13388
+ // src/plugins/cloud-config-sync-plugin.ts
13389
+ init_atomic_write();
13390
+ init_error();
13391
+ var DEFAULT_INTERVAL_SECONDS = 300;
13392
+ var MIN_INTERVAL_SECONDS = 60;
13393
+ function createCloudConfigSyncPlugin(opts) {
13394
+ let engine = null;
13395
+ let timer = null;
13396
+ let passRunning = false;
13397
+ return {
13398
+ name: "wstack-cloud-config-sync",
13399
+ version: "1.0.0",
13400
+ description: "my.wrongstack.com config synchronization (namespaced sync API v1)",
13401
+ apiVersion: "^0.1",
13402
+ capabilities: { slashCommands: true },
13403
+ defaultConfig: {},
13404
+ setup(api) {
13405
+ const rawConfig = api.config;
13406
+ const paths = opts?.paths ?? rawConfig.paths;
13407
+ const configStore = opts?.configStore ?? rawConfig.configStore;
13408
+ const vault = opts?.vault ?? rawConfig.vault;
13409
+ const appVersion = opts?.appVersion ?? rawConfig.appVersion;
13410
+ if (!paths?.configDir || !configStore || !vault) {
13411
+ api.log.warn(
13412
+ "[cloud-config-sync] paths, configStore, or vault not available \u2014 /cloudsync disabled"
13413
+ );
13414
+ return;
13415
+ }
13416
+ const profileConfigPath = path31.join(paths.configDir, "config.json");
13417
+ const statePath = path31.join(paths.configDir, "cloud-sync-state.json");
13418
+ const warn = (msg) => api.log.warn(msg);
13419
+ const readLocalConfig = async () => {
13420
+ const raw = await readJsonObjectFile(profileConfigPath).catch(() => ({}));
13421
+ return decryptConfigSecrets(raw, vault, { warn });
13422
+ };
13423
+ const writeLocalConfig = async (mutator) => {
13424
+ const current = await readLocalConfig();
13425
+ const next = mutator(current);
13426
+ const encrypted = encryptConfigSecrets(next, vault);
13427
+ await atomicWrite(profileConfigPath, `${JSON.stringify(encrypted, null, 2)}
13428
+ `, {
13429
+ mode: 384
13430
+ });
13431
+ configStore.update({ cloudSync: next.cloudSync });
13432
+ };
13433
+ const getSettings = () => {
13434
+ const cfg = configStore.get().cloudSync;
13435
+ if (!cfg?.enabled || !cfg.url || !cfg.token) return null;
13436
+ const token = cfg.token.startsWith("enc:") && vault.decrypt ? vault.decrypt(cfg.token) : cfg.token;
13437
+ return { enabled: true, url: cfg.url, token };
13438
+ };
13439
+ engine = new CloudConfigSync({
13440
+ statePath,
13441
+ readLocalConfig,
13442
+ writeLocalConfig,
13443
+ getSettings,
13444
+ appVersion,
13445
+ logger: { info: (m) => api.log.info(m), warn }
13446
+ });
13447
+ const runPass = async () => {
13448
+ if (!engine) return "Cloud config sync is not initialized.";
13449
+ if (passRunning) return "A sync pass is already running.";
13450
+ passRunning = true;
13451
+ try {
13452
+ const result = await engine.syncOnce();
13453
+ return result.message;
13454
+ } finally {
13455
+ passRunning = false;
13456
+ }
13457
+ };
13458
+ const scheduleLoop = () => {
13459
+ if (timer) {
13460
+ clearInterval(timer);
13461
+ timer = null;
13462
+ }
13463
+ const cfg = configStore.get().cloudSync;
13464
+ if (!cfg?.enabled) return;
13465
+ const seconds = Math.max(
13466
+ MIN_INTERVAL_SECONDS,
13467
+ cfg.intervalSeconds ?? DEFAULT_INTERVAL_SECONDS
13468
+ );
13469
+ timer = setInterval(() => {
13470
+ void runPass().catch((err) => warn(`[cloud-config-sync] ${toErrorMessage(err)}`));
13471
+ }, seconds * 1e3);
13472
+ timer.unref?.();
13473
+ };
13474
+ const setCloudSyncConfig = async (patch) => {
13475
+ await writeLocalConfig((config) => ({
13476
+ ...config,
13477
+ cloudSync: { ...config.cloudSync, ...patch }
13478
+ }));
13479
+ scheduleLoop();
13480
+ };
13481
+ api.slashCommands.register(
13482
+ buildCloudSyncCommand(engine, configStore, setCloudSyncConfig, runPass)
13483
+ );
13484
+ scheduleLoop();
13485
+ api.log.info("[cloud-config-sync] loaded \u2014 /cloudsync available.");
13486
+ },
13487
+ teardown(api) {
13488
+ if (timer) {
13489
+ clearInterval(timer);
13490
+ timer = null;
13491
+ }
13492
+ api.slashCommands.unregister("cloudsync");
13493
+ api.log.info("[cloud-config-sync] unloaded");
13494
+ },
13495
+ async health() {
13496
+ return { ok: true, message: "CloudConfigSync ready" };
13497
+ }
13498
+ };
13499
+ }
13500
+ function buildCloudSyncCommand(engine, configStore, setCloudSyncConfig, runPass) {
13501
+ return {
13502
+ name: "cloudsync",
13503
+ description: "Portal config sync: /cloudsync [status|on|off|now|set url|set token]",
13504
+ async run(args, _ctx) {
13505
+ const [verb, ...rest] = args.trim().split(/\s+/);
13506
+ const restJoined = rest.join(" ").trim();
13507
+ switch (verb) {
13508
+ case "":
13509
+ case "status":
13510
+ return { message: await engine.statusText() };
13511
+ case "on": {
13512
+ const cfg = configStore.get().cloudSync;
13513
+ if (!cfg?.url || !cfg.token) {
13514
+ return {
13515
+ message: [
13516
+ "Set the portal first:",
13517
+ " /cloudsync set url https://my.wrongstack.com",
13518
+ ' /cloudsync set token wst_\u2026 (from the dashboard "Connect client" flow)'
13519
+ ].join("\n")
13520
+ };
13521
+ }
13522
+ await setCloudSyncConfig({ enabled: true });
13523
+ const first = await runPass();
13524
+ return { message: `Cloud config sync enabled.
13525
+ ${first}` };
13526
+ }
13527
+ case "off": {
13528
+ await setCloudSyncConfig({ enabled: false });
13529
+ return { message: "Cloud config sync disabled. Local config kept as-is." };
13530
+ }
13531
+ case "now": {
13532
+ try {
13533
+ return { message: await runPass() };
13534
+ } catch (err) {
13535
+ return { message: `Sync failed: ${toErrorMessage(err)}` };
13536
+ }
13537
+ }
13538
+ case "set": {
13539
+ const [field, ...valueParts] = restJoined.split(/\s+/);
13540
+ const value = valueParts.join(" ").trim();
13541
+ if (field === "url") {
13542
+ if (!/^https?:\/\//.test(value)) {
13543
+ return { message: "Usage: /cloudsync set url https://my.wrongstack.com" };
13544
+ }
13545
+ await setCloudSyncConfig({ url: value.replace(/\/+$/, "") });
13546
+ return { message: `Portal URL set to ${value}.` };
13547
+ }
13548
+ if (field === "token") {
13549
+ if (!/^wst_[A-Za-z0-9_-]{43}$/.test(value)) {
13550
+ return {
13551
+ message: 'That does not look like a machine token (wst_ + 43 chars). Create one in the dashboard "Connect client" flow.'
13552
+ };
13553
+ }
13554
+ await setCloudSyncConfig({ token: value });
13555
+ return { message: "Machine token stored (encrypted at rest)." };
13556
+ }
13557
+ if (field === "interval") {
13558
+ const seconds = Number.parseInt(value, 10);
13559
+ if (!Number.isFinite(seconds) || seconds < MIN_INTERVAL_SECONDS) {
13560
+ return {
13561
+ message: `Usage: /cloudsync set interval <seconds \u2265 ${MIN_INTERVAL_SECONDS}>`
13562
+ };
13563
+ }
13564
+ await setCloudSyncConfig({ intervalSeconds: seconds });
13565
+ return { message: `Sync interval set to ${seconds}s.` };
13566
+ }
13567
+ return { message: "Usage: /cloudsync set url|token|interval <value>" };
13568
+ }
13569
+ default:
13570
+ return {
13571
+ message: [
13572
+ "/cloudsync \u2014 my.wrongstack.com config sync",
13573
+ "",
13574
+ " /cloudsync status Show sync state",
13575
+ " /cloudsync set url <url> Set the portal URL",
13576
+ " /cloudsync set token <wst_\u2026> Store the machine token (encrypted)",
13577
+ " /cloudsync set interval <sec> Background pass interval",
13578
+ " /cloudsync on Enable + run the first pass",
13579
+ " /cloudsync off Disable background sync",
13580
+ " /cloudsync now Run one pass immediately",
13581
+ "",
13582
+ "Secrets never sync: providers keys, hq binding, and bot tokens stay local."
13583
+ ].join("\n")
13584
+ };
13585
+ }
13586
+ }
13587
+ };
13588
+ }
11957
13589
  export {
11958
13590
  CHIMERA_REVIEW_PROMPT,
11959
13591
  DefaultPluginAPI,
@@ -11961,6 +13593,7 @@ export {
11961
13593
  buildReviewerModelPool,
11962
13594
  createAutoReviewPlugin,
11963
13595
  createChimeraPlugin,
13596
+ createCloudConfigSyncPlugin,
11964
13597
  createPromptsPlugin,
11965
13598
  createSkillsPlugin,
11966
13599
  createSyncPlugin,
@@ -11973,10 +13606,12 @@ export {
11973
13606
  parseChimeraReviewReport,
11974
13607
  parseReviewSeverity,
11975
13608
  persistReviewReport,
13609
+ pluginEntryMatchesName,
11976
13610
  recordCompletedReview,
11977
13611
  recordStartedReview,
11978
13612
  redactPluginConfig,
11979
13613
  resolvePluginConfig,
13614
+ resolvePluginEnablement,
11980
13615
  resolvePluginManifestConfig,
11981
13616
  selectRoundRobinReviewerAssignment,
11982
13617
  unloadPlugins,