driftseal 1.1.2 → 1.1.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 +18 -4
- package/README.zh-CN.md +14 -4
- package/bin/driftseal-mcp.js +48 -0
- package/bin/driftseal.js +257 -14
- package/package.json +1 -1
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.
|
|
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
|
|
|
@@ -132,6 +137,7 @@ The v1 server provides:
|
|
|
132
137
|
| --- | --- |
|
|
133
138
|
| `driftseal_status`, `driftseal_log` | Read the current intent and intent history. |
|
|
134
139
|
| `driftseal_begin`, `driftseal_end` | Open and honestly close a work round. |
|
|
140
|
+
| `driftseal_absorb` | Repair merge collisions or absorb another worktree's logs while remapping colliding IDs. |
|
|
135
141
|
| `driftseal_reclaim`, `driftseal_unreclaim` | Hide meaningless closed records behind append-only markers, or restore them. |
|
|
136
142
|
| `driftseal_decision_list`, `driftseal_decision_show` | Find and read MADR records. |
|
|
137
143
|
| `driftseal_decision_add`, `driftseal_decision_update` | Add selective decisions and reconcile linked ones. |
|
|
@@ -139,6 +145,12 @@ The v1 server provides:
|
|
|
139
145
|
| `driftseal://intents/recent` | Read the ten most recent intents as a JSON resource. |
|
|
140
146
|
| `driftseal://decisions` | Read the decision catalog as a JSON resource. |
|
|
141
147
|
|
|
148
|
+
`driftseal_absorb` accepts optional incoming intent-log and decision-directory
|
|
149
|
+
paths, an `ours` or `theirs` abandon strategy, and a dry-run mode. Incoming
|
|
150
|
+
paths are read-only sources; all repaired output stays under the repository
|
|
151
|
+
fixed at server startup. The Git merge-driver form remains a CLI-only plumbing
|
|
152
|
+
command.
|
|
153
|
+
|
|
142
154
|
MCP changes only the execution surface. It does not add policy beyond the
|
|
143
155
|
repository's `AGENTS.md`, and the companion skill remains limited to discovery
|
|
144
156
|
and recovery guidance.
|
|
@@ -224,7 +236,7 @@ content change starts a new work round.
|
|
|
224
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. |
|
|
225
237
|
| `driftseal hook install --target TARGET [--scope project\|global] [--root path] [--force]` | Install advisory lifecycle reminders into Kimi Code, Claude Code, or Codex. |
|
|
226
238
|
| `driftseal hook prompt\|stop [--format plain\|claude-code]` | Emit the reminder a lifecycle hook injects; never blocks. |
|
|
227
|
-
| `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`). |
|
|
228
240
|
| `driftseal --version` or `driftseal -V` | Print the installed DriftSeal version. |
|
|
229
241
|
| `driftseal help` | Print CLI usage. |
|
|
230
242
|
|
|
@@ -267,8 +279,10 @@ conflicts cannot block later decision work.
|
|
|
267
279
|
New events carry a schema version. DriftSeal rejects newer unsupported schemas and
|
|
268
280
|
fails closed if a legacy client closes a linked intent without reconciliation.
|
|
269
281
|
`driftseal init` writes versioned managed blocks and upgrades only exact, recognized
|
|
270
|
-
older blocks.
|
|
271
|
-
|
|
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`.
|
|
272
286
|
|
|
273
287
|
`--count` prints only the number of records remaining after status filtering.
|
|
274
288
|
It cannot be combined with `--last`, whose limiting semantics would make the
|
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
|
|
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
|
|
|
@@ -128,6 +132,7 @@ v1 server 提供:
|
|
|
128
132
|
| --- | --- |
|
|
129
133
|
| `driftseal_status`, `driftseal_log` | 读取当前 intent 和 intent 历史。 |
|
|
130
134
|
| `driftseal_begin`, `driftseal_end` | 开启并诚实关闭一轮工作。 |
|
|
135
|
+
| `driftseal_absorb` | 修复 merge 撞号,或吸收另一条 worktree 日志并重编号冲突 ID。 |
|
|
131
136
|
| `driftseal_reclaim`, `driftseal_unreclaim` | 用 append-only 标记隐藏已无意义的已关闭记录,或将其恢复。 |
|
|
132
137
|
| `driftseal_decision_list`, `driftseal_decision_show` | 查找并读取 MADR record。 |
|
|
133
138
|
| `driftseal_decision_add`, `driftseal_decision_update` | 克制地增加 decision,并 reconcile 已关联的 decision。 |
|
|
@@ -135,6 +140,10 @@ v1 server 提供:
|
|
|
135
140
|
| `driftseal://intents/recent` | 以 JSON resource 读取最近十条 intent。 |
|
|
136
141
|
| `driftseal://decisions` | 以 JSON resource 读取 decision catalog。 |
|
|
137
142
|
|
|
143
|
+
`driftseal_absorb` 可以接收另一份 intent log、decision 目录、`ours` 或 `theirs`
|
|
144
|
+
放弃策略,以及 dry-run 模式。传入的路径只作为只读来源;修复后的内容仍只会写入
|
|
145
|
+
server 启动时固定的 repository。Git merge driver 形式仍是 CLI 专用的底层命令。
|
|
146
|
+
|
|
138
147
|
MCP 只替换执行入口,不会在 repository 的 `AGENTS.md` 之外增加 policy;
|
|
139
148
|
配套 skill 也仍只负责发现与恢复工作流。
|
|
140
149
|
|
|
@@ -215,7 +224,7 @@ Git 操作完全不计入 intent log,因为 Git 会自行维护历史。查看
|
|
|
215
224
|
| `driftseal mcp install --target TARGET [--scope project\|global] [--root path] [--force]` | 把固定到 repository 的 MCP server 安装到 Codex、Kimi Code、OpenCode、Claude Code 或 Cursor。 |
|
|
216
225
|
| `driftseal hook install --target TARGET [--scope project\|global] [--root path] [--force]` | 把建议性的 lifecycle 提醒安装到 Kimi Code、Claude Code 或 Codex。 |
|
|
217
226
|
| `driftseal hook prompt\|stop [--format plain\|claude-code]` | 输出 lifecycle hook 注入的提醒;绝不阻断。 |
|
|
218
|
-
| `driftseal init` | 把接入协议写入 `AGENTS.md`,并配置 git merge driver
|
|
227
|
+
| `driftseal init [--lang <tag>]` | 把接入协议写入 `AGENTS.md`,并配置 git merge driver。`--lang` 设置 intent / decision log 的语言(BCP 47,默认 `en`)。 |
|
|
219
228
|
| `driftseal --version` 或 `driftseal -V` | 输出当前安装的 DriftSeal 版本。 |
|
|
220
229
|
| `driftseal help` | 查看 CLI 用法。 |
|
|
221
230
|
|
|
@@ -252,8 +261,9 @@ recovery 只处理当前 intent,因此历史冲突不会阻塞之后的 decisi
|
|
|
252
261
|
新 event 带有 schema version。遇到更高且不支持的版本时,DriftSeal 会拒绝继续;如果
|
|
253
262
|
旧 client 未经 reconciliation 就关闭 linked intent,新 client 也会 fail closed。
|
|
254
263
|
`driftseal init` 会写入带版本的 managed blocks,并且只升级内容完全匹配的已知旧版本。
|
|
255
|
-
|
|
256
|
-
|
|
264
|
+
当前版本的 block 如果只是 log language 不同,也会被识别,因此可以用 `--lang`
|
|
265
|
+
改语言而不必手改协议。遇到更新的协议版本、无法识别的 block 或自定义内容时,
|
|
266
|
+
它会保持 `AGENTS.md` 不变并拒绝继续。
|
|
257
267
|
|
|
258
268
|
`--count` 只输出 status 筛选后的记录总数。它不能与 `--last` 一起使用,以免
|
|
259
269
|
“先限制再计数”造成歧义。Decision 文件名会构成一个轻量的内存索引:`show` 只
|
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
|
@@ -36,7 +36,8 @@ const DECISION_STATUSES = [
|
|
|
36
36
|
'superseded',
|
|
37
37
|
];
|
|
38
38
|
const EVENT_SCHEMA_VERSION = 3;
|
|
39
|
-
const PROTOCOL_VERSION =
|
|
39
|
+
const PROTOCOL_VERSION = 11;
|
|
40
|
+
const DEFAULT_LOG_LANGUAGE = 'en';
|
|
40
41
|
const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
41
42
|
const LOCK_INIT_STALE_MS = 5 * 1000;
|
|
42
43
|
const MAX_DECISION_SLUG_LENGTH = 180;
|
|
@@ -1122,10 +1123,213 @@ const INTENT_PROTOCOL_MARKER = '<!-- driftseal -->';
|
|
|
1122
1123
|
const INTENT_PROTOCOL_END = '<!-- /driftseal -->';
|
|
1123
1124
|
const DECISION_PROTOCOL_MARKER = '<!-- driftseal-decisions -->';
|
|
1124
1125
|
const DECISION_PROTOCOL_END = '<!-- /driftseal-decisions -->';
|
|
1126
|
+
const LOG_LANGUAGE_COMMENT_RE = /^<!-- driftseal-log-language: ([^>\r\n]+) -->\r?$/m;
|
|
1127
|
+
const LOG_LANGUAGE_PROSE_RE = /\*\*Log language:\*\* `([^`]+)`/;
|
|
1128
|
+
const IRREGULAR_GRANDFATHERED_TAGS = new Map([
|
|
1129
|
+
['en-gb-oed', 'en-GB-oed'],
|
|
1130
|
+
['i-ami', 'i-ami'],
|
|
1131
|
+
['i-bnn', 'i-bnn'],
|
|
1132
|
+
['i-default', 'i-default'],
|
|
1133
|
+
['i-enochian', 'i-enochian'],
|
|
1134
|
+
['i-hak', 'i-hak'],
|
|
1135
|
+
['i-klingon', 'i-klingon'],
|
|
1136
|
+
['i-lux', 'i-lux'],
|
|
1137
|
+
['i-mingo', 'i-mingo'],
|
|
1138
|
+
['i-navajo', 'i-navajo'],
|
|
1139
|
+
['i-pwn', 'i-pwn'],
|
|
1140
|
+
['i-tao', 'i-tao'],
|
|
1141
|
+
['i-tay', 'i-tay'],
|
|
1142
|
+
['i-tsu', 'i-tsu'],
|
|
1143
|
+
['sgn-be-fr', 'sgn-BE-FR'],
|
|
1144
|
+
['sgn-be-nl', 'sgn-BE-NL'],
|
|
1145
|
+
['sgn-ch-de', 'sgn-CH-DE'],
|
|
1146
|
+
]);
|
|
1147
|
+
|
|
1148
|
+
function titleCaseSubtag(value) {
|
|
1149
|
+
return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function isBcp47Variant(subtag) {
|
|
1153
|
+
return /^[A-Za-z0-9]{5,8}$/.test(subtag) || /^[0-9][A-Za-z0-9]{3}$/.test(subtag);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function isBcp47Singleton(subtag) {
|
|
1157
|
+
return /^[0-9A-WY-Za-wy-z]$/.test(subtag);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function parsePrivateUseSubtags(subtags, start, parts) {
|
|
1161
|
+
if (start >= subtags.length || !/^x$/i.test(subtags[start])) return null;
|
|
1162
|
+
parts.push('x');
|
|
1163
|
+
let index = start + 1;
|
|
1164
|
+
let count = 0;
|
|
1165
|
+
while (index < subtags.length && /^[A-Za-z0-9]{1,8}$/.test(subtags[index])) {
|
|
1166
|
+
parts.push(subtags[index].toLowerCase());
|
|
1167
|
+
index += 1;
|
|
1168
|
+
count += 1;
|
|
1169
|
+
}
|
|
1170
|
+
if (count === 0 || index !== subtags.length) return null;
|
|
1171
|
+
return parts.join('-');
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function wellFormedBcp47(tag) {
|
|
1175
|
+
if (typeof tag !== 'string' || tag.length === 0 || tag.startsWith('-') || tag.endsWith('-')) {
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
const irregular = IRREGULAR_GRANDFATHERED_TAGS.get(tag.toLowerCase());
|
|
1179
|
+
if (irregular) return irregular;
|
|
1180
|
+
|
|
1181
|
+
const subtags = tag.split('-');
|
|
1182
|
+
if (subtags.some((subtag) => subtag.length === 0)) return null;
|
|
1183
|
+
|
|
1184
|
+
const parts = [];
|
|
1185
|
+
if (/^x$/i.test(subtags[0])) return parsePrivateUseSubtags(subtags, 0, parts);
|
|
1186
|
+
|
|
1187
|
+
let index = 0;
|
|
1188
|
+
const language = subtags[index];
|
|
1189
|
+
if (!/^[A-Za-z]{2,8}$/.test(language)) return null;
|
|
1190
|
+
parts.push(language.toLowerCase());
|
|
1191
|
+
index += 1;
|
|
1192
|
+
if (language.length <= 3) {
|
|
1193
|
+
let extlang = 0;
|
|
1194
|
+
while (index < subtags.length && /^[A-Za-z]{3}$/.test(subtags[index]) && extlang < 3) {
|
|
1195
|
+
parts.push(subtags[index].toLowerCase());
|
|
1196
|
+
index += 1;
|
|
1197
|
+
extlang += 1;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
if (index < subtags.length && /^[A-Za-z]{4}$/.test(subtags[index])) {
|
|
1202
|
+
parts.push(titleCaseSubtag(subtags[index]));
|
|
1203
|
+
index += 1;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if (index < subtags.length && /^[A-Za-z]{2}$/.test(subtags[index])) {
|
|
1207
|
+
parts.push(subtags[index].toUpperCase());
|
|
1208
|
+
index += 1;
|
|
1209
|
+
} else if (index < subtags.length && /^[0-9]{3}$/.test(subtags[index])) {
|
|
1210
|
+
parts.push(subtags[index]);
|
|
1211
|
+
index += 1;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const variants = new Set();
|
|
1215
|
+
while (index < subtags.length && isBcp47Variant(subtags[index])) {
|
|
1216
|
+
const variant = subtags[index].toLowerCase();
|
|
1217
|
+
if (variants.has(variant)) return null;
|
|
1218
|
+
variants.add(variant);
|
|
1219
|
+
parts.push(variant);
|
|
1220
|
+
index += 1;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const singletons = new Set();
|
|
1224
|
+
while (index < subtags.length && isBcp47Singleton(subtags[index])) {
|
|
1225
|
+
const singleton = subtags[index].toLowerCase();
|
|
1226
|
+
if (singletons.has(singleton)) return null;
|
|
1227
|
+
singletons.add(singleton);
|
|
1228
|
+
parts.push(singleton);
|
|
1229
|
+
index += 1;
|
|
1230
|
+
let following = 0;
|
|
1231
|
+
while (index < subtags.length && /^[A-Za-z0-9]{2,8}$/.test(subtags[index])) {
|
|
1232
|
+
parts.push(subtags[index].toLowerCase());
|
|
1233
|
+
index += 1;
|
|
1234
|
+
following += 1;
|
|
1235
|
+
}
|
|
1236
|
+
if (following === 0) return null;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
if (index < subtags.length && /^x$/i.test(subtags[index])) {
|
|
1240
|
+
return parsePrivateUseSubtags(subtags, index, parts);
|
|
1241
|
+
}
|
|
1242
|
+
if (index !== subtags.length) return null;
|
|
1243
|
+
return parts.join('-');
|
|
1244
|
+
}
|
|
1125
1245
|
|
|
1126
|
-
function
|
|
1246
|
+
function canonicalizeLogLanguage(value) {
|
|
1247
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
1248
|
+
fail('invalid log language: use a BCP 47 tag such as en or zh-CN');
|
|
1249
|
+
}
|
|
1250
|
+
const canonical = wellFormedBcp47(value.trim());
|
|
1251
|
+
if (!canonical) {
|
|
1252
|
+
fail(`invalid log language "${value}": use a BCP 47 tag such as en or zh-CN`);
|
|
1253
|
+
}
|
|
1254
|
+
return canonical;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function intentLogLanguageParagraph(language) {
|
|
1258
|
+
return `**Log language:** \`${language}\`. Write intent-log prose (intent, note,
|
|
1259
|
+
verify-result, and reclaim/unreclaim reason) in that language. Keep command
|
|
1260
|
+
names, flags, status tokens, and ids in English.`;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function decisionLogLanguageParagraph(language) {
|
|
1264
|
+
return `**Log language:** \`${language}\`. Write decision-log prose (title, context,
|
|
1265
|
+
outcome, drivers, options, consequences, and update notes) in that language.
|
|
1266
|
+
Keep MADR section headings, status tokens, and ids in English.`;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function parseLogLanguageFromBlock(block, label) {
|
|
1270
|
+
const commentMatch = block.match(LOG_LANGUAGE_COMMENT_RE);
|
|
1271
|
+
const proseMatch = block.match(LOG_LANGUAGE_PROSE_RE);
|
|
1272
|
+
const comment = commentMatch ? canonicalizeLogLanguage(commentMatch[1]) : null;
|
|
1273
|
+
const prose = proseMatch ? canonicalizeLogLanguage(proseMatch[1]) : null;
|
|
1274
|
+
if (comment && prose && comment !== prose) {
|
|
1275
|
+
fail(
|
|
1276
|
+
`${label} declares different log languages in the comment (${comment}) and prose (${prose}); pass --lang to set one`
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
return comment || prose || null;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function extractManagedBlock(content, marker, endMarker) {
|
|
1283
|
+
const start = content.indexOf(marker);
|
|
1284
|
+
if (start === -1) return null;
|
|
1285
|
+
const end = content.indexOf(endMarker, start);
|
|
1286
|
+
if (end === -1) return null;
|
|
1287
|
+
return content.slice(start, end + endMarker.length);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function resolveInitLogLanguage(requested, content) {
|
|
1291
|
+
if (requested !== undefined) return canonicalizeLogLanguage(requested);
|
|
1292
|
+
const languages = new Set();
|
|
1293
|
+
const intent = extractManagedBlock(content, INTENT_PROTOCOL_MARKER, INTENT_PROTOCOL_END);
|
|
1294
|
+
const decision = extractManagedBlock(content, DECISION_PROTOCOL_MARKER, DECISION_PROTOCOL_END);
|
|
1295
|
+
if (intent) {
|
|
1296
|
+
const language = parseLogLanguageFromBlock(intent, 'intent protocol');
|
|
1297
|
+
if (language) languages.add(language);
|
|
1298
|
+
}
|
|
1299
|
+
if (decision) {
|
|
1300
|
+
const language = parseLogLanguageFromBlock(decision, 'decision protocol');
|
|
1301
|
+
if (language) languages.add(language);
|
|
1302
|
+
}
|
|
1303
|
+
if (languages.size > 1) {
|
|
1304
|
+
fail(
|
|
1305
|
+
`intent and decision protocols declare different log languages (${[...languages].join(', ')}); pass --lang to set one`
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
return languages.size === 1 ? [...languages][0] : DEFAULT_LOG_LANGUAGE;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function protocolBlockKey(block) {
|
|
1312
|
+
return block
|
|
1313
|
+
.replace(/^<!-- driftseal-log-language: [^>\r\n]+ -->\r?$/m, '<!-- driftseal-log-language: -->')
|
|
1314
|
+
.replace(/\*\*Log language:\*\* `[^`]+`/g, '**Log language:** ``');
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function stripIntentLogLanguage(block, language = DEFAULT_LOG_LANGUAGE) {
|
|
1318
|
+
return block
|
|
1319
|
+
.replace(`\n<!-- driftseal-log-language: ${language} -->`, '')
|
|
1320
|
+
.replace(`\n${intentLogLanguageParagraph(language)}\n`, '');
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function stripDecisionLogLanguage(block, language = DEFAULT_LOG_LANGUAGE) {
|
|
1324
|
+
return block
|
|
1325
|
+
.replace(`\n<!-- driftseal-log-language: ${language} -->`, '')
|
|
1326
|
+
.replace(`\n${decisionLogLanguageParagraph(language)}\n`, '');
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function intentProtocolBlock(version = PROTOCOL_VERSION, language = DEFAULT_LOG_LANGUAGE) {
|
|
1127
1330
|
return `${INTENT_PROTOCOL_MARKER}
|
|
1128
1331
|
<!-- driftseal-version: ${version} -->
|
|
1332
|
+
<!-- driftseal-log-language: ${language} -->
|
|
1129
1333
|
|
|
1130
1334
|
## Agent protocol: intent write-ahead log
|
|
1131
1335
|
|
|
@@ -1135,6 +1339,8 @@ This \`AGENTS.md\` protocol is the source of truth. Use the \`driftseal\` CLI by
|
|
|
1135
1339
|
default; the companion skill only helps discover and resume the workflow, while
|
|
1136
1340
|
MCP and lifecycle hooks are optional adapters.
|
|
1137
1341
|
|
|
1342
|
+
${intentLogLanguageParagraph(language)}
|
|
1343
|
+
|
|
1138
1344
|
1. **Write intent first**, before modifying, creating, or deleting files, or
|
|
1139
1345
|
making any other non-Git change that may need a rollback:
|
|
1140
1346
|
\`driftseal begin "<what this round will accomplish>" --verify "<command or check that proves it>"\`.
|
|
@@ -1178,8 +1384,9 @@ ${INTENT_PROTOCOL_END}`;
|
|
|
1178
1384
|
}
|
|
1179
1385
|
|
|
1180
1386
|
function previousIntentProtocolBlock(version) {
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1387
|
+
const v10 = stripIntentLogLanguage(intentProtocolBlock(version, DEFAULT_LOG_LANGUAGE));
|
|
1388
|
+
if (version >= 10) return v10;
|
|
1389
|
+
const v9 = v10.replace(
|
|
1183
1390
|
'1. **Write intent first**, before modifying, creating, or deleting files, or\n' +
|
|
1184
1391
|
' making any other non-Git change that may need a rollback:\n' +
|
|
1185
1392
|
' `driftseal begin "<what this round will accomplish>" --verify "<command or check that proves it>"`.\n' +
|
|
@@ -1258,9 +1465,10 @@ function protocolEol(content, eol) {
|
|
|
1258
1465
|
return eol === '\n' ? content : content.replace(/\n/g, eol);
|
|
1259
1466
|
}
|
|
1260
1467
|
|
|
1261
|
-
function decisionProtocolBlock(version = PROTOCOL_VERSION) {
|
|
1468
|
+
function decisionProtocolBlock(version = PROTOCOL_VERSION, language = DEFAULT_LOG_LANGUAGE) {
|
|
1262
1469
|
return `${DECISION_PROTOCOL_MARKER}
|
|
1263
1470
|
<!-- driftseal-decisions-version: ${version} -->
|
|
1471
|
+
<!-- driftseal-log-language: ${language} -->
|
|
1264
1472
|
|
|
1265
1473
|
## Agent protocol: decision log
|
|
1266
1474
|
|
|
@@ -1270,6 +1478,8 @@ revisiting, non-obvious rationale behind a long-lived or costly-to-reverse accep
|
|
|
1270
1478
|
choice, or a deprecated or superseded decision. Do not record routine, local,
|
|
1271
1479
|
readily reversible choices.
|
|
1272
1480
|
|
|
1481
|
+
${decisionLogLanguageParagraph(language)}
|
|
1482
|
+
|
|
1273
1483
|
\`driftseal decision add "<title>" --context "<problem and constraints>" --outcome "<decision and rationale>" --driver "<decision driver>" --option "<considered option>" --consequence "<result>"\`
|
|
1274
1484
|
|
|
1275
1485
|
Add one \`--driver\`, \`--option\`, or \`--consequence\` flag per item. Use
|
|
@@ -1338,9 +1548,9 @@ Commit \`.decision-log/\` with the code.`;
|
|
|
1338
1548
|
}
|
|
1339
1549
|
|
|
1340
1550
|
function previousDecisionProtocolBlock(version) {
|
|
1341
|
-
const
|
|
1342
|
-
if (version >= 9) return
|
|
1343
|
-
const v8 =
|
|
1551
|
+
const v10 = stripDecisionLogLanguage(decisionProtocolBlock(version, DEFAULT_LOG_LANGUAGE));
|
|
1552
|
+
if (version >= 9) return v10;
|
|
1553
|
+
const v8 = v10.replace(
|
|
1344
1554
|
'\nAfter a merge, colliding decision ids are remapped with `driftseal absorb`;\n' +
|
|
1345
1555
|
'concurrent edits of a shared decision are not auto-merged.',
|
|
1346
1556
|
''
|
|
@@ -1381,7 +1591,11 @@ function upgradeManagedBlock({
|
|
|
1381
1591
|
`protocol version ${version} requires a newer DriftSeal client (supported: ${PROTOCOL_VERSION})`
|
|
1382
1592
|
);
|
|
1383
1593
|
}
|
|
1384
|
-
if (
|
|
1594
|
+
if (
|
|
1595
|
+
block !== replacement &&
|
|
1596
|
+
!knownManagedBlocks.includes(block) &&
|
|
1597
|
+
protocolBlockKey(block) !== protocolBlockKey(replacement)
|
|
1598
|
+
) {
|
|
1385
1599
|
fail(`cannot safely upgrade customized protocol block beginning with ${marker}`);
|
|
1386
1600
|
}
|
|
1387
1601
|
return {
|
|
@@ -1709,6 +1923,19 @@ function directoryDigest(directory) {
|
|
|
1709
1923
|
return digest.digest('hex');
|
|
1710
1924
|
}
|
|
1711
1925
|
|
|
1926
|
+
function preserveRegularFileModes(source, destination) {
|
|
1927
|
+
for (const name of fs.readdirSync(source)) {
|
|
1928
|
+
const sourcePath = path.join(source, name);
|
|
1929
|
+
const destinationPath = path.join(destination, name);
|
|
1930
|
+
const stat = fs.lstatSync(sourcePath);
|
|
1931
|
+
if (stat.isDirectory()) {
|
|
1932
|
+
preserveRegularFileModes(sourcePath, destinationPath);
|
|
1933
|
+
} else if (stat.isFile()) {
|
|
1934
|
+
fs.chmodSync(destinationPath, stat.mode & 0o777);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1712
1939
|
function installSkill(request) {
|
|
1713
1940
|
const { force, root, scope, skillDir, skillsDir, target, targetLabel } = request;
|
|
1714
1941
|
const sourceDir = path.join(__dirname, '..', 'skills', SKILL_NAME);
|
|
@@ -1736,6 +1963,7 @@ function installSkill(request) {
|
|
|
1736
1963
|
let movedExisting = false;
|
|
1737
1964
|
try {
|
|
1738
1965
|
fs.cpSync(sourceDir, temporary, { recursive: true, errorOnExist: true });
|
|
1966
|
+
preserveRegularFileModes(sourceDir, temporary);
|
|
1739
1967
|
if (existingDigest !== null) {
|
|
1740
1968
|
fs.renameSync(skillDir, backup);
|
|
1741
1969
|
movedExisting = true;
|
|
@@ -3308,14 +3536,15 @@ const commands = {
|
|
|
3308
3536
|
},
|
|
3309
3537
|
|
|
3310
3538
|
init(argv) {
|
|
3311
|
-
const { positionals } = parseArgs(argv, {});
|
|
3312
|
-
if (positionals.length > 0) fail('usage: driftseal init');
|
|
3539
|
+
const { positionals, flags } = parseArgs(argv, { lang: 'single' });
|
|
3540
|
+
if (positionals.length > 0) fail('usage: driftseal init [--lang <tag>]');
|
|
3313
3541
|
const target = path.join(process.cwd(), 'AGENTS.md');
|
|
3314
3542
|
const existed = fs.existsSync(target);
|
|
3315
3543
|
const current = existed ? fs.readFileSync(target, 'utf8') : '';
|
|
3316
3544
|
const eol = current.includes('\r\n') ? '\r\n' : '\n';
|
|
3317
|
-
const
|
|
3318
|
-
const
|
|
3545
|
+
const language = resolveInitLogLanguage(flags.lang, current);
|
|
3546
|
+
const intentBlock = protocolEol(intentProtocolBlock(PROTOCOL_VERSION, language), eol);
|
|
3547
|
+
const decisionBlock = protocolEol(decisionProtocolBlock(PROTOCOL_VERSION, language), eol);
|
|
3319
3548
|
let updated = current;
|
|
3320
3549
|
const intent = upgradeManagedBlock({
|
|
3321
3550
|
content: updated,
|
|
@@ -3332,6 +3561,7 @@ const commands = {
|
|
|
3332
3561
|
protocolEol(previousIntentProtocolBlock(7), eol),
|
|
3333
3562
|
protocolEol(previousIntentProtocolBlock(8), eol),
|
|
3334
3563
|
protocolEol(previousIntentProtocolBlock(9), eol),
|
|
3564
|
+
protocolEol(previousIntentProtocolBlock(10), eol),
|
|
3335
3565
|
],
|
|
3336
3566
|
knownLegacyBlocks: [protocolEol(legacyIntentProtocolBlock(), eol)],
|
|
3337
3567
|
});
|
|
@@ -3351,6 +3581,7 @@ const commands = {
|
|
|
3351
3581
|
protocolEol(previousDecisionProtocolBlock(7), eol),
|
|
3352
3582
|
protocolEol(previousDecisionProtocolBlock(8), eol),
|
|
3353
3583
|
protocolEol(previousDecisionProtocolBlock(9), eol),
|
|
3584
|
+
protocolEol(previousDecisionProtocolBlock(10), eol),
|
|
3354
3585
|
],
|
|
3355
3586
|
knownLegacyBlocks: [protocolEol(legacyDecisionProtocolBlock(), eol)],
|
|
3356
3587
|
});
|
|
@@ -3431,7 +3662,8 @@ usage:
|
|
|
3431
3662
|
targets: kimi-code (global only), claude-code, codex (prompt only)
|
|
3432
3663
|
driftseal hook prompt|stop [--format plain|claude-code]
|
|
3433
3664
|
emit the reminder a lifecycle hook injects; never blocks
|
|
3434
|
-
driftseal init
|
|
3665
|
+
driftseal init [--lang <tag>] inject protocols into ./AGENTS.md and configure the git merge driver
|
|
3666
|
+
--lang sets the intent/decision log language (BCP 47, default: en)
|
|
3435
3667
|
driftseal --version | -V print the installed DriftSeal version
|
|
3436
3668
|
driftseal help
|
|
3437
3669
|
|
|
@@ -3588,6 +3820,17 @@ function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
|
|
|
3588
3820
|
if (all) argv.push('--all');
|
|
3589
3821
|
return call(argv);
|
|
3590
3822
|
},
|
|
3823
|
+
absorb({ otherLog, otherDecisions, abandon, dryRun = false } = {}) {
|
|
3824
|
+
if (abandon && !['ours', 'theirs'].includes(abandon)) {
|
|
3825
|
+
fail('absorb abandon must be "ours" or "theirs"');
|
|
3826
|
+
}
|
|
3827
|
+
const argv = ['absorb'];
|
|
3828
|
+
if (otherLog) argv.push(String(otherLog));
|
|
3829
|
+
appendFlag(argv, '--decisions', otherDecisions);
|
|
3830
|
+
if (abandon) argv.push(`--abandon-${abandon}`);
|
|
3831
|
+
if (dryRun) argv.push('--dry-run');
|
|
3832
|
+
return call(argv);
|
|
3833
|
+
},
|
|
3591
3834
|
reclaim({ ids = [], reason, olderThan, force = false, dryRun = false }) {
|
|
3592
3835
|
const argv = ['reclaim', ...ids.map(String), '--reason', reason];
|
|
3593
3836
|
appendFlag(argv, '--older-than', olderThan);
|