driftseal 1.1.1 → 1.1.3
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 +7 -0
- package/README.zh-CN.md +5 -0
- package/bin/driftseal-mcp.js +48 -0
- package/bin/driftseal.js +55 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -132,6 +132,7 @@ The v1 server provides:
|
|
|
132
132
|
| --- | --- |
|
|
133
133
|
| `driftseal_status`, `driftseal_log` | Read the current intent and intent history. |
|
|
134
134
|
| `driftseal_begin`, `driftseal_end` | Open and honestly close a work round. |
|
|
135
|
+
| `driftseal_absorb` | Repair merge collisions or absorb another worktree's logs while remapping colliding IDs. |
|
|
135
136
|
| `driftseal_reclaim`, `driftseal_unreclaim` | Hide meaningless closed records behind append-only markers, or restore them. |
|
|
136
137
|
| `driftseal_decision_list`, `driftseal_decision_show` | Find and read MADR records. |
|
|
137
138
|
| `driftseal_decision_add`, `driftseal_decision_update` | Add selective decisions and reconcile linked ones. |
|
|
@@ -139,6 +140,12 @@ The v1 server provides:
|
|
|
139
140
|
| `driftseal://intents/recent` | Read the ten most recent intents as a JSON resource. |
|
|
140
141
|
| `driftseal://decisions` | Read the decision catalog as a JSON resource. |
|
|
141
142
|
|
|
143
|
+
`driftseal_absorb` accepts optional incoming intent-log and decision-directory
|
|
144
|
+
paths, an `ours` or `theirs` abandon strategy, and a dry-run mode. Incoming
|
|
145
|
+
paths are read-only sources; all repaired output stays under the repository
|
|
146
|
+
fixed at server startup. The Git merge-driver form remains a CLI-only plumbing
|
|
147
|
+
command.
|
|
148
|
+
|
|
142
149
|
MCP changes only the execution surface. It does not add policy beyond the
|
|
143
150
|
repository's `AGENTS.md`, and the companion skill remains limited to discovery
|
|
144
151
|
and recovery guidance.
|
package/README.zh-CN.md
CHANGED
|
@@ -128,6 +128,7 @@ v1 server 提供:
|
|
|
128
128
|
| --- | --- |
|
|
129
129
|
| `driftseal_status`, `driftseal_log` | 读取当前 intent 和 intent 历史。 |
|
|
130
130
|
| `driftseal_begin`, `driftseal_end` | 开启并诚实关闭一轮工作。 |
|
|
131
|
+
| `driftseal_absorb` | 修复 merge 撞号,或吸收另一条 worktree 日志并重编号冲突 ID。 |
|
|
131
132
|
| `driftseal_reclaim`, `driftseal_unreclaim` | 用 append-only 标记隐藏已无意义的已关闭记录,或将其恢复。 |
|
|
132
133
|
| `driftseal_decision_list`, `driftseal_decision_show` | 查找并读取 MADR record。 |
|
|
133
134
|
| `driftseal_decision_add`, `driftseal_decision_update` | 克制地增加 decision,并 reconcile 已关联的 decision。 |
|
|
@@ -135,6 +136,10 @@ v1 server 提供:
|
|
|
135
136
|
| `driftseal://intents/recent` | 以 JSON resource 读取最近十条 intent。 |
|
|
136
137
|
| `driftseal://decisions` | 以 JSON resource 读取 decision catalog。 |
|
|
137
138
|
|
|
139
|
+
`driftseal_absorb` 可以接收另一份 intent log、decision 目录、`ours` 或 `theirs`
|
|
140
|
+
放弃策略,以及 dry-run 模式。传入的路径只作为只读来源;修复后的内容仍只会写入
|
|
141
|
+
server 启动时固定的 repository。Git merge driver 形式仍是 CLI 专用的底层命令。
|
|
142
|
+
|
|
138
143
|
MCP 只替换执行入口,不会在 repository 的 `AGENTS.md` 之外增加 policy;
|
|
139
144
|
配套 skill 也仍只负责发现与恢复工作流。
|
|
140
145
|
|
package/bin/driftseal-mcp.js
CHANGED
|
@@ -88,6 +88,19 @@ function registerTools(server, api, z) {
|
|
|
88
88
|
file: z.string(),
|
|
89
89
|
});
|
|
90
90
|
const decisionWithContent = decisionRecord.extend({ content: z.string() });
|
|
91
|
+
const absorbResult = z.object({
|
|
92
|
+
mappings: z.array(
|
|
93
|
+
z.object({
|
|
94
|
+
kind: z.enum(['intent', 'decision']),
|
|
95
|
+
from: z.string(),
|
|
96
|
+
to: z.string(),
|
|
97
|
+
})
|
|
98
|
+
),
|
|
99
|
+
abandoned: z.string().nullable(),
|
|
100
|
+
copies: z.array(z.string()),
|
|
101
|
+
outputFile: z.string(),
|
|
102
|
+
exitCode: z.number().int(),
|
|
103
|
+
});
|
|
91
104
|
const closedStatus = z.enum(END_STATUSES);
|
|
92
105
|
const decisionStatus = z.enum(DECISION_STATUSES);
|
|
93
106
|
const decisionId = z.string().regex(/^\d+$/, 'decision id must contain only digits');
|
|
@@ -181,6 +194,41 @@ function registerTools(server, api, z) {
|
|
|
181
194
|
})
|
|
182
195
|
);
|
|
183
196
|
|
|
197
|
+
server.registerTool(
|
|
198
|
+
'driftseal_absorb',
|
|
199
|
+
{
|
|
200
|
+
title: 'Absorb another DriftSeal lineage',
|
|
201
|
+
description:
|
|
202
|
+
'Repair the fixed repository after a merge collision or absorb another worktree\'s intent and decision logs, remapping colliding IDs. Omit otherLog to repair the current repository. This rewrites only the fixed repository; incoming paths are read-only sources.',
|
|
203
|
+
inputSchema: {
|
|
204
|
+
otherLog: z
|
|
205
|
+
.string()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe('Incoming events.jsonl path. Relative paths resolve from the fixed repository.'),
|
|
208
|
+
otherDecisions: z
|
|
209
|
+
.string()
|
|
210
|
+
.optional()
|
|
211
|
+
.describe('Incoming decision directory. Relative paths resolve from the fixed repository.'),
|
|
212
|
+
abandon: z
|
|
213
|
+
.enum(['ours', 'theirs'])
|
|
214
|
+
.optional()
|
|
215
|
+
.describe('Side whose open intent to abandon when both lineages have one in progress.'),
|
|
216
|
+
dryRun: z.boolean().default(false),
|
|
217
|
+
},
|
|
218
|
+
outputSchema: { root: z.string(), result: absorbResult },
|
|
219
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
220
|
+
},
|
|
221
|
+
async (input) =>
|
|
222
|
+
guarded(() => {
|
|
223
|
+
const result = api.absorb(input);
|
|
224
|
+
const action = input.dryRun ? 'Absorb dry run' : 'Absorb';
|
|
225
|
+
return success(
|
|
226
|
+
{ root: api.root, result },
|
|
227
|
+
`${action} completed with ${result.mappings.length} ID remapping(s).`
|
|
228
|
+
);
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
|
|
184
232
|
server.registerTool(
|
|
185
233
|
'driftseal_reclaim',
|
|
186
234
|
{
|
package/bin/driftseal.js
CHANGED
|
@@ -1686,7 +1686,7 @@ function directoryDigest(directory) {
|
|
|
1686
1686
|
function visit(current, relative) {
|
|
1687
1687
|
const stat = fs.lstatSync(current);
|
|
1688
1688
|
if (stat.isDirectory()) {
|
|
1689
|
-
digest.update(`directory\0${relative}\0
|
|
1689
|
+
digest.update(`directory\0${relative}\0`);
|
|
1690
1690
|
for (const name of fs.readdirSync(current).sort()) {
|
|
1691
1691
|
visit(path.join(current, name), relative ? path.join(relative, name) : name);
|
|
1692
1692
|
}
|
|
@@ -1709,6 +1709,19 @@ function directoryDigest(directory) {
|
|
|
1709
1709
|
return digest.digest('hex');
|
|
1710
1710
|
}
|
|
1711
1711
|
|
|
1712
|
+
function preserveRegularFileModes(source, destination) {
|
|
1713
|
+
for (const name of fs.readdirSync(source)) {
|
|
1714
|
+
const sourcePath = path.join(source, name);
|
|
1715
|
+
const destinationPath = path.join(destination, name);
|
|
1716
|
+
const stat = fs.lstatSync(sourcePath);
|
|
1717
|
+
if (stat.isDirectory()) {
|
|
1718
|
+
preserveRegularFileModes(sourcePath, destinationPath);
|
|
1719
|
+
} else if (stat.isFile()) {
|
|
1720
|
+
fs.chmodSync(destinationPath, stat.mode & 0o777);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1712
1725
|
function installSkill(request) {
|
|
1713
1726
|
const { force, root, scope, skillDir, skillsDir, target, targetLabel } = request;
|
|
1714
1727
|
const sourceDir = path.join(__dirname, '..', 'skills', SKILL_NAME);
|
|
@@ -1736,6 +1749,7 @@ function installSkill(request) {
|
|
|
1736
1749
|
let movedExisting = false;
|
|
1737
1750
|
try {
|
|
1738
1751
|
fs.cpSync(sourceDir, temporary, { recursive: true, errorOnExist: true });
|
|
1752
|
+
preserveRegularFileModes(sourceDir, temporary);
|
|
1739
1753
|
if (existingDigest !== null) {
|
|
1740
1754
|
fs.renameSync(skillDir, backup);
|
|
1741
1755
|
movedExisting = true;
|
|
@@ -2301,6 +2315,7 @@ function planDecisionAbsorb({ oursEntries, theirsEntries, baseEntries = [], base
|
|
|
2301
2315
|
const mappings = [];
|
|
2302
2316
|
const copies = [];
|
|
2303
2317
|
const decisionMap = new Map();
|
|
2318
|
+
const hashMap = new Map();
|
|
2304
2319
|
const usedIds = new Set(oursEntries.map((entry) => entry.id));
|
|
2305
2320
|
const oursById = new Map();
|
|
2306
2321
|
const baseById = new Map();
|
|
@@ -2377,17 +2392,21 @@ function planDecisionAbsorb({ oursEntries, theirsEntries, baseEntries = [], base
|
|
|
2377
2392
|
usedIds.add(newId);
|
|
2378
2393
|
decisionMap.set(entry.id, newId);
|
|
2379
2394
|
mappings.push({ kind: 'decision', from: entry.id, to: newId });
|
|
2395
|
+
const rewritten = rewriteDecisionId(theirsContent, newId);
|
|
2396
|
+
const originalHash = contentHash(theirsContent);
|
|
2397
|
+
const rewrittenHash = contentHash(rewritten);
|
|
2398
|
+
if (originalHash !== rewrittenHash) hashMap.set(originalHash, rewrittenHash);
|
|
2380
2399
|
copies.push({
|
|
2381
2400
|
fromFile: entry.file,
|
|
2382
2401
|
toFile: `${newId}-${decisionSlugFromFile(entry.file)}.md`,
|
|
2383
|
-
content:
|
|
2402
|
+
content: rewritten,
|
|
2384
2403
|
removeFile: entry.file !== ours.file ? entry.file : null,
|
|
2385
2404
|
});
|
|
2386
2405
|
}
|
|
2387
|
-
return { decisionMap, mappings, copies };
|
|
2406
|
+
return { decisionMap, hashMap, mappings, copies };
|
|
2388
2407
|
}
|
|
2389
2408
|
|
|
2390
|
-
function remapEvent(event, intentMap, decisionMap) {
|
|
2409
|
+
function remapEvent(event, intentMap, decisionMap, hashMap = new Map()) {
|
|
2391
2410
|
const next = { ...event };
|
|
2392
2411
|
if (intentMap.has(event.id)) next.id = intentMap.get(event.id);
|
|
2393
2412
|
if (Array.isArray(next.decisions) && next.decisions.length > 0) {
|
|
@@ -2400,10 +2419,15 @@ function remapEvent(event, intentMap, decisionMap) {
|
|
|
2400
2419
|
const normalized = normalizeDecisionId(next.decisionId);
|
|
2401
2420
|
if (decisionMap.has(normalized)) next.decisionId = decisionMap.get(normalized);
|
|
2402
2421
|
}
|
|
2422
|
+
for (const field of ['oldHash', 'newHash', 'fileHash']) {
|
|
2423
|
+
if (typeof next[field] === 'string' && hashMap.has(next[field])) {
|
|
2424
|
+
next[field] = hashMap.get(next[field]);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2403
2427
|
return next;
|
|
2404
2428
|
}
|
|
2405
2429
|
|
|
2406
|
-
function remapTheirsRecords(theirsNew, oursUsedEvents, decisionMap) {
|
|
2430
|
+
function remapTheirsRecords(theirsNew, oursUsedEvents, decisionMap, hashMap = new Map()) {
|
|
2407
2431
|
const intentMap = new Map();
|
|
2408
2432
|
const mappings = [];
|
|
2409
2433
|
const used = [...oursUsedEvents];
|
|
@@ -2415,14 +2439,14 @@ function remapTheirsRecords(theirsNew, oursUsedEvents, decisionMap) {
|
|
|
2415
2439
|
intentMap.set(event.id, newId);
|
|
2416
2440
|
mappings.push({ kind: 'intent', from: event.id, to: newId });
|
|
2417
2441
|
}
|
|
2418
|
-
event = remapEvent(event, intentMap, decisionMap);
|
|
2442
|
+
event = remapEvent(event, intentMap, decisionMap, hashMap);
|
|
2419
2443
|
used.push(event);
|
|
2420
2444
|
return { event };
|
|
2421
2445
|
});
|
|
2422
2446
|
return { records, mappings };
|
|
2423
2447
|
}
|
|
2424
2448
|
|
|
2425
|
-
function repairDuplicateIntentRecords(records, decisionMap) {
|
|
2449
|
+
function repairDuplicateIntentRecords(records, decisionMap, hashMap = new Map()) {
|
|
2426
2450
|
const seenBegins = new Set();
|
|
2427
2451
|
const intentMap = new Map();
|
|
2428
2452
|
const used = [];
|
|
@@ -2440,7 +2464,12 @@ function repairDuplicateIntentRecords(records, decisionMap) {
|
|
|
2440
2464
|
} else if (event.type === 'begin') {
|
|
2441
2465
|
seenBegins.add(event.id);
|
|
2442
2466
|
}
|
|
2443
|
-
const remapped = remapEvent(
|
|
2467
|
+
const remapped = remapEvent(
|
|
2468
|
+
event,
|
|
2469
|
+
intentMap,
|
|
2470
|
+
incomingSide ? decisionMap : new Map(),
|
|
2471
|
+
incomingSide ? hashMap : new Map()
|
|
2472
|
+
);
|
|
2444
2473
|
const changed = remapped !== event && JSON.stringify(remapped) !== JSON.stringify(event);
|
|
2445
2474
|
result.push(changed ? { event: remapped } : record);
|
|
2446
2475
|
used.push(result.at(-1).event);
|
|
@@ -2658,7 +2687,8 @@ function absorbFromStreams(ours, theirs, baseRecords, options) {
|
|
|
2658
2687
|
const remapped = remapTheirsRecords(
|
|
2659
2688
|
streams.theirsNew,
|
|
2660
2689
|
[...streams.base, ...streams.oursNew].map((record) => record.event),
|
|
2661
|
-
decisionPlan.decisionMap
|
|
2690
|
+
decisionPlan.decisionMap,
|
|
2691
|
+
decisionPlan.hashMap
|
|
2662
2692
|
);
|
|
2663
2693
|
const result = [...streams.base, ...streams.oursNew, ...remapped.records];
|
|
2664
2694
|
return finishAbsorb({
|
|
@@ -2755,7 +2785,11 @@ function absorbLogs(otherFile, otherDecisions, { abandon, dryRun }) {
|
|
|
2755
2785
|
baseEntries: gitBase ? gitDecisionEntries(gitBase) : [],
|
|
2756
2786
|
baseIds: gitBase ? gitDecisionIds(gitBase) : new Set(),
|
|
2757
2787
|
});
|
|
2758
|
-
const repaired = repairDuplicateIntentRecords(
|
|
2788
|
+
const repaired = repairDuplicateIntentRecords(
|
|
2789
|
+
loaded.records,
|
|
2790
|
+
decisionPlan.decisionMap,
|
|
2791
|
+
decisionPlan.hashMap
|
|
2792
|
+
);
|
|
2759
2793
|
if (decisionPlan.mappings.length > 0 && !repaired.incomingSide) {
|
|
2760
2794
|
fail(
|
|
2761
2795
|
'cannot determine which intent records own the duplicate decision; ' +
|
|
@@ -3568,6 +3602,17 @@ function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
|
|
|
3568
3602
|
if (all) argv.push('--all');
|
|
3569
3603
|
return call(argv);
|
|
3570
3604
|
},
|
|
3605
|
+
absorb({ otherLog, otherDecisions, abandon, dryRun = false } = {}) {
|
|
3606
|
+
if (abandon && !['ours', 'theirs'].includes(abandon)) {
|
|
3607
|
+
fail('absorb abandon must be "ours" or "theirs"');
|
|
3608
|
+
}
|
|
3609
|
+
const argv = ['absorb'];
|
|
3610
|
+
if (otherLog) argv.push(String(otherLog));
|
|
3611
|
+
appendFlag(argv, '--decisions', otherDecisions);
|
|
3612
|
+
if (abandon) argv.push(`--abandon-${abandon}`);
|
|
3613
|
+
if (dryRun) argv.push('--dry-run');
|
|
3614
|
+
return call(argv);
|
|
3615
|
+
},
|
|
3571
3616
|
reclaim({ ids = [], reason, olderThan, force = false, dryRun = false }) {
|
|
3572
3617
|
const argv = ['reclaim', ...ids.map(String), '--reason', reason];
|
|
3573
3618
|
appendFlag(argv, '--older-than', olderThan);
|