driftseal 1.1.3 → 1.1.6

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 CHANGED
@@ -36,7 +36,12 @@ driftseal init
36
36
 
37
37
  `driftseal init` writes the protocol to `AGENTS.md`, including how to `absorb`
38
38
  colliding worktree logs, and configures the local git merge driver. It can be
39
- run again without duplicating either. DriftSeal requires Node.js 18+.
39
+ run again without duplicating either. Pass `--lang zh-CN` (or another
40
+ [BCP 47](https://www.rfc-editor.org/rfc/rfc5646.html) tag) to declare the
41
+ language agents should use for intent and decision prose; the default is `en`.
42
+ Command names, flags, status tokens, ids, and MADR section headings stay in
43
+ English. Re-running `init` without `--lang` preserves the declared language
44
+ while upgrading the protocol. DriftSeal requires Node.js 18+.
40
45
 
41
46
  For local development from this checkout:
42
47
 
@@ -231,7 +236,7 @@ content change starts a new work round.
231
236
  | `driftseal mcp install --target TARGET [--scope project\|global] [--root path] [--force]` | Install the repository-pinned MCP server into Codex, Kimi Code, OpenCode, Claude Code, or Cursor. |
232
237
  | `driftseal hook install --target TARGET [--scope project\|global] [--root path] [--force]` | Install advisory lifecycle reminders into Kimi Code, Claude Code, or Codex. |
233
238
  | `driftseal hook prompt\|stop [--format plain\|claude-code]` | Emit the reminder a lifecycle hook injects; never blocks. |
234
- | `driftseal init` | Add the adoption protocol to `AGENTS.md` and configure the git merge driver. |
239
+ | `driftseal init [--lang <tag>]` | Add the adoption protocol to `AGENTS.md` and configure the git merge driver. `--lang` sets the intent/decision log language (BCP 47, default `en`). |
235
240
  | `driftseal --version` or `driftseal -V` | Print the installed DriftSeal version. |
236
241
  | `driftseal help` | Print CLI usage. |
237
242
 
@@ -274,8 +279,10 @@ conflicts cannot block later decision work.
274
279
  New events carry a schema version. DriftSeal rejects newer unsupported schemas and
275
280
  fails closed if a legacy client closes a linked intent without reconciliation.
276
281
  `driftseal init` writes versioned managed blocks and upgrades only exact, recognized
277
- older blocks. It refuses newer protocol versions and any unrecognized or
278
- customized block without changing `AGENTS.md`.
282
+ older blocks. Current-version blocks that differ only by log language are also
283
+ recognized, so `--lang` can change the language without rewriting policy by hand.
284
+ It refuses newer protocol versions and any unrecognized or customized block
285
+ without changing `AGENTS.md`.
279
286
 
280
287
  `--count` prints only the number of records remaining after status filtering.
281
288
  It cannot be combined with `--last`, whose limiting semantics would make the
@@ -310,6 +317,20 @@ ambiguous decision catalog. Run `driftseal absorb`, stage the repaired intent
310
317
  and decision logs, then continue the merge. Clones need `init` again because
311
318
  the driver lives in local git config.
312
319
 
320
+ In a Git worktree, `begin` parks the open intent in Git metadata instead of
321
+ appending to the tracked `events.jsonl`. Git can merge while that intent is
322
+ still in progress, so you do not need a log-only commit just to get a clean
323
+ tree. `end` moves the parked records into the tracked log and writes the closing
324
+ record there — never into Git metadata — so an interrupted `end` leaves the
325
+ intent open in the log and can simply be run again. If the parked intent's id
326
+ collides with incoming merged events, DriftSeal remaps it the same way `absorb`
327
+ remaps colliding worktree ids.
328
+
329
+ When a merge brings in a second open intent, `absorb --abandon-ours` closes the
330
+ parked one into the tracked log and `absorb --abandon-theirs` closes the
331
+ incoming one and leaves yours parked. `end <id>` also works on the incoming
332
+ intent directly, and `begin --force` abandons every open intent at once.
333
+
313
334
  ## Storage
314
335
 
315
336
  - `.intent-log/events.jsonl` is the append-only intent log. All access goes through `driftseal` (CLI or MCP) — never read, edit, move, or delete it directly; use `driftseal reclaim` to retire meaningless records instead of deleting log lines. After a merge collision, use `driftseal absorb` instead of editing the file.
package/README.zh-CN.md CHANGED
@@ -35,7 +35,11 @@ driftseal init
35
35
  ```
36
36
 
37
37
  `driftseal init` 会把协议写入 `AGENTS.md`(包括 worktree 撞号后如何 `absorb`),
38
- 并配置 local git merge driver。重复运行不会产生副本。DriftSeal 需要 Node.js 18+。
38
+ 并配置 local git merge driver。重复运行不会产生副本。用 `--lang zh-CN`(或其他
39
+ [BCP 47](https://www.rfc-editor.org/rfc/rfc5646.html) 标签)声明 agent 写入
40
+ intent / decision 正文时应使用的语言,默认是 `en`。命令名、flag、status token、
41
+ id 以及 MADR 小节标题仍保持英文。再次运行不带 `--lang` 的 `init` 会保留已声明
42
+ 的语言并升级协议。DriftSeal 需要 Node.js 18+。
39
43
 
40
44
  从当前 checkout 本地开发时:
41
45
 
@@ -220,7 +224,7 @@ Git 操作完全不计入 intent log,因为 Git 会自行维护历史。查看
220
224
  | `driftseal mcp install --target TARGET [--scope project\|global] [--root path] [--force]` | 把固定到 repository 的 MCP server 安装到 Codex、Kimi Code、OpenCode、Claude Code 或 Cursor。 |
221
225
  | `driftseal hook install --target TARGET [--scope project\|global] [--root path] [--force]` | 把建议性的 lifecycle 提醒安装到 Kimi Code、Claude Code 或 Codex。 |
222
226
  | `driftseal hook prompt\|stop [--format plain\|claude-code]` | 输出 lifecycle hook 注入的提醒;绝不阻断。 |
223
- | `driftseal init` | 把接入协议写入 `AGENTS.md`,并配置 git merge driver |
227
+ | `driftseal init [--lang <tag>]` | 把接入协议写入 `AGENTS.md`,并配置 git merge driver。`--lang` 设置 intent / decision log 的语言(BCP 47,默认 `en`)。 |
224
228
  | `driftseal --version` 或 `driftseal -V` | 输出当前安装的 DriftSeal 版本。 |
225
229
  | `driftseal help` | 查看 CLI 用法。 |
226
230
 
@@ -257,8 +261,9 @@ recovery 只处理当前 intent,因此历史冲突不会阻塞之后的 decisi
257
261
  新 event 带有 schema version。遇到更高且不支持的版本时,DriftSeal 会拒绝继续;如果
258
262
  旧 client 未经 reconciliation 就关闭 linked intent,新 client 也会 fail closed。
259
263
  `driftseal init` 会写入带版本的 managed blocks,并且只升级内容完全匹配的已知旧版本。
260
- 遇到更新的协议版本、无法识别的 block 或自定义内容时,它会保持 `AGENTS.md`
261
- 不变并拒绝继续。
264
+ 当前版本的 block 如果只是 log language 不同,也会被识别,因此可以用 `--lang`
265
+ 改语言而不必手改协议。遇到更新的协议版本、无法识别的 block 或自定义内容时,
266
+ 它会保持 `AGENTS.md` 不变并拒绝继续。
262
267
 
263
268
  `--count` 只输出 status 筛选后的记录总数。它不能与 `--last` 一起使用,以免
264
269
  “先限制再计数”造成歧义。Decision 文件名会构成一个轻量的内存索引:`show` 只
@@ -283,6 +288,18 @@ driftseal absorb ../other-worktree/.intent-log/events.jsonl \
283
288
  `driftseal absorb`、stage 修复后的 intent / decision logs,再继续 merge。clone
284
289
  之后需要再跑一次 `init`,因为 driver 只存在于 local git config。
285
290
 
291
+ 在 Git worktree 里,`begin` 会把未关闭的 intent 停在 Git 元数据中,而不是追加到
292
+ 已跟踪的 `events.jsonl`。因此工作树保持干净,`git merge` 可以在 intent 仍进行中
293
+ 时执行,不必为了清工作树而多做一个只含日志的提交。`end` 会先把停放的记录移入跟踪
294
+ 日志,再把关闭记录写在那里,而不会写进 Git 元数据;因此 `end` 中途失败时,intent
295
+ 只是以未关闭状态留在日志里,重跑一次即可。如果停放中的 intent id 与合并进来的事件
296
+ 撞号,DriftSeal 会按 `absorb` 同样的规则重编号。
297
+
298
+ 合并带进来第二个未关闭的 intent 时,`absorb --abandon-ours` 会关闭停放的那一个并
299
+ 写入跟踪日志,`absorb --abandon-theirs` 则关闭合并进来的那一个、保留自己的停放
300
+ 状态。也可以用 `end <id>` 直接关闭合并进来的 intent,或用 `begin --force` 一次性
301
+ 放弃所有未关闭的 intent。
302
+
286
303
  ## 数据保存在哪里
287
304
 
288
305
  - `.intent-log/events.jsonl`:append-only intent log。所有读写都必须经过 `driftseal`(CLI 或 MCP)——不要直接读取、修改、移动或删除该文件;需要让无意义的记录退场时使用 `driftseal reclaim`,而不是删除日志行。合并撞号时用 `driftseal absorb`,不要手改这个文件。
package/bin/driftseal.js CHANGED
@@ -16,6 +16,7 @@
16
16
  * { "type": "end", "id", "ts", "status", "note", "verifyResult" }
17
17
  *
18
18
  * Intent log: $DRIFTSEAL_HOME/events.jsonl, or .intent-log/events.jsonl in cwd.
19
+ * In a Git worktree, an open intent is parked in Git metadata until end.
19
20
  * Decision log: $DRIFTSEAL_DECISION_HOME, or .decision-log/ in cwd.
20
21
  */
21
22
 
@@ -23,6 +24,7 @@ const fs = require('fs');
23
24
  const path = require('path');
24
25
  const crypto = require('crypto');
25
26
  const os = require('os');
27
+ const { isDeepStrictEqual } = require('util');
26
28
  const { execFileSync } = require('child_process');
27
29
  const { version: PACKAGE_VERSION } = require('../package.json');
28
30
 
@@ -36,7 +38,9 @@ const DECISION_STATUSES = [
36
38
  'superseded',
37
39
  ];
38
40
  const EVENT_SCHEMA_VERSION = 3;
39
- const PROTOCOL_VERSION = 10;
41
+ const PROTOCOL_VERSION = 11;
42
+ const DEFAULT_LOG_LANGUAGE = 'en';
43
+ const IN_PROGRESS_GIT_PATH = 'driftseal-in-progress.jsonl';
40
44
  const LOCK_STALE_MS = 30 * 60 * 1000;
41
45
  const LOCK_INIT_STALE_MS = 5 * 1000;
42
46
  const MAX_DECISION_SLUG_LENGTH = 180;
@@ -192,7 +196,47 @@ function normalizeEvent(event, line) {
192
196
  fail(`unknown event type "${event.type}" on log line ${line}`);
193
197
  }
194
198
 
195
- function readEvents({ repairTail = false, file = logFile() } = {}) {
199
+ function gitWorktreeRoot(cwd = process.cwd()) {
200
+ if (!isGitWorkTree(cwd)) return null;
201
+ return gitCapture(['rev-parse', '--show-toplevel'], cwd);
202
+ }
203
+
204
+ function worktreeInProgressFile(cwd = process.cwd()) {
205
+ if (!isGitWorkTree(cwd)) return null;
206
+ const gitPath = gitCapture(['rev-parse', '--git-path', IN_PROGRESS_GIT_PATH], cwd);
207
+ if (!gitPath) return null;
208
+ return path.resolve(cwd, gitPath);
209
+ }
210
+
211
+ function isParkableIntentLog() {
212
+ if (process.env.DRIFTSEAL_HOME) return false;
213
+ const root = gitWorktreeRoot();
214
+ if (!root) return false;
215
+ return path.resolve(logFile()) === path.resolve(root, '.intent-log', 'events.jsonl');
216
+ }
217
+
218
+ function inProgressFile() {
219
+ if (!isParkableIntentLog()) return null;
220
+ return worktreeInProgressFile();
221
+ }
222
+
223
+ function liveWorktreeIntentLog() {
224
+ const root = gitWorktreeRoot();
225
+ if (!root) return null;
226
+ return path.resolve(root, '.intent-log', 'events.jsonl');
227
+ }
228
+
229
+ function sameResolvedPath(left, right) {
230
+ return path.resolve(left) === path.resolve(right);
231
+ }
232
+
233
+ function shouldAttachInProgress(file) {
234
+ if (process.env.DRIFTSEAL_HOME) return false;
235
+ const live = liveWorktreeIntentLog();
236
+ return live !== null && sameResolvedPath(file, live);
237
+ }
238
+
239
+ function readJsonlRecordsFromFile(file, { repairTail = false } = {}) {
196
240
  if (!fs.existsSync(file)) return [];
197
241
  let content = fs.readFileSync(file, 'utf8');
198
242
  const rawLines = content.split('\n');
@@ -213,7 +257,65 @@ function readEvents({ repairTail = false, file = logFile() } = {}) {
213
257
  content = content.slice(0, validLength);
214
258
  }
215
259
  }
216
- return parseJsonlRecords(content, file).map((record) => record.event);
260
+ return parseJsonlRecords(content, file);
261
+ }
262
+
263
+ /**
264
+ * True when the parked records already sit in the committed log. A flush appends them, so
265
+ * they land as a suffix; a merge that appends afterwards leaves them as an interior run.
266
+ */
267
+ function overlayIsCommitted(committedEvents, overlayEvents) {
268
+ if (overlayEvents.length === 0 || committedEvents.length < overlayEvents.length) return false;
269
+ const head = overlayEvents[0];
270
+ for (let start = committedEvents.length - overlayEvents.length; start >= 0; start--) {
271
+ const candidate = committedEvents[start];
272
+ if (candidate.type !== head.type || candidate.id !== head.id || candidate.ts !== head.ts) continue;
273
+ const matches = overlayEvents.every((event, index) =>
274
+ isDeepStrictEqual(committedEvents[start + index], event)
275
+ );
276
+ if (matches) return true;
277
+ }
278
+ return false;
279
+ }
280
+
281
+ /** How a parked overlay lines up with the committed log; touches neither file. */
282
+ function planInProgressOverlay(committedEvents, park, { repairTail = false } = {}) {
283
+ if (!park || !fs.existsSync(park)) return null;
284
+ const overlayRecords = readJsonlRecordsFromFile(park, { repairTail });
285
+ const overlayEvents = overlayRecords.map((record) => record.event);
286
+ if (overlayEvents.length === 0 || overlayIsCommitted(committedEvents, overlayEvents)) {
287
+ return { park, records: [], mappings: [], alreadyCommitted: true };
288
+ }
289
+ const remapped = remapTheirsRecords(overlayRecords, committedEvents, new Map(), new Map());
290
+ return { park, records: remapped.records, mappings: remapped.mappings, alreadyCommitted: false };
291
+ }
292
+
293
+ function discardInProgressLog(park) {
294
+ fs.unlinkSync(park);
295
+ fsyncDirectory(path.dirname(park));
296
+ }
297
+
298
+ function reconcileInProgressRecords(committedEvents, { repairTail = false, park = inProgressFile() } = {}) {
299
+ const plan = planInProgressOverlay(committedEvents, park, { repairTail });
300
+ if (!plan) return [];
301
+ if (plan.alreadyCommitted) {
302
+ discardInProgressLog(park);
303
+ return [];
304
+ }
305
+ if (plan.mappings.length > 0) writeJsonl(park, plan.records);
306
+ return plan.records;
307
+ }
308
+
309
+ function readEvents({ repairTail = false, file = logFile() } = {}) {
310
+ const records = readJsonlRecordsFromFile(file, { repairTail });
311
+ const events = records.map((record) => record.event);
312
+ if (!shouldAttachInProgress(file)) return events;
313
+ return events.concat(
314
+ reconcileInProgressRecords(events, {
315
+ repairTail,
316
+ park: worktreeInProgressFile(),
317
+ }).map((record) => record.event)
318
+ );
217
319
  }
218
320
 
219
321
  function parseJsonlRecords(content, source = 'log') {
@@ -256,15 +358,11 @@ function ensureDirectoryDurable(directory) {
256
358
  for (const created of missing.reverse()) fsyncDirectory(path.dirname(created));
257
359
  }
258
360
 
259
- function appendEvent(event) {
260
- ensureDirectoryDurable(logDir());
261
- const file = logFile();
361
+ function appendEventTo(file, event) {
362
+ ensureDirectoryDurable(path.dirname(file));
262
363
  const existed = fs.existsSync(file);
263
364
  const storedEvent = { schemaVersion: EVENT_SCHEMA_VERSION, ...event };
264
- const line = Buffer.from(
265
- JSON.stringify(storedEvent) + '\n',
266
- 'utf8'
267
- );
365
+ const line = Buffer.from(`${JSON.stringify(storedEvent)}\n`, 'utf8');
268
366
  const fd = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND, 0o600);
269
367
  try {
270
368
  const stat = fs.fstatSync(fd);
@@ -284,10 +382,66 @@ function appendEvent(event) {
284
382
  } finally {
285
383
  fs.closeSync(fd);
286
384
  }
287
- if (!existed) fsyncDirectory(logDir());
385
+ if (!existed) fsyncDirectory(path.dirname(file));
288
386
  return storedEvent;
289
387
  }
290
388
 
389
+ /**
390
+ * Move the parked records into the tracked log. Safe to retry: a remap is persisted to the
391
+ * park first, the log is written before the park file is dropped, so an interruption either
392
+ * leaves nothing committed or leaves the overlay recognizable as already committed.
393
+ * Returns the intent ids it had to remap.
394
+ */
395
+ function flushInProgressLog() {
396
+ const park = inProgressFile();
397
+ if (!park || !fs.existsSync(park)) return new Map();
398
+ const committedRecords = readJsonlRecordsFromFile(logFile());
399
+ const plan = planInProgressOverlay(
400
+ committedRecords.map((record) => record.event),
401
+ park,
402
+ { repairTail: true }
403
+ );
404
+ if (!plan) return new Map();
405
+ if (plan.alreadyCommitted) {
406
+ discardInProgressLog(park);
407
+ return new Map();
408
+ }
409
+ // Persist a remap before touching the log: after any crash the park then matches what the
410
+ // log received (or will receive), so recovery never re-remaps it into a duplicate.
411
+ if (plan.mappings.length > 0) writeJsonl(park, plan.records);
412
+ writeJsonl(logFile(), [...committedRecords, ...plan.records]);
413
+ discardInProgressLog(park);
414
+ return new Map(
415
+ plan.mappings.filter((mapping) => mapping.kind === 'intent').map((mapping) => [mapping.from, mapping.to])
416
+ );
417
+ }
418
+
419
+ function parkedOpenIntent(park) {
420
+ if (!fs.existsSync(park)) return null;
421
+ const records = readJsonlRecordsFromFile(park, { repairTail: true });
422
+ return openIntent(fold(records.map((record) => record.event)));
423
+ }
424
+
425
+ function appendEvent(event) {
426
+ const park = inProgressFile();
427
+ if (!park) return appendEventTo(logFile(), event);
428
+
429
+ const open = parkedOpenIntent(park);
430
+ // A park with nothing open left in it belongs in the log; an interrupted end retries here.
431
+ if (!open) flushInProgressLog();
432
+
433
+ if (event.type === 'begin') return appendEventTo(park, event);
434
+ if (!open || open.id !== event.id) return appendEventTo(logFile(), event);
435
+ if (event.type !== 'end') return appendEventTo(park, event);
436
+ // Close in the tracked log, never in Git metadata: the parked records move first, so the
437
+ // closing record cannot end up somewhere a clone or a removed worktree would drop it.
438
+ const remapped = flushInProgressLog();
439
+ if (process.env._DRIFTSEAL_TEST_CRASH_AFTER_IN_PROGRESS_FLUSH === '1') {
440
+ fail('simulated interruption after the in-progress flush');
441
+ }
442
+ return appendEventTo(logFile(), remapEvent(event, remapped, new Map()));
443
+ }
444
+
291
445
  function contentHash(content) {
292
446
  return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
293
447
  }
@@ -1122,10 +1276,213 @@ const INTENT_PROTOCOL_MARKER = '<!-- driftseal -->';
1122
1276
  const INTENT_PROTOCOL_END = '<!-- /driftseal -->';
1123
1277
  const DECISION_PROTOCOL_MARKER = '<!-- driftseal-decisions -->';
1124
1278
  const DECISION_PROTOCOL_END = '<!-- /driftseal-decisions -->';
1279
+ const LOG_LANGUAGE_COMMENT_RE = /^<!-- driftseal-log-language: ([^>\r\n]+) -->\r?$/m;
1280
+ const LOG_LANGUAGE_PROSE_RE = /\*\*Log language:\*\* `([^`]+)`/;
1281
+ const IRREGULAR_GRANDFATHERED_TAGS = new Map([
1282
+ ['en-gb-oed', 'en-GB-oed'],
1283
+ ['i-ami', 'i-ami'],
1284
+ ['i-bnn', 'i-bnn'],
1285
+ ['i-default', 'i-default'],
1286
+ ['i-enochian', 'i-enochian'],
1287
+ ['i-hak', 'i-hak'],
1288
+ ['i-klingon', 'i-klingon'],
1289
+ ['i-lux', 'i-lux'],
1290
+ ['i-mingo', 'i-mingo'],
1291
+ ['i-navajo', 'i-navajo'],
1292
+ ['i-pwn', 'i-pwn'],
1293
+ ['i-tao', 'i-tao'],
1294
+ ['i-tay', 'i-tay'],
1295
+ ['i-tsu', 'i-tsu'],
1296
+ ['sgn-be-fr', 'sgn-BE-FR'],
1297
+ ['sgn-be-nl', 'sgn-BE-NL'],
1298
+ ['sgn-ch-de', 'sgn-CH-DE'],
1299
+ ]);
1300
+
1301
+ function titleCaseSubtag(value) {
1302
+ return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
1303
+ }
1304
+
1305
+ function isBcp47Variant(subtag) {
1306
+ return /^[A-Za-z0-9]{5,8}$/.test(subtag) || /^[0-9][A-Za-z0-9]{3}$/.test(subtag);
1307
+ }
1308
+
1309
+ function isBcp47Singleton(subtag) {
1310
+ return /^[0-9A-WY-Za-wy-z]$/.test(subtag);
1311
+ }
1312
+
1313
+ function parsePrivateUseSubtags(subtags, start, parts) {
1314
+ if (start >= subtags.length || !/^x$/i.test(subtags[start])) return null;
1315
+ parts.push('x');
1316
+ let index = start + 1;
1317
+ let count = 0;
1318
+ while (index < subtags.length && /^[A-Za-z0-9]{1,8}$/.test(subtags[index])) {
1319
+ parts.push(subtags[index].toLowerCase());
1320
+ index += 1;
1321
+ count += 1;
1322
+ }
1323
+ if (count === 0 || index !== subtags.length) return null;
1324
+ return parts.join('-');
1325
+ }
1326
+
1327
+ function wellFormedBcp47(tag) {
1328
+ if (typeof tag !== 'string' || tag.length === 0 || tag.startsWith('-') || tag.endsWith('-')) {
1329
+ return null;
1330
+ }
1331
+ const irregular = IRREGULAR_GRANDFATHERED_TAGS.get(tag.toLowerCase());
1332
+ if (irregular) return irregular;
1333
+
1334
+ const subtags = tag.split('-');
1335
+ if (subtags.some((subtag) => subtag.length === 0)) return null;
1336
+
1337
+ const parts = [];
1338
+ if (/^x$/i.test(subtags[0])) return parsePrivateUseSubtags(subtags, 0, parts);
1339
+
1340
+ let index = 0;
1341
+ const language = subtags[index];
1342
+ if (!/^[A-Za-z]{2,8}$/.test(language)) return null;
1343
+ parts.push(language.toLowerCase());
1344
+ index += 1;
1345
+ if (language.length <= 3) {
1346
+ let extlang = 0;
1347
+ while (index < subtags.length && /^[A-Za-z]{3}$/.test(subtags[index]) && extlang < 3) {
1348
+ parts.push(subtags[index].toLowerCase());
1349
+ index += 1;
1350
+ extlang += 1;
1351
+ }
1352
+ }
1353
+
1354
+ if (index < subtags.length && /^[A-Za-z]{4}$/.test(subtags[index])) {
1355
+ parts.push(titleCaseSubtag(subtags[index]));
1356
+ index += 1;
1357
+ }
1358
+
1359
+ if (index < subtags.length && /^[A-Za-z]{2}$/.test(subtags[index])) {
1360
+ parts.push(subtags[index].toUpperCase());
1361
+ index += 1;
1362
+ } else if (index < subtags.length && /^[0-9]{3}$/.test(subtags[index])) {
1363
+ parts.push(subtags[index]);
1364
+ index += 1;
1365
+ }
1125
1366
 
1126
- function intentProtocolBlock(version = PROTOCOL_VERSION) {
1367
+ const variants = new Set();
1368
+ while (index < subtags.length && isBcp47Variant(subtags[index])) {
1369
+ const variant = subtags[index].toLowerCase();
1370
+ if (variants.has(variant)) return null;
1371
+ variants.add(variant);
1372
+ parts.push(variant);
1373
+ index += 1;
1374
+ }
1375
+
1376
+ const singletons = new Set();
1377
+ while (index < subtags.length && isBcp47Singleton(subtags[index])) {
1378
+ const singleton = subtags[index].toLowerCase();
1379
+ if (singletons.has(singleton)) return null;
1380
+ singletons.add(singleton);
1381
+ parts.push(singleton);
1382
+ index += 1;
1383
+ let following = 0;
1384
+ while (index < subtags.length && /^[A-Za-z0-9]{2,8}$/.test(subtags[index])) {
1385
+ parts.push(subtags[index].toLowerCase());
1386
+ index += 1;
1387
+ following += 1;
1388
+ }
1389
+ if (following === 0) return null;
1390
+ }
1391
+
1392
+ if (index < subtags.length && /^x$/i.test(subtags[index])) {
1393
+ return parsePrivateUseSubtags(subtags, index, parts);
1394
+ }
1395
+ if (index !== subtags.length) return null;
1396
+ return parts.join('-');
1397
+ }
1398
+
1399
+ function canonicalizeLogLanguage(value) {
1400
+ if (typeof value !== 'string' || value.trim().length === 0) {
1401
+ fail('invalid log language: use a BCP 47 tag such as en or zh-CN');
1402
+ }
1403
+ const canonical = wellFormedBcp47(value.trim());
1404
+ if (!canonical) {
1405
+ fail(`invalid log language "${value}": use a BCP 47 tag such as en or zh-CN`);
1406
+ }
1407
+ return canonical;
1408
+ }
1409
+
1410
+ function intentLogLanguageParagraph(language) {
1411
+ return `**Log language:** \`${language}\`. Write intent-log prose (intent, note,
1412
+ verify-result, and reclaim/unreclaim reason) in that language. Keep command
1413
+ names, flags, status tokens, and ids in English.`;
1414
+ }
1415
+
1416
+ function decisionLogLanguageParagraph(language) {
1417
+ return `**Log language:** \`${language}\`. Write decision-log prose (title, context,
1418
+ outcome, drivers, options, consequences, and update notes) in that language.
1419
+ Keep MADR section headings, status tokens, and ids in English.`;
1420
+ }
1421
+
1422
+ function parseLogLanguageFromBlock(block, label) {
1423
+ const commentMatch = block.match(LOG_LANGUAGE_COMMENT_RE);
1424
+ const proseMatch = block.match(LOG_LANGUAGE_PROSE_RE);
1425
+ const comment = commentMatch ? canonicalizeLogLanguage(commentMatch[1]) : null;
1426
+ const prose = proseMatch ? canonicalizeLogLanguage(proseMatch[1]) : null;
1427
+ if (comment && prose && comment !== prose) {
1428
+ fail(
1429
+ `${label} declares different log languages in the comment (${comment}) and prose (${prose}); pass --lang to set one`
1430
+ );
1431
+ }
1432
+ return comment || prose || null;
1433
+ }
1434
+
1435
+ function extractManagedBlock(content, marker, endMarker) {
1436
+ const start = content.indexOf(marker);
1437
+ if (start === -1) return null;
1438
+ const end = content.indexOf(endMarker, start);
1439
+ if (end === -1) return null;
1440
+ return content.slice(start, end + endMarker.length);
1441
+ }
1442
+
1443
+ function resolveInitLogLanguage(requested, content) {
1444
+ if (requested !== undefined) return canonicalizeLogLanguage(requested);
1445
+ const languages = new Set();
1446
+ const intent = extractManagedBlock(content, INTENT_PROTOCOL_MARKER, INTENT_PROTOCOL_END);
1447
+ const decision = extractManagedBlock(content, DECISION_PROTOCOL_MARKER, DECISION_PROTOCOL_END);
1448
+ if (intent) {
1449
+ const language = parseLogLanguageFromBlock(intent, 'intent protocol');
1450
+ if (language) languages.add(language);
1451
+ }
1452
+ if (decision) {
1453
+ const language = parseLogLanguageFromBlock(decision, 'decision protocol');
1454
+ if (language) languages.add(language);
1455
+ }
1456
+ if (languages.size > 1) {
1457
+ fail(
1458
+ `intent and decision protocols declare different log languages (${[...languages].join(', ')}); pass --lang to set one`
1459
+ );
1460
+ }
1461
+ return languages.size === 1 ? [...languages][0] : DEFAULT_LOG_LANGUAGE;
1462
+ }
1463
+
1464
+ function protocolBlockKey(block) {
1465
+ return block
1466
+ .replace(/^<!-- driftseal-log-language: [^>\r\n]+ -->\r?$/m, '<!-- driftseal-log-language: -->')
1467
+ .replace(/\*\*Log language:\*\* `[^`]+`/g, '**Log language:** ``');
1468
+ }
1469
+
1470
+ function stripIntentLogLanguage(block, language = DEFAULT_LOG_LANGUAGE) {
1471
+ return block
1472
+ .replace(`\n<!-- driftseal-log-language: ${language} -->`, '')
1473
+ .replace(`\n${intentLogLanguageParagraph(language)}\n`, '');
1474
+ }
1475
+
1476
+ function stripDecisionLogLanguage(block, language = DEFAULT_LOG_LANGUAGE) {
1477
+ return block
1478
+ .replace(`\n<!-- driftseal-log-language: ${language} -->`, '')
1479
+ .replace(`\n${decisionLogLanguageParagraph(language)}\n`, '');
1480
+ }
1481
+
1482
+ function intentProtocolBlock(version = PROTOCOL_VERSION, language = DEFAULT_LOG_LANGUAGE) {
1127
1483
  return `${INTENT_PROTOCOL_MARKER}
1128
1484
  <!-- driftseal-version: ${version} -->
1485
+ <!-- driftseal-log-language: ${language} -->
1129
1486
 
1130
1487
  ## Agent protocol: intent write-ahead log
1131
1488
 
@@ -1135,6 +1492,8 @@ This \`AGENTS.md\` protocol is the source of truth. Use the \`driftseal\` CLI by
1135
1492
  default; the companion skill only helps discover and resume the workflow, while
1136
1493
  MCP and lifecycle hooks are optional adapters.
1137
1494
 
1495
+ ${intentLogLanguageParagraph(language)}
1496
+
1138
1497
  1. **Write intent first**, before modifying, creating, or deleting files, or
1139
1498
  making any other non-Git change that may need a rollback:
1140
1499
  \`driftseal begin "<what this round will accomplish>" --verify "<command or check that proves it>"\`.
@@ -1178,8 +1537,9 @@ ${INTENT_PROTOCOL_END}`;
1178
1537
  }
1179
1538
 
1180
1539
  function previousIntentProtocolBlock(version) {
1181
- const v9 = intentProtocolBlock(version)
1182
- .replace(
1540
+ const v10 = stripIntentLogLanguage(intentProtocolBlock(version, DEFAULT_LOG_LANGUAGE));
1541
+ if (version >= 10) return v10;
1542
+ const v9 = v10.replace(
1183
1543
  '1. **Write intent first**, before modifying, creating, or deleting files, or\n' +
1184
1544
  ' making any other non-Git change that may need a rollback:\n' +
1185
1545
  ' `driftseal begin "<what this round will accomplish>" --verify "<command or check that proves it>"`.\n' +
@@ -1258,9 +1618,10 @@ function protocolEol(content, eol) {
1258
1618
  return eol === '\n' ? content : content.replace(/\n/g, eol);
1259
1619
  }
1260
1620
 
1261
- function decisionProtocolBlock(version = PROTOCOL_VERSION) {
1621
+ function decisionProtocolBlock(version = PROTOCOL_VERSION, language = DEFAULT_LOG_LANGUAGE) {
1262
1622
  return `${DECISION_PROTOCOL_MARKER}
1263
1623
  <!-- driftseal-decisions-version: ${version} -->
1624
+ <!-- driftseal-log-language: ${language} -->
1264
1625
 
1265
1626
  ## Agent protocol: decision log
1266
1627
 
@@ -1270,6 +1631,8 @@ revisiting, non-obvious rationale behind a long-lived or costly-to-reverse accep
1270
1631
  choice, or a deprecated or superseded decision. Do not record routine, local,
1271
1632
  readily reversible choices.
1272
1633
 
1634
+ ${decisionLogLanguageParagraph(language)}
1635
+
1273
1636
  \`driftseal decision add "<title>" --context "<problem and constraints>" --outcome "<decision and rationale>" --driver "<decision driver>" --option "<considered option>" --consequence "<result>"\`
1274
1637
 
1275
1638
  Add one \`--driver\`, \`--option\`, or \`--consequence\` flag per item. Use
@@ -1338,9 +1701,9 @@ Commit \`.decision-log/\` with the code.`;
1338
1701
  }
1339
1702
 
1340
1703
  function previousDecisionProtocolBlock(version) {
1341
- const v9 = decisionProtocolBlock(version);
1342
- if (version >= 9) return v9;
1343
- const v8 = v9.replace(
1704
+ const v10 = stripDecisionLogLanguage(decisionProtocolBlock(version, DEFAULT_LOG_LANGUAGE));
1705
+ if (version >= 9) return v10;
1706
+ const v8 = v10.replace(
1344
1707
  '\nAfter a merge, colliding decision ids are remapped with `driftseal absorb`;\n' +
1345
1708
  'concurrent edits of a shared decision are not auto-merged.',
1346
1709
  ''
@@ -1381,7 +1744,11 @@ function upgradeManagedBlock({
1381
1744
  `protocol version ${version} requires a newer DriftSeal client (supported: ${PROTOCOL_VERSION})`
1382
1745
  );
1383
1746
  }
1384
- if (block !== replacement && !knownManagedBlocks.includes(block)) {
1747
+ if (
1748
+ block !== replacement &&
1749
+ !knownManagedBlocks.includes(block) &&
1750
+ protocolBlockKey(block) !== protocolBlockKey(replacement)
1751
+ ) {
1385
1752
  fail(`cannot safely upgrade customized protocol block beginning with ${marker}`);
1386
1753
  }
1387
1754
  return {
@@ -2020,9 +2387,14 @@ function hookLogFile() {
2020
2387
  return fs.existsSync(configured) ? configured : null;
2021
2388
  }
2022
2389
  let current = path.resolve(process.cwd());
2390
+ const root = gitWorktreeRoot(current);
2023
2391
  while (true) {
2024
2392
  const candidate = path.join(current, '.intent-log', 'events.jsonl');
2025
2393
  if (fs.existsSync(candidate)) return candidate;
2394
+ if (root && path.resolve(root) === current) {
2395
+ const park = worktreeInProgressFile(current);
2396
+ if (park && fs.existsSync(park)) return candidate;
2397
+ }
2026
2398
  const parent = path.dirname(current);
2027
2399
  if (parent === current) return null;
2028
2400
  current = parent;
@@ -2513,8 +2885,9 @@ function printAbsorbReport({ mappings, abandoned, intentCount }) {
2513
2885
  `absorbed ${intentCount} intent(s), remapped ${remappedIntents} intent id(s), ${remappedDecisions} decision id(s)`
2514
2886
  );
2515
2887
  for (const mapping of mappings) {
2516
- if (mapping.kind === 'intent') printLine(`${mapping.from} (theirs) -> ${mapping.to}`);
2517
- else printLine(`decision ${mapping.from} (theirs) -> ${mapping.to}`);
2888
+ const side = mapping.side || 'theirs';
2889
+ if (mapping.kind === 'intent') printLine(`${mapping.from} (${side}) -> ${mapping.to}`);
2890
+ else printLine(`decision ${mapping.from} (${side}) -> ${mapping.to}`);
2518
2891
  }
2519
2892
  if (abandoned) printLine(`abandoned ${abandoned} during absorb`);
2520
2893
  }
@@ -2534,23 +2907,45 @@ function abandonOpenIntent(records, targetId, side) {
2534
2907
  return targetId;
2535
2908
  }
2536
2909
 
2537
- function resolveOpenIntents(result, oursRecords, theirsRecords, abandon, { allowConflict = false } = {}) {
2910
+ function resolveOpenIntents(
2911
+ result,
2912
+ oursRecords,
2913
+ theirsRecords,
2914
+ abandon,
2915
+ { allowConflict = false, overlay = [], parkedOpen = null } = {}
2916
+ ) {
2538
2917
  const oursOpen = openIntent(fold(oursRecords.map((record) => record.event)));
2539
2918
  const theirsOpen = openIntent(fold(theirsRecords.map((record) => record.event)));
2540
2919
  try {
2541
- openIntent(fold(result.map((record) => record.event)));
2542
- return { abandoned: null, conflict: false };
2920
+ openIntent(fold([...result, ...overlay].map((record) => record.event)));
2921
+ return { abandoned: null, conflict: false, parkedClosed: false };
2543
2922
  } catch (err) {
2544
2923
  if (!(err instanceof DriftSealError) || !/multiple intents in progress/.test(err.message)) {
2545
2924
  throw err;
2546
2925
  }
2547
2926
  if (abandon === 'theirs' && theirsOpen) {
2548
- return { abandoned: abandonOpenIntent(result, theirsOpen.id, 'theirs'), conflict: false };
2927
+ return {
2928
+ abandoned: abandonOpenIntent(result, theirsOpen.id, 'theirs'),
2929
+ conflict: false,
2930
+ parkedClosed: false,
2931
+ };
2932
+ }
2933
+ // A parked intent is local by construction, so --abandon-ours targets it before the log.
2934
+ if (abandon === 'ours' && parkedOpen) {
2935
+ return {
2936
+ abandoned: abandonOpenIntent(overlay, parkedOpen.id, 'ours'),
2937
+ conflict: false,
2938
+ parkedClosed: true,
2939
+ };
2549
2940
  }
2550
2941
  if (abandon === 'ours' && oursOpen) {
2551
- return { abandoned: abandonOpenIntent(result, oursOpen.id, 'ours'), conflict: false };
2942
+ return {
2943
+ abandoned: abandonOpenIntent(result, oursOpen.id, 'ours'),
2944
+ conflict: false,
2945
+ parkedClosed: false,
2946
+ };
2552
2947
  }
2553
- if (allowConflict) return { abandoned: null, conflict: true };
2948
+ if (allowConflict) return { abandoned: null, conflict: true, parkedClosed: false };
2554
2949
  fail(`${err.message}; re-run with --abandon-theirs or --abandon-ours`);
2555
2950
  }
2556
2951
  }
@@ -2645,20 +3040,51 @@ function finishAbsorb({
2645
3040
  allowConflict = false,
2646
3041
  followupMessage = null,
2647
3042
  }) {
2648
- const { abandoned, conflict } = resolveOpenIntents(result, oursRecords, theirsRecords, abandon, {
2649
- allowConflict,
3043
+ // An intent parked in Git metadata is part of our side even though the log never saw it.
3044
+ const park = shouldAttachInProgress(outputFile) ? inProgressFile() : null;
3045
+ const plan = planInProgressOverlay(result.map((record) => record.event), park, {
3046
+ repairTail: true,
2650
3047
  });
2651
- fold(result.map((record) => record.event));
2652
- if (!conflict) openIntent(fold(result.map((record) => record.event)));
3048
+ const overlay = plan && !plan.alreadyCommitted ? plan.records : [];
3049
+ const parkedOpen =
3050
+ overlay.length > 0 ? openIntent(fold(overlay.map((record) => record.event))) : null;
3051
+ const parkMappings = plan
3052
+ ? plan.mappings.map((mapping) => ({ ...mapping, side: 'parked' }))
3053
+ : [];
3054
+ const allMappings = [...mappings, ...parkMappings];
3055
+ const { abandoned, conflict, parkedClosed } = resolveOpenIntents(
3056
+ result,
3057
+ oursRecords,
3058
+ theirsRecords,
3059
+ abandon,
3060
+ { allowConflict, overlay, parkedOpen }
3061
+ );
3062
+ // A parked overlay with nothing left open in it belongs in the tracked log, whether the
3063
+ // abandon flag just closed it or an interrupted end left it closed.
3064
+ const flushOverlay = parkedClosed || (overlay.length > 0 && !parkedOpen);
3065
+ const merged = flushOverlay ? [...result, ...overlay] : result;
3066
+ const effective = [...result, ...overlay].map((record) => record.event);
3067
+ fold(effective);
3068
+ if (!conflict) openIntent(fold(effective));
2653
3069
  if (!dryRun) {
2654
- writeJsonl(outputFile, result);
3070
+ writeJsonl(outputFile, merged);
2655
3071
  applyDecisionCopies(copies, dryRun);
3072
+ if (plan) {
3073
+ if (plan.alreadyCommitted || flushOverlay) discardInProgressLog(park);
3074
+ else if (plan.mappings.length > 0) writeJsonl(park, overlay);
3075
+ }
2656
3076
  }
2657
- if (intentCount === 0 && mappings.length === 0 && copies.length === 0 && !abandoned) {
3077
+ if (
3078
+ intentCount === 0 &&
3079
+ allMappings.length === 0 &&
3080
+ copies.length === 0 &&
3081
+ !abandoned &&
3082
+ !flushOverlay
3083
+ ) {
2658
3084
  printLine('nothing to absorb');
2659
3085
  } else {
2660
3086
  printAbsorbReport({
2661
- mappings,
3087
+ mappings: allMappings,
2662
3088
  abandoned,
2663
3089
  intentCount,
2664
3090
  });
@@ -2668,7 +3094,7 @@ function finishAbsorb({
2668
3094
  }
2669
3095
  if (followupMessage) printLine(followupMessage);
2670
3096
  return {
2671
- mappings,
3097
+ mappings: allMappings,
2672
3098
  abandoned,
2673
3099
  copies: copies.map((item) => item.toFile),
2674
3100
  outputFile,
@@ -2897,22 +3323,30 @@ const commands = {
2897
3323
 
2898
3324
  const events = readEvents({ repairTail: true });
2899
3325
  const records = fold(events);
2900
- const open = openIntent(records);
2901
- if (open) {
2902
- if (!flags.force) {
2903
- fail(
2904
- `intent ${open.id} is still in_progress: "${open.intent}"\n` +
2905
- `end it first (driftseal end) or re-run with --force to abandon it`
2906
- );
2907
- }
3326
+ // A parked intent and a merged-in one can both be open; --force clears every one of them.
3327
+ const open = records.filter((record) => record.status === 'in_progress');
3328
+ if (open.length > 1 && !flags.force) {
3329
+ fail(
3330
+ `multiple intents in progress: ${open.map((record) => record.id).join(', ')}\n` +
3331
+ 'resolve them with driftseal absorb --abandon-ours or --abandon-theirs, ' +
3332
+ 'or re-run with --force to abandon all of them'
3333
+ );
3334
+ }
3335
+ if (open.length === 1 && !flags.force) {
3336
+ fail(
3337
+ `intent ${open[0].id} is still in_progress: "${open[0].intent}"\n` +
3338
+ `end it first (driftseal end) or re-run with --force to abandon it`
3339
+ );
3340
+ }
3341
+ for (const record of open) {
2908
3342
  const status = closeIntentAsEscape(
2909
3343
  events,
2910
- open,
3344
+ record,
2911
3345
  'abandoned',
2912
3346
  'superseded by --force',
2913
3347
  null
2914
3348
  );
2915
- printError(`driftseal: ${status} ${open.id}`);
3349
+ printError(`driftseal: ${status} ${record.id}`);
2916
3350
  }
2917
3351
 
2918
3352
  const id = nextId(events);
@@ -3322,14 +3756,15 @@ const commands = {
3322
3756
  },
3323
3757
 
3324
3758
  init(argv) {
3325
- const { positionals } = parseArgs(argv, {});
3326
- if (positionals.length > 0) fail('usage: driftseal init');
3759
+ const { positionals, flags } = parseArgs(argv, { lang: 'single' });
3760
+ if (positionals.length > 0) fail('usage: driftseal init [--lang <tag>]');
3327
3761
  const target = path.join(process.cwd(), 'AGENTS.md');
3328
3762
  const existed = fs.existsSync(target);
3329
3763
  const current = existed ? fs.readFileSync(target, 'utf8') : '';
3330
3764
  const eol = current.includes('\r\n') ? '\r\n' : '\n';
3331
- const intentBlock = protocolEol(intentProtocolBlock(), eol);
3332
- const decisionBlock = protocolEol(decisionProtocolBlock(), eol);
3765
+ const language = resolveInitLogLanguage(flags.lang, current);
3766
+ const intentBlock = protocolEol(intentProtocolBlock(PROTOCOL_VERSION, language), eol);
3767
+ const decisionBlock = protocolEol(decisionProtocolBlock(PROTOCOL_VERSION, language), eol);
3333
3768
  let updated = current;
3334
3769
  const intent = upgradeManagedBlock({
3335
3770
  content: updated,
@@ -3346,6 +3781,7 @@ const commands = {
3346
3781
  protocolEol(previousIntentProtocolBlock(7), eol),
3347
3782
  protocolEol(previousIntentProtocolBlock(8), eol),
3348
3783
  protocolEol(previousIntentProtocolBlock(9), eol),
3784
+ protocolEol(previousIntentProtocolBlock(10), eol),
3349
3785
  ],
3350
3786
  knownLegacyBlocks: [protocolEol(legacyIntentProtocolBlock(), eol)],
3351
3787
  });
@@ -3365,6 +3801,7 @@ const commands = {
3365
3801
  protocolEol(previousDecisionProtocolBlock(7), eol),
3366
3802
  protocolEol(previousDecisionProtocolBlock(8), eol),
3367
3803
  protocolEol(previousDecisionProtocolBlock(9), eol),
3804
+ protocolEol(previousDecisionProtocolBlock(10), eol),
3368
3805
  ],
3369
3806
  knownLegacyBlocks: [protocolEol(legacyDecisionProtocolBlock(), eol)],
3370
3807
  });
@@ -3445,7 +3882,8 @@ usage:
3445
3882
  targets: kimi-code (global only), claude-code, codex (prompt only)
3446
3883
  driftseal hook prompt|stop [--format plain|claude-code]
3447
3884
  emit the reminder a lifecycle hook injects; never blocks
3448
- driftseal init inject protocols into ./AGENTS.md and configure the git merge driver
3885
+ driftseal init [--lang <tag>] inject protocols into ./AGENTS.md and configure the git merge driver
3886
+ --lang sets the intent/decision log language (BCP 47, default: en)
3449
3887
  driftseal --version | -V print the installed DriftSeal version
3450
3888
  driftseal help
3451
3889
 
@@ -3456,7 +3894,8 @@ decision add options:
3456
3894
  --consequence "..." repeat for each consequence
3457
3895
 
3458
3896
  intent log: $DRIFTSEAL_HOME/events.jsonl, or .intent-log/events.jsonl
3459
- decision log: $DRIFTSEAL_DECISION_HOME, or .decision-log/ in the current directory`);
3897
+ decision log: $DRIFTSEAL_DECISION_HOME, or .decision-log/ in the current directory
3898
+ In a Git worktree, begin parks an open intent in Git metadata until end, so merge does not need a log-only commit.`);
3460
3899
  return null;
3461
3900
  },
3462
3901
 
@@ -3505,7 +3944,8 @@ function dispatch(argv) {
3505
3944
  ['begin', 'end', 'init', 'skill', 'mcp', 'reclaim', 'unreclaim', 'absorb'].includes(cmd) ||
3506
3945
  (cmd === 'hook' && rest[0] === 'install') ||
3507
3946
  (cmd === 'decision' && ['add', 'update'].includes(rest[0]));
3508
- const readsIntentLog = ['status', 'log'].includes(cmd);
3947
+ const readsIntentLog =
3948
+ ['status', 'log'].includes(cmd) || (cmd === 'hook' && ['prompt', 'stop'].includes(rest[0]));
3509
3949
  if (mutates || readsIntentLog) {
3510
3950
  const resources = readsIntentLog ? [logDir()] : mutationResources(cmd, rest);
3511
3951
  const data = withMutationLocks(resources, () => fn(rest));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driftseal",
3
- "version": "1.1.3",
3
+ "version": "1.1.6",
4
4
  "description": "Seal intent, verification, and decisions into an auditable workflow for agentic coding",
5
5
  "keywords": [
6
6
  "driftseal",
@@ -39,11 +39,17 @@ driftseal help
39
39
 
40
40
  ## After a merge
41
41
 
42
- If `status` or `log` fails with a duplicate id, or the intent log has conflict
43
- markers, run `driftseal absorb` instead of editing `.intent-log/events.jsonl`.
44
- When both sides still have an open intent, add `--abandon-theirs` or
45
- `--abandon-ours`. `driftseal init` also configures the local git merge driver;
46
- clones need `init` again for that driver.
42
+ If `status` or `log` fails with a duplicate id or with `multiple intents in
43
+ progress`, or the intent log has conflict markers, run `driftseal absorb`
44
+ instead of editing `.intent-log/events.jsonl`. When both sides still have an
45
+ open intent, add `--abandon-theirs` or `--abandon-ours`; this works whether your
46
+ open intent sits in the log or is parked in Git metadata. `driftseal init` also
47
+ configures the local git merge driver; clones need `init` again for that driver.
48
+
49
+ In a Git worktree, `begin` does not dirty the tracked intent log, so `git merge`
50
+ can run with an intent still in progress. `end` writes the closed record to
51
+ `.intent-log/events.jsonl`; if it is interrupted, the intent stays open there
52
+ and `end` can be run again.
47
53
 
48
54
  Do not treat this skill, MCP descriptions, or lifecycle-hook reminders as
49
55
  additional policy. If they conflict with `AGENTS.md`, follow `AGENTS.md`.