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.
- package/dist/mcp_server.cjs +499 -2
- package/dist/threadnote.cjs +458 -4
- package/docs/share.md +46 -4
- package/package.json +1 -1
package/dist/mcp_server.cjs
CHANGED
|
@@ -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)) {
|
package/dist/threadnote.cjs
CHANGED
|
@@ -8902,7 +8902,35 @@ async function fetchShareUpdateStatuses(config) {
|
|
|
8902
8902
|
return statuses;
|
|
8903
8903
|
}
|
|
8904
8904
|
async function runShareSync(config, options) {
|
|
8905
|
-
const
|
|
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
|
|
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 #
|
|
295
|
-
threadnote share sync --team friends #
|
|
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`
|
|
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
|