threadnote 1.7.2 → 1.8.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.
@@ -37126,13 +37126,105 @@ async function fetchShareUpdateStatuses(config2) {
37126
37126
  }
37127
37127
  return statuses;
37128
37128
  }
37129
+ async function listShareConflicts(config2, options = {}) {
37130
+ const teams = await teamsForShareQuery(config2, options.team);
37131
+ const state = autoShareState(config2);
37132
+ await loadPendingReindexes(config2, state);
37133
+ const summaries = [];
37134
+ for (const team of teams) {
37135
+ const pending = state.pendingReindexes.get(team.name) ?? [];
37136
+ for (const change of pending) {
37137
+ if (!isShareableMemoryChange(change)) {
37138
+ continue;
37139
+ }
37140
+ summaries.push(await buildShareConflictSummary(config2, team, normalizePendingChange(team, change)));
37141
+ }
37142
+ }
37143
+ return summaries;
37144
+ }
37145
+ async function showShareConflict(config2, reference, options = {}) {
37146
+ const conflict = await readPendingShareConflict(config2, reference, options.team);
37147
+ const inspected = await inspectShareConflict(config2, conflict.team, conflict.change);
37148
+ return {
37149
+ ...inspected,
37150
+ diff: formatShareConflictDiff(inspected),
37151
+ resolutionGuidance: shareConflictResolutionGuidance(inspected.id)
37152
+ };
37153
+ }
37154
+ async function resolveShareConflict(config2, reference, options) {
37155
+ const fromFile = options.fromFile?.trim();
37156
+ const mergedContent = options.mergedContent;
37157
+ const rawTake = options.take;
37158
+ if (rawTake !== void 0 && rawTake !== "shared" && rawTake !== "local") {
37159
+ throw new Error(`Unsupported --take value "${rawTake}". Expected "shared" or "local".`);
37160
+ }
37161
+ const take = rawTake;
37162
+ if ((take ? 1 : 0) + (fromFile ? 1 : 0) + (mergedContent !== void 0 ? 1 : 0) !== 1) {
37163
+ throw new Error(
37164
+ "Choose exactly one resolution: --take shared, --take local, --from-file <path>, or mergedContent via MCP."
37165
+ );
37166
+ }
37167
+ const conflict = await readPendingShareConflict(config2, reference, options.team);
37168
+ const inspected = await inspectShareConflict(config2, conflict.team, conflict.change);
37169
+ const dryRun = options.dryRun === true;
37170
+ const ov = await openVikingCliForMode(dryRun);
37171
+ const messages = [];
37172
+ const gitMessages = [];
37173
+ const backupPath = dryRun ? void 0 : await backupShareConflict(config2, inspected);
37174
+ if (take === "shared") {
37175
+ if (inspected.status === "removed") {
37176
+ if (inspected.hasLocalContent) {
37177
+ await removeMemoryUri(config2, ov, inspected.uri, dryRun);
37178
+ messages.push(`Accepted shared deletion for ${inspected.uri}.`);
37179
+ } else {
37180
+ messages.push(`Shared deletion was already reflected in OpenViking for ${inspected.uri}.`);
37181
+ }
37182
+ } else {
37183
+ if (inspected.sharedContent === void 0) {
37184
+ throw new Error(`Cannot take shared for ${inspected.id}: shared file is missing or not readable.`);
37185
+ }
37186
+ await ensureSharedDirectoryChain(config2, ov, inspected.uri, dryRun);
37187
+ await writeMemoryFile(
37188
+ config2,
37189
+ ov,
37190
+ inspected.uri,
37191
+ inspected.sharedContent,
37192
+ inspected.hasLocalContent ? "replace" : "create",
37193
+ dryRun
37194
+ );
37195
+ messages.push(`Accepted shared file content for ${inspected.uri}.`);
37196
+ }
37197
+ } else {
37198
+ const content = await conflictResolutionContent(inspected, take, fromFile, mergedContent);
37199
+ await writeSharedConflictFile(conflict.team, inspected, content, dryRun);
37200
+ await ensureSharedDirectoryChain(config2, ov, inspected.uri, dryRun);
37201
+ await writeMemoryFile(config2, ov, inspected.uri, content, inspected.hasLocalContent ? "replace" : "create", dryRun);
37202
+ const message = options.message ?? `share: resolve ${inspected.relativePath}`;
37203
+ gitMessages.push(
37204
+ ...await publishShareGitChange(conflict.team.config.worktree, inspected.relativePath, message, {
37205
+ dryRun,
37206
+ push: options.push
37207
+ })
37208
+ );
37209
+ messages.push(
37210
+ take === "local" ? `Published local OpenViking content for ${inspected.uri}.` : `Applied merged content for ${inspected.uri}.`
37211
+ );
37212
+ }
37213
+ if (!dryRun) {
37214
+ await clearPendingShareConflict(config2, conflict.team.name, inspected.relativePath);
37215
+ }
37216
+ return { backupPath, gitMessages, id: inspected.id, messages, team: inspected.team, uri: inspected.uri };
37217
+ }
37129
37218
  async function runShareSyncQuiet(config2, state, options) {
37130
37219
  const team = await resolveTeam(config2, options.team);
37131
37220
  const git = await requiredExecutable("git");
37132
37221
  const worktree = team.config.worktree;
37133
37222
  const pendingChanges = state.pendingReindexes.get(team.name);
37134
37223
  if (pendingChanges && pendingChanges.length > 0) {
37135
- await applyAndPersistChanges(config2, team.config, state, pendingChanges, { quiet: true });
37224
+ const result = await applyAndPersistChanges(config2, team.config, state, pendingChanges, { quiet: true });
37225
+ if (result.failed.length > 0) {
37226
+ return `Shared team "${team.name}" has ${result.failed.length} pending shared memory conflict(s). Run \`threadnote share conflicts --team ${team.name}\` to inspect, then \`threadnote share conflict resolve <id> --take shared|local\` or \`--from-file <path>\`.`;
37227
+ }
37136
37228
  }
37137
37229
  if (await hasUncommittedChanges(worktree)) {
37138
37230
  return `Shared team "${team.name}" has uncommitted changes; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or resolve them.`;
@@ -37162,11 +37254,271 @@ async function runShareSyncQuiet(config2, state, options) {
37162
37254
  if (changes.length > 0) {
37163
37255
  const stillPending = state.pendingReindexes.get(team.name) ?? [];
37164
37256
  const combined = mergeChanges(stillPending, changes);
37165
- await applyAndPersistChanges(config2, team.config, state, combined, { quiet: true });
37257
+ const result = await applyAndPersistChanges(config2, team.config, state, combined, { quiet: true });
37258
+ if (result.failed.length > 0) {
37259
+ return `Shared team "${team.name}" has ${result.failed.length} pending shared memory conflict(s). Run \`threadnote share conflicts --team ${team.name}\` to inspect, then \`threadnote share conflict resolve <id> --take shared|local\` or \`--from-file <path>\`.`;
37260
+ }
37166
37261
  }
37167
37262
  }
37168
37263
  return void 0;
37169
37264
  }
37265
+ async function teamsForShareQuery(config2, teamName) {
37266
+ if (teamName) {
37267
+ return [await resolveTeam(config2, teamName)];
37268
+ }
37269
+ const teams = await readTeamsFile(config2);
37270
+ const entries = Object.entries(teams.teams);
37271
+ if (entries.length === 0) {
37272
+ throw new Error("No shared teams configured. Run: threadnote share init <remote-url>");
37273
+ }
37274
+ return entries.map(([name, team]) => ({ config: team, name }));
37275
+ }
37276
+ async function readPendingShareConflict(config2, reference, optionTeam) {
37277
+ const target = await parseShareConflictReference(config2, reference, optionTeam);
37278
+ const state = autoShareState(config2);
37279
+ await loadPendingReindexes(config2, state);
37280
+ const pending = state.pendingReindexes.get(target.team.name) ?? [];
37281
+ const change = pending.find((candidate) => candidate.relativePath === target.relativePath);
37282
+ if (!change) {
37283
+ const available = pending.filter(isShareableMemoryChange).map((candidate) => conflictId(target.team.name, candidate.relativePath));
37284
+ throw new Error(
37285
+ [
37286
+ `No pending shared memory conflict found for ${conflictId(target.team.name, target.relativePath)}.`,
37287
+ available.length > 0 ? `Pending conflicts for this team:
37288
+ ${available.map((id) => `- ${id}`).join("\n")}` : `No pending conflicts for team "${target.team.name}".`
37289
+ ].join("\n")
37290
+ );
37291
+ }
37292
+ return { change: normalizePendingChange(target.team, change), team: target.team };
37293
+ }
37294
+ async function parseShareConflictReference(config2, reference, optionTeam) {
37295
+ const trimmed = reference.trim();
37296
+ if (!trimmed) {
37297
+ throw new Error("Provide a conflict id, relative path, or viking:// shared memory URI.");
37298
+ }
37299
+ if (trimmed.startsWith("viking://")) {
37300
+ const teamName = sharedTeamNameForUri(config2, trimmed);
37301
+ if (!teamName) {
37302
+ throw new Error(`Shared memory URI does not include a configured team: ${trimmed}`);
37303
+ }
37304
+ const team2 = await resolveTeam(config2, optionTeam ?? teamName);
37305
+ return { relativePath: assertSafeShareRelativePath(vikingUriToWorktreeRelative(config2, trimmed, team2.name)), team: team2 };
37306
+ }
37307
+ const colon = trimmed.indexOf(":");
37308
+ if (colon > 0 && !trimmed.slice(0, colon).includes("/")) {
37309
+ const team2 = await resolveTeam(config2, optionTeam ?? trimmed.slice(0, colon));
37310
+ return { relativePath: assertSafeShareRelativePath(trimmed.slice(colon + 1)), team: team2 };
37311
+ }
37312
+ const team = await resolveTeam(config2, optionTeam);
37313
+ return { relativePath: assertSafeShareRelativePath(trimmed), team };
37314
+ }
37315
+ function assertSafeShareRelativePath(relativePath) {
37316
+ if (!relativePath || relativePath.startsWith("/") || relativePath.split("/").some((segment) => segment === ".." || segment.length === 0)) {
37317
+ throw new Error(`Invalid shared relative path: ${relativePath}`);
37318
+ }
37319
+ return relativePath;
37320
+ }
37321
+ function normalizePendingChange(team, change) {
37322
+ return { ...change, path: (0, import_node_path3.join)(team.config.worktree, change.relativePath) };
37323
+ }
37324
+ function isShareableMemoryChange(change) {
37325
+ const firstSegment = change.relativePath.split("/")[0];
37326
+ return change.relativePath.endsWith(".md") && SHAREABLE_MEMORY_KIND_DIRS.includes(firstSegment);
37327
+ }
37328
+ async function buildShareConflictSummary(config2, team, change) {
37329
+ const inspected = await inspectShareConflict(config2, team, change);
37330
+ return {
37331
+ hasLocalContent: inspected.hasLocalContent,
37332
+ hasPreviousContent: inspected.hasPreviousContent,
37333
+ hasSharedContent: inspected.hasSharedContent,
37334
+ id: inspected.id,
37335
+ reason: inspected.reason,
37336
+ relativePath: inspected.relativePath,
37337
+ status: inspected.status,
37338
+ team: inspected.team,
37339
+ uri: inspected.uri
37340
+ };
37341
+ }
37342
+ async function inspectShareConflict(config2, team, change) {
37343
+ const ov = await openVikingCliForMode(false);
37344
+ const uri = workfileToVikingUri(config2, team.config, change.path);
37345
+ const localContent = await readOptionalMemoryContent(config2, ov, uri);
37346
+ const shared = await readOptionalSharedConflictContent(uri, change);
37347
+ const previousContent = change.previousContent === void 0 ? void 0 : prepareSharedInboundContent(uri, change.previousContent);
37348
+ return {
37349
+ hasLocalContent: localContent !== void 0,
37350
+ hasPreviousContent: previousContent !== void 0,
37351
+ hasSharedContent: shared.content !== void 0,
37352
+ id: conflictId(team.name, change.relativePath),
37353
+ localContent,
37354
+ previousContent,
37355
+ reason: shareConflictReason(change, localContent, shared.content, previousContent, shared.error),
37356
+ relativePath: change.relativePath,
37357
+ sharedContent: shared.content,
37358
+ status: change.status,
37359
+ team: team.name,
37360
+ uri
37361
+ };
37362
+ }
37363
+ async function readOptionalSharedConflictContent(uri, change) {
37364
+ try {
37365
+ if (change.status === "removed" || !await isRegularFileNoSymlink(change.path)) {
37366
+ return {};
37367
+ }
37368
+ return { content: await readSharedInboundFileContent(uri, change.path) };
37369
+ } catch (err) {
37370
+ return { error: err instanceof Error ? err.message : String(err) };
37371
+ }
37372
+ }
37373
+ async function readOptionalMemoryContent(config2, ov, uri) {
37374
+ if (!await vikingResourceExists(ov, config2, uri)) {
37375
+ return void 0;
37376
+ }
37377
+ return readMemoryContent(config2, ov, uri, false);
37378
+ }
37379
+ function shareConflictReason(change, localContent, sharedContent, previousContent, sharedError) {
37380
+ if (sharedError) {
37381
+ return `shared file is not readable: ${sharedError}`;
37382
+ }
37383
+ if (change.status === "added") {
37384
+ if (localContent === void 0) {
37385
+ return "shared file is pending ingestion into OpenViking";
37386
+ }
37387
+ if (sharedContent === void 0) {
37388
+ return "shared file is missing or not readable";
37389
+ }
37390
+ return sharedMemoryContentsEquivalent(localContent, sharedContent) ? "pending replay is already reflected in OpenViking" : "local OpenViking content differs from the newly added shared file";
37391
+ }
37392
+ if (change.status === "modified") {
37393
+ if (localContent === void 0) {
37394
+ return "OpenViking resource is missing while a shared update is pending";
37395
+ }
37396
+ if (previousContent === void 0) {
37397
+ return "previous shared content is unavailable, so local edits cannot be distinguished from upstream edits";
37398
+ }
37399
+ return sharedMemoryContentsEquivalent(localContent, previousContent) ? "shared update is pending ingestion into OpenViking" : "local OpenViking content differs from the previous shared version";
37400
+ }
37401
+ if (localContent === void 0) {
37402
+ return "shared deletion is already reflected in OpenViking";
37403
+ }
37404
+ if (previousContent === void 0) {
37405
+ return "previous shared content is unavailable, so local deletion cannot be verified safely";
37406
+ }
37407
+ return sharedMemoryContentsEquivalent(localContent, previousContent) ? "shared deletion is pending removal from OpenViking" : "local OpenViking content differs from the deleted shared version";
37408
+ }
37409
+ async function conflictResolutionContent(conflict, take, fromFile, mergedContent) {
37410
+ const raw = fromFile !== void 0 ? await (0, import_promises4.readFile)(expandPath(fromFile), "utf8") : mergedContent !== void 0 ? mergedContent : take === "local" ? conflict.localContent : void 0;
37411
+ if (raw === void 0) {
37412
+ throw new Error(`Cannot resolve ${conflict.id}: local OpenViking content is unavailable.`);
37413
+ }
37414
+ const scrub = applyScrubber(stripPersonalProvenance(raw), { redact: false });
37415
+ if (scrub.blocker) {
37416
+ throw new Error(
37417
+ `Refusing to resolve ${conflict.id}: possible ${scrub.blocker}. Strip the sensitive value before writing it to shared memory.`
37418
+ );
37419
+ }
37420
+ return scrub.cleaned;
37421
+ }
37422
+ async function writeSharedConflictFile(team, conflict, content, dryRun) {
37423
+ const filePath = (0, import_node_path3.join)(team.config.worktree, conflict.relativePath);
37424
+ if (dryRun) {
37425
+ console.log(`Would write shared file: ${portablePath(filePath)}`);
37426
+ return;
37427
+ }
37428
+ await (0, import_promises4.mkdir)((0, import_node_path3.dirname)(filePath), { recursive: true });
37429
+ await (0, import_promises4.writeFile)(filePath, content, "utf8");
37430
+ }
37431
+ async function backupShareConflict(config2, conflict) {
37432
+ const backupDir = (0, import_node_path3.join)(
37433
+ config2.agentContextHome,
37434
+ "share",
37435
+ "conflict-backups",
37436
+ safeTimestamp(),
37437
+ conflict.team,
37438
+ ...conflict.relativePath.split("/")
37439
+ );
37440
+ await (0, import_promises4.mkdir)(backupDir, { recursive: true });
37441
+ const metadata = {
37442
+ id: conflict.id,
37443
+ reason: conflict.reason,
37444
+ relativePath: conflict.relativePath,
37445
+ status: conflict.status,
37446
+ team: conflict.team,
37447
+ uri: conflict.uri
37448
+ };
37449
+ await (0, import_promises4.writeFile)((0, import_node_path3.join)(backupDir, "metadata.json"), `${JSON.stringify(metadata, void 0, 2)}
37450
+ `, "utf8");
37451
+ if (conflict.localContent !== void 0) {
37452
+ await (0, import_promises4.writeFile)((0, import_node_path3.join)(backupDir, "local.md"), conflict.localContent, "utf8");
37453
+ }
37454
+ if (conflict.sharedContent !== void 0) {
37455
+ await (0, import_promises4.writeFile)((0, import_node_path3.join)(backupDir, "shared.md"), conflict.sharedContent, "utf8");
37456
+ }
37457
+ if (conflict.previousContent !== void 0) {
37458
+ await (0, import_promises4.writeFile)((0, import_node_path3.join)(backupDir, "previous.md"), conflict.previousContent, "utf8");
37459
+ }
37460
+ return backupDir;
37461
+ }
37462
+ async function clearPendingShareConflict(config2, teamName, relativePath) {
37463
+ const state = autoShareState(config2);
37464
+ await loadPendingReindexes(config2, state);
37465
+ const pending = state.pendingReindexes.get(teamName) ?? [];
37466
+ const remaining = pending.filter((change) => change.relativePath !== relativePath);
37467
+ if (remaining.length > 0) {
37468
+ state.pendingReindexes.set(teamName, remaining);
37469
+ } else {
37470
+ state.pendingReindexes.delete(teamName);
37471
+ }
37472
+ await writePendingReindexes(config2, state);
37473
+ }
37474
+ function conflictId(team, relativePath) {
37475
+ return `${team}:${relativePath}`;
37476
+ }
37477
+ function shareConflictResolutionGuidance(id) {
37478
+ return [
37479
+ `threadnote share conflict resolve ${id} --take shared`,
37480
+ `threadnote share conflict resolve ${id} --take local`,
37481
+ `threadnote share conflict resolve ${id} --from-file merged.md`
37482
+ ];
37483
+ }
37484
+ function formatShareConflictDiff(conflict) {
37485
+ const parts = [];
37486
+ if (conflict.previousContent !== void 0) {
37487
+ parts.push(
37488
+ formatTwoWayDiff("previous shared", conflict.previousContent, "local OpenViking", conflict.localContent)
37489
+ );
37490
+ }
37491
+ parts.push(formatTwoWayDiff("local OpenViking", conflict.localContent, "shared file", conflict.sharedContent));
37492
+ return parts.join("\n\n");
37493
+ }
37494
+ function formatTwoWayDiff(leftLabel, leftContent, rightLabel, rightContent) {
37495
+ if (leftContent === void 0 && rightContent === void 0) {
37496
+ return `${leftLabel} and ${rightLabel} are both unavailable.`;
37497
+ }
37498
+ if (leftContent === rightContent) {
37499
+ return `${leftLabel} and ${rightLabel} are identical.`;
37500
+ }
37501
+ const leftLines = splitDiffLines(leftContent);
37502
+ const rightLines = splitDiffLines(rightContent);
37503
+ const lines = [`--- ${leftLabel}`, `+++ ${rightLabel}`];
37504
+ for (const line of leftLines) {
37505
+ lines.push(`-${line}`);
37506
+ }
37507
+ for (const line of rightLines) {
37508
+ lines.push(`+${line}`);
37509
+ }
37510
+ return lines.join("\n");
37511
+ }
37512
+ function splitDiffLines(content) {
37513
+ if (content === void 0) {
37514
+ return ["<missing>"];
37515
+ }
37516
+ const lines = content.split(/\n/);
37517
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
37518
+ return lines.slice(0, -1);
37519
+ }
37520
+ return lines;
37521
+ }
37170
37522
  async function publishShareGitChange(worktree, relativePath, commitMessage, options = {}) {
37171
37523
  const dryRun = options.dryRun === true;
37172
37524
  const push = options.push !== false;
@@ -39743,6 +40095,7 @@ async function main() {
39743
40095
  "When updating the same active issue, pass project/topic or replaceUri to remember_context so duplicate durable memories or handoffs do not accumulate.",
39744
40096
  "Use compact_context with dryRun=true for scoped memory hygiene when recall surfaces overlapping active memories.",
39745
40097
  "To share a durable memory with teammates, call `share_publish` with its viking:// URI. share_publish scrubs for secrets, writes and pushes the shared copy first, then removes the personal copy after the push succeeds. Do not publish handoffs, preferences, or anything carrying machine-local paths or in-flight task context.",
40098
+ 'When recall/read reports pending shared memory conflicts, call `share_conflicts`, inspect one with `share_conflict_show`, then resolve it only after user direction with `share_conflict_resolve` using take="shared", take="local", or mergedContent.',
39746
40099
  "To share a local Codex/Claude skill or Claude command with teammates, call `share_skill` with the local file path. It publishes into the shared artifact catalog after the same scrubber checks.",
39747
40100
  "To use a team shared skill as a native local skill, call `list_shared_skills` first, then `install_shared_skill` for the selected name/agent/kind.",
39748
40101
  "Do not store secrets, customer data, raw production logs, or credentials."
@@ -39962,6 +40315,71 @@ function registerTools(server, config2) {
39962
40315
  return runSharePublishTool(config2, checkedUri.value, { message, preview, push, redact, team });
39963
40316
  }
39964
40317
  );
40318
+ server.registerTool(
40319
+ "share_conflicts",
40320
+ {
40321
+ annotations: { readOnlyHint: true, destructiveHint: false },
40322
+ description: "List pending shared memory conflicts left by share sync reindex failures. Use this when recall/read/sync reports pending shared memory conflicts. Each result includes a stable id plus exact next-step guidance for show/resolve.",
40323
+ inputSchema: {
40324
+ team: external_exports.string().optional().describe("Team name; omit to inspect all configured teams")
40325
+ }
40326
+ },
40327
+ async ({ team }) => runShareConflictsTool(config2, { team })
40328
+ );
40329
+ server.registerTool(
40330
+ "share_conflict_show",
40331
+ {
40332
+ annotations: { readOnlyHint: true, destructiveHint: false },
40333
+ description: "Show one pending shared memory conflict, including local OpenViking content vs shared file diff and safe resolution options. The id comes from share_conflicts and has the form team:durable/projects/.../topic.md; a shared viking:// URI also works.",
40334
+ inputSchema: {
40335
+ id: external_exports.string().optional().describe("Required conflict id from share_conflicts, relative path plus team, or shared viking:// URI"),
40336
+ team: external_exports.string().optional().describe("Team name when id is only a relative path")
40337
+ }
40338
+ },
40339
+ async ({ id, team }) => {
40340
+ const checkedId = requiredText(id, "share_conflict_show", "id", {
40341
+ id: "default:durable/projects/foo/bar.md"
40342
+ });
40343
+ if (!checkedId.ok) {
40344
+ return checkedId.error;
40345
+ }
40346
+ return runShareConflictShowTool(config2, checkedId.value, { team });
40347
+ }
40348
+ );
40349
+ server.registerTool(
40350
+ "share_conflict_resolve",
40351
+ {
40352
+ annotations: { readOnlyHint: false, destructiveHint: true },
40353
+ description: 'Resolve one pending shared memory conflict on the user\u2019s behalf after they choose a winner. Use take="shared" to accept the shared git file into OpenViking, take="local" to publish local OpenViking content back to the shared repo, or mergedContent to write explicit merged markdown to both places. Creates a local backup before mutation and clears only the resolved pending entry.',
40354
+ inputSchema: {
40355
+ dryRun: external_exports.boolean().optional().describe("Preview without writing OpenViking, shared files, git commits, or pending state"),
40356
+ id: external_exports.string().optional().describe("Required conflict id from share_conflicts, relative path plus team, or shared viking:// URI"),
40357
+ mergedContent: external_exports.string().optional().describe(
40358
+ "Explicit merged memory markdown. Mutually exclusive with take. MCP equivalent of CLI --from-file."
40359
+ ),
40360
+ message: external_exports.string().optional().describe("Commit message when writing local or merged content to the shared repo"),
40361
+ push: external_exports.boolean().optional().describe("Push local/merged resolution commit to remote; defaults to true"),
40362
+ take: external_exports.enum(["shared", "local"]).optional().describe("Resolution side. Mutually exclusive with mergedContent."),
40363
+ team: external_exports.string().optional().describe("Team name when id is only a relative path")
40364
+ }
40365
+ },
40366
+ async ({ dryRun, id, mergedContent, message, push, take, team }) => {
40367
+ const checkedId = requiredText(id, "share_conflict_resolve", "id", {
40368
+ id: "default:durable/projects/foo/bar.md"
40369
+ });
40370
+ if (!checkedId.ok) {
40371
+ return checkedId.error;
40372
+ }
40373
+ return runShareConflictResolveTool(config2, checkedId.value, {
40374
+ dryRun,
40375
+ mergedContent,
40376
+ message,
40377
+ push,
40378
+ take,
40379
+ team
40380
+ });
40381
+ }
40382
+ );
39965
40383
  server.registerTool(
39966
40384
  "share_skill",
39967
40385
  {
@@ -41489,6 +41907,85 @@ async function forgetVikingResourceWithRetry(config2, uri) {
41489
41907
  const ov = await requiredOpenVikingCli2();
41490
41908
  return removeVikingResourceWithRetry(ov, config2, uri);
41491
41909
  }
41910
+ async function runShareConflictsTool(config2, options) {
41911
+ try {
41912
+ const conflicts = await listShareConflicts(config2, options);
41913
+ if (conflicts.length === 0) {
41914
+ return {
41915
+ content: [
41916
+ {
41917
+ type: "text",
41918
+ text: options.team ? `No pending shared memory conflicts for team "${options.team}".` : "No pending shared memory conflicts."
41919
+ }
41920
+ ]
41921
+ };
41922
+ }
41923
+ const lines = [`Pending shared memory conflicts: ${conflicts.length}`];
41924
+ for (const conflict of conflicts) {
41925
+ lines.push(
41926
+ "",
41927
+ conflict.id,
41928
+ `uri: ${conflict.uri}`,
41929
+ `status: ${conflict.status}`,
41930
+ `reason: ${conflict.reason}`,
41931
+ `show: share_conflict_show({"id":${JSON.stringify(conflict.id)}})`,
41932
+ `take shared: share_conflict_resolve({"id":${JSON.stringify(conflict.id)},"take":"shared"})`,
41933
+ `take local: share_conflict_resolve({"id":${JSON.stringify(conflict.id)},"take":"local"})`,
41934
+ `merged: share_conflict_resolve({"id":${JSON.stringify(conflict.id)},"mergedContent":"<merged MEMORY markdown>"})`
41935
+ );
41936
+ }
41937
+ return { content: [{ type: "text", text: lines.join("\n") }] };
41938
+ } catch (err) {
41939
+ return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
41940
+ }
41941
+ }
41942
+ async function runShareConflictShowTool(config2, id, options) {
41943
+ try {
41944
+ const detail = await showShareConflict(config2, id, options);
41945
+ return {
41946
+ content: [
41947
+ {
41948
+ type: "text",
41949
+ text: [
41950
+ `Conflict: ${detail.id}`,
41951
+ `URI: ${detail.uri}`,
41952
+ `Status: ${detail.status}`,
41953
+ `Reason: ${detail.reason}`,
41954
+ "",
41955
+ detail.diff,
41956
+ "",
41957
+ "Resolve:",
41958
+ `share_conflict_resolve({"id":${JSON.stringify(detail.id)},"take":"shared"})`,
41959
+ `share_conflict_resolve({"id":${JSON.stringify(detail.id)},"take":"local"})`,
41960
+ `share_conflict_resolve({"id":${JSON.stringify(detail.id)},"mergedContent":"<merged MEMORY markdown>"})`
41961
+ ].join("\n")
41962
+ }
41963
+ ]
41964
+ };
41965
+ } catch (err) {
41966
+ return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
41967
+ }
41968
+ }
41969
+ async function runShareConflictResolveTool(config2, id, options) {
41970
+ try {
41971
+ const result = await resolveShareConflict(config2, id, {
41972
+ dryRun: options.dryRun,
41973
+ mergedContent: options.mergedContent,
41974
+ message: options.message,
41975
+ push: options.push,
41976
+ take: options.take,
41977
+ team: options.team
41978
+ });
41979
+ const lines = [...result.messages];
41980
+ if (result.backupPath) {
41981
+ lines.push(`Backup: ${result.backupPath}`);
41982
+ }
41983
+ lines.push(...result.gitMessages, `Resolved shared memory conflict: ${result.id}`);
41984
+ return { content: [{ type: "text", text: lines.join("\n") }] };
41985
+ } catch (err) {
41986
+ return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
41987
+ }
41988
+ }
41492
41989
  async function runSharePublishTool(config2, sourceUri, options) {
41493
41990
  try {
41494
41991
  if (isInSharedNamespace(config2, sourceUri)) {
@@ -8902,7 +8902,35 @@ async function fetchShareUpdateStatuses(config) {
8902
8902
  return statuses;
8903
8903
  }
8904
8904
  async function runShareSync(config, options) {
8905
- const team = await resolveTeam(config, options.team);
8905
+ const teams = await teamsForShareQuery(config, options.team);
8906
+ if (options.team) {
8907
+ const team = teams[0];
8908
+ if (!team) {
8909
+ throw new Error("No shared teams configured. Run: threadnote share init <remote-url>");
8910
+ }
8911
+ await runShareSyncForTeam(config, team, options);
8912
+ return;
8913
+ }
8914
+ const failures = [];
8915
+ for (const [index, team] of teams.entries()) {
8916
+ if (teams.length > 1) {
8917
+ console.log(`Syncing shared team "${team.name}" (${index + 1}/${teams.length})...`);
8918
+ }
8919
+ try {
8920
+ await runShareSyncForTeam(config, team, options);
8921
+ } catch (error) {
8922
+ failures.push(`${team.name}: ${error instanceof Error ? error.message : String(error)}`);
8923
+ }
8924
+ }
8925
+ if (failures.length > 0) {
8926
+ throw new Error(
8927
+ [`share sync failed for ${failures.length} shared team(s):`, ...failures.map((failure2) => `- ${failure2}`)].join(
8928
+ "\n"
8929
+ )
8930
+ );
8931
+ }
8932
+ }
8933
+ async function runShareSyncForTeam(config, team, options) {
8906
8934
  const dryRun = options.dryRun === true;
8907
8935
  const git = await requiredExecutable("git");
8908
8936
  const worktree = team.config.worktree;
@@ -8968,6 +8996,7 @@ Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or
8968
8996
  console.warn(
8969
8997
  `share sync: ${result.failed.length} file(s) could not be ingested on this run; they are persisted and will be retried on the next sync or agent recall/read.`
8970
8998
  );
8999
+ console.warn(formatShareConflictNextSteps(team.name, result.failed));
8971
9000
  }
8972
9001
  }
8973
9002
  }
@@ -8982,13 +9011,152 @@ Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or
8982
9011
  }
8983
9012
  }
8984
9013
  }
9014
+ async function runShareConflicts(config, options) {
9015
+ const conflicts = await listShareConflicts(config, options);
9016
+ if (conflicts.length === 0) {
9017
+ const team = options.team ? ` for team "${options.team}"` : "";
9018
+ console.log(`No pending shared memory conflicts${team}.`);
9019
+ return;
9020
+ }
9021
+ console.log(`Pending shared memory conflicts: ${conflicts.length}`);
9022
+ for (const conflict of conflicts) {
9023
+ console.log("");
9024
+ console.log(`${conflict.id}`);
9025
+ console.log(` uri: ${conflict.uri}`);
9026
+ console.log(` status: ${conflict.status}`);
9027
+ console.log(` reason: ${conflict.reason}`);
9028
+ console.log(` show: threadnote share conflict show ${conflict.id}`);
9029
+ console.log(` take shared: threadnote share conflict resolve ${conflict.id} --take shared`);
9030
+ console.log(` take local: threadnote share conflict resolve ${conflict.id} --take local`);
9031
+ console.log(` merged file: threadnote share conflict resolve ${conflict.id} --from-file merged.md`);
9032
+ }
9033
+ }
9034
+ async function runShareConflictShow(config, reference, options) {
9035
+ const detail = await showShareConflict(config, reference, options);
9036
+ console.log(`Conflict: ${detail.id}`);
9037
+ console.log(`URI: ${detail.uri}`);
9038
+ console.log(`Status: ${detail.status}`);
9039
+ console.log(`Reason: ${detail.reason}`);
9040
+ console.log("");
9041
+ console.log(detail.diff);
9042
+ console.log("");
9043
+ console.log("Resolve:");
9044
+ for (const line of detail.resolutionGuidance) {
9045
+ console.log(` ${line}`);
9046
+ }
9047
+ }
9048
+ async function runShareConflictResolve(config, reference, options) {
9049
+ const result = await resolveShareConflict(config, reference, options);
9050
+ for (const message of result.messages) {
9051
+ console.log(message);
9052
+ }
9053
+ if (result.backupPath) {
9054
+ console.log(`Backup: ${portablePath(result.backupPath)}`);
9055
+ }
9056
+ for (const message of result.gitMessages) {
9057
+ console.log(message);
9058
+ }
9059
+ console.log(`Resolved shared memory conflict: ${result.id}`);
9060
+ }
9061
+ async function listShareConflicts(config, options = {}) {
9062
+ const teams = await teamsForShareQuery(config, options.team);
9063
+ const state = autoShareState(config);
9064
+ await loadPendingReindexes(config, state);
9065
+ const summaries = [];
9066
+ for (const team of teams) {
9067
+ const pending = state.pendingReindexes.get(team.name) ?? [];
9068
+ for (const change of pending) {
9069
+ if (!isShareableMemoryChange(change)) {
9070
+ continue;
9071
+ }
9072
+ summaries.push(await buildShareConflictSummary(config, team, normalizePendingChange(team, change)));
9073
+ }
9074
+ }
9075
+ return summaries;
9076
+ }
9077
+ async function showShareConflict(config, reference, options = {}) {
9078
+ const conflict = await readPendingShareConflict(config, reference, options.team);
9079
+ const inspected = await inspectShareConflict(config, conflict.team, conflict.change);
9080
+ return {
9081
+ ...inspected,
9082
+ diff: formatShareConflictDiff(inspected),
9083
+ resolutionGuidance: shareConflictResolutionGuidance(inspected.id)
9084
+ };
9085
+ }
9086
+ async function resolveShareConflict(config, reference, options) {
9087
+ const fromFile = options.fromFile?.trim();
9088
+ const mergedContent = options.mergedContent;
9089
+ const rawTake = options.take;
9090
+ if (rawTake !== void 0 && rawTake !== "shared" && rawTake !== "local") {
9091
+ throw new Error(`Unsupported --take value "${rawTake}". Expected "shared" or "local".`);
9092
+ }
9093
+ const take = rawTake;
9094
+ if ((take ? 1 : 0) + (fromFile ? 1 : 0) + (mergedContent !== void 0 ? 1 : 0) !== 1) {
9095
+ throw new Error(
9096
+ "Choose exactly one resolution: --take shared, --take local, --from-file <path>, or mergedContent via MCP."
9097
+ );
9098
+ }
9099
+ const conflict = await readPendingShareConflict(config, reference, options.team);
9100
+ const inspected = await inspectShareConflict(config, conflict.team, conflict.change);
9101
+ const dryRun = options.dryRun === true;
9102
+ const ov = await openVikingCliForMode(dryRun);
9103
+ const messages = [];
9104
+ const gitMessages = [];
9105
+ const backupPath = dryRun ? void 0 : await backupShareConflict(config, inspected);
9106
+ if (take === "shared") {
9107
+ if (inspected.status === "removed") {
9108
+ if (inspected.hasLocalContent) {
9109
+ await removeMemoryUri(config, ov, inspected.uri, dryRun);
9110
+ messages.push(`Accepted shared deletion for ${inspected.uri}.`);
9111
+ } else {
9112
+ messages.push(`Shared deletion was already reflected in OpenViking for ${inspected.uri}.`);
9113
+ }
9114
+ } else {
9115
+ if (inspected.sharedContent === void 0) {
9116
+ throw new Error(`Cannot take shared for ${inspected.id}: shared file is missing or not readable.`);
9117
+ }
9118
+ await ensureSharedDirectoryChain(config, ov, inspected.uri, dryRun);
9119
+ await writeMemoryFile(
9120
+ config,
9121
+ ov,
9122
+ inspected.uri,
9123
+ inspected.sharedContent,
9124
+ inspected.hasLocalContent ? "replace" : "create",
9125
+ dryRun
9126
+ );
9127
+ messages.push(`Accepted shared file content for ${inspected.uri}.`);
9128
+ }
9129
+ } else {
9130
+ const content = await conflictResolutionContent(inspected, take, fromFile, mergedContent);
9131
+ await writeSharedConflictFile(conflict.team, inspected, content, dryRun);
9132
+ await ensureSharedDirectoryChain(config, ov, inspected.uri, dryRun);
9133
+ await writeMemoryFile(config, ov, inspected.uri, content, inspected.hasLocalContent ? "replace" : "create", dryRun);
9134
+ const message = options.message ?? `share: resolve ${inspected.relativePath}`;
9135
+ gitMessages.push(
9136
+ ...await publishShareGitChange(conflict.team.config.worktree, inspected.relativePath, message, {
9137
+ dryRun,
9138
+ push: options.push
9139
+ })
9140
+ );
9141
+ messages.push(
9142
+ take === "local" ? `Published local OpenViking content for ${inspected.uri}.` : `Applied merged content for ${inspected.uri}.`
9143
+ );
9144
+ }
9145
+ if (!dryRun) {
9146
+ await clearPendingShareConflict(config, conflict.team.name, inspected.relativePath);
9147
+ }
9148
+ return { backupPath, gitMessages, id: inspected.id, messages, team: inspected.team, uri: inspected.uri };
9149
+ }
8985
9150
  async function runShareSyncQuiet(config, state, options) {
8986
9151
  const team = await resolveTeam(config, options.team);
8987
9152
  const git = await requiredExecutable("git");
8988
9153
  const worktree = team.config.worktree;
8989
9154
  const pendingChanges = state.pendingReindexes.get(team.name);
8990
9155
  if (pendingChanges && pendingChanges.length > 0) {
8991
- await applyAndPersistChanges(config, team.config, state, pendingChanges, { quiet: true });
9156
+ const result = await applyAndPersistChanges(config, team.config, state, pendingChanges, { quiet: true });
9157
+ if (result.failed.length > 0) {
9158
+ return `Shared team "${team.name}" has ${result.failed.length} pending shared memory conflict(s). Run \`threadnote share conflicts --team ${team.name}\` to inspect, then \`threadnote share conflict resolve <id> --take shared|local\` or \`--from-file <path>\`.`;
9159
+ }
8992
9160
  }
8993
9161
  if (await hasUncommittedChanges(worktree)) {
8994
9162
  return `Shared team "${team.name}" has uncommitted changes; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or resolve them.`;
@@ -9018,11 +9186,287 @@ async function runShareSyncQuiet(config, state, options) {
9018
9186
  if (changes.length > 0) {
9019
9187
  const stillPending = state.pendingReindexes.get(team.name) ?? [];
9020
9188
  const combined = mergeChanges(stillPending, changes);
9021
- await applyAndPersistChanges(config, team.config, state, combined, { quiet: true });
9189
+ const result = await applyAndPersistChanges(config, team.config, state, combined, { quiet: true });
9190
+ if (result.failed.length > 0) {
9191
+ return `Shared team "${team.name}" has ${result.failed.length} pending shared memory conflict(s). Run \`threadnote share conflicts --team ${team.name}\` to inspect, then \`threadnote share conflict resolve <id> --take shared|local\` or \`--from-file <path>\`.`;
9192
+ }
9022
9193
  }
9023
9194
  }
9024
9195
  return void 0;
9025
9196
  }
9197
+ async function teamsForShareQuery(config, teamName) {
9198
+ if (teamName) {
9199
+ return [await resolveTeam(config, teamName)];
9200
+ }
9201
+ const teams = await readTeamsFile(config);
9202
+ const entries = Object.entries(teams.teams);
9203
+ if (entries.length === 0) {
9204
+ throw new Error("No shared teams configured. Run: threadnote share init <remote-url>");
9205
+ }
9206
+ return entries.map(([name, team]) => ({ config: team, name }));
9207
+ }
9208
+ async function readPendingShareConflict(config, reference, optionTeam) {
9209
+ const target = await parseShareConflictReference(config, reference, optionTeam);
9210
+ const state = autoShareState(config);
9211
+ await loadPendingReindexes(config, state);
9212
+ const pending = state.pendingReindexes.get(target.team.name) ?? [];
9213
+ const change = pending.find((candidate) => candidate.relativePath === target.relativePath);
9214
+ if (!change) {
9215
+ const available = pending.filter(isShareableMemoryChange).map((candidate) => conflictId(target.team.name, candidate.relativePath));
9216
+ throw new Error(
9217
+ [
9218
+ `No pending shared memory conflict found for ${conflictId(target.team.name, target.relativePath)}.`,
9219
+ available.length > 0 ? `Pending conflicts for this team:
9220
+ ${available.map((id) => `- ${id}`).join("\n")}` : `No pending conflicts for team "${target.team.name}".`
9221
+ ].join("\n")
9222
+ );
9223
+ }
9224
+ return { change: normalizePendingChange(target.team, change), team: target.team };
9225
+ }
9226
+ async function parseShareConflictReference(config, reference, optionTeam) {
9227
+ const trimmed = reference.trim();
9228
+ if (!trimmed) {
9229
+ throw new Error("Provide a conflict id, relative path, or viking:// shared memory URI.");
9230
+ }
9231
+ if (trimmed.startsWith("viking://")) {
9232
+ const teamName = sharedTeamNameForUri(config, trimmed);
9233
+ if (!teamName) {
9234
+ throw new Error(`Shared memory URI does not include a configured team: ${trimmed}`);
9235
+ }
9236
+ const team2 = await resolveTeam(config, optionTeam ?? teamName);
9237
+ return { relativePath: assertSafeShareRelativePath(vikingUriToWorktreeRelative(config, trimmed, team2.name)), team: team2 };
9238
+ }
9239
+ const colon = trimmed.indexOf(":");
9240
+ if (colon > 0 && !trimmed.slice(0, colon).includes("/")) {
9241
+ const team2 = await resolveTeam(config, optionTeam ?? trimmed.slice(0, colon));
9242
+ return { relativePath: assertSafeShareRelativePath(trimmed.slice(colon + 1)), team: team2 };
9243
+ }
9244
+ const team = await resolveTeam(config, optionTeam);
9245
+ return { relativePath: assertSafeShareRelativePath(trimmed), team };
9246
+ }
9247
+ function assertSafeShareRelativePath(relativePath) {
9248
+ if (!relativePath || relativePath.startsWith("/") || relativePath.split("/").some((segment) => segment === ".." || segment.length === 0)) {
9249
+ throw new Error(`Invalid shared relative path: ${relativePath}`);
9250
+ }
9251
+ return relativePath;
9252
+ }
9253
+ function normalizePendingChange(team, change) {
9254
+ return { ...change, path: (0, import_node_path7.join)(team.config.worktree, change.relativePath) };
9255
+ }
9256
+ function isShareableMemoryChange(change) {
9257
+ const firstSegment = change.relativePath.split("/")[0];
9258
+ return change.relativePath.endsWith(".md") && SHAREABLE_MEMORY_KIND_DIRS.includes(firstSegment);
9259
+ }
9260
+ async function buildShareConflictSummary(config, team, change) {
9261
+ const inspected = await inspectShareConflict(config, team, change);
9262
+ return {
9263
+ hasLocalContent: inspected.hasLocalContent,
9264
+ hasPreviousContent: inspected.hasPreviousContent,
9265
+ hasSharedContent: inspected.hasSharedContent,
9266
+ id: inspected.id,
9267
+ reason: inspected.reason,
9268
+ relativePath: inspected.relativePath,
9269
+ status: inspected.status,
9270
+ team: inspected.team,
9271
+ uri: inspected.uri
9272
+ };
9273
+ }
9274
+ async function inspectShareConflict(config, team, change) {
9275
+ const ov = await openVikingCliForMode(false);
9276
+ const uri = workfileToVikingUri(config, team.config, change.path);
9277
+ const localContent = await readOptionalMemoryContent(config, ov, uri);
9278
+ const shared = await readOptionalSharedConflictContent(uri, change);
9279
+ const previousContent = change.previousContent === void 0 ? void 0 : prepareSharedInboundContent(uri, change.previousContent);
9280
+ return {
9281
+ hasLocalContent: localContent !== void 0,
9282
+ hasPreviousContent: previousContent !== void 0,
9283
+ hasSharedContent: shared.content !== void 0,
9284
+ id: conflictId(team.name, change.relativePath),
9285
+ localContent,
9286
+ previousContent,
9287
+ reason: shareConflictReason(change, localContent, shared.content, previousContent, shared.error),
9288
+ relativePath: change.relativePath,
9289
+ sharedContent: shared.content,
9290
+ status: change.status,
9291
+ team: team.name,
9292
+ uri
9293
+ };
9294
+ }
9295
+ async function readOptionalSharedConflictContent(uri, change) {
9296
+ try {
9297
+ if (change.status === "removed" || !await isRegularFileNoSymlink(change.path)) {
9298
+ return {};
9299
+ }
9300
+ return { content: await readSharedInboundFileContent(uri, change.path) };
9301
+ } catch (err) {
9302
+ return { error: err instanceof Error ? err.message : String(err) };
9303
+ }
9304
+ }
9305
+ async function readOptionalMemoryContent(config, ov, uri) {
9306
+ if (!await vikingResourceExists(ov, config, uri)) {
9307
+ return void 0;
9308
+ }
9309
+ return readMemoryContent(config, ov, uri, false);
9310
+ }
9311
+ function shareConflictReason(change, localContent, sharedContent, previousContent, sharedError) {
9312
+ if (sharedError) {
9313
+ return `shared file is not readable: ${sharedError}`;
9314
+ }
9315
+ if (change.status === "added") {
9316
+ if (localContent === void 0) {
9317
+ return "shared file is pending ingestion into OpenViking";
9318
+ }
9319
+ if (sharedContent === void 0) {
9320
+ return "shared file is missing or not readable";
9321
+ }
9322
+ return sharedMemoryContentsEquivalent(localContent, sharedContent) ? "pending replay is already reflected in OpenViking" : "local OpenViking content differs from the newly added shared file";
9323
+ }
9324
+ if (change.status === "modified") {
9325
+ if (localContent === void 0) {
9326
+ return "OpenViking resource is missing while a shared update is pending";
9327
+ }
9328
+ if (previousContent === void 0) {
9329
+ return "previous shared content is unavailable, so local edits cannot be distinguished from upstream edits";
9330
+ }
9331
+ return sharedMemoryContentsEquivalent(localContent, previousContent) ? "shared update is pending ingestion into OpenViking" : "local OpenViking content differs from the previous shared version";
9332
+ }
9333
+ if (localContent === void 0) {
9334
+ return "shared deletion is already reflected in OpenViking";
9335
+ }
9336
+ if (previousContent === void 0) {
9337
+ return "previous shared content is unavailable, so local deletion cannot be verified safely";
9338
+ }
9339
+ return sharedMemoryContentsEquivalent(localContent, previousContent) ? "shared deletion is pending removal from OpenViking" : "local OpenViking content differs from the deleted shared version";
9340
+ }
9341
+ async function conflictResolutionContent(conflict, take, fromFile, mergedContent) {
9342
+ const raw = fromFile !== void 0 ? await (0, import_promises5.readFile)(expandPath(fromFile), "utf8") : mergedContent !== void 0 ? mergedContent : take === "local" ? conflict.localContent : void 0;
9343
+ if (raw === void 0) {
9344
+ throw new Error(`Cannot resolve ${conflict.id}: local OpenViking content is unavailable.`);
9345
+ }
9346
+ const scrub = applyScrubber(stripPersonalProvenance(raw), { redact: false });
9347
+ if (scrub.blocker) {
9348
+ throw new Error(
9349
+ `Refusing to resolve ${conflict.id}: possible ${scrub.blocker}. Strip the sensitive value before writing it to shared memory.`
9350
+ );
9351
+ }
9352
+ return scrub.cleaned;
9353
+ }
9354
+ async function writeSharedConflictFile(team, conflict, content, dryRun) {
9355
+ const filePath = (0, import_node_path7.join)(team.config.worktree, conflict.relativePath);
9356
+ if (dryRun) {
9357
+ console.log(`Would write shared file: ${portablePath(filePath)}`);
9358
+ return;
9359
+ }
9360
+ await (0, import_promises5.mkdir)((0, import_node_path7.dirname)(filePath), { recursive: true });
9361
+ await (0, import_promises5.writeFile)(filePath, content, "utf8");
9362
+ }
9363
+ async function backupShareConflict(config, conflict) {
9364
+ const backupDir = (0, import_node_path7.join)(
9365
+ config.agentContextHome,
9366
+ "share",
9367
+ "conflict-backups",
9368
+ safeTimestamp(),
9369
+ conflict.team,
9370
+ ...conflict.relativePath.split("/")
9371
+ );
9372
+ await (0, import_promises5.mkdir)(backupDir, { recursive: true });
9373
+ const metadata = {
9374
+ id: conflict.id,
9375
+ reason: conflict.reason,
9376
+ relativePath: conflict.relativePath,
9377
+ status: conflict.status,
9378
+ team: conflict.team,
9379
+ uri: conflict.uri
9380
+ };
9381
+ await (0, import_promises5.writeFile)((0, import_node_path7.join)(backupDir, "metadata.json"), `${JSON.stringify(metadata, void 0, 2)}
9382
+ `, "utf8");
9383
+ if (conflict.localContent !== void 0) {
9384
+ await (0, import_promises5.writeFile)((0, import_node_path7.join)(backupDir, "local.md"), conflict.localContent, "utf8");
9385
+ }
9386
+ if (conflict.sharedContent !== void 0) {
9387
+ await (0, import_promises5.writeFile)((0, import_node_path7.join)(backupDir, "shared.md"), conflict.sharedContent, "utf8");
9388
+ }
9389
+ if (conflict.previousContent !== void 0) {
9390
+ await (0, import_promises5.writeFile)((0, import_node_path7.join)(backupDir, "previous.md"), conflict.previousContent, "utf8");
9391
+ }
9392
+ return backupDir;
9393
+ }
9394
+ async function clearPendingShareConflict(config, teamName, relativePath) {
9395
+ const state = autoShareState(config);
9396
+ await loadPendingReindexes(config, state);
9397
+ const pending = state.pendingReindexes.get(teamName) ?? [];
9398
+ const remaining = pending.filter((change) => change.relativePath !== relativePath);
9399
+ if (remaining.length > 0) {
9400
+ state.pendingReindexes.set(teamName, remaining);
9401
+ } else {
9402
+ state.pendingReindexes.delete(teamName);
9403
+ }
9404
+ await writePendingReindexes(config, state);
9405
+ }
9406
+ function conflictId(team, relativePath) {
9407
+ return `${team}:${relativePath}`;
9408
+ }
9409
+ function shareConflictResolutionGuidance(id) {
9410
+ return [
9411
+ `threadnote share conflict resolve ${id} --take shared`,
9412
+ `threadnote share conflict resolve ${id} --take local`,
9413
+ `threadnote share conflict resolve ${id} --from-file merged.md`
9414
+ ];
9415
+ }
9416
+ function formatShareConflictNextSteps(teamName, changes) {
9417
+ const ids = changes.filter(isShareableMemoryChange).map((change) => conflictId(teamName, change.relativePath));
9418
+ if (ids.length === 0) {
9419
+ return `Run \`threadnote share conflicts --team ${teamName}\` to inspect pending reindexes.`;
9420
+ }
9421
+ return [
9422
+ `Resolve pending shared memory conflicts with:`,
9423
+ ` threadnote share conflicts --team ${teamName}`,
9424
+ ...ids.flatMap((id) => [
9425
+ ` threadnote share conflict show ${id}`,
9426
+ ` threadnote share conflict resolve ${id} --take shared`,
9427
+ ` threadnote share conflict resolve ${id} --take local`,
9428
+ ` threadnote share conflict resolve ${id} --from-file merged.md`
9429
+ ])
9430
+ ].join("\n");
9431
+ }
9432
+ function formatShareConflictDiff(conflict) {
9433
+ const parts = [];
9434
+ if (conflict.previousContent !== void 0) {
9435
+ parts.push(
9436
+ formatTwoWayDiff("previous shared", conflict.previousContent, "local OpenViking", conflict.localContent)
9437
+ );
9438
+ }
9439
+ parts.push(formatTwoWayDiff("local OpenViking", conflict.localContent, "shared file", conflict.sharedContent));
9440
+ return parts.join("\n\n");
9441
+ }
9442
+ function formatTwoWayDiff(leftLabel, leftContent, rightLabel, rightContent) {
9443
+ if (leftContent === void 0 && rightContent === void 0) {
9444
+ return `${leftLabel} and ${rightLabel} are both unavailable.`;
9445
+ }
9446
+ if (leftContent === rightContent) {
9447
+ return `${leftLabel} and ${rightLabel} are identical.`;
9448
+ }
9449
+ const leftLines = splitDiffLines(leftContent);
9450
+ const rightLines = splitDiffLines(rightContent);
9451
+ const lines = [`--- ${leftLabel}`, `+++ ${rightLabel}`];
9452
+ for (const line of leftLines) {
9453
+ lines.push(`-${line}`);
9454
+ }
9455
+ for (const line of rightLines) {
9456
+ lines.push(`+${line}`);
9457
+ }
9458
+ return lines.join("\n");
9459
+ }
9460
+ function splitDiffLines(content) {
9461
+ if (content === void 0) {
9462
+ return ["<missing>"];
9463
+ }
9464
+ const lines = content.split(/\n/);
9465
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
9466
+ return lines.slice(0, -1);
9467
+ }
9468
+ return lines;
9469
+ }
9026
9470
  async function stageShareableChanges(dryRun, git, worktree) {
9027
9471
  await removeOrphanPackIndexes(dryRun, git, worktree);
9028
9472
  const pathspecs = await existingShareablePathspecs(git, worktree);
@@ -17794,9 +18238,19 @@ async function main() {
17794
18238
  share.command("status").description("Show git status and ahead/behind counts for a shared team").option("--team <name>", "Team name; defaults to the configured default team").option("--dry-run", "Print git commands without running them").action(async (options) => {
17795
18239
  await runShareStatus(getRuntimeConfig(program2), options);
17796
18240
  });
17797
- share.command("sync").description("Pull, reindex, and push the shared memories repo for a team").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message when auto-committing local edits").option("--no-auto-commit", "Refuse to sync if there are uncommitted local changes").option("--no-push", "Skip the push step after pulling and reindexing").option("--dry-run", "Print actions without running them").action(async (options) => {
18241
+ share.command("sync").description("Pull, reindex, and push shared memories repos").option("--team <name>", "Team name; when omitted, syncs all configured teams").option("--message <text>", "Commit message when auto-committing local edits").option("--no-auto-commit", "Refuse to sync if there are uncommitted local changes").option("--no-push", "Skip the push step after pulling and reindexing").option("--dry-run", "Print actions without running them").action(async (options) => {
17798
18242
  await runShareSync(getRuntimeConfig(program2), options);
17799
18243
  });
18244
+ share.command("conflicts").description("List pending shared memory conflicts from failed share sync reindexes").option("--team <name>", "Team name; defaults to all configured teams").action(async (options) => {
18245
+ await runShareConflicts(getRuntimeConfig(program2), options);
18246
+ });
18247
+ const shareConflict = share.command("conflict").description("Inspect or resolve one pending shared memory conflict");
18248
+ shareConflict.command("show").description("Show local/shared content and resolution commands for one pending shared memory conflict").argument("<conflict-id>", "Conflict id from `threadnote share conflicts`, relative path, or shared viking:// URI").option("--team <name>", "Team name when conflict-id is a relative path").action(async (conflictId2, options) => {
18249
+ await runShareConflictShow(getRuntimeConfig(program2), conflictId2, options);
18250
+ });
18251
+ shareConflict.command("resolve").description("Resolve one pending shared memory conflict with shared, local, or merged content").argument("<conflict-id>", "Conflict id from `threadnote share conflicts`, relative path, or shared viking:// URI").option("--team <name>", "Team name when conflict-id is a relative path").option("--take <side>", "Resolution side: shared or local").option("--from-file <path>", "Merged memory markdown to write to both OpenViking and the shared repo").option("--message <text>", "Commit message when writing local or merged content to the shared repo").option("--no-push", "Skip pushing a local/merged resolution commit").option("--dry-run", "Print actions without writing OpenViking, shared files, git commits, or pending state").action(async (conflictId2, options) => {
18252
+ await runShareConflictResolve(getRuntimeConfig(program2), conflictId2, options);
18253
+ });
17800
18254
  share.command("publish").description("Move a personal memory into the shared team namespace, commit and push").argument("<viking-uri>", "viking:// memory URI to publish").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message override").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").option(
17801
18255
  "--preview",
17802
18256
  "Print the exact bytes that would land in the shared git repo (after frontmatter strip and scrubber redaction) without writing, committing, or pushing"
package/docs/share.md CHANGED
@@ -291,12 +291,13 @@ Manual sync remains useful when you want to publish local edits, clear a dirty
291
291
  shared worktree, resolve git conflicts, or force a sync immediately:
292
292
 
293
293
  ```bash
294
- threadnote share sync # default team
295
- threadnote share sync --team friends # other team
296
- threadnote share sync --no-push # pull only
294
+ threadnote share sync # all configured teams
295
+ threadnote share sync --team friends # one team
296
+ threadnote share sync --no-push # pull only, all configured teams
297
297
  ```
298
298
 
299
- `share sync` will auto-commit edits in Threadnote-managed share paths, fetch and
299
+ `share sync` without `--team` runs this flow for every configured team. For each
300
+ team it will auto-commit edits in Threadnote-managed share paths, fetch and
300
301
  rebase onto the configured upstream, reindex pulled markdown files into
301
302
  OpenViking (so `recall` finds them immediately), and push. Managed share paths
302
303
  are root guidance/metadata files (`README.md`, `AGENTS.md`, `CLAUDE.md`,
@@ -397,6 +398,47 @@ upstream. When git can't merge cleanly:
397
398
  Two publishes touching the same `<topic>.md` from different machines will
398
399
  collide; coordinate ownership per-topic, or use distinct topics.
399
400
 
401
+ ### Resolve pending shared memory conflicts
402
+
403
+ If `share sync` cannot safely reindex a shared memory into OpenViking, it keeps
404
+ that file in the pending reindex queue and prints resolver commands. This is
405
+ different from a git rebase conflict: the shared git worktree is clean, but the
406
+ local OpenViking resource differs from the shared file or from the previous
407
+ shared version.
408
+
409
+ Inspect pending conflicts:
410
+
411
+ ```bash
412
+ threadnote share conflicts
413
+ threadnote share conflicts --team default
414
+ threadnote share conflict show default:durable/projects/foo/bar.md
415
+ ```
416
+
417
+ Resolve exactly one pending entry:
418
+
419
+ ```bash
420
+ # Accept the shared git file into OpenViking. No git commit is made.
421
+ threadnote share conflict resolve default:durable/projects/foo/bar.md --take shared
422
+
423
+ # Publish the local OpenViking content back to the shared repo, then push.
424
+ threadnote share conflict resolve default:durable/projects/foo/bar.md --take local
425
+
426
+ # Write an explicit merge result to both OpenViking and the shared repo.
427
+ threadnote share conflict resolve default:durable/projects/foo/bar.md --from-file merged.md
428
+ ```
429
+
430
+ Each resolver writes a backup under
431
+ `~/.openviking/share/conflict-backups/<timestamp>/...` before mutating local
432
+ state. `--take shared` clears the pending item after updating or deleting the
433
+ OpenViking resource. `--take local` and `--from-file` run the same scrubber as
434
+ publish, commit the shared file, push by default, update OpenViking, and then
435
+ clear only the resolved pending item. Pass `--no-push` to leave the resolution
436
+ commit local.
437
+
438
+ MCP agents have equivalent tools: `share_conflicts`, `share_conflict_show`, and
439
+ `share_conflict_resolve`. `share_conflict_resolve` accepts `take:"shared"`,
440
+ `take:"local"`, or `mergedContent:"..."`.
441
+
400
442
  ## Cross-machine identity notes
401
443
 
402
444
  Each user clones into their own user-namespaced path. A memory authored on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "1.7.2",
3
+ "version": "1.8.0",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",