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 +73 -11
- package/README.zh-CN.md +63 -9
- package/benchmark/recent-log.js +181 -0
- package/bin/driftseal-mcp.js +117 -4
- package/bin/driftseal.js +1034 -251
- package/lib/outcome-fold.js +377 -0
- package/lib/outcome-index-sqlite.js +802 -0
- package/lib/sqlite-runtime.js +71 -0
- package/package.json +7 -2
- package/skills/use-driftseal/SKILL.md +4 -1
- package/test/package-smoke.js +150 -0
package/bin/driftseal.js
CHANGED
|
@@ -32,6 +32,15 @@ const { isDeepStrictEqual } = require('util');
|
|
|
32
32
|
const { StringDecoder } = require('string_decoder');
|
|
33
33
|
const { execFileSync, spawnSync } = require('child_process');
|
|
34
34
|
const { version: PACKAGE_VERSION } = require('../package.json');
|
|
35
|
+
const { createOutcomeFold } = require('../lib/outcome-fold.js');
|
|
36
|
+
const {
|
|
37
|
+
OutcomeIndexError,
|
|
38
|
+
SqliteUnavailableError,
|
|
39
|
+
openOutcomeIndex,
|
|
40
|
+
removeIndexFiles,
|
|
41
|
+
temporaryIndexPath,
|
|
42
|
+
} = require('../lib/outcome-index-sqlite.js');
|
|
43
|
+
const { assertSupportedNode } = require('../lib/sqlite-runtime.js');
|
|
35
44
|
|
|
36
45
|
const END_STATUSES = ['completed', 'partial', 'failed', 'abandoned'];
|
|
37
46
|
const DECISION_STATUSES = [
|
|
@@ -43,11 +52,17 @@ const DECISION_STATUSES = [
|
|
|
43
52
|
'superseded',
|
|
44
53
|
];
|
|
45
54
|
const LOG_VERSION = 2;
|
|
46
|
-
const EVENT_SCHEMA_VERSION =
|
|
55
|
+
const EVENT_SCHEMA_VERSION = 2;
|
|
56
|
+
const DEFAULT_WRITE_SCHEMA_VERSION = 1;
|
|
47
57
|
const LEGACY_EVENT_SCHEMA_VERSION = 4;
|
|
48
|
-
const PROTOCOL_VERSION = '2.
|
|
58
|
+
const PROTOCOL_VERSION = '2.1';
|
|
49
59
|
const DEFAULT_LOG_LANGUAGE = 'en';
|
|
60
|
+
const DEFAULT_LANE = 'main';
|
|
61
|
+
const LANE_NAME_RE = /^[a-z][a-z0-9-]{0,62}$/;
|
|
50
62
|
const IN_PROGRESS_GIT_PATH = 'driftseal-v2-in-progress.jsonl';
|
|
63
|
+
const CURRENT_LANE_GIT_PATH = 'driftseal-v2-current-lane';
|
|
64
|
+
const LANE_INDEX_GIT_PATH = 'driftseal-v3-outcome-index.sqlite';
|
|
65
|
+
const LEGACY_LANE_INDEX_GIT_PATH = 'driftseal-v2-lane-index.json';
|
|
51
66
|
const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
52
67
|
const LOCK_INIT_STALE_MS = 5 * 1000;
|
|
53
68
|
const READ_ONLY_NOTICE = '(read-only: another mutation holds the lock; tail repair skipped)';
|
|
@@ -57,6 +72,12 @@ const VERIFICATION_OUTPUT_CHUNK_BYTES = 64 * 1024;
|
|
|
57
72
|
const CAPTURE_OUTPUT_EDGE_CHARACTERS = 32 * 1024;
|
|
58
73
|
const CAPTURE_OUTPUT_OMISSION = '\n... [driftseal captured output truncated] ...\n';
|
|
59
74
|
const LOCAL_OUTCOME_PROVENANCE_FILE = '.driftseal-local-outcome.json';
|
|
75
|
+
const outcomeFoldEngine = createOutcomeFold({
|
|
76
|
+
fail,
|
|
77
|
+
contentHash,
|
|
78
|
+
logVersion: LOG_VERSION,
|
|
79
|
+
defaultLane: DEFAULT_LANE,
|
|
80
|
+
});
|
|
60
81
|
|
|
61
82
|
class DriftSealError extends Error {
|
|
62
83
|
constructor(message) {
|
|
@@ -84,7 +105,12 @@ function usageFor(key) {
|
|
|
84
105
|
verify: 'usage: driftseal verify [--allow-tracked-command]',
|
|
85
106
|
end: 'usage: driftseal end [id] [options]',
|
|
86
107
|
status: 'usage: driftseal status',
|
|
87
|
-
log: 'usage: driftseal log [--last N] [--all]',
|
|
108
|
+
log: 'usage: driftseal log [--last N] [--all] [--all-lanes]',
|
|
109
|
+
lane: 'usage: driftseal lane [add|switch|assign|show] ... (run: driftseal help)',
|
|
110
|
+
'lane add': 'usage: driftseal lane add <name> [--desc "<why this capability exists>"]',
|
|
111
|
+
'lane switch': 'usage: driftseal lane switch <name>',
|
|
112
|
+
'lane assign': 'usage: driftseal lane assign <id> <name>',
|
|
113
|
+
'lane show': 'usage: driftseal lane show [name]',
|
|
88
114
|
reclaim:
|
|
89
115
|
'usage: driftseal reclaim [id ...] --reason "<why>" [--older-than <days>] [--force] [--dry-run]',
|
|
90
116
|
unreclaim: 'usage: driftseal unreclaim <id> --reason "<why>"',
|
|
@@ -265,6 +291,26 @@ function normalizeMigrationSource(value, line) {
|
|
|
265
291
|
};
|
|
266
292
|
}
|
|
267
293
|
|
|
294
|
+
function laneEventId(name) {
|
|
295
|
+
return `lane:${name}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function normalizeLaneName(value, { optional = false, line } = {}) {
|
|
299
|
+
const prefix = line === undefined ? '' : ` on log line ${line}`;
|
|
300
|
+
if (value === undefined || value === null || value === '') {
|
|
301
|
+
if (optional) return DEFAULT_LANE;
|
|
302
|
+
fail(`lane name required${prefix}`);
|
|
303
|
+
}
|
|
304
|
+
if (typeof value !== 'string') fail(`invalid lane name${prefix}`);
|
|
305
|
+
const name = value.trim();
|
|
306
|
+
if (!LANE_NAME_RE.test(name)) {
|
|
307
|
+
fail(
|
|
308
|
+
`invalid lane name "${name}"${prefix} (expected a lowercase letter, then up to 62 letters, digits, or hyphens)`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
return name;
|
|
312
|
+
}
|
|
313
|
+
|
|
268
314
|
function normalizeEvent(event, line) {
|
|
269
315
|
if (!event || typeof event !== 'object' || Array.isArray(event)) {
|
|
270
316
|
fail(`invalid event object on log line ${line}`);
|
|
@@ -311,7 +357,8 @@ function normalizeEvent(event, line) {
|
|
|
311
357
|
if (new Set(decisions).size !== decisions.length) {
|
|
312
358
|
fail(`duplicate linked decision on log line ${line}`);
|
|
313
359
|
}
|
|
314
|
-
|
|
360
|
+
const lane = normalizeLaneName(event.lane, { optional: true, line });
|
|
361
|
+
return { ...event, logVersion, outcome, acceptance, decisions, lane, head: normalizeHead(event.head) };
|
|
315
362
|
}
|
|
316
363
|
|
|
317
364
|
if (event.type === 'extend') {
|
|
@@ -412,7 +459,8 @@ function normalizeEvent(event, line) {
|
|
|
412
459
|
typeof source.id !== 'string' || source.id.length === 0)) {
|
|
413
460
|
fail(`invalid imported outcome source on log line ${line}`);
|
|
414
461
|
}
|
|
415
|
-
|
|
462
|
+
const lane = normalizeLaneName(event.lane, { optional: true, line });
|
|
463
|
+
return { ...event, logVersion, decisions, lane, head: normalizeHead(event.head) };
|
|
416
464
|
}
|
|
417
465
|
|
|
418
466
|
if (event.type === 'migration') {
|
|
@@ -432,6 +480,32 @@ function normalizeEvent(event, line) {
|
|
|
432
480
|
};
|
|
433
481
|
}
|
|
434
482
|
|
|
483
|
+
if (event.type === 'lane_add') {
|
|
484
|
+
if (logVersion !== LOG_VERSION) fail(`invalid lane add event on log line ${line}`);
|
|
485
|
+
const lane = normalizeLaneName(event.lane, { line });
|
|
486
|
+
if (lane === DEFAULT_LANE) fail(`cannot add the default lane on log line ${line}`);
|
|
487
|
+
if (event.id !== laneEventId(lane)) {
|
|
488
|
+
fail(`lane add id must be ${laneEventId(lane)} on log line ${line}`);
|
|
489
|
+
}
|
|
490
|
+
if (
|
|
491
|
+
event.description !== undefined &&
|
|
492
|
+
event.description !== null &&
|
|
493
|
+
typeof event.description !== 'string'
|
|
494
|
+
) {
|
|
495
|
+
fail(`invalid lane description on log line ${line}`);
|
|
496
|
+
}
|
|
497
|
+
const description = event.description && event.description.trim().length > 0
|
|
498
|
+
? event.description.trim()
|
|
499
|
+
: null;
|
|
500
|
+
return { ...event, logVersion, lane, description };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (event.type === 'lane_assign') {
|
|
504
|
+
if (logVersion !== LOG_VERSION) fail(`invalid lane assign event on log line ${line}`);
|
|
505
|
+
const lane = normalizeLaneName(event.lane, { line });
|
|
506
|
+
return { ...event, logVersion, lane };
|
|
507
|
+
}
|
|
508
|
+
|
|
435
509
|
if (event.type === 'reclaim' || event.type === 'unreclaim') {
|
|
436
510
|
if (typeof event.reason !== 'string' || event.reason.trim().length === 0) {
|
|
437
511
|
fail(`invalid ${event.type} event on log line ${line}`);
|
|
@@ -519,6 +593,542 @@ function inProgressFile() {
|
|
|
519
593
|
return worktreeInProgressFile();
|
|
520
594
|
}
|
|
521
595
|
|
|
596
|
+
function worktreeMetadataFile(gitPath, cwd = process.cwd()) {
|
|
597
|
+
if (!isGitWorkTree(cwd)) return null;
|
|
598
|
+
const resolved = gitCapture(['rev-parse', '--git-path', gitPath], cwd);
|
|
599
|
+
if (!resolved) return null;
|
|
600
|
+
return path.resolve(cwd, resolved);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function currentLaneFile() {
|
|
604
|
+
if (isParkableOutcomeLog()) return worktreeMetadataFile(CURRENT_LANE_GIT_PATH);
|
|
605
|
+
return path.join(logDir(), '.current-lane');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function laneIndexFile() {
|
|
609
|
+
if (isParkableOutcomeLog()) return worktreeMetadataFile(LANE_INDEX_GIT_PATH);
|
|
610
|
+
return path.join(logDir(), '.outcome-index.sqlite');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function legacyLaneIndexFile() {
|
|
614
|
+
if (isParkableOutcomeLog()) return worktreeMetadataFile(LEGACY_LANE_INDEX_GIT_PATH);
|
|
615
|
+
return path.join(logDir(), '.lane-index.json');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function emptyLaneCatalog() {
|
|
619
|
+
return outcomeFoldEngine.emptyLaneCatalog();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function readCurrentLaneName() {
|
|
623
|
+
const file = currentLaneFile();
|
|
624
|
+
if (!file || !fs.existsSync(file)) return DEFAULT_LANE;
|
|
625
|
+
const name = fs.readFileSync(file, 'utf8').trim();
|
|
626
|
+
if (!name) return DEFAULT_LANE;
|
|
627
|
+
return normalizeLaneName(name);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function writeCurrentLaneName(name, { readOnly = false } = {}) {
|
|
631
|
+
if (readOnly) return;
|
|
632
|
+
const file = currentLaneFile();
|
|
633
|
+
if (!file) fail('cannot persist the current lane outside a writable seal');
|
|
634
|
+
ensureDirectoryDurable(path.dirname(file));
|
|
635
|
+
ensureDerivedLaneSidecarIgnore();
|
|
636
|
+
atomicWriteFile(file, `${name}\n`, 0o600);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function ensureDerivedLaneSidecarIgnore() {
|
|
640
|
+
if (isParkableOutcomeLog()) return;
|
|
641
|
+
if (!isGitWorkTree(logDir())) return;
|
|
642
|
+
const ignoreFile = path.join(logDir(), '.gitignore');
|
|
643
|
+
let current = fs.existsSync(ignoreFile) ? fs.readFileSync(ignoreFile, 'utf8') : '';
|
|
644
|
+
let next = current;
|
|
645
|
+
for (const name of [
|
|
646
|
+
'.current-lane',
|
|
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;
|
|
656
|
+
if (next && !next.endsWith('\n')) next += '\n';
|
|
657
|
+
next += `${name}\n`;
|
|
658
|
+
}
|
|
659
|
+
if (next === current) return;
|
|
660
|
+
ensureDirectoryDurable(logDir());
|
|
661
|
+
atomicWriteFile(ignoreFile, next, 0o644);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function hashFilePrefix(file, length) {
|
|
665
|
+
const hash = crypto.createHash('sha256');
|
|
666
|
+
if (length <= 0) return hash.digest('hex');
|
|
667
|
+
const fd = fs.openSync(file, 'r');
|
|
668
|
+
try {
|
|
669
|
+
const buffer = Buffer.alloc(Math.min(1024 * 1024, length));
|
|
670
|
+
let position = 0;
|
|
671
|
+
while (position < length) {
|
|
672
|
+
const requested = Math.min(buffer.length, length - position);
|
|
673
|
+
const read = fs.readSync(fd, buffer, 0, requested, position);
|
|
674
|
+
if (read === 0) break;
|
|
675
|
+
hash.update(buffer.subarray(0, read));
|
|
676
|
+
position += read;
|
|
677
|
+
}
|
|
678
|
+
if (position !== length) return null;
|
|
679
|
+
return hash.digest('hex');
|
|
680
|
+
} finally {
|
|
681
|
+
fs.closeSync(fd);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function laneIndexSourceIdentity(file, indexedThrough, indexedLines = 0) {
|
|
686
|
+
if (!fs.existsSync(file)) {
|
|
687
|
+
return {
|
|
688
|
+
indexedThrough: 0,
|
|
689
|
+
indexedLines: 0,
|
|
690
|
+
walHash: contentHash(''),
|
|
691
|
+
device: null,
|
|
692
|
+
inode: null,
|
|
693
|
+
mtimeMs: null,
|
|
694
|
+
ctimeMs: null,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const stat = fs.statSync(file);
|
|
698
|
+
return {
|
|
699
|
+
indexedThrough,
|
|
700
|
+
indexedLines,
|
|
701
|
+
walHash: hashFilePrefix(file, indexedThrough),
|
|
702
|
+
device: Number(stat.dev),
|
|
703
|
+
inode: Number(stat.ino),
|
|
704
|
+
mtimeMs: stat.mtimeMs,
|
|
705
|
+
ctimeMs: stat.ctimeMs,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function laneIndexMatchesFile(source, file, { exact = false } = {}) {
|
|
710
|
+
if (
|
|
711
|
+
!source ||
|
|
712
|
+
typeof source.walHash !== 'string' ||
|
|
713
|
+
!Number.isSafeInteger(source.indexedLines) ||
|
|
714
|
+
source.indexedLines < 0
|
|
715
|
+
) {
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
if (!fs.existsSync(file)) return source.indexedThrough === 0;
|
|
719
|
+
const stat = fs.statSync(file);
|
|
720
|
+
const size = stat.size;
|
|
721
|
+
if (size < source.indexedThrough || (exact && size !== source.indexedThrough)) return false;
|
|
722
|
+
if (
|
|
723
|
+
source.device !== null &&
|
|
724
|
+
source.inode !== null &&
|
|
725
|
+
(Number(stat.dev) !== source.device || Number(stat.ino) !== source.inode)
|
|
726
|
+
) {
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
if (
|
|
730
|
+
size === source.indexedThrough &&
|
|
731
|
+
stat.mtimeMs === source.mtimeMs &&
|
|
732
|
+
stat.ctimeMs === source.ctimeMs
|
|
733
|
+
) {
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
return hashFilePrefix(file, source.indexedThrough) === source.walHash;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function applyFoldEvent(state, ev) {
|
|
740
|
+
return outcomeFoldEngine.applyFoldEvent(state, ev);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function consumeLogSlice(file, startByte, onEvent, { repairTail = false, readOnly = false, startLine = 0 } = {}) {
|
|
744
|
+
if (!fs.existsSync(file)) return { endByte: 0, endLine: 0 };
|
|
745
|
+
const fd = fs.openSync(file, 'r');
|
|
746
|
+
let size;
|
|
747
|
+
let buf;
|
|
748
|
+
try {
|
|
749
|
+
size = fs.fstatSync(fd).size;
|
|
750
|
+
if (startByte > size) fail('lane index is ahead of the outcome log');
|
|
751
|
+
if (startByte === size) return { endByte: size, endLine: startLine };
|
|
752
|
+
buf = Buffer.alloc(size - startByte);
|
|
753
|
+
fs.readSync(fd, buf, 0, buf.length, startByte);
|
|
754
|
+
} finally {
|
|
755
|
+
fs.closeSync(fd);
|
|
756
|
+
}
|
|
757
|
+
let text = buf.toString('utf8');
|
|
758
|
+
if (text.length > 0 && !text.endsWith('\n')) {
|
|
759
|
+
const rawLines = text.split('\n');
|
|
760
|
+
const tail = rawLines.at(-1);
|
|
761
|
+
try {
|
|
762
|
+
JSON.parse(tail);
|
|
763
|
+
} catch {
|
|
764
|
+
const validLength = text.lastIndexOf('\n') + 1;
|
|
765
|
+
if (!readOnly) {
|
|
766
|
+
if (!repairTail) fail(`corrupt final log line in ${file}`);
|
|
767
|
+
const truncateTo = startByte + Buffer.byteLength(text.slice(0, validLength), 'utf8');
|
|
768
|
+
const repairFd = fs.openSync(file, 'r+');
|
|
769
|
+
try {
|
|
770
|
+
fs.ftruncateSync(repairFd, truncateTo);
|
|
771
|
+
fs.fsyncSync(repairFd);
|
|
772
|
+
} finally {
|
|
773
|
+
fs.closeSync(repairFd);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
text = text.slice(0, validLength);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const parts = text.split('\n');
|
|
780
|
+
let pos = startByte;
|
|
781
|
+
let lineNumber = startLine;
|
|
782
|
+
for (let i = 0; i < parts.length; i++) {
|
|
783
|
+
const line = parts[i];
|
|
784
|
+
if (i === parts.length - 1 && line === '') break;
|
|
785
|
+
lineNumber += 1;
|
|
786
|
+
const start = pos;
|
|
787
|
+
pos += Buffer.byteLength(line, 'utf8');
|
|
788
|
+
if (i < parts.length - 1) pos += 1;
|
|
789
|
+
if (line.trim().length === 0) continue;
|
|
790
|
+
try {
|
|
791
|
+
const event = normalizeEvent(JSON.parse(line), lineNumber);
|
|
792
|
+
if (event.logVersion !== LOG_VERSION) {
|
|
793
|
+
fail(`v1 intent log cannot be used as a v2 outcome log; run driftseal migrate v1-to-v2 inspect`);
|
|
794
|
+
}
|
|
795
|
+
onEvent(event, start, pos);
|
|
796
|
+
} catch (err) {
|
|
797
|
+
if (err instanceof DriftSealError || err instanceof OutcomeIndexError) throw err;
|
|
798
|
+
fail(`corrupt log line ${lineNumber} in ${file}`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return { endByte: pos, endLine: lineNumber };
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function replaceOutcomeIndexFile(temporary, target) {
|
|
805
|
+
try {
|
|
806
|
+
fs.renameSync(temporary, target);
|
|
807
|
+
} catch (error) {
|
|
808
|
+
if (!['EEXIST', 'EPERM'].includes(error && error.code)) throw error;
|
|
809
|
+
removeIndexFiles(target);
|
|
810
|
+
fs.renameSync(temporary, target);
|
|
811
|
+
}
|
|
812
|
+
fs.chmodSync(target, 0o600);
|
|
813
|
+
for (const suffix of ['-journal', '-wal', '-shm']) {
|
|
814
|
+
fs.rmSync(`${target}${suffix}`, { force: true });
|
|
815
|
+
}
|
|
816
|
+
fsyncDirectory(path.dirname(target));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function removeLegacyLaneIndex() {
|
|
820
|
+
const legacy = legacyLaneIndexFile();
|
|
821
|
+
if (legacy) fs.rmSync(legacy, { force: true });
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function rebuildCommittedOutcomeIndex({ repairTail = false } = {}) {
|
|
825
|
+
const wal = logFile();
|
|
826
|
+
const target = laneIndexFile();
|
|
827
|
+
if (!target) return null;
|
|
828
|
+
ensureDirectoryDurable(path.dirname(target));
|
|
829
|
+
ensureDerivedLaneSidecarIgnore();
|
|
830
|
+
const temporary = temporaryIndexPath(target);
|
|
831
|
+
removeIndexFiles(temporary);
|
|
832
|
+
let index;
|
|
833
|
+
try {
|
|
834
|
+
index = openOutcomeIndex(temporary);
|
|
835
|
+
let slice;
|
|
836
|
+
index.transaction(() => {
|
|
837
|
+
const indexedEvents = [];
|
|
838
|
+
slice = consumeLogSlice(
|
|
839
|
+
wal,
|
|
840
|
+
0,
|
|
841
|
+
(event, startByte, endByte) =>
|
|
842
|
+
indexedEvents.push({ event, startByte, endByte }),
|
|
843
|
+
{ repairTail }
|
|
844
|
+
);
|
|
845
|
+
index.replaceFromFoldState(
|
|
846
|
+
outcomeFoldEngine.foldState(indexedEvents.map((item) => item.event)),
|
|
847
|
+
indexedEvents
|
|
848
|
+
);
|
|
849
|
+
index.setSource(
|
|
850
|
+
laneIndexSourceIdentity(wal, slice.endByte, slice.endLine),
|
|
851
|
+
'full'
|
|
852
|
+
);
|
|
853
|
+
index.acceptProjection();
|
|
854
|
+
});
|
|
855
|
+
if (!index.integrityCheck()) fail('rebuilt SQLite outcome index failed integrity check');
|
|
856
|
+
index.close();
|
|
857
|
+
index = null;
|
|
858
|
+
replaceOutcomeIndexFile(temporary, target);
|
|
859
|
+
removeLegacyLaneIndex();
|
|
860
|
+
const reopened = openOutcomeIndex(target);
|
|
861
|
+
reopened.build = 'full';
|
|
862
|
+
return reopened;
|
|
863
|
+
} catch (error) {
|
|
864
|
+
if (index) index.close();
|
|
865
|
+
removeIndexFiles(temporary);
|
|
866
|
+
throw error;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function syncCommittedLaneIndex({ repairTail = false, readOnly = false, forceFull = false } = {}) {
|
|
871
|
+
if (process.env._DRIFTSEAL_TEST_DISABLE_OUTCOME_INDEX === '1') return null;
|
|
872
|
+
const wal = logFile();
|
|
873
|
+
const target = laneIndexFile();
|
|
874
|
+
if (!target) return null;
|
|
875
|
+
if (readOnly) {
|
|
876
|
+
if (!fs.existsSync(target)) return null;
|
|
877
|
+
try {
|
|
878
|
+
const index = openOutcomeIndex(target, { readOnly: true });
|
|
879
|
+
if (
|
|
880
|
+
!index.projectionTrusted() ||
|
|
881
|
+
!laneIndexMatchesFile(index.source(), wal, { exact: true })
|
|
882
|
+
) {
|
|
883
|
+
index.close();
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
index.build = 'hot';
|
|
887
|
+
return index;
|
|
888
|
+
} catch {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (forceFull || !fs.existsSync(target)) {
|
|
893
|
+
try {
|
|
894
|
+
return rebuildCommittedOutcomeIndex({ repairTail });
|
|
895
|
+
} catch (error) {
|
|
896
|
+
if (error instanceof SqliteUnavailableError) return null;
|
|
897
|
+
throw error;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
let index;
|
|
901
|
+
try {
|
|
902
|
+
index = openOutcomeIndex(target);
|
|
903
|
+
const source = index.source();
|
|
904
|
+
if (!index.projectionTrusted() || !laneIndexMatchesFile(source, wal)) {
|
|
905
|
+
index.close();
|
|
906
|
+
return rebuildCommittedOutcomeIndex({ repairTail });
|
|
907
|
+
}
|
|
908
|
+
const size = fs.existsSync(wal) ? fs.statSync(wal).size : 0;
|
|
909
|
+
if (size === source.indexedThrough) {
|
|
910
|
+
index.build = 'hot';
|
|
911
|
+
removeLegacyLaneIndex();
|
|
912
|
+
return index;
|
|
913
|
+
}
|
|
914
|
+
let slice;
|
|
915
|
+
index.transaction(() => {
|
|
916
|
+
slice = consumeLogSlice(
|
|
917
|
+
wal,
|
|
918
|
+
source.indexedThrough,
|
|
919
|
+
(event, start, end) =>
|
|
920
|
+
index.applyEvent(event, start, end, { applyFoldEvent, defaultLane: DEFAULT_LANE }),
|
|
921
|
+
{
|
|
922
|
+
repairTail,
|
|
923
|
+
startLine: source.indexedLines || 0,
|
|
924
|
+
}
|
|
925
|
+
);
|
|
926
|
+
index.setSource(
|
|
927
|
+
laneIndexSourceIdentity(wal, slice.endByte, slice.endLine),
|
|
928
|
+
'incremental'
|
|
929
|
+
);
|
|
930
|
+
index.acceptProjection();
|
|
931
|
+
});
|
|
932
|
+
index.build = 'incremental';
|
|
933
|
+
removeLegacyLaneIndex();
|
|
934
|
+
return index;
|
|
935
|
+
} catch (error) {
|
|
936
|
+
if (index) index.close();
|
|
937
|
+
if (error instanceof DriftSealError) throw error;
|
|
938
|
+
if (error instanceof SqliteUnavailableError) return null;
|
|
939
|
+
return rebuildCommittedOutcomeIndex({ repairTail });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function attachIndexedOverlay(index, committed, plan, { readOnly = false } = {}) {
|
|
944
|
+
const lanes = index.laneCatalog();
|
|
945
|
+
if (!plan || plan.alreadyCommitted || plan.records.length === 0) {
|
|
946
|
+
committed.lanes = lanes;
|
|
947
|
+
return committed;
|
|
948
|
+
}
|
|
949
|
+
if (!readOnly && plan.mappings.length > 0) writeJsonl(plan.park, plan.records);
|
|
950
|
+
const overlay = fold(plan.records.map((record) => record.event));
|
|
951
|
+
for (const record of overlay) {
|
|
952
|
+
let lane = lanes.get(record.lane);
|
|
953
|
+
if (!lane) {
|
|
954
|
+
const foldedLane = overlay.lanes.get(record.lane);
|
|
955
|
+
lane = {
|
|
956
|
+
name: record.lane,
|
|
957
|
+
description: foldedLane ? foldedLane.description : null,
|
|
958
|
+
addedAt: foldedLane ? foldedLane.addedAt : null,
|
|
959
|
+
inferred: foldedLane ? foldedLane.inferred === true : true,
|
|
960
|
+
head: null,
|
|
961
|
+
count: 0,
|
|
962
|
+
visible: 0,
|
|
963
|
+
};
|
|
964
|
+
lanes.set(record.lane, lane);
|
|
965
|
+
}
|
|
966
|
+
lane.count = (lane.count || 0) + 1;
|
|
967
|
+
if (!record.reclaimed) lane.visible = (lane.visible || 0) + 1;
|
|
968
|
+
}
|
|
969
|
+
const records = [...committed, ...overlay];
|
|
970
|
+
records.lanes = lanes;
|
|
971
|
+
return records;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function queryOutcomeView(index, park, { repairTail = false, readOnly = false } = {}) {
|
|
975
|
+
const committed = index.queryAll();
|
|
976
|
+
if (!park || !fs.existsSync(park)) {
|
|
977
|
+
committed.lanes = index.laneCatalog();
|
|
978
|
+
return { index: null, records: committed };
|
|
979
|
+
}
|
|
980
|
+
const plan = planIndexedInProgressOverlay(index, park, { repairTail, readOnly });
|
|
981
|
+
if (plan && plan.alreadyCommitted && !readOnly) discardInProgressLog(park);
|
|
982
|
+
return {
|
|
983
|
+
index: null,
|
|
984
|
+
records: attachIndexedOverlay(index, committed, plan, { readOnly }),
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function recoverableOutcomeIndexError(error) {
|
|
989
|
+
return (
|
|
990
|
+
error instanceof OutcomeIndexError ||
|
|
991
|
+
/^SQLITE_|^ERR_SQLITE_/.test(String(error && error.code))
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function loadOutcomeView({ repairTail = false, readOnly = false } = {}) {
|
|
996
|
+
const park = inProgressFile();
|
|
997
|
+
let index = syncCommittedLaneIndex({ repairTail, readOnly });
|
|
998
|
+
if (!index) {
|
|
999
|
+
const records = fold(readEvents({ repairTail, readOnly }));
|
|
1000
|
+
return { index: null, records };
|
|
1001
|
+
}
|
|
1002
|
+
try {
|
|
1003
|
+
return queryOutcomeView(index, park, { repairTail, readOnly });
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
index.close();
|
|
1006
|
+
index = null;
|
|
1007
|
+
if (readOnly && (error.code === 'ENOENT' || recoverableOutcomeIndexError(error))) {
|
|
1008
|
+
return { index: null, records: fold(readEvents({ repairTail, readOnly })) };
|
|
1009
|
+
}
|
|
1010
|
+
if (!recoverableOutcomeIndexError(error)) throw error;
|
|
1011
|
+
index = syncCommittedLaneIndex({ repairTail, forceFull: true });
|
|
1012
|
+
if (!index) return { index: null, records: fold(readEvents({ repairTail })) };
|
|
1013
|
+
return queryOutcomeView(index, park, { repairTail });
|
|
1014
|
+
} finally {
|
|
1015
|
+
if (index) index.close();
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function laneSummary(records, name) {
|
|
1020
|
+
const lanes = records.lanes || emptyLaneCatalog();
|
|
1021
|
+
const lane = lanes.get(name);
|
|
1022
|
+
const members = records.filter((record) => (record.lane || DEFAULT_LANE) === name);
|
|
1023
|
+
const visible = members.filter((record) => !record.reclaimed);
|
|
1024
|
+
return {
|
|
1025
|
+
name,
|
|
1026
|
+
description: lane ? lane.description : null,
|
|
1027
|
+
addedAt: lane ? lane.addedAt : null,
|
|
1028
|
+
inferred: Boolean(lane && lane.inferred),
|
|
1029
|
+
visible: visible.length,
|
|
1030
|
+
count: members.length,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function publicLane(summary) {
|
|
1035
|
+
return {
|
|
1036
|
+
name: summary.name,
|
|
1037
|
+
description: summary.description,
|
|
1038
|
+
addedAt: summary.addedAt || null,
|
|
1039
|
+
inferred: summary.inferred === true,
|
|
1040
|
+
visible: summary.visible,
|
|
1041
|
+
count: summary.count,
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function resolveCurrentLane(records, { required = false } = {}) {
|
|
1046
|
+
const requested = readCurrentLaneName();
|
|
1047
|
+
const lanes = records.lanes || emptyLaneCatalog();
|
|
1048
|
+
if (lanes.has(requested)) return { current: requested, missing: null };
|
|
1049
|
+
if (required) {
|
|
1050
|
+
fail(`current lane ${requested} does not exist; switch to ${DEFAULT_LANE} or add it`);
|
|
1051
|
+
}
|
|
1052
|
+
return { current: DEFAULT_LANE, missing: requested };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function currentLaneOrFail(records) {
|
|
1056
|
+
return resolveCurrentLane(records, { required: true }).current;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function warnMissingCurrentLane(missing) {
|
|
1060
|
+
if (!missing) return;
|
|
1061
|
+
printLine(`warning: current lane ${missing} does not exist; showing ${DEFAULT_LANE}`);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function renderLaneLine(records, name) {
|
|
1065
|
+
const summary = laneSummary(records, name);
|
|
1066
|
+
return `lane: ${name} (${summary.visible} visible / ${summary.count} in lane)`;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function selectLaneLogRecords(records, current) {
|
|
1070
|
+
return records.filter(
|
|
1071
|
+
(record) => (record.lane || DEFAULT_LANE) === current || record.status === 'in_progress'
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function selectLastLogRecords(records, current, n) {
|
|
1076
|
+
const inLane = records.filter((record) => (record.lane || DEFAULT_LANE) === current);
|
|
1077
|
+
const clipped = inLane.slice(-n);
|
|
1078
|
+
const kept = new Set(clipped.map((record) => record.id));
|
|
1079
|
+
const extras = records.filter((record) => record.status === 'in_progress' && !kept.has(record.id));
|
|
1080
|
+
if (extras.length === 0) return clipped;
|
|
1081
|
+
const order = new Map(records.map((record, index) => [record.id, index]));
|
|
1082
|
+
return [...clipped, ...extras].sort((left, right) => order.get(left.id) - order.get(right.id));
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function indexedLaneSummary(state, name) {
|
|
1086
|
+
const lane = state.lanes.get(name);
|
|
1087
|
+
return {
|
|
1088
|
+
name,
|
|
1089
|
+
description: lane ? lane.description : null,
|
|
1090
|
+
addedAt: lane ? lane.addedAt : null,
|
|
1091
|
+
inferred: Boolean(lane && lane.inferred),
|
|
1092
|
+
visible: lane ? lane.visible : 0,
|
|
1093
|
+
count: lane ? lane.count : 0,
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function tryLoadRecentOutcomeView(n, { includeReclaimed = false, repairTail = false, readOnly = false } = {}) {
|
|
1098
|
+
const park = inProgressFile();
|
|
1099
|
+
let index = syncCommittedLaneIndex({ repairTail, readOnly });
|
|
1100
|
+
if (!index) return null;
|
|
1101
|
+
const query = () => {
|
|
1102
|
+
const plan =
|
|
1103
|
+
park && fs.existsSync(park)
|
|
1104
|
+
? planIndexedInProgressOverlay(index, park, { repairTail, readOnly })
|
|
1105
|
+
: null;
|
|
1106
|
+
if (plan && plan.alreadyCommitted && !readOnly) discardInProgressLog(park);
|
|
1107
|
+
const overlayView = attachIndexedOverlay(index, [], plan, { readOnly });
|
|
1108
|
+
const state = { lanes: overlayView.lanes };
|
|
1109
|
+
const { current, missing } = resolveCurrentLane(state);
|
|
1110
|
+
const committed = index.queryRecent(current, n, { includeReclaimed });
|
|
1111
|
+
const records = selectLastLogRecords(
|
|
1112
|
+
[...committed, ...overlayView],
|
|
1113
|
+
current,
|
|
1114
|
+
n
|
|
1115
|
+
);
|
|
1116
|
+
return { state, records, current, missing };
|
|
1117
|
+
};
|
|
1118
|
+
try {
|
|
1119
|
+
return query();
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
if (readOnly && error && error.code === 'ENOENT') return null;
|
|
1122
|
+
index.close();
|
|
1123
|
+
index = null;
|
|
1124
|
+
if (readOnly) return null;
|
|
1125
|
+
index = syncCommittedLaneIndex({ repairTail, forceFull: true });
|
|
1126
|
+
return query();
|
|
1127
|
+
} finally {
|
|
1128
|
+
if (index) index.close();
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
522
1132
|
function liveWorktreeOutcomeLog() {
|
|
523
1133
|
const root = gitWorktreeRoot();
|
|
524
1134
|
if (!root) return null;
|
|
@@ -600,6 +1210,27 @@ function planInProgressOverlay(committedEvents, park, { repairTail = false, read
|
|
|
600
1210
|
return { park, records: remapped.records, mappings: remapped.mappings, alreadyCommitted: false };
|
|
601
1211
|
}
|
|
602
1212
|
|
|
1213
|
+
function planIndexedInProgressOverlay(index, park, { repairTail = false, readOnly = false } = {}) {
|
|
1214
|
+
if (!park || !fs.existsSync(park)) return null;
|
|
1215
|
+
const overlayRecords = readJsonlRecordsFromFile(park, { repairTail, readOnly });
|
|
1216
|
+
const overlayEvents = overlayRecords.map((record) => record.event);
|
|
1217
|
+
if (overlayEvents.length === 0 || index.containsEventSequence(overlayEvents)) {
|
|
1218
|
+
return { park, records: [], mappings: [], alreadyCommitted: true };
|
|
1219
|
+
}
|
|
1220
|
+
const remapped = remapTheirsRecords(
|
|
1221
|
+
overlayRecords,
|
|
1222
|
+
index.outcomeStartEvents(),
|
|
1223
|
+
new Map(),
|
|
1224
|
+
new Map()
|
|
1225
|
+
);
|
|
1226
|
+
return {
|
|
1227
|
+
park,
|
|
1228
|
+
records: remapped.records,
|
|
1229
|
+
mappings: remapped.mappings,
|
|
1230
|
+
alreadyCommitted: false,
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
|
|
603
1234
|
function discardInProgressLog(park) {
|
|
604
1235
|
fs.unlinkSync(park);
|
|
605
1236
|
fsyncDirectory(path.dirname(park));
|
|
@@ -706,10 +1337,27 @@ function ensureV2OutcomeLogExists() {
|
|
|
706
1337
|
fsyncDirectory(logDir());
|
|
707
1338
|
}
|
|
708
1339
|
|
|
1340
|
+
function eventWriteSchemaVersion(event) {
|
|
1341
|
+
if (Number.isSafeInteger(event.schemaVersion)) return event.schemaVersion;
|
|
1342
|
+
if (event.type === 'lane_add' || event.type === 'lane_assign') return EVENT_SCHEMA_VERSION;
|
|
1343
|
+
if (
|
|
1344
|
+
(event.type === 'begin' || event.type === 'import') &&
|
|
1345
|
+
event.lane &&
|
|
1346
|
+
event.lane !== DEFAULT_LANE
|
|
1347
|
+
) {
|
|
1348
|
+
return EVENT_SCHEMA_VERSION;
|
|
1349
|
+
}
|
|
1350
|
+
return DEFAULT_WRITE_SCHEMA_VERSION;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
709
1353
|
function appendEventTo(file, event) {
|
|
710
1354
|
ensureDirectoryDurable(path.dirname(file));
|
|
711
1355
|
const existed = fs.existsSync(file);
|
|
712
|
-
const storedEvent = {
|
|
1356
|
+
const storedEvent = {
|
|
1357
|
+
logVersion: LOG_VERSION,
|
|
1358
|
+
...event,
|
|
1359
|
+
schemaVersion: eventWriteSchemaVersion(event),
|
|
1360
|
+
};
|
|
713
1361
|
const line = Buffer.from(`${JSON.stringify(storedEvent)}\n`, 'utf8');
|
|
714
1362
|
const fd = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND, 0o600);
|
|
715
1363
|
try {
|
|
@@ -1156,237 +1804,20 @@ function withMutationLocks(resources, action, { tryWaitMs } = {}) {
|
|
|
1156
1804
|
}
|
|
1157
1805
|
|
|
1158
1806
|
function outcomeContractHash(record) {
|
|
1159
|
-
return
|
|
1160
|
-
outcome: record.outcome,
|
|
1161
|
-
extensions: record.extensions.map(({ extension, acceptance, verify, decisions }) => ({
|
|
1162
|
-
extension,
|
|
1163
|
-
acceptance,
|
|
1164
|
-
verify,
|
|
1165
|
-
decisions,
|
|
1166
|
-
})),
|
|
1167
|
-
acceptance: record.acceptance,
|
|
1168
|
-
verify: record.verify,
|
|
1169
|
-
decisions: record.decisions,
|
|
1170
|
-
}));
|
|
1807
|
+
return outcomeFoldEngine.outcomeContractHash(record);
|
|
1171
1808
|
}
|
|
1172
1809
|
|
|
1173
1810
|
function newOutcomeRecord(ev) {
|
|
1174
|
-
|
|
1175
|
-
id: ev.id,
|
|
1176
|
-
tsBegin: ev.ts,
|
|
1177
|
-
outcome: ev.outcome,
|
|
1178
|
-
extensions: [],
|
|
1179
|
-
acceptance: Array.isArray(ev.acceptance) ? ev.acceptance : [],
|
|
1180
|
-
verify: ev.verify || null,
|
|
1181
|
-
beginHead: ev.head || null,
|
|
1182
|
-
decisions: Array.isArray(ev.decisions) ? ev.decisions : [],
|
|
1183
|
-
logVersion: ev.logVersion || 1,
|
|
1184
|
-
schemaVersion: ev.schemaVersion || 1,
|
|
1185
|
-
decisionPrepares: [],
|
|
1186
|
-
decisionTerminals: [],
|
|
1187
|
-
decisionUpdates: [],
|
|
1188
|
-
verificationAttempts: [],
|
|
1189
|
-
verification: null,
|
|
1190
|
-
status: 'in_progress',
|
|
1191
|
-
tsEnd: null,
|
|
1192
|
-
note: null,
|
|
1193
|
-
verifyResult: null,
|
|
1194
|
-
endHead: null,
|
|
1195
|
-
reclaimed: false,
|
|
1196
|
-
reclaimReason: null,
|
|
1197
|
-
reclaimedAt: null,
|
|
1198
|
-
imported: null,
|
|
1199
|
-
contractHash: null,
|
|
1200
|
-
};
|
|
1201
|
-
record.contractHash = outcomeContractHash(record);
|
|
1202
|
-
return record;
|
|
1811
|
+
return outcomeFoldEngine.newOutcomeRecord(ev);
|
|
1203
1812
|
}
|
|
1204
1813
|
|
|
1205
1814
|
/** Fold the event stream into one record per outcome. Legacy v1 events are accepted for migration. */
|
|
1206
1815
|
function fold(events) {
|
|
1207
|
-
|
|
1208
|
-
const reconciliations = new Map();
|
|
1209
|
-
const order = [];
|
|
1210
|
-
for (const ev of events) {
|
|
1211
|
-
if (ev.type === 'begin') {
|
|
1212
|
-
if (records.has(ev.id)) fail(`duplicate begin event for outcome id: ${ev.id}`);
|
|
1213
|
-
records.set(ev.id, newOutcomeRecord(ev));
|
|
1214
|
-
order.push(ev.id);
|
|
1215
|
-
} else if (ev.type === 'import') {
|
|
1216
|
-
if (records.has(ev.id)) fail(`duplicate imported outcome id: ${ev.id}`);
|
|
1217
|
-
const record = newOutcomeRecord({
|
|
1218
|
-
...ev,
|
|
1219
|
-
ts: ev.beganAt,
|
|
1220
|
-
acceptance: [],
|
|
1221
|
-
verify: null,
|
|
1222
|
-
});
|
|
1223
|
-
record.status = ev.status;
|
|
1224
|
-
record.tsEnd = ev.endedAt;
|
|
1225
|
-
record.note = ev.summary || null;
|
|
1226
|
-
record.reclaimed = ev.reclaimed === true;
|
|
1227
|
-
record.reclaimReason = ev.reclaimReason || null;
|
|
1228
|
-
record.reclaimedAt = ev.reclaimedAt || null;
|
|
1229
|
-
record.imported = {
|
|
1230
|
-
sourceIds: ev.sources.map((source) => source.id),
|
|
1231
|
-
sourceFingerprint: ev.sourceFingerprint,
|
|
1232
|
-
sources: ev.sources,
|
|
1233
|
-
};
|
|
1234
|
-
records.set(ev.id, record);
|
|
1235
|
-
order.push(ev.id);
|
|
1236
|
-
} else if (ev.type === 'migration') {
|
|
1237
|
-
continue;
|
|
1238
|
-
} else if (ev.type === 'extend') {
|
|
1239
|
-
const rec = records.get(ev.id);
|
|
1240
|
-
if (!rec) fail(`extension references unknown outcome id: ${ev.id}`);
|
|
1241
|
-
if (rec.status !== 'in_progress') fail(`extension occurred after outcome ${ev.id} was closed`);
|
|
1242
|
-
rec.extensions.push({
|
|
1243
|
-
extension: ev.extension,
|
|
1244
|
-
acceptance: ev.acceptance,
|
|
1245
|
-
verify: ev.verify,
|
|
1246
|
-
decisions: ev.decisions,
|
|
1247
|
-
extendedAt: ev.ts,
|
|
1248
|
-
head: ev.head || null,
|
|
1249
|
-
});
|
|
1250
|
-
rec.acceptance = [...new Set([...rec.acceptance, ...ev.acceptance])];
|
|
1251
|
-
if (ev.verify) rec.verify = ev.verify;
|
|
1252
|
-
rec.decisions = [...new Set([...rec.decisions, ...ev.decisions])];
|
|
1253
|
-
rec.contractHash = outcomeContractHash(rec);
|
|
1254
|
-
rec.verification = null;
|
|
1255
|
-
// Reconciliation certifies the final cumulative outcome contract, just like
|
|
1256
|
-
// machine verification. Any later extension makes every earlier confirmation stale.
|
|
1257
|
-
rec.decisionUpdates = [];
|
|
1258
|
-
} else if (ev.type === 'verify') {
|
|
1259
|
-
const rec = records.get(ev.id);
|
|
1260
|
-
if (!rec) fail(`verification event references unknown outcome id: ${ev.id}`);
|
|
1261
|
-
if (rec.status !== 'in_progress') fail(`verification occurred after outcome ${ev.id} was closed`);
|
|
1262
|
-
if (rec.acceptance.length === 0 || !rec.verify) {
|
|
1263
|
-
fail(`verification event references outcome ${ev.id} without acceptance criteria`);
|
|
1264
|
-
}
|
|
1265
|
-
if (ev.command !== rec.verify) fail(`verification command does not match outcome ${ev.id}`);
|
|
1266
|
-
if (rec.logVersion === LOG_VERSION && ev.contractHash !== rec.contractHash) {
|
|
1267
|
-
fail(`verification contract does not match outcome ${ev.id}`);
|
|
1268
|
-
}
|
|
1269
|
-
rec.verificationAttempts.push(ev);
|
|
1270
|
-
rec.verification = ev;
|
|
1271
|
-
} else if (ev.type === 'reclaim' || ev.type === 'unreclaim') {
|
|
1272
|
-
const rec = records.get(ev.id);
|
|
1273
|
-
if (!rec) fail(`${ev.type} event references unknown outcome id: ${ev.id}`);
|
|
1274
|
-
if (ev.type === 'reclaim') {
|
|
1275
|
-
if (rec.status === 'in_progress') fail(`cannot reclaim outcome ${ev.id} while it is in_progress`);
|
|
1276
|
-
if (rec.reclaimed) fail(`duplicate reclaim event for outcome id: ${ev.id}`);
|
|
1277
|
-
rec.reclaimed = true;
|
|
1278
|
-
rec.reclaimReason = ev.reason;
|
|
1279
|
-
rec.reclaimedAt = ev.ts;
|
|
1280
|
-
} else {
|
|
1281
|
-
if (!rec.reclaimed) fail(`unreclaim event for outcome id that is not reclaimed: ${ev.id}`);
|
|
1282
|
-
rec.reclaimed = false;
|
|
1283
|
-
rec.reclaimReason = null;
|
|
1284
|
-
rec.reclaimedAt = null;
|
|
1285
|
-
}
|
|
1286
|
-
} else if (ev.type === 'end') {
|
|
1287
|
-
const rec = records.get(ev.id);
|
|
1288
|
-
if (!rec) fail(`end event references unknown outcome id: ${ev.id}`);
|
|
1289
|
-
if (rec.status !== 'in_progress') fail(`duplicate end event for outcome id: ${ev.id}`);
|
|
1290
|
-
const conflictingCancellation = rec.decisionTerminals.find(
|
|
1291
|
-
(terminal) => terminal.type === 'decision_reconcile_cancel' && terminal.outcomeStatus !== ev.status
|
|
1292
|
-
);
|
|
1293
|
-
if (conflictingCancellation) {
|
|
1294
|
-
fail(`outcome ${ev.id} was closed as ${ev.status} after reconciliation recovery was cancelled for ${conflictingCancellation.outcomeStatus}`);
|
|
1295
|
-
}
|
|
1296
|
-
if (
|
|
1297
|
-
['completed', 'partial'].includes(ev.status) &&
|
|
1298
|
-
rec.decisions.length > 0 &&
|
|
1299
|
-
((rec.logVersion === 1 && rec.schemaVersion >= 2 && (ev.schemaVersion || 1) < 2) ||
|
|
1300
|
-
rec.decisions.some((decisionId) => qualifyingDecisionUpdates(rec, decisionId).length === 0))
|
|
1301
|
-
) {
|
|
1302
|
-
fail(`linked outcome ${ev.id} was closed without reconciling every declared decision`);
|
|
1303
|
-
}
|
|
1304
|
-
if (ev.status === 'completed' && rec.acceptance.length > 0) {
|
|
1305
|
-
if (!rec.verification || !rec.verification.passed) {
|
|
1306
|
-
fail(`acceptance-bound outcome ${ev.id} was completed without successful machine verification`);
|
|
1307
|
-
}
|
|
1308
|
-
if (
|
|
1309
|
-
(rec.logVersion === 1 && (ev.schemaVersion || 1) < 4) ||
|
|
1310
|
-
ev.verificationId !== rec.verification.verificationId ||
|
|
1311
|
-
(ev.workspace ?? null) !== rec.verification.workspace ||
|
|
1312
|
-
(rec.logVersion === LOG_VERSION &&
|
|
1313
|
-
(ev.contractHash !== rec.contractHash || rec.verification.contractHash !== rec.contractHash))
|
|
1314
|
-
) {
|
|
1315
|
-
fail(`acceptance-bound outcome ${ev.id} was completed with stale machine verification`);
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
rec.status = ev.status;
|
|
1319
|
-
rec.tsEnd = ev.ts;
|
|
1320
|
-
rec.note = ev.note || null;
|
|
1321
|
-
rec.verifyResult = ev.verifyResult || null;
|
|
1322
|
-
rec.endHead = ev.head || null;
|
|
1323
|
-
} else if (ev.type === 'decision_reconcile_prepare') {
|
|
1324
|
-
const rec = records.get(ev.id);
|
|
1325
|
-
if (!rec) fail(`decision reconciliation references unknown outcome id: ${ev.id}`);
|
|
1326
|
-
if (rec.status !== 'in_progress') fail(`decision reconciliation occurred after outcome ${ev.id} was closed`);
|
|
1327
|
-
if (!rec.decisions.includes(ev.decisionId)) fail(`decision reconciliation references unlinked decision ${ev.decisionId}`);
|
|
1328
|
-
if (reconciliations.has(ev.reconciliationId)) fail(`duplicate reconciliation id: ${ev.reconciliationId}`);
|
|
1329
|
-
rec.decisionPrepares.push(ev);
|
|
1330
|
-
reconciliations.set(ev.reconciliationId, {
|
|
1331
|
-
prepare: ev,
|
|
1332
|
-
terminal: null,
|
|
1333
|
-
contractHash: rec.contractHash,
|
|
1334
|
-
});
|
|
1335
|
-
} else if (ev.type === 'decision_reconcile') {
|
|
1336
|
-
const rec = records.get(ev.id);
|
|
1337
|
-
if (!rec) fail(`decision reconciliation references unknown outcome id: ${ev.id}`);
|
|
1338
|
-
if (rec.status !== 'in_progress') fail(`decision reconciliation occurred after outcome ${ev.id} was closed`);
|
|
1339
|
-
if (rec.logVersion === 1 && rec.schemaVersion >= 2) {
|
|
1340
|
-
fail(`linked legacy schema-v2 outcome ${rec.id} contains a legacy decision reconciliation`);
|
|
1341
|
-
}
|
|
1342
|
-
rec.decisionUpdates.push(ev);
|
|
1343
|
-
} else if (
|
|
1344
|
-
ev.type === 'decision_reconcile_commit' ||
|
|
1345
|
-
ev.type === 'decision_reconcile_abort' ||
|
|
1346
|
-
ev.type === 'decision_reconcile_cancel'
|
|
1347
|
-
) {
|
|
1348
|
-
const rec = records.get(ev.id);
|
|
1349
|
-
const reconciliation = reconciliations.get(ev.reconciliationId);
|
|
1350
|
-
if (rec && rec.status !== 'in_progress') fail(`decision reconciliation occurred after outcome ${ev.id} was closed`);
|
|
1351
|
-
if (!rec || !reconciliation || reconciliation.prepare.id !== ev.id || reconciliation.prepare.decisionId !== ev.decisionId) {
|
|
1352
|
-
fail(`decision reconciliation terminal has no matching prepare: ${ev.reconciliationId}`);
|
|
1353
|
-
}
|
|
1354
|
-
if (reconciliation.terminal) fail(`decision reconciliation already has a terminal event: ${ev.reconciliationId}`);
|
|
1355
|
-
const priorCancellation = rec.decisionTerminals.find((terminal) => terminal.type === 'decision_reconcile_cancel');
|
|
1356
|
-
if (ev.type === 'decision_reconcile_cancel' && priorCancellation && priorCancellation.outcomeStatus !== ev.outcomeStatus) {
|
|
1357
|
-
fail(`outcome ${ev.id} has conflicting reconciliation cancellation statuses`);
|
|
1358
|
-
}
|
|
1359
|
-
if (
|
|
1360
|
-
ev.type === 'decision_reconcile_commit' &&
|
|
1361
|
-
(reconciliation.prepare.newHash !== ev.fileHash ||
|
|
1362
|
-
reconciliation.prepare.fromStatus !== ev.fromStatus ||
|
|
1363
|
-
reconciliation.prepare.toStatus !== ev.toStatus)
|
|
1364
|
-
) {
|
|
1365
|
-
fail(`decision reconciliation commit does not match prepare: ${ev.reconciliationId}`);
|
|
1366
|
-
}
|
|
1367
|
-
reconciliation.terminal = ev;
|
|
1368
|
-
rec.decisionTerminals.push(ev);
|
|
1369
|
-
if (
|
|
1370
|
-
ev.type === 'decision_reconcile_commit' &&
|
|
1371
|
-
reconciliation.contractHash === rec.contractHash
|
|
1372
|
-
) {
|
|
1373
|
-
rec.decisionUpdates.push(ev);
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
return order.map((id) => records.get(id));
|
|
1816
|
+
return outcomeFoldEngine.fold(events);
|
|
1378
1817
|
}
|
|
1379
1818
|
|
|
1380
1819
|
function qualifyingDecisionUpdates(record, decisionId) {
|
|
1381
|
-
return
|
|
1382
|
-
if (update.decisionId !== decisionId) return false;
|
|
1383
|
-
if (record.logVersion === 1 && record.schemaVersion < 2) return true;
|
|
1384
|
-
return (
|
|
1385
|
-
update.type === 'decision_reconcile_commit' &&
|
|
1386
|
-
(update.logVersion === LOG_VERSION || (update.schemaVersion || 1) >= 2) &&
|
|
1387
|
-
typeof update.fileHash === 'string'
|
|
1388
|
-
);
|
|
1389
|
-
});
|
|
1820
|
+
return outcomeFoldEngine.qualifyingDecisionUpdates(record, decisionId);
|
|
1390
1821
|
}
|
|
1391
1822
|
|
|
1392
1823
|
function openOutcome(records) {
|
|
@@ -1756,8 +2187,12 @@ function parseArgs(argv, spec, usageKey) {
|
|
|
1756
2187
|
return { positionals, flags };
|
|
1757
2188
|
}
|
|
1758
2189
|
|
|
1759
|
-
function render(rec) {
|
|
2190
|
+
function render(rec, { currentLane } = {}) {
|
|
1760
2191
|
const lines = [`[${rec.id}] ${rec.status}`];
|
|
2192
|
+
const lane = rec.lane || DEFAULT_LANE;
|
|
2193
|
+
if (lane !== DEFAULT_LANE || (currentLane && lane !== currentLane)) {
|
|
2194
|
+
lines.push(` lane: ${lane}`);
|
|
2195
|
+
}
|
|
1761
2196
|
lines.push(` outcome: ${rec.outcome}`);
|
|
1762
2197
|
for (const extension of rec.extensions) lines.push(` extend: ${extension.extension}`);
|
|
1763
2198
|
for (const criterion of rec.acceptance) lines.push(` accept: ${criterion}`);
|
|
@@ -1807,6 +2242,7 @@ function publicOutcome(rec) {
|
|
|
1807
2242
|
return {
|
|
1808
2243
|
id: rec.id,
|
|
1809
2244
|
outcome: rec.outcome,
|
|
2245
|
+
lane: rec.lane || DEFAULT_LANE,
|
|
1810
2246
|
extensions: rec.extensions.map((extension) => ({ ...extension })),
|
|
1811
2247
|
acceptance: [...rec.acceptance],
|
|
1812
2248
|
verify: rec.verify,
|
|
@@ -2085,12 +2521,12 @@ function stripDecisionLogLanguage(block, language = DEFAULT_LOG_LANGUAGE) {
|
|
|
2085
2521
|
function outcomeLogLanguageParagraph(language) {
|
|
2086
2522
|
return `**Log language:** \`${language}\`. Write outcome-log prose (outcome, extension, note,
|
|
2087
2523
|
verify-result, and reclaim/unreclaim reason) in that language. Keep command
|
|
2088
|
-
names, flags, status tokens, and
|
|
2524
|
+
names, flags, status tokens, ids, and lane names in English.`;
|
|
2089
2525
|
}
|
|
2090
2526
|
|
|
2091
|
-
function
|
|
2527
|
+
function intentProtocolBlockV20(language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
2092
2528
|
return `${INTENT_PROTOCOL_MARKER}
|
|
2093
|
-
<!-- driftseal-version:
|
|
2529
|
+
<!-- driftseal-version: 2.0 -->
|
|
2094
2530
|
<!-- driftseal-log-language: ${language} -->${localLog ? '\n<!-- driftseal-local-log: true -->' : ''}
|
|
2095
2531
|
|
|
2096
2532
|
## Agent protocol: outcome write-ahead log
|
|
@@ -2099,7 +2535,9 @@ This repository uses DriftSeal (\`driftseal\`) to prevent agent drift. This
|
|
|
2099
2535
|
\`AGENTS.md\` protocol is the source of truth; use the CLI by default, with MCP
|
|
2100
2536
|
and lifecycle hooks as optional adapters.
|
|
2101
2537
|
|
|
2102
|
-
|
|
2538
|
+
**Log language:** \`${language}\`. Write outcome-log prose (outcome, extension, note,
|
|
2539
|
+
verify-result, and reclaim/unreclaim reason) in that language. Keep command
|
|
2540
|
+
names, flags, status tokens, and ids in English.
|
|
2103
2541
|
|
|
2104
2542
|
1. **Write the outcome first**, before changing durable project content:
|
|
2105
2543
|
\`driftseal begin "<coherent delivery outcome>" --accept "<observable result>" --verify "<exact command that proves the cumulative contract>"\`.
|
|
@@ -2141,6 +2579,70 @@ Seal root: \`.seal/\` (override with \`$DRIFTSEAL_HOME\`); outcome log:
|
|
|
2141
2579
|
${INTENT_PROTOCOL_END}`;
|
|
2142
2580
|
}
|
|
2143
2581
|
|
|
2582
|
+
function intentProtocolBlockV21(version = PROTOCOL_VERSION, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
2583
|
+
return `${INTENT_PROTOCOL_MARKER}
|
|
2584
|
+
<!-- driftseal-version: ${version} -->
|
|
2585
|
+
<!-- driftseal-log-language: ${language} -->${localLog ? '\n<!-- driftseal-local-log: true -->' : ''}
|
|
2586
|
+
|
|
2587
|
+
## Agent protocol: outcome write-ahead log
|
|
2588
|
+
|
|
2589
|
+
This repository uses DriftSeal (\`driftseal\`) to prevent agent drift. This
|
|
2590
|
+
\`AGENTS.md\` protocol is the source of truth; use the CLI by default, with MCP
|
|
2591
|
+
and lifecycle hooks as optional adapters.
|
|
2592
|
+
|
|
2593
|
+
${outcomeLogLanguageParagraph(language)}
|
|
2594
|
+
|
|
2595
|
+
1. **Write the outcome first**, before changing durable project content:
|
|
2596
|
+
\`driftseal begin "<coherent delivery outcome>" --accept "<observable result>" --verify "<exact command that proves the cumulative contract>"\`.
|
|
2597
|
+
Repeat \`--accept\` for independently observable criteria and add one
|
|
2598
|
+
\`--decision <id>\` for each existing MADR this outcome may change.
|
|
2599
|
+
Record outcomes for changes intended to persist in the project: code,
|
|
2600
|
+
configuration, documentation, dependencies, and equivalent files, inside or
|
|
2601
|
+
outside Git. Git operations, checks, temporary auxiliary work, and external
|
|
2602
|
+
state changes are exempt when they do not write durable project content here.
|
|
2603
|
+
2. **Extend only the same outcome.** For another step toward the same coherent
|
|
2604
|
+
delivery goal, append \`driftseal extend "<addition>"\`. It may add
|
|
2605
|
+
\`--accept\`, \`--decision\`, and a replacement \`--verify\`; adding acceptance
|
|
2606
|
+
requires a replacement verifier that proves the complete accumulated contract.
|
|
2607
|
+
Every extension invalidates earlier verification and MADR reconciliation. If
|
|
2608
|
+
the delivery goal changes, close the current outcome honestly and begin a new one.
|
|
2609
|
+
One open outcome belongs to one worktree, or one configured non-Git project
|
|
2610
|
+
root. Every agent changing durable content in the same root re-anchors and
|
|
2611
|
+
continues it; separate worktrees hold separate outcomes.
|
|
2612
|
+
Outcomes belong to one named lane (\`driftseal lane\`). The default lane is
|
|
2613
|
+
\`main\`; untagged history lives there. Re-anchoring and \`driftseal log\`
|
|
2614
|
+
follow the current lane. Close the open outcome before switching lanes.
|
|
2615
|
+
Create a lane only for a long-lived capability you expect to leave and resume.
|
|
2616
|
+
3. **Reconcile, verify, then close.** After the final extension, reconcile every
|
|
2617
|
+
linked MADR with \`driftseal decision update\`. Inspect \`driftseal status\`,
|
|
2618
|
+
then run \`driftseal verify\` for an acceptance-bound outcome. A verifier
|
|
2619
|
+
without matching local provenance is untrusted and requires
|
|
2620
|
+
\`--allow-tracked-command\` after inspection. Finish with
|
|
2621
|
+
\`driftseal end -s completed|partial|failed|abandoned -n "<what happened>"\`.
|
|
2622
|
+
Completed outcomes require fresh successful verification bound to both the
|
|
2623
|
+
current contract hash and Git-visible workspace. Never report success without
|
|
2624
|
+
closing the outcome.
|
|
2625
|
+
4. **Re-anchor after context loss or handoff:** run \`driftseal status\` and
|
|
2626
|
+
\`driftseal log --last 3\` before changing durable content. Both follow the
|
|
2627
|
+
current lane. Resume the open outcome when it still matches; otherwise close
|
|
2628
|
+
it and begin a new one. If the requested work belongs to a different existing
|
|
2629
|
+
lane, switch first.
|
|
2630
|
+
|
|
2631
|
+
**Log access goes only through DriftSeal.** Never read, edit, move, or delete
|
|
2632
|
+
\`.seal/outcomes/events.jsonl\` (or its configured equivalent) directly. Use
|
|
2633
|
+
\`reclaim\`/\`unreclaim\` for visibility markers and \`absorb\` after merge
|
|
2634
|
+
collisions. These operations preserve append-only single-lineage history.
|
|
2635
|
+
|
|
2636
|
+
Seal root: \`.seal/\` (override with \`$DRIFTSEAL_HOME\`); outcome log:
|
|
2637
|
+
\`.seal/outcomes/events.jsonl\`; ${localLog ? 'keep `.seal/` local and untracked.' : 'commit `.seal/` with the code.'}
|
|
2638
|
+
${INTENT_PROTOCOL_END}`;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
function intentProtocolBlock(version = PROTOCOL_VERSION, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
2642
|
+
if (String(version) === '2.0') return intentProtocolBlockV20(language, localLog);
|
|
2643
|
+
return intentProtocolBlockV21(version, language, localLog);
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2144
2646
|
function v1IntentProtocolBlock(version = 14, language = DEFAULT_LOG_LANGUAGE, localLog = false) {
|
|
2145
2647
|
return `${INTENT_PROTOCOL_MARKER}
|
|
2146
2648
|
<!-- driftseal-version: ${version} -->
|
|
@@ -2858,6 +3360,8 @@ const SKILL_RELEASE_DIGESTS = new Set([
|
|
|
2858
3360
|
'72ddea79940bdf2bce66d491888f11423ae1bd383e1b511028fda617e6f6fb27', // f395778 1.1.6 parked intents
|
|
2859
3361
|
'df8bc7035de1a19faf307c92f9bb0f4052e683d1a94881c2c5d5cbef48b67568', // dc9899d 1.1.7 parked intents in absorb
|
|
2860
3362
|
'42a0549dff21483c0508ea4a79658e7bf05cd98f8af4238c95dbd23cdcde7ee6', // 2.0.0 outcome workflow
|
|
3363
|
+
'38e89060ff37ecdd663eae73a3e0c646d6bb220fc8232a5762356375d84ba69b', // 2.1.0 outcome lanes
|
|
3364
|
+
'b4b2cc27ea71c5777b5eb9b67d861fbe1da43f31ab5d422d6a877e0cad592493', // 2.1.0 lane re-anchor recovery
|
|
2861
3365
|
]);
|
|
2862
3366
|
|
|
2863
3367
|
function skillInstallUsage() {
|
|
@@ -3849,6 +4353,19 @@ function rebindV2ContractHashes(records) {
|
|
|
3849
4353
|
});
|
|
3850
4354
|
}
|
|
3851
4355
|
|
|
4356
|
+
function dropDuplicateLaneAdds(oursEvents, theirsRecords) {
|
|
4357
|
+
const seen = new Set();
|
|
4358
|
+
for (const event of oursEvents) {
|
|
4359
|
+
if (event.type === 'lane_add') seen.add(event.lane);
|
|
4360
|
+
}
|
|
4361
|
+
return theirsRecords.filter((record) => {
|
|
4362
|
+
if (record.event.type !== 'lane_add') return true;
|
|
4363
|
+
if (seen.has(record.event.lane)) return false;
|
|
4364
|
+
seen.add(record.event.lane);
|
|
4365
|
+
return true;
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4368
|
+
|
|
3852
4369
|
function remapTheirsRecords(theirsNew, oursUsedEvents, decisionMap, hashMap = new Map()) {
|
|
3853
4370
|
const intentMap = new Map();
|
|
3854
4371
|
const mappings = [];
|
|
@@ -3870,6 +4387,7 @@ function remapTheirsRecords(theirsNew, oursUsedEvents, decisionMap, hashMap = ne
|
|
|
3870
4387
|
|
|
3871
4388
|
function repairDuplicateOutcomeRecords(records, decisionMap, hashMap = new Map()) {
|
|
3872
4389
|
const seenBegins = new Set();
|
|
4390
|
+
const seenLanes = new Set();
|
|
3873
4391
|
const intentMap = new Map();
|
|
3874
4392
|
const used = [];
|
|
3875
4393
|
const mappings = [];
|
|
@@ -3877,6 +4395,10 @@ function repairDuplicateOutcomeRecords(records, decisionMap, hashMap = new Map()
|
|
|
3877
4395
|
let incomingSide = false;
|
|
3878
4396
|
for (const record of records) {
|
|
3879
4397
|
let event = record.event;
|
|
4398
|
+
if (event.type === 'lane_add') {
|
|
4399
|
+
if (seenLanes.has(event.lane)) continue;
|
|
4400
|
+
seenLanes.add(event.lane);
|
|
4401
|
+
}
|
|
3880
4402
|
if (isOutcomeStart(event) && seenBegins.has(event.id)) {
|
|
3881
4403
|
incomingSide = true;
|
|
3882
4404
|
const { date } = parseOutcomeId(event.id);
|
|
@@ -3946,7 +4468,7 @@ function abandonOpenIntent(records, targetId, side) {
|
|
|
3946
4468
|
records.push({
|
|
3947
4469
|
event: {
|
|
3948
4470
|
logVersion: LOG_VERSION,
|
|
3949
|
-
schemaVersion:
|
|
4471
|
+
schemaVersion: DEFAULT_WRITE_SCHEMA_VERSION,
|
|
3950
4472
|
type: 'end',
|
|
3951
4473
|
id: targetId,
|
|
3952
4474
|
ts: new Date().toISOString(),
|
|
@@ -4169,7 +4691,10 @@ function absorbFromStreams(ours, theirs, baseRecords, options) {
|
|
|
4169
4691
|
baseIds: options.baseDecisionIds || new Set(),
|
|
4170
4692
|
});
|
|
4171
4693
|
const remapped = remapTheirsRecords(
|
|
4172
|
-
|
|
4694
|
+
dropDuplicateLaneAdds(
|
|
4695
|
+
[...streams.base, ...streams.oursNew].map((record) => record.event),
|
|
4696
|
+
streams.theirsNew
|
|
4697
|
+
),
|
|
4173
4698
|
[...streams.base, ...streams.oursNew].map((record) => record.event),
|
|
4174
4699
|
decisionPlan.decisionMap,
|
|
4175
4700
|
decisionPlan.hashMap
|
|
@@ -4913,7 +5438,7 @@ function validateMigrationPlan(plan, snapshot) {
|
|
|
4913
5438
|
}
|
|
4914
5439
|
|
|
4915
5440
|
function storedV2Event(event) {
|
|
4916
|
-
return { logVersion: LOG_VERSION, schemaVersion:
|
|
5441
|
+
return { logVersion: LOG_VERSION, schemaVersion: DEFAULT_WRITE_SCHEMA_VERSION, ...event };
|
|
4917
5442
|
}
|
|
4918
5443
|
|
|
4919
5444
|
function migrationImportEvent(snapshot, validated, group, events) {
|
|
@@ -5270,6 +5795,116 @@ function checkMigration(snapshot, { sourceMissing = false } = {}) {
|
|
|
5270
5795
|
};
|
|
5271
5796
|
}
|
|
5272
5797
|
|
|
5798
|
+
function listLaneSnapshot(records) {
|
|
5799
|
+
const catalog = records.lanes || emptyLaneCatalog();
|
|
5800
|
+
const { current, missing } = resolveCurrentLane(records);
|
|
5801
|
+
const lanes = [...catalog.keys()].map((name) => {
|
|
5802
|
+
const summary = laneSummary(records, name);
|
|
5803
|
+
return {
|
|
5804
|
+
...publicLane(summary),
|
|
5805
|
+
current: name === current,
|
|
5806
|
+
};
|
|
5807
|
+
});
|
|
5808
|
+
return { current, missingCurrentLane: missing, lanes, total: records.length };
|
|
5809
|
+
}
|
|
5810
|
+
|
|
5811
|
+
function printLaneSnapshot(snapshot) {
|
|
5812
|
+
warnMissingCurrentLane(snapshot.missingCurrentLane);
|
|
5813
|
+
const lines = snapshot.lanes.map((lane) => {
|
|
5814
|
+
const mark = lane.current ? '*' : ' ';
|
|
5815
|
+
const desc = lane.description ? ` — ${lane.description}` : '';
|
|
5816
|
+
const inferred = lane.inferred ? ' (inferred)' : '';
|
|
5817
|
+
return `${mark} ${lane.name} ${lane.visible} visible / ${lane.count} in lane${desc}${inferred}`;
|
|
5818
|
+
});
|
|
5819
|
+
printLine(`current lane: ${snapshot.current}`);
|
|
5820
|
+
printLine(lines.join('\n'));
|
|
5821
|
+
return snapshot;
|
|
5822
|
+
}
|
|
5823
|
+
|
|
5824
|
+
function showLanes(argv, { readOnly = false } = {}) {
|
|
5825
|
+
const { positionals } = parseArgs(argv, {}, 'lane show');
|
|
5826
|
+
const view = loadOutcomeView({ repairTail: true, readOnly });
|
|
5827
|
+
const snapshot = listLaneSnapshot(view.records);
|
|
5828
|
+
if (positionals.length === 0) return printLaneSnapshot(snapshot);
|
|
5829
|
+
if (positionals.length !== 1) fail(usageFor('lane show'));
|
|
5830
|
+
const name = normalizeLaneName(positionals[0]);
|
|
5831
|
+
const lane = snapshot.lanes.find((item) => item.name === name);
|
|
5832
|
+
if (!lane) fail(`unknown lane ${name}`);
|
|
5833
|
+
printLine(
|
|
5834
|
+
`${lane.current ? 'current ' : ''}lane: ${lane.name} (${lane.visible} visible / ${lane.count} in lane)`
|
|
5835
|
+
);
|
|
5836
|
+
if (lane.description) printLine(lane.description);
|
|
5837
|
+
return lane;
|
|
5838
|
+
}
|
|
5839
|
+
|
|
5840
|
+
function addLane(argv) {
|
|
5841
|
+
const { positionals, flags } = parseArgs(argv, { desc: 'single' }, 'lane add');
|
|
5842
|
+
if (positionals.length !== 1) fail(usageFor('lane add'));
|
|
5843
|
+
const name = normalizeLaneName(positionals[0]);
|
|
5844
|
+
if (name === DEFAULT_LANE) fail(`lane ${DEFAULT_LANE} always exists`);
|
|
5845
|
+
const events = readEvents({ repairTail: true });
|
|
5846
|
+
const records = fold(events);
|
|
5847
|
+
const catalog = records.lanes || emptyLaneCatalog();
|
|
5848
|
+
const existing = catalog.get(name);
|
|
5849
|
+
if (existing && !existing.inferred) fail(`lane ${name} already exists`);
|
|
5850
|
+
const description = flags.desc && flags.desc.trim() ? flags.desc.trim() : null;
|
|
5851
|
+
appendEvent({
|
|
5852
|
+
type: 'lane_add',
|
|
5853
|
+
id: laneEventId(name),
|
|
5854
|
+
ts: new Date().toISOString(),
|
|
5855
|
+
lane: name,
|
|
5856
|
+
description,
|
|
5857
|
+
});
|
|
5858
|
+
printLine(`added lane ${name}`);
|
|
5859
|
+
return { name, description };
|
|
5860
|
+
}
|
|
5861
|
+
|
|
5862
|
+
function switchLane(argv) {
|
|
5863
|
+
const { positionals } = parseArgs(argv, {}, 'lane switch');
|
|
5864
|
+
if (positionals.length !== 1) fail(usageFor('lane switch'));
|
|
5865
|
+
const name = normalizeLaneName(positionals[0]);
|
|
5866
|
+
const events = readEvents({ repairTail: true });
|
|
5867
|
+
const records = fold(events);
|
|
5868
|
+
const catalog = records.lanes || emptyLaneCatalog();
|
|
5869
|
+
if (!catalog.has(name)) fail(`unknown lane ${name}; add it with driftseal lane add ${name}`);
|
|
5870
|
+
const open = openOutcome(records);
|
|
5871
|
+
if (open) {
|
|
5872
|
+
fail(
|
|
5873
|
+
`outcome ${open.id} is still in_progress on lane ${open.lane || DEFAULT_LANE}; ` +
|
|
5874
|
+
'end it before switching lanes'
|
|
5875
|
+
);
|
|
5876
|
+
}
|
|
5877
|
+
writeCurrentLaneName(name);
|
|
5878
|
+
printLine(`switched to lane ${name}`);
|
|
5879
|
+
return { current: name };
|
|
5880
|
+
}
|
|
5881
|
+
|
|
5882
|
+
function assignLane(argv) {
|
|
5883
|
+
const { positionals } = parseArgs(argv, {}, 'lane assign');
|
|
5884
|
+
if (positionals.length !== 2) fail(usageFor('lane assign'));
|
|
5885
|
+
const id = positionals[0];
|
|
5886
|
+
const name = normalizeLaneName(positionals[1]);
|
|
5887
|
+
const events = readEvents({ repairTail: true });
|
|
5888
|
+
const records = fold(events);
|
|
5889
|
+
const record = records.find((candidate) => candidate.id === id);
|
|
5890
|
+
if (!record) fail(`unknown outcome id: ${id}`);
|
|
5891
|
+
if (record.status === 'in_progress') fail(`cannot assign lane of in_progress outcome ${id}`);
|
|
5892
|
+
const catalog = records.lanes || emptyLaneCatalog();
|
|
5893
|
+
if (!catalog.has(name)) fail(`unknown lane ${name}; add it with driftseal lane add ${name}`);
|
|
5894
|
+
if ((record.lane || DEFAULT_LANE) === name) {
|
|
5895
|
+
printLine(`${id} already on lane ${name}`);
|
|
5896
|
+
return publicOutcome(record);
|
|
5897
|
+
}
|
|
5898
|
+
appendEvent({
|
|
5899
|
+
type: 'lane_assign',
|
|
5900
|
+
id,
|
|
5901
|
+
ts: new Date().toISOString(),
|
|
5902
|
+
lane: name,
|
|
5903
|
+
});
|
|
5904
|
+
printLine(`${id} assigned to lane ${name}`);
|
|
5905
|
+
return { ...publicOutcome(record), lane: name };
|
|
5906
|
+
}
|
|
5907
|
+
|
|
5273
5908
|
const commands = {
|
|
5274
5909
|
begin(argv) {
|
|
5275
5910
|
const { positionals, flags } = parseArgs(argv, {
|
|
@@ -5324,6 +5959,7 @@ const commands = {
|
|
|
5324
5959
|
}
|
|
5325
5960
|
|
|
5326
5961
|
const id = nextId(events);
|
|
5962
|
+
const currentLane = currentLaneOrFail(records);
|
|
5327
5963
|
events.push(appendEvent({
|
|
5328
5964
|
type: 'begin',
|
|
5329
5965
|
id,
|
|
@@ -5332,6 +5968,7 @@ const commands = {
|
|
|
5332
5968
|
acceptance,
|
|
5333
5969
|
verify: flags.verify || null,
|
|
5334
5970
|
decisions,
|
|
5971
|
+
...(currentLane !== DEFAULT_LANE ? { lane: currentLane } : {}),
|
|
5335
5972
|
head: gitCapture(['rev-parse', 'HEAD']),
|
|
5336
5973
|
}));
|
|
5337
5974
|
const record = fold(events).find((candidate) => candidate.id === id);
|
|
@@ -5513,34 +6150,101 @@ const commands = {
|
|
|
5513
6150
|
printLine('parked v1 intent; close it with: driftseal end --status abandoned --note "close parked v1 intent before migration"');
|
|
5514
6151
|
return publicOutcome(parkedV1);
|
|
5515
6152
|
}
|
|
5516
|
-
const
|
|
6153
|
+
const view = loadOutcomeView({ repairTail: true, readOnly });
|
|
6154
|
+
const records = view.records;
|
|
6155
|
+
const { current, missing } = resolveCurrentLane(records);
|
|
6156
|
+
warnMissingCurrentLane(missing);
|
|
6157
|
+
const customLanes = records.lanes && records.lanes.size > 1;
|
|
6158
|
+
if (customLanes || current !== DEFAULT_LANE) {
|
|
6159
|
+
printLine(renderLaneLine(records, current));
|
|
6160
|
+
}
|
|
6161
|
+
const open = openOutcome(records);
|
|
5517
6162
|
if (!open) {
|
|
5518
6163
|
printLine('no outcome in progress');
|
|
5519
6164
|
return null;
|
|
5520
6165
|
}
|
|
5521
|
-
printLine(render(open));
|
|
6166
|
+
printLine(render(open, { currentLane: current }));
|
|
5522
6167
|
return publicOutcome(open);
|
|
5523
6168
|
},
|
|
5524
6169
|
|
|
5525
6170
|
log(argv, { readOnly = false } = {}) {
|
|
5526
|
-
const { positionals, flags } = parseArgs(argv, {
|
|
6171
|
+
const { positionals, flags } = parseArgs(argv, {
|
|
6172
|
+
last: '-n',
|
|
6173
|
+
all: 'boolean',
|
|
6174
|
+
'all-lanes': 'boolean',
|
|
6175
|
+
}, 'log');
|
|
5527
6176
|
if (positionals.length > 0) fail(usageFor('log'));
|
|
5528
|
-
|
|
6177
|
+
const n = flags.last === undefined ? null : positiveInteger(flags.last, '--last');
|
|
6178
|
+
const allLanes = flags['all-lanes'] === true;
|
|
5529
6179
|
const parkedV1 = legacyParkedIntent();
|
|
6180
|
+
if (
|
|
6181
|
+
n !== null &&
|
|
6182
|
+
!allLanes &&
|
|
6183
|
+
!parkedV1 &&
|
|
6184
|
+
process.env._DRIFTSEAL_TEST_DISABLE_RECENT_INDEX !== '1'
|
|
6185
|
+
) {
|
|
6186
|
+
const recent = tryLoadRecentOutcomeView(n, {
|
|
6187
|
+
includeReclaimed: flags.all === true,
|
|
6188
|
+
repairTail: true,
|
|
6189
|
+
readOnly,
|
|
6190
|
+
});
|
|
6191
|
+
if (recent) {
|
|
6192
|
+
const { state, records, current, missing } = recent;
|
|
6193
|
+
warnMissingCurrentLane(missing);
|
|
6194
|
+
const customLanes = state.lanes.size > 1;
|
|
6195
|
+
if (customLanes || current !== DEFAULT_LANE || missing) {
|
|
6196
|
+
const summary = indexedLaneSummary(state, current);
|
|
6197
|
+
printLine(`lane: ${current} (${summary.visible} visible / ${summary.count} in lane)`);
|
|
6198
|
+
}
|
|
6199
|
+
if (records.length === 0) {
|
|
6200
|
+
printLine('log is empty');
|
|
6201
|
+
return [];
|
|
6202
|
+
}
|
|
6203
|
+
printLine(records.map((record) => render(record, { currentLane: current })).join('\n\n'));
|
|
6204
|
+
return records.map(publicOutcome);
|
|
6205
|
+
}
|
|
6206
|
+
}
|
|
6207
|
+
if (process.env._DRIFTSEAL_TEST_REQUIRE_RECENT_INDEX === '1' && n !== null && !allLanes) {
|
|
6208
|
+
fail('recent lane index fast path was not used');
|
|
6209
|
+
}
|
|
6210
|
+
const view = loadOutcomeView({ repairTail: true, readOnly });
|
|
6211
|
+
let records = view.records;
|
|
5530
6212
|
if (parkedV1 && !records.some((record) => record.id === parkedV1.id && record.status === 'in_progress')) {
|
|
5531
|
-
records = [...records, parkedV1];
|
|
6213
|
+
records = Object.assign([...records, parkedV1], { lanes: records.lanes });
|
|
5532
6214
|
}
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
6215
|
+
const catalog = records.lanes || emptyLaneCatalog();
|
|
6216
|
+
const { current, missing } = resolveCurrentLane(view.records);
|
|
6217
|
+
warnMissingCurrentLane(missing);
|
|
6218
|
+
if (!allLanes) {
|
|
6219
|
+
records = Object.assign(selectLaneLogRecords(records, current), { lanes: catalog });
|
|
5537
6220
|
}
|
|
5538
|
-
|
|
6221
|
+
const visible = flags.all ? records : records.filter((record) => !record.reclaimed);
|
|
6222
|
+
const customLanes = catalog.size > 1;
|
|
6223
|
+
if (!allLanes && (customLanes || current !== DEFAULT_LANE || missing)) {
|
|
6224
|
+
printLine(renderLaneLine(view.records, current));
|
|
6225
|
+
}
|
|
6226
|
+
let shown = visible;
|
|
6227
|
+
if (n !== null) {
|
|
6228
|
+
shown = selectLastLogRecords(visible, current, n);
|
|
6229
|
+
}
|
|
6230
|
+
if (shown.length === 0) {
|
|
5539
6231
|
printLine('log is empty');
|
|
5540
6232
|
return [];
|
|
5541
6233
|
}
|
|
5542
|
-
printLine(
|
|
5543
|
-
return
|
|
6234
|
+
printLine(shown.map((record) => render(record, { currentLane: current })).join('\n\n'));
|
|
6235
|
+
return shown.map(publicOutcome);
|
|
6236
|
+
},
|
|
6237
|
+
|
|
6238
|
+
lane(argv, { readOnly = false } = {}) {
|
|
6239
|
+
const [subcommand, ...rest] = argv;
|
|
6240
|
+
if (subcommand === '--help' || subcommand === '-h') throw new HelpRequested('lane');
|
|
6241
|
+
if (!subcommand || subcommand === 'show') {
|
|
6242
|
+
return showLanes(rest, { readOnly });
|
|
6243
|
+
}
|
|
6244
|
+
if (subcommand === 'add') return addLane(rest);
|
|
6245
|
+
if (subcommand === 'switch') return switchLane(rest);
|
|
6246
|
+
if (subcommand === 'assign') return assignLane(rest);
|
|
6247
|
+
fail(usageFor('lane'));
|
|
5544
6248
|
},
|
|
5545
6249
|
|
|
5546
6250
|
reclaim(argv) {
|
|
@@ -5906,6 +6610,9 @@ const commands = {
|
|
|
5906
6610
|
knownManagedBlocks: [
|
|
5907
6611
|
...sourceLanguages.flatMap((source) => [
|
|
5908
6612
|
protocolEol(intentProtocolBlock(PROTOCOL_VERSION, source), eol),
|
|
6613
|
+
protocolEol(intentProtocolBlock(PROTOCOL_VERSION, source, true), eol),
|
|
6614
|
+
protocolEol(intentProtocolBlockV20(source), eol),
|
|
6615
|
+
protocolEol(intentProtocolBlockV20(source, true), eol),
|
|
5909
6616
|
protocolEol(v1IntentProtocolBlock(14, source), eol),
|
|
5910
6617
|
protocolEol(v1IntentProtocolBlock(14, source, true), eol),
|
|
5911
6618
|
protocolEol(previousIntentProtocolBlock(13, source), eol),
|
|
@@ -5937,6 +6644,9 @@ const commands = {
|
|
|
5937
6644
|
knownManagedBlocks: [
|
|
5938
6645
|
...sourceLanguages.flatMap((source) => [
|
|
5939
6646
|
protocolEol(decisionProtocolBlock(PROTOCOL_VERSION, source), eol),
|
|
6647
|
+
protocolEol(decisionProtocolBlock(PROTOCOL_VERSION, source, true), eol),
|
|
6648
|
+
protocolEol(decisionProtocolBlock('2.0', source), eol),
|
|
6649
|
+
protocolEol(decisionProtocolBlock('2.0', source, true), eol),
|
|
5940
6650
|
protocolEol(v1DecisionProtocolBlock(14, source), eol),
|
|
5941
6651
|
protocolEol(v1DecisionProtocolBlock(14, source, true), eol),
|
|
5942
6652
|
protocolEol(previousDecisionProtocolBlock(13, source), eol),
|
|
@@ -6016,7 +6726,13 @@ usage:
|
|
|
6016
6726
|
to the current contract and Git-visible workspace
|
|
6017
6727
|
driftseal end [id] [--status completed|partial|failed|abandoned] [--note "..."] [--verify-result "..."]
|
|
6018
6728
|
driftseal status show the outcome currently in progress
|
|
6019
|
-
driftseal log [--last N] [--all]
|
|
6729
|
+
driftseal log [--last N] [--all] [--all-lanes]
|
|
6730
|
+
show outcome history (current lane; --all-lanes is global)
|
|
6731
|
+
driftseal lane show named outcome lanes and the current lane
|
|
6732
|
+
driftseal lane add <name> [--desc "..."]
|
|
6733
|
+
driftseal lane switch <name>
|
|
6734
|
+
driftseal lane assign <id> <name>
|
|
6735
|
+
partition outcome history by long-lived capability
|
|
6020
6736
|
driftseal reclaim [id ...] --reason "<why>" [--older-than <days>] [--force] [--dry-run]
|
|
6021
6737
|
hide meaningless closed records without deleting them
|
|
6022
6738
|
driftseal unreclaim <id> --reason "<why>"
|
|
@@ -6096,6 +6812,8 @@ const VALUE_TAKING_FLAGS = {
|
|
|
6096
6812
|
extend: ['--accept', '--verify', '-v', '--decision'],
|
|
6097
6813
|
end: ['--status', '-s', '--note', '-n', '--verify-result', '-r'],
|
|
6098
6814
|
log: ['--last', '-n'],
|
|
6815
|
+
lane: ['--desc'],
|
|
6816
|
+
'lane add': ['--desc'],
|
|
6099
6817
|
reclaim: ['--reason', '-r', '--older-than'],
|
|
6100
6818
|
unreclaim: ['--reason', '-r'],
|
|
6101
6819
|
absorb: ['--decisions'],
|
|
@@ -6133,6 +6851,7 @@ function mutationResources(cmd, argv) {
|
|
|
6133
6851
|
if (cmd === 'init') return [process.cwd()];
|
|
6134
6852
|
if (cmd === 'migrate') return [process.cwd()];
|
|
6135
6853
|
if (cmd === 'reclaim' || cmd === 'unreclaim') return [logDir()];
|
|
6854
|
+
if (cmd === 'lane') return [logDir()];
|
|
6136
6855
|
if (cmd === 'absorb' && argv[0] === '--git') {
|
|
6137
6856
|
const ours = argv[2];
|
|
6138
6857
|
return ours ? [path.dirname(path.resolve(ours))] : [process.cwd()];
|
|
@@ -6152,7 +6871,7 @@ function mutationResources(cmd, argv) {
|
|
|
6152
6871
|
|
|
6153
6872
|
function usesV2RepositoryState(cmd, rest) {
|
|
6154
6873
|
if (
|
|
6155
|
-
['begin', 'extend', 'verify', 'end', 'status', 'log', 'reclaim', 'unreclaim', 'absorb', 'decision', 'init'].includes(cmd)
|
|
6874
|
+
['begin', 'extend', 'verify', 'end', 'status', 'log', 'lane', 'reclaim', 'unreclaim', 'absorb', 'decision', 'init'].includes(cmd)
|
|
6156
6875
|
) {
|
|
6157
6876
|
return true;
|
|
6158
6877
|
}
|
|
@@ -6226,9 +6945,52 @@ function repositoryOutcomeLogFiles() {
|
|
|
6226
6945
|
return files;
|
|
6227
6946
|
}
|
|
6228
6947
|
|
|
6948
|
+
function indexedMigrationEvent(file) {
|
|
6949
|
+
if (
|
|
6950
|
+
canonicalPath(file) !== canonicalPath(logFile()) ||
|
|
6951
|
+
!laneIndexFile() ||
|
|
6952
|
+
!fs.existsSync(laneIndexFile())
|
|
6953
|
+
) {
|
|
6954
|
+
return { usable: false, migration: null };
|
|
6955
|
+
}
|
|
6956
|
+
let index;
|
|
6957
|
+
try {
|
|
6958
|
+
index = openOutcomeIndex(laneIndexFile(), { readOnly: true });
|
|
6959
|
+
const source = index.source();
|
|
6960
|
+
if (!laneIndexMatchesFile(source, file)) {
|
|
6961
|
+
return { usable: false, migration: null };
|
|
6962
|
+
}
|
|
6963
|
+
let migration = index.migrationEvent();
|
|
6964
|
+
const size = fs.existsSync(file) ? fs.statSync(file).size : 0;
|
|
6965
|
+
if (size > source.indexedThrough) {
|
|
6966
|
+
consumeLogSlice(
|
|
6967
|
+
file,
|
|
6968
|
+
source.indexedThrough,
|
|
6969
|
+
(event) => {
|
|
6970
|
+
if (event.type === 'migration' && event.id === 'v1-to-v2') migration = event;
|
|
6971
|
+
},
|
|
6972
|
+
{
|
|
6973
|
+
readOnly: true,
|
|
6974
|
+
startLine: source.indexedLines || 0,
|
|
6975
|
+
}
|
|
6976
|
+
);
|
|
6977
|
+
}
|
|
6978
|
+
return { usable: true, migration };
|
|
6979
|
+
} catch {
|
|
6980
|
+
return { usable: false, migration: null };
|
|
6981
|
+
} finally {
|
|
6982
|
+
if (index) index.close();
|
|
6983
|
+
}
|
|
6984
|
+
}
|
|
6985
|
+
|
|
6229
6986
|
function repositoryMigrationEvent() {
|
|
6230
6987
|
for (const file of repositoryOutcomeLogFiles()) {
|
|
6231
6988
|
try {
|
|
6989
|
+
const indexed = indexedMigrationEvent(file);
|
|
6990
|
+
if (indexed.usable) {
|
|
6991
|
+
if (indexed.migration) return indexed.migration;
|
|
6992
|
+
continue;
|
|
6993
|
+
}
|
|
6232
6994
|
const migration = findMigrationEvent(file);
|
|
6233
6995
|
if (migration) return migration;
|
|
6234
6996
|
} catch {
|
|
@@ -6421,11 +7183,14 @@ function dispatch(argv) {
|
|
|
6421
7183
|
assertV2RepositoryReady(cmd, rest);
|
|
6422
7184
|
const mutates =
|
|
6423
7185
|
['begin', 'extend', 'end', 'init', 'skill', 'mcp', 'reclaim', 'unreclaim', 'absorb'].includes(cmd) ||
|
|
7186
|
+
(cmd === 'lane' && ['add', 'switch', 'assign'].includes(rest[0])) ||
|
|
6424
7187
|
(cmd === 'migrate' && rest[1] === 'apply') ||
|
|
6425
7188
|
(cmd === 'hook' && rest[0] === 'install') ||
|
|
6426
7189
|
(cmd === 'decision' && ['add', 'update'].includes(rest[0]));
|
|
6427
7190
|
const readsIntentLog =
|
|
6428
|
-
['status', 'log'].includes(cmd) ||
|
|
7191
|
+
['status', 'log'].includes(cmd) ||
|
|
7192
|
+
(cmd === 'lane' && (!rest[0] || rest[0] === 'show')) ||
|
|
7193
|
+
(cmd === 'hook' && ['prompt', 'stop'].includes(rest[0]));
|
|
6429
7194
|
if (mutates || readsIntentLog) {
|
|
6430
7195
|
if (readsIntentLog) {
|
|
6431
7196
|
let resources;
|
|
@@ -6493,6 +7258,7 @@ function repositoryRoot(root) {
|
|
|
6493
7258
|
}
|
|
6494
7259
|
|
|
6495
7260
|
function runCommand(argv, { root = process.cwd(), isolateStorage = false, capture = true } = {}) {
|
|
7261
|
+
assertSupportedNode();
|
|
6496
7262
|
if (!Array.isArray(argv) || argv.some((arg) => typeof arg !== 'string')) {
|
|
6497
7263
|
fail('command arguments must be an array of strings');
|
|
6498
7264
|
}
|
|
@@ -6553,6 +7319,7 @@ function appendFlag(argv, flag, value) {
|
|
|
6553
7319
|
}
|
|
6554
7320
|
|
|
6555
7321
|
function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
|
|
7322
|
+
assertSupportedNode();
|
|
6556
7323
|
const fixedRoot = repositoryRoot(root);
|
|
6557
7324
|
let lastReadOnly = false;
|
|
6558
7325
|
const call = (argv) => {
|
|
@@ -6595,12 +7362,27 @@ function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
|
|
|
6595
7362
|
appendFlag(argv, '--verify-result', verifyResult);
|
|
6596
7363
|
return call(argv);
|
|
6597
7364
|
},
|
|
6598
|
-
log({ last, all = false } = {}) {
|
|
7365
|
+
log({ last, all = false, allLanes = false } = {}) {
|
|
6599
7366
|
const argv = ['log'];
|
|
6600
7367
|
appendFlag(argv, '--last', last);
|
|
6601
7368
|
if (all) argv.push('--all');
|
|
7369
|
+
if (allLanes) argv.push('--all-lanes');
|
|
6602
7370
|
return call(argv);
|
|
6603
7371
|
},
|
|
7372
|
+
lane() {
|
|
7373
|
+
return call(['lane']);
|
|
7374
|
+
},
|
|
7375
|
+
laneAdd({ name, description } = {}) {
|
|
7376
|
+
const argv = ['lane', 'add', String(name)];
|
|
7377
|
+
appendFlag(argv, '--desc', description);
|
|
7378
|
+
return call(argv);
|
|
7379
|
+
},
|
|
7380
|
+
laneSwitch({ name } = {}) {
|
|
7381
|
+
return call(['lane', 'switch', String(name)]);
|
|
7382
|
+
},
|
|
7383
|
+
laneAssign({ id, lane } = {}) {
|
|
7384
|
+
return call(['lane', 'assign', String(id), String(lane)]);
|
|
7385
|
+
},
|
|
6604
7386
|
absorb({ otherLog, otherDecisions, abandon, dryRun = false } = {}) {
|
|
6605
7387
|
if (abandon && !['ours', 'theirs'].includes(abandon)) {
|
|
6606
7388
|
fail('absorb abandon must be "ours" or "theirs"');
|
|
@@ -6674,6 +7456,7 @@ function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
|
|
|
6674
7456
|
|
|
6675
7457
|
function main() {
|
|
6676
7458
|
try {
|
|
7459
|
+
assertSupportedNode();
|
|
6677
7460
|
const result = dispatch(process.argv.slice(2));
|
|
6678
7461
|
process.exitCode = result.exitCode;
|
|
6679
7462
|
} catch (err) {
|