driftseal 3.2.0 → 3.3.5
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/README.md +11 -1
- package/README.zh-CN.md +9 -1
- package/bin/driftseal-mcp.js +10 -2
- package/bin/driftseal.js +211 -25
- package/package.json +1 -1
- package/skills/use-driftseal/SKILL.md +6 -2
package/README.md
CHANGED
|
@@ -216,7 +216,7 @@ driftseal decision update 1 --status accepted --note "Confirmed by the final imp
|
|
|
216
216
|
| `driftseal lane add\|switch\|assign\|show` | Partition history by long-lived capability. |
|
|
217
217
|
| `driftseal reclaim [id ...] --reason "..." [--force]` | Hide meaningless closed records with append-only markers. |
|
|
218
218
|
| `driftseal unreclaim <id> --reason "..."` | Restore a reclaimed record. |
|
|
219
|
-
| `driftseal absorb [other-events.jsonl] [--decisions dir] [--abandon-theirs\|--abandon-ours]` | Merge another lineage
|
|
219
|
+
| `driftseal absorb [other-events.jsonl] [--decisions dir] [--abandon-theirs\|--abandon-ours]` | Merge another lineage, remap colliding ids, and repair stale MADR Decision History references. |
|
|
220
220
|
| `driftseal decision add\|update\|list\|show` | Manage MADR records. |
|
|
221
221
|
| `driftseal migrate v1-to-v2 inspect --json [migration paths]` | Normalize v1 state for model-assisted grouping. |
|
|
222
222
|
| `driftseal migrate v1-to-v2 apply --plan <file> [migration paths]` | Validate a grouping plan and stage the v2 seal beside v1. |
|
|
@@ -314,6 +314,16 @@ ids, rebinds affected contract hashes, and refuses concurrent edits of a shared
|
|
|
314
314
|
MADR. If both lineages remain open, choose explicitly with `--abandon-theirs` or
|
|
315
315
|
`--abandon-ours`.
|
|
316
316
|
|
|
317
|
+
`absorb` also repairs managed MADR Decision History outcome references by joining
|
|
318
|
+
their reconciliation IDs to the merged log, and rebinds matching content hashes.
|
|
319
|
+
It uses the local seal's `madr/` directory by default; when importing another log,
|
|
320
|
+
its sibling `../madr/` is the default source (`--decisions` overrides that source).
|
|
321
|
+
Run `driftseal absorb --dry-run` to preview repairs, including stale references
|
|
322
|
+
left by an older merge whose outcome IDs are already unique. Entries without a
|
|
323
|
+
matching reconciliation ID are left unchanged. If the Git merge driver reports
|
|
324
|
+
that decision history requires worktree repair, run `driftseal absorb`, stage the
|
|
325
|
+
repaired log and MADRs, then finish the merge.
|
|
326
|
+
|
|
317
327
|
## Node API and MCP
|
|
318
328
|
|
|
319
329
|
```js
|
package/README.zh-CN.md
CHANGED
|
@@ -196,7 +196,7 @@ driftseal decision update 1 --status accepted --note "Confirmed by the final imp
|
|
|
196
196
|
| `driftseal lane add\|switch\|assign\|show` | 按长期能力切分历史。 |
|
|
197
197
|
| `driftseal reclaim [id ...] --reason "..." [--force]` | 通过 append-only marker 隐藏无意义的已关闭记录。 |
|
|
198
198
|
| `driftseal unreclaim <id> --reason "..."` | 恢复 reclaimed record。 |
|
|
199
|
-
| `driftseal absorb [other-events.jsonl] [--decisions dir] [--abandon-theirs\|--abandon-ours]` | 合并另一条 lineage
|
|
199
|
+
| `driftseal absorb [other-events.jsonl] [--decisions dir] [--abandon-theirs\|--abandon-ours]` | 合并另一条 lineage,处理撞号,并修复 MADR Decision History 中的过期 outcome 引用。 |
|
|
200
200
|
| `driftseal decision add\|update\|list\|show` | 管理 MADR。 |
|
|
201
201
|
| `driftseal migrate v1-to-v2 inspect --json [migration paths]` | 规范化 v1 状态,供模型分组。 |
|
|
202
202
|
| `driftseal migrate v1-to-v2 apply --plan <file> [migration paths]` | 校验分组计划,并在 v1 旁边创建 v2 seal。 |
|
|
@@ -284,6 +284,14 @@ driftseal absorb
|
|
|
284
284
|
contract hash,并拒绝自动合并 shared MADR 的并发编辑。若两条 lineage 都处于 open
|
|
285
285
|
状态,必须显式选择 `--abandon-theirs` 或 `--abandon-ours`。
|
|
286
286
|
|
|
287
|
+
`absorb` 还会通过 reconciliation ID 将 MADR 的 Decision History 条目与合并后的 log
|
|
288
|
+
对应,修正其中的 outcome 引用,并同步匹配的内容 hash。默认处理当前 seal 的 `madr/`;
|
|
289
|
+
导入其他 log 时,默认从该 log 的 `../madr/` 读取 MADR,可用 `--decisions` 指定其他来源。
|
|
290
|
+
运行 `driftseal absorb --dry-run` 可预览修复;旧版本合并后留下的错链,即使 outcome ID
|
|
291
|
+
已经没有冲突,也可以修复。找不到对应 reconciliation ID 的条目会保持原样。若 Git merge
|
|
292
|
+
driver 提示 decision history 需要修复,运行 `driftseal absorb`,将修复后的 log 和 MADR
|
|
293
|
+
加入 staging area,再完成 merge。
|
|
294
|
+
|
|
287
295
|
## Node API 与 MCP
|
|
288
296
|
|
|
289
297
|
```js
|
package/bin/driftseal-mcp.js
CHANGED
|
@@ -135,6 +135,7 @@ function registerTools(server, api, z) {
|
|
|
135
135
|
abandoned: z.string().nullable(),
|
|
136
136
|
copies: z.array(z.string()),
|
|
137
137
|
outputFile: z.string(),
|
|
138
|
+
repairs: z.number().int().nonnegative(),
|
|
138
139
|
exitCode: z.number().int(),
|
|
139
140
|
});
|
|
140
141
|
const closedStatus = z.enum(END_STATUSES);
|
|
@@ -416,7 +417,7 @@ function registerTools(server, api, z) {
|
|
|
416
417
|
{
|
|
417
418
|
title: 'Absorb another DriftSeal lineage',
|
|
418
419
|
description:
|
|
419
|
-
'Repair the fixed repository after a merge collision or absorb another worktree\'s outcome and MADR logs, remapping colliding IDs.',
|
|
420
|
+
'Repair the fixed repository after a merge collision or absorb another worktree\'s outcome and MADR logs, remapping colliding IDs and repairing stale Decision History outcome references. Run with no incoming log after Git stops for decision-id or decision-history worktree repair.',
|
|
420
421
|
inputSchema: {
|
|
421
422
|
otherLog: z
|
|
422
423
|
.string()
|
|
@@ -439,9 +440,16 @@ function registerTools(server, api, z) {
|
|
|
439
440
|
guarded(() => {
|
|
440
441
|
const result = api.absorb(input);
|
|
441
442
|
const action = input.dryRun ? 'Absorb dry run' : 'Absorb';
|
|
443
|
+
const parts = [];
|
|
444
|
+
if (result.mappings.length > 0) {
|
|
445
|
+
parts.push(`${result.mappings.length} ID remapping(s)`);
|
|
446
|
+
}
|
|
447
|
+
if (result.repairs > 0) {
|
|
448
|
+
parts.push(`${result.repairs} decision history repair(s)`);
|
|
449
|
+
}
|
|
442
450
|
return success(
|
|
443
451
|
{ root: api.root, result },
|
|
444
|
-
`${action} completed with ${
|
|
452
|
+
`${action} completed with ${parts.join(' and ') || 'nothing to apply'}.`
|
|
445
453
|
);
|
|
446
454
|
})
|
|
447
455
|
);
|
package/bin/driftseal.js
CHANGED
|
@@ -2636,7 +2636,10 @@ Seal root: \`.seal/\` (override with \`$DRIFTSEAL_HOME\`); outcome log:
|
|
|
2636
2636
|
${INTENT_PROTOCOL_END}`;
|
|
2637
2637
|
}
|
|
2638
2638
|
|
|
2639
|
-
function
|
|
2639
|
+
function intentProtocolBlockV21Text(version = PROTOCOL_VERSION, language = DEFAULT_LOG_LANGUAGE, localLog = false, { historyRepair = true } = {}) {
|
|
2640
|
+
const absorbWhen = historyRepair
|
|
2641
|
+
? 'collisions or when Decision History outcome references are stale'
|
|
2642
|
+
: 'collisions';
|
|
2640
2643
|
return `${INTENT_PROTOCOL_MARKER}
|
|
2641
2644
|
<!-- driftseal-version: ${version} -->
|
|
2642
2645
|
<!-- driftseal-log-language: ${language} -->${localLog ? '\n<!-- driftseal-local-log: true -->' : ''}
|
|
@@ -2688,13 +2691,21 @@ ${outcomeLogLanguageParagraph(language)}
|
|
|
2688
2691
|
**Log access goes only through DriftSeal.** Never read, edit, move, or delete
|
|
2689
2692
|
\`.seal/outcomes/events.jsonl\` (or its configured equivalent) directly. Use
|
|
2690
2693
|
\`reclaim\`/\`unreclaim\` for visibility markers and \`absorb\` after merge
|
|
2691
|
-
|
|
2694
|
+
${absorbWhen}. These operations preserve append-only single-lineage history.
|
|
2692
2695
|
|
|
2693
2696
|
Seal root: \`.seal/\` (override with \`$DRIFTSEAL_HOME\`); outcome log:
|
|
2694
2697
|
\`.seal/outcomes/events.jsonl\`; ${localLog ? 'keep `.seal/` local and untracked.' : 'commit `.seal/` with the code.'}
|
|
2695
2698
|
${INTENT_PROTOCOL_END}`;
|
|
2696
2699
|
}
|
|
2697
2700
|
|
|
2701
|
+
function intentProtocolBlockV21(version = PROTOCOL_VERSION, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
2702
|
+
return intentProtocolBlockV21Text(version, language, localLog, { historyRepair: true });
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
function intentProtocolBlockV21AbsorbCollisions(version = PROTOCOL_VERSION, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
2706
|
+
return intentProtocolBlockV21Text(version, language, localLog, { historyRepair: false });
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2698
2709
|
function intentProtocolBlock(version = PROTOCOL_VERSION, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
2699
2710
|
if (String(version) === '2.0') return intentProtocolBlockV20(language, localLog);
|
|
2700
2711
|
return intentProtocolBlockV21(version, language, localLog);
|
|
@@ -2991,7 +3002,19 @@ function protocolEol(content, eol) {
|
|
|
2991
3002
|
return eol === '\n' ? content : content.replace(/\n/g, eol);
|
|
2992
3003
|
}
|
|
2993
3004
|
|
|
3005
|
+
function decisionProtocolBlockAbsorbCollisions(version = PROTOCOL_VERSION, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
3006
|
+
return decisionProtocolBlockText(version, language, localLog, { historyRepair: false });
|
|
3007
|
+
}
|
|
3008
|
+
|
|
2994
3009
|
function decisionProtocolBlock(version = PROTOCOL_VERSION, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
3010
|
+
if (String(version) === '2.0') return decisionProtocolBlockAbsorbCollisions(version, language, localLog);
|
|
3011
|
+
return decisionProtocolBlockText(version, language, localLog, { historyRepair: true });
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
function decisionProtocolBlockText(version = PROTOCOL_VERSION, language = DEFAULT_LOG_LANGUAGE, localLog = false, { historyRepair = true } = {}) {
|
|
3015
|
+
const absorbSentence = historyRepair
|
|
3016
|
+
? 'After a\nmerge, `driftseal absorb` remaps colliding ids and repairs managed Decision History\noutcome references; it never auto-merges concurrent edits of a shared MADR.'
|
|
3017
|
+
: 'After a\nmerge, `driftseal absorb` remaps colliding ids; it never auto-merges concurrent\nedits of a shared MADR.';
|
|
2995
3018
|
return `${DECISION_PROTOCOL_MARKER}
|
|
2996
3019
|
<!-- driftseal-decisions-version: ${version} -->
|
|
2997
3020
|
<!-- driftseal-log-language: ${language} -->${localLog ? '\n<!-- driftseal-local-log: true -->' : ''}
|
|
@@ -3009,9 +3032,7 @@ ${decisionLogLanguageParagraph(language)}
|
|
|
3009
3032
|
|
|
3010
3033
|
Use \`proposed|accepted|rejected|deferred|deprecated|superseded\` statuses. Link
|
|
3011
3034
|
existing MADRs from \`begin\` or \`extend\`, then reconcile each linked record
|
|
3012
|
-
with \`driftseal decision update\` before successful or partial closure.
|
|
3013
|
-
merge, \`driftseal absorb\` remaps colliding ids; it never auto-merges concurrent
|
|
3014
|
-
edits of a shared MADR.
|
|
3035
|
+
with \`driftseal decision update\` before successful or partial closure. ${absorbSentence}
|
|
3015
3036
|
${localLog ? 'Keep `.seal/madr/` local and untracked.' : 'Commit `.seal/madr/` with the code.'}
|
|
3016
3037
|
${DECISION_PROTOCOL_END}`;
|
|
3017
3038
|
}
|
|
@@ -3111,6 +3132,14 @@ function previousDecisionProtocolBlock(version, language = DEFAULT_LOG_LANGUAGE,
|
|
|
3111
3132
|
return v8.replace(' --driver "<decision driver>"', '');
|
|
3112
3133
|
}
|
|
3113
3134
|
|
|
3135
|
+
function customizedProtocolBlockError(marker) {
|
|
3136
|
+
return (
|
|
3137
|
+
`cannot safely upgrade customized protocol block beginning with ${marker}; ` +
|
|
3138
|
+
'the block may come from a newer DriftSeal release even when its protocol version matches ' +
|
|
3139
|
+
`(current client: ${PACKAGE_VERSION}); upgrade DriftSeal and retry`
|
|
3140
|
+
);
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3114
3143
|
function upgradeManagedBlock({
|
|
3115
3144
|
content,
|
|
3116
3145
|
marker,
|
|
@@ -3161,7 +3190,7 @@ function upgradeManagedBlock({
|
|
|
3161
3190
|
key === protocolBlockKey(replacement) ||
|
|
3162
3191
|
knownManagedBlocks.some((known) => key === protocolBlockKey(known));
|
|
3163
3192
|
if (!recognized) {
|
|
3164
|
-
fail(
|
|
3193
|
+
fail(customizedProtocolBlockError(marker));
|
|
3165
3194
|
}
|
|
3166
3195
|
return {
|
|
3167
3196
|
content: content.slice(0, start) + replacement + content.slice(after),
|
|
@@ -3176,7 +3205,7 @@ function upgradeManagedBlock({
|
|
|
3176
3205
|
found: true,
|
|
3177
3206
|
};
|
|
3178
3207
|
}
|
|
3179
|
-
fail(
|
|
3208
|
+
fail(customizedProtocolBlockError(marker));
|
|
3180
3209
|
}
|
|
3181
3210
|
|
|
3182
3211
|
const MCP_TARGETS = ['codex', 'kimi-code', 'opencode', 'claude-code', 'cursor'];
|
|
@@ -3421,6 +3450,7 @@ const SKILL_RELEASE_DIGESTS = new Set([
|
|
|
3421
3450
|
'b4b2cc27ea71c5777b5eb9b67d861fbe1da43f31ab5d422d6a877e0cad592493', // 2.1.0 lane re-anchor recovery
|
|
3422
3451
|
'6c49523c2f4a36ea1cd9d0b4e793ea3cdbd9d299174cf1f9a0a54f3d9a7cd331', // workspace park sidecar wording
|
|
3423
3452
|
'678cbaa8380f75f7a45d634010b500e51a59b06ea759eaf23cff2980142662f0', // default-seal parking help wording
|
|
3453
|
+
'b5f2379fba311e19a5139dd6eb80795473fa6754fa959f90d4a36cc273b56e8f', // absorb Decision History repair
|
|
3424
3454
|
]);
|
|
3425
3455
|
|
|
3426
3456
|
function skillInstallUsage() {
|
|
@@ -4226,6 +4256,75 @@ function rewriteDecisionId(content, newId) {
|
|
|
4226
4256
|
return content.replace(/^# [0-9]+\. /, `# ${String(BigInt(newId))}. `);
|
|
4227
4257
|
}
|
|
4228
4258
|
|
|
4259
|
+
function decisionIdFromFile(file) {
|
|
4260
|
+
const match = path.basename(file).match(/^(\d{4,})-/);
|
|
4261
|
+
if (!match) fail(`invalid decision filename: ${file}`);
|
|
4262
|
+
return normalizeDecisionId(match[1]);
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
function reconciliationOutcomes(records) {
|
|
4266
|
+
return new Map(records
|
|
4267
|
+
.filter((record) => record.event.type === 'decision_reconcile_prepare')
|
|
4268
|
+
.map((record) => [record.event.reconciliationId, record.event]));
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
function rewriteDecisionHistory(content, decisionId, outcomes) {
|
|
4272
|
+
let repairs = 0;
|
|
4273
|
+
const rewritten = content.replace(
|
|
4274
|
+
/^(<!-- [a-z][a-z0-9-]*-reconciliation: ([^>\r\n]+) -->\r?\n### [^\r\n]+ — (?:Outcome|Intent) `)(\d{4}-\d{2}-\d{2}-\d{3,})(`\r?$)/gm,
|
|
4275
|
+
(entry, prefix, reconciliationId, outcomeId, suffix) => {
|
|
4276
|
+
const prepare = outcomes.get(reconciliationId);
|
|
4277
|
+
if (!prepare || normalizeDecisionId(prepare.decisionId) !== decisionId || prepare.id === outcomeId) {
|
|
4278
|
+
return entry;
|
|
4279
|
+
}
|
|
4280
|
+
repairs++;
|
|
4281
|
+
return `${prefix}${prepare.id}${suffix}`;
|
|
4282
|
+
}
|
|
4283
|
+
);
|
|
4284
|
+
return { content: rewritten, repairs };
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
function planDecisionHistoryRepair(records, copies, entries) {
|
|
4288
|
+
const outcomes = reconciliationOutcomes(records);
|
|
4289
|
+
const targets = new Map(entries.map((entry) => [entry.file, entry]));
|
|
4290
|
+
const plannedCopies = new Map(copies.map((copy) => [copy.toFile, copy]));
|
|
4291
|
+
const hashMap = new Map();
|
|
4292
|
+
let repairs = 0;
|
|
4293
|
+
for (const copy of copies) {
|
|
4294
|
+
if (copy.removeFile) targets.delete(copy.removeFile);
|
|
4295
|
+
}
|
|
4296
|
+
for (const copy of copies) {
|
|
4297
|
+
targets.set(copy.toFile, {
|
|
4298
|
+
file: copy.toFile,
|
|
4299
|
+
id: decisionIdFromFile(copy.toFile),
|
|
4300
|
+
content: copy.content,
|
|
4301
|
+
});
|
|
4302
|
+
}
|
|
4303
|
+
for (const entry of targets.values()) {
|
|
4304
|
+
const content = entry.content !== undefined ? entry.content : fs.readFileSync(entry.path, 'utf8');
|
|
4305
|
+
const rewritten = rewriteDecisionHistory(content, entry.id, outcomes);
|
|
4306
|
+
if (rewritten.repairs === 0) continue;
|
|
4307
|
+
repairs += rewritten.repairs;
|
|
4308
|
+
// Rebind only hashes that attest the exact pre-repair bytes. Unrelated edits
|
|
4309
|
+
// must still fail reconciliation recovery or closure after this repair.
|
|
4310
|
+
hashMap.set(contentHash(content), contentHash(rewritten.content));
|
|
4311
|
+
plannedCopies.set(entry.file, {
|
|
4312
|
+
fromFile: entry.file,
|
|
4313
|
+
toFile: entry.file,
|
|
4314
|
+
removeFile: null,
|
|
4315
|
+
...plannedCopies.get(entry.file),
|
|
4316
|
+
content: rewritten.content,
|
|
4317
|
+
});
|
|
4318
|
+
}
|
|
4319
|
+
const unchangedIds = new Map();
|
|
4320
|
+
const rewrittenRecords = records.map((record) =>
|
|
4321
|
+
['oldHash', 'newHash', 'fileHash'].some((field) => hashMap.has(record.event[field]))
|
|
4322
|
+
? { event: remapEvent(record.event, unchangedIds, unchangedIds, hashMap) }
|
|
4323
|
+
: record
|
|
4324
|
+
);
|
|
4325
|
+
return { records: rewrittenRecords, copies: [...plannedCopies.values()], repairs };
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4229
4328
|
function splitDuplicateDecisions(entries) {
|
|
4230
4329
|
const ours = [];
|
|
4231
4330
|
const theirs = [];
|
|
@@ -4676,6 +4775,7 @@ function finishAbsorb({
|
|
|
4676
4775
|
outcomeCount,
|
|
4677
4776
|
allowConflict = false,
|
|
4678
4777
|
followupMessage = null,
|
|
4778
|
+
repairDecisionHistory = true,
|
|
4679
4779
|
}) {
|
|
4680
4780
|
// A parked outcome is local even though the tracked log never saw it.
|
|
4681
4781
|
const park = shouldAttachInProgress(outputFile)
|
|
@@ -4703,33 +4803,52 @@ function finishAbsorb({
|
|
|
4703
4803
|
// A parked overlay with nothing left open in it belongs in the tracked log, whether the
|
|
4704
4804
|
// abandon flag just closed it or an interrupted end left it closed.
|
|
4705
4805
|
const flushOverlay = parkedClosed || (overlay.length > 0 && !parkedOpen);
|
|
4706
|
-
const merged = flushOverlay ? [...result, ...overlay] : result;
|
|
4707
4806
|
const effective = [...result, ...overlay].map((record) => record.event);
|
|
4708
4807
|
fold(effective);
|
|
4709
4808
|
if (!conflict) openOutcome(fold(effective));
|
|
4809
|
+
const history = repairDecisionHistory
|
|
4810
|
+
? planDecisionHistoryRepair(
|
|
4811
|
+
[...result, ...overlay], copies, listDecisionEntries(decisionDir(), { allowDuplicates: true })
|
|
4812
|
+
)
|
|
4813
|
+
: { records: [...result, ...overlay], copies, repairs: 0 };
|
|
4814
|
+
const repairedResult = history.records.slice(0, result.length);
|
|
4815
|
+
const repairedOverlay = history.records.slice(result.length);
|
|
4816
|
+
const merged = flushOverlay ? history.records : repairedResult;
|
|
4817
|
+
fold(history.records.map((record) => record.event));
|
|
4710
4818
|
if (!dryRun) {
|
|
4711
4819
|
writeJsonl(outputFile, merged);
|
|
4712
|
-
applyDecisionCopies(copies, dryRun);
|
|
4820
|
+
applyDecisionCopies(history.copies, dryRun);
|
|
4713
4821
|
if (plan) {
|
|
4714
4822
|
if (plan.alreadyCommitted || flushOverlay) discardInProgressLog(park);
|
|
4715
|
-
else if (plan.mappings.length > 0
|
|
4823
|
+
else if (plan.mappings.length > 0 || repairedOverlay.some((record, i) => record !== overlay[i])) {
|
|
4824
|
+
writeJsonl(park, repairedOverlay);
|
|
4825
|
+
}
|
|
4716
4826
|
}
|
|
4717
4827
|
}
|
|
4828
|
+
const historyOnly =
|
|
4829
|
+
history.repairs > 0 &&
|
|
4830
|
+
outcomeCount === 0 &&
|
|
4831
|
+
allMappings.length === 0 &&
|
|
4832
|
+
!abandoned &&
|
|
4833
|
+
!flushOverlay;
|
|
4718
4834
|
if (
|
|
4719
4835
|
outcomeCount === 0 &&
|
|
4720
4836
|
allMappings.length === 0 &&
|
|
4721
|
-
copies.length === 0 &&
|
|
4837
|
+
history.copies.length === 0 &&
|
|
4722
4838
|
!abandoned &&
|
|
4723
4839
|
!flushOverlay
|
|
4724
4840
|
) {
|
|
4725
4841
|
printLine('nothing to absorb');
|
|
4726
|
-
} else {
|
|
4842
|
+
} else if (!historyOnly) {
|
|
4727
4843
|
printAbsorbReport({
|
|
4728
4844
|
mappings: allMappings,
|
|
4729
4845
|
abandoned,
|
|
4730
4846
|
outcomeCount,
|
|
4731
4847
|
});
|
|
4732
4848
|
}
|
|
4849
|
+
if (history.repairs > 0) {
|
|
4850
|
+
printLine(`${dryRun ? 'would repair' : 'repaired'} ${history.repairs} decision history reference(s)`);
|
|
4851
|
+
}
|
|
4733
4852
|
if (conflict) {
|
|
4734
4853
|
printLine('multiple outcomes remain in progress; re-run with --abandon-theirs or --abandon-ours');
|
|
4735
4854
|
}
|
|
@@ -4737,13 +4856,14 @@ function finishAbsorb({
|
|
|
4737
4856
|
return {
|
|
4738
4857
|
mappings: allMappings,
|
|
4739
4858
|
abandoned,
|
|
4740
|
-
copies: copies.map((item) => item.toFile),
|
|
4859
|
+
copies: history.copies.map((item) => item.toFile),
|
|
4741
4860
|
outputFile,
|
|
4861
|
+
repairs: history.repairs,
|
|
4742
4862
|
exitCode: conflict || followupMessage ? 1 : 0,
|
|
4743
4863
|
};
|
|
4744
4864
|
}
|
|
4745
4865
|
|
|
4746
|
-
function
|
|
4866
|
+
function planAbsorbMerge(ours, theirs, baseRecords, options = {}) {
|
|
4747
4867
|
const streams = mergeRecordStreams(ours, theirs, baseRecords);
|
|
4748
4868
|
const decisionPlan = planDecisionAbsorb({
|
|
4749
4869
|
oursEntries: options.oursDecisionEntries || [],
|
|
@@ -4751,28 +4871,65 @@ function absorbFromStreams(ours, theirs, baseRecords, options) {
|
|
|
4751
4871
|
baseEntries: options.baseDecisionEntries || [],
|
|
4752
4872
|
baseIds: options.baseDecisionIds || new Set(),
|
|
4753
4873
|
});
|
|
4874
|
+
const oursRecords = [...streams.base, ...streams.oursNew];
|
|
4754
4875
|
const remapped = remapTheirsRecords(
|
|
4755
4876
|
dropDuplicateLaneAdds(
|
|
4756
|
-
|
|
4877
|
+
oursRecords.map((record) => record.event),
|
|
4757
4878
|
streams.theirsNew
|
|
4758
4879
|
),
|
|
4759
|
-
|
|
4880
|
+
oursRecords.map((record) => record.event),
|
|
4760
4881
|
decisionPlan.decisionMap,
|
|
4761
4882
|
decisionPlan.hashMap
|
|
4762
4883
|
);
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4884
|
+
return {
|
|
4885
|
+
decisionPlan,
|
|
4886
|
+
remapped,
|
|
4887
|
+
result: [...oursRecords, ...remapped.records],
|
|
4888
|
+
oursRecords,
|
|
4767
4889
|
theirsRecords: [...streams.base, ...remapped.records],
|
|
4768
4890
|
mappings: [...remapped.mappings, ...decisionPlan.mappings],
|
|
4769
|
-
|
|
4891
|
+
outcomeCount: streams.theirsNew.filter((record) => isOutcomeStart(record.event)).length,
|
|
4892
|
+
};
|
|
4893
|
+
}
|
|
4894
|
+
|
|
4895
|
+
function previewParkedOverlay(result, outputFile) {
|
|
4896
|
+
// Git merge drivers often receive a temporary %A that is not the live WAL, so
|
|
4897
|
+
// parked history still has to be previewed from the worktree sidecar.
|
|
4898
|
+
const park = shouldAttachInProgress(outputFile)
|
|
4899
|
+
? existingInProgressFileForLog(outputFile)
|
|
4900
|
+
: existingInProgressFile();
|
|
4901
|
+
if (!park) return [];
|
|
4902
|
+
const plan = planInProgressOverlay(
|
|
4903
|
+
result.map((record) => record.event),
|
|
4904
|
+
park,
|
|
4905
|
+
{ repairTail: true, readOnly: true }
|
|
4906
|
+
);
|
|
4907
|
+
return plan && !plan.alreadyCommitted ? plan.records : [];
|
|
4908
|
+
}
|
|
4909
|
+
|
|
4910
|
+
function decisionHistoryNeedsRepair(records, entries) {
|
|
4911
|
+
const outcomes = reconciliationOutcomes(records);
|
|
4912
|
+
return entries.some((entry) => {
|
|
4913
|
+
const content = entry.content !== undefined ? entry.content : fs.readFileSync(entry.path, 'utf8');
|
|
4914
|
+
return rewriteDecisionHistory(content, entry.id, outcomes).repairs > 0;
|
|
4915
|
+
});
|
|
4916
|
+
}
|
|
4917
|
+
|
|
4918
|
+
function absorbFromStreams(ours, theirs, baseRecords, options) {
|
|
4919
|
+
const merge = planAbsorbMerge(ours, theirs, baseRecords, options);
|
|
4920
|
+
return finishAbsorb({
|
|
4921
|
+
result: merge.result,
|
|
4922
|
+
oursRecords: merge.oursRecords,
|
|
4923
|
+
theirsRecords: merge.theirsRecords,
|
|
4924
|
+
mappings: merge.mappings,
|
|
4925
|
+
copies: merge.decisionPlan.copies,
|
|
4770
4926
|
abandon: options.abandon,
|
|
4771
4927
|
dryRun: options.dryRun,
|
|
4772
4928
|
outputFile: options.outputFile,
|
|
4773
4929
|
allowConflict: options.allowConflict,
|
|
4774
4930
|
followupMessage: options.followupMessage,
|
|
4775
|
-
|
|
4931
|
+
repairDecisionHistory: options.repairDecisionHistory,
|
|
4932
|
+
outcomeCount: merge.outcomeCount,
|
|
4776
4933
|
});
|
|
4777
4934
|
}
|
|
4778
4935
|
|
|
@@ -4920,14 +5077,19 @@ function absorbGit(baseFile, oursFile, theirsFile, { abandon, dryRun }) {
|
|
|
4920
5077
|
gitFindCommitForFile(theirsFile, '.seal/outcomes/events.jsonl') ||
|
|
4921
5078
|
gitFindCommitForFile(theirsFile, '.intent-log/events.jsonl');
|
|
4922
5079
|
const mergeBase = otherHead ? gitMergeBaseFor('HEAD', otherHead) : null;
|
|
5080
|
+
// Driver merge remaps the jsonl only. Decision copies stay with a later
|
|
5081
|
+
// worktree absorb so Git can finish merging MADR files first.
|
|
5082
|
+
const merge = planAbsorbMerge(ours.records, theirs.records, base.records, {});
|
|
4923
5083
|
let followupMessage = null;
|
|
4924
5084
|
if (!otherHead) {
|
|
4925
5085
|
followupMessage =
|
|
4926
5086
|
'incoming Git tree could not be identified safely; re-run driftseal absorb after Git stops the merge';
|
|
4927
5087
|
} else {
|
|
5088
|
+
const oursDecisionEntries = gitDecisionEntries('HEAD');
|
|
5089
|
+
const theirsDecisionEntries = gitDecisionEntries(otherHead);
|
|
4928
5090
|
const decisionPlan = planDecisionAbsorb({
|
|
4929
|
-
oursEntries:
|
|
4930
|
-
theirsEntries:
|
|
5091
|
+
oursEntries: oursDecisionEntries,
|
|
5092
|
+
theirsEntries: theirsDecisionEntries,
|
|
4931
5093
|
baseEntries: mergeBase ? gitDecisionEntries(mergeBase) : [],
|
|
4932
5094
|
baseIds: mergeBase
|
|
4933
5095
|
? gitDecisionIds(mergeBase)
|
|
@@ -4939,14 +5101,33 @@ function absorbGit(baseFile, oursFile, theirsFile, { abandon, dryRun }) {
|
|
|
4939
5101
|
if (requiresDecisionRepair) {
|
|
4940
5102
|
followupMessage =
|
|
4941
5103
|
'decision ids require worktree repair; run driftseal absorb, then stage the repaired logs';
|
|
5104
|
+
} else {
|
|
5105
|
+
const overlay = previewParkedOverlay(merge.result, oursFile);
|
|
5106
|
+
const worktreeEntries = listDecisionEntries(decisionDir(), { allowDuplicates: true });
|
|
5107
|
+
if (decisionHistoryNeedsRepair(
|
|
5108
|
+
[...merge.result, ...overlay],
|
|
5109
|
+
[...oursDecisionEntries, ...theirsDecisionEntries, ...worktreeEntries]
|
|
5110
|
+
)) {
|
|
5111
|
+
followupMessage =
|
|
5112
|
+
'decision history requires worktree repair; run driftseal absorb, then stage the repaired logs';
|
|
5113
|
+
}
|
|
4942
5114
|
}
|
|
4943
5115
|
}
|
|
4944
|
-
return
|
|
5116
|
+
return finishAbsorb({
|
|
5117
|
+
result: merge.result,
|
|
5118
|
+
oursRecords: merge.oursRecords,
|
|
5119
|
+
theirsRecords: merge.theirsRecords,
|
|
5120
|
+
mappings: merge.mappings,
|
|
5121
|
+
copies: merge.decisionPlan.copies,
|
|
4945
5122
|
abandon,
|
|
4946
5123
|
dryRun,
|
|
4947
5124
|
outputFile: oursFile,
|
|
4948
5125
|
allowConflict: !abandon,
|
|
4949
5126
|
followupMessage,
|
|
5127
|
+
// Git owns the worktree MADR merge. Stop for a normal absorb when history
|
|
5128
|
+
// needs repair rather than racing Git by writing other paths from its driver.
|
|
5129
|
+
repairDecisionHistory: false,
|
|
5130
|
+
outcomeCount: merge.outcomeCount,
|
|
4950
5131
|
});
|
|
4951
5132
|
}
|
|
4952
5133
|
|
|
@@ -6672,6 +6853,8 @@ const commands = {
|
|
|
6672
6853
|
...sourceLanguages.flatMap((source) => [
|
|
6673
6854
|
protocolEol(intentProtocolBlock(PROTOCOL_VERSION, source), eol),
|
|
6674
6855
|
protocolEol(intentProtocolBlock(PROTOCOL_VERSION, source, true), eol),
|
|
6856
|
+
protocolEol(intentProtocolBlockV21AbsorbCollisions(PROTOCOL_VERSION, source), eol),
|
|
6857
|
+
protocolEol(intentProtocolBlockV21AbsorbCollisions(PROTOCOL_VERSION, source, true), eol),
|
|
6675
6858
|
protocolEol(intentProtocolBlockV20(source), eol),
|
|
6676
6859
|
protocolEol(intentProtocolBlockV20(source, true), eol),
|
|
6677
6860
|
protocolEol(v1IntentProtocolBlock(14, source), eol),
|
|
@@ -6706,6 +6889,8 @@ const commands = {
|
|
|
6706
6889
|
...sourceLanguages.flatMap((source) => [
|
|
6707
6890
|
protocolEol(decisionProtocolBlock(PROTOCOL_VERSION, source), eol),
|
|
6708
6891
|
protocolEol(decisionProtocolBlock(PROTOCOL_VERSION, source, true), eol),
|
|
6892
|
+
protocolEol(decisionProtocolBlockAbsorbCollisions(PROTOCOL_VERSION, source), eol),
|
|
6893
|
+
protocolEol(decisionProtocolBlockAbsorbCollisions(PROTOCOL_VERSION, source, true), eol),
|
|
6709
6894
|
protocolEol(decisionProtocolBlock('2.0', source), eol),
|
|
6710
6895
|
protocolEol(decisionProtocolBlock('2.0', source, true), eol),
|
|
6711
6896
|
protocolEol(v1DecisionProtocolBlock(14, source), eol),
|
|
@@ -6807,6 +6992,7 @@ usage:
|
|
|
6807
6992
|
driftseal absorb [other-events.jsonl] [--decisions <dir>]
|
|
6808
6993
|
[--abandon-theirs | --abandon-ours] [--dry-run]
|
|
6809
6994
|
merge another outcome log, remapping colliding ids
|
|
6995
|
+
and repairing stale MADR Decision History references
|
|
6810
6996
|
driftseal absorb --git <base> <ours> <theirs>
|
|
6811
6997
|
git merge driver for .seal/outcomes/events.jsonl
|
|
6812
6998
|
driftseal decision add "<title>" --context "..." --outcome "..." [options]
|
package/package.json
CHANGED
|
@@ -44,8 +44,12 @@ driftseal help
|
|
|
44
44
|
## After a merge
|
|
45
45
|
|
|
46
46
|
If `status` or `log` fails with a duplicate id or with `multiple outcomes in
|
|
47
|
-
progress`, or the outcome log has conflict markers,
|
|
48
|
-
|
|
47
|
+
progress`, or the outcome log has conflict markers, or Git stops because
|
|
48
|
+
decision ids or Decision History need worktree repair, run `driftseal absorb`
|
|
49
|
+
instead of editing `.seal/outcomes/events.jsonl`. A merge can leave the log
|
|
50
|
+
healthy while MADR Decision History still quotes pre-merge outcome ids; `absorb`
|
|
51
|
+
(and `absorb --dry-run`) repairs those references even when ids are already
|
|
52
|
+
unique. When both sides still have an
|
|
49
53
|
open outcome, add `--abandon-theirs` or `--abandon-ours`; this works whether your
|
|
50
54
|
open outcome sits in the log or is parked beside the WAL. `driftseal init` also
|
|
51
55
|
configures the local git merge driver; clones need `init` again for that driver.
|