@wrongstack/core 0.300.0 → 0.302.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 (146) 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 +2 -2
  13. package/dist/coordination/index.js +6787 -6447
  14. package/dist/coordination/mail-tools.d.ts +3 -3
  15. package/dist/coordination/mailbox-project-server.js +3 -4
  16. package/dist/coordination/mailbox-types.d.ts +6 -0
  17. package/dist/core/agent-response.d.ts +5 -1
  18. package/dist/core/agent-tools.d.ts +3 -0
  19. package/dist/core/context.d.ts +20 -0
  20. package/dist/core/index.d.ts +8 -7
  21. package/dist/core/index.js +893 -284
  22. package/dist/core/instruction-template.d.ts +6 -3
  23. package/dist/core/next-steps-slot.d.ts +88 -0
  24. package/dist/core/system-prompt-builder.d.ts +5 -3
  25. package/dist/core/system-prompt-memory-skills.d.ts +1 -0
  26. package/dist/core/system-prompt-skill-bodies.d.ts +3 -3
  27. package/dist/defaults/index.js +9797 -8898
  28. package/dist/design/index.js +11 -3
  29. package/dist/execution/auto-compaction-middleware.d.ts +17 -0
  30. package/dist/execution/autonomy-brain.d.ts +8 -2
  31. package/dist/execution/brain-runtime.d.ts +3 -1
  32. package/dist/execution/compaction-core.d.ts +41 -2
  33. package/dist/execution/council-brain.d.ts +37 -2
  34. package/dist/execution/council-orchestrator.d.ts +2 -2
  35. package/dist/execution/index.d.ts +11 -10
  36. package/dist/execution/index.js +5179 -4078
  37. package/dist/execution/tool-executor.d.ts +4 -1
  38. package/dist/execution/topic-shift-advisor.d.ts +53 -0
  39. package/dist/extension/index.js +8 -2
  40. package/dist/extension/registry.d.ts +8 -1
  41. package/dist/goal/index.js +11 -3
  42. package/dist/hooks/index.js +42 -9
  43. package/dist/hooks/runner.d.ts +12 -1
  44. package/dist/hq/index.js +41 -17
  45. package/dist/hq/publisher.d.ts +21 -3
  46. package/dist/index.d.ts +21 -20
  47. package/dist/index.js +10901 -8067
  48. package/dist/infrastructure/index.js +108 -21
  49. package/dist/kernel/events/session-events.d.ts +13 -0
  50. package/dist/kernel/events/tool-events.d.ts +3 -3
  51. package/dist/models/index.d.ts +1 -0
  52. package/dist/models/index.js +17 -0
  53. package/dist/models/provider-credentials.d.ts +54 -0
  54. package/dist/plugin/index.d.ts +1 -0
  55. package/dist/plugin/index.js +2024 -502
  56. package/dist/plugins/auto-review-plugin.d.ts +3 -0
  57. package/dist/plugins/cloud-config-sync-plugin.d.ts +22 -0
  58. package/dist/plugins/review-claim-registry.d.ts +23 -8
  59. package/dist/plugins/review-types.d.ts +2 -0
  60. package/dist/registry/index.js +109 -74
  61. package/dist/registry/tool-registry.d.ts +15 -0
  62. package/dist/security/capabilities.d.ts +2 -0
  63. package/dist/security/index.d.ts +1 -1
  64. package/dist/security/index.js +9 -2
  65. package/dist/security/readonly-permission-policy.d.ts +20 -0
  66. package/dist/session-registry-atomic-file.d.ts +32 -5
  67. package/dist/session-registry-types.d.ts +17 -0
  68. package/dist/session-registry.d.ts +1 -1
  69. package/dist/skills/frontmatter.d.ts +6 -0
  70. package/dist/skills/index.js +22 -5
  71. package/dist/storage/cloud-config-sync/sanitize.d.ts +54 -0
  72. package/dist/storage/cloud-config-sync.d.ts +87 -0
  73. package/dist/storage/index.d.ts +2 -1
  74. package/dist/storage/index.js +854 -66
  75. package/dist/tools/index.d.ts +1 -1
  76. package/dist/tools/index.js +511 -234
  77. package/dist/tools/one-shot-llm-tool.d.ts +1 -0
  78. package/dist/tools/plugin-manager.d.ts +27 -0
  79. package/dist/types/config/mcp-features.d.ts +18 -11
  80. package/dist/types/config/root.d.ts +8 -1
  81. package/dist/types/config/runtime.d.ts +20 -6
  82. package/dist/types/config/skills-fleet-brain.d.ts +12 -4
  83. package/dist/types/config/tools.d.ts +16 -0
  84. package/dist/types/context-window.d.ts +1 -0
  85. package/dist/types/hooks.d.ts +14 -0
  86. package/dist/types/index.d.ts +2 -2
  87. package/dist/types/index.js +1 -0
  88. package/dist/types/multi-agent.d.ts +2 -0
  89. package/dist/types/one-shot-llm.d.ts +16 -0
  90. package/dist/types/provider.d.ts +16 -0
  91. package/dist/types/runtime-capability-manifest.d.ts +168 -0
  92. package/dist/types/skill.d.ts +5 -0
  93. package/dist/types/system-prompt.d.ts +3 -1
  94. package/dist/types/tool-executor.d.ts +8 -1
  95. package/dist/utils/context-breakdown.d.ts +8 -2
  96. package/dist/utils/index.d.ts +1 -1
  97. package/dist/utils/index.js +141 -31
  98. package/dist/utils/regex-guard.d.ts +16 -2
  99. package/dist/utils/sage-output-block.d.ts +7 -10
  100. package/dist/utils/wstack-paths.d.ts +11 -3
  101. package/instructions/agents/browser.md +1 -4
  102. package/instructions/agents/e2e.md +17 -15
  103. package/instructions/agents/ios.md +3 -3
  104. package/instructions/agents/search.md +1 -1
  105. package/instructions/autonomy/goal-preamble.md +4 -4
  106. package/instructions/coordination/director-preamble.md +2 -3
  107. package/instructions/coordination/subagent-baseline.md +3 -1
  108. package/instructions/leader-after-task.md +12 -0
  109. package/instructions/llm/chimera-review.md +1 -1
  110. package/instructions/modes/audit-lite.md +1 -1
  111. package/instructions/sections/tool/common-patterns.md +18 -2
  112. package/instructions/sections/tool/mailbox-compact.md +1 -1
  113. package/instructions/sections/tool/mailbox-full.md +1 -1
  114. package/instructions/system-lite.md +10 -0
  115. package/instructions/system-pro.md +23 -14
  116. package/instructions/system.md +22 -13
  117. package/package.json +3 -3
  118. package/skills/api-design/SKILL.md +3 -0
  119. package/skills/audit-log/SKILL.md +2 -0
  120. package/skills/auto-review/SKILL.md +6 -1
  121. package/skills/bug-hunter/SKILL.md +4 -1
  122. package/skills/chimera/SKILL.md +3 -0
  123. package/skills/data-governance/SKILL.md +3 -0
  124. package/skills/design-system/SKILL.md +5 -2
  125. package/skills/docker-deploy/SKILL.md +2 -0
  126. package/skills/git-flow/SKILL.md +2 -0
  127. package/skills/mailbox-bridge/SKILL.md +3 -0
  128. package/skills/mnemosyne/SKILL.md +2 -0
  129. package/skills/multi-agent/SKILL.md +4 -1
  130. package/skills/node-modern/SKILL.md +4 -1
  131. package/skills/observability/SKILL.md +3 -0
  132. package/skills/output-standards/SKILL.md +2 -0
  133. package/skills/plugin-author/SKILL.md +4 -1
  134. package/skills/prompt-engineering/SKILL.md +3 -1
  135. package/skills/react-modern/SKILL.md +6 -3
  136. package/skills/refactor-planner/SKILL.md +2 -0
  137. package/skills/research-web/SKILL.md +3 -0
  138. package/skills/sdd/SKILL.md +3 -1
  139. package/skills/security-scanner/SKILL.md +3 -0
  140. package/skills/skill-creator/SKILL.md +2 -0
  141. package/skills/tech-stack/SKILL.md +3 -0
  142. package/skills/testing/SKILL.md +4 -1
  143. package/skills/typescript-strict/SKILL.md +4 -1
  144. package/skills/wrongstack-kanban/SKILL.md +6 -3
  145. package/skills/wrongstack-mailbox/SKILL.md +4 -1
  146. 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
  }
@@ -2209,8 +2215,8 @@ function definePlugin(metadata, factory) {
2209
2215
  // src/plugins/auto-review-plugin.ts
2210
2216
  import { spawn as spawn3 } from "node:child_process";
2211
2217
  import { createHash as createHash7 } from "node:crypto";
2212
- import * as fsp from "node:fs/promises";
2213
- import * as path14 from "node:path";
2218
+ import * as fsp2 from "node:fs/promises";
2219
+ import * as path15 from "node:path";
2214
2220
 
2215
2221
  // src/core/model-availability-calendar.ts
2216
2222
  function logicalCalendarTarget(providerId, model) {
@@ -2572,14 +2578,327 @@ var FREEZER_EMPTY = Object.freeze([]);
2572
2578
  init_error();
2573
2579
 
2574
2580
  // src/plugins/review-claim-registry.ts
2575
- 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();
2576
2721
  var claimsByEventBus = /* @__PURE__ */ new WeakMap();
2577
2722
  var startedReviews = /* @__PURE__ */ new WeakMap();
2723
+ var pendingStartedReviews = /* @__PURE__ */ new WeakMap();
2578
2724
  function fingerprint(content) {
2579
2725
  return createHash("sha256").update(content).digest("hex");
2580
2726
  }
2727
+ function normalizeKeyPart(p) {
2728
+ const forward = p.replace(/\\/g, "/").replace(/\/+$/, "");
2729
+ return process.platform === "win32" ? forward.toLowerCase() : forward;
2730
+ }
2581
2731
  function claimKey(cwd, filePath) {
2582
- 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
+ );
2583
2902
  }
2584
2903
  function ledgerFor(events) {
2585
2904
  let ledger = claimsByEventBus.get(events);
@@ -2589,68 +2908,162 @@ function ledgerFor(events) {
2589
2908
  }
2590
2909
  return ledger;
2591
2910
  }
2592
- function claimFiles(events, cwd, files) {
2911
+ function claimFilesInMemory(events, cwd, files, ttlMs) {
2593
2912
  const ledger = ledgerFor(events);
2913
+ const now = Date.now();
2594
2914
  const claims = [];
2595
2915
  for (const file of files) {
2596
2916
  const key = claimKey(cwd, file.path);
2597
2917
  const fileFingerprint = fingerprint(file.content);
2598
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
+ }
2599
2928
  if (!activeFingerprints) {
2600
- activeFingerprints = /* @__PURE__ */ new Set();
2929
+ activeFingerprints = /* @__PURE__ */ new Map();
2601
2930
  ledger.set(key, activeFingerprints);
2602
2931
  }
2603
2932
  if (activeFingerprints.has(fileFingerprint)) continue;
2604
- activeFingerprints.add(fileFingerprint);
2605
- 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 });
2606
2935
  }
2607
2936
  return claims;
2608
2937
  }
2609
- function releaseClaims(events, claims) {
2938
+ function releaseClaimsInMemory(events, claims) {
2610
2939
  const ledger = ledgerFor(events);
2611
2940
  for (const claim of claims) {
2612
2941
  const activeFingerprints = ledger.get(claim.key);
2613
2942
  if (!activeFingerprints) continue;
2614
- activeFingerprints.delete(claim.fingerprint);
2943
+ if (activeFingerprints.get(claim.fingerprint)?.ts === claim.ts) {
2944
+ activeFingerprints.delete(claim.fingerprint);
2945
+ }
2615
2946
  if (activeFingerprints.size === 0) ledger.delete(claim.key);
2616
2947
  }
2617
2948
  }
2618
- 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) {
2619
3001
  if (!payload || typeof payload !== "object") return;
3002
+ const bundle = payload;
3003
+ if (startedReviews.has(bundle) || pendingStartedReviews.has(bundle)) return;
2620
3004
  const candidate = payload;
2621
3005
  if (typeof candidate.cwd !== "string" || !Array.isArray(candidate.files)) return;
3006
+ const cwd = candidate.cwd;
2622
3007
  const files = candidate.files.filter(
2623
3008
  (file) => Boolean(file) && typeof file === "object" && typeof file.path === "string" && typeof file.content === "string"
2624
3009
  );
2625
- const claims = claimFiles(events, candidate.cwd, files);
2626
- if (claims.length > 0) {
2627
- 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
+ }
2628
3028
  }
2629
3029
  }
2630
- function recordCompletedReview(events, payload) {
3030
+ async function recordCompletedReview(events, payload) {
2631
3031
  if (!payload || typeof payload !== "object") return;
2632
3032
  const bundle = payload.bundle;
2633
3033
  if (!bundle || typeof bundle !== "object") return;
2634
- 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
+ }
2635
3039
  if (!startedReview || startedReview.events !== events) return;
2636
- releaseClaims(events, startedReview.claims);
3040
+ await releaseClaims(events, startedReview.claims, startedReview.storeDir);
2637
3041
  startedReviews.delete(bundle);
2638
3042
  }
2639
- function emitReviewIfChanged(api, bundle) {
2640
- 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);
2641
3045
  if (claims.length === 0) return null;
2642
- const claimedKeys = new Set(claims.map((claim) => claim.key));
3046
+ const claimed = new Set(claims.map((claim) => `${claim.key}\0${claim.fingerprint}`));
2643
3047
  const claimedBundle = {
2644
3048
  ...bundle,
2645
- 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
+ )
2646
3055
  };
2647
- startedReviews.set(claimedBundle, { events: api.events, claims });
3056
+ startedReviews.set(claimedBundle, {
3057
+ events: api.events,
3058
+ storeDir,
3059
+ claims
3060
+ });
2648
3061
  try {
2649
3062
  api.emitCustom("chimera.review_needed", claimedBundle);
2650
3063
  return claimedBundle;
2651
3064
  } catch (err) {
2652
3065
  startedReviews.delete(claimedBundle);
2653
- releaseClaims(api.events, claims);
3066
+ await releaseClaims(api.events, claims, storeDir);
2654
3067
  throw err;
2655
3068
  }
2656
3069
  }
@@ -2661,27 +3074,30 @@ import { spawn as spawn2 } from "node:child_process";
2661
3074
  // src/chronicle/project-access.ts
2662
3075
  import * as fsPromises from "node:fs/promises";
2663
3076
  import * as os4 from "node:os";
2664
- import * as path13 from "node:path";
3077
+ import * as path14 from "node:path";
2665
3078
 
2666
3079
  // src/utils/wstack-paths.ts
2667
3080
  import { createHash as createHash2 } from "node:crypto";
2668
3081
  import * as fs from "node:fs";
2669
3082
  import * as os from "node:os";
2670
- import * as path from "node:path";
3083
+ import * as path2 from "node:path";
2671
3084
  function canonicalProjectRoot(absRoot) {
2672
- const checkoutRoot = path.resolve(absRoot);
2673
- const dotGit = path.join(checkoutRoot, ".git");
3085
+ const checkoutRoot = path2.resolve(absRoot);
3086
+ const dotGit = path2.join(checkoutRoot, ".git");
2674
3087
  try {
2675
3088
  if (!fs.statSync(dotGit).isFile()) return checkoutRoot;
2676
3089
  const gitDirLine = fs.readFileSync(dotGit, "utf8").trim();
2677
3090
  const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
2678
3091
  if (!match?.[1]) return checkoutRoot;
2679
- const gitDir = path.resolve(checkoutRoot, match[1].trim());
2680
- const commonDirFile = path.join(gitDir, "commondir");
3092
+ const gitDir = path2.resolve(checkoutRoot, match[1].trim());
3093
+ const commonDirFile = path2.join(gitDir, "commondir");
2681
3094
  if (!fs.statSync(commonDirFile).isFile()) return checkoutRoot;
2682
- const commonDir = path.resolve(gitDir, fs.readFileSync(commonDirFile, "utf8").trim());
2683
- if (path.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
2684
- 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);
2685
3101
  } catch {
2686
3102
  return checkoutRoot;
2687
3103
  }
@@ -2691,7 +3107,7 @@ function projectHash(absRoot) {
2691
3107
  }
2692
3108
  function projectSlug(absRoot) {
2693
3109
  const identityRoot = canonicalProjectRoot(absRoot);
2694
- const base = slugify(path.basename(identityRoot));
3110
+ const base = slugify(path2.basename(identityRoot));
2695
3111
  const hash = createHash2("sha256").update(identityRoot).digest("hex").slice(0, 6);
2696
3112
  return `${base}-${hash}`;
2697
3113
  }
@@ -2702,9 +3118,9 @@ function safeProfileName(name) {
2702
3118
  const safe = (name ?? "").replace(/[/\\:]/g, "_").replace(/\.\./g, "_").trim();
2703
3119
  return safe || "default";
2704
3120
  }
2705
- function activeProfileName(globalRoot) {
3121
+ function bootstrapProfileName(globalRoot) {
2706
3122
  try {
2707
- 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"));
2708
3124
  return safeProfileName(
2709
3125
  typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0
2710
3126
  );
@@ -2714,17 +3130,17 @@ function activeProfileName(globalRoot) {
2714
3130
  }
2715
3131
  function wstackGlobalRoot() {
2716
3132
  const fromEnv = process.env["WRONGSTACK_HOME"];
2717
- if (fromEnv && fromEnv.trim().length > 0) return path.resolve(fromEnv);
2718
- return path.join(os.homedir(), ".wrongstack");
3133
+ if (fromEnv && fromEnv.trim().length > 0) return path2.resolve(fromEnv);
3134
+ return path2.join(os.homedir(), ".wrongstack");
2719
3135
  }
2720
3136
  function resolveWstackPaths(opts) {
2721
- 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());
2722
3138
  const homeDir = opts.userHome ?? os.homedir();
2723
- const profileName = safeProfileName(opts.profileName ?? activeProfileName(globalRoot));
2724
- const profileDir = path.join(globalRoot, "profiles", profileName);
3139
+ const profileName = safeProfileName(opts.profileName ?? bootstrapProfileName(globalRoot));
3140
+ const profileDir = path2.join(globalRoot, "profiles", profileName);
2725
3141
  const hash = projectHash(opts.projectRoot);
2726
3142
  const slug = projectSlug(opts.projectRoot);
2727
- const projectDir = path.join(globalRoot, "projects", slug);
3143
+ const projectDir = path2.join(globalRoot, "projects", slug);
2728
3144
  return {
2729
3145
  globalRoot,
2730
3146
  profileName,
@@ -2732,79 +3148,84 @@ function resolveWstackPaths(opts) {
2732
3148
  projectRoot: opts.projectRoot,
2733
3149
  homeDir,
2734
3150
  configDir: profileDir,
2735
- globalConfig: path.join(globalRoot, "config.json"),
2736
- profilesDir: path.join(globalRoot, "profiles"),
3151
+ globalConfig: path2.join(globalRoot, "config.json"),
3152
+ profilesDir: path2.join(globalRoot, "profiles"),
2737
3153
  profileConfig: (name) => {
2738
- return path.join(globalRoot, "profiles", safeProfileName(name), "config.json");
3154
+ return path2.join(globalRoot, "profiles", safeProfileName(name), "config.json");
2739
3155
  },
2740
3156
  profileStatuslineConfig: (name) => {
2741
- return path.join(globalRoot, "profiles", safeProfileName(name), "statusline.json");
3157
+ return path2.join(globalRoot, "profiles", safeProfileName(name), "statusline.json");
2742
3158
  },
2743
3159
  profileModeConfig: (name) => {
2744
- return path.join(globalRoot, "profiles", safeProfileName(name), "mode.json");
3160
+ return path2.join(globalRoot, "profiles", safeProfileName(name), "mode.json");
2745
3161
  },
2746
3162
  profileProviderStatus: (name) => {
2747
- return path.join(globalRoot, "profiles", safeProfileName(name), "provider-status.json");
3163
+ return path2.join(globalRoot, "profiles", safeProfileName(name), "provider-status.json");
2748
3164
  },
2749
3165
  profileUpdateCache: (name) => {
2750
- return path.join(globalRoot, "profiles", safeProfileName(name), "update-cache.json");
3166
+ return path2.join(globalRoot, "profiles", safeProfileName(name), "update-cache.json");
2751
3167
  },
2752
- secretsKey: path.join(globalRoot, ".key"),
2753
- globalMemory: path.join(profileDir, "memory.md"),
2754
- globalSkills: path.join(profileDir, "skills"),
2755
- globalClaudeSkills: path.join(homeDir, ".claude", "skills"),
2756
- globalDesignKits: path.join(profileDir, "design-kits"),
2757
- globalPrompts: path.join(profileDir, "prompts"),
2758
- globalInstructions: path.join(profileDir, "instructions"),
2759
- promptUsage: path.join(profileDir, "prompt-usage.json"),
2760
- cacheDir: path.join(globalRoot, "cache"),
2761
- modelsCache: path.join(globalRoot, "cache", "models.dev.json"),
2762
- modelsOverlayCache: path.join(globalRoot, "cache", "models-overlay.json"),
2763
- historyFile: path.join(profileDir, "history"),
2764
- 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"),
2765
3181
  projectDir,
2766
- projectCodebaseIndex: path.join(projectDir, "codebase-index"),
2767
- projectMemory: path.join(projectDir, "memory.md"),
2768
- projectSessions: path.join(projectDir, "sessions"),
2769
- projectTrust: path.join(projectDir, "trust.json"),
2770
- projectMeta: path.join(projectDir, "meta.json"),
2771
- projectLocalConfig: path.join(projectDir, "config.local.json"),
2772
- inProjectConfig: path.join(opts.projectRoot, ".wrongstack", "config.json"),
2773
- inProjectAgentsFile: path.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
2774
- inProjectSkills: path.join(opts.projectRoot, ".wrongstack", "skills"),
2775
- inProjectClaudeSkills: path.join(opts.projectRoot, ".claude", "skills"),
2776
- inProjectPrompts: path.join(opts.projectRoot, ".wrongstack", "prompts"),
2777
- inProjectInstructions: path.join(opts.projectRoot, ".wrongstack", "instructions"),
2778
- inProjectDesignKits: path.join(opts.projectRoot, ".wrongstack", "design-kits"),
2779
- 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"),
2780
3196
  projectHash: hash,
2781
3197
  projectSlug: slug,
2782
- projectGoal: path.join(projectDir, "goal.json"),
2783
- projectInputHistory: path.join(projectDir, "input-history.json"),
2784
- projectSpecs: path.join(projectDir, "specs"),
2785
- projectTaskGraphs: path.join(projectDir, "task-graphs"),
2786
- projectSddSession: path.join(projectDir, "sdd-session.json"),
2787
- projectPlan: path.join(projectDir, "plan.json"),
2788
- projectAutophase: path.join(projectDir, "autophase"),
2789
- projectSddBoards: path.join(projectDir, "sdd-boards"),
2790
- projectRequirementIntakes: path.join(projectDir, "requirement-intakes"),
2791
- syncConfig: path.join(profileDir, "sync.json"),
2792
- configHistoryDir: path.join(globalRoot, "config-history"),
2793
- 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
+ }
2794
3215
  };
2795
3216
  }
2796
3217
 
2797
3218
  // src/chronicle/identity.ts
2798
3219
  import { createHash as createHash3 } from "node:crypto";
2799
3220
  import * as os2 from "node:os";
2800
- import * as path2 from "node:path";
3221
+ import * as path3 from "node:path";
2801
3222
  function resolveChronicleRuntimeLocation(input) {
2802
3223
  const day = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2803
3224
  return {
2804
- installationId: stableId("installation", path2.resolve(input.globalRoot)),
3225
+ installationId: stableId("installation", path3.resolve(input.globalRoot)),
2805
3226
  machineId: stableId("machine", `${os2.hostname()}\0${os2.platform()}\0${os2.arch()}`),
2806
3227
  projectId: input.projectId,
2807
- chronicleDirectory: path2.join(input.projectDir, "chronicle"),
3228
+ chronicleDirectory: path3.join(input.projectDir, "chronicle"),
2808
3229
  day
2809
3230
  };
2810
3231
  }
@@ -2815,10 +3236,10 @@ function stableId(prefix, value) {
2815
3236
  // src/chronicle/journal.ts
2816
3237
  init_file_permissions();
2817
3238
  init_atomic_write();
2818
- import { randomUUID } from "node:crypto";
3239
+ import { randomUUID as randomUUID2 } from "node:crypto";
2819
3240
  import { createReadStream } from "node:fs";
2820
3241
  import * as fs2 from "node:fs/promises";
2821
- import * as path4 from "node:path";
3242
+ import * as path5 from "node:path";
2822
3243
  import { createInterface } from "node:readline";
2823
3244
 
2824
3245
  // src/chronicle/event-hash.ts
@@ -2841,14 +3262,14 @@ function chronicleEventHash(event) {
2841
3262
  }
2842
3263
 
2843
3264
  // src/chronicle/partition-filename.ts
2844
- import * as path3 from "node:path";
3265
+ import * as path4 from "node:path";
2845
3266
  var PARTITION_FILE_PATTERN = /^(.*\.events)(?:\.(\d{5}))?\.jsonl$/;
2846
3267
  function partitionFamily(filePath) {
2847
- const match = PARTITION_FILE_PATTERN.exec(path3.basename(filePath));
2848
- 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));
2849
3270
  }
2850
3271
  function partitionSequence(filePath) {
2851
- const match = PARTITION_FILE_PATTERN.exec(path3.basename(filePath));
3272
+ const match = PARTITION_FILE_PATTERN.exec(path4.basename(filePath));
2852
3273
  return Number(match?.[2] ?? 0);
2853
3274
  }
2854
3275
  function comparePartitionPaths(left, right) {
@@ -2897,10 +3318,10 @@ var ChronicleJournal = class {
2897
3318
  lastHash = GENESIS_HASH;
2898
3319
  lastAutoPurgeAt = 0;
2899
3320
  constructor(options) {
2900
- this.basePath = path4.resolve(options.filePath);
3321
+ this.basePath = path5.resolve(options.filePath);
2901
3322
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
2902
3323
  this.monotonicNow = options.monotonicNow ?? (() => process.hrtime.bigint());
2903
- this.idFactory = options.idFactory ?? randomUUID;
3324
+ this.idFactory = options.idFactory ?? randomUUID2;
2904
3325
  this.maxPending = Math.max(1, options.maxPending ?? 1e5);
2905
3326
  this.batchWindowMs = Math.max(0, options.batchWindowMs ?? 5);
2906
3327
  this.maxPartitionSizeBytes = options.maxPartitionSizeBytes ?? DEFAULT_MAX_PARTITION_BYTES;
@@ -2970,14 +3391,14 @@ var ChronicleJournal = class {
2970
3391
  }
2971
3392
  await this.refreshStateFromDisk();
2972
3393
  const cutoff = Date.now() - options.retentionDays * 864e5;
2973
- const activePath = path4.resolve(this.path);
3394
+ const activePath = path5.resolve(this.path);
2974
3395
  const errors = [];
2975
3396
  let dc = 0, db = 0, sc = 0;
2976
3397
  const suppliedFiles = options.files === void 0 ? await collectJournalPartitions(this.basePath) : [...options.files];
2977
3398
  const eligible = /* @__PURE__ */ new Set();
2978
3399
  for (const suppliedPath of new Set(suppliedFiles)) {
2979
- const file = path4.resolve(suppliedPath);
2980
- 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)) {
2981
3402
  errors.push({
2982
3403
  file: suppliedPath,
2983
3404
  reason: "not a Chronicle journal partition in this journal directory"
@@ -3198,34 +3619,34 @@ var ChronicleJournal = class {
3198
3619
  }
3199
3620
  };
3200
3621
  function rotatedPath(basePath, index) {
3201
- const dir = path4.dirname(basePath);
3202
- const ext = path4.extname(basePath);
3203
- const base = path4.basename(basePath, ext);
3204
- 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}`);
3205
3626
  }
3206
3627
  async function collectPartitions(basePath) {
3207
- const dir = path4.dirname(basePath);
3208
- const ext = path4.extname(basePath);
3209
- 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);
3210
3631
  const pattern = new RegExp(`^${escapeRegex(base)}(?:\\.\\d{5})?${escapeRegex(ext)}$`);
3211
3632
  const result = [];
3212
3633
  try {
3213
3634
  const entries = await fs2.readdir(dir, { withFileTypes: true });
3214
3635
  for (const entry of entries)
3215
- 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));
3216
3637
  } catch {
3217
3638
  }
3218
- const baseFile = path4.join(dir, base + ext);
3639
+ const baseFile = path5.join(dir, base + ext);
3219
3640
  const rotated = result.filter((file) => file !== baseFile).sort((left, right) => parseIndex(left, base, ext) - parseIndex(right, base, ext));
3220
3641
  return [baseFile, ...rotated];
3221
3642
  }
3222
3643
  async function collectJournalPartitions(basePath) {
3223
- const directory = path4.dirname(basePath);
3644
+ const directory = path5.dirname(basePath);
3224
3645
  const result = [];
3225
3646
  try {
3226
3647
  const entries = await fs2.readdir(directory, { withFileTypes: true });
3227
3648
  for (const entry of entries) {
3228
- const file = path4.join(directory, entry.name);
3649
+ const file = path5.join(directory, entry.name);
3229
3650
  if (entry.isFile() && isJournalPartition(file, directory, basePath)) result.push(file);
3230
3651
  }
3231
3652
  } catch {
@@ -3233,13 +3654,13 @@ async function collectJournalPartitions(basePath) {
3233
3654
  return result.sort(comparePartitionPaths);
3234
3655
  }
3235
3656
  function isJournalPartition(filePath, directory, basePath) {
3236
- if (path4.dirname(path4.resolve(filePath)) !== path4.resolve(directory)) return false;
3237
- const fileName = path4.basename(filePath);
3657
+ if (path5.dirname(path5.resolve(filePath)) !== path5.resolve(directory)) return false;
3658
+ const fileName = path5.basename(filePath);
3238
3659
  if (!basePath) return false;
3239
- const baseName = path4.basename(basePath);
3660
+ const baseName = path5.basename(basePath);
3240
3661
  const dailyFamily = /^\d{4}-\d{2}-\d{2}\.events(?:\.\d{5})?\.jsonl$/;
3241
3662
  if (/^\d{4}-\d{2}-\d{2}\.events\.jsonl$/.test(baseName)) return dailyFamily.test(fileName);
3242
- return partitionFamilyBase(path4.resolve(filePath)) === partitionFamilyBase(path4.resolve(basePath));
3663
+ return partitionFamilyBase(path5.resolve(filePath)) === partitionFamilyBase(path5.resolve(basePath));
3243
3664
  }
3244
3665
  function groupPartitionsByFamily(files) {
3245
3666
  const groups = /* @__PURE__ */ new Map();
@@ -3363,12 +3784,12 @@ function isHash(value) {
3363
3784
  return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
3364
3785
  }
3365
3786
  function parseIndex(filePath, base, ext) {
3366
- const suffix = path4.basename(filePath).slice(base.length + 1, -ext.length);
3787
+ const suffix = path5.basename(filePath).slice(base.length + 1, -ext.length);
3367
3788
  return suffix ? parseInt(suffix, 10) : 0;
3368
3789
  }
3369
3790
  function partitionIndex(filePath, basePath) {
3370
- const ext = path4.extname(basePath);
3371
- return parseIndex(filePath, path4.basename(basePath, ext), ext);
3791
+ const ext = path5.extname(basePath);
3792
+ return parseIndex(filePath, path5.basename(basePath, ext), ext);
3372
3793
  }
3373
3794
  function escapeRegex(text) {
3374
3795
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -3382,8 +3803,8 @@ async function readLastEntryState(filePath) {
3382
3803
  throw error;
3383
3804
  }
3384
3805
  try {
3385
- const stat7 = await handle.stat();
3386
- const size = stat7.size;
3806
+ const stat8 = await handle.stat();
3807
+ const size = stat8.size;
3387
3808
  let position = size, suffix = "";
3388
3809
  while (position > 0) {
3389
3810
  const length = Math.min(65536, position);
@@ -3400,14 +3821,14 @@ async function readLastEntryState(filePath) {
3400
3821
  return {
3401
3822
  entry: JSON.parse(trimmed),
3402
3823
  size,
3403
- birthtimeMs: stat7.birthtimeMs
3824
+ birthtimeMs: stat8.birthtimeMs
3404
3825
  };
3405
3826
  } catch {
3406
3827
  }
3407
3828
  }
3408
3829
  suffix = lines[0] ?? "";
3409
3830
  }
3410
- return { size, birthtimeMs: stat7.birthtimeMs };
3831
+ return { size, birthtimeMs: stat8.birthtimeMs };
3411
3832
  } finally {
3412
3833
  await handle.close();
3413
3834
  }
@@ -3425,7 +3846,7 @@ async function* streamEntriesStrict(filePath) {
3425
3846
  try {
3426
3847
  entry = JSON.parse(trimmed);
3427
3848
  } catch {
3428
- 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)}`);
3429
3850
  }
3430
3851
  yield entry;
3431
3852
  }
@@ -3450,7 +3871,7 @@ function errorMessage(error) {
3450
3871
  init_atomic_write();
3451
3872
  import * as fs5 from "node:fs/promises";
3452
3873
  import { createRequire } from "node:module";
3453
- import * as path7 from "node:path";
3874
+ import * as path8 from "node:path";
3454
3875
 
3455
3876
  // src/utils/sqlite-warning.ts
3456
3877
  var SQLITE_EXPERIMENTAL_WARNING_RE = /sqlite is an experimental feature/i;
@@ -3479,13 +3900,13 @@ function withSqliteExperimentalWarningSuppressed(run) {
3479
3900
  import { createHash as createHash5 } from "node:crypto";
3480
3901
  import { createReadStream as createReadStream2 } from "node:fs";
3481
3902
  import * as fs4 from "node:fs/promises";
3482
- import * as path6 from "node:path";
3903
+ import * as path7 from "node:path";
3483
3904
  import { createInterface as createInterface2 } from "node:readline";
3484
3905
 
3485
3906
  // src/chronicle/partition-range-cache.ts
3486
3907
  init_atomic_write();
3487
3908
  import * as fs3 from "node:fs/promises";
3488
- import * as path5 from "node:path";
3909
+ import * as path6 from "node:path";
3489
3910
  var CACHE_VERSION = 1;
3490
3911
  function isRange(value) {
3491
3912
  if (!value || typeof value !== "object") return false;
@@ -3495,14 +3916,14 @@ function isRange(value) {
3495
3916
  var ChroniclePartitionRangeCache = class {
3496
3917
  constructor(directory) {
3497
3918
  this.directory = directory;
3498
- this.cachePath = path5.join(directory, "range-cache.json");
3919
+ this.cachePath = path6.join(directory, "range-cache.json");
3499
3920
  }
3500
3921
  directory;
3501
3922
  cachePath;
3502
3923
  entries;
3503
3924
  dirty = false;
3504
3925
  toKey(filePath) {
3505
- return path5.relative(this.directory, filePath).split(path5.sep).join("/");
3926
+ return path6.relative(this.directory, filePath).split(path6.sep).join("/");
3506
3927
  }
3507
3928
  async load() {
3508
3929
  if (this.entries) return this.entries;
@@ -3603,7 +4024,7 @@ var ChronicleQueryEngine = class _ChronicleQueryEngine {
3603
4024
  /** The one file (if any) that could still be actively appended to — never cache-checked or skipped. */
3604
4025
  activeFile;
3605
4026
  static async fromDirectory(directory, options) {
3606
- const resolved = path6.resolve(directory);
4027
+ const resolved = path7.resolve(directory);
3607
4028
  const files = await findPartitions(resolved);
3608
4029
  const rangeCache = options?.rangeCache ?? new ChroniclePartitionRangeCache(resolved);
3609
4030
  return new _ChronicleQueryEngine(
@@ -4000,7 +4421,7 @@ async function findPartitions(root) {
4000
4421
  return;
4001
4422
  }
4002
4423
  for (const entry of entries) {
4003
- const full = path6.join(directory, entry.name);
4424
+ const full = path7.join(directory, entry.name);
4004
4425
  if (entry.isDirectory()) await visit(full);
4005
4426
  else if (entry.isFile() && PARTITION_FILE_PATTERN.test(entry.name)) result.push(full);
4006
4427
  }
@@ -4187,7 +4608,7 @@ async function resolveSnapshotFiles(files, snapshot) {
4187
4608
  }));
4188
4609
  }
4189
4610
  function fileId(file) {
4190
- return createHash5("sha256").update(path6.resolve(file), "utf8").digest("base64url");
4611
+ return createHash5("sha256").update(path7.resolve(file), "utf8").digest("base64url");
4191
4612
  }
4192
4613
  function stableStringify2(value) {
4193
4614
  if (value === null || typeof value !== "object") return JSON.stringify(value);
@@ -4299,8 +4720,8 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
4299
4720
  directory;
4300
4721
  dbPath;
4301
4722
  constructor(directory) {
4302
- this.directory = path7.resolve(directory);
4303
- this.dbPath = path7.join(this.directory, "metrics.db");
4723
+ this.directory = path8.resolve(directory);
4724
+ this.dbPath = path8.join(this.directory, "metrics.db");
4304
4725
  const Database = loadDatabaseSync();
4305
4726
  this.db = new Database(this.dbPath);
4306
4727
  this.db.exec("PRAGMA journal_mode = WAL");
@@ -4325,7 +4746,7 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
4325
4746
  const files = await findChroniclePartitions(this.directory);
4326
4747
  const offsets = this.loadOffsets();
4327
4748
  for (const file of files) {
4328
- const key = normalizeKey(path7.relative(this.directory, file));
4749
+ const key = normalizeKey(path8.relative(this.directory, file));
4329
4750
  const consumed = offsets.get(key) ?? 0;
4330
4751
  const ingested = await this.ingestFile(file, key, consumed, result);
4331
4752
  if (ingested) result.sourceFiles++;
@@ -4695,7 +5116,7 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
4695
5116
  }
4696
5117
  pruneOffsets(existingFiles) {
4697
5118
  const keep = new Set(
4698
- existingFiles.map((file) => normalizeKey(path7.relative(this.directory, file)))
5119
+ existingFiles.map((file) => normalizeKey(path8.relative(this.directory, file)))
4699
5120
  );
4700
5121
  for (const row of this.db.prepare("SELECT file FROM ingest_state").all()) {
4701
5122
  if (!keep.has(row.file))
@@ -4992,7 +5413,7 @@ function normalizePathKey(value) {
4992
5413
 
4993
5414
  // src/chronicle/legacy-journal-import.ts
4994
5415
  import * as fs6 from "node:fs/promises";
4995
- import * as path8 from "node:path";
5416
+ import * as path9 from "node:path";
4996
5417
  var ChronicleImportError = class extends Error {
4997
5418
  constructor(message, day, sequence) {
4998
5419
  super(message);
@@ -5035,7 +5456,7 @@ async function importLegacyChronicleJournal(journal, directory) {
5035
5456
  let importedFamilies = 0;
5036
5457
  for (const familyBase of families) {
5037
5458
  const day = dayOfFamily(familyBase);
5038
- const basePath = path8.join(directory, `${familyBase}.jsonl`);
5459
+ const basePath = path9.join(directory, `${familyBase}.jsonl`);
5039
5460
  let familyEvents = 0;
5040
5461
  if (journal.hasImportedDay(day)) continue;
5041
5462
  try {
@@ -5098,9 +5519,10 @@ async function importLegacyChronicleJournal(journal, directory) {
5098
5519
  }
5099
5520
 
5100
5521
  // src/chronicle/sqlite-journal.ts
5101
- import { randomUUID as randomUUID2 } from "node:crypto";
5522
+ import { randomUUID as randomUUID3 } from "node:crypto";
5523
+ import * as fs8 from "node:fs";
5102
5524
  import { createRequire as createRequire2 } from "node:module";
5103
- import * as path10 from "node:path";
5525
+ import * as path11 from "node:path";
5104
5526
 
5105
5527
  // src/utils/index.ts
5106
5528
  init_atomic_write();
@@ -5129,7 +5551,7 @@ function envFlag(value) {
5129
5551
  return !/^(0|false|no|off)$/i.test(value.trim());
5130
5552
  }
5131
5553
  var COLOR = isColorTty();
5132
- 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;
5133
5555
  var color = {
5134
5556
  reset: wrap("0", "0"),
5135
5557
  bold: wrap("1", "22"),
@@ -5149,6 +5571,20 @@ var color = {
5149
5571
  bgGreen: wrap("42", "49")
5150
5572
  };
5151
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
+
5152
5588
  // src/skills/limits.ts
5153
5589
  var SKILL_LIMITS = {
5154
5590
  /**
@@ -5249,7 +5685,14 @@ var SKILL_LIMITS = {
5249
5685
  };
5250
5686
 
5251
5687
  // src/skills/frontmatter.ts
5252
- 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
+ ]);
5253
5696
  function parseSkillFrontmatter(raw) {
5254
5697
  const text = normalizeLineEndings(raw);
5255
5698
  if (!text.startsWith("---")) return {};
@@ -5308,8 +5751,9 @@ function parseFrontmatterBlock(block) {
5308
5751
  out[normalizeKey2(key)] = collected.join("\n").trim();
5309
5752
  continue;
5310
5753
  }
5311
- if (key === "allowed-tools" || key === "allowedTools") {
5312
- 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;
5313
5757
  i++;
5314
5758
  continue;
5315
5759
  }
@@ -5323,7 +5767,11 @@ function parseFrontmatterBlock(block) {
5323
5767
  return out;
5324
5768
  }
5325
5769
  function normalizeKey2(key) {
5326
- 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;
5327
5775
  }
5328
5776
  function unquote(s) {
5329
5777
  if (s.length >= 2 && (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'"))) {
@@ -5364,7 +5812,7 @@ function capSkillBody(body) {
5364
5812
 
5365
5813
  // src/utils/instruction-file.ts
5366
5814
  import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
5367
- import * as path9 from "node:path";
5815
+ import * as path10 from "node:path";
5368
5816
  import { fileURLToPath } from "node:url";
5369
5817
  var textCache = /* @__PURE__ */ new Map();
5370
5818
  var rootCandidates;
@@ -5374,7 +5822,7 @@ function readBundledInstructionText(relativePath) {
5374
5822
  let resolved = "";
5375
5823
  for (const root of instructionRootCandidates()) {
5376
5824
  try {
5377
- resolved = readFileSync2(path9.join(root, relativePath), "utf8").trimEnd();
5825
+ resolved = readFileSync2(path10.join(root, relativePath), "utf8").trimEnd();
5378
5826
  break;
5379
5827
  } catch {
5380
5828
  }
@@ -5390,11 +5838,11 @@ function renderInstructionTemplate(template, values) {
5390
5838
  }
5391
5839
  function instructionRootCandidates() {
5392
5840
  if (rootCandidates !== void 0) return rootCandidates;
5393
- const here = path9.dirname(fileURLToPath(import.meta.url));
5841
+ const here = path10.dirname(fileURLToPath(import.meta.url));
5394
5842
  const candidates = [
5395
- path9.resolve(here, "../../instructions"),
5396
- path9.resolve(here, "../instructions"),
5397
- path9.resolve(here, "instructions")
5843
+ path10.resolve(here, "../../instructions"),
5844
+ path10.resolve(here, "../instructions"),
5845
+ path10.resolve(here, "instructions")
5398
5846
  ];
5399
5847
  rootCandidates = candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
5400
5848
  return rootCandidates;
@@ -5508,7 +5956,7 @@ var ChronicleSqliteQueryEngine = class {
5508
5956
  const comparison = order === "asc" ? ">" : "<";
5509
5957
  const keyset = cursor ? ` AND (day, sequence) ${comparison} (?, ?)` : "";
5510
5958
  const sql = `SELECT day, sequence, payload FROM events WHERE ${pushed.clause}${keyset} ORDER BY day ${direction}, sequence ${direction} LIMIT ? OFFSET ?`;
5511
- const summary = createSummaryAccumulator();
5959
+ const summary2 = createSummaryAccumulator();
5512
5960
  const page = [];
5513
5961
  let total = 0;
5514
5962
  let scannedEvents = 0;
@@ -5532,7 +5980,7 @@ var ChronicleSqliteQueryEngine = class {
5532
5980
  }
5533
5981
  if (!matches(event, query)) continue;
5534
5982
  total++;
5535
- updateSummary(summary, event);
5983
+ updateSummary(summary2, event);
5536
5984
  if (page.length < limit) {
5537
5985
  page.push(event);
5538
5986
  last = { day: row.day, sequence: row.sequence };
@@ -5547,7 +5995,7 @@ var ChronicleSqliteQueryEngine = class {
5547
5995
  scannedEvents,
5548
5996
  sourceFiles: this.diagnostics.sourceFiles,
5549
5997
  invalidLines: this.diagnostics.invalidLines,
5550
- summary: finalizeSummary(summary)
5998
+ summary: finalizeSummary(summary2)
5551
5999
  };
5552
6000
  if (total > page.length && last) {
5553
6001
  return { ...result, nextCursor: encodeCursor2(last) };
@@ -5682,6 +6130,25 @@ var CHRONICLE_SQLITE_FILE = "chronicle.sqlite";
5682
6130
  var LEGACY_JSONL_MIGRATION_KEY = "legacy-jsonl-v1";
5683
6131
  var LEGACY_JSONL_QUARANTINE_KEY = "legacy-jsonl-v1:quarantine";
5684
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
+ };
5685
6152
  var Ctor2;
5686
6153
  function loadDatabaseSync2() {
5687
6154
  if (Ctor2) return Ctor2;
@@ -5704,6 +6171,14 @@ var ChronicleSqliteJournal = class {
5704
6171
  now;
5705
6172
  monotonicNow;
5706
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;
5707
6182
  /**
5708
6183
  * Cached chain head per day. Cleared wholesale on any write failure so the
5709
6184
  * next append rebuilds from the database rather than trusting a counter that
@@ -5721,14 +6196,40 @@ var ChronicleSqliteJournal = class {
5721
6196
  };
5722
6197
  lastBatchDurationMs;
5723
6198
  constructor(options) {
5724
- this.dbPath = path10.join(path10.resolve(options.directory), CHRONICLE_SQLITE_FILE);
6199
+ this.dbPath = path11.join(path11.resolve(options.directory), CHRONICLE_SQLITE_FILE);
5725
6200
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
5726
6201
  this.monotonicNow = options.monotonicNow ?? (() => process.hrtime.bigint());
5727
- 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;
5728
6223
  const Database = loadDatabaseSync2();
5729
6224
  this.db = new Database(this.dbPath);
5730
- this.db.exec("PRAGMA journal_mode = WAL");
6225
+ this.db.exec(`PRAGMA journal_mode = ${this.maxBytes === void 0 ? "WAL" : "DELETE"}`);
5731
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
+ }
5732
6233
  }
5733
6234
  close() {
5734
6235
  this.db.close();
@@ -5787,8 +6288,10 @@ var ChronicleSqliteJournal = class {
5787
6288
  events.push(event);
5788
6289
  previous = { sequence: event.sequence, hash: event.hash };
5789
6290
  }
6291
+ let retainedCountAfterCommit = this.retainedEventCount;
5790
6292
  try {
5791
6293
  this.db.exec("BEGIN IMMEDIATE");
6294
+ this.assertWithinByteQuota(events);
5792
6295
  const insert = this.db.prepare(
5793
6296
  `INSERT INTO events (
5794
6297
  day, sequence, event_id, hash, previous_hash, occurred_at, event_type, outcome,
@@ -5820,6 +6323,8 @@ var ChronicleSqliteJournal = class {
5820
6323
  JSON.stringify(event)
5821
6324
  );
5822
6325
  }
6326
+ retainedCountAfterCommit = this.enforceEventLimitWithinTransaction();
6327
+ this.assertActualAllocationWithinQuota();
5823
6328
  this.db.exec("COMMIT");
5824
6329
  } catch (error) {
5825
6330
  try {
@@ -5829,11 +6334,13 @@ var ChronicleSqliteJournal = class {
5829
6334
  this.anchors.clear();
5830
6335
  this.counters.failedEvents += inputs.length;
5831
6336
  this.lastBatchDurationMs = performance.now() - started;
5832
- throw error;
6337
+ throw this.normalizeQuotaError(error);
5833
6338
  }
5834
6339
  this.anchors.set(day, previous);
5835
6340
  this.counters.persistedEvents += events.length;
6341
+ this.retainedEventCount = retainedCountAfterCommit;
5836
6342
  this.lastBatchDurationMs = performance.now() - started;
6343
+ await this.enforceRetentionIfDue();
5837
6344
  return events;
5838
6345
  }
5839
6346
  async readAll() {
@@ -5894,6 +6401,145 @@ var ChronicleSqliteJournal = class {
5894
6401
  }
5895
6402
  return { ok: true, entries, lastSequence, lastHash };
5896
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
+ }
6430
+ /**
6431
+ * Bytes the journal actually occupies: live pages in the main database plus
6432
+ * whatever the rollback/WAL sidecars currently hold.
6433
+ *
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.
6447
+ */
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
+ }
6456
+ }
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
+ }
5897
6543
  /**
5898
6544
  * Drop events older than the retention window.
5899
6545
  *
@@ -5939,6 +6585,7 @@ var ChronicleSqliteJournal = class {
5939
6585
  };
5940
6586
  }
5941
6587
  this.anchors.clear();
6588
+ this.retainedEventCount = Math.max(0, this.retainedEventCount - count);
5942
6589
  return { ...empty, deletedCount: count };
5943
6590
  }
5944
6591
  /**
@@ -5970,7 +6617,9 @@ var ChronicleSqliteJournal = class {
5970
6617
  ON CONFLICT(day) DO UPDATE SET sequence = excluded.sequence, hash = excluded.hash`
5971
6618
  );
5972
6619
  this.db.exec("BEGIN IMMEDIATE");
6620
+ let retainedCountAfterCommit = this.retainedEventCount;
5973
6621
  try {
6622
+ this.assertWithinByteQuota([]);
5974
6623
  await load({
5975
6624
  insert: (day, event) => {
5976
6625
  const row = projectEvent(event);
@@ -6000,16 +6649,19 @@ var ChronicleSqliteJournal = class {
6000
6649
  checkpoint.run(day, sequence, hash);
6001
6650
  }
6002
6651
  });
6652
+ retainedCountAfterCommit = this.enforceEventLimitWithinTransaction();
6653
+ this.assertActualAllocationWithinQuota();
6003
6654
  this.db.exec("COMMIT");
6004
6655
  } catch (error) {
6005
6656
  try {
6006
6657
  this.db.exec("ROLLBACK");
6007
6658
  } catch {
6008
6659
  }
6009
- throw error;
6660
+ throw this.normalizeQuotaError(error);
6010
6661
  } finally {
6011
6662
  this.anchors.clear();
6012
6663
  }
6664
+ this.retainedEventCount = retainedCountAfterCommit;
6013
6665
  }
6014
6666
  /**
6015
6667
  * A read engine over this journal's own connection.
@@ -6165,15 +6817,15 @@ function projectEvent(event) {
6165
6817
 
6166
6818
  // src/chronicle/project-server-client.ts
6167
6819
  import { spawn } from "node:child_process";
6168
- import * as fs7 from "node:fs";
6820
+ import * as fs9 from "node:fs";
6169
6821
  import * as net from "node:net";
6170
- import * as path12 from "node:path";
6822
+ import * as path13 from "node:path";
6171
6823
  import { fileURLToPath as fileURLToPath2 } from "node:url";
6172
6824
 
6173
6825
  // src/chronicle/project-server-endpoint.ts
6174
6826
  import { createHash as createHash6 } from "node:crypto";
6175
6827
  import * as os3 from "node:os";
6176
- import * as path11 from "node:path";
6828
+ import * as path12 from "node:path";
6177
6829
 
6178
6830
  // src/chronicle/project-server-protocol.ts
6179
6831
  var CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION = 2;
@@ -6186,39 +6838,39 @@ function encodeChronicleProjectServerMessage(message) {
6186
6838
  // src/chronicle/project-server-endpoint.ts
6187
6839
  var CHRONICLE_PROJECT_SERVER_METADATA_FILE = "server.json";
6188
6840
  function normalizedPath(value) {
6189
- const resolved = path11.resolve(value);
6841
+ const resolved = path12.resolve(value);
6190
6842
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
6191
6843
  }
6192
6844
  function chronicleProjectServerKey(projectDir) {
6193
- 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);
6194
6846
  }
6195
6847
  function chronicleProjectServerEndpoint(projectDir) {
6196
6848
  const key = chronicleProjectServerKey(projectDir);
6197
6849
  if (process.platform === "win32") {
6198
6850
  return `\\\\.\\pipe\\wrongstack-chronicle-v${CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION}-${key}`;
6199
6851
  }
6200
- return path11.join(
6852
+ return path12.join(
6201
6853
  os3.tmpdir(),
6202
6854
  `wsch-v${CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION}`,
6203
6855
  `${key}.sock`
6204
6856
  );
6205
6857
  }
6206
6858
  function chronicleProjectServerMetadataPath(projectDir) {
6207
- return path11.join(projectDir, "chronicle", CHRONICLE_PROJECT_SERVER_METADATA_FILE);
6859
+ return path12.join(projectDir, "chronicle", CHRONICLE_PROJECT_SERVER_METADATA_FILE);
6208
6860
  }
6209
6861
 
6210
6862
  // src/chronicle/project-server-client.ts
6211
6863
  var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
6212
6864
  var SERVER_START_TIMEOUT_MS = 1e4;
6213
6865
  var DEFAULT_CALL_TIMEOUT_MS = 3e4;
6214
- function resolveChronicleDaemonAvailability(moduleUrl = import.meta.url, exists = fs7.existsSync) {
6866
+ function resolveChronicleDaemonAvailability(moduleUrl = import.meta.url, exists = fs9.existsSync) {
6215
6867
  if (process.env["WRONGSTACK_CHRONICLE_INLINE"] || process.env["WRONGSTACK_CHRONICLE_SERVER"] === "0") {
6216
6868
  return { kind: "inline-requested" };
6217
6869
  }
6218
6870
  const url = locateChronicleProjectServer(moduleUrl, exists);
6219
6871
  return url ? { kind: "available", url } : { kind: "missing-build" };
6220
6872
  }
6221
- function resolveChronicleProjectServerUrl(moduleUrl = import.meta.url, exists = fs7.existsSync) {
6873
+ function resolveChronicleProjectServerUrl(moduleUrl = import.meta.url, exists = fs9.existsSync) {
6222
6874
  const availability = resolveChronicleDaemonAvailability(moduleUrl, exists);
6223
6875
  return availability.kind === "available" ? availability.url : null;
6224
6876
  }
@@ -6387,7 +7039,7 @@ var ChronicleProjectServerClient = class {
6387
7039
  currentAuthToken() {
6388
7040
  if (this.authToken === void 0) {
6389
7041
  try {
6390
- const raw = fs7.readFileSync(
7042
+ const raw = fs9.readFileSync(
6391
7043
  chronicleProjectServerMetadataPath(this.options.projectDir),
6392
7044
  "utf8"
6393
7045
  );
@@ -6525,7 +7177,7 @@ var ChronicleProjectServerClient = class {
6525
7177
  }
6526
7178
  };
6527
7179
  function normalizePath(value) {
6528
- const resolved = path12.resolve(value);
7180
+ const resolved = path13.resolve(value);
6529
7181
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
6530
7182
  }
6531
7183
 
@@ -6533,7 +7185,7 @@ function normalizePath(value) {
6533
7185
  function resolveChronicleProjectServerOptions(options) {
6534
7186
  if (options.projectPaths) {
6535
7187
  return {
6536
- projectRoot: path13.resolve(options.projectRoot),
7188
+ projectRoot: path14.resolve(options.projectRoot),
6537
7189
  ...options.projectPaths,
6538
7190
  ...options.retentionDays !== void 0 ? { retentionDays: options.retentionDays } : {}
6539
7191
  };
@@ -6543,7 +7195,7 @@ function resolveChronicleProjectServerOptions(options) {
6543
7195
  userHome: options.userHome ?? os4.homedir()
6544
7196
  });
6545
7197
  return {
6546
- projectRoot: path13.resolve(options.projectRoot),
7198
+ projectRoot: path14.resolve(options.projectRoot),
6547
7199
  globalRoot: paths.globalRoot,
6548
7200
  projectId: paths.projectHash,
6549
7201
  projectDir: paths.projectDir,
@@ -6571,7 +7223,7 @@ function assertInlineWasRequested(caller) {
6571
7223
  );
6572
7224
  }
6573
7225
  function legacyPartitionPath(location) {
6574
- return path13.join(location.chronicleDirectory, `${location.day}.events.jsonl`);
7226
+ return path14.join(location.chronicleDirectory, `${location.day}.events.jsonl`);
6575
7227
  }
6576
7228
  function useSqliteStore() {
6577
7229
  return process.env["WRONGSTACK_CHRONICLE_STORE"] !== "jsonl";
@@ -6677,7 +7329,7 @@ var InlineChronicleProjectAccess = class {
6677
7329
  }
6678
7330
  }
6679
7331
  get chronicleDirectory() {
6680
- return path13.join(this.options.projectDir, "chronicle");
7332
+ return path14.join(this.options.projectDir, "chronicle");
6681
7333
  }
6682
7334
  journalForToday() {
6683
7335
  const now = /* @__PURE__ */ new Date();
@@ -6886,16 +7538,16 @@ async function findActiveKanbanCard(projectRoot) {
6886
7538
  }
6887
7539
  async function findFileProvenance(projectRoot, filePaths) {
6888
7540
  if (filePaths.length === 0) return [];
6889
- let access6;
7541
+ let access7;
6890
7542
  try {
6891
- access6 = createChronicleProjectAccess({ projectRoot });
7543
+ access7 = createChronicleProjectAccess({ projectRoot });
6892
7544
  } catch {
6893
7545
  return [];
6894
7546
  }
6895
7547
  const provenance = [];
6896
7548
  try {
6897
7549
  const uniquePaths = [...new Set(filePaths)];
6898
- const result = await access6.call("metrics", {
7550
+ const result = await access7.call("metrics", {
6899
7551
  view: "files",
6900
7552
  refresh: false,
6901
7553
  files: {
@@ -6920,7 +7572,7 @@ async function findFileProvenance(projectRoot, filePaths) {
6920
7572
  });
6921
7573
  }
6922
7574
  } finally {
6923
- await access6.close();
7575
+ await access7.close();
6924
7576
  }
6925
7577
  return provenance;
6926
7578
  }
@@ -7017,6 +7669,7 @@ function resolveAutoReviewConfig(cfg, sessionConfig) {
7017
7669
  provider: resolvedProvider,
7018
7670
  model: resolvedModel,
7019
7671
  fallbackModels,
7672
+ modelSelection: cfg.modelSelection === "random" ? "random" : "round-robin",
7020
7673
  debounceMs: cfg.debounceMs ?? DEFAULT_DEBOUNCE_MS,
7021
7674
  maxFilesPerBatch: cfg.maxFilesPerBatch ?? DEFAULT_MAX_FILES_PER_BATCH,
7022
7675
  maxConcurrentReviews: cfg.maxConcurrentReviews ?? DEFAULT_MAX_CONCURRENT_REVIEWS,
@@ -7137,9 +7790,9 @@ async function snapshotChangedFiles(cwd) {
7137
7790
  if (file.path.startsWith(".wrongstack/")) continue;
7138
7791
  if (budget <= 0) break;
7139
7792
  try {
7140
- const stat7 = await fsp.stat(path14.join(cwd, file.path));
7141
- if (!stat7.isFile() || stat7.size > MAX_SNAPSHOT_FILE_BYTES) continue;
7142
- 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");
7143
7796
  budget -= content.length;
7144
7797
  snapshots.push({
7145
7798
  ...file,
@@ -7161,7 +7814,7 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
7161
7814
  "",
7162
7815
  "Continuous code review that fires on every change during a session.",
7163
7816
  "Detects git-tracked file edits and dispatches review subagents",
7164
- "with configurable provider/model/fallback.",
7817
+ "with configurable provider/model/fallback and profile selection.",
7165
7818
  "",
7166
7819
  "Reports are persisted and shown as passive notifications.",
7167
7820
  "They never wake the leader or spawn mutating follow-up agents.",
@@ -7176,6 +7829,7 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
7176
7829
  " provider provider id for review agents",
7177
7830
  " model model id for review agents",
7178
7831
  " fallbackProfile named profile from config.fallbackProfiles",
7832
+ " modelSelection round-robin | random (default round-robin)",
7179
7833
  " debounceMs debounce window (default 15000)",
7180
7834
  " maxFilesPerBatch max files per review (default 15)",
7181
7835
  " maxConcurrentReviews max parallel reviews (default 2)",
@@ -7202,7 +7856,8 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
7202
7856
  "",
7203
7857
  ` Provider: ${cfg.provider}`,
7204
7858
  ` Model: ${cfg.model}`,
7205
- ` Fallback: ${cfg.fallbackModels.length > 0 ? cfg.fallbackModels.join(", ") : "(none)"}`,
7859
+ ` Fallback / RR: ${cfg.fallbackModels.length > 0 ? cfg.fallbackModels.join(", ") : "(none)"}`,
7860
+ ` Selection: ${cfg.modelSelection}`,
7206
7861
  ` Debounce: ${cfg.debounceMs} ms`,
7207
7862
  ` Max files: ${cfg.maxFilesPerBatch}`,
7208
7863
  ` Max parallel: ${cfg.maxConcurrentReviews}`,
@@ -7421,6 +8076,7 @@ function createAutoReviewPlugin() {
7421
8076
  maxCascadeDepth: cfg.maxCascadeDepth
7422
8077
  });
7423
8078
  bundle.reviewFallbackModels = [...cfg.fallbackModels];
8079
+ bundle.reviewModelSelection = cfg.modelSelection;
7424
8080
  const trackedChangedPaths = new Set(
7425
8081
  snapshots.filter((file) => !file.path.startsWith(".wrongstack/")).map((file) => file.path)
7426
8082
  );
@@ -7431,7 +8087,7 @@ function createAutoReviewPlugin() {
7431
8087
  api.log.info(
7432
8088
  `[auto-review] #${reviewCounter} emitting review_needed (${filesWithContent.length} files, provider=${cfg.provider} model=${cfg.model})`
7433
8089
  );
7434
- const emittedBundle = emitReviewIfChanged(api, bundle);
8090
+ const emittedBundle = await emitReviewIfChanged(api, bundle);
7435
8091
  if (!emittedBundle) {
7436
8092
  api.log.info(
7437
8093
  `[auto-review] #${reviewCounter} skipped \u2014 file content already has a review in progress`
@@ -7464,89 +8120,94 @@ function createAutoReviewPlugin() {
7464
8120
  api.onEvent("iteration.completed", async (payload) => {
7465
8121
  await handleIterationCompleted?.(payload);
7466
8122
  });
7467
- api.onEvent("session.ended", async () => {
7468
- sessionEnded = true;
7469
- const handedOffFiles = pendingFiles.size;
7470
- cancelPendingReviewTimer();
7471
- if (handedOffFiles > 0) {
7472
- api.log.info(
7473
- `[auto-review] session ended \u2014 handed ${handedOffFiles} pending mid-session file(s) to post-session review`
7474
- );
7475
- }
7476
- await pendingReviewWork;
7477
- try {
7478
- const cfg = resolved;
7479
- if (!cfg.enabled) return;
7480
- const cwd = api.config.cwd ?? process.cwd();
7481
- if (!await isGitRepo(cwd)) return;
7482
- const allChanged = await getChangedFiles(cwd);
7483
- const existing = [];
7484
- for (const f of allChanged) {
7485
- if (f.path.startsWith(".wrongstack/")) continue;
7486
- try {
7487
- await fsp.access(path14.join(cwd, f.path));
7488
- existing.push(f);
7489
- } catch {
7490
- }
7491
- }
7492
- if (existing.length === 0) return;
7493
- const toReview = existing.slice(0, cfg.maxFilesPerBatch);
7494
- const filesWithContent = [];
7495
- for (const f of toReview) {
7496
- try {
7497
- const absPath = path14.join(cwd, f.path);
7498
- const content = await fsp.readFile(absPath, "utf8");
7499
- filesWithContent.push({ path: f.path, status: f.status, content });
7500
- } catch {
7501
- }
7502
- }
7503
- if (filesWithContent.length === 0) return;
7504
- 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) {
7505
8129
  api.log.info(
7506
- `[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`
7507
8131
  );
7508
- return;
7509
8132
  }
7510
- const bundle = await buildReviewContext({
7511
- cwd,
7512
- config: {
7513
- enabled: true,
7514
- provider: cfg.provider,
7515
- model: cfg.model,
7516
- maxFiles: cfg.maxFilesPerBatch,
7517
- autoFix: "off",
7518
- cascadeOn: "off",
7519
- maxCascadeDepth: 0
7520
- },
7521
- files: filesWithContent,
7522
- cascadeOn: cfg.cascadeOn,
7523
- maxCascadeDepth: cfg.maxCascadeDepth
7524
- });
7525
- bundle.reviewFallbackModels = [...cfg.fallbackModels];
7526
- const trackedChangedPaths = new Set(existing.map((file) => file.path));
7527
- bundle.allChangedFiles = bundle.allChangedFiles?.filter(
7528
- (file) => trackedChangedPaths.has(file.path)
7529
- );
7530
- const emittedBundle = emitReviewIfChanged(api, bundle);
7531
- 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);
7532
8202
  api.log.info(
7533
- "[auto-review] session end \u2014 unchanged files already have reviews in progress"
8203
+ `[auto-review] session end \u2014 final review (${emittedBundle.files.length} files)`
7534
8204
  );
7535
- return;
8205
+ } catch (err) {
8206
+ api.log.warn(`[auto-review] session.ended handler failed: ${toErrorMessage(err)}`);
7536
8207
  }
7537
- const inflightEntry = {
7538
- files: emittedBundle.files.map((file) => file.path),
7539
- startedAt: Date.now(),
7540
- subagentType: "review"
7541
- };
7542
- inFlight.push(inflightEntry);
7543
- expireInFlight(inflightEntry);
7544
- api.log.info(
7545
- `[auto-review] session end \u2014 final review (${emittedBundle.files.length} files)`
7546
- );
7547
- } catch (err) {
7548
- api.log.warn(`[auto-review] session.ended handler failed: ${toErrorMessage(err)}`);
7549
- }
8208
+ })();
8209
+ event?.waitUntil?.(work);
8210
+ return work;
7550
8211
  });
7551
8212
  api.onPattern("chimera.review_complete", (_event, payload) => {
7552
8213
  try {
@@ -7592,16 +8253,16 @@ function createAutoReviewPlugin() {
7592
8253
  // src/plugins/chimera-plugin.ts
7593
8254
  init_error();
7594
8255
  import { spawn as spawn4 } from "node:child_process";
7595
- import * as fsp4 from "node:fs/promises";
8256
+ import * as fsp5 from "node:fs/promises";
7596
8257
  import * as os5 from "node:os";
7597
- import * as path17 from "node:path";
8258
+ import * as path18 from "node:path";
7598
8259
 
7599
8260
  // src/plugins/review-finding-commands.ts
7600
8261
  init_review_finding_store();
7601
8262
 
7602
8263
  // src/plugins/review-finding-parser.ts
7603
8264
  init_review_finding_types();
7604
- import { randomUUID as randomUUID4 } from "node:crypto";
8265
+ import { randomUUID as randomUUID5 } from "node:crypto";
7605
8266
  var SUGGEST_LINE = /^\s*(?:→|->|=>)\s*(.+)$/;
7606
8267
  var DURATION_LINE = /^Duration:\s*(\d+)s\s*$/im;
7607
8268
  function parseChimeraReviewReport(reportText, context = {}) {
@@ -7609,7 +8270,7 @@ function parseChimeraReviewReport(reportText, context = {}) {
7609
8270
  return { findings: [], unparseableCount: 0 };
7610
8271
  }
7611
8272
  const findings = [];
7612
- const reportId = context.reportId ?? randomUUID4();
8273
+ const reportId = context.reportId ?? randomUUID5();
7613
8274
  let unparseableCount = 0;
7614
8275
  let durationSeconds;
7615
8276
  let currentSeverity = null;
@@ -7710,7 +8371,7 @@ function parseFindingSegment(segment, severity, context) {
7710
8371
  const fullDesc = suggestions.length > 0 ? cleanDesc + "\n" + suggestions.map((s) => " \u2192 " + s).join("\n") : cleanDesc;
7711
8372
  const suggestedFix = suggestions.length > 0 ? suggestions.join("\n") : void 0;
7712
8373
  return {
7713
- id: randomUUID4(),
8374
+ id: randomUUID5(),
7714
8375
  fingerprint: computeFindingFingerprint(file ?? "", line ?? null, title),
7715
8376
  severity,
7716
8377
  source: normalizeFindingSource(context.reviewType),
@@ -7721,7 +8382,7 @@ function parseFindingSegment(segment, severity, context) {
7721
8382
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7722
8383
  status: "active",
7723
8384
  originReport: {
7724
- reportId: context.reportId ?? randomUUID4(),
8385
+ reportId: context.reportId ?? randomUUID5(),
7725
8386
  sessionId: context.sessionId ?? "",
7726
8387
  agentId: context.agentId ?? "",
7727
8388
  reviewerModel: context.reviewerModel ?? ""
@@ -8375,7 +9036,10 @@ function buildChimeraCommand(getConfig, events) {
8375
9036
  ].join("\n")
8376
9037
  };
8377
9038
  }
8378
- events.emit("chimera.set_autofix", { mode });
9039
+ events.emit(
9040
+ "chimera.set_autofix",
9041
+ { mode }
9042
+ );
8379
9043
  return {
8380
9044
  message: `\u2705 Chimera autoFix set to "${mode}" for this session (config file unchanged).`
8381
9045
  };
@@ -8414,8 +9078,8 @@ function buildReviewCommand(_getConfig) {
8414
9078
  async run(args) {
8415
9079
  const cwd = process.cwd();
8416
9080
  const slug = projectSlug(cwd);
8417
- const globalRoot = path17.join(os5.homedir(), ".wrongstack");
8418
- const projectDir = path17.join(globalRoot, "projects", slug);
9081
+ const globalRoot = path18.join(os5.homedir(), ".wrongstack");
9082
+ const projectDir = path18.join(globalRoot, "projects", slug);
8419
9083
  const trimmed = (args ?? "").trim();
8420
9084
  const parts = trimmed.split(/\s+/).filter(Boolean);
8421
9085
  return {
@@ -8448,10 +9112,10 @@ function createChimeraPlugin() {
8448
9112
  }
8449
9113
  });
8450
9114
  api.onPattern("chimera.review_needed", (_event, payload) => {
8451
- recordStartedReview(api.events, payload);
9115
+ void recordStartedReview(api.events, payload).catch(() => void 0);
8452
9116
  });
8453
9117
  api.onPattern("chimera.review_complete", (_event, payload) => {
8454
- recordCompletedReview(api.events, payload);
9118
+ void recordCompletedReview(api.events, payload).catch(() => void 0);
8455
9119
  });
8456
9120
  if (!resolved.enabled) {
8457
9121
  api.log.info("[chimera] disabled by config");
@@ -8462,64 +9126,70 @@ function createChimeraPlugin() {
8462
9126
  );
8463
9127
  api.slashCommands.register(buildChimeraCommand(() => resolved, api.events));
8464
9128
  api.slashCommands.register(buildReviewCommand(() => resolved));
8465
- api.onEvent("session.ended", async () => {
8466
- try {
8467
- const cfg = resolved;
8468
- if (!cfg.enabled) return;
8469
- const cwd = api.config.cwd ?? process.cwd();
8470
- if (!await isGitRepo2(cwd)) {
8471
- api.log.info("[chimera] skipped \u2014 not a git repo");
8472
- return;
8473
- }
8474
- const allChanged = await getChangedFiles2(cwd);
8475
- const existing = [];
8476
- for (const f of allChanged) {
8477
- if (f.path.startsWith(".wrongstack/")) continue;
8478
- try {
8479
- await fsp4.access(path17.join(cwd, f.path));
8480
- existing.push(f);
8481
- } 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;
8482
9138
  }
8483
- }
8484
- if (existing.length === 0) {
8485
- api.log.info("[chimera] no changed files to review");
8486
- return;
8487
- }
8488
- const toReview = existing.slice(0, cfg.maxFiles);
8489
- if (existing.length > cfg.maxFiles) {
8490
- api.log.info(`[chimera] capping review at ${cfg.maxFiles} of ${existing.length} files`);
8491
- }
8492
- const filesWithContent = [];
8493
- for (const f of toReview) {
8494
- try {
8495
- const absPath = path17.join(cwd, f.path);
8496
- const content = await fsp4.readFile(absPath, "utf8");
8497
- filesWithContent.push({ path: f.path, status: f.status, content });
8498
- } 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
+ }
8499
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)}`);
8500
9189
  }
8501
- if (filesWithContent.length === 0) {
8502
- api.log.info("[chimera] could not read changed files");
8503
- return;
8504
- }
8505
- const bundle = await buildReviewContext({
8506
- cwd,
8507
- config: cfg,
8508
- files: filesWithContent,
8509
- cascadeOn: cfg.cascadeOn,
8510
- maxCascadeDepth: cfg.maxCascadeDepth
8511
- });
8512
- const emittedBundle = emitReviewIfChanged(api, bundle);
8513
- if (!emittedBundle) {
8514
- api.log.info("[chimera] skipped \u2014 changed files already have reviews in progress");
8515
- return;
8516
- }
8517
- api.log.info(
8518
- `[chimera] emitted review_needed event (${emittedBundle.files.length} files)`
8519
- );
8520
- } catch (err) {
8521
- api.log.warn(`[chimera] session.ended handler failed: ${toErrorMessage(err)}`);
8522
- }
9190
+ })();
9191
+ event?.waitUntil?.(work);
9192
+ return work;
8523
9193
  });
8524
9194
  },
8525
9195
  teardown(api) {
@@ -8534,17 +9204,17 @@ function createChimeraPlugin() {
8534
9204
  }
8535
9205
 
8536
9206
  // src/plugins/prompts-plugin.ts
8537
- import * as fs11 from "node:fs/promises";
8538
- import * as path20 from "node:path";
9207
+ import * as fs13 from "node:fs/promises";
9208
+ import * as path21 from "node:path";
8539
9209
 
8540
9210
  // src/execution/prompt-loader.ts
8541
- import * as fs9 from "node:fs/promises";
8542
- import * as path19 from "node:path";
9211
+ import * as fs11 from "node:fs/promises";
9212
+ import * as path20 from "node:path";
8543
9213
 
8544
9214
  // src/storage/prompt-store.ts
8545
9215
  init_atomic_write();
8546
- import * as fs8 from "node:fs/promises";
8547
- import * as path18 from "node:path";
9216
+ import * as fs10 from "node:fs/promises";
9217
+ import * as path19 from "node:path";
8548
9218
  var SCHEMA_VERSION3 = 2;
8549
9219
  function migratePromptEntry(raw) {
8550
9220
  if (!raw || typeof raw !== "object") return null;
@@ -8605,12 +9275,12 @@ var DefaultPromptStore = class {
8605
9275
  await ensureDir(this.dir);
8606
9276
  const entries = [];
8607
9277
  try {
8608
- const files = await fs8.readdir(this.dir);
9278
+ const files = await fs10.readdir(this.dir);
8609
9279
  for (const file of files) {
8610
9280
  if (!file.endsWith(".json")) continue;
8611
9281
  try {
8612
9282
  const raw = JSON.parse(
8613
- await fs8.readFile(path18.join(this.dir, file), "utf8")
9283
+ await fs10.readFile(path19.join(this.dir, file), "utf8")
8614
9284
  );
8615
9285
  const migrated = migratePromptEntry(raw.entry);
8616
9286
  if (migrated) entries.push(migrated);
@@ -8624,9 +9294,9 @@ var DefaultPromptStore = class {
8624
9294
  );
8625
9295
  }
8626
9296
  async get(id) {
8627
- const file = path18.join(this.dir, `${id}.json`);
9297
+ const file = path19.join(this.dir, `${id}.json`);
8628
9298
  try {
8629
- const raw = JSON.parse(await fs8.readFile(file, "utf8"));
9299
+ const raw = JSON.parse(await fs10.readFile(file, "utf8"));
8630
9300
  return migratePromptEntry(raw.entry);
8631
9301
  } catch {
8632
9302
  return null;
@@ -8634,14 +9304,14 @@ var DefaultPromptStore = class {
8634
9304
  }
8635
9305
  async save(entry) {
8636
9306
  await ensureDir(this.dir);
8637
- const file = path18.join(this.dir, `${entry.id}.json`);
9307
+ const file = path19.join(this.dir, `${entry.id}.json`);
8638
9308
  const raw = { version: SCHEMA_VERSION3, entry };
8639
9309
  await atomicWrite(file, JSON.stringify(raw, null, 2));
8640
9310
  }
8641
9311
  async delete(id) {
8642
- const file = path18.join(this.dir, `${id}.json`);
9312
+ const file = path19.join(this.dir, `${id}.json`);
8643
9313
  try {
8644
- await fs8.unlink(file);
9314
+ await fs10.unlink(file);
8645
9315
  return true;
8646
9316
  } catch {
8647
9317
  return false;
@@ -8728,7 +9398,7 @@ var DefaultPromptLoader = class {
8728
9398
  constructor(opts) {
8729
9399
  this.projectStore = typeof opts.paths.inProjectPrompts === "string" ? new DefaultPromptStore(opts.paths.inProjectPrompts) : void 0;
8730
9400
  this.userStore = typeof opts.paths.globalPrompts === "string" ? new DefaultPromptStore(opts.paths.globalPrompts) : void 0;
8731
- this.builtinDir = opts.bundledDir ? path19.join(opts.bundledDir, "prompts") : void 0;
9401
+ this.builtinDir = opts.bundledDir ? path20.join(opts.bundledDir, "prompts") : void 0;
8732
9402
  }
8733
9403
  async list() {
8734
9404
  if (this.cache) return this.cache;
@@ -8841,7 +9511,7 @@ var DefaultPromptLoader = class {
8841
9511
  const files = await walkJson(dir);
8842
9512
  for (const file of files) {
8843
9513
  try {
8844
- const parsed = JSON.parse(await fs9.readFile(file, "utf8"));
9514
+ const parsed = JSON.parse(await fs11.readFile(file, "utf8"));
8845
9515
  const migrated = migratePromptEntry(parsed);
8846
9516
  if (migrated) out.push({ ...migrated, source: "builtin" });
8847
9517
  } catch {
@@ -8855,12 +9525,12 @@ async function walkJson(dir) {
8855
9525
  const out = [];
8856
9526
  let entries;
8857
9527
  try {
8858
- entries = await fs9.readdir(dir, { withFileTypes: true });
9528
+ entries = await fs11.readdir(dir, { withFileTypes: true });
8859
9529
  } catch {
8860
9530
  return out;
8861
9531
  }
8862
9532
  for (const e of entries) {
8863
- const full = path19.join(dir, e.name);
9533
+ const full = path20.join(dir, e.name);
8864
9534
  if (e.isDirectory()) {
8865
9535
  out.push(...await walkJson(full));
8866
9536
  } else if (e.name.endsWith(".json") && e.name !== "index.json" && e.name !== "schema.json") {
@@ -8935,7 +9605,7 @@ function escapeRegExp(s) {
8935
9605
 
8936
9606
  // src/storage/prompt-usage-store.ts
8937
9607
  init_atomic_write();
8938
- import * as fs10 from "node:fs/promises";
9608
+ import * as fs12 from "node:fs/promises";
8939
9609
  var PromptUsageStore = class {
8940
9610
  constructor(file) {
8941
9611
  this.file = file;
@@ -8955,7 +9625,7 @@ var PromptUsageStore = class {
8955
9625
  return this.cachedUsage;
8956
9626
  }
8957
9627
  try {
8958
- const raw = JSON.parse(await fs10.readFile(this.file, "utf8"));
9628
+ const raw = JSON.parse(await fs12.readFile(this.file, "utf8"));
8959
9629
  if (raw && typeof raw === "object" && raw.usage && typeof raw.usage === "object") {
8960
9630
  this.cachedUsage = raw.usage;
8961
9631
  this.cachedSignature = signature;
@@ -9035,8 +9705,8 @@ var PromptUsageStore = class {
9035
9705
  }
9036
9706
  async fileSignature() {
9037
9707
  try {
9038
- const stat7 = await fs10.stat(this.file);
9039
- 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 };
9040
9710
  } catch {
9041
9711
  return null;
9042
9712
  }
@@ -9222,7 +9892,7 @@ ${exact.content}` };
9222
9892
  2
9223
9893
  );
9224
9894
  try {
9225
- await fs11.writeFile(target, payload, "utf8");
9895
+ await fs13.writeFile(target, payload, "utf8");
9226
9896
  return { message: `Exported ${own.length} prompt(s) \u2192 ${target}` };
9227
9897
  } catch (err) {
9228
9898
  return { message: `Export failed: ${err instanceof Error ? err.message : String(err)}` };
@@ -9234,7 +9904,7 @@ ${exact.content}` };
9234
9904
  const src = resolveIoPath(restJoined, ctx);
9235
9905
  let raw;
9236
9906
  try {
9237
- raw = JSON.parse(await fs11.readFile(src, "utf8"));
9907
+ raw = JSON.parse(await fs13.readFile(src, "utf8"));
9238
9908
  } catch (err) {
9239
9909
  return { message: `Import failed: ${err instanceof Error ? err.message : String(err)}` };
9240
9910
  }
@@ -9440,9 +10110,9 @@ ${dim(`slug: ${entry.slug} | category: ${entry.category} | source: ${entry.sourc
9440
10110
  }
9441
10111
  function resolveIoPath(p, ctx) {
9442
10112
  const cleaned = p.trim().replace(/^["']|["']$/g, "");
9443
- if (path20.isAbsolute(cleaned)) return cleaned;
10113
+ if (path21.isAbsolute(cleaned)) return cleaned;
9444
10114
  const base = ctx.projectRoot ?? process.cwd();
9445
- return path20.resolve(base, cleaned);
10115
+ return path21.resolve(base, cleaned);
9446
10116
  }
9447
10117
  function sourceGlyph(e) {
9448
10118
  return e.source === "project" ? "\u{1F4C1}" : e.source === "user" ? "\u{1F464}" : e.source === "synced" ? "\u2601" : "\u{1F4E6}";
@@ -9566,8 +10236,8 @@ async function integrateFindings(payload, projectDir, reportId) {
9566
10236
  init_atomic_write();
9567
10237
  init_review_finding_store();
9568
10238
  init_review_report_store();
9569
- import * as fsp5 from "node:fs/promises";
9570
- import * as path21 from "node:path";
10239
+ import * as fsp6 from "node:fs/promises";
10240
+ import * as path22 from "node:path";
9571
10241
  var REVIEW_STORE_COMPACTION_THRESHOLD_BYTES = 8 * 1024 * 1024;
9572
10242
  var REVIEW_STORE_COMPACTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
9573
10243
  var REVIEW_STORE_MAINTENANCE_FILE = ".review-store-maintenance.json";
@@ -9575,7 +10245,7 @@ async function maybeCompactReviewStores(projectDir, opts = {}) {
9575
10245
  const thresholdBytes = opts.thresholdBytes ?? REVIEW_STORE_COMPACTION_THRESHOLD_BYTES;
9576
10246
  const intervalMs = opts.intervalMs ?? REVIEW_STORE_COMPACTION_INTERVAL_MS;
9577
10247
  const now = opts.now ?? Date.now();
9578
- const maintenancePath = path21.join(projectDir, REVIEW_STORE_MAINTENANCE_FILE);
10248
+ const maintenancePath = path22.join(projectDir, REVIEW_STORE_MAINTENANCE_FILE);
9579
10249
  return withFileLock(maintenancePath, async () => {
9580
10250
  const totalBytes = await fileSize(resolveReportStorePath(projectDir)) + await fileSize(resolveFindingStorePath(projectDir));
9581
10251
  if (totalBytes < thresholdBytes) {
@@ -9615,7 +10285,7 @@ function emptyResult(reason, totalBytes) {
9615
10285
  }
9616
10286
  async function fileSize(filePath) {
9617
10287
  try {
9618
- return (await fsp5.stat(filePath)).size;
10288
+ return (await fsp6.stat(filePath)).size;
9619
10289
  } catch (error) {
9620
10290
  if (error.code === "ENOENT") return 0;
9621
10291
  throw error;
@@ -9623,7 +10293,7 @@ async function fileSize(filePath) {
9623
10293
  }
9624
10294
  async function readMaintenanceState(filePath) {
9625
10295
  try {
9626
- const parsed = JSON.parse(await fsp5.readFile(filePath, "utf8"));
10296
+ const parsed = JSON.parse(await fsp6.readFile(filePath, "utf8"));
9627
10297
  return typeof parsed.compactedAt === "string" && Number.isFinite(parsed.totalBytesBefore) ? parsed : null;
9628
10298
  } catch (error) {
9629
10299
  if (error.code === "ENOENT" || error instanceof SyntaxError) {
@@ -9635,7 +10305,7 @@ async function readMaintenanceState(filePath) {
9635
10305
 
9636
10306
  // src/plugins/skills-plugin.ts
9637
10307
  import * as os7 from "node:os";
9638
- import * as path26 from "node:path";
10308
+ import * as path27 from "node:path";
9639
10309
 
9640
10310
  // src/skills/foreign-sources.ts
9641
10311
  var FOREIGN_SKILL_TOOLS = [
@@ -9817,8 +10487,8 @@ function numField(rec, key) {
9817
10487
  // src/skills/skill-generator.ts
9818
10488
  init_errors();
9819
10489
  import { spawn as spawn5 } from "node:child_process";
9820
- import * as fs12 from "node:fs/promises";
9821
- import * as path22 from "node:path";
10490
+ import * as fs14 from "node:fs/promises";
10491
+ import * as path23 from "node:path";
9822
10492
  async function validateSkillNameAvailable(name, loader) {
9823
10493
  const formatViolations = validateSkillName(name);
9824
10494
  const conflicts = loader ? (await loader.listEntries()).filter((e) => e.name === name) : [];
@@ -9923,7 +10593,12 @@ async function openInEditor(filePath, env = process.env) {
9923
10593
  const child = spawn5(parts[0], [...parts.slice(1), filePath], {
9924
10594
  stdio: "ignore",
9925
10595
  detached: true,
9926
- 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
9927
10602
  });
9928
10603
  child.unref();
9929
10604
  }
@@ -9937,11 +10612,11 @@ async function writeSkeletonSkill(skillsDir, body, opts = {}) {
9937
10612
  subsystem: "general"
9938
10613
  });
9939
10614
  }
9940
- const skillDir = path22.join(skillsDir, name);
9941
- const skillFile = path22.join(skillDir, "SKILL.md");
10615
+ const skillDir = path23.join(skillsDir, name);
10616
+ const skillFile = path23.join(skillDir, "SKILL.md");
9942
10617
  if (!opts.overwrite) {
9943
10618
  try {
9944
- await fs12.access(skillFile);
10619
+ await fs14.access(skillFile);
9945
10620
  throw new WrongStackError({
9946
10621
  message: `A skill already exists at ${skillFile}. Use --force to overwrite.`,
9947
10622
  code: ERROR_CODES.VALIDATION_ERROR,
@@ -9952,8 +10627,8 @@ async function writeSkeletonSkill(skillsDir, body, opts = {}) {
9952
10627
  if (err instanceof WrongStackError) throw err;
9953
10628
  }
9954
10629
  }
9955
- await fs12.mkdir(skillDir, { recursive: true });
9956
- await fs12.writeFile(skillFile, body, "utf8");
10630
+ await fs14.mkdir(skillDir, { recursive: true });
10631
+ await fs14.writeFile(skillFile, body, "utf8");
9957
10632
  return skillFile;
9958
10633
  }
9959
10634
  function bodyLineAdvisory(body) {
@@ -9996,15 +10671,15 @@ function defaultEditor() {
9996
10671
  // src/skills/skill-installer.ts
9997
10672
  init_errors();
9998
10673
  init_error();
9999
- import * as fs15 from "node:fs/promises";
10000
- import * as path25 from "node:path";
10674
+ import * as fs17 from "node:fs/promises";
10675
+ import * as path26 from "node:path";
10001
10676
 
10002
10677
  // src/skills/github-fetcher.ts
10003
10678
  init_errors();
10004
10679
  import { createWriteStream } from "node:fs";
10005
- import * as fs13 from "node:fs/promises";
10680
+ import * as fs15 from "node:fs/promises";
10006
10681
  import * as os6 from "node:os";
10007
- import * as path23 from "node:path";
10682
+ import * as path24 from "node:path";
10008
10683
  import { Readable } from "node:stream";
10009
10684
  import { pipeline } from "node:stream/promises";
10010
10685
  import { createGunzip } from "node:zlib";
@@ -10146,7 +10821,7 @@ async function downloadGitHubTarball(parsed) {
10146
10821
  }
10147
10822
  });
10148
10823
  }
10149
- const tempDir = await fs13.mkdtemp(path23.join(os6.tmpdir(), "wskill-"));
10824
+ const tempDir = await fs15.mkdtemp(path24.join(os6.tmpdir(), "wskill-"));
10150
10825
  try {
10151
10826
  if (!response.body) {
10152
10827
  throw new WrongStackError({
@@ -10157,17 +10832,17 @@ async function downloadGitHubTarball(parsed) {
10157
10832
  });
10158
10833
  }
10159
10834
  const nodeStream = Readable.fromWeb(response.body);
10160
- const tarPath = path23.join(tempDir, ".wrongstack-download.tar");
10835
+ const tarPath = path24.join(tempDir, ".wrongstack-download.tar");
10161
10836
  await writeBoundedGzipStream(nodeStream, tarPath, SKILL_LIMITS.MAX_UNCOMPRESSED_TARBALL_SIZE);
10162
- const tarBuf = await fs13.readFile(tarPath);
10837
+ const tarBuf = await fs15.readFile(tarPath);
10163
10838
  try {
10164
10839
  await extractTar(tarBuf, tempDir);
10165
10840
  } finally {
10166
- await fs13.rm(tarPath, { force: true });
10841
+ await fs15.rm(tarPath, { force: true });
10167
10842
  }
10168
10843
  return { tempDir };
10169
10844
  } catch (err) {
10170
- await fs13.rm(tempDir, { recursive: true, force: true }).catch(() => {
10845
+ await fs15.rm(tempDir, { recursive: true, force: true }).catch(() => {
10171
10846
  });
10172
10847
  throw err;
10173
10848
  }
@@ -10222,25 +10897,25 @@ async function extractTar(buf, destDir) {
10222
10897
  const fullPath = prefix ? `${prefix}/${name}` : name;
10223
10898
  const relPath = stripTopDir(fullPath);
10224
10899
  if (relPath && relPath !== "." && relPath !== "..") {
10225
- const destPath = path23.join(destDir, relPath);
10226
- const resolvedDest = path23.resolve(destPath);
10227
- const resolvedRoot = path23.resolve(destDir);
10228
- 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)) {
10229
10904
  offset += 512 + Math.ceil(size / 512) * 512;
10230
10905
  continue;
10231
10906
  }
10232
10907
  if (typeflag === 53 || typeflag === 0) {
10233
10908
  if (relPath.endsWith("/") || typeflag === 53) {
10234
- await fs13.mkdir(destPath, { recursive: true });
10909
+ await fs15.mkdir(destPath, { recursive: true });
10235
10910
  }
10236
10911
  }
10237
10912
  if ((typeflag === 48 || typeflag === 0 || typeflag === 0) && size > 0) {
10238
- const dir = path23.dirname(destPath);
10239
- await fs13.mkdir(dir, { recursive: true });
10913
+ const dir = path24.dirname(destPath);
10914
+ await fs15.mkdir(dir, { recursive: true });
10240
10915
  const dataStart = offset + 512;
10241
10916
  const dataEnd = dataStart + size;
10242
10917
  if (dataEnd > buf.length) break;
10243
- await fs13.writeFile(destPath, buf.subarray(dataStart, dataEnd));
10918
+ await fs15.writeFile(destPath, buf.subarray(dataStart, dataEnd));
10244
10919
  }
10245
10920
  }
10246
10921
  offset += 512 + Math.ceil(size / 512) * 512;
@@ -10249,8 +10924,8 @@ async function extractTar(buf, destDir) {
10249
10924
 
10250
10925
  // src/skills/manifest-store.ts
10251
10926
  init_atomic_write();
10252
- import * as fs14 from "node:fs/promises";
10253
- import * as path24 from "node:path";
10927
+ import * as fs16 from "node:fs/promises";
10928
+ import * as path25 from "node:path";
10254
10929
  var SkillManifestStore = class {
10255
10930
  manifestPath;
10256
10931
  cache;
@@ -10260,7 +10935,7 @@ var SkillManifestStore = class {
10260
10935
  async read() {
10261
10936
  if (this.cache) return this.cache;
10262
10937
  try {
10263
- const raw = await fs14.readFile(this.manifestPath, "utf8");
10938
+ const raw = await fs16.readFile(this.manifestPath, "utf8");
10264
10939
  const data = JSON.parse(raw);
10265
10940
  if (!Array.isArray(data.skills)) {
10266
10941
  this.cache = { skills: [] };
@@ -10273,8 +10948,8 @@ var SkillManifestStore = class {
10273
10948
  return this.cache;
10274
10949
  }
10275
10950
  async write(data) {
10276
- const dir = path24.dirname(this.manifestPath);
10277
- await fs14.mkdir(dir, { recursive: true });
10951
+ const dir = path25.dirname(this.manifestPath);
10952
+ await fs16.mkdir(dir, { recursive: true });
10278
10953
  await atomicWrite(this.manifestPath, JSON.stringify(data, null, 2) + "\n");
10279
10954
  this.cache = data;
10280
10955
  }
@@ -10312,8 +10987,8 @@ var SkillManifestStore = class {
10312
10987
 
10313
10988
  // src/skills/skill-installer.ts
10314
10989
  function isInside(resolved, destDir) {
10315
- const root = path25.resolve(destDir);
10316
- return resolved === root || resolved.startsWith(root + path25.sep);
10990
+ const root = path26.resolve(destDir);
10991
+ return resolved === root || resolved.startsWith(root + path26.sep);
10317
10992
  }
10318
10993
  var MAX_SKILL_FILE_SIZE = SKILL_LIMITS.MAX_SKILL_FILE_SIZE;
10319
10994
  var SkillInstaller = class {
@@ -10368,13 +11043,13 @@ var SkillInstaller = class {
10368
11043
  this.opts.log?.(`Overwriting existing skill "${skill.name}" (${scope})...`);
10369
11044
  await this.removeSkillFiles(skill.name, scope);
10370
11045
  }
10371
- const destDir = path25.join(targetDir, skill.name);
10372
- await fs15.mkdir(destDir, { recursive: true });
11046
+ const destDir = path26.join(targetDir, skill.name);
11047
+ await fs17.mkdir(destDir, { recursive: true });
10373
11048
  const copiedFiles = [];
10374
11049
  for (const file of skill.files) {
10375
- const srcPath = path25.join(skill.baseDir, file);
10376
- const destPath = path25.join(destDir, file);
10377
- 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);
10378
11053
  if (!isInside(resolved2, destDir)) {
10379
11054
  throw new FsError({
10380
11055
  message: `Path traversal detected in skill file: ${file}`,
@@ -10383,17 +11058,17 @@ var SkillInstaller = class {
10383
11058
  context: { reason: "path_traversal", skillName: skill.name }
10384
11059
  });
10385
11060
  }
10386
- const stat7 = await fs15.stat(srcPath);
10387
- if (stat7.size > MAX_SKILL_FILE_SIZE) {
11061
+ const stat8 = await fs17.stat(srcPath);
11062
+ if (stat8.size > MAX_SKILL_FILE_SIZE) {
10388
11063
  throw new FsError({
10389
- 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`,
10390
11065
  code: ERROR_CODES.FS_WRITE_FAILED,
10391
11066
  path: srcPath,
10392
- 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 }
10393
11068
  });
10394
11069
  }
10395
- await fs15.mkdir(path25.dirname(destPath), { recursive: true });
10396
- await fs15.copyFile(srcPath, destPath);
11070
+ await fs17.mkdir(path26.dirname(destPath), { recursive: true });
11071
+ await fs17.copyFile(srcPath, destPath);
10397
11072
  copiedFiles.push(file);
10398
11073
  }
10399
11074
  const entry = {
@@ -10423,7 +11098,7 @@ var SkillInstaller = class {
10423
11098
  this.invalidateLoaderCache();
10424
11099
  return results;
10425
11100
  } finally {
10426
- await fs15.rm(tempDir, { recursive: true, force: true }).catch(() => {
11101
+ await fs17.rm(tempDir, { recursive: true, force: true }).catch(() => {
10427
11102
  });
10428
11103
  }
10429
11104
  }
@@ -10438,7 +11113,7 @@ var SkillInstaller = class {
10438
11113
  const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
10439
11114
  let entries;
10440
11115
  try {
10441
- entries = await fs15.readdir(srcDir, { withFileTypes: true });
11116
+ entries = await fs17.readdir(srcDir, { withFileTypes: true });
10442
11117
  } catch {
10443
11118
  throw new WrongStackError({
10444
11119
  message: `Source directory not found or not readable: ${srcDir}`,
@@ -10450,10 +11125,10 @@ var SkillInstaller = class {
10450
11125
  const results = [];
10451
11126
  for (const e of entries) {
10452
11127
  if (!await entryIsDirectory(srcDir, e)) continue;
10453
- const skillMdPath = path25.join(srcDir, e.name, "SKILL.md");
11128
+ const skillMdPath = path26.join(srcDir, e.name, "SKILL.md");
10454
11129
  let content;
10455
11130
  try {
10456
- content = await fs15.readFile(skillMdPath, "utf8");
11131
+ content = await fs17.readFile(skillMdPath, "utf8");
10457
11132
  } catch {
10458
11133
  continue;
10459
11134
  }
@@ -10463,15 +11138,15 @@ var SkillInstaller = class {
10463
11138
  if (existing.find((x) => x.scope === scope)) {
10464
11139
  await this.removeSkillFiles(fm.name, scope);
10465
11140
  }
10466
- const destDir = path25.join(targetDir, fm.name);
10467
- await fs15.mkdir(destDir, { recursive: true });
10468
- 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);
10469
11144
  const files = await collectFiles(srcSkillDir, srcSkillDir);
10470
11145
  const copiedFiles = [];
10471
11146
  for (const file of files) {
10472
- const srcPath = path25.join(srcSkillDir, file);
10473
- const destPath = path25.join(destDir, file);
10474
- 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);
10475
11150
  if (!isInside(resolved, destDir)) {
10476
11151
  throw new FsError({
10477
11152
  message: `Path traversal detected in skill file: ${file}`,
@@ -10480,16 +11155,16 @@ var SkillInstaller = class {
10480
11155
  context: { reason: "path_traversal", skillName: fm.name }
10481
11156
  });
10482
11157
  }
10483
- await fs15.mkdir(path25.dirname(destPath), { recursive: true });
11158
+ await fs17.mkdir(path26.dirname(destPath), { recursive: true });
10484
11159
  if (opts?.link) {
10485
11160
  try {
10486
- await fs15.symlink(srcPath, destPath);
11161
+ await fs17.symlink(srcPath, destPath);
10487
11162
  } catch (err) {
10488
- await fs15.copyFile(srcPath, destPath);
11163
+ await fs17.copyFile(srcPath, destPath);
10489
11164
  this.opts.log?.(`symlink failed for ${file} (${toErrorMessage(err)}); copied instead`);
10490
11165
  }
10491
11166
  } else {
10492
- await fs15.copyFile(srcPath, destPath);
11167
+ await fs17.copyFile(srcPath, destPath);
10493
11168
  }
10494
11169
  copiedFiles.push(file);
10495
11170
  }
@@ -10643,10 +11318,10 @@ var SkillInstaller = class {
10643
11318
  */
10644
11319
  async detectSkills(baseDir) {
10645
11320
  const results = [];
10646
- const rootSkillMd = path25.join(baseDir, "SKILL.md");
11321
+ const rootSkillMd = path26.join(baseDir, "SKILL.md");
10647
11322
  try {
10648
- await fs15.access(rootSkillMd);
10649
- const content = await fs15.readFile(rootSkillMd, "utf8");
11323
+ await fs17.access(rootSkillMd);
11324
+ const content = await fs17.readFile(rootSkillMd, "utf8");
10650
11325
  const fm = parseSkillFrontmatter(content);
10651
11326
  if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
10652
11327
  results.push({
@@ -10658,17 +11333,17 @@ var SkillInstaller = class {
10658
11333
  }
10659
11334
  } catch {
10660
11335
  }
10661
- const skillsDir = path25.join(baseDir, "skills");
11336
+ const skillsDir = path26.join(baseDir, "skills");
10662
11337
  try {
10663
- const entries = await fs15.readdir(skillsDir, { withFileTypes: true });
11338
+ const entries = await fs17.readdir(skillsDir, { withFileTypes: true });
10664
11339
  for (const entry of entries) {
10665
11340
  if (!entry.isDirectory()) continue;
10666
- const skillFile = path25.join(skillsDir, entry.name, "SKILL.md");
11341
+ const skillFile = path26.join(skillsDir, entry.name, "SKILL.md");
10667
11342
  try {
10668
- const content = await fs15.readFile(skillFile, "utf8");
11343
+ const content = await fs17.readFile(skillFile, "utf8");
10669
11344
  const fm = parseSkillFrontmatter(content);
10670
11345
  if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
10671
- const skillDir = path25.join(skillsDir, entry.name);
11346
+ const skillDir = path26.join(skillsDir, entry.name);
10672
11347
  const files = await collectFiles(skillDir, skillDir);
10673
11348
  results.push({
10674
11349
  name: fm.name,
@@ -10701,8 +11376,8 @@ var SkillInstaller = class {
10701
11376
  */
10702
11377
  async removeSkillFiles(name, scope) {
10703
11378
  const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
10704
- const root = path25.resolve(targetDir);
10705
- const skillDir = path25.resolve(path25.join(targetDir, name));
11379
+ const root = path26.resolve(targetDir);
11380
+ const skillDir = path26.resolve(path26.join(targetDir, name));
10706
11381
  if (skillDir === root || !isInside(skillDir, root)) {
10707
11382
  throw new FsError({
10708
11383
  message: `Refusing to delete skill files outside the skills directory: ${name}`,
@@ -10711,7 +11386,7 @@ var SkillInstaller = class {
10711
11386
  context: { reason: "path_traversal", skillName: name, scope }
10712
11387
  });
10713
11388
  }
10714
- await fs15.rm(skillDir, { recursive: true, force: true });
11389
+ await fs17.rm(skillDir, { recursive: true, force: true });
10715
11390
  }
10716
11391
  /**
10717
11392
  * Invalidate the skill loader's cache so newly installed skills appear.
@@ -10746,7 +11421,7 @@ async function entryIsDirectory(dir, entry) {
10746
11421
  if (entry.isDirectory()) return true;
10747
11422
  if (entry.isSymbolicLink()) {
10748
11423
  try {
10749
- return (await fs15.stat(path25.join(dir, entry.name))).isDirectory();
11424
+ return (await fs17.stat(path26.join(dir, entry.name))).isDirectory();
10750
11425
  } catch {
10751
11426
  return false;
10752
11427
  }
@@ -10755,10 +11430,10 @@ async function entryIsDirectory(dir, entry) {
10755
11430
  }
10756
11431
  async function collectFiles(dir, baseDir) {
10757
11432
  const results = [];
10758
- const entries = await fs15.readdir(dir, { withFileTypes: true });
11433
+ const entries = await fs17.readdir(dir, { withFileTypes: true });
10759
11434
  for (const entry of entries) {
10760
- const fullPath = path25.join(dir, entry.name);
10761
- const relPath = path25.relative(baseDir, fullPath);
11435
+ const fullPath = path26.join(dir, entry.name);
11436
+ const relPath = path26.relative(baseDir, fullPath);
10762
11437
  if (entry.isDirectory()) {
10763
11438
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
10764
11439
  results.push(...await collectFiles(fullPath, baseDir));
@@ -10837,7 +11512,7 @@ function createSkillsPlugin(opts) {
10837
11512
  function makeInstaller(skillLoader, projectRoot, registryAdapters) {
10838
11513
  const paths = resolveWstackPaths({ projectRoot });
10839
11514
  return new SkillInstaller({
10840
- manifestPath: path26.join(paths.configDir, "installed-skills.json"),
11515
+ manifestPath: path27.join(paths.configDir, "installed-skills.json"),
10841
11516
  projectSkillsDir: paths.inProjectSkills,
10842
11517
  globalSkillsDir: paths.globalSkills,
10843
11518
  projectHash: paths.projectHash,
@@ -11216,7 +11891,7 @@ function resolveImportSourceDir(tool, opts) {
11216
11891
  const entry = IMPORT_SOURCE_TOOLS.find((t) => t.id === tool);
11217
11892
  if (!entry) return void 0;
11218
11893
  const base = opts.global ? opts.homeDir ?? os7.homedir() : opts.projectRoot;
11219
- return path26.join(base, "." + entry.id, entry.subdir);
11894
+ return path27.join(base, "." + entry.id, entry.subdir);
11220
11895
  }
11221
11896
  function buildSkillImportCommand(skillLoader) {
11222
11897
  return {
@@ -11257,7 +11932,7 @@ function buildSkillImportCommand(skillLoader) {
11257
11932
  };
11258
11933
  }
11259
11934
  } else if (positional) {
11260
- srcDir = path26.resolve(ctx.projectRoot, positional);
11935
+ srcDir = path27.resolve(ctx.projectRoot, positional);
11261
11936
  } else {
11262
11937
  return {
11263
11938
  message: "Usage: /skill-import <src-dir> | --from <tool> | --from-claude [--global] [--link]"
@@ -11384,14 +12059,14 @@ function truncate(s, max) {
11384
12059
  }
11385
12060
 
11386
12061
  // src/plugins/sync-plugin.ts
11387
- import * as path28 from "node:path";
12062
+ import * as path29 from "node:path";
11388
12063
  init_error();
11389
12064
 
11390
12065
  // src/storage/cloud-sync.ts
11391
12066
  init_atomic_write();
11392
12067
  init_errors();
11393
- import * as fs16 from "node:fs/promises";
11394
- import * as path27 from "node:path";
12068
+ import * as fs18 from "node:fs/promises";
12069
+ import * as path28 from "node:path";
11395
12070
  import { createHash as createHash9 } from "node:crypto";
11396
12071
  var ALL_SYNC_CATEGORIES = ["settings", "skills", "prompts", "memory", "history"];
11397
12072
  var CloudSync = class {
@@ -11399,8 +12074,8 @@ var CloudSync = class {
11399
12074
  this.paths = paths;
11400
12075
  this.getConfig = getConfig;
11401
12076
  this.setConfig = setConfig;
11402
- this.statePath = path27.join(paths.configDir, "sync-state.json");
11403
- 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"));
11404
12079
  }
11405
12080
  paths;
11406
12081
  getConfig;
@@ -11555,7 +12230,7 @@ var CloudSync = class {
11555
12230
  }
11556
12231
  async loadState() {
11557
12232
  try {
11558
- const raw = await fs16.readFile(this.statePath, "utf8");
12233
+ const raw = await fs18.readFile(this.statePath, "utf8");
11559
12234
  this.state = JSON.parse(raw);
11560
12235
  } catch {
11561
12236
  this.state = null;
@@ -11680,18 +12355,18 @@ var CloudSync = class {
11680
12355
  const localPath = this.categoryToPath(cat);
11681
12356
  if (!localPath) continue;
11682
12357
  try {
11683
- const stat7 = await fs16.lstat(localPath);
11684
- if (stat7.isSymbolicLink()) continue;
11685
- if (stat7.isDirectory()) {
12358
+ const stat8 = await fs18.lstat(localPath);
12359
+ if (stat8.isSymbolicLink()) continue;
12360
+ if (stat8.isDirectory()) {
11686
12361
  const files = await this.walkDir(localPath, localPath);
11687
12362
  for (const file of files) {
11688
- const content = await fs16.readFile(file, "utf8");
11689
- 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, "/");
11690
12365
  entries.push({ path: `data/${cat}/${rel}`, content, mode: "100644" });
11691
12366
  hashes.push(`${cat}/${rel}\0${content}`);
11692
12367
  }
11693
12368
  } else {
11694
- const content = await fs16.readFile(localPath, "utf8");
12369
+ const content = await fs18.readFile(localPath, "utf8");
11695
12370
  entries.push({ path: `data/${cat}`, content, mode: "100644" });
11696
12371
  hashes.push(`${cat}\0${content}`);
11697
12372
  }
@@ -11707,17 +12382,17 @@ var CloudSync = class {
11707
12382
  const localPath = this.categoryToPath(cat);
11708
12383
  if (!localPath) continue;
11709
12384
  try {
11710
- const stat7 = await fs16.lstat(localPath);
11711
- if (stat7.isSymbolicLink()) continue;
11712
- if (stat7.isDirectory()) {
12385
+ const stat8 = await fs18.lstat(localPath);
12386
+ if (stat8.isSymbolicLink()) continue;
12387
+ if (stat8.isDirectory()) {
11713
12388
  const files = await this.walkDir(localPath, localPath);
11714
12389
  for (const file of files) {
11715
- const content = await fs16.readFile(file, "utf8");
11716
- 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, "/");
11717
12392
  hashes.push(`${cat}/${rel}\0${content}`);
11718
12393
  }
11719
12394
  } else {
11720
- const content = await fs16.readFile(localPath, "utf8");
12395
+ const content = await fs18.readFile(localPath, "utf8");
11721
12396
  hashes.push(`${cat}\0${content}`);
11722
12397
  }
11723
12398
  } catch {
@@ -11744,10 +12419,10 @@ var CloudSync = class {
11744
12419
  }
11745
12420
  async walkDir(dir, base) {
11746
12421
  const results = [];
11747
- const entries = await fs16.readdir(dir, { withFileTypes: true });
12422
+ const entries = await fs18.readdir(dir, { withFileTypes: true });
11748
12423
  entries.sort((a, b) => a.name.localeCompare(b.name));
11749
12424
  for (const entry of entries) {
11750
- const full = path27.join(dir, entry.name);
12425
+ const full = path28.join(dir, entry.name);
11751
12426
  if (entry.isSymbolicLink()) continue;
11752
12427
  if (entry.isDirectory()) {
11753
12428
  results.push(...await this.walkDir(full, base));
@@ -11760,29 +12435,29 @@ var CloudSync = class {
11760
12435
  };
11761
12436
  async function preparePulledDestination(cat, localPath, destPath, remotePath) {
11762
12437
  const directoryBacked = cat === "skills" || cat === "prompts";
11763
- const rootPath = directoryBacked ? localPath : path27.dirname(localPath);
12438
+ const rootPath = directoryBacked ? localPath : path28.dirname(localPath);
11764
12439
  const rootStat = await lstatIfExists(rootPath);
11765
12440
  if (rootStat?.isSymbolicLink()) {
11766
12441
  throw unsafePulledSymlinkError(remotePath, rootPath);
11767
12442
  }
11768
12443
  if (directoryBacked) {
11769
- const relativeParent = path27.relative(rootPath, path27.dirname(destPath));
12444
+ const relativeParent = path28.relative(rootPath, path28.dirname(destPath));
11770
12445
  let cursor = rootPath;
11771
12446
  if (relativeParent) {
11772
- for (const segment of relativeParent.split(path27.sep)) {
11773
- cursor = path27.join(cursor, segment);
11774
- const stat7 = await lstatIfExists(cursor);
11775
- 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);
11776
12451
  }
11777
12452
  }
11778
12453
  }
11779
12454
  const destStat = await lstatIfExists(destPath);
11780
12455
  if (destStat?.isSymbolicLink()) throw unsafePulledSymlinkError(remotePath, destPath);
11781
- await fs16.mkdir(path27.dirname(destPath), { recursive: true });
12456
+ await fs18.mkdir(path28.dirname(destPath), { recursive: true });
11782
12457
  }
11783
12458
  async function lstatIfExists(filePath) {
11784
12459
  try {
11785
- return await fs16.lstat(filePath);
12460
+ return await fs18.lstat(filePath);
11786
12461
  } catch (err) {
11787
12462
  if (err.code === "ENOENT") return null;
11788
12463
  throw err;
@@ -11808,9 +12483,9 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
11808
12483
  return localPath;
11809
12484
  }
11810
12485
  if (!rel) return localPath;
11811
- const normalizedRel = path27.normalize(rel);
11812
- const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path27.sep}`);
11813
- 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) {
11814
12489
  throw new FsError({
11815
12490
  message: `Refusing CloudSync path traversal: ${remotePath}`,
11816
12491
  code: ERROR_CODES.FS_DELETE_FAILED,
@@ -11818,10 +12493,10 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
11818
12493
  context: { reason: "path_traversal", normalizedRel }
11819
12494
  });
11820
12495
  }
11821
- const dest = path27.resolve(localPath, normalizedRel);
11822
- const root = path27.resolve(localPath);
11823
- const relative5 = path27.relative(root, dest);
11824
- 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)) {
11825
12500
  throw new FsError({
11826
12501
  message: `Refusing CloudSync path outside category root: ${remotePath}`,
11827
12502
  code: ERROR_CODES.FS_DELETE_FAILED,
@@ -11864,7 +12539,7 @@ function createSyncPlugin(opts) {
11864
12539
  api.log.warn("[sync] paths, configStore, or configDir not available \u2014 /sync disabled");
11865
12540
  return;
11866
12541
  }
11867
- const syncConfigPath = paths.syncConfig ?? path28.join(paths.configDir, "sync.json");
12542
+ const syncConfigPath = paths.syncConfig ?? path29.join(paths.configDir, "sync.json");
11868
12543
  cloud = new CloudSync(
11869
12544
  paths,
11870
12545
  () => {
@@ -11875,7 +12550,7 @@ function createSyncPlugin(opts) {
11875
12550
  await persistSyncConfig(syncConfigPath, cfg, vault);
11876
12551
  configStore?.update({ sync: cfg });
11877
12552
  },
11878
- () => path28.join(paths.configDir, "config.json")
12553
+ () => path29.join(paths.configDir, "config.json")
11879
12554
  );
11880
12555
  void cloud.loadState();
11881
12556
  api.slashCommands.register(buildSyncCommand(cloud, configStore, vault, syncConfigPath));
@@ -12065,6 +12740,852 @@ Warning: sync completed, but metadata persistence failed: ${toErrorMessage(err)}
12065
12740
  }
12066
12741
  };
12067
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
+ }
12068
13589
  export {
12069
13590
  CHIMERA_REVIEW_PROMPT,
12070
13591
  DefaultPluginAPI,
@@ -12072,6 +13593,7 @@ export {
12072
13593
  buildReviewerModelPool,
12073
13594
  createAutoReviewPlugin,
12074
13595
  createChimeraPlugin,
13596
+ createCloudConfigSyncPlugin,
12075
13597
  createPromptsPlugin,
12076
13598
  createSkillsPlugin,
12077
13599
  createSyncPlugin,