driftseal 3.0.0 → 3.1.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 +32 -20
- package/README.zh-CN.md +26 -15
- package/bin/driftseal.js +147 -74
- package/package.json +1 -1
- package/skills/use-driftseal/SKILL.md +6 -5
package/README.md
CHANGED
|
@@ -66,8 +66,9 @@ fresh clone because Git config is local to each clone.
|
|
|
66
66
|
|
|
67
67
|
Use `driftseal init --lang <BCP-47-tag>` to choose the prose language stored in
|
|
68
68
|
outcome and MADR records. Use `--local-log` only when `.seal/` should remain
|
|
69
|
-
untracked; DriftSeal reports tracked state but does not edit
|
|
70
|
-
Git index.
|
|
69
|
+
untracked; DriftSeal reports tracked state but does not edit the repository-root
|
|
70
|
+
`.gitignore` or the Git index. When the seal sits inside a Git worktree, `init`
|
|
71
|
+
may still write `.seal/outcomes/.gitignore` so derived sidecars stay untracked.
|
|
71
72
|
|
|
72
73
|
## Core workflow
|
|
73
74
|
|
|
@@ -156,13 +157,13 @@ has, `status`, `log`, and `lane` fall back to `main` with a warning; `begin`
|
|
|
156
157
|
still refuses until you `lane switch main` or add the lane. `log --last N` can
|
|
157
158
|
return more than N records when an open outcome sits on another lane.
|
|
158
159
|
|
|
159
|
-
A derived SQLite outcome index stores folded records
|
|
160
|
-
|
|
161
|
-
canonical history. Incremental sync follows
|
|
162
|
-
`indexedLines`. File identity makes unchanged hot reads
|
|
163
|
-
full checksum of the previously indexed WAL prefix
|
|
164
|
-
catch-up. A source rewrite, incompatible schema,
|
|
165
|
-
SQLite read error triggers a full rebuild.
|
|
160
|
+
A derived SQLite outcome index stores folded records beside the WAL
|
|
161
|
+
(`outcomes/.outcome-index.sqlite`). It is a disposable read model;
|
|
162
|
+
`events.jsonl` remains the only canonical history. Incremental sync follows
|
|
163
|
+
`indexedThrough` and `indexedLines`. File identity makes unchanged hot reads
|
|
164
|
+
constant-time, while a full checksum of the previously indexed WAL prefix
|
|
165
|
+
validates every incremental catch-up. A source rewrite, incompatible schema,
|
|
166
|
+
malformed indexed row, or SQLite read error triggers a full rebuild.
|
|
166
167
|
|
|
167
168
|
`log --last N` uses a `(lane, reclaimed, ordinal)` SQLite index and reads only
|
|
168
169
|
the selected outcome rows plus open outcomes from other lanes. Parked events
|
|
@@ -171,9 +172,10 @@ identities, so an open outcome does not force a committed-WAL scan. When a
|
|
|
171
172
|
lock-free read sees a missing or stale database, DriftSeal folds the canonical
|
|
172
173
|
WAL in memory instead of serving stale index data. Stored WAL byte ranges remain
|
|
173
174
|
available for future projections but are not required for recent-log lookup.
|
|
174
|
-
The database is reconstructable and is not committed with the log.
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
The database is reconstructable and is not committed with the log. Sidecars sit
|
|
176
|
+
next to `events.jsonl`; when that directory is inside a Git worktree, they are
|
|
177
|
+
listed in its `.gitignore`. Keeping the file in the workspace avoids agent
|
|
178
|
+
sandboxes that deny writes under `.git/`.
|
|
177
179
|
|
|
178
180
|
## Decisions and MADR
|
|
179
181
|
|
|
@@ -292,9 +294,11 @@ migrated state.
|
|
|
292
294
|
|
|
293
295
|
## Git and merge behavior
|
|
294
296
|
|
|
295
|
-
In a Git
|
|
296
|
-
|
|
297
|
-
|
|
297
|
+
In a default Git-repository seal, `begin` parks the open outcome beside the WAL
|
|
298
|
+
(`.seal/outcomes/.in-progress.jsonl`, gitignored) so it does not dirty the
|
|
299
|
+
tracked log. Custom `$DRIFTSEAL_HOME` seals write that open outcome directly to
|
|
300
|
+
`events.jsonl`. `end` flushes a parked lineage to `.seal/outcomes/events.jsonl`.
|
|
301
|
+
The event log is append-only during normal work.
|
|
298
302
|
|
|
299
303
|
After a merge collision, run:
|
|
300
304
|
|
|
@@ -341,11 +345,19 @@ MADR tools, and the three migration tools. Resources are:
|
|
|
341
345
|
DriftSeal; use `reclaim`, `unreclaim`, `lane`, and `absorb` instead of manual edits.
|
|
342
346
|
- `.seal/madr/` stores numbered MADR documents.
|
|
343
347
|
- `$DRIFTSEAL_HOME` replaces the `.seal` root.
|
|
344
|
-
- The
|
|
345
|
-
|
|
346
|
-
`
|
|
347
|
-
|
|
348
|
-
|
|
348
|
+
- The derived SQLite outcome index and the current-lane pointer sit next to the
|
|
349
|
+
WAL (`outcomes/.outcome-index.sqlite`, `outcomes/.current-lane`). The index is
|
|
350
|
+
reconstructable from `events.jsonl`; a missing or stale current-lane pointer
|
|
351
|
+
falls back to `main`. A default repository seal also parks the open outcome and
|
|
352
|
+
local verification provenance beside the WAL
|
|
353
|
+
(`outcomes/.in-progress.jsonl`, `outcomes/.driftseal-local-outcome.json`). Those
|
|
354
|
+
two files are not reconstructable: deleting the park discards the still-open
|
|
355
|
+
outcome, and deleting provenance changes verifier trust. Custom
|
|
356
|
+
`$DRIFTSEAL_HOME` seals write open outcomes directly to the WAL and still keep
|
|
357
|
+
provenance beside that log. When the outcomes directory is inside a Git
|
|
358
|
+
worktree, the sidecars are gitignored. A default repository seal keeps one
|
|
359
|
+
ignored copy per worktree; a shared `$DRIFTSEAL_HOME` shares the files that
|
|
360
|
+
exist there.
|
|
349
361
|
- Advisory hooks remind agents about lifecycle state but never broaden the
|
|
350
362
|
repository's `AGENTS.md` policy.
|
|
351
363
|
|
package/README.zh-CN.md
CHANGED
|
@@ -60,7 +60,8 @@ attribute,并配置本地 Git merge driver。Git config 不会随 clone 传播
|
|
|
60
60
|
|
|
61
61
|
`driftseal init --lang <BCP-47-tag>` 用来指定 outcome 与 MADR 正文的语言。
|
|
62
62
|
`--local-log` 会让 `.seal/` 保持本地、不被跟踪;DriftSeal 只报告当前 tracked 状态,
|
|
63
|
-
|
|
63
|
+
不会替你修改仓库根目录的 `.gitignore` 或 Git index。seal 在 Git worktree 内时,
|
|
64
|
+
`init` 仍可能写入 `.seal/outcomes/.gitignore`,用来忽略派生 sidecar。
|
|
64
65
|
|
|
65
66
|
## 基本工作流
|
|
66
67
|
|
|
@@ -141,19 +142,21 @@ lane 不能改名或删除。`lane add` 打错的名字会一直出现在 `drift
|
|
|
141
142
|
直到 `lane switch main` 或把该 lane 加回来。`log --last N` 在 open outcome 属于
|
|
142
143
|
别的 lane 时,返回条数可以多于 N。
|
|
143
144
|
|
|
144
|
-
派生的 SQLite outcome index 放在
|
|
145
|
-
|
|
146
|
-
`indexedThrough` 和 `indexedLines`。未变化的 hot read 用 file
|
|
147
|
-
|
|
148
|
-
不兼容、indexed row 损坏或 SQLite read 失败时,DriftSeal
|
|
145
|
+
派生的 SQLite outcome index 放在 WAL 旁边(`outcomes/.outcome-index.sqlite`)。
|
|
146
|
+
它只是可以随时删除重建的 read model;`events.jsonl` 仍是唯一 canonical history。
|
|
147
|
+
增量同步跟随 `indexedThrough` 和 `indexedLines`。未变化的 hot read 用 file
|
|
148
|
+
identity 做常量时间校验;增量追赶前会校验此前全部 WAL prefix 的 checksum。
|
|
149
|
+
source 被改写、schema 不兼容、indexed row 损坏或 SQLite read 失败时,DriftSeal
|
|
150
|
+
会全量重建。
|
|
149
151
|
|
|
150
152
|
`log --last N` 使用 `(lane, reclaimed, ordinal)` SQLite index,只读取命中的 outcome
|
|
151
153
|
row,并补上其他 lane 的 open outcome。parked event 会依据 index 中保存的 committed
|
|
152
154
|
event identity 做定向 overlay,不再因此重扫 committed WAL。无锁读取遇到缺失或 stale
|
|
153
155
|
database 时,会在内存中 fold canonical WAL,绝不会返回 stale index。保存的 WAL byte
|
|
154
156
|
range 留给后续 projection 使用,但 recent-log lookup 不依赖它。database 可以重建,
|
|
155
|
-
不会随 log
|
|
156
|
-
|
|
157
|
+
不会随 log 一起提交。sidecar 放在 `events.jsonl` 旁边;该目录在 Git worktree
|
|
158
|
+
内时,由目录里的 `.gitignore` 忽略。文件留在工作树里,避免被默认禁止写入
|
|
159
|
+
`.git/` 的 agent sandbox 拦住。
|
|
157
160
|
|
|
158
161
|
## Decision 与 MADR
|
|
159
162
|
|
|
@@ -262,9 +265,11 @@ tools 可以立刻看到 migration 后的状态。
|
|
|
262
265
|
|
|
263
266
|
## Git 与 merge
|
|
264
267
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
+
在默认 Git-repository seal 中,`begin` 会把 open outcome park 到 WAL 旁边
|
|
269
|
+
(`.seal/outcomes/.in-progress.jsonl`,已被 gitignore),避免弄脏 tracked log。
|
|
270
|
+
自定义 `$DRIFTSEAL_HOME` 会把该 open outcome 直接写入 `events.jsonl`。`end`
|
|
271
|
+
再把 parked lineage flush 到 `.seal/outcomes/events.jsonl`。正常工作期间 event
|
|
272
|
+
log 保持 append-only。
|
|
268
273
|
|
|
269
274
|
发生 merge collision 后执行:
|
|
270
275
|
|
|
@@ -310,10 +315,16 @@ resources 为:
|
|
|
310
315
|
`absorb`,不要手改。
|
|
311
316
|
- `.seal/madr/` 保存编号化 MADR。
|
|
312
317
|
- `$DRIFTSEAL_HOME` 替换整个 `.seal` root。
|
|
313
|
-
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
318
|
+
- 派生 SQLite outcome index 和当前 lane 指针放在 WAL 旁边
|
|
319
|
+
(`outcomes/.outcome-index.sqlite`、`outcomes/.current-lane`)。index 可以从
|
|
320
|
+
`events.jsonl` 重建;缺失或过期的 current-lane 会回退到 `main`。默认 repo
|
|
321
|
+
seal 还会把 open outcome 和本地 verification provenance park 在 WAL 旁边
|
|
322
|
+
(`outcomes/.in-progress.jsonl`、`outcomes/.driftseal-local-outcome.json`)。
|
|
323
|
+
这两份文件不能重建:删掉 park 会丢掉尚未 flush 的 open outcome,删掉
|
|
324
|
+
provenance 会改变 verifier 信任。自定义 `$DRIFTSEAL_HOME` 会把 open outcome
|
|
325
|
+
直接写入 WAL,provenance 仍放在该 log 旁边。outcomes 目录在 Git worktree 内时,
|
|
326
|
+
这些 sidecar 会被 gitignore。默认 repo seal 下每个 worktree 各自一份;共享的
|
|
327
|
+
`$DRIFTSEAL_HOME` 会共用那里实际存在的文件。
|
|
317
328
|
- advisory hook 只提示 lifecycle 状态,不会扩大 repo 中 `AGENTS.md` 的政策边界。
|
|
318
329
|
|
|
319
330
|
DriftSeal 不会替你判断 verification command 是否安全,也不会判断测试本身是否充分。
|
package/bin/driftseal.js
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Seal root: $DRIFTSEAL_HOME, or .seal in cwd.
|
|
22
22
|
* Outcome log: <seal-root>/outcomes/events.jsonl.
|
|
23
|
-
* In a Git
|
|
23
|
+
* In a default Git-repository seal, an open outcome is parked beside the WAL until end.
|
|
24
|
+
* Custom $DRIFTSEAL_HOME seals write open outcomes directly to the WAL.
|
|
24
25
|
* MADR records: <seal-root>/madr/.
|
|
25
26
|
*/
|
|
26
27
|
|
|
@@ -59,10 +60,22 @@ const PROTOCOL_VERSION = '2.1';
|
|
|
59
60
|
const DEFAULT_LOG_LANGUAGE = 'en';
|
|
60
61
|
const DEFAULT_LANE = 'main';
|
|
61
62
|
const LANE_NAME_RE = /^[a-z][a-z0-9-]{0,62}$/;
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
const IN_PROGRESS_SIDECAR = '.in-progress.jsonl';
|
|
64
|
+
const WORKSPACE_SIDECAR_IGNORE_NAMES = Object.freeze([
|
|
65
|
+
'.current-lane',
|
|
66
|
+
'.lane-index.json',
|
|
67
|
+
'.in-progress.jsonl',
|
|
68
|
+
'.driftseal-local-outcome.json',
|
|
69
|
+
'.outcome-index.sqlite',
|
|
70
|
+
'.outcome-index.sqlite-journal',
|
|
71
|
+
'.outcome-index.sqlite-wal',
|
|
72
|
+
'.outcome-index.sqlite-shm',
|
|
73
|
+
'..outcome-index.sqlite.*.tmp',
|
|
74
|
+
'..outcome-index.sqlite.*.tmp-*',
|
|
75
|
+
'..current-lane.*.tmp',
|
|
76
|
+
'..in-progress.jsonl.*.tmp',
|
|
77
|
+
'..driftseal-local-outcome.json.*.tmp',
|
|
78
|
+
]);
|
|
66
79
|
const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
67
80
|
const LOCK_INIT_STALE_MS = 5 * 1000;
|
|
68
81
|
const READ_ONLY_NOTICE = '(read-only: another mutation holds the lock; tail repair skipped)';
|
|
@@ -574,13 +587,6 @@ function gitWorktreeRoot(cwd = process.cwd()) {
|
|
|
574
587
|
return gitCapture(['rev-parse', '--show-toplevel'], cwd);
|
|
575
588
|
}
|
|
576
589
|
|
|
577
|
-
function worktreeInProgressFile(cwd = process.cwd()) {
|
|
578
|
-
if (!isGitWorkTree(cwd)) return null;
|
|
579
|
-
const gitPath = gitCapture(['rev-parse', '--git-path', IN_PROGRESS_GIT_PATH], cwd);
|
|
580
|
-
if (!gitPath) return null;
|
|
581
|
-
return path.resolve(cwd, gitPath);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
590
|
function isParkableOutcomeLog() {
|
|
585
591
|
if (process.env.DRIFTSEAL_HOME) return false;
|
|
586
592
|
const root = gitWorktreeRoot();
|
|
@@ -588,30 +594,45 @@ function isParkableOutcomeLog() {
|
|
|
588
594
|
return path.resolve(logFile()) === path.resolve(root, '.seal', 'outcomes', 'events.jsonl');
|
|
589
595
|
}
|
|
590
596
|
|
|
597
|
+
function inProgressFileForLog(file) {
|
|
598
|
+
return path.join(path.dirname(file), IN_PROGRESS_SIDECAR);
|
|
599
|
+
}
|
|
600
|
+
|
|
591
601
|
function inProgressFile() {
|
|
592
602
|
if (!isParkableOutcomeLog()) return null;
|
|
593
|
-
return
|
|
603
|
+
return inProgressFileForLog(logFile());
|
|
594
604
|
}
|
|
595
605
|
|
|
596
|
-
function
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
606
|
+
function existingInProgressFileForLog(file) {
|
|
607
|
+
return inProgressFileForLog(file);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function existingInProgressFile() {
|
|
611
|
+
if (!isParkableOutcomeLog()) return null;
|
|
612
|
+
return existingInProgressFileForLog(logFile());
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function adoptInProgressFile() {
|
|
616
|
+
const current = inProgressFile();
|
|
617
|
+
if (!current) return null;
|
|
618
|
+
ensureDerivedLaneSidecarIgnore();
|
|
619
|
+
ensureDirectoryDurable(path.dirname(current));
|
|
620
|
+
return current;
|
|
601
621
|
}
|
|
602
622
|
|
|
603
623
|
function currentLaneFile() {
|
|
604
|
-
|
|
624
|
+
// Keep the worktree-local pointer beside the WAL so agent sandboxes that
|
|
625
|
+
// deny .git writes can still switch lanes.
|
|
605
626
|
return path.join(logDir(), '.current-lane');
|
|
606
627
|
}
|
|
607
628
|
|
|
608
629
|
function laneIndexFile() {
|
|
609
|
-
|
|
630
|
+
// Keep the disposable index beside the WAL so agent sandboxes that deny
|
|
631
|
+
// .git writes can still rebuild it inside the workspace.
|
|
610
632
|
return path.join(logDir(), '.outcome-index.sqlite');
|
|
611
633
|
}
|
|
612
634
|
|
|
613
635
|
function legacyLaneIndexFile() {
|
|
614
|
-
if (isParkableOutcomeLog()) return worktreeMetadataFile(LEGACY_LANE_INDEX_GIT_PATH);
|
|
615
636
|
return path.join(logDir(), '.lane-index.json');
|
|
616
637
|
}
|
|
617
638
|
|
|
@@ -619,14 +640,17 @@ function emptyLaneCatalog() {
|
|
|
619
640
|
return outcomeFoldEngine.emptyLaneCatalog();
|
|
620
641
|
}
|
|
621
642
|
|
|
622
|
-
function
|
|
623
|
-
|
|
624
|
-
if (!file || !fs.existsSync(file)) return DEFAULT_LANE;
|
|
643
|
+
function readLaneNameFromFile(file) {
|
|
644
|
+
if (!file || !fs.existsSync(file)) return null;
|
|
625
645
|
const name = fs.readFileSync(file, 'utf8').trim();
|
|
626
|
-
if (!name) return
|
|
646
|
+
if (!name) return null;
|
|
627
647
|
return normalizeLaneName(name);
|
|
628
648
|
}
|
|
629
649
|
|
|
650
|
+
function readCurrentLaneName() {
|
|
651
|
+
return readLaneNameFromFile(currentLaneFile()) || DEFAULT_LANE;
|
|
652
|
+
}
|
|
653
|
+
|
|
630
654
|
function writeCurrentLaneName(name, { readOnly = false } = {}) {
|
|
631
655
|
if (readOnly) return;
|
|
632
656
|
const file = currentLaneFile();
|
|
@@ -636,29 +660,59 @@ function writeCurrentLaneName(name, { readOnly = false } = {}) {
|
|
|
636
660
|
atomicWriteFile(file, `${name}\n`, 0o600);
|
|
637
661
|
}
|
|
638
662
|
|
|
663
|
+
function existingAncestor(dir) {
|
|
664
|
+
let current = path.resolve(dir);
|
|
665
|
+
while (!fs.existsSync(current)) {
|
|
666
|
+
const parent = path.dirname(current);
|
|
667
|
+
if (parent === current) return current;
|
|
668
|
+
current = parent;
|
|
669
|
+
}
|
|
670
|
+
return current;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function listedIgnoreNames(content) {
|
|
674
|
+
return new Set(
|
|
675
|
+
content
|
|
676
|
+
.split(/\r?\n/)
|
|
677
|
+
.map((line) => line.trim())
|
|
678
|
+
.filter((line) => line.length > 0)
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function workspaceSidecarIgnoreFile() {
|
|
683
|
+
return path.join(logDir(), '.gitignore');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function gitContainedOutcomeLog() {
|
|
687
|
+
return isGitWorkTree(existingAncestor(logDir()));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function workspaceSidecarIgnoreComplete() {
|
|
691
|
+
const ignoreFile = workspaceSidecarIgnoreFile();
|
|
692
|
+
if (!fs.existsSync(ignoreFile)) return false;
|
|
693
|
+
const listed = listedIgnoreNames(fs.readFileSync(ignoreFile, 'utf8'));
|
|
694
|
+
return WORKSPACE_SIDECAR_IGNORE_NAMES.every((name) => listed.has(name));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function canPersistDerivedOutcomeIndex() {
|
|
698
|
+
return !gitContainedOutcomeLog() || workspaceSidecarIgnoreComplete();
|
|
699
|
+
}
|
|
700
|
+
|
|
639
701
|
function ensureDerivedLaneSidecarIgnore() {
|
|
640
|
-
if (
|
|
641
|
-
|
|
642
|
-
const ignoreFile = path.join(logDir(), '.gitignore');
|
|
702
|
+
if (!gitContainedOutcomeLog()) return { changed: false, target: null };
|
|
703
|
+
const ignoreFile = workspaceSidecarIgnoreFile();
|
|
643
704
|
let current = fs.existsSync(ignoreFile) ? fs.readFileSync(ignoreFile, 'utf8') : '';
|
|
705
|
+
const listed = listedIgnoreNames(current);
|
|
644
706
|
let next = current;
|
|
645
|
-
for (const name of
|
|
646
|
-
|
|
647
|
-
'.lane-index.json',
|
|
648
|
-
'.outcome-index.sqlite',
|
|
649
|
-
'.outcome-index.sqlite-journal',
|
|
650
|
-
'.outcome-index.sqlite-wal',
|
|
651
|
-
'.outcome-index.sqlite-shm',
|
|
652
|
-
'..outcome-index.sqlite.*.tmp',
|
|
653
|
-
]) {
|
|
654
|
-
const present = next.split(/\r?\n/).some((line) => line.trim() === name);
|
|
655
|
-
if (present) continue;
|
|
707
|
+
for (const name of WORKSPACE_SIDECAR_IGNORE_NAMES) {
|
|
708
|
+
if (listed.has(name)) continue;
|
|
656
709
|
if (next && !next.endsWith('\n')) next += '\n';
|
|
657
710
|
next += `${name}\n`;
|
|
658
711
|
}
|
|
659
|
-
if (next === current) return;
|
|
712
|
+
if (next === current) return { changed: false, target: ignoreFile };
|
|
660
713
|
ensureDirectoryDurable(logDir());
|
|
661
714
|
atomicWriteFile(ignoreFile, next, 0o644);
|
|
715
|
+
return { changed: true, target: ignoreFile };
|
|
662
716
|
}
|
|
663
717
|
|
|
664
718
|
function hashFilePrefix(file, length) {
|
|
@@ -817,16 +871,15 @@ function replaceOutcomeIndexFile(temporary, target) {
|
|
|
817
871
|
}
|
|
818
872
|
|
|
819
873
|
function removeLegacyLaneIndex() {
|
|
820
|
-
|
|
821
|
-
if (legacy) fs.rmSync(legacy, { force: true });
|
|
874
|
+
fs.rmSync(legacyLaneIndexFile(), { force: true });
|
|
822
875
|
}
|
|
823
876
|
|
|
824
877
|
function rebuildCommittedOutcomeIndex({ repairTail = false } = {}) {
|
|
825
878
|
const wal = logFile();
|
|
826
879
|
const target = laneIndexFile();
|
|
827
880
|
if (!target) return null;
|
|
881
|
+
if (!canPersistDerivedOutcomeIndex()) return null;
|
|
828
882
|
ensureDirectoryDurable(path.dirname(target));
|
|
829
|
-
ensureDerivedLaneSidecarIgnore();
|
|
830
883
|
const temporary = temporaryIndexPath(target);
|
|
831
884
|
removeIndexFiles(temporary);
|
|
832
885
|
let index;
|
|
@@ -872,6 +925,7 @@ function syncCommittedLaneIndex({ repairTail = false, readOnly = false, forceFul
|
|
|
872
925
|
const wal = logFile();
|
|
873
926
|
const target = laneIndexFile();
|
|
874
927
|
if (!target) return null;
|
|
928
|
+
if (!readOnly && !canPersistDerivedOutcomeIndex()) return null;
|
|
875
929
|
if (readOnly) {
|
|
876
930
|
if (!fs.existsSync(target)) return null;
|
|
877
931
|
try {
|
|
@@ -993,7 +1047,7 @@ function recoverableOutcomeIndexError(error) {
|
|
|
993
1047
|
}
|
|
994
1048
|
|
|
995
1049
|
function loadOutcomeView({ repairTail = false, readOnly = false } = {}) {
|
|
996
|
-
const park =
|
|
1050
|
+
const park = existingInProgressFile();
|
|
997
1051
|
let index = syncCommittedLaneIndex({ repairTail, readOnly });
|
|
998
1052
|
if (!index) {
|
|
999
1053
|
const records = fold(readEvents({ repairTail, readOnly }));
|
|
@@ -1095,7 +1149,7 @@ function indexedLaneSummary(state, name) {
|
|
|
1095
1149
|
}
|
|
1096
1150
|
|
|
1097
1151
|
function tryLoadRecentOutcomeView(n, { includeReclaimed = false, repairTail = false, readOnly = false } = {}) {
|
|
1098
|
-
const park =
|
|
1152
|
+
const park = existingInProgressFile();
|
|
1099
1153
|
let index = syncCommittedLaneIndex({ repairTail, readOnly });
|
|
1100
1154
|
if (!index) return null;
|
|
1101
1155
|
const query = () => {
|
|
@@ -1150,7 +1204,7 @@ function readJsonlRecordsFromFile(file, { repairTail = false, readOnly = false }
|
|
|
1150
1204
|
if (
|
|
1151
1205
|
readOnly &&
|
|
1152
1206
|
process.env._DRIFTSEAL_TEST_UNLINK_PARK_BEFORE_READ === '1' &&
|
|
1153
|
-
path.basename(file) ===
|
|
1207
|
+
path.basename(file) === IN_PROGRESS_SIDECAR
|
|
1154
1208
|
) {
|
|
1155
1209
|
// Simulate a writer flushing and unlinking the park between the existence
|
|
1156
1210
|
// check and the read; the next attempt sees the park as absent.
|
|
@@ -1232,13 +1286,13 @@ function planIndexedInProgressOverlay(index, park, { repairTail = false, readOnl
|
|
|
1232
1286
|
}
|
|
1233
1287
|
|
|
1234
1288
|
function discardInProgressLog(park) {
|
|
1235
|
-
fs.
|
|
1289
|
+
fs.rmSync(park, { force: true });
|
|
1236
1290
|
fsyncDirectory(path.dirname(park));
|
|
1237
1291
|
}
|
|
1238
1292
|
|
|
1239
1293
|
function reconcileInProgressRecords(
|
|
1240
1294
|
committedEvents,
|
|
1241
|
-
{ repairTail = false, readOnly = false, park =
|
|
1295
|
+
{ repairTail = false, readOnly = false, park = existingInProgressFile() } = {}
|
|
1242
1296
|
) {
|
|
1243
1297
|
const plan = planInProgressOverlay(committedEvents, park, { repairTail, readOnly });
|
|
1244
1298
|
if (!plan) return [];
|
|
@@ -1260,7 +1314,7 @@ function readEventsSnapshot(file, { repairTail = false, readOnly = false } = {})
|
|
|
1260
1314
|
reconcileInProgressRecords(events, {
|
|
1261
1315
|
repairTail,
|
|
1262
1316
|
readOnly,
|
|
1263
|
-
park:
|
|
1317
|
+
park: existingInProgressFileForLog(file),
|
|
1264
1318
|
}).map((record) => record.event)
|
|
1265
1319
|
);
|
|
1266
1320
|
}
|
|
@@ -1389,7 +1443,7 @@ function appendEventTo(file, event) {
|
|
|
1389
1443
|
* Returns the outcome ids it had to remap.
|
|
1390
1444
|
*/
|
|
1391
1445
|
function flushInProgressLog() {
|
|
1392
|
-
const park =
|
|
1446
|
+
const park = adoptInProgressFile();
|
|
1393
1447
|
if (!park || !fs.existsSync(park)) return new Map();
|
|
1394
1448
|
const committedRecords = readJsonlRecordsFromFile(logFile());
|
|
1395
1449
|
const plan = planInProgressOverlay(
|
|
@@ -1419,7 +1473,8 @@ function parkedOpenOutcome(park) {
|
|
|
1419
1473
|
}
|
|
1420
1474
|
|
|
1421
1475
|
function appendEvent(event) {
|
|
1422
|
-
|
|
1476
|
+
ensureDerivedLaneSidecarIgnore();
|
|
1477
|
+
const park = adoptInProgressFile();
|
|
1423
1478
|
if (!park) {
|
|
1424
1479
|
const stored = appendEventTo(logFile(), event);
|
|
1425
1480
|
if (event.type === 'begin') writeLocalOutcomeProvenance(stored);
|
|
@@ -1434,7 +1489,7 @@ function appendEvent(event) {
|
|
|
1434
1489
|
if (event.type === 'begin') return appendEventTo(park, event);
|
|
1435
1490
|
if (!open || open.id !== event.id) return appendEventTo(logFile(), event);
|
|
1436
1491
|
if (event.type !== 'end') return appendEventTo(park, event);
|
|
1437
|
-
// Close in the tracked log, never in
|
|
1492
|
+
// Close in the tracked log, never only in the park: the parked records move first, so the
|
|
1438
1493
|
// closing record cannot end up somewhere a clone or a removed worktree would drop it.
|
|
1439
1494
|
const remapped = flushInProgressLog();
|
|
1440
1495
|
if (process.env._DRIFTSEAL_TEST_CRASH_AFTER_IN_PROGRESS_FLUSH === '1') {
|
|
@@ -1448,11 +1503,7 @@ function contentHash(content) {
|
|
|
1448
1503
|
}
|
|
1449
1504
|
|
|
1450
1505
|
function localOutcomeProvenanceFile() {
|
|
1451
|
-
|
|
1452
|
-
const key = contentHash(path.resolve(logFile())).slice(0, 16);
|
|
1453
|
-
if (!root) return path.join(logDir(), LOCAL_OUTCOME_PROVENANCE_FILE);
|
|
1454
|
-
const gitPath = gitCapture(['rev-parse', '--git-path', `driftseal-local-outcome-${key}.json`]);
|
|
1455
|
-
return gitPath ? path.resolve(process.cwd(), gitPath) : null;
|
|
1506
|
+
return path.join(logDir(), LOCAL_OUTCOME_PROVENANCE_FILE);
|
|
1456
1507
|
}
|
|
1457
1508
|
|
|
1458
1509
|
function localOutcomeLogIdentity() {
|
|
@@ -1471,6 +1522,7 @@ function localOutcomeProvenanceFingerprint({ id, ts, verify }) {
|
|
|
1471
1522
|
function writeLocalOutcomeProvenance(event) {
|
|
1472
1523
|
const file = localOutcomeProvenanceFile();
|
|
1473
1524
|
if (!file) return;
|
|
1525
|
+
ensureDerivedLaneSidecarIgnore();
|
|
1474
1526
|
ensureDirectoryDurable(path.dirname(file));
|
|
1475
1527
|
atomicWriteFile(
|
|
1476
1528
|
file,
|
|
@@ -1484,8 +1536,7 @@ function writeLocalOutcomeProvenance(event) {
|
|
|
1484
1536
|
);
|
|
1485
1537
|
}
|
|
1486
1538
|
|
|
1487
|
-
function
|
|
1488
|
-
const file = localOutcomeProvenanceFile();
|
|
1539
|
+
function parseLocalOutcomeProvenance(file) {
|
|
1489
1540
|
if (!file || !fs.existsSync(file)) return null;
|
|
1490
1541
|
try {
|
|
1491
1542
|
const provenance = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -1503,6 +1554,10 @@ function readLocalOutcomeProvenance() {
|
|
|
1503
1554
|
}
|
|
1504
1555
|
}
|
|
1505
1556
|
|
|
1557
|
+
function readLocalOutcomeProvenance() {
|
|
1558
|
+
return parseLocalOutcomeProvenance(localOutcomeProvenanceFile());
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1506
1561
|
function hasMatchingLocalOutcomeProvenance(outcome) {
|
|
1507
1562
|
const provenance = readLocalOutcomeProvenance();
|
|
1508
1563
|
return (
|
|
@@ -1519,11 +1574,13 @@ function hasMatchingLocalOutcomeProvenance(outcome) {
|
|
|
1519
1574
|
}
|
|
1520
1575
|
|
|
1521
1576
|
function clearLocalOutcomeProvenance(id) {
|
|
1522
|
-
const file = localOutcomeProvenanceFile();
|
|
1523
1577
|
const provenance = readLocalOutcomeProvenance();
|
|
1524
|
-
if (!
|
|
1525
|
-
|
|
1526
|
-
|
|
1578
|
+
if (!provenance || provenance.id !== id) return;
|
|
1579
|
+
const file = localOutcomeProvenanceFile();
|
|
1580
|
+
if (file && fs.existsSync(file)) {
|
|
1581
|
+
fs.unlinkSync(file);
|
|
1582
|
+
fsyncDirectory(path.dirname(file));
|
|
1583
|
+
}
|
|
1527
1584
|
}
|
|
1528
1585
|
|
|
1529
1586
|
function atomicWriteFile(target, content, createMode = 0o644) {
|
|
@@ -3362,6 +3419,8 @@ const SKILL_RELEASE_DIGESTS = new Set([
|
|
|
3362
3419
|
'42a0549dff21483c0508ea4a79658e7bf05cd98f8af4238c95dbd23cdcde7ee6', // 2.0.0 outcome workflow
|
|
3363
3420
|
'38e89060ff37ecdd663eae73a3e0c646d6bb220fc8232a5762356375d84ba69b', // 2.1.0 outcome lanes
|
|
3364
3421
|
'b4b2cc27ea71c5777b5eb9b67d861fbe1da43f31ab5d422d6a877e0cad592493', // 2.1.0 lane re-anchor recovery
|
|
3422
|
+
'6c49523c2f4a36ea1cd9d0b4e793ea3cdbd9d299174cf1f9a0a54f3d9a7cd331', // workspace park sidecar wording
|
|
3423
|
+
'678cbaa8380f75f7a45d634010b500e51a59b06ea759eaf23cff2980142662f0', // default-seal parking help wording
|
|
3365
3424
|
]);
|
|
3366
3425
|
|
|
3367
3426
|
function skillInstallUsage() {
|
|
@@ -3783,11 +3842,9 @@ function hookLogFile() {
|
|
|
3783
3842
|
const root = gitWorktreeRoot(current);
|
|
3784
3843
|
while (true) {
|
|
3785
3844
|
const candidate = path.join(current, '.seal', 'outcomes', 'events.jsonl');
|
|
3786
|
-
|
|
3787
|
-
if (
|
|
3788
|
-
|
|
3789
|
-
if (park && fs.existsSync(park)) return candidate;
|
|
3790
|
-
}
|
|
3845
|
+
const workspacePark = path.join(current, '.seal', 'outcomes', IN_PROGRESS_SIDECAR);
|
|
3846
|
+
if (fs.existsSync(candidate) || fs.existsSync(workspacePark)) return candidate;
|
|
3847
|
+
if (root && path.resolve(root) === current) return null;
|
|
3791
3848
|
const parent = path.dirname(current);
|
|
3792
3849
|
if (parent === current) return null;
|
|
3793
3850
|
current = parent;
|
|
@@ -4620,8 +4677,12 @@ function finishAbsorb({
|
|
|
4620
4677
|
allowConflict = false,
|
|
4621
4678
|
followupMessage = null,
|
|
4622
4679
|
}) {
|
|
4623
|
-
//
|
|
4624
|
-
const park = shouldAttachInProgress(outputFile)
|
|
4680
|
+
// A parked outcome is local even though the tracked log never saw it.
|
|
4681
|
+
const park = shouldAttachInProgress(outputFile)
|
|
4682
|
+
? dryRun
|
|
4683
|
+
? existingInProgressFileForLog(outputFile)
|
|
4684
|
+
: adoptInProgressFile() || existingInProgressFileForLog(outputFile)
|
|
4685
|
+
: null;
|
|
4625
4686
|
const plan = planInProgressOverlay(result.map((record) => record.event), park, {
|
|
4626
4687
|
repairTail: true,
|
|
4627
4688
|
});
|
|
@@ -4992,7 +5053,7 @@ function runMachineVerification({ allowTrackedCommand = false } = {}) {
|
|
|
4992
5053
|
fail(`outcome ${outcome.id} has no acceptance criteria; declare them with driftseal begin --accept`);
|
|
4993
5054
|
}
|
|
4994
5055
|
if (!outcome.verify) fail(`outcome ${outcome.id} has no verification command`);
|
|
4995
|
-
const park =
|
|
5056
|
+
const park = existingInProgressFile();
|
|
4996
5057
|
const parked = park ? parkedOpenOutcome(park) : null;
|
|
4997
5058
|
const locallyProvenanced = hasMatchingLocalOutcomeProvenance(outcome);
|
|
4998
5059
|
return {
|
|
@@ -6691,10 +6752,11 @@ const commands = {
|
|
|
6691
6752
|
} catch (err) {
|
|
6692
6753
|
printLine(`warning: could not configure git merge driver: ${err && err.message ? err.message : err}`);
|
|
6693
6754
|
}
|
|
6755
|
+
const indexIgnore = ensureDerivedLaneSidecarIgnore();
|
|
6694
6756
|
|
|
6695
6757
|
if (localLog) warnIfDefaultLogsTracked();
|
|
6696
6758
|
|
|
6697
|
-
if (updated === current && !attributes.changed && !driver.changed) {
|
|
6759
|
+
if (updated === current && !attributes.changed && !driver.changed && !indexIgnore.changed) {
|
|
6698
6760
|
printLine('AGENTS.md already contains the DriftSeal protocols; nothing to do');
|
|
6699
6761
|
return { changed: false, target };
|
|
6700
6762
|
}
|
|
@@ -6708,6 +6770,9 @@ const commands = {
|
|
|
6708
6770
|
if (driver.changed) {
|
|
6709
6771
|
printLine('Configured local git merge driver for DriftSeal outcome logs');
|
|
6710
6772
|
}
|
|
6773
|
+
if (indexIgnore.changed) {
|
|
6774
|
+
printLine(`Configured derived outcome-index ignore: ${indexIgnore.target}`);
|
|
6775
|
+
}
|
|
6711
6776
|
return { changed: true, target };
|
|
6712
6777
|
},
|
|
6713
6778
|
|
|
@@ -6785,7 +6850,7 @@ seal root: $DRIFTSEAL_HOME, or .seal in the current directory
|
|
|
6785
6850
|
outcome log: <seal-root>/outcomes/events.jsonl
|
|
6786
6851
|
MADR records: <seal-root>/madr/
|
|
6787
6852
|
$DRIFTSEAL_DECISION_HOME is a v1-only default for migration source detection; v2 runtime ignores it.
|
|
6788
|
-
In a Git
|
|
6853
|
+
In a default Git-repository seal, begin parks an open outcome beside the WAL until end, so merge does not need a log-only commit. Custom $DRIFTSEAL_HOME seals write that open outcome directly to events.jsonl.`);
|
|
6789
6854
|
return null;
|
|
6790
6855
|
},
|
|
6791
6856
|
|
|
@@ -7197,7 +7262,7 @@ function dispatch(argv) {
|
|
|
7197
7262
|
if (cmd === 'hook') {
|
|
7198
7263
|
// Hooks read the nearest ancestor log, not the cwd-relative one: lock
|
|
7199
7264
|
// the directory of the file the hook will actually read (the park a
|
|
7200
|
-
// writer flushes under that same lock lives
|
|
7265
|
+
// writer flushes under that same lock lives beside the WAL).
|
|
7201
7266
|
// With no ancestor log the hook prints nothing, so skip locking — this
|
|
7202
7267
|
// also avoids creating a spurious <cwd>/.intent-log.
|
|
7203
7268
|
const hookFile = hookLogFile();
|
|
@@ -7205,6 +7270,14 @@ function dispatch(argv) {
|
|
|
7205
7270
|
resources = [path.dirname(hookFile)];
|
|
7206
7271
|
} else if (legacyParkedIntent()) {
|
|
7207
7272
|
resources = [path.dirname(legacyIntentLogFile())];
|
|
7273
|
+
} else if (!fs.existsSync(logDir())) {
|
|
7274
|
+
// Do not mkdir a cwd-relative seal just to lock a read: status/log
|
|
7275
|
+
// from a subdirectory would otherwise plant packages/**/.seal.
|
|
7276
|
+
const data = fn(rest, { readOnly: true });
|
|
7277
|
+
return {
|
|
7278
|
+
data,
|
|
7279
|
+
exitCode: data && Number.isInteger(data.exitCode) ? data.exitCode : 0,
|
|
7280
|
+
};
|
|
7208
7281
|
} else {
|
|
7209
7282
|
resources = [logDir()];
|
|
7210
7283
|
}
|
package/package.json
CHANGED
|
@@ -47,13 +47,14 @@ If `status` or `log` fails with a duplicate id or with `multiple outcomes in
|
|
|
47
47
|
progress`, or the outcome log has conflict markers, run `driftseal absorb`
|
|
48
48
|
instead of editing `.seal/outcomes/events.jsonl`. When both sides still have an
|
|
49
49
|
open outcome, add `--abandon-theirs` or `--abandon-ours`; this works whether your
|
|
50
|
-
open outcome sits in the log or is parked
|
|
50
|
+
open outcome sits in the log or is parked beside the WAL. `driftseal init` also
|
|
51
51
|
configures the local git merge driver; clones need `init` again for that driver.
|
|
52
52
|
|
|
53
|
-
In a Git
|
|
54
|
-
can run with an outcome still in
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
In a default Git-repository seal, `begin` parks beside the WAL and does not dirty
|
|
54
|
+
the tracked outcome log, so `git merge` can run with an outcome still in
|
|
55
|
+
progress. Custom `$DRIFTSEAL_HOME` seals write that open outcome directly to
|
|
56
|
+
`events.jsonl`. `end` writes the closed record to `.seal/outcomes/events.jsonl`;
|
|
57
|
+
if it is interrupted, the outcome stays open there and `end` can be run again.
|
|
57
58
|
|
|
58
59
|
For a v1 repository, do not reinterpret or delete the old logs directly. Use
|
|
59
60
|
`driftseal migrate v1-to-v2 inspect`, prepare a reviewed grouping plan, apply
|