driftseal 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,15 +28,19 @@ DriftSeal v2 is an outcome log rather than an intent-per-step log.
28
28
  decision link to the currently open outcome.
29
29
  - Every extension changes the contract hash and invalidates earlier verification
30
30
  and MADR reconciliation.
31
- - Stored events use `logVersion: 2` and `schemaVersion: 1`.
32
- - The generated `AGENTS.md` protocol series is `2.0`; compatible protocol
33
- refinements use `2.1`, `2.2`, and so on.
31
+ - Stored events use `logVersion: 2`. Compatible clients accept `schemaVersion`
32
+ `1` or `2`; lane events and non-default `begin.lane` use `schemaVersion: 2`.
33
+ - The generated `AGENTS.md` protocol series is `2.1`. `driftseal init` upgrades
34
+ recognized `2.0` blocks.
35
+ - Named lanes partition outcome history on the same WAL. The default lane is
36
+ `main`; `driftseal log` follows the current lane.
34
37
  - The public CLI, Node API, MCP tools, and MCP resources use outcome terminology.
35
38
  v1 names and storage paths are not runtime aliases.
36
39
 
37
40
  ## Install
38
41
 
39
- DriftSeal requires Node.js 18 or newer.
42
+ DriftSeal requires Node.js 22.13 or newer. Its derived outcome index uses the
43
+ built-in `node:sqlite` module; no native database package is installed.
40
44
 
41
45
  ```sh
42
46
  npm install --global driftseal
@@ -121,6 +125,56 @@ The scope belongs to the worktree, not to the agent process. Agents and subagent
121
125
  working in the same worktree resume its matching open outcome. Separate worktrees
122
126
  hold separate outcomes.
123
127
 
128
+ ## Lanes
129
+
130
+ Orthogonal long-lived capabilities can share one append-only log without sharing
131
+ narrative context. Each outcome belongs to one named lane. Untagged history lives
132
+ on `main`. `status` and `log --last 3` follow the current lane, which is local to
133
+ the worktree.
134
+
135
+ ```sh
136
+ driftseal lane add index --desc "On-disk inverted index"
137
+ driftseal lane switch index
138
+ driftseal begin "Ship the inverted index" --accept "lookups return stored postings" --verify "npm test"
139
+ ```
140
+
141
+ Close the open outcome before switching. Later work on the same capability
142
+ switches back and begins a new outcome on that lane; it does not reopen a closed
143
+ record. `driftseal lane assign <id> <name>` moves a closed outcome. Cross-cutting
144
+ work stays on `main`. An open outcome stays visible in `log` even when it belongs
145
+ to another lane; `status` prints that lane when it differs from the current one.
146
+
147
+ Lanes cannot be renamed or removed. A typo in `lane add` stays in `driftseal lane`
148
+ output; add the intended name and stop using the old one.
149
+
150
+ If a `begin` names a lane whose `lane_add` is missing, DriftSeal infers the lane
151
+ so `status`, `log`, and `lane` still work, and `lane add` can write the missing
152
+ event. Duplicate `lane_add` records last-write the description instead of failing
153
+ the fold. A `lane_assign` that names an unknown lane infers it the same way.
154
+ If the worktree's current-lane pointer names a lane the WAL no longer
155
+ has, `status`, `log`, and `lane` fall back to `main` with a warning; `begin`
156
+ still refuses until you `lane switch main` or add the lane. `log --last N` can
157
+ return more than N records when an open outcome sits on another lane.
158
+
159
+ A derived SQLite outcome index stores folded records in Git metadata (or beside
160
+ a custom seal). It is a disposable read model; `events.jsonl` remains the only
161
+ canonical history. Incremental sync follows `indexedThrough` and
162
+ `indexedLines`. File identity makes unchanged hot reads constant-time, while a
163
+ full checksum of the previously indexed WAL prefix validates every incremental
164
+ catch-up. A source rewrite, incompatible schema, malformed indexed row, or
165
+ SQLite read error triggers a full rebuild.
166
+
167
+ `log --last N` uses a `(lane, reclaimed, ordinal)` SQLite index and reads only
168
+ the selected outcome rows plus open outcomes from other lanes. Parked events
169
+ are checked and folded as a targeted overlay using indexed committed event
170
+ identities, so an open outcome does not force a committed-WAL scan. When a
171
+ lock-free read sees a missing or stale database, DriftSeal folds the canonical
172
+ WAL in memory instead of serving stale index data. Stored WAL byte ranges remain
173
+ available for future projections but are not required for recent-log lookup.
174
+ The database is reconstructable and is not committed with the log. Custom-home
175
+ sidecars sit next to `events.jsonl`; when that directory is inside a Git
176
+ worktree, they are listed in its `.gitignore`.
177
+
124
178
  ## Decisions and MADR
125
179
 
126
180
  Use a MADR only for context that the outcome log and Git cannot recover: a
@@ -152,8 +206,9 @@ driftseal decision update 1 --status accepted --note "Confirmed by the final imp
152
206
  | `driftseal extend "<addition>" [--accept "..."] [--verify "..."] [--decision id]` | Append scope to the same outcome and invalidate earlier verification. |
153
207
  | `driftseal verify [--allow-tracked-command]` | Execute the declared cumulative verifier and bind evidence. |
154
208
  | `driftseal end [id] [-s status] [-n note] [-r verify-result]` | Close an outcome honestly. |
155
- | `driftseal status` | Show the outcome in progress. |
156
- | `driftseal log [--last N] [--all]` | Review outcome history. |
209
+ | `driftseal status` | Show the outcome in progress and the current lane. |
210
+ | `driftseal log [--last N] [--all] [--all-lanes]` | Review outcome history (current lane unless `--all-lanes`). |
211
+ | `driftseal lane add\|switch\|assign\|show` | Partition history by long-lived capability. |
157
212
  | `driftseal reclaim [id ...] --reason "..." [--force]` | Hide meaningless closed records with append-only markers. |
158
213
  | `driftseal unreclaim <id> --reason "..."` | Restore a reclaimed record. |
159
214
  | `driftseal absorb [other-events.jsonl] [--decisions dir] [--abandon-theirs\|--abandon-ours]` | Merge another lineage and remap colliding outcome or MADR ids. |
@@ -266,24 +321,31 @@ seal.begin({
266
321
  seal.extend({ extension: 'Document token expiry' });
267
322
  ```
268
323
 
269
- The API also exposes `status`, `verify`, `end`, `log`, `absorb`, reclaim,
270
- decision, init, and migration methods.
324
+ The API also exposes `status`, `verify`, `end`, `log`, `lane`, `laneAdd`,
325
+ `laneSwitch`, `laneAssign`, `absorb`, reclaim, decision, init, and migration
326
+ methods.
271
327
 
272
328
  The stdio MCP server fixes all operations to one repository root. Its v2 tools
273
329
  include `driftseal_status`, `driftseal_begin`, `driftseal_extend`,
274
- `driftseal_verify`, `driftseal_end`, outcome history and absorb tools, MADR
275
- tools, and the three migration tools. Resources are:
330
+ `driftseal_verify`, `driftseal_end`, outcome history, lane, and absorb tools,
331
+ MADR tools, and the three migration tools. Resources are:
276
332
 
277
333
  - `driftseal://outcome/current`
278
334
  - `driftseal://outcomes/recent`
335
+ - `driftseal://lanes`
279
336
  - `driftseal://madr`
280
337
 
281
338
  ## Storage and trust boundary
282
339
 
283
340
  - `.seal/outcomes/events.jsonl` is the append-only outcome log. Access it through
284
- DriftSeal; use `reclaim`, `unreclaim`, and `absorb` instead of manual edits.
341
+ DriftSeal; use `reclaim`, `unreclaim`, `lane`, and `absorb` instead of manual edits.
285
342
  - `.seal/madr/` stores numbered MADR documents.
286
343
  - `$DRIFTSEAL_HOME` replaces the `.seal` root.
344
+ - The current lane and derived SQLite outcome index live in Git metadata for a default
345
+ repository seal, or beside a custom seal (`outcomes/.current-lane` and
346
+ `outcomes/.outcome-index.sqlite`). When the custom seal sits inside a Git
347
+ worktree, those sidecars are gitignored. They are reconstructable and are not
348
+ part of the committed WAL.
287
349
  - Advisory hooks remind agents about lifecycle state but never broaden the
288
350
  repository's `AGENTS.md` policy.
289
351
 
package/README.zh-CN.md CHANGED
@@ -23,14 +23,18 @@ DriftSeal v2 从“按步骤记录 intent”改为“按交付记录 outcome”
23
23
  目录;migration 时需要显式传入旧位置,之后再 unset 或替换这个变量。
24
24
  - `driftseal extend` 可以向当前 outcome 追加步骤、acceptance、verifier 或 decision link。
25
25
  - 每次 extend 都会改变 contract hash,并让之前的 verification 与 MADR reconciliation 失效。
26
- - event 使用 `logVersion: 2`、`schemaVersion: 1`。
27
- - `AGENTS.md` 的新协议版本从 `2.0` 开始,兼容改进依次使用 `2.1`、`2.2`。
26
+ - event 使用 `logVersion: 2`。兼容客户端接受 `schemaVersion` `1` 或 `2`;lane
27
+ 事件以及非默认的 `begin.lane` 使用 `schemaVersion: 2`。
28
+ - `AGENTS.md` 的新协议版本是 `2.1`。`driftseal init` 会升级可识别的 `2.0` block。
29
+ - 具名 lane 在同一条 WAL 上切分 outcome 历史。默认 lane 是 `main`;`driftseal log`
30
+ 跟随当前 lane。
28
31
  - CLI、Node API、MCP tool 与 resource 全部使用 outcome 命名;v1 名称和路径不会作为
29
32
  runtime alias 保留。
30
33
 
31
34
  ## 安装
32
35
 
33
- DriftSeal 需要 Node.js 18 或更高版本。
36
+ DriftSeal 需要 Node.js 22.13 或更高版本。派生 outcome index 使用 Node 内置的
37
+ `node:sqlite`,不会安装 native database package。
34
38
 
35
39
  ```sh
36
40
  npm install --global driftseal
@@ -109,6 +113,48 @@ driftseal log --last 3
109
113
  作用域属于 worktree,而不是某一个 agent process。同一 worktree 内的 agent 与
110
114
  subagent 重新锚定并继续匹配的 open outcome;不同 worktree 各自持有 outcome。
111
115
 
116
+ ## Lane
117
+
118
+ 彼此正交、会长期回来接着做的能力,可以共用一条 append-only log,而不共用叙事上下文。
119
+ 每个 outcome 只属于一条具名 lane。未打标签的历史在 `main` 上。`status` 与
120
+ `log --last 3` 跟随当前 lane,指针是 worktree 本地的。
121
+
122
+ ```sh
123
+ driftseal lane add index --desc "On-disk inverted index"
124
+ driftseal lane switch index
125
+ driftseal begin "Ship the inverted index" --accept "lookups return stored postings" --verify "npm test"
126
+ ```
127
+
128
+ 有 open outcome 时不能切 lane。以后回到同一能力,是切回去再 `begin` 新的 outcome,
129
+ 而不是重开已关闭的记录。`driftseal lane assign <id> <name>` 可以把已关闭的 outcome
130
+ 移过去。跨切工作留在 `main`。open outcome 即使属于别的 lane,也会出现在 `log` 里;
131
+ 若它和当前 lane 不同,`status` 会写出它的 lane。
132
+
133
+ lane 不能改名或删除。`lane add` 打错的名字会一直出现在 `driftseal lane` 输出里;
134
+ 补上正确名字,旧名字不要再用。
135
+
136
+ 如果 `begin` 引用了 WAL 里没有 `lane_add` 的 lane,DriftSeal 会推断这条 lane,
137
+ 让 `status` / `log` / `lane` 仍可读,也允许再用 `lane add` 补上缺失事件。重复的
138
+ `lane_add` 只更新 description,不会让 fold 失败;没有前置 `lane_add` 的
139
+ `lane_assign` 同样会推断该 lane。若 worktree 的 current-lane 指针指向 WAL 里已
140
+ 不存在的 lane,`status` / `log` / `lane` 会警告并回落到 `main`;`begin` 仍会拒绝,
141
+ 直到 `lane switch main` 或把该 lane 加回来。`log --last N` 在 open outcome 属于
142
+ 别的 lane 时,返回条数可以多于 N。
143
+
144
+ 派生的 SQLite outcome index 放在 Git metadata(或自定义 seal 旁边)。它只是可以
145
+ 随时删除重建的 read model;`events.jsonl` 仍是唯一 canonical history。增量同步跟随
146
+ `indexedThrough` 和 `indexedLines`。未变化的 hot read 用 file identity 做常量时间
147
+ 校验;增量追赶前会校验此前全部 WAL prefix 的 checksum。source 被改写、schema
148
+ 不兼容、indexed row 损坏或 SQLite read 失败时,DriftSeal 会全量重建。
149
+
150
+ `log --last N` 使用 `(lane, reclaimed, ordinal)` SQLite index,只读取命中的 outcome
151
+ row,并补上其他 lane 的 open outcome。parked event 会依据 index 中保存的 committed
152
+ event identity 做定向 overlay,不再因此重扫 committed WAL。无锁读取遇到缺失或 stale
153
+ database 时,会在内存中 fold canonical WAL,绝不会返回 stale index。保存的 WAL byte
154
+ range 留给后续 projection 使用,但 recent-log lookup 不依赖它。database 可以重建,
155
+ 不会随 log 一起提交。自定义 home 下的 sidecar 放在 `events.jsonl` 旁边;该目录在 Git
156
+ worktree 内时,由目录里的 `.gitignore` 忽略。
157
+
112
158
  ## Decision 与 MADR
113
159
 
114
160
  只有当 outcome log 与 Git 无法还原重要上下文时才建立 MADR,例如值得以后重访的
@@ -139,8 +185,9 @@ driftseal decision update 1 --status accepted --note "Confirmed by the final imp
139
185
  | `driftseal extend "<addition>" [--accept "..."] [--verify "..."] [--decision id]` | 向同一 outcome 追加内容,并让旧 verification 失效。 |
140
186
  | `driftseal verify [--allow-tracked-command]` | 执行累计 verifier 并绑定证据。 |
141
187
  | `driftseal end [id] [-s status] [-n note] [-r verify-result]` | 诚实关闭 outcome。 |
142
- | `driftseal status` | 查看进行中的 outcome。 |
143
- | `driftseal log [--last N] [--all]` | 查看 outcome 历史。 |
188
+ | `driftseal status` | 查看进行中的 outcome 和当前 lane。 |
189
+ | `driftseal log [--last N] [--all] [--all-lanes]` | 查看 outcome 历史(默认当前 lane)。 |
190
+ | `driftseal lane add\|switch\|assign\|show` | 按长期能力切分历史。 |
144
191
  | `driftseal reclaim [id ...] --reason "..." [--force]` | 通过 append-only marker 隐藏无意义的已关闭记录。 |
145
192
  | `driftseal unreclaim <id> --reason "..."` | 恢复 reclaimed record。 |
146
193
  | `driftseal absorb [other-events.jsonl] [--decisions dir] [--abandon-theirs\|--abandon-ours]` | 合并另一条 lineage 并处理撞号。 |
@@ -243,23 +290,30 @@ seal.begin({
243
290
  seal.extend({ extension: 'Document token expiry' });
244
291
  ```
245
292
 
246
- API 还提供 `status`、`verify`、`end`、`log`、`absorb`、reclaim、decision、init 与
247
- migration 方法。
293
+ API 还提供 `status`、`verify`、`end`、`log`、`lane`、`laneAdd`、`laneSwitch`、
294
+ `laneAssign`、`absorb`、reclaim、decision、init 与 migration 方法。
248
295
 
249
296
  stdio MCP server 会把所有操作固定在一个 repo root。v2 tools 包括
250
297
  `driftseal_status`、`driftseal_begin`、`driftseal_extend`、`driftseal_verify`、
251
- `driftseal_end`、outcome history/absorb、MADR,以及三个 migration tools。resources 为:
298
+ `driftseal_end`、outcome history、lane、absorb、MADR,以及三个 migration tools。
299
+ resources 为:
252
300
 
253
301
  - `driftseal://outcome/current`
254
302
  - `driftseal://outcomes/recent`
303
+ - `driftseal://lanes`
255
304
  - `driftseal://madr`
256
305
 
257
306
  ## 存储与信任边界
258
307
 
259
308
  - `.seal/outcomes/events.jsonl` 是 append-only outcome log。通过 DriftSeal 访问它;
260
- 需要调整可见性或处理 merge 时使用 `reclaim`、`unreclaim`、`absorb`,不要手改。
309
+ 需要调整可见性、切分 lane 或处理 merge 时使用 `reclaim`、`unreclaim`、`lane`、
310
+ `absorb`,不要手改。
261
311
  - `.seal/madr/` 保存编号化 MADR。
262
312
  - `$DRIFTSEAL_HOME` 替换整个 `.seal` root。
313
+ - 当前 lane 与派生 SQLite outcome index 对默认 repo seal 存在 Git metadata 里,对自定义 seal
314
+ 则放在旁边(`outcomes/.current-lane` 与 `outcomes/.outcome-index.sqlite`)。自定义
315
+ seal 在 Git worktree 内时,这些 sidecar 会被 gitignore。它们可以重建,不是
316
+ committed WAL 的一部分。
263
317
  - advisory hook 只提示 lifecycle 状态,不会扩大 repo 中 `AGENTS.md` 的政策边界。
264
318
 
265
319
  DriftSeal 不会替你判断 verification command 是否安全,也不会判断测试本身是否充分。
@@ -0,0 +1,181 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const { execFileSync } = require('node:child_process');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const { performance } = require('node:perf_hooks');
9
+ const { openOutcomeIndex } = require('../lib/outcome-index-sqlite.js');
10
+
11
+ const DRIFTSEAL = path.join(__dirname, '..', 'bin', 'driftseal.js');
12
+ const quick = process.argv.includes('--quick');
13
+ const large = process.argv.includes('--large');
14
+ const unrelatedOutcomes = large ? 100_000 : quick ? 2_000 : 10_000;
15
+ const laneOutcomes = large ? 1_000 : quick ? 20 : 100;
16
+ const samples = quick ? 5 : 20;
17
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'driftseal-recent-log-benchmark-'));
18
+ const home = path.join(root, 'seal');
19
+ const outcomeDirectory = path.join(home, 'outcomes');
20
+ const wal = path.join(outcomeDirectory, 'events.jsonl');
21
+ const indexFile = path.join(outcomeDirectory, '.outcome-index.sqlite');
22
+ const env = {
23
+ ...process.env,
24
+ DRIFTSEAL_HOME: home,
25
+ DRIFTSEAL_DECISION_HOME: path.join(home, 'madr'),
26
+ };
27
+
28
+ function runCli(args, envOverrides = {}) {
29
+ return execFileSync(process.execPath, [DRIFTSEAL, ...args], {
30
+ cwd: root,
31
+ encoding: 'utf8',
32
+ env: { ...env, ...envOverrides },
33
+ stdio: ['ignore', 'pipe', 'pipe'],
34
+ maxBuffer: 64 * 1024 * 1024,
35
+ });
36
+ }
37
+
38
+ function event(type, fields) {
39
+ return {
40
+ logVersion: 2,
41
+ schemaVersion: type === 'lane_add' || fields.lane ? 2 : 1,
42
+ type,
43
+ ...fields,
44
+ };
45
+ }
46
+
47
+ function outcomeEvents(id, outcome, lane, minute) {
48
+ const beganAt = new Date(Date.UTC(2026, 0, 1, 0, minute, 0)).toISOString();
49
+ const endedAt = new Date(Date.UTC(2026, 0, 1, 0, minute, 1)).toISOString();
50
+ return [
51
+ event('begin', {
52
+ id,
53
+ ts: beganAt,
54
+ outcome,
55
+ acceptance: [],
56
+ verify: null,
57
+ decisions: [],
58
+ ...(lane === 'main' ? {} : { lane }),
59
+ }),
60
+ event('end', {
61
+ id,
62
+ ts: endedAt,
63
+ status: 'abandoned',
64
+ note: `closed ${outcome}`,
65
+ verifyResult: null,
66
+ head: null,
67
+ }),
68
+ ];
69
+ }
70
+
71
+ function writeSyntheticWal() {
72
+ fs.mkdirSync(outcomeDirectory, { recursive: true });
73
+ const lines = [];
74
+ let sequence = 1;
75
+ for (let i = 0; i < unrelatedOutcomes; i++) {
76
+ const id = `2026-01-01-${String(sequence++).padStart(6, '0')}`;
77
+ lines.push(...outcomeEvents(id, `unrelated work ${i}`, 'main', i % 1440));
78
+ }
79
+ lines.push(
80
+ event('lane_add', {
81
+ id: 'lane:focus',
82
+ lane: 'focus',
83
+ description: 'Benchmark focus lane',
84
+ ts: '2026-01-02T00:00:00.000Z',
85
+ })
86
+ );
87
+ for (let i = 0; i < laneOutcomes; i++) {
88
+ const id = `2026-01-02-${String(sequence++).padStart(6, '0')}`;
89
+ lines.push(...outcomeEvents(id, `focus work ${i}`, 'focus', i));
90
+ }
91
+ fs.writeFileSync(wal, `${lines.map(JSON.stringify).join('\n')}\n`);
92
+ fs.writeFileSync(path.join(outcomeDirectory, '.current-lane'), 'focus\n');
93
+ }
94
+
95
+ function distribution(values) {
96
+ const sorted = [...values].sort((left, right) => left - right);
97
+ return {
98
+ p50: Number(sorted[Math.floor(sorted.length * 0.5)].toFixed(2)),
99
+ p95: Number(sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))].toFixed(2)),
100
+ };
101
+ }
102
+
103
+ function measure(action, count = samples) {
104
+ const durations = [];
105
+ let value;
106
+ for (let i = 0; i < count; i++) {
107
+ const started = performance.now();
108
+ value = action();
109
+ durations.push(performance.now() - started);
110
+ }
111
+ return { ...distribution(durations), value };
112
+ }
113
+
114
+ try {
115
+ writeSyntheticWal();
116
+ const cold = measure(() => runCli(['log', '--last', '3']), 1);
117
+ const hot = measure(() => runCli(['log', '--last', '3']));
118
+ const fullFold = measure(
119
+ () =>
120
+ runCli(['log', '--last', '3'], {
121
+ _DRIFTSEAL_TEST_DISABLE_OUTCOME_INDEX: '1',
122
+ }),
123
+ quick ? 1 : 3
124
+ );
125
+ assert.equal(hot.value, fullFold.value);
126
+ assert.match(hot.value, new RegExp(`focus work ${laneOutcomes - 1}`));
127
+ assert.doesNotMatch(hot.value, /unrelated work/);
128
+
129
+ const index = openOutcomeIndex(indexFile, { readOnly: true });
130
+ const direct = measure(() => index.queryRecent('focus', 3));
131
+ const queryPlan = index.explainRecent().map((row) => row.detail);
132
+ index.close();
133
+ assert.equal(direct.value.length, 3);
134
+ assert.ok(
135
+ queryPlan.some((detail) => /outcomes_lane_visible_ordinal/.test(detail)),
136
+ `expected recent query index in plan: ${queryPlan.join('; ')}`
137
+ );
138
+
139
+ const nextId = `2026-01-03-${String(unrelatedOutcomes + laneOutcomes + 1).padStart(6, '0')}`;
140
+ fs.appendFileSync(
141
+ wal,
142
+ `${outcomeEvents(nextId, 'incremental focus work', 'focus', 0)
143
+ .map(JSON.stringify)
144
+ .join('\n')}\n`
145
+ );
146
+ const incremental = measure(() => runCli(['log', '--last', '3']), 1);
147
+ assert.match(incremental.value, /incremental focus work/);
148
+
149
+ const startup = measure(() =>
150
+ execFileSync(process.execPath, ['-e', ''], {
151
+ cwd: root,
152
+ stdio: 'ignore',
153
+ })
154
+ );
155
+ const stats = {
156
+ mode: large ? 'large' : quick ? 'quick' : 'default',
157
+ node: process.version,
158
+ platform: `${process.platform}-${process.arch}`,
159
+ outcomes: {
160
+ unrelated: unrelatedOutcomes,
161
+ currentLane: laneOutcomes,
162
+ },
163
+ samples,
164
+ bytes: {
165
+ wal: fs.statSync(wal).size,
166
+ sqlite: fs.statSync(indexFile).size,
167
+ },
168
+ milliseconds: {
169
+ processStartup: { p50: startup.p50, p95: startup.p95 },
170
+ coldBuildAndRecent: { p50: cold.p50, p95: cold.p95 },
171
+ hotRecentCli: { p50: hot.p50, p95: hot.p95 },
172
+ hotRecentQuery: { p50: direct.p50, p95: direct.p95 },
173
+ fullFoldRecent: { p50: fullFold.p50, p95: fullFold.p95 },
174
+ incrementalAndRecent: { p50: incremental.p50, p95: incremental.p95 },
175
+ },
176
+ queryPlan,
177
+ };
178
+ process.stdout.write(`${JSON.stringify(stats, null, 2)}\n`);
179
+ } finally {
180
+ fs.rmSync(root, { recursive: true, force: true });
181
+ }
@@ -94,6 +94,7 @@ function registerTools(server, api, z) {
94
94
  const outcomeRecord = z.object({
95
95
  id: z.string(),
96
96
  outcome: z.string(),
97
+ lane: z.string().optional(),
97
98
  extensions: z.array(extensionRecord),
98
99
  acceptance: z.array(z.string()),
99
100
  verify: z.string().nullable(),
@@ -281,10 +282,14 @@ function registerTools(server, api, z) {
281
282
  {
282
283
  title: 'List DriftSeal outcome history',
283
284
  description:
284
- 'Review recent or complete DriftSeal outcome history. Reclaimed records are hidden unless includeReclaimed is set.',
285
+ 'Review recent or complete DriftSeal outcome history. Defaults to the current lane. Reclaimed records are hidden unless includeReclaimed is set.',
285
286
  inputSchema: {
286
287
  last: z.number().int().positive().max(100).optional(),
287
288
  includeReclaimed: z.boolean().default(false),
289
+ allLanes: z
290
+ .boolean()
291
+ .default(false)
292
+ .describe('Show outcomes from every lane instead of the current lane.'),
288
293
  },
289
294
  outputSchema: {
290
295
  root: z.string(),
@@ -295,7 +300,11 @@ function registerTools(server, api, z) {
295
300
  },
296
301
  async (input) =>
297
302
  guarded(() => {
298
- const outcomes = api.log({ last: input.last, all: input.includeReclaimed });
303
+ const outcomes = api.log({
304
+ last: input.last,
305
+ all: input.includeReclaimed,
306
+ allLanes: input.allLanes,
307
+ });
299
308
  const readOnly = api.readOnly;
300
309
  const summary = `Found ${outcomes.length} DriftSeal outcome records.`;
301
310
  return success(
@@ -305,6 +314,103 @@ function registerTools(server, api, z) {
305
314
  })
306
315
  );
307
316
 
317
+ const laneRecord = z.object({
318
+ name: z.string(),
319
+ description: z.string().nullable(),
320
+ addedAt: z.string().nullable(),
321
+ inferred: z.boolean().optional(),
322
+ visible: z.number().int().nonnegative(),
323
+ count: z.number().int().nonnegative(),
324
+ current: z.boolean().optional(),
325
+ });
326
+
327
+ server.registerTool(
328
+ 'driftseal_lane',
329
+ {
330
+ title: 'Show DriftSeal lanes',
331
+ description:
332
+ 'List named outcome lanes and the current lane. Re-anchoring and log history follow the current lane.',
333
+ inputSchema: {},
334
+ outputSchema: {
335
+ root: z.string(),
336
+ current: z.string(),
337
+ missingCurrentLane: z.string().nullable().optional(),
338
+ lanes: z.array(laneRecord),
339
+ total: z.number().int().nonnegative(),
340
+ readOnly: z.boolean().optional(),
341
+ },
342
+ annotations: readOnly,
343
+ },
344
+ async () =>
345
+ guarded(() => {
346
+ const snapshot = api.lane();
347
+ const readOnly = api.readOnly;
348
+ return success(
349
+ { root: api.root, ...snapshot, ...(readOnly ? { readOnly: true } : {}) },
350
+ `Current DriftSeal lane is ${snapshot.current}.`
351
+ );
352
+ })
353
+ );
354
+
355
+ server.registerTool(
356
+ 'driftseal_lane_add',
357
+ {
358
+ title: 'Add a DriftSeal lane',
359
+ description:
360
+ 'Create a named lane for a long-lived capability. The default lane main always exists.',
361
+ inputSchema: {
362
+ name: nonEmpty.describe('Lane name: a lowercase letter, then letters, digits, or hyphens.'),
363
+ description: nonEmpty.optional(),
364
+ },
365
+ outputSchema: { root: z.string(), name: z.string(), description: z.string().nullable() },
366
+ annotations: localWrite,
367
+ },
368
+ async (input) =>
369
+ guarded(() => {
370
+ const lane = api.laneAdd({ name: input.name, description: input.description });
371
+ return success({ root: api.root, ...lane }, `Added DriftSeal lane ${lane.name}.`);
372
+ })
373
+ );
374
+
375
+ server.registerTool(
376
+ 'driftseal_lane_switch',
377
+ {
378
+ title: 'Switch the current DriftSeal lane',
379
+ description:
380
+ 'Move this worktree onto an existing lane. Refused while an outcome is open.',
381
+ inputSchema: { name: nonEmpty },
382
+ outputSchema: { root: z.string(), current: z.string() },
383
+ annotations: localWrite,
384
+ },
385
+ async (input) =>
386
+ guarded(() => {
387
+ const result = api.laneSwitch({ name: input.name });
388
+ return success({ root: api.root, ...result }, `Switched to DriftSeal lane ${result.current}.`);
389
+ })
390
+ );
391
+
392
+ server.registerTool(
393
+ 'driftseal_lane_assign',
394
+ {
395
+ title: 'Assign a closed outcome to a lane',
396
+ description: 'Move a closed outcome onto an existing lane with an append-only assign event.',
397
+ inputSchema: {
398
+ id: nonEmpty.describe('Closed outcome id.'),
399
+ lane: nonEmpty,
400
+ },
401
+ outputSchema: { root: z.string(), outcome: outcomeRecord },
402
+ annotations: localWrite,
403
+ },
404
+ async (input) =>
405
+ guarded(() => {
406
+ const outcome = api.laneAssign({ id: input.id, lane: input.lane });
407
+ return success(
408
+ { root: api.root, outcome },
409
+ `Assigned outcome ${outcome.id} to lane ${outcome.lane}.`
410
+ );
411
+ })
412
+ );
413
+
308
414
  server.registerTool(
309
415
  'driftseal_absorb',
310
416
  {
@@ -568,9 +674,16 @@ function registerResources(server, api) {
568
674
  'recent-outcomes',
569
675
  'driftseal://outcomes/recent',
570
676
  'Recent DriftSeal outcomes',
571
- 'The ten most recent outcome records for the fixed repository.',
677
+ 'The ten most recent outcome records on the current lane.',
572
678
  () => ({ root: api.root, outcomes: api.log({ last: 10 }) })
573
679
  );
680
+ registerJson(
681
+ 'outcome-lanes',
682
+ 'driftseal://lanes',
683
+ 'DriftSeal outcome lanes',
684
+ 'Named lanes that partition outcome history, including the current lane.',
685
+ () => ({ root: api.root, ...api.lane() })
686
+ );
574
687
  registerJson(
575
688
  'decision-catalog',
576
689
  'driftseal://madr',
@@ -592,7 +705,7 @@ async function createServer({ root }) {
592
705
  { name: SERVER_NAME, version: SERVER_VERSION },
593
706
  {
594
707
  instructions:
595
- 'Use driftseal_status before durable project changes or after context loss. Open one coherent outcome with driftseal_begin and append same-outcome steps with driftseal_extend. Reconcile linked decisions after the final extension, inspect the cumulative verifier, use driftseal_verify for fresh contract-bound evidence, and close honestly with driftseal_end.',
708
+ 'Use driftseal_status before durable project changes or after context loss. Open one coherent outcome with driftseal_begin and append same-outcome steps with driftseal_extend. Reconcile linked decisions after the final extension, inspect the cumulative verifier, use driftseal_verify for fresh contract-bound evidence, and close honestly with driftseal_end. Named lanes isolate orthogonal capability history; driftseal_log follows the current lane.',
596
709
  }
597
710
  );
598
711
  registerTools(server, api, z);