@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
@@ -854,6 +854,9 @@ function canonicalProjectRoot(absRoot) {
854
854
  const commonDirFile = path2.join(gitDir, "commondir");
855
855
  if (!fs2.statSync(commonDirFile).isFile()) return checkoutRoot;
856
856
  const commonDir = path2.resolve(gitDir, fs2.readFileSync(commonDirFile, "utf8").trim());
857
+ const worktreesDir = path2.dirname(gitDir);
858
+ if (path2.basename(worktreesDir).toLowerCase() !== "worktrees") return checkoutRoot;
859
+ if (path2.resolve(worktreesDir, "..") !== commonDir) return checkoutRoot;
857
860
  if (path2.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
858
861
  return path2.dirname(commonDir);
859
862
  } catch {
@@ -876,7 +879,7 @@ function safeProfileName(name) {
876
879
  const safe = (name ?? "").replace(/[/\\:]/g, "_").replace(/\.\./g, "_").trim();
877
880
  return safe || "default";
878
881
  }
879
- function activeProfileName(globalRoot) {
882
+ function bootstrapProfileName(globalRoot) {
880
883
  try {
881
884
  const parsed = JSON.parse(fs2.readFileSync(path2.join(globalRoot, "config.json"), "utf8"));
882
885
  return safeProfileName(
@@ -894,7 +897,7 @@ function wstackGlobalRoot() {
894
897
  function resolveWstackPaths(opts) {
895
898
  const globalRoot = opts.globalRoot ?? (opts.userHome ? path2.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
896
899
  const homeDir = opts.userHome ?? os.homedir();
897
- const profileName = safeProfileName(opts.profileName ?? activeProfileName(globalRoot));
900
+ const profileName = safeProfileName(opts.profileName ?? bootstrapProfileName(globalRoot));
898
901
  const profileDir = path2.join(globalRoot, "profiles", profileName);
899
902
  const hash = projectHash(opts.projectRoot);
900
903
  const slug = projectSlug(opts.projectRoot);
@@ -964,7 +967,12 @@ function resolveWstackPaths(opts) {
964
967
  projectRequirementIntakes: path2.join(projectDir, "requirement-intakes"),
965
968
  syncConfig: path2.join(profileDir, "sync.json"),
966
969
  configHistoryDir: path2.join(globalRoot, "config-history"),
967
- projectStatus: (projectHash2) => path2.join(globalRoot, "projects", projectHash2, "status.json")
970
+ projectStatus: (statusProjectSlug) => {
971
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,39})-[a-f0-9]{6}$/.test(statusProjectSlug)) {
972
+ throw new Error(`Invalid project slug: ${statusProjectSlug}`);
973
+ }
974
+ return path2.join(globalRoot, "projects", statusProjectSlug, "status.json");
975
+ }
968
976
  };
969
977
  }
970
978
 
@@ -1056,13 +1064,93 @@ function ulid(seedTime = Date.now()) {
1056
1064
  }
1057
1065
 
1058
1066
  // src/utils/regex-guard.ts
1059
- var MAX_PATTERN_LEN = 512;
1067
+ var MAX_PATTERN_LEN = 256;
1060
1068
  var DANGEROUS_PATTERNS = [
1069
+ // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier
1061
1070
  /(\([^)]*[+*][^)]*\))[+*]/,
1062
- // (a+)+, (.*)+, etc
1063
- /(\(\?:[^)]*[+*][^)]*\))[+*]/
1064
- // same, with non-capturing group
1071
+ /(\(\?:[^)]*[+*][^)]*\))[+*]/,
1072
+ // Adjacent quantifiers: a++ a*+
1073
+ /[+*]{2,}/,
1074
+ // Quantifier on alternation with length 2+
1075
+ /\([^|)]+\|[^)]+\)[+*][+*]/,
1076
+ // Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
1077
+ /[([][^)\]]*[+*][^)\]]*[)\]][^)]*\?\??/
1065
1078
  ];
1079
+ function hasAmbiguousQuantifiedAlternation(pattern) {
1080
+ for (let i = 0; i < pattern.length; i++) {
1081
+ if (pattern[i] !== "(") continue;
1082
+ if (i > 0 && pattern[i - 1] === "\\") continue;
1083
+ let depth = 0;
1084
+ let inClass = false;
1085
+ let j = i;
1086
+ for (; j < pattern.length; j++) {
1087
+ const ch = pattern[j];
1088
+ if (ch === "\\") {
1089
+ j++;
1090
+ continue;
1091
+ }
1092
+ if (inClass) {
1093
+ if (ch === "]") inClass = false;
1094
+ continue;
1095
+ }
1096
+ if (ch === "[") {
1097
+ inClass = true;
1098
+ continue;
1099
+ }
1100
+ if (ch === "(") depth++;
1101
+ else if (ch === ")") {
1102
+ depth--;
1103
+ if (depth === 0) break;
1104
+ }
1105
+ }
1106
+ if (j >= pattern.length) return false;
1107
+ const next = pattern[j + 1];
1108
+ if (next !== "+" && next !== "*" && next !== "{") continue;
1109
+ let inner = pattern.slice(i + 1, j);
1110
+ inner = inner.replace(/^\?(?::|<?[=!])/u, "");
1111
+ const branches = [];
1112
+ let current = "";
1113
+ let d = 0;
1114
+ let cls = false;
1115
+ for (let k = 0; k < inner.length; k++) {
1116
+ const ch = inner[k];
1117
+ if (ch === "\\") {
1118
+ current += ch + (inner[k + 1] ?? "");
1119
+ k++;
1120
+ continue;
1121
+ }
1122
+ if (cls) {
1123
+ if (ch === "]") cls = false;
1124
+ current += ch;
1125
+ continue;
1126
+ }
1127
+ if (ch === "[") {
1128
+ cls = true;
1129
+ current += ch;
1130
+ continue;
1131
+ }
1132
+ if (ch === "(") d++;
1133
+ if (ch === ")") d--;
1134
+ if (ch === "|" && d === 0) {
1135
+ branches.push(current);
1136
+ current = "";
1137
+ continue;
1138
+ }
1139
+ current += ch;
1140
+ }
1141
+ branches.push(current);
1142
+ if (branches.length < 2) continue;
1143
+ for (let a = 0; a < branches.length; a++) {
1144
+ for (let b = a + 1; b < branches.length; b++) {
1145
+ const x = branches[a];
1146
+ const y = branches[b];
1147
+ if (x === "" || y === "") return true;
1148
+ if (x === y || x.startsWith(y) || y.startsWith(x)) return true;
1149
+ }
1150
+ }
1151
+ }
1152
+ return false;
1153
+ }
1066
1154
  function compileUserRegex(pattern, flags) {
1067
1155
  if (typeof pattern !== "string") {
1068
1156
  return { ok: false, reason: "pattern must be a string" };
@@ -1081,6 +1169,12 @@ function compileUserRegex(pattern, flags) {
1081
1169
  };
1082
1170
  }
1083
1171
  }
1172
+ if (hasAmbiguousQuantifiedAlternation(pattern)) {
1173
+ return {
1174
+ ok: false,
1175
+ reason: "pattern quantifies an alternation with overlapping branches \u2014 rewrite so no two branches can match the same text"
1176
+ };
1177
+ }
1084
1178
  try {
1085
1179
  return { ok: true, regex: new RegExp(pattern, flags) };
1086
1180
  } catch (err) {
@@ -1090,6 +1184,7 @@ function compileUserRegex(pattern, flags) {
1090
1184
  };
1091
1185
  }
1092
1186
  }
1187
+ var MAX_SUBJECT_LEN = 64 * 1024;
1093
1188
 
1094
1189
  // src/utils/safe-json.ts
1095
1190
  function safeParse(input, maxBytes = 5e6) {
@@ -2731,8 +2826,8 @@ function scrubPersistedSessionEvent(event, scrubber) {
2731
2826
  function scrubPersistedSessionData(data, scrubber) {
2732
2827
  return scrubber.scrubObject(data);
2733
2828
  }
2734
- function scrubPersistedSessionSummary(summary, scrubber) {
2735
- const scrubbed = scrubber.scrubObject(summary);
2829
+ function scrubPersistedSessionSummary(summary2, scrubber) {
2830
+ const scrubbed = scrubber.scrubObject(summary2);
2736
2831
  const { name: _name, lastUserMessage: _lastUserMessage, ...rest } = scrubbed;
2737
2832
  const title = sessionContentText(scrubbed.title) || "(empty session)";
2738
2833
  const name = scrubbed.name === void 0 ? "" : sessionContentText(scrubbed.name);
@@ -3639,15 +3734,15 @@ function compareSessionSummaries(a, b) {
3639
3734
  if (a.startedAt > b.startedAt) return -1;
3640
3735
  return a.id.localeCompare(b.id);
3641
3736
  }
3642
- function matchesSessionFilter(summary, criteria) {
3643
- if (criteria.since && summary.startedAt < criteria.since) return false;
3644
- if (criteria.until && summary.startedAt > criteria.until) return false;
3645
- if (criteria.provider && summary.provider !== criteria.provider) return false;
3646
- if (criteria.model && summary.model !== criteria.model) return false;
3647
- if (criteria.minTokens !== void 0 && summary.tokenTotal < criteria.minTokens) return false;
3737
+ function matchesSessionFilter(summary2, criteria) {
3738
+ if (criteria.since && summary2.startedAt < criteria.since) return false;
3739
+ if (criteria.until && summary2.startedAt > criteria.until) return false;
3740
+ if (criteria.provider && summary2.provider !== criteria.provider) return false;
3741
+ if (criteria.model && summary2.model !== criteria.model) return false;
3742
+ if (criteria.minTokens !== void 0 && summary2.tokenTotal < criteria.minTokens) return false;
3648
3743
  if (criteria.titleContains) {
3649
3744
  const needle = criteria.titleContains.toLocaleLowerCase();
3650
- if (!summary.title.toLocaleLowerCase().includes(needle)) return false;
3745
+ if (!summary2.title.toLocaleLowerCase().includes(needle)) return false;
3651
3746
  }
3652
3747
  return true;
3653
3748
  }
@@ -3670,15 +3765,15 @@ async function readOrBuildShardManifestEntry(opts) {
3670
3765
  async (ref) => {
3671
3766
  const manifest = await opts.readSummaryManifest(ref.id);
3672
3767
  if (manifest) return { summary: manifest, needsBackfill: false };
3673
- const summary = await opts.summaryHeaderFor(ref);
3674
- if (!summary) return null;
3675
- const hydrated = await opts.summaryFor(summary.id).catch(() => summary);
3768
+ const summary2 = await opts.summaryHeaderFor(ref);
3769
+ if (!summary2) return null;
3770
+ const hydrated = await opts.summaryFor(summary2.id).catch(() => summary2);
3676
3771
  return { summary: hydrated, needsBackfill: false };
3677
3772
  }
3678
3773
  );
3679
3774
  const summaries = candidates.filter((candidate) => candidate !== null).map((candidate) => candidate.summary);
3680
3775
  summaries.sort(compareSessionSummaries);
3681
- const entry = { summaries, ids: summaries.map((summary) => summary.id) };
3776
+ const entry = { summaries, ids: summaries.map((summary2) => summary2.id) };
3682
3777
  await atomicWrite(opts.manifestPath, JSON.stringify(entry), { mode: 384 }).catch(
3683
3778
  () => void 0
3684
3779
  );
@@ -3925,7 +4020,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
3925
4020
  }
3926
4021
  }
3927
4022
  scrubSummaries(summaries) {
3928
- return summaries.map((summary) => scrubPersistedSessionSummary(summary, this.secretScrubber));
4023
+ return summaries.map((summary2) => scrubPersistedSessionSummary(summary2, this.secretScrubber));
3929
4024
  }
3930
4025
  /**
3931
4026
  * Clear the load() cache. Useful for testing or when the caller knows
@@ -4304,12 +4399,12 @@ var DefaultSessionStore = class _DefaultSessionStore {
4304
4399
  indexAppendCount = 0;
4305
4400
  static COMPACT_EVERY = 30;
4306
4401
  /** Append a session summary to the index, propagating persistence failures. */
4307
- async appendToIndexStrict(summary) {
4402
+ async appendToIndexStrict(summary2) {
4308
4403
  await ensureDir(this.dir);
4309
4404
  let shouldCompact = false;
4310
4405
  await withFileLock(this.indexFile, async () => {
4311
- await this.invalidateShardManifestBySessionId(summary.id);
4312
- const line = JSON.stringify(summary) + "\n";
4406
+ await this.invalidateShardManifestBySessionId(summary2.id);
4407
+ const line = JSON.stringify(summary2) + "\n";
4313
4408
  await fsp12.appendFile(this.indexFile, line, { encoding: "utf8", mode: SECRET_FILE_MODE });
4314
4409
  this._indexCache = null;
4315
4410
  this.indexAppendCount++;
@@ -4323,8 +4418,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
4323
4418
  }
4324
4419
  }
4325
4420
  /** Best-effort index append used by writer close. */
4326
- async appendToIndex(summary) {
4327
- await this.appendToIndexStrict(summary).catch(() => {
4421
+ async appendToIndex(summary2) {
4422
+ await this.appendToIndexStrict(summary2).catch(() => {
4328
4423
  });
4329
4424
  }
4330
4425
  /** Append a tombstone entry for a deleted session. */
@@ -4457,8 +4552,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
4457
4552
  );
4458
4553
  const out = [];
4459
4554
  for (const entry of shardEntries) {
4460
- for (const summary of entry.summaries) {
4461
- out.push({ summary, needsBackfill: false });
4555
+ for (const summary2 of entry.summaries) {
4556
+ out.push({ summary: summary2, needsBackfill: false });
4462
4557
  }
4463
4558
  }
4464
4559
  out.sort((a, b) => compareSessionSummaries(a.summary, b.summary));
@@ -4529,8 +4624,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
4529
4624
  try {
4530
4625
  const full = this.sessionPath(id, ".jsonl");
4531
4626
  const stat15 = await fsp12.stat(full);
4532
- const summary = await this.summarize(id, stat15.mtime.toISOString());
4533
- await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
4627
+ const summary2 = await this.summarize(id, stat15.mtime.toISOString());
4628
+ await atomicWrite(manifest, JSON.stringify(summary2), { mode: 384 }).catch((err) => {
4534
4629
  const msg = toErrorMessage(err);
4535
4630
  emitSessionStoreError(this.events, id, manifest, "summary_fallback", msg, true);
4536
4631
  this.logWarn("Session manifest write failed", {
@@ -4550,7 +4645,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
4550
4645
  Date.now() - t0,
4551
4646
  errorMsg
4552
4647
  );
4553
- return summary;
4648
+ return summary2;
4554
4649
  } catch (err) {
4555
4650
  outcome = "failure";
4556
4651
  errorMsg = toErrorMessage(err);
@@ -4623,14 +4718,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
4623
4718
  let updated;
4624
4719
  try {
4625
4720
  updated = await withFileLock(manifest, async () => {
4626
- const summary = await this.readSummaryManifest(id) ?? await this.summaryFor(id);
4627
- const { name: _drop, ...rest } = summary;
4721
+ const summary2 = await this.readSummaryManifest(id) ?? await this.summaryFor(id);
4722
+ const { name: _drop, ...rest } = summary2;
4628
4723
  const next = trimmed ? { ...rest, name: trimmed } : rest;
4629
4724
  await atomicWrite(manifest, JSON.stringify(next), { mode: 384 });
4630
4725
  try {
4631
4726
  await this.appendToIndexStrict(next);
4632
4727
  } catch (err) {
4633
- await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 });
4728
+ await atomicWrite(manifest, JSON.stringify(summary2), { mode: 384 });
4634
4729
  throw err;
4635
4730
  }
4636
4731
  return next;
@@ -6607,7 +6702,7 @@ var CONFIG_BEHAVIOR_DEFAULTS = {
6607
6702
  enabled: false
6608
6703
  }
6609
6704
  },
6610
- skills: { readClaudeSkills: true },
6705
+ skills: { readClaudeSkills: true, mode: "progressive" },
6611
6706
  mcpServers: {},
6612
6707
  fallbackAuto: true,
6613
6708
  maxConcurrent: 4,
@@ -6707,19 +6802,19 @@ function repairConfigDefaults(input) {
6707
6802
  });
6708
6803
  }
6709
6804
  for (const [key, defaultValue] of Object.entries(defaults)) {
6710
- const path30 = prefix ? `${prefix}.${key}` : key;
6805
+ const path31 = prefix ? `${prefix}.${key}` : key;
6711
6806
  if (!Object.hasOwn(target, key)) {
6712
6807
  target[key] = cloneJsonValue(defaultValue);
6713
- changes.push({ path: path30, action: "added" });
6808
+ changes.push({ path: path31, action: "added" });
6714
6809
  continue;
6715
6810
  }
6716
6811
  const current = target[key];
6717
6812
  if (isPlainRecord(defaultValue)) {
6718
6813
  if (isPlainRecord(current)) {
6719
- repair(current, defaultValue, path30, depth + 1);
6814
+ repair(current, defaultValue, path31, depth + 1);
6720
6815
  } else {
6721
6816
  target[key] = cloneJsonValue(defaultValue);
6722
- changes.push({ path: path30, action: "replaced" });
6817
+ changes.push({ path: path31, action: "replaced" });
6723
6818
  }
6724
6819
  continue;
6725
6820
  }
@@ -6727,7 +6822,7 @@ function repairConfigDefaults(input) {
6727
6822
  const compatible = defaultIsArray ? Array.isArray(current) : !Array.isArray(current) && typeof current === typeof defaultValue;
6728
6823
  if (!compatible) {
6729
6824
  target[key] = cloneJsonValue(defaultValue);
6730
- changes.push({ path: path30, action: "replaced" });
6825
+ changes.push({ path: path31, action: "replaced" });
6731
6826
  }
6732
6827
  }
6733
6828
  };
@@ -6849,6 +6944,10 @@ var KNOWN_DENIED_IN_PROJECT = [
6849
6944
  reason: "Controls which plugin boot states the LLM may mutate; user-owned trust policy."
6850
6945
  },
6851
6946
  { key: "sync", reason: "Carries githubToken credential and target repo." },
6947
+ {
6948
+ key: "cloudSync",
6949
+ reason: "Carries the my.wrongstack.com machine token credential and endpoint URL."
6950
+ },
6852
6951
  { key: "yolo", reason: "Disables all permission confirmation prompts." },
6853
6952
  { key: "extensions", reason: "Per-plugin config can carry command/credential fields." },
6854
6953
  { key: "hq", reason: "Carries HQ client token credential and endpoint URL." },
@@ -6911,6 +7010,7 @@ var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
6911
7010
  "modelRuntime",
6912
7011
  "hq",
6913
7012
  "sync",
7013
+ "cloudSync",
6914
7014
  "extensions",
6915
7015
  "acp",
6916
7016
  "fleet",
@@ -6967,8 +7067,8 @@ var IN_PROJECT_DENIED_PATHS = [
6967
7067
  reason: "The other half of the filesystem confinement switch."
6968
7068
  }
6969
7069
  ];
6970
- function deleteNestedPath(target, path30) {
6971
- const segments = path30.split(".");
7070
+ function deleteNestedPath(target, path31) {
7071
+ const segments = path31.split(".");
6972
7072
  const last = segments[segments.length - 1];
6973
7073
  if (last === void 0) return false;
6974
7074
  let cursor = target;
@@ -7025,16 +7125,16 @@ function assertInProjectAllowListComplete() {
7025
7125
  );
7026
7126
  }
7027
7127
  const orphanedPaths = IN_PROJECT_DENIED_PATHS.filter(
7028
- ({ path: path30 }) => !IN_PROJECT_ALLOWED_KEYS.has(path30.split(".")[0] ?? "")
7029
- ).map(({ path: path30 }) => path30);
7128
+ ({ path: path31 }) => !IN_PROJECT_ALLOWED_KEYS.has(path31.split(".")[0] ?? "")
7129
+ ).map(({ path: path31 }) => path31);
7030
7130
  if (orphanedPaths.length > 0) {
7031
7131
  problems.push(
7032
7132
  `IN_PROJECT_DENIED_PATHS entr(ies) whose top-level parent is not allowed: ` + orphanedPaths.join(", ") + ". The parent is already stripped wholesale, so the nested denial is dead \u2014 remove it."
7033
7133
  );
7034
7134
  }
7035
7135
  const malformedPaths = IN_PROJECT_DENIED_PATHS.filter(
7036
- ({ path: path30 }) => path30.split(".").length < 2 || path30.split(".").some((s) => s.length === 0)
7037
- ).map(({ path: path30 }) => path30);
7136
+ ({ path: path31 }) => path31.split(".").length < 2 || path31.split(".").some((s) => s.length === 0)
7137
+ ).map(({ path: path31 }) => path31);
7038
7138
  if (malformedPaths.length > 0) {
7039
7139
  problems.push(
7040
7140
  `IN_PROJECT_DENIED_PATHS entr(ies) are not dotted nested paths: ` + malformedPaths.join(", ") + ". Top-level keys belong in KNOWN_DENIED_IN_PROJECT instead."
@@ -7062,8 +7162,8 @@ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => conso
7062
7162
  }
7063
7163
  stripped.push(k);
7064
7164
  }
7065
- for (const { path: path30 } of IN_PROJECT_DENIED_PATHS) {
7066
- if (deleteNestedPath(out, path30)) stripped.push(path30);
7165
+ for (const { path: path31 } of IN_PROJECT_DENIED_PATHS) {
7166
+ if (deleteNestedPath(out, path31)) stripped.push(path31);
7067
7167
  }
7068
7168
  if (stripped.length > 0) {
7069
7169
  warn(
@@ -9618,30 +9718,86 @@ import * as path24 from "node:path";
9618
9718
  // src/session-registry-atomic-file.ts
9619
9719
  import { randomUUID as randomUUID5 } from "node:crypto";
9620
9720
  import * as fs11 from "node:fs/promises";
9721
+ import { hostname as hostname2 } from "node:os";
9621
9722
  import * as path23 from "node:path";
9622
9723
  var STALE_LOCK_MS = 1e4;
9724
+ var SAME_HOST_STALE_MS = 2 * STALE_LOCK_MS;
9623
9725
  var STALE_TMP_MS = 6e4;
9624
9726
  var MAX_STALE_TMP_FILES = 20;
9727
+ var HOST_NAME = hostname2();
9728
+ function lockOwnerStamp() {
9729
+ return `${HOST_NAME}:${process.pid}`;
9730
+ }
9731
+ async function maybeUnlinkOwnedLock(lockPath) {
9732
+ const owner = await fs11.readFile(lockPath, "utf8").catch(() => null);
9733
+ if (owner === null || owner.trim() !== lockOwnerStamp()) return;
9734
+ for (let attempt = 0; attempt < 3; attempt++) {
9735
+ try {
9736
+ await fs11.unlink(lockPath);
9737
+ return;
9738
+ } catch {
9739
+ if (attempt < 2) await new Promise((resolve11) => setTimeout(resolve11, 5));
9740
+ }
9741
+ }
9742
+ }
9625
9743
  async function breakStaleLock(lockPath) {
9626
9744
  try {
9627
- const [stat15, content] = await Promise.all([
9628
- fs11.stat(lockPath),
9629
- /* v8 ignore start -- best-effort lock-content read; .catch only fires if the lock vanished */
9630
- fs11.readFile(lockPath, "utf8").catch(() => "")
9631
- /* v8 ignore stop */
9632
- ]);
9633
- const ageMs = Date.now() - stat15.mtimeMs;
9634
- const ownerPid = Number.parseInt(content.trim(), 10);
9635
- const ownerDead = Number.isInteger(ownerPid) && ownerPid > 0 && ownerPid !== process.pid && !isPidAlive(ownerPid);
9636
- if (ownerDead || ageMs > STALE_LOCK_MS) {
9637
- await fs11.unlink(lockPath).catch(() => void 0);
9638
- return true;
9745
+ const content = await fs11.readFile(lockPath, "utf8").catch(() => "");
9746
+ const trimmed = content.trim();
9747
+ const colonIdx = trimmed.indexOf(":");
9748
+ if (colonIdx === -1) {
9749
+ const barePid = Number.parseInt(trimmed, 10);
9750
+ if (Number.isInteger(barePid) && barePid > 0 && !isPidAlive(barePid)) {
9751
+ return await breakStaleLockVerified(lockPath, async () => {
9752
+ const reread = await fs11.readFile(lockPath, "utf8").catch(() => "");
9753
+ return reread.trim() === trimmed;
9754
+ });
9755
+ }
9756
+ return await breakStaleLockVerified(lockPath, async () => {
9757
+ const st = await fs11.stat(lockPath);
9758
+ return Date.now() - st.mtimeMs > STALE_LOCK_MS;
9759
+ });
9639
9760
  }
9640
- return false;
9761
+ const ownerHost = trimmed.slice(0, colonIdx);
9762
+ const ownerPid = Number.parseInt(trimmed.slice(colonIdx + 1), 10);
9763
+ if (ownerHost === HOST_NAME && Number.isInteger(ownerPid) && ownerPid > 0) {
9764
+ if (isPidAlive(ownerPid)) {
9765
+ const stat15 = await fs11.stat(lockPath);
9766
+ if (Date.now() - stat15.mtimeMs > SAME_HOST_STALE_MS) {
9767
+ return await breakStaleLockVerified(lockPath, async () => {
9768
+ const st = await fs11.stat(lockPath);
9769
+ return Date.now() - st.mtimeMs > SAME_HOST_STALE_MS;
9770
+ });
9771
+ }
9772
+ return false;
9773
+ }
9774
+ return await breakStaleLockVerified(lockPath, async () => {
9775
+ const reread = await fs11.readFile(lockPath, "utf8").catch(() => "");
9776
+ return reread.trim() === trimmed;
9777
+ });
9778
+ }
9779
+ return await breakStaleLockVerified(lockPath, async () => {
9780
+ const st = await fs11.stat(lockPath);
9781
+ return Date.now() - st.mtimeMs > STALE_LOCK_MS;
9782
+ });
9641
9783
  } catch {
9642
9784
  return true;
9643
9785
  }
9644
9786
  }
9787
+ async function breakStaleLockVerified(lockPath, verify) {
9788
+ if (!await verify()) return false;
9789
+ return await breakLockAtomically(lockPath) === true;
9790
+ }
9791
+ async function breakLockAtomically(lockPath) {
9792
+ const tombstone = `${lockPath}.stale-${randomUUID5()}.tmp`;
9793
+ try {
9794
+ await fs11.rename(lockPath, tombstone);
9795
+ } catch {
9796
+ return false;
9797
+ }
9798
+ void fs11.unlink(tombstone).catch(() => void 0);
9799
+ return true;
9800
+ }
9645
9801
  async function writeAtomicFile(filePath, registry) {
9646
9802
  const tmp = path23.join(
9647
9803
  path23.dirname(filePath),
@@ -10102,16 +10258,33 @@ var SessionRegistry = class {
10102
10258
  continue;
10103
10259
  }
10104
10260
  }
10261
+ let stampFailed = false;
10105
10262
  try {
10106
- await lockHandle.writeFile(String(process.pid)).catch(() => void 0);
10263
+ let stamped = false;
10264
+ for (let stampAttempt = 0; stampAttempt < 3 && !stamped; stampAttempt++) {
10265
+ try {
10266
+ await lockHandle.writeFile(lockOwnerStamp());
10267
+ stamped = true;
10268
+ } catch {
10269
+ if (stampAttempt < 2) await new Promise((resolve11) => setTimeout(resolve11, 5));
10270
+ }
10271
+ }
10272
+ if (!stamped) {
10273
+ stampFailed = true;
10274
+ throw new Error("failed to stamp session-registry lock owner");
10275
+ }
10107
10276
  const raw = await fs12.readFile(this.filePath, "utf8").catch(() => "{}");
10108
10277
  const registry = parseRegistry(raw);
10109
10278
  fn(registry);
10110
10279
  await this.writeAtomicWithRetry(registry);
10111
10280
  return;
10112
10281
  } finally {
10113
- await lockHandle.close();
10114
- await fs12.unlink(lockPath).catch(() => void 0);
10282
+ await lockHandle.close().catch(() => void 0);
10283
+ if (stampFailed) {
10284
+ await fs12.unlink(lockPath).catch(() => void 0);
10285
+ } else {
10286
+ await maybeUnlinkOwnedLock(lockPath);
10287
+ }
10115
10288
  }
10116
10289
  } catch (err) {
10117
10290
  if (err instanceof SessionOwnershipConflictError) throw err;
@@ -11915,7 +12088,7 @@ async function mutateTasks(filePath, sessionId, fn, events, traceId) {
11915
12088
 
11916
12089
  // src/storage/director-state.ts
11917
12090
  import * as fsp21 from "node:fs/promises";
11918
- import { hostname as hostname2 } from "node:os";
12091
+ import { hostname as hostname3 } from "node:os";
11919
12092
  async function loadDirectorState(filePath) {
11920
12093
  let raw;
11921
12094
  try {
@@ -11946,7 +12119,7 @@ async function acquireDirectorStateLock(lockPath, processId = process.pid) {
11946
12119
  }
11947
12120
  const lock = {
11948
12121
  pid: processId,
11949
- hostname: hostname2(),
12122
+ hostname: hostname3(),
11950
12123
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
11951
12124
  };
11952
12125
  await atomicWrite(lockPath, JSON.stringify(lock), { mode: 384 });
@@ -13529,6 +13702,613 @@ function timeAgo(iso) {
13529
13702
  return `${days}d ago`;
13530
13703
  }
13531
13704
 
13705
+ // src/storage/cloud-config-sync.ts
13706
+ import { createHash as createHash9, randomUUID as nodeRandomUUID } from "node:crypto";
13707
+ import * as fs18 from "node:fs/promises";
13708
+ import * as path30 from "node:path";
13709
+
13710
+ // src/storage/cloud-config-sync/sanitize.ts
13711
+ var SAGE_TREE = {
13712
+ enabled: true,
13713
+ storage: { projectLocal: true, directory: true },
13714
+ inject: {
13715
+ turnContext: true,
13716
+ toolResults: true,
13717
+ taskAware: true,
13718
+ maxHintsPerTool: true,
13719
+ maxCharsPerTool: true,
13720
+ minScore: true,
13721
+ repeatCooldownMs: true,
13722
+ maxTurnMemories: true,
13723
+ maxCharsPerTurn: true,
13724
+ minImportance: true,
13725
+ relationFloor: true
13726
+ },
13727
+ hygiene: {
13728
+ autoAfterSession: true,
13729
+ autoOnFileChange: true,
13730
+ retentionDays: true,
13731
+ archiveLowConfidenceAfterDays: true
13732
+ },
13733
+ embeddings: { enabled: true }
13734
+ };
13735
+ var CORE_RUNTIME_TREE = {
13736
+ maxConcurrent: true,
13737
+ context: {
13738
+ mode: true,
13739
+ autoCompact: true,
13740
+ strategy: true,
13741
+ warnThreshold: true,
13742
+ softThreshold: true,
13743
+ hardThreshold: true,
13744
+ preserveK: true,
13745
+ eliseThreshold: true,
13746
+ targetLoad: true
13747
+ },
13748
+ tools: {
13749
+ defaultExecutionStrategy: true,
13750
+ maxIterations: true,
13751
+ iterationTimeoutMs: true,
13752
+ sessionTimeoutMs: true,
13753
+ perIterationOutputCapBytes: true,
13754
+ autoExtendLimit: true,
13755
+ restrictToProjectRoot: true,
13756
+ descriptionMode: true,
13757
+ disabledTools: true,
13758
+ maxToolTimeoutMs: true,
13759
+ loopDetection: {
13760
+ mode: true,
13761
+ steerThreshold: true,
13762
+ cutThreshold: true,
13763
+ windowSize: true,
13764
+ callRepeatThreshold: true
13765
+ }
13766
+ },
13767
+ log: { level: true, format: true },
13768
+ features: {
13769
+ mcp: true,
13770
+ plugins: true,
13771
+ memory: true,
13772
+ modelsRegistry: true,
13773
+ skills: true,
13774
+ prompts: true,
13775
+ tokenSavingMode: true,
13776
+ allowOutsideProjectRoot: true,
13777
+ developerMode: true
13778
+ },
13779
+ session: { auditLevel: true, sampling: { toolProgress: { sampleRate: true } } },
13780
+ indexing: { onSessionStart: true, onEdit: true, watchExternal: true, debounceMs: true },
13781
+ circuitBreaker: { enabled: true, autoKillResetMs: true },
13782
+ modelRuntime: {
13783
+ reasoning: { mode: true, effort: true, preserve: true },
13784
+ cache: { ttl: true }
13785
+ },
13786
+ systemPrompt: { variant: true },
13787
+ skills: { readClaudeSkills: true },
13788
+ Sage: SAGE_TREE,
13789
+ fallbackAuto: true,
13790
+ yolo: true,
13791
+ nextPrediction: true,
13792
+ hints: true,
13793
+ debugStream: true
13794
+ };
13795
+ var UI_PREFERENCES_TREE = {
13796
+ autonomy: {
13797
+ defaultMode: true,
13798
+ autoProceedDelayMs: true,
13799
+ autoProceedMaxIterations: true,
13800
+ autonomyNextPrompt: true,
13801
+ terminalTitleAnimation: true,
13802
+ yolo: true,
13803
+ streamFleet: true,
13804
+ chime: true,
13805
+ confirmExit: true,
13806
+ mouseMode: true,
13807
+ enhance: true,
13808
+ enhanceDelayMs: true,
13809
+ enhanceCountdownMs: true,
13810
+ enhanceLanguage: true,
13811
+ statuslineMode: true,
13812
+ thinkingWord: true,
13813
+ midRunSendPicker: true,
13814
+ animationStyle: true,
13815
+ fleetChatVerbosity: true,
13816
+ refinerFallbackProfile: true,
13817
+ refinerProvider: true,
13818
+ refinerModel: true,
13819
+ showModelReasoning: true,
13820
+ shellBangWarningDontShowAgain: true,
13821
+ showAgentSwarmPanel: true,
13822
+ showSageMemoryInject: true,
13823
+ readAdvancedMode: true,
13824
+ panelPositions: true
13825
+ },
13826
+ launch: { mode: true, autonomy: true },
13827
+ uiLocale: true
13828
+ };
13829
+ var MODELS_ROUTING_TREE = {
13830
+ provider: true,
13831
+ model: true,
13832
+ favoriteModels: true,
13833
+ favoriteModelsOnly: true,
13834
+ fallbackModels: true,
13835
+ fallbackProfiles: true,
13836
+ modelMatrix: { "*": { fallbackProfile: true } },
13837
+ brain: {
13838
+ mode: true,
13839
+ maxAutoRisk: true,
13840
+ models: true,
13841
+ strategy: true,
13842
+ decisionTimeoutMs: true,
13843
+ humanTimeoutMs: true,
13844
+ council: {
13845
+ enabled: true,
13846
+ minRisk: true,
13847
+ voters: true
13848
+ },
13849
+ ledger: { enabled: true, autoDenyAfterFailures: true },
13850
+ trace: { enabled: true }
13851
+ }
13852
+ };
13853
+ var PROVIDERS_CATALOG_TREE = {
13854
+ providers: {
13855
+ "*": {
13856
+ type: true,
13857
+ family: true,
13858
+ baseUrl: true,
13859
+ envVars: true,
13860
+ models: true,
13861
+ activeKey: true
13862
+ }
13863
+ }
13864
+ };
13865
+ var MCP_SERVERS_TREE = {
13866
+ mcpServers: {
13867
+ "*": {
13868
+ name: true,
13869
+ transport: true,
13870
+ command: true,
13871
+ args: true,
13872
+ url: true,
13873
+ description: true,
13874
+ permission: true,
13875
+ enabled: true,
13876
+ lazy: true,
13877
+ envVars: true
13878
+ }
13879
+ }
13880
+ };
13881
+ var EXTENSIONS_PLUGINS_TREE = {
13882
+ plugins: true,
13883
+ extensions: true
13884
+ };
13885
+ var CLOUD_SYNC_CONTRACT = {
13886
+ "core.runtime": CORE_RUNTIME_TREE,
13887
+ "ui.preferences": UI_PREFERENCES_TREE,
13888
+ "models.routing": MODELS_ROUTING_TREE,
13889
+ "providers.catalog": PROVIDERS_CATALOG_TREE,
13890
+ "mcp.servers": MCP_SERVERS_TREE,
13891
+ "extensions.plugins": EXTENSIONS_PLUGINS_TREE
13892
+ };
13893
+ var CLOUD_SYNC_NAMESPACES = Object.keys(CLOUD_SYNC_CONTRACT);
13894
+ var NAMESPACE_SCHEMA_VERSIONS = {
13895
+ "core.runtime": 1,
13896
+ "ui.preferences": 1,
13897
+ "models.routing": 1,
13898
+ "providers.catalog": 1,
13899
+ "mcp.servers": 2,
13900
+ "extensions.plugins": 1
13901
+ };
13902
+ var LOCAL_ONLY_TOP_LEVEL = [
13903
+ "version",
13904
+ "configScope",
13905
+ "hq",
13906
+ "cloudSync",
13907
+ "sync",
13908
+ "models",
13909
+ "apiKey",
13910
+ "baseUrl"
13911
+ ];
13912
+ var ENCRYPTED_VALUE = /enc:v\d+:/;
13913
+ var SECRET_KEY = /^(api[-_]?key|apikeys|.*token|secret.*|.*secret|password|passwd|credential.*|private[-_]?key|authorization)$/i;
13914
+ function isPlainObject(value) {
13915
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13916
+ }
13917
+ function stripSecretMaterial(value) {
13918
+ if (typeof value === "string") {
13919
+ return ENCRYPTED_VALUE.test(value) ? void 0 : value;
13920
+ }
13921
+ if (Array.isArray(value)) {
13922
+ const cleaned = value.map((entry) => stripSecretMaterial(entry));
13923
+ return cleaned.filter((entry) => entry !== void 0);
13924
+ }
13925
+ if (isPlainObject(value)) {
13926
+ const out = {};
13927
+ for (const [key, child] of Object.entries(value)) {
13928
+ if (SECRET_KEY.test(key)) continue;
13929
+ const cleaned = stripSecretMaterial(child);
13930
+ if (cleaned !== void 0) out[key] = cleaned;
13931
+ }
13932
+ return out;
13933
+ }
13934
+ return value;
13935
+ }
13936
+ function project(value, tree) {
13937
+ if (tree === true) return value;
13938
+ if (!isPlainObject(value)) return void 0;
13939
+ const wildcard = tree["*"];
13940
+ const out = {};
13941
+ for (const [key, child] of Object.entries(value)) {
13942
+ const node = Object.hasOwn(tree, key) && key !== "*" ? tree[key] : wildcard;
13943
+ if (node === void 0) continue;
13944
+ const projected = project(child, node);
13945
+ if (projected !== void 0) out[key] = projected;
13946
+ }
13947
+ return out;
13948
+ }
13949
+ function buildNamespacePayloads(config) {
13950
+ const payloads = {};
13951
+ for (const [namespace, tree] of Object.entries(CLOUD_SYNC_CONTRACT)) {
13952
+ if (tree === true) continue;
13953
+ const payload = {};
13954
+ for (const [key, node] of Object.entries(tree)) {
13955
+ if (!(key in config) || config[key] === void 0) continue;
13956
+ const projected = project(config[key], node);
13957
+ if (projected === void 0) continue;
13958
+ const cleaned = stripSecretMaterial(projected);
13959
+ if (cleaned === void 0) continue;
13960
+ if (isPlainObject(cleaned) && Object.keys(cleaned).length === 0 && node !== true) continue;
13961
+ payload[key] = cleaned;
13962
+ }
13963
+ if (Object.keys(payload).length > 0) payloads[namespace] = payload;
13964
+ }
13965
+ return payloads;
13966
+ }
13967
+ function reinjectLocalSecrets(local, incoming) {
13968
+ if (!isPlainObject(local) || !isPlainObject(incoming)) return incoming;
13969
+ const out = { ...incoming };
13970
+ for (const [key, value] of Object.entries(local)) {
13971
+ const secretish = SECRET_KEY.test(key) || typeof value === "string" && ENCRYPTED_VALUE.test(value);
13972
+ if (secretish && !(key in incoming)) {
13973
+ out[key] = value;
13974
+ continue;
13975
+ }
13976
+ if (isPlainObject(value) && isPlainObject(incoming[key])) {
13977
+ out[key] = reinjectLocalSecrets(value, incoming[key]);
13978
+ }
13979
+ }
13980
+ return out;
13981
+ }
13982
+ function mergeAtContract(local, incoming, tree) {
13983
+ if (tree === true) return reinjectLocalSecrets(local, incoming);
13984
+ if (!isPlainObject(incoming)) return local;
13985
+ const base = isPlainObject(local) ? { ...local } : {};
13986
+ const wildcard = tree["*"];
13987
+ for (const [key, child] of Object.entries(incoming)) {
13988
+ const node = Object.hasOwn(tree, key) && key !== "*" ? tree[key] : wildcard;
13989
+ if (node === void 0) continue;
13990
+ base[key] = mergeAtContract(base[key], child, node);
13991
+ }
13992
+ return base;
13993
+ }
13994
+ function applyNamespacePayload(config, namespace, payload) {
13995
+ const tree = CLOUD_SYNC_CONTRACT[namespace];
13996
+ if (!tree || tree === true) return config;
13997
+ const next = { ...config };
13998
+ for (const [key, incoming] of Object.entries(payload)) {
13999
+ const node = Object.hasOwn(tree, key) ? tree[key] : void 0;
14000
+ if (node === void 0) continue;
14001
+ next[key] = mergeAtContract(next[key], incoming, node);
14002
+ }
14003
+ return next;
14004
+ }
14005
+
14006
+ // src/storage/cloud-config-sync.ts
14007
+ var EMPTY_STATE = {
14008
+ version: 1,
14009
+ cursor: 0,
14010
+ namespaceRevisions: {},
14011
+ syncedHashes: {},
14012
+ syncedPayloads: {},
14013
+ pendingMutations: {},
14014
+ rejectedHashes: {}
14015
+ };
14016
+ function stableStringify3(value) {
14017
+ if (Array.isArray(value)) {
14018
+ return `[${value.map((entry) => stableStringify3(entry)).join(",")}]`;
14019
+ }
14020
+ if (typeof value === "object" && value !== null) {
14021
+ 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)}`);
14022
+ return `{${entries.join(",")}}`;
14023
+ }
14024
+ return JSON.stringify(value) ?? "null";
14025
+ }
14026
+ function hashPayload(payload) {
14027
+ return createHash9("sha256").update(stableStringify3(payload)).digest("hex");
14028
+ }
14029
+ function deepEquals(a, b) {
14030
+ return stableStringify3(a) === stableStringify3(b);
14031
+ }
14032
+ var CloudConfigSync = class {
14033
+ constructor(deps) {
14034
+ this.deps = deps;
14035
+ this.fetchImpl = deps.fetchImpl ?? fetch;
14036
+ this.uuid = deps.randomUUID ?? (() => nodeRandomUUID());
14037
+ this.timeoutMs = deps.requestTimeoutMs ?? 15e3;
14038
+ }
14039
+ deps;
14040
+ state = null;
14041
+ fetchImpl;
14042
+ uuid;
14043
+ timeoutMs;
14044
+ /** One full pull-then-push-then-heartbeat pass. Never throws; failures are summarized. */
14045
+ async syncOnce() {
14046
+ const settings = this.deps.getSettings();
14047
+ if (!settings?.enabled || !settings.url || !settings.token) {
14048
+ return summary({ skipped: true, message: "Cloud sync is not configured." });
14049
+ }
14050
+ const state = await this.loadState();
14051
+ const pulled = [];
14052
+ const pushed = [];
14053
+ const conflicted = [];
14054
+ const rejected = [];
14055
+ try {
14056
+ await this.pullChanges(settings, state, pulled);
14057
+ await this.pushDirty(settings, state, { pushed, conflicted, rejected });
14058
+ await this.heartbeat(settings, state);
14059
+ state.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
14060
+ await this.saveState(state);
14061
+ return summary({
14062
+ ok: true,
14063
+ pulled,
14064
+ pushed,
14065
+ conflicted,
14066
+ rejected,
14067
+ message: pulled.length + pushed.length === 0 ? "Already in sync." : `Pulled ${pulled.length}, pushed ${pushed.length} namespace(s).`
14068
+ });
14069
+ } catch (error) {
14070
+ await this.saveState(state).catch(() => {
14071
+ });
14072
+ const message = error instanceof Error ? error.message : String(error);
14073
+ this.deps.logger?.warn(`cloud-config-sync: pass failed: ${message}`);
14074
+ return summary({ pulled, pushed, conflicted, rejected, message });
14075
+ }
14076
+ }
14077
+ /** Human-readable status for the slash command. */
14078
+ async statusText() {
14079
+ const settings = this.deps.getSettings();
14080
+ if (!settings?.enabled) {
14081
+ return "Cloud config sync: disabled. Run `/cloudsync on` after setting url + token.";
14082
+ }
14083
+ const state = await this.loadState();
14084
+ const dirty = await this.dirtyNamespaces(state);
14085
+ const rejectedList = Object.keys(state.rejectedHashes);
14086
+ const lines = [
14087
+ "Cloud config sync: enabled",
14088
+ ` url: ${settings.url}`,
14089
+ ` cursor: ${state.cursor}`,
14090
+ ` last sync: ${state.lastSyncedAt ?? "never"}`,
14091
+ ` dirty: ${dirty.length > 0 ? dirty.join(", ") : "none"}`
14092
+ ];
14093
+ if (rejectedList.length > 0) {
14094
+ lines.push(` rejected: ${rejectedList.join(", ")} (kept local; edit to retry)`);
14095
+ }
14096
+ return lines.join("\n");
14097
+ }
14098
+ // ── Pull ────────────────────────────────────────────────────────────────
14099
+ async pullChanges(settings, state, pulled) {
14100
+ const delta = (await this.request(settings, "GET", `/api/sync/v1/config?since=${state.cursor}`)).json;
14101
+ const toRead = delta.full ? CLOUD_SYNC_NAMESPACES.map((namespace) => ({ namespace })) : delta.changed.filter(
14102
+ (entry) => CLOUD_SYNC_NAMESPACES.includes(entry.namespace) && entry.revision > (state.namespaceRevisions[entry.namespace] ?? 0)
14103
+ );
14104
+ const dirtyBefore = new Set(await this.dirtyNamespaces(state));
14105
+ for (const entry of toRead) {
14106
+ const snapshot = (await this.request(
14107
+ settings,
14108
+ "GET",
14109
+ `/api/sync/v1/config/${encodeURIComponent(entry.namespace)}`
14110
+ )).json;
14111
+ if ((state.namespaceRevisions[entry.namespace] ?? 0) >= snapshot.revision) continue;
14112
+ if (snapshot.revision === 0) {
14113
+ state.namespaceRevisions[entry.namespace] = 0;
14114
+ continue;
14115
+ }
14116
+ if (dirtyBefore.has(entry.namespace)) {
14117
+ const local = await this.deps.readLocalConfig();
14118
+ const localPayload = buildNamespacePayloads(local)[entry.namespace] ?? {};
14119
+ const base = state.syncedPayloads[entry.namespace] ?? {};
14120
+ const merged = { ...snapshot.payload };
14121
+ for (const [key, localValue] of Object.entries(localPayload)) {
14122
+ if (!deepEquals(localValue, base[key])) merged[key] = localValue;
14123
+ }
14124
+ await this.deps.writeLocalConfig(
14125
+ (config) => applyNamespacePayload(config, entry.namespace, merged)
14126
+ );
14127
+ state.namespaceRevisions[entry.namespace] = snapshot.revision;
14128
+ state.syncedPayloads[entry.namespace] = snapshot.payload;
14129
+ } else {
14130
+ await this.applySnapshot(entry.namespace, snapshot, state);
14131
+ }
14132
+ pulled.push(entry.namespace);
14133
+ }
14134
+ state.cursor = Math.max(state.cursor, delta.cursor);
14135
+ }
14136
+ async applySnapshot(namespace, snapshot, state) {
14137
+ await this.deps.writeLocalConfig(
14138
+ (config) => applyNamespacePayload(config, namespace, snapshot.payload)
14139
+ );
14140
+ state.namespaceRevisions[namespace] = snapshot.revision;
14141
+ state.syncedPayloads[namespace] = snapshot.payload;
14142
+ const local = await this.deps.readLocalConfig();
14143
+ const projected = buildNamespacePayloads(local)[namespace] ?? {};
14144
+ state.syncedHashes[namespace] = hashPayload(projected);
14145
+ delete state.pendingMutations[namespace];
14146
+ delete state.rejectedHashes[namespace];
14147
+ }
14148
+ // ── Push ────────────────────────────────────────────────────────────────
14149
+ async dirtyNamespaces(state) {
14150
+ const local = await this.deps.readLocalConfig();
14151
+ const payloads = buildNamespacePayloads(local);
14152
+ return Object.entries(payloads).filter(([namespace, payload]) => hashPayload(payload) !== state.syncedHashes[namespace]).map(([namespace]) => namespace);
14153
+ }
14154
+ async pushDirty(settings, state, out) {
14155
+ const local = await this.deps.readLocalConfig();
14156
+ const payloads = buildNamespacePayloads(local);
14157
+ for (const [namespace, payload] of Object.entries(payloads)) {
14158
+ const hash = hashPayload(payload);
14159
+ if (hash === state.syncedHashes[namespace]) continue;
14160
+ if (state.rejectedHashes[namespace] === hash) continue;
14161
+ const pending = state.pendingMutations[namespace];
14162
+ const mutationId = pending?.hash === hash ? pending.mutationId : this.uuid();
14163
+ state.pendingMutations[namespace] = { mutationId, hash };
14164
+ await this.saveState(state);
14165
+ const result = await this.putNamespace(settings, state, namespace, payload, mutationId);
14166
+ if (result.kind === "applied") {
14167
+ out.pushed.push(namespace);
14168
+ } else if (result.kind === "conflict") {
14169
+ out.conflicted.push(namespace);
14170
+ await this.resolveConflict(settings, state, namespace, payload, result.server, out);
14171
+ } else {
14172
+ state.rejectedHashes[namespace] = hash;
14173
+ delete state.pendingMutations[namespace];
14174
+ out.rejected.push(namespace);
14175
+ this.deps.logger?.warn(
14176
+ `cloud-config-sync: server rejected ${namespace} (${result.code}); keeping it local until the config changes.`
14177
+ );
14178
+ }
14179
+ }
14180
+ }
14181
+ async putNamespace(settings, state, namespace, payload, mutationId) {
14182
+ const response = await this.request(
14183
+ settings,
14184
+ "PUT",
14185
+ `/api/sync/v1/config/${encodeURIComponent(namespace)}`,
14186
+ {
14187
+ baseRevision: state.namespaceRevisions[namespace] ?? 0,
14188
+ mutationId,
14189
+ schemaVersion: NAMESPACE_SCHEMA_VERSIONS[namespace] ?? 1,
14190
+ payload
14191
+ },
14192
+ // 409 (conflict) and 422 (contract mismatch) are protocol answers, not failures.
14193
+ [409, 422]
14194
+ );
14195
+ if (response.status === 422) {
14196
+ const code = response.json.error?.code;
14197
+ return { kind: "rejected", code: code ?? "invalid_payload" };
14198
+ }
14199
+ const body = response.json;
14200
+ if (response.status === 409) {
14201
+ return { kind: "conflict", server: body };
14202
+ }
14203
+ state.namespaceRevisions[namespace] = body.revision;
14204
+ state.syncedHashes[namespace] = hashPayload(payload);
14205
+ state.syncedPayloads[namespace] = payload;
14206
+ if (typeof body.cursor === "number") state.cursor = Math.max(state.cursor, body.cursor);
14207
+ delete state.pendingMutations[namespace];
14208
+ delete state.rejectedHashes[namespace];
14209
+ return { kind: "applied" };
14210
+ }
14211
+ /**
14212
+ * Three-way merge at owned-top-level-key granularity: keys unchanged locally since the
14213
+ * last sync adopt the server value; locally-changed keys win and are re-pushed on top
14214
+ * of the server revision with a fresh mutation id.
14215
+ */
14216
+ async resolveConflict(settings, state, namespace, localPayload, server, out) {
14217
+ const base = state.syncedPayloads[namespace] ?? {};
14218
+ const merged = { ...server.payload };
14219
+ for (const [key, localValue] of Object.entries(localPayload)) {
14220
+ if (!deepEquals(localValue, base[key])) merged[key] = localValue;
14221
+ }
14222
+ await this.deps.writeLocalConfig((config) => applyNamespacePayload(config, namespace, merged));
14223
+ state.namespaceRevisions[namespace] = server.revision;
14224
+ const mutationId = this.uuid();
14225
+ const hash = hashPayload(merged);
14226
+ state.pendingMutations[namespace] = { mutationId, hash };
14227
+ await this.saveState(state);
14228
+ const result = await this.putNamespace(settings, state, namespace, merged, mutationId);
14229
+ if (result.kind === "applied") {
14230
+ out.pushed.push(namespace);
14231
+ } else {
14232
+ this.deps.logger?.warn(
14233
+ `cloud-config-sync: conflict resolution for ${namespace} did not converge (${result.kind}); will retry next pass.`
14234
+ );
14235
+ }
14236
+ }
14237
+ // ── Heartbeat ───────────────────────────────────────────────────────────
14238
+ async heartbeat(settings, state) {
14239
+ await this.request(settings, "POST", "/api/sync/v1/heartbeat", {
14240
+ transport: "poll",
14241
+ syncCursor: state.cursor,
14242
+ namespaceRevisions: state.namespaceRevisions
14243
+ }).catch((error) => {
14244
+ const message = error instanceof Error ? error.message : String(error);
14245
+ this.deps.logger?.warn(`cloud-config-sync: heartbeat failed: ${message}`);
14246
+ });
14247
+ }
14248
+ // ── HTTP + state plumbing ───────────────────────────────────────────────
14249
+ async request(settings, method, pathname, body, acceptStatuses = []) {
14250
+ const url = `${settings.url.replace(/\/+$/, "")}${pathname}`;
14251
+ const headers = {
14252
+ Authorization: `Bearer ${settings.token}`,
14253
+ "X-WrongStack-Version": this.deps.appVersion ?? "unknown"
14254
+ };
14255
+ if (body !== void 0) headers["Content-Type"] = "application/json";
14256
+ const init = {
14257
+ method,
14258
+ headers,
14259
+ signal: AbortSignal.timeout(this.timeoutMs)
14260
+ };
14261
+ if (body !== void 0) init.body = JSON.stringify(body);
14262
+ const response = await this.fetchImpl(url, init);
14263
+ const ok = response.ok || acceptStatuses.includes(response.status);
14264
+ if (!ok) {
14265
+ if (response.status === 401) {
14266
+ throw new Error("unauthorized: the machine token is invalid, expired, or revoked.");
14267
+ }
14268
+ if (response.status === 429) {
14269
+ throw new Error("rate limited by the portal; backing off until the next pass.");
14270
+ }
14271
+ throw new Error(`${method} ${pathname} failed with HTTP ${response.status}.`);
14272
+ }
14273
+ const json = response.status === 204 ? null : await response.json().catch(() => null);
14274
+ return { status: response.status, json };
14275
+ }
14276
+ async loadState() {
14277
+ if (this.state) return this.state;
14278
+ try {
14279
+ const raw = await fs18.readFile(this.deps.statePath, "utf8");
14280
+ const parsed = JSON.parse(raw);
14281
+ this.state = {
14282
+ ...structuredClone(EMPTY_STATE),
14283
+ ...parsed,
14284
+ version: 1
14285
+ };
14286
+ } catch {
14287
+ this.state = structuredClone(EMPTY_STATE);
14288
+ }
14289
+ return this.state;
14290
+ }
14291
+ async saveState(state) {
14292
+ this.state = state;
14293
+ await fs18.mkdir(path30.dirname(this.deps.statePath), { recursive: true });
14294
+ await atomicWrite(this.deps.statePath, `${JSON.stringify(state, null, 2)}
14295
+ `, {
14296
+ mode: 384
14297
+ });
14298
+ }
14299
+ };
14300
+ function summary(partial) {
14301
+ return {
14302
+ ok: partial.ok ?? false,
14303
+ skipped: partial.skipped ?? false,
14304
+ pulled: partial.pulled ?? [],
14305
+ pushed: partial.pushed ?? [],
14306
+ conflicted: partial.conflicted ?? [],
14307
+ rejected: partial.rejected ?? [],
14308
+ message: partial.message
14309
+ };
14310
+ }
14311
+
13532
14312
  // src/storage/session-event-bridge.ts
13533
14313
  var CORE_RECONSTRUCT_EVENTS = /* @__PURE__ */ new Set([
13534
14314
  "session_start",
@@ -13669,8 +14449,11 @@ export {
13669
14449
  ALL_SYNC_CATEGORIES,
13670
14450
  AgentStatusTracker,
13671
14451
  AnnotationsStore,
14452
+ CLOUD_SYNC_CONTRACT,
14453
+ CLOUD_SYNC_NAMESPACES,
13672
14454
  CONFIG_BEHAVIOR_DEFAULTS,
13673
14455
  CORE_RECONSTRUCT_EVENTS,
14456
+ CloudConfigSync,
13674
14457
  CloudSync,
13675
14458
  ConfigMigrationError,
13676
14459
  DEFAULT_CONFIG_MIGRATIONS,
@@ -13687,8 +14470,10 @@ export {
13687
14470
  GraphMemoryBackend,
13688
14471
  INPUT_HISTORY_DEFAULT_MAX,
13689
14472
  InputHistoryStore,
14473
+ LOCAL_ONLY_TOP_LEVEL,
13690
14474
  MAX_JOURNAL_ENTRIES,
13691
14475
  MAX_PROGRESS_HISTORY,
14476
+ NAMESPACE_SCHEMA_VERSIONS,
13692
14477
  PromptUsageStore,
13693
14478
  QUEUE_MAX_BYTES,
13694
14479
  QUEUE_MAX_ITEMS,
@@ -13706,9 +14491,11 @@ export {
13706
14491
  addPlanItem,
13707
14492
  appendJournal,
13708
14493
  applyGoalDeliverableCompletions,
14494
+ applyNamespacePayload,
13709
14495
  applyRewindToConversation,
13710
14496
  attachPlanCheckpoint,
13711
14497
  attachTodosCheckpoint,
14498
+ buildNamespacePayloads,
13712
14499
  clearPlan,
13713
14500
  coordinateGoalIteration,
13714
14501
  createGoalKanbanBoard,
@@ -13770,6 +14557,7 @@ export {
13770
14557
  setPlanItemStatus,
13771
14558
  setProgress,
13772
14559
  stripGoalDeliverableMarker,
14560
+ stripSecretMaterial,
13773
14561
  summarizeUsage,
13774
14562
  watchProviderConfig
13775
14563
  };