@narumitw/pi-sync 0.56.0 → 0.57.1

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 (32) hide show
  1. package/README.md +19 -9
  2. package/dist/chunks/{agent-candidate-3JN6DJKT.ts → agent-candidate-GNEYR225.ts} +3 -3
  3. package/dist/chunks/{agent-plan-QCUJWBP4.ts → agent-plan-K3CRYVXV.ts} +3 -3
  4. package/dist/chunks/{chunk-FQOSHGJ7.ts → chunk-5QAE3G3K.ts} +11 -6
  5. package/dist/chunks/chunk-5QAE3G3K.ts.map +7 -0
  6. package/dist/chunks/{chunk-S2FMH5FT.ts → chunk-QOC47PBC.ts} +2 -2
  7. package/dist/chunks/{chunk-5Q5SSKNG.ts → chunk-S7QS4AWK.ts} +89 -2
  8. package/dist/chunks/chunk-S7QS4AWK.ts.map +7 -0
  9. package/dist/chunks/{chunk-OTZRCKYP.ts → chunk-T3NC77TU.ts} +29 -16
  10. package/dist/chunks/chunk-T3NC77TU.ts.map +7 -0
  11. package/dist/chunks/{conflict-review-6WIEQLXW.ts → conflict-review-ZCGWFKEK.ts} +3 -3
  12. package/dist/chunks/{continuous-sync-AWAAOXK2.ts → continuous-sync-LAIMSW7D.ts} +2 -2
  13. package/dist/chunks/{sync-operations-HDUDT5PD.ts → sync-operations-W5HFFMTP.ts} +3 -3
  14. package/dist/index.ts +4 -4
  15. package/docs/settings.md +24 -322
  16. package/package.json +12 -5
  17. package/skills/configure-pi-sync/SKILL.md +54 -0
  18. package/skills/configure-pi-sync/references/configuration.md +650 -0
  19. package/src/sync/agent-candidate.ts +8 -3
  20. package/src/sync/merged-sync.ts +1 -1
  21. package/src/sync/package-merge.ts +87 -0
  22. package/src/sync/settings-conflicts.ts +25 -13
  23. package/src/sync/settings-merge.ts +48 -0
  24. package/dist/chunks/chunk-5Q5SSKNG.ts.map +0 -7
  25. package/dist/chunks/chunk-FQOSHGJ7.ts.map +0 -7
  26. package/dist/chunks/chunk-OTZRCKYP.ts.map +0 -7
  27. /package/dist/chunks/{agent-candidate-3JN6DJKT.ts.map → agent-candidate-GNEYR225.ts.map} +0 -0
  28. /package/dist/chunks/{agent-plan-QCUJWBP4.ts.map → agent-plan-K3CRYVXV.ts.map} +0 -0
  29. /package/dist/chunks/{chunk-S2FMH5FT.ts.map → chunk-QOC47PBC.ts.map} +0 -0
  30. /package/dist/chunks/{conflict-review-6WIEQLXW.ts.map → conflict-review-ZCGWFKEK.ts.map} +0 -0
  31. /package/dist/chunks/{continuous-sync-AWAAOXK2.ts.map → continuous-sync-LAIMSW7D.ts.map} +0 -0
  32. /package/dist/chunks/{sync-operations-HDUDT5PD.ts.map → sync-operations-W5HFFMTP.ts.map} +0 -0
package/README.md CHANGED
@@ -15,6 +15,7 @@ Reusable storage connections hold credentials, while named sync setups define ex
15
15
  - Writes settings atomically, preserves unknown fields, rejects stale edits, and fails closed on unsafe configuration.
16
16
  - Offers default-off experimental Agent mode for autonomous, validated text/settings conflict resolution, with a separate Continuous sync opt-in for monitoring and recovery.
17
17
  - Loads a generated split runtime with lazy UI and backend chunks.
18
+ - Bundles a manual-only skill for configuration questions and safe setup guidance.
18
19
 
19
20
  ## 📦 Install
20
21
 
@@ -137,7 +138,7 @@ Choose **Review differences (recommended)** in an ordinary file-direction confli
137
138
  Then choose one reviewed direction:
138
139
 
139
140
  - **Keep local content and replace remote…** uses the existing forced-push path.
140
- It applies the [secret-scan setting](./docs/settings.md#secret-scanning), shows the exact remote publication effect, re-reads a changed remote head, and asks again if the reviewed plan changed.
141
+ It applies the [secret-scan setting](./skills/configure-pi-sync/references/configuration.md#secret-scanning), shows the exact remote publication effect, re-reads a changed remote head, and asks again if the reviewed plan changed.
141
142
  - **Use remote content and replace local…** uses the existing forced-pull path.
142
143
  It shows exact local writes and deletions, protects the live session, and creates a local backup before applying.
143
144
  - First sync uses **Use local as initial source…** and **Use remote as initial source…** labels.
@@ -167,7 +168,7 @@ Missing settings stay unconfigured without creating files or locks.
167
168
 
168
169
  Set the top-level `"debug": true` in `pi-sync.json`, or enable **Settings → Advanced → Debug startup timings**, then restart Pi or use `/reload`.
169
170
  The default is `false`. Timing reports appear in TUI/RPC notifications, not the model context or a log file.
170
- See [Debug startup timings](./docs/settings.md#debug-startup-timings) for measured stages and limits.
171
+ See [Debug startup timings](./skills/configure-pi-sync/references/configuration.md#debug-startup-timings) for measured stages and limits.
171
172
 
172
173
  ### Background startup checks
173
174
 
@@ -185,7 +186,7 @@ Shutdown behavior is unchanged: when **Automatic sync** is On and sessions are i
185
186
 
186
187
  **Warning:** eligible conflict text is sent to the selected Pi model and can incur costs. Accepted results can replace/delete selected local and remote content without another transfer prompt. The resolver has no tools or parent conversation, and validates candidate ownership and current versions before using the existing publication/backup/journal flow. Validation is not a guarantee of semantic correctness or lossless editing against other local processes.
187
188
 
188
- The first implementation resolves bounded overlapping `AGENTS.md` and prompt/skill Markdown/plain-text edits, and selects complete atomic global settings units. Verified ancestors are required. Code, session histories, deletion conflicts and path collisions are not model-resolved; unsupported/refused work pauses, unless existing partial-sync policy permits independent progress. No resources reload automatically. Disabling stops subsequent authorization, not completed writes or provider charges. Read [Agent mode settings, limits, privacy and recovery](./docs/settings.md#experimental-agent-mode) before enabling.
189
+ The first implementation resolves bounded overlapping `AGENTS.md` and prompt/skill Markdown/plain-text edits, and selects complete atomic global settings units. For `settings.json` packages, it keeps the higher explicit npm SemVer version from local and remote without a registry lookup; incompatible filters and non-comparable sources retain existing conflict handling. Verified ancestors are required. Code, session histories, deletion conflicts and path collisions are not model-resolved; unsupported/refused work pauses, unless existing partial-sync policy permits independent progress. No resources reload automatically. Disabling stops subsequent authorization, not completed writes or provider charges. Read [Agent mode settings, limits, privacy and recovery](./skills/configure-pi-sync/references/configuration.md#experimental-agent-mode) before enabling.
189
190
 
190
191
  ### Experimental Continuous sync
191
192
 
@@ -193,7 +194,7 @@ The first implementation resolves bounded overlapping `AGENTS.md` and prompt/ski
193
194
 
194
195
  Selected-file hints are coalesced after one quiet second. A 60–66 second remote/local check covers missed events. Temporary failures use bounded backoff. One local session owns monitoring for each shared agent directory; each computer remains a separate replica. Busy sessions and foreground commands release ownership after draining work. Nothing runs after Pi exits, and resources never reload automatically.
195
196
 
196
- **Warning:** local changes and retries can cause extra network requests between polls. Automatic model requests are limited to two per candidate and two per replica binding in a rolling 15-minute window, across restarts. These are request limits, not a currency cap. Safety blocks still require changed evidence or repair; uncertain provider/Git errors are not guessed to be retryable. See [Continuous sync consent, budgets and recovery](./docs/settings.md#experimental-continuous-sync).
197
+ **Warning:** local changes and retries can cause extra network requests between polls. Automatic model requests are limited to two per candidate and two per replica binding in a rolling 15-minute window, across restarts. These are request limits, not a currency cap. Safety blocks still require changed evidence or repair; uncertain provider/Git errors are not guessed to be retryable. See [Continuous sync consent, budgets and recovery](./skills/configure-pi-sync/references/configuration.md#experimental-continuous-sync).
197
198
 
198
199
  ### Opt-in automatic transfer
199
200
 
@@ -201,13 +202,13 @@ Selected-file hints are coalesced after one quiet second. A 60–66 second remot
201
202
 
202
203
  A baseline and matching authoritative content list are required. First-source choice, changed selection, missing established remote, secrets, and real conflicts remain review barriers. Automatic transfer requires conditional or lease-protected publication: Git, verified WebDAV, and R2/S3 with verified conditional writes are supported. The existing **Automatic sync** setting and shutdown policy are unchanged when this new opt-in is absent.
203
204
 
204
- Unchanged automatic transfers use a read-only fast path after a fresh head read and verified baseline/cache checks, avoiding snapshot downloads and baseline rewrites. Recovery, partial-sync, legacy, and uncertain cases keep the full path. See [unchanged transfer conditions](./docs/settings.md#unchanged-transfer-fast-path).
205
+ Unchanged automatic transfers use a read-only fast path after a fresh head read and verified baseline/cache checks, avoiding snapshot downloads and baseline rewrites. Recovery, partial-sync, legacy, and uncertain cases keep the full path. See [unchanged transfer conditions](./skills/configure-pi-sync/references/configuration.md#unchanged-transfer-fast-path).
205
206
 
206
207
  Successful transfers show one summary. They do not reload resources, activate extension code, or rewrite model context. After local resources change, reload or restart when ready; newly pulled conversations still require explicit resume. Headless print/JSON skip this new startup feature. Reviewed directional settings/session-root transitions record both roots and verified settings evidence so interrupted apply can recover before or after settings installation. Older pending journals without transition evidence retain their original root-ownership checks and may require reviewed recovery; preserve their backups rather than downgrading. See [merge safety and recovery](./docs/merge-implementation-audit.md) for journal boundaries, external-writer limits, and downgrade guidance.
207
208
 
208
209
  **Settings → Advanced → Merge settings fields** (experimental) controls per-setup `sync.mergeSettings`, default **Off**. It combines independent global `settings.json` fields, not arbitrary JSON or keybindings. Missing/corrupt ancestors, unsupported syntax, and incompatible fields retain whole-transfer review. Sensitive accepted content is cached privately under the denied state root; disabling does not delete evidence.
209
210
 
210
- **Settings → Sync content → Machine-local fields** edits explicit `sync.localFields` root field names for global `settings.json` (default none). Excluded values and absence stay local; future portable snapshots omit them. Enabling rules opts into settings version 4 / snapshot version 2, which older clients must refuse. All receiving machines must configure compatible rules. Additions/removals require a separately confirmed force-direction migration, even with `--yes`; old remote history is **not erased**. See [settings merge and local fields](./docs/settings.md#settings-field-merge-and-machine-local-fields) for formats, compatibility and recovery.
211
+ **Settings → Sync content → Machine-local fields** edits explicit `sync.localFields` root field names for global `settings.json` (default none). Excluded values and absence stay local; future portable snapshots omit them. Enabling rules opts into settings version 4 / snapshot version 2, which older clients must refuse. All receiving machines must configure compatible rules. Additions/removals require a separately confirmed force-direction migration, even with `--yes`; old remote history is **not erased**. See [settings merge and local fields](./skills/configure-pi-sync/references/configuration.md#settings-field-merge-and-machine-local-fields) for formats, compatibility and recovery.
211
212
 
212
213
  **Settings → Advanced → Content merge / partial progress** (experimental) controls independent `sync.mergeContent` and `sync.partialSync` opt-ins, both default **Off**. Saving opts into settings version 5; partial publications use snapshot version 3, rejected by older readers. Supported Markdown/plain-text resources merge bounded independent line edits without Git or conflict markers. Diff grids and hunk comparisons share an eight-million-work-unit budget across the operation and its retries; exhausted paths remain conflicts for review. Same-session reconciliation only accepts validated complete byte-prefix extensions; divergent histories and the loaded session require review. A syntactically clean text merge is **not a semantic correctness guarantee**.
213
214
 
@@ -216,7 +217,7 @@ Partial sync keeps each side's unresolved versions, preserves their previous acc
216
217
  **Settings → Advanced → Show sync status** defaults to **On**. Turning it Off immediately clears and suppresses pi-sync status text for background checks, transfers, and review attention; widgets and notifications remain available.
217
218
 
218
219
  **Settings → Advanced → Skip secret scan** defaults to **Off**; enable it only after reviewing the destination and selected content because it disables push scanning for every setup.
219
- See [Secret scanning](./docs/settings.md#secret-scanning) for the setting and diagnostic behavior.
220
+ See [Secret scanning](./skills/configure-pi-sync/references/configuration.md#secret-scanning) for the setting and diagnostic behavior.
220
221
 
221
222
  A minimal Git setup uses an existing private remote and keeps automatic sync off:
222
223
 
@@ -257,7 +258,15 @@ Version 1, version 2, and non-empty unversioned settings require manual recovery
257
258
  Adding `sessions` can upload prompts, tool output, paths, images, and secrets; interactive flows require a privacy acknowledgement.
258
259
  Startup checks report included-content differences without changing anything. Transfers retain their existing included-content policy checks instead of silently expanding local scope.
259
260
 
260
- Read the [settings reference](./docs/settings.md) for complete S3/R2, Git, and WebDAV examples, backend fields, included-content rules, legacy paths, and recovery steps.
261
+ Read the [settings reference](./skills/configure-pi-sync/references/configuration.md) for complete S3/R2, Git, and WebDAV examples, backend fields, included-content rules, legacy paths, and recovery steps.
262
+
263
+ ## 🧠 Skills
264
+
265
+ Run `/skill:configure-pi-sync` before asking Pi about storage choices, included content, automatic controls, or settings recovery.
266
+ The [configuration skill](./skills/configure-pi-sync/SKILL.md) is manual-only and provides guidance only: it does not inspect private settings, collect credentials, edit files, execute diagnostics, or sync content.
267
+ Enter credentials in the existing masked TUI and perform settings changes and reviewed transfers through `/sync` yourself.
268
+ The skill's instructions are not a permission sandbox for the agent's other tools.
269
+ Read the [settings reference](./skills/configure-pi-sync/references/configuration.md) directly when no agent assistance is needed.
261
270
 
262
271
  ## 💬 Commands
263
272
 
@@ -362,7 +371,8 @@ packages/pi-sync/
362
371
  │ └── ui/ # Manager, settings, reviews, and setup flows
363
372
  ├── dist/ # Generated Jiti runtime
364
373
  ├── scripts/build-runtime.mjs # Runtime builder
365
- ├── docs/ # Published reference documentation
374
+ ├── docs/ # Operational audits and settings link compatibility
375
+ ├── skills/configure-pi-sync/ # Manual-only guidance and authoritative settings reference
366
376
  └── test/ # Behavior and lifecycle coverage
367
377
  ```
368
378
 
@@ -5,8 +5,8 @@ import {
5
5
  buildAgentEvidence,
6
6
  digest,
7
7
  parseAgentCandidate
8
- } from "./chunk-FQOSHGJ7.ts";
9
- import "./chunk-5Q5SSKNG.ts";
8
+ } from "./chunk-5QAE3G3K.ts";
9
+ import "./chunk-S7QS4AWK.ts";
10
10
  import "./chunk-QOFMXPM5.ts";
11
11
  import "./chunk-IWZH5KYS.ts";
12
12
  import "./chunk-76NB4R7U.ts";
@@ -23,4 +23,4 @@ export {
23
23
  digest,
24
24
  parseAgentCandidate
25
25
  };
26
- //# sourceMappingURL=agent-candidate-3JN6DJKT.ts.map
26
+ //# sourceMappingURL=agent-candidate-GNEYR225.ts.map
@@ -19,8 +19,8 @@ import {
19
19
  applyAgentCandidate,
20
20
  buildAgentEvidence,
21
21
  digest
22
- } from "./chunk-FQOSHGJ7.ts";
23
- import "./chunk-5Q5SSKNG.ts";
22
+ } from "./chunk-5QAE3G3K.ts";
23
+ import "./chunk-S7QS4AWK.ts";
24
24
  import {
25
25
  createSnapshot
26
26
  } from "./chunk-QOFMXPM5.ts";
@@ -96,4 +96,4 @@ async function resolveAgentPlan(ctx, options, config, backend, state, local, loc
96
96
  export {
97
97
  resolveAgentPlan
98
98
  };
99
- //# sourceMappingURL=agent-plan-QCUJWBP4.ts.map
99
+ //# sourceMappingURL=agent-plan-K3CRYVXV.ts.map
@@ -4,7 +4,7 @@ import {
4
4
  COUPLED_FIELDS,
5
5
  mergeSettingsJson,
6
6
  snapshotFile
7
- } from "./chunk-5Q5SSKNG.ts";
7
+ } from "./chunk-S7QS4AWK.ts";
8
8
  import {
9
9
  scanSnapshot
10
10
  } from "./chunk-QOFMXPM5.ts";
@@ -87,9 +87,13 @@ function buildAgentEvidence(plan, local, remote, ancestors) {
87
87
  let units;
88
88
  if (conflict.path === "settings.json") {
89
89
  const documents = versions.map((value) => parseSettingsDocument(Buffer.from(value)));
90
- const merge = mergeSettingsJson(...versions.map((value) => Buffer.from(value)));
91
- if (merge.kind !== "review" || merge.reason !== "field-conflict") throw new Error("unsupported settings");
92
- const fields = new Set(merge.fields);
90
+ const merge = mergeSettingsJson(
91
+ ...versions.map((value) => Buffer.from(value)),
92
+ void 0,
93
+ true
94
+ );
95
+ if (merge.kind === "review" && merge.reason !== "field-conflict") throw new Error("unsupported settings");
96
+ const fields = new Set(merge.kind === "review" ? merge.fields : []);
93
97
  const groups = COUPLED_FIELDS.map((group) => [...group]);
94
98
  const coupled = new Set(groups.flat());
95
99
  groups.push(...[...fields].filter((key) => !coupled.has(key)).map((key) => [key]));
@@ -187,7 +191,8 @@ function applyAgentCandidate(plan, evidence, candidate) {
187
191
  Buffer.from(file.base),
188
192
  Buffer.from(file.local),
189
193
  Buffer.from(file.remote),
190
- selections
194
+ selections,
195
+ true
191
196
  );
192
197
  if (merged.kind !== "merged") throw new Error("Agent settings units remain unresolved.");
193
198
  parseSettingsDocument(merged.content);
@@ -212,4 +217,4 @@ export {
212
217
  parseAgentCandidate,
213
218
  applyAgentCandidate
214
219
  };
215
- //# sourceMappingURL=chunk-FQOSHGJ7.ts.map
220
+ //# sourceMappingURL=chunk-5QAE3G3K.ts.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/sync/agent-candidate.ts"],
4
+ "sourcesContent": ["import { createHash } from \"node:crypto\";\nimport { scanSnapshot } from \"../snapshot/snapshot.js\";\nimport type { Snapshot, SnapshotFile } from \"../snapshot/snapshot-types.js\";\nimport { AGENT_INPUT_BYTES, AGENT_MAX_UNITS, AGENT_OUTPUT_BYTES } from \"./agent-policy.js\";\nimport { snapshotFile } from \"./content-conflicts.js\";\nimport type { FileMergePlan } from \"./file-merge-planner.js\";\nimport { parseJsonObjectDocument, parseSettingsDocument } from \"./json-document.js\";\nimport { COUPLED_FIELDS, mergeSettingsJson } from \"./settings-merge.js\";\nimport { edits, type Hunk, isMergeTextPath, lines, text } from \"./text-merge.js\";\n\nexport interface AgentUnit {\n id: string;\n path: string;\n kind: \"text\" | \"settings\";\n base: string;\n local: string;\n remote: string;\n}\ninterface CandidateFile {\n path: string;\n base: string;\n local: string;\n remote: string;\n parts?: (string | AgentUnit)[];\n fields?: { id: string; key: string }[];\n}\nexport interface AgentEvidence {\n units: AgentUnit[];\n files: CandidateFile[];\n unsupported: string[];\n}\nexport interface AgentCandidate {\n version: 1;\n results: (\n | { id: string; content: string }\n | { id: string; source: \"local\" | \"remote\" }\n | { id: string; refuse: true }\n )[];\n}\nexport const digest = (value: unknown) => createHash(\"sha256\").update(JSON.stringify(value)).digest(\"hex\");\n\nfunction overlaps(a: Hunk, b: Hunk) {\n return a.start === a.end || b.start === b.end\n ? a.start <= b.end && b.start <= a.end\n : a.start < b.end && b.start < a.end;\n}\n\n/** Only overlapping edit regions are model-writable; all other bytes are assembled by code. */\nfunction textParts(filePath: string, baseText: string, localText: string, remoteText: string): (string | AgentUnit)[] {\n const base = lines(baseText);\n const changes = [\n ...edits(base, lines(localText)).map((hunk) => ({ ...hunk, side: \"local\" as const })),\n ...edits(base, lines(remoteText)).map((hunk) => ({ ...hunk, side: \"remote\" as const })),\n ].sort((a, b) => a.start - b.start || a.end - b.end);\n const clusters: (typeof changes)[] = [];\n for (const change of changes) {\n const last = clusters.at(-1);\n if (last?.some((item) => overlaps(item, change))) last.push(change);\n else clusters.push([change]);\n }\n const result: (string | AgentUnit)[] = [];\n let cursor = 0;\n for (const cluster of clusters) {\n const start = Math.min(...cluster.map((item) => item.start));\n const end = Math.max(...cluster.map((item) => item.end));\n result.push(base.slice(cursor, start).join(\"\"));\n const image = (side: \"local\" | \"remote\") => {\n let position = start;\n const output: string[] = [];\n for (const hunk of cluster.filter((item) => item.side === side)) {\n output.push(...base.slice(position, hunk.start), ...hunk.lines);\n position = hunk.end;\n }\n return [...output, ...base.slice(position, end)].join(\"\");\n };\n const local = image(\"local\");\n const remote = image(\"remote\");\n const original = base.slice(start, end).join(\"\");\n if (local === remote || local === original || remote === original) result.push(local === original ? remote : local);\n else result.push({ id: \"\", path: filePath, kind: \"text\", base: original, local, remote });\n cursor = end;\n }\n result.push(base.slice(cursor).join(\"\"));\n return result;\n}\n\nexport function buildAgentEvidence(\n plan: FileMergePlan,\n local: Snapshot,\n remote: Snapshot,\n ancestors: SnapshotFile[],\n): AgentEvidence {\n const evidence: AgentEvidence = { units: [], files: [], unsupported: [] };\n if (plan.kind !== \"planned\") return evidence;\n if (plan.conflicts.length > AGENT_MAX_UNITS)\n throw new Error(\"Agent mode paused: too many conflict paths for bounded analysis.\");\n for (const conflict of plan.conflicts) {\n try {\n if (conflict.reason !== \"both-changed\") throw new Error(\"unsupported conflict\");\n const versions = [ancestors, local.files, remote.files].map((files) => {\n const file = files.find((item) => item.path === conflict.path);\n if (!file) throw new Error(\"missing ancestor/version\");\n const bytes = Buffer.from(file.contentBase64, \"base64\");\n if (bytes.length > 8192) throw new Error(\"file bound\");\n return text(bytes);\n }) as [string, string, string];\n const [base, left, right] = versions;\n const file: CandidateFile = { path: conflict.path, base, local: left, remote: right };\n let units: AgentUnit[];\n if (conflict.path === \"settings.json\") {\n const documents = versions.map((value) => parseSettingsDocument(Buffer.from(value)));\n const merge = mergeSettingsJson(\n ...(versions.map((value) => Buffer.from(value)) as [Buffer, Buffer, Buffer]),\n undefined,\n true,\n );\n if (merge.kind === \"review\" && merge.reason !== \"field-conflict\") throw new Error(\"unsupported settings\");\n const fields = new Set(merge.kind === \"review\" ? merge.fields : []);\n const groups = COUPLED_FIELDS.map((group) => [...group] as string[]);\n const coupled = new Set(groups.flat());\n groups.push(...[...fields].filter((key) => !coupled.has(key)).map((key) => [key]));\n file.fields = [];\n units = groups\n .filter((group) => group.some((key) => fields.has(key)))\n .map((group) => {\n const values = documents.map((document) =>\n JSON.stringify(\n Object.fromEntries(\n group.flatMap((key) => {\n const node = document.root.members.get(key)?.node;\n return node ? [[key, node.value]] : [];\n }),\n ),\n ),\n );\n const unit: AgentUnit = {\n id: \"\",\n path: file.path,\n kind: \"settings\",\n base: values[0] ?? \"{}\",\n local: values[1] ?? \"{}\",\n remote: values[2] ?? \"{}\",\n };\n file.fields?.push({ id: String(evidence.units.length + file.fields.length), key: group[0] ?? \"\" });\n return unit;\n });\n } else {\n if (!isMergeTextPath(conflict.path)) throw new Error(\"unsupported path\");\n file.parts = textParts(file.path, base, left, right);\n units = file.parts.filter((part): part is AgentUnit => typeof part !== \"string\");\n }\n for (const unit of units) {\n unit.id = String(evidence.units.length);\n evidence.units.push(unit);\n }\n evidence.files.push(file);\n } catch {\n evidence.unsupported.push(conflict.path);\n }\n }\n if (evidence.units.length > AGENT_MAX_UNITS || Buffer.byteLength(JSON.stringify(evidence.units)) > AGENT_INPUT_BYTES)\n throw new Error(\"Agent mode paused: conflict evidence exceeds its input bound.\");\n // Scan only the actual disclosure image; skipSecretScan never authorizes model disclosure.\n if (\n scanSnapshot({\n ...local,\n files: evidence.units.flatMap((unit) =>\n [unit.path, unit.base, unit.local, unit.remote].map((value) => snapshotFile(unit.path, Buffer.from(value))),\n ),\n }).length\n )\n throw new Error(\"Agent mode paused: possible secrets in model evidence; nothing was disclosed.\");\n return evidence;\n}\n\nexport function parseAgentCandidate(raw: string, evidence: AgentEvidence): AgentCandidate {\n if (Buffer.byteLength(raw) > AGENT_OUTPUT_BYTES) throw new Error(\"Agent output exceeds its bound.\");\n const parsed = parseJsonObjectDocument(Buffer.from(raw)).root.value as Record<string, unknown>;\n if (\n Object.keys(parsed).sort().join(\",\") !== \"results,version\" ||\n parsed.version !== 1 ||\n !Array.isArray(parsed.results) ||\n parsed.results.length !== evidence.units.length\n )\n throw new Error(\"Invalid agent candidate contract.\");\n const seen = new Set<string>();\n for (const result of parsed.results) {\n if (!result || typeof result !== \"object\" || Array.isArray(result)) throw new Error(\"Invalid agent result.\");\n const row = result as Record<string, unknown>;\n const unit = evidence.units.find((unit) => unit.id === row.id);\n if (!unit || seen.has(unit.id)) throw new Error(\"Unowned or duplicate agent unit.\");\n seen.add(unit.id);\n const keys = Object.keys(row).sort().join(\",\");\n if (keys === \"id,refuse\" && row.refuse === true) continue;\n if (unit.kind === \"settings\" && keys === \"id,source\" && (row.source === \"local\" || row.source === \"remote\"))\n continue;\n if (unit.kind === \"text\" && keys === \"content,id\" && typeof row.content === \"string\" && row.content.length > 0) {\n text(Buffer.from(row.content));\n if (/^(?:<{7}|={7}|>{7}|\\|{7})/mu.test(row.content)) throw new Error(\"Unresolved conflict markers.\");\n continue;\n }\n throw new Error(\"Unsupported agent action.\");\n }\n return parsed as unknown as AgentCandidate;\n}\n\nexport function applyAgentCandidate(\n plan: FileMergePlan,\n evidence: AgentEvidence,\n candidate: AgentCandidate,\n): FileMergePlan {\n // Persisted candidates cross a trust boundary too.\n parseAgentCandidate(JSON.stringify(candidate), evidence);\n if (plan.kind !== \"planned\") throw new Error(\"Unverified merge plan.\");\n const results = new Map(candidate.results.map((row) => [row.id, row]));\n const replacements = new Map<string, SnapshotFile>();\n for (const file of evidence.files) {\n const units = evidence.units.filter((unit) => unit.path === file.path);\n if (units.some((unit) => results.get(unit.id) && \"refuse\" in (results.get(unit.id) ?? {}))) continue;\n let bytes: Buffer;\n if (file.parts) {\n bytes = Buffer.from(\n file.parts\n .map((part) => (typeof part === \"string\" ? part : (results.get(part.id) as { content: string }).content))\n .join(\"\"),\n );\n } else {\n const selections = new Map(\n (file.fields ?? []).map(({ id, key }) => [key, (results.get(id) as { source: \"local\" | \"remote\" }).source]),\n );\n const merged = mergeSettingsJson(\n Buffer.from(file.base),\n Buffer.from(file.local),\n Buffer.from(file.remote),\n selections,\n true,\n );\n if (merged.kind !== \"merged\") throw new Error(\"Agent settings units remain unresolved.\");\n // The existing renderer/parser retains atomic values, unknown members and exact numeric policy.\n parseSettingsDocument(merged.content);\n for (const { key } of file.fields ?? []) {\n if (!selections.has(key)) throw new Error(\"Missing atomic settings selection.\");\n }\n bytes = merged.content;\n }\n if (bytes.length > AGENT_OUTPUT_BYTES) throw new Error(\"Agent file exceeds output bound.\");\n replacements.set(file.path, snapshotFile(file.path, bytes));\n }\n const decisions = plan.decisions.map((decision) => {\n const file = replacements.get(decision.path);\n return file && decision.kind === \"conflict\"\n ? { kind: \"accepted\" as const, path: file.path, source: \"merged\" as const, file }\n : decision;\n });\n return { kind: \"planned\", decisions, conflicts: decisions.filter((item) => item.kind === \"conflict\") };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["file", "unit"]
7
+ }
@@ -27,7 +27,7 @@ async function withAgentSyncLock(ctx, options, run) {
27
27
  request = error;
28
28
  }
29
29
  const { withAgentClaim, readAgentCandidate, saveAgentCandidate } = await import("./agent-store-A3F6IYWZ.ts");
30
- const { parseAgentCandidate } = await import("./agent-candidate-3JN6DJKT.ts");
30
+ const { parseAgentCandidate } = await import("./agent-candidate-GNEYR225.ts");
31
31
  const { runAgentResolver } = await import("./agent-runner-E2JU2VZ6.ts");
32
32
  return withAgentClaim(async (assertOwner) => {
33
33
  const clearStatus = () => setSyncStatus(ctx, void 0);
@@ -94,4 +94,4 @@ async function withAgentSyncLock(ctx, options, run) {
94
94
  export {
95
95
  withAgentSyncLock
96
96
  };
97
- //# sourceMappingURL=chunk-S2FMH5FT.ts.map
97
+ //# sourceMappingURL=chunk-QOC47PBC.ts.map
@@ -74,6 +74,58 @@ async function resolveContentConflicts(config, state, local, remote, plan, prote
74
74
  };
75
75
  }
76
76
 
77
+ // src/sync/package-merge.ts
78
+ import { compare, valid } from "semver";
79
+ function entries(value) {
80
+ if (!Array.isArray(value)) return void 0;
81
+ const result = /* @__PURE__ */ new Map();
82
+ for (const item of value) {
83
+ if (typeof item !== "string" && (!item || Array.isArray(item) || typeof item !== "object")) return void 0;
84
+ const source = typeof item === "string" ? item : item.source;
85
+ if (typeof source !== "string") return void 0;
86
+ const spec = source.startsWith("npm:") ? source.slice(4).trim() : void 0;
87
+ const match = spec?.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/u);
88
+ const key = match ? `npm:${match[1]}` : source;
89
+ if (result.has(key)) return void 0;
90
+ const options = typeof item === "string" ? null : Object.fromEntries(Object.entries(item).filter(([key2]) => key2 !== "source"));
91
+ result.set(key, { value: item, options, version: match?.[2] });
92
+ }
93
+ return result;
94
+ }
95
+ function mergePackageVersions(base, local, remote) {
96
+ const ancestor = entries(base ?? []);
97
+ const left = entries(local);
98
+ const right = entries(remote);
99
+ if (!ancestor || !left || !right) return void 0;
100
+ const otherEntries = (items) => [...items].filter(([key]) => !key.startsWith("npm:")).map(([, entry]) => entry.value);
101
+ if (!jsonEqual(otherEntries(ancestor), otherEntries(left)) || !jsonEqual(otherEntries(ancestor), otherEntries(right)))
102
+ return void 0;
103
+ const baseKeys = [...ancestor.keys()];
104
+ const leftKeys = [...left.keys()];
105
+ const rightKeys = [...right.keys()];
106
+ const order = jsonEqual(leftKeys, rightKeys) ? leftKeys : jsonEqual(leftKeys, baseKeys) ? rightKeys : jsonEqual(rightKeys, baseKeys) ? leftKeys : void 0;
107
+ if (!order) return void 0;
108
+ const output = [];
109
+ for (const key of /* @__PURE__ */ new Set([...order, ...baseKeys])) {
110
+ const a = ancestor.get(key);
111
+ const l = left.get(key);
112
+ const r = right.get(key);
113
+ let chosen;
114
+ if (l && r && key.startsWith("npm:") && l.version && r.version && valid(l.version) && valid(r.version) && jsonEqual(l.options, r.options)) {
115
+ const precedence = compare(l.version, r.version);
116
+ if (precedence !== 0) chosen = precedence > 0 ? l : r;
117
+ }
118
+ if (!chosen) {
119
+ if (jsonEqual(l?.value, r?.value)) chosen = l;
120
+ else if (jsonEqual(l?.value, a?.value)) chosen = r;
121
+ else if (jsonEqual(r?.value, a?.value)) chosen = l;
122
+ else return void 0;
123
+ }
124
+ if (chosen) output.push(chosen.value);
125
+ }
126
+ return output;
127
+ }
128
+
77
129
  // src/sync/settings-merge.ts
78
130
  var COUPLED_FIELDS = [
79
131
  ["defaultProvider", "defaultModel"],
@@ -82,7 +134,34 @@ var COUPLED_FIELDS = [
82
134
  ["skills", "enableSkillCommands"],
83
135
  ["enableAnalytics", "trackingId"]
84
136
  ];
85
- function mergeSettingsJson(ancestor, local, remote, selections = /* @__PURE__ */ new Map()) {
137
+ function selectPackages(base, left, right) {
138
+ const packages = mergePackageVersions(
139
+ base.root.members.get("packages")?.node.value,
140
+ left.root.members.get("packages")?.node.value,
141
+ right.root.members.get("packages")?.node.value
142
+ );
143
+ if (!packages) return void 0;
144
+ const document = [left, right].find((source) => jsonEqual(source.root.members.get("packages")?.node.value, packages)) ?? parseSettingsDocument(Buffer.from(JSON.stringify({ packages })));
145
+ const node = document.root.members.get("packages")?.node;
146
+ return node ? { document, node } : void 0;
147
+ }
148
+ function mergeAcceptedSettingsPackages(ancestor, local, remote, accepted) {
149
+ try {
150
+ const base = parseSettingsDocument(ancestor);
151
+ const left = parseSettingsDocument(local);
152
+ const right = parseSettingsDocument(remote);
153
+ const document = parseSettingsDocument(accepted);
154
+ const packages = selectPackages(base, left, right);
155
+ if (!packages || jsonEqual(packages.node.value, document.root.members.get("packages")?.node.value))
156
+ return void 0;
157
+ const selected = new Map([...document.root.members].map(([key, member]) => [key, { document, node: member.node }]));
158
+ selected.set("packages", packages);
159
+ return renderSettingsMembers(document, selected);
160
+ } catch {
161
+ return void 0;
162
+ }
163
+ }
164
+ function mergeSettingsJson(ancestor, local, remote, selections = /* @__PURE__ */ new Map(), preferHigherPackageVersions = false) {
86
165
  let documents;
87
166
  try {
88
167
  documents = [ancestor, local, remote].map(parseSettingsDocument);
@@ -97,6 +176,13 @@ function mergeSettingsJson(ancestor, local, remote, selections = /* @__PURE__ */
97
176
  units.push(...[...fields].filter((field) => !coupled.has(field)).map((field) => [field]));
98
177
  const conflicts = [];
99
178
  for (const unit of units) {
179
+ if (preferHigherPackageVersions && unit[0] === "packages") {
180
+ const packages = selectPackages(base, left, right);
181
+ if (packages) {
182
+ selected.set("packages", packages);
183
+ continue;
184
+ }
185
+ }
100
186
  const equal = (first, second) => unit.every(
101
187
  (field) => jsonEqual(first.root.members.get(field)?.node.value, second.root.members.get(field)?.node.value)
102
188
  );
@@ -123,6 +209,7 @@ export {
123
209
  snapshotFile,
124
210
  resolveContentConflicts,
125
211
  COUPLED_FIELDS,
212
+ mergeAcceptedSettingsPackages,
126
213
  mergeSettingsJson
127
214
  };
128
- //# sourceMappingURL=chunk-5Q5SSKNG.ts.map
215
+ //# sourceMappingURL=chunk-S7QS4AWK.ts.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/sync/content-conflicts.ts", "../../src/sync/package-merge.ts", "../../src/sync/settings-merge.ts"],
4
+ "sourcesContent": ["import { createHash } from \"node:crypto\";\nimport type { AnySyncConfig } from \"../settings/settings-types.js\";\nimport type { Snapshot, SnapshotFile } from \"../snapshot/snapshot-types.js\";\nimport { readMergeAncestors } from \"../state/merge-baseline-store.js\";\nimport type { SyncState } from \"../state/state-types.js\";\nimport { type FileMergePlan, mergePathIdentity } from \"./file-merge-planner.js\";\nimport { mergeSession } from \"./session-merge.js\";\nimport { isMergeTextPath, mergeText, TEXT_MERGE_WORK_LIMIT, type TextMergeBudget } from \"./text-merge.js\";\nexport function snapshotFile(filePath: string, bytes: Buffer): SnapshotFile {\n return {\n path: filePath,\n contentBase64: bytes.toString(\"base64\"),\n sha256: createHash(\"sha256\").update(bytes).digest(\"hex\"),\n };\n}\nexport async function resolveContentConflicts(\n config: AnySyncConfig,\n state: SyncState,\n local: Snapshot,\n remote: Snapshot,\n plan: FileMergePlan,\n protectedPaths: ReadonlySet<string>,\n options: { textBudget?: TextMergeBudget; signal?: AbortSignal } = {},\n): Promise<FileMergePlan> {\n if (plan.kind !== \"planned\" || !config.mergeContent) return plan;\n options.signal?.throwIfAborted();\n const budget = options.textBudget ?? { remaining: TEXT_MERGE_WORK_LIMIT };\n const decisions = [...plan.decisions];\n let ancestors: SnapshotFile[] = [];\n try {\n ancestors = (await readMergeAncestors(config, state)) ?? [];\n } catch {\n options.signal?.throwIfAborted();\n return plan;\n }\n options.signal?.throwIfAborted();\n const oursByPath = new Map(local.files.map((file) => [file.path, file]));\n const theirsByPath = new Map(remote.files.map((file) => [file.path, file]));\n const ancestorsByPath = new Map(ancestors.map((file) => [file.path, file]));\n const decisionIndex = new Map(decisions.map((decision, index) => [decision.path, index]));\n const protectedKeys = new Set([...protectedPaths].map(mergePathIdentity));\n for (const conflict of plan.conflicts) {\n options.signal?.throwIfAborted();\n if (conflict.reason !== \"both-changed\" || protectedKeys.has(mergePathIdentity(conflict.path))) continue;\n const session = conflict.path.startsWith(\"sessions/\") && conflict.path.endsWith(\".jsonl\");\n if (!session && (!isMergeTextPath(conflict.path) || budget.remaining === 0)) continue;\n const ours = oursByPath.get(conflict.path);\n const theirs = theirsByPath.get(conflict.path);\n if (!ours || !theirs) continue;\n const ancestor = ancestorsByPath.get(conflict.path);\n const base = ancestor ? Buffer.from(ancestor.contentBase64, \"base64\") : undefined;\n if (!base) continue;\n const localBytes = Buffer.from(ours.contentBase64, \"base64\");\n const remoteBytes = Buffer.from(theirs.contentBase64, \"base64\");\n const bytes = session\n ? mergeSession(base, localBytes, remoteBytes)\n : mergeText(base, localBytes, remoteBytes, { budget, signal: options.signal });\n if (!bytes) continue;\n const index = decisionIndex.get(conflict.path);\n if (index === undefined) continue;\n decisions[index] = {\n kind: \"accepted\",\n path: conflict.path,\n source: \"merged\",\n file: snapshotFile(conflict.path, bytes),\n };\n }\n return {\n kind: \"planned\",\n decisions,\n conflicts: decisions.filter((item): item is Extract<typeof item, { kind: \"conflict\" }> => item.kind === \"conflict\"),\n };\n}\n", "import { compare, valid } from \"semver\";\nimport { type JsonValue, jsonEqual } from \"./json-document.js\";\n\ninterface Entry {\n value: JsonValue;\n version?: string;\n options: JsonValue;\n}\n\nfunction entries(value: JsonValue | undefined): Map<string, Entry> | undefined {\n if (!Array.isArray(value)) return undefined;\n const result = new Map<string, Entry>();\n for (const item of value) {\n if (typeof item !== \"string\" && (!item || Array.isArray(item) || typeof item !== \"object\")) return undefined;\n const source = typeof item === \"string\" ? item : item.source;\n if (typeof source !== \"string\") return undefined;\n const spec = source.startsWith(\"npm:\") ? source.slice(4).trim() : undefined;\n const match = spec?.match(/^(@?[^@]+(?:\\/[^@]+)?)(?:@(.+))?$/u);\n const key = match ? `npm:${match[1]}` : source;\n if (result.has(key)) return undefined;\n const options =\n typeof item === \"string\" ? null : Object.fromEntries(Object.entries(item).filter(([key]) => key !== \"source\"));\n result.set(key, { value: item, options, version: match?.[2] });\n }\n return result;\n}\n\n/** Compare only explicit npm versions; do not resolve ranges, tags, Git refs or registry state. */\nexport function mergePackageVersions(\n base: JsonValue | undefined,\n local: JsonValue | undefined,\n remote: JsonValue | undefined,\n): JsonValue[] | undefined {\n const ancestor = entries(base ?? []);\n const left = entries(local);\n const right = entries(remote);\n if (!ancestor || !left || !right) return undefined;\n // Raw Git refs and local path aliases do not prove distinct package identities.\n // Leave changes to those sources under the existing atomic field policy.\n const otherEntries = (items: Map<string, Entry>) =>\n [...items].filter(([key]) => !key.startsWith(\"npm:\")).map(([, entry]) => entry.value);\n if (!jsonEqual(otherEntries(ancestor), otherEntries(left)) || !jsonEqual(otherEntries(ancestor), otherEntries(right)))\n return undefined;\n\n // Package order affects resource precedence. Merge the ordered identity list\n // atomically, then select versions without changing that order.\n const baseKeys = [...ancestor.keys()];\n const leftKeys = [...left.keys()];\n const rightKeys = [...right.keys()];\n const order = jsonEqual(leftKeys, rightKeys)\n ? leftKeys\n : jsonEqual(leftKeys, baseKeys)\n ? rightKeys\n : jsonEqual(rightKeys, baseKeys)\n ? leftKeys\n : undefined;\n if (!order) return undefined;\n const output: JsonValue[] = [];\n for (const key of new Set([...order, ...baseKeys])) {\n const a = ancestor.get(key);\n const l = left.get(key);\n const r = right.get(key);\n let chosen: Entry | undefined;\n if (\n l &&\n r &&\n key.startsWith(\"npm:\") &&\n l.version &&\n r.version &&\n valid(l.version) &&\n valid(r.version) &&\n jsonEqual(l.options, r.options)\n ) {\n const precedence = compare(l.version, r.version);\n if (precedence !== 0) chosen = precedence > 0 ? l : r;\n }\n // Build metadata and equivalent version spelling do not establish a higher version.\n if (!chosen) {\n if (jsonEqual(l?.value, r?.value)) chosen = l;\n else if (jsonEqual(l?.value, a?.value)) chosen = r;\n else if (jsonEqual(r?.value, a?.value)) chosen = l;\n else return undefined;\n }\n if (chosen) output.push(chosen.value);\n }\n return output;\n}\n", "import {\n type JsonDocument,\n type JsonNode,\n jsonEqual,\n parseSettingsDocument,\n renderSettingsMembers,\n} from \"./json-document.js\";\n\nimport { mergePackageVersions } from \"./package-merge.js\";\n\nexport type SettingsMergeResult =\n | { kind: \"merged\"; content: Buffer }\n | { kind: \"review\"; reason: \"unsupported-format\" | \"field-conflict\"; fields: string[] };\n\nexport const COUPLED_FIELDS = [\n [\"defaultProvider\", \"defaultModel\"],\n [\"queueMode\", \"steeringMode\"],\n [\"websockets\", \"transport\"],\n [\"skills\", \"enableSkillCommands\"],\n [\"enableAnalytics\", \"trackingId\"],\n] as const;\n\nfunction selectPackages(base: JsonDocument, left: JsonDocument, right: JsonDocument) {\n const packages = mergePackageVersions(\n base.root.members.get(\"packages\")?.node.value,\n left.root.members.get(\"packages\")?.node.value,\n right.root.members.get(\"packages\")?.node.value,\n );\n if (!packages) return undefined;\n const document =\n [left, right].find((source) => jsonEqual(source.root.members.get(\"packages\")?.node.value, packages)) ??\n parseSettingsDocument(Buffer.from(JSON.stringify({ packages })));\n const node = document.root.members.get(\"packages\")?.node;\n return node ? { document, node } : undefined;\n}\n\n/** Accepted files need only a package override, not another full-document field merge. */\nexport function mergeAcceptedSettingsPackages(\n ancestor: Buffer,\n local: Buffer,\n remote: Buffer,\n accepted: Buffer,\n): Buffer | undefined {\n try {\n const base = parseSettingsDocument(ancestor);\n const left = parseSettingsDocument(local);\n const right = parseSettingsDocument(remote);\n const document = parseSettingsDocument(accepted);\n const packages = selectPackages(base, left, right);\n if (!packages || jsonEqual(packages.node.value, document.root.members.get(\"packages\")?.node.value))\n return undefined;\n const selected = new Map([...document.root.members].map(([key, member]) => [key, { document, node: member.node }]));\n selected.set(\"packages\", packages);\n return renderSettingsMembers(document, selected);\n } catch {\n // Unsupported settings must not replace an accepted file with a reconstructed document.\n return undefined;\n }\n}\n\n/** Only global settings.json; objects/arrays are atomic units until their invariants are proven. */\nexport function mergeSettingsJson(\n ancestor: Buffer,\n local: Buffer,\n remote: Buffer,\n selections: ReadonlyMap<string, \"local\" | \"remote\"> = new Map(),\n preferHigherPackageVersions = false,\n): SettingsMergeResult {\n let documents: JsonDocument[];\n try {\n documents = [ancestor, local, remote].map(parseSettingsDocument);\n } catch {\n return { kind: \"review\", reason: \"unsupported-format\", fields: [] };\n }\n const [base, left, right] = documents as [JsonDocument, JsonDocument, JsonDocument];\n const selected = new Map<string, { document: JsonDocument; node: JsonNode }>();\n const fields = new Set(documents.flatMap((document) => [...document.root.members.keys()]));\n const units: string[][] = COUPLED_FIELDS.map((group) => [...group]);\n const coupled = new Set(units.flat());\n units.push(...[...fields].filter((field) => !coupled.has(field)).map((field) => [field]));\n const conflicts: string[] = [];\n for (const unit of units) {\n if (preferHigherPackageVersions && unit[0] === \"packages\") {\n const packages = selectPackages(base, left, right);\n if (packages) {\n selected.set(\"packages\", packages);\n continue;\n }\n }\n const equal = (first: JsonDocument, second: JsonDocument) =>\n unit.every((field) =>\n jsonEqual(first.root.members.get(field)?.node.value, second.root.members.get(field)?.node.value),\n );\n const choice = selections.get(unit[0] ?? \"\");\n const source = equal(left, right)\n ? left\n : equal(left, base)\n ? right\n : equal(right, base)\n ? left\n : choice === \"local\"\n ? left\n : choice === \"remote\"\n ? right\n : undefined;\n if (!source) {\n conflicts.push(...unit.filter((field) => fields.has(field)));\n continue;\n }\n for (const field of unit) {\n const member = source.root.members.get(field);\n if (member) selected.set(field, { document: source, node: member.node });\n }\n }\n if (conflicts.length) return { kind: \"review\", reason: \"field-conflict\", fields: conflicts };\n try {\n return { kind: \"merged\", content: renderSettingsMembers(left, selected) };\n } catch {\n return { kind: \"review\", reason: \"unsupported-format\", fields: [] };\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["key"]
7
+ }
@@ -66,9 +66,10 @@ import {
66
66
  expectedRemoteHead
67
67
  } from "./chunk-YDVQYUHE.ts";
68
68
  import {
69
+ mergeAcceptedSettingsPackages,
69
70
  mergeSettingsJson,
70
71
  resolveContentConflicts
71
- } from "./chunk-5Q5SSKNG.ts";
72
+ } from "./chunk-S7QS4AWK.ts";
72
73
  import {
73
74
  agentDir,
74
75
  createSnapshot,
@@ -1025,21 +1026,33 @@ async function resolveSettingsConflicts(config, state, local, remote, plan) {
1025
1026
  const decisions = [...plan.decisions];
1026
1027
  for (let index = 0; index < decisions.length; index++) {
1027
1028
  const decision = decisions[index];
1028
- if (decision?.kind !== "conflict" || decision.reason !== "both-changed" || decision.path !== "settings.json")
1029
- continue;
1029
+ if (decision?.path !== "settings.json") continue;
1030
+ if (decision.kind === "conflict" ? decision.reason !== "both-changed" : !config.agentMode) continue;
1030
1031
  const left = local.files.find((file) => file.path === decision.path);
1031
1032
  const right = remote.files.find((file) => file.path === decision.path);
1032
1033
  if (!left || !right) continue;
1033
1034
  const ancestor = await readMergeAncestor(config, state, decision.path);
1034
1035
  if (!ancestor) continue;
1035
- const merged = mergeSettingsJson(
1036
- ancestor,
1037
- Buffer.from(left.contentBase64, "base64"),
1038
- Buffer.from(right.contentBase64, "base64")
1039
- );
1040
- if (merged.kind === "review") {
1041
- fields.push(...merged.fields);
1042
- continue;
1036
+ const leftBytes = Buffer.from(left.contentBase64, "base64");
1037
+ const rightBytes = Buffer.from(right.contentBase64, "base64");
1038
+ let content;
1039
+ if (decision.kind === "accepted") {
1040
+ if (!decision.file) continue;
1041
+ const patched = mergeAcceptedSettingsPackages(
1042
+ ancestor,
1043
+ leftBytes,
1044
+ rightBytes,
1045
+ Buffer.from(decision.file.contentBase64, "base64")
1046
+ );
1047
+ if (!patched) continue;
1048
+ content = patched;
1049
+ } else {
1050
+ const merged = mergeSettingsJson(ancestor, leftBytes, rightBytes, void 0, Boolean(config.agentMode));
1051
+ if (merged.kind === "review") {
1052
+ fields.push(...merged.fields);
1053
+ continue;
1054
+ }
1055
+ content = merged.content;
1043
1056
  }
1044
1057
  decisions[index] = {
1045
1058
  kind: "accepted",
@@ -1047,8 +1060,8 @@ async function resolveSettingsConflicts(config, state, local, remote, plan) {
1047
1060
  source: "merged",
1048
1061
  file: {
1049
1062
  path: decision.path,
1050
- contentBase64: merged.content.toString("base64"),
1051
- sha256: createHash3("sha256").update(merged.content).digest("hex")
1063
+ contentBase64: content.toString("base64"),
1064
+ sha256: createHash3("sha256").update(content).digest("hex")
1052
1065
  }
1053
1066
  };
1054
1067
  }
@@ -1409,7 +1422,7 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
1409
1422
  );
1410
1423
  await validate();
1411
1424
  let resolved = { plan, fields: [] };
1412
- if (config.mergeSettings) {
1425
+ if (config.mergeSettings || config.agentMode) {
1413
1426
  try {
1414
1427
  resolved = await timings.measure(
1415
1428
  "transfer settings merge",
@@ -1443,7 +1456,7 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
1443
1456
  await validate();
1444
1457
  }
1445
1458
  if (config.agentMode && !resolution) {
1446
- const { resolveAgentPlan } = await import("./agent-plan-QCUJWBP4.ts");
1459
+ const { resolveAgentPlan } = await import("./agent-plan-K3CRYVXV.ts");
1447
1460
  await validate();
1448
1461
  plan = await resolveAgentPlan(
1449
1462
  ctx,
@@ -1937,4 +1950,4 @@ export {
1937
1950
  createSyncDecision,
1938
1951
  mergeSync
1939
1952
  };
1940
- //# sourceMappingURL=chunk-OTZRCKYP.ts.map
1953
+ //# sourceMappingURL=chunk-T3NC77TU.ts.map